diff --git a/internal/tender/entity.go b/internal/tender/entity.go index b4e891b..2b712e7 100644 --- a/internal/tender/entity.go +++ b/internal/tender/entity.go @@ -82,8 +82,10 @@ type Tender struct { SourceFileName string `bson:"source_file_name" json:"source_file_name"` ContentXML string `bson:"content_xml,omitempty" json:"content_xml,omitempty"` ProcessingMetadata ProcessingMetadata `bson:"processing_metadata" json:"processing_metadata"` - TenderID string `bson:"tender_id" json:"tender_id"` // PBL Project Number (SCDYYNNN format) - ProjectName string `bson:"project_name" json:"project_name"` // PBL Project Name (__) + // AIOverallSummary optional denormalized overall summary for search/list (e.g. synced from AI pipeline storage). + AIOverallSummary string `bson:"ai_overall_summary,omitempty" json:"ai_overall_summary,omitempty"` + TenderID string `bson:"tender_id" json:"tender_id"` // PBL Project Number (SCDYYNNN format) + ProjectName string `bson:"project_name" json:"project_name"` // PBL Project Name (__) // Status-specific fields CancellationReason string `bson:"cancellation_reason,omitempty" json:"cancellation_reason,omitempty"` diff --git a/internal/tender/form.go b/internal/tender/form.go index 0a5e7b6..f3814cf 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -15,27 +15,47 @@ type UpdateTenderRequest struct { // SearchForm represents a request to list tenders with pagination and filtering type SearchForm struct { - Search *string `query:"q" valid:"optional"` - NoticeType *string `query:"notice_type" valid:"optional"` - ProcurementType *string `query:"procurement_type" valid:"optional"` - CountryCodes []string `query:"country_codes" valid:"optional"` - RegionCodes []string `query:"region_codes" valid:"optional"` - CompanyID *string `query:"company_id" valid:"optional"` - CustomerID *string `query:"customer_id" valid:"optional"` - Status []string `query:"status" valid:"optional"` - Classifications []string `query:"classifications" valid:"optional"` - MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"` - MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"` - Currency string `query:"currency" valid:"optional"` - DeadlineFrom *int64 `query:"deadline_from" valid:"optional"` - DeadlineTo *int64 `query:"deadline_to" valid:"optional"` - PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"` - PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"` - Source []string `query:"source" valid:"optional"` - BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"` - Languages []string `query:"languages" valid:"optional"` - OnlyActiveDeadlines bool `query:"-" valid:"optional"` - Language *string `query:"lang" valid:"optional"` + Search *string `query:"q" valid:"optional"` + Title *string `query:"title" valid:"optional"` + Description *string `query:"description" valid:"optional"` + NoticeType *string `query:"notice_type" valid:"optional"` + NoticeTypes []string `query:"notice_types" valid:"optional"` + FormTypes []string `query:"form_types" valid:"optional"` + ProcurementType *string `query:"procurement_type" valid:"optional"` + Country *string `query:"country" valid:"optional"` + CountryCode *string `query:"country_code" valid:"optional"` + CountryCodes []string `query:"country_codes" valid:"optional"` + RegionCodes []string `query:"region_codes" valid:"optional"` + CompanyID *string `query:"company_id" valid:"optional"` + CustomerID *string `query:"customer_id" valid:"optional"` + Status []string `query:"status" valid:"optional"` + MainClassification *string `query:"main_classification" valid:"optional"` + Classifications []string `query:"classifications" valid:"optional"` + CpvCodes []string `query:"cpv_codes" valid:"optional"` + MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"` + MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"` + Currency string `query:"currency" valid:"optional"` + CreatedAt *int64 `query:"created_at" valid:"optional"` + CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"` + CreatedAtTo *int64 `query:"created_at_to" valid:"optional"` + TenderDeadline *int64 `query:"tender_deadline" valid:"optional"` + TenderDeadlineFrom *int64 `query:"tender_deadline_from" valid:"optional"` + TenderDeadlineTo *int64 `query:"tender_deadline_to" valid:"optional"` + DeadlineFrom *int64 `query:"deadline_from" valid:"optional"` + DeadlineTo *int64 `query:"deadline_to" valid:"optional"` + PublicationDate *int64 `query:"publication_date" valid:"optional"` + PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"` + PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"` + SubmissionDeadline *int64 `query:"submission_deadline" valid:"optional"` + SubmissionDateAliasFrom *int64 `query:"submission_deadline_from" valid:"optional"` + SubmissionDateAliasTo *int64 `query:"submission_deadline_to" valid:"optional"` + SubmissionDateFrom *int64 `query:"submission_date_from" valid:"optional"` + SubmissionDateTo *int64 `query:"submission_date_to" valid:"optional"` + Source []string `query:"source" valid:"optional"` + BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"` + Languages []string `query:"languages" valid:"optional"` + OnlyActiveDeadlines bool `query:"-" valid:"optional"` + Language *string `query:"lang" valid:"optional"` } // SearchResponse represents the response for listing tenders @@ -48,10 +68,12 @@ type SearchResponse struct { type TenderResponse struct { ID string `json:"id"` NoticePublicationID string `json:"notice_publication_id"` + NoticeTypeCode string `json:"notice_type_code"` Title string `json:"title"` Description string `json:"description"` ProcurementTypeCode string `json:"procurement_type_code"` ProcedureCode string `json:"procedure_code"` + MainClassification string `json:"main_classification"` EstimatedValue float64 `json:"estimated_value"` Currency string `json:"currency"` Duration string `json:"duration"` @@ -115,10 +137,12 @@ func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse { response := &TenderResponse{ ID: t.ID.Hex(), NoticePublicationID: t.NoticePublicationID, + NoticeTypeCode: t.NoticeTypeCode, Title: title, Description: description, ProcurementTypeCode: t.ProcurementTypeCode, ProcedureCode: t.ProcedureCode, + MainClassification: t.MainClassification, EstimatedValue: t.EstimatedValue, Currency: t.Currency, Duration: t.Duration, diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 15f1aaa..717e71f 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -131,19 +131,28 @@ func (h *TenderHandler) Delete(c echo.Context) error { // @Tags Admin-Tenders // @Produce json // @Param company_id query string false "Company ID for match percentage calculation and sorting" -// @Param q query string false "Search query for title and description" -// @Param notice_type query string false "Filter by notice type code" +// @Param q query string false "Keyword search across title, description, per-document AI summaries (document_summaries), and optional stored overall summary (ai_overall_summary)" +// @Param notice_type query string false "Filter by TED notice type code (BT-02, notice_type_code)" +// @Param notice_types query []string false "Filter by multiple notice type codes (notice_type_code)" +// @Param form_types query []string false "Filter by TED form type / listName (e.g. competition, result, planning, contract-award, prior-information)" // @Param procurement_type query string false "Filter by procurement type code" -// @Param country_codes query []string false "Filter by country codes (comma-separated)" +// @Param country query string false "Filter by a single ISO country code" +// @Param country_codes query []string false "Filter by country codes (repeat param or comma-separated)" // @Param status query []string false "Filter by status (comma-separated)" // @Param source query []string false "Filter by source (comma-separated)" +// @Param classifications query []string false "Filter by CPV / classification codes (main, additional, or procurement lot CPVs)" +// @Param cpv_codes query []string false "Alias for classifications: one or more CPV codes" // @Param min_estimated_value query number false "Minimum estimated value" // @Param max_estimated_value query number false "Maximum estimated value" // @Param currency query string false "Filter by currency" -// @Param deadline_from query number false "Deadline from" -// @Param deadline_to query number false "Deadline to" -// @Param publication_date_from query number false "Publication date from" -// @Param publication_date_to query number false "Publication date to" +// @Param created_at_from query number false "Created at lower bound (Unix timestamp, seconds)" +// @Param created_at_to query number false "Created at upper bound (Unix timestamp, seconds)" +// @Param deadline_from query number false "Tender deadline from (Unix timestamp, seconds)" +// @Param deadline_to query number false "Tender deadline to (Unix timestamp, seconds)" +// @Param publication_date_from query number false "Publication date from (Unix timestamp, seconds)" +// @Param publication_date_to query number false "Publication date to (Unix timestamp, seconds)" +// @Param submission_date_from query number false "Submission deadline from (Unix timestamp, seconds)" +// @Param submission_date_to query number false "Submission deadline to (Unix timestamp, seconds)" // @Param languages query []string false "Filter by languages (comma-separated)" // @Param buyer_organization_id query string false "Filter by buyer organization ID" // @Param limit query int false "Number of items per page (default: 20, max: 100)" @@ -184,19 +193,28 @@ func (h *TenderHandler) Search(c echo.Context) error { // @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile // @Tags Tenders // @Produce json -// @Param q query string false "Search query for title and description" -// @Param notice_type query string false "Filter by notice type code" +// @Param q query string false "Keyword search across title, description, AI document summaries, optional stored overall summary" +// @Param notice_type query string false "Filter by TED notice type code (notice_type_code)" +// @Param notice_types query []string false "Multiple notice type codes" +// @Param form_types query []string false "TED form types: competition, result, planning, contract-award, prior-information, etc." // @Param procurement_type query string false "Filter by procurement type code" -// @Param country_codes query []string false "Filter by country codes (comma-separated)" +// @Param country query string false "Single ISO country code" +// @Param country_codes query []string false "Filter by country codes" // @Param status query []string false "Filter by status (comma-separated)" // @Param source query []string false "Filter by source (comma-separated)" +// @Param classifications query []string false "CPV / classification codes" +// @Param cpv_codes query []string false "One or more CPV codes (same as classifications)" // @Param min_estimated_value query number false "Minimum estimated value" // @Param max_estimated_value query number false "Maximum estimated value" // @Param currency query string false "Filter by currency" -// @Param deadline_from query number false "Deadline from" -// @Param deadline_to query number false "Deadline to" -// @Param publication_date_from query number false "Publication date from" -// @Param publication_date_to query number false "Publication date to" +// @Param created_at_from query number false "Created at from (Unix seconds)" +// @Param created_at_to query number false "Created at to (Unix seconds)" +// @Param deadline_from query number false "Tender deadline from (Unix seconds)" +// @Param deadline_to query number false "Tender deadline to (Unix seconds)" +// @Param publication_date_from query number false "Publication date from (Unix seconds)" +// @Param publication_date_to query number false "Publication date to (Unix seconds)" +// @Param submission_date_from query number false "Submission deadline from (Unix seconds)" +// @Param submission_date_to query number false "Submission deadline to (Unix seconds)" // @Param languages query []string false "Filter by languages (comma-separated)" // @Param buyer_organization_id query string false "Filter by buyer organization ID" // @Param limit query int false "Number of items per page (default: 20, max: 100)" @@ -248,19 +266,28 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile // @Tags Tenders // @Produce json -// @Param q query string false "Search query for title and description" -// @Param notice_type query string false "Filter by notice type code" +// @Param q query string false "Keyword search across title, description, AI document summaries, optional stored overall summary" +// @Param notice_type query string false "Filter by TED notice type code (notice_type_code)" +// @Param notice_types query []string false "Multiple notice type codes" +// @Param form_types query []string false "TED form types: competition, result, planning, contract-award, prior-information, etc." // @Param procurement_type query string false "Filter by procurement type code" -// @Param country_codes query []string false "Filter by country codes (comma-separated)" +// @Param country query string false "Single ISO country code" +// @Param country_codes query []string false "Filter by country codes" // @Param status query []string false "Filter by status (comma-separated)" // @Param source query []string false "Filter by source (comma-separated)" +// @Param classifications query []string false "CPV / classification codes" +// @Param cpv_codes query []string false "One or more CPV codes" // @Param min_estimated_value query number false "Minimum estimated value" // @Param max_estimated_value query number false "Maximum estimated value" // @Param currency query string false "Filter by currency" -// @Param deadline_from query number false "Deadline from" -// @Param deadline_to query number false "Deadline to" -// @Param publication_date_from query number false "Publication date from" -// @Param publication_date_to query number false "Publication date to" +// @Param created_at_from query number false "Created at from (Unix seconds)" +// @Param created_at_to query number false "Created at to (Unix seconds)" +// @Param deadline_from query number false "Tender deadline from (Unix seconds)" +// @Param deadline_to query number false "Tender deadline to (Unix seconds)" +// @Param publication_date_from query number false "Publication date from (Unix seconds)" +// @Param publication_date_to query number false "Publication date to (Unix seconds)" +// @Param submission_date_from query number false "Submission deadline from (Unix seconds)" +// @Param submission_date_to query number false "Submission deadline to (Unix seconds)" // @Param languages query []string false "Filter by languages (comma-separated)" // @Param buyer_organization_id query string false "Filter by buyer organization ID" // @Param limit query int false "Number of items per page (default: 20, max: 100)" diff --git a/internal/tender/repository.go b/internal/tender/repository.go index e1074d9..fc40577 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -2,6 +2,8 @@ package tender import ( "context" + "regexp" + "strings" "time" "tm/pkg/logger" orm "tm/pkg/mongo" @@ -54,6 +56,7 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te *orm.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}), *orm.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}), *orm.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}), + *orm.NewIndex("form_type_idx", bson.D{{Key: "form_type", Value: 1}}), *orm.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}), *orm.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}), *orm.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}), @@ -77,7 +80,7 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te {Key: "additional_classifications", Value: 1}, {Key: "publication_date", Value: -1}, }), - // Text index for search + // Text index (legacy; list search uses $regex across title, description, and AI summary fields for unified matching) *orm.CreateTextIndex("search_idx", "title", "description"), } @@ -549,37 +552,176 @@ func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) ( return counts, nil } +func dedupeNonEmptyStrings(values []string) []string { + seen := make(map[string]struct{}, len(values)) + out := make([]string, 0, len(values)) + for _, v := range values { + v = strings.TrimSpace(v) + if v == "" { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} + +// normalizeUnixTimestamp accepts seconds or milliseconds and always returns seconds. +func normalizeUnixTimestamp(value int64) int64 { + // 1e12 is safely above current Unix seconds and in millisecond range for modern dates. + if value > 1_000_000_000_000 { + return value / 1000 + } + return value +} + +func exactOrRange(exact, from, to *int64) (*int64, *int64) { + if exact != nil { + v := normalizeUnixTimestamp(*exact) + return &v, &v + } + + var normalizedFrom *int64 + if from != nil { + v := normalizeUnixTimestamp(*from) + normalizedFrom = &v + } + + var normalizedTo *int64 + if to != nil { + v := normalizeUnixTimestamp(*to) + normalizedTo = &v + } + + return normalizedFrom, normalizedTo +} + // buildSearchFilter builds MongoDB filter from search form func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M { filter := bson.M{} + var keywordOr []bson.M if form.Search != nil { - filter["$text"] = bson.M{"$search": *form.Search} + q := strings.TrimSpace(*form.Search) + if q != "" { + pattern := regexp.QuoteMeta(q) + keywordOr = []bson.M{ + {"title": bson.M{"$regex": pattern, "$options": "i"}}, + {"description": bson.M{"$regex": pattern, "$options": "i"}}, + {"document_summaries.summary": bson.M{"$regex": pattern, "$options": "i"}}, + {"ai_overall_summary": bson.M{"$regex": pattern, "$options": "i"}}, + } + } + } + if form.Title != nil { + title := strings.TrimSpace(*form.Title) + if title != "" { + filter["title"] = bson.M{ + "$regex": regexp.QuoteMeta(title), + "$options": "i", + } + } + } + if form.Description != nil { + description := strings.TrimSpace(*form.Description) + if description != "" { + filter["description"] = bson.M{ + "$regex": regexp.QuoteMeta(description), + "$options": "i", + } + } } + var noticeCodes []string if form.NoticeType != nil { - filter["notice_type_code"] = form.NoticeType + if t := strings.TrimSpace(*form.NoticeType); t != "" { + noticeCodes = append(noticeCodes, t) + } + } + noticeCodes = append(noticeCodes, form.NoticeTypes...) + noticeCodes = dedupeNonEmptyStrings(noticeCodes) + if len(noticeCodes) == 1 { + filter["notice_type_code"] = noticeCodes[0] + } else if len(noticeCodes) > 1 { + filter["notice_type_code"] = bson.M{"$in": noticeCodes} + } + + formTypes := dedupeNonEmptyStrings(form.FormTypes) + if len(formTypes) == 1 { + filter["form_type"] = formTypes[0] + } else if len(formTypes) > 1 { + filter["form_type"] = bson.M{"$in": formTypes} } if form.ProcurementType != nil { filter["procurement_type_code"] = form.ProcurementType } - if len(form.CountryCodes) > 0 { - filter["country_code"] = bson.M{"$in": form.CountryCodes} + countryCodes := dedupeNonEmptyStrings(form.CountryCodes) + if form.CountryCode != nil { + if c := strings.TrimSpace(*form.CountryCode); c != "" { + countryCodes = append(countryCodes, c) + countryCodes = dedupeNonEmptyStrings(countryCodes) + } + } + if form.Country != nil { + if c := strings.TrimSpace(*form.Country); c != "" { + countryCodes = append(countryCodes, c) + countryCodes = dedupeNonEmptyStrings(countryCodes) + } + } + if len(countryCodes) == 1 { + filter["country_code"] = countryCodes[0] + } else if len(countryCodes) > 1 { + filter["country_code"] = bson.M{"$in": countryCodes} } if len(form.RegionCodes) > 0 { - filter["region_code"] = bson.M{"$in": form.RegionCodes} + filter["region_code"] = bson.M{"$in": dedupeNonEmptyStrings(form.RegionCodes)} } - if len(form.Classifications) > 0 { - filter["$or"] = []bson.M{ - {"main_classification": bson.M{"$in": form.Classifications}}, - {"additional_classifications": bson.M{"$in": form.Classifications}}, + if form.MainClassification != nil { + if mainClassification := strings.TrimSpace(*form.MainClassification); mainClassification != "" { + filter["main_classification"] = mainClassification } } + cpvCodes := dedupeNonEmptyStrings(form.Classifications) + cpvCodes = append(cpvCodes, dedupeNonEmptyStrings(form.CpvCodes)...) + cpvCodes = dedupeNonEmptyStrings(cpvCodes) + var cpvOr []bson.M + if len(cpvCodes) > 0 { + cpvOr = []bson.M{ + {"main_classification": bson.M{"$in": cpvCodes}}, + {"additional_classifications": bson.M{"$in": cpvCodes}}, + { + "procurement_lots": bson.M{ + "$elemMatch": bson.M{ + "$or": []bson.M{ + {"main_classification": bson.M{"$in": cpvCodes}}, + {"additional_classifications": bson.M{"$in": cpvCodes}}, + }, + }, + }, + }, + } + } + + switch { + case len(keywordOr) > 0 && len(cpvOr) > 0: + filter["$and"] = []bson.M{ + {"$or": keywordOr}, + {"$or": cpvOr}, + } + case len(keywordOr) > 0: + filter["$or"] = keywordOr + case len(cpvOr) > 0: + filter["$or"] = cpvOr + } + if form.MinEstimatedValue != nil || form.MaxEstimatedValue != nil { valueFilter := bson.M{} if form.MinEstimatedValue != nil { @@ -595,37 +737,79 @@ func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M { filter["currency"] = form.Currency } - if form.DeadlineFrom != nil || form.DeadlineTo != nil || form.OnlyActiveDeadlines { + createdAtFrom, createdAtTo := exactOrRange(form.CreatedAt, form.CreatedAtFrom, form.CreatedAtTo) + if createdAtFrom != nil || createdAtTo != nil { + createdFilter := bson.M{} + if createdAtFrom != nil { + createdFilter["$gte"] = *createdAtFrom + } + if createdAtTo != nil { + createdFilter["$lte"] = *createdAtTo + } + filter["created_at"] = createdFilter + } + + deadlineFrom := form.DeadlineFrom + if form.TenderDeadlineFrom != nil { + deadlineFrom = form.TenderDeadlineFrom + } + deadlineTo := form.DeadlineTo + if form.TenderDeadlineTo != nil { + deadlineTo = form.TenderDeadlineTo + } + tenderDeadlineFrom, tenderDeadlineTo := exactOrRange(form.TenderDeadline, deadlineFrom, deadlineTo) + if tenderDeadlineFrom != nil || tenderDeadlineTo != nil || form.OnlyActiveDeadlines { deadlineFilter := bson.M{} if form.OnlyActiveDeadlines { deadlineFilter["$gt"] = time.Now().Unix() } - if form.DeadlineFrom != nil { - deadlineFilter["$gte"] = *form.DeadlineFrom + if tenderDeadlineFrom != nil { + deadlineFilter["$gte"] = *tenderDeadlineFrom } - if form.DeadlineTo != nil { - deadlineFilter["$lte"] = *form.DeadlineTo + if tenderDeadlineTo != nil { + deadlineFilter["$lte"] = *tenderDeadlineTo } filter["tender_deadline"] = deadlineFilter } - if form.PublicationDateFrom != nil || form.PublicationDateTo != nil { + publicationDateFrom, publicationDateTo := exactOrRange(form.PublicationDate, form.PublicationDateFrom, form.PublicationDateTo) + if publicationDateFrom != nil || publicationDateTo != nil { pubDateFilter := bson.M{} - if form.PublicationDateFrom != nil { - pubDateFilter["$gte"] = *form.PublicationDateFrom + if publicationDateFrom != nil { + pubDateFilter["$gte"] = *publicationDateFrom } - if form.PublicationDateTo != nil { - pubDateFilter["$lte"] = *form.PublicationDateTo + if publicationDateTo != nil { + pubDateFilter["$lte"] = *publicationDateTo } filter["publication_date"] = pubDateFilter } + submissionDateFrom := form.SubmissionDateFrom + if form.SubmissionDateAliasFrom != nil { + submissionDateFrom = form.SubmissionDateAliasFrom + } + submissionDateTo := form.SubmissionDateTo + if form.SubmissionDateAliasTo != nil { + submissionDateTo = form.SubmissionDateAliasTo + } + submissionDeadlineFrom, submissionDeadlineTo := exactOrRange(form.SubmissionDeadline, submissionDateFrom, submissionDateTo) + if submissionDeadlineFrom != nil || submissionDeadlineTo != nil { + subFilter := bson.M{} + if submissionDeadlineFrom != nil { + subFilter["$gte"] = *submissionDeadlineFrom + } + if submissionDeadlineTo != nil { + subFilter["$lte"] = *submissionDeadlineTo + } + filter["submission_deadline"] = subFilter + } + if len(form.Status) > 0 { - filter["status"] = bson.M{"$in": form.Status} + filter["status"] = bson.M{"$in": dedupeNonEmptyStrings(form.Status)} } if len(form.Source) > 0 { - filter["source"] = bson.M{"$in": form.Source} + filter["source"] = bson.M{"$in": dedupeNonEmptyStrings(form.Source)} } if form.BuyerOrganizationID != nil { @@ -633,7 +817,7 @@ func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M { } if len(form.Languages) > 0 { - filter["notice_language_code"] = bson.M{"$in": form.Languages} + filter["notice_language_code"] = bson.M{"$in": dedupeNonEmptyStrings(form.Languages)} } return filter