diff --git a/internal/tender/recommendation_response.go b/internal/tender/recommendation_response.go index 35e080a..688ea86 100644 --- a/internal/tender/recommendation_response.go +++ b/internal/tender/recommendation_response.go @@ -6,13 +6,9 @@ import ( "strings" "sync" - "golang.org/x/sync/errgroup" - "tm/pkg/ai_summarizer" ) -const recommendationTranslationConcurrency = 8 - type rankedRecommendedTender struct { tender Tender rank int @@ -54,52 +50,21 @@ func (c *recommendationTranslationCache) set(tenderID, language string, stored a c.items[recommendationTranslationCacheKey(tenderID, language)] = stored } -func (s *tenderService) buildRecommendedTenderResponsesParallel( - ctx context.Context, - items []rankedRecommendedTender, - language string, -) []TenderResponse { +// buildRecommendedTenderListResponses builds list responses without MinIO translation lookups. +// Recommendation pages use MongoDB text; tender detail endpoints still enrich from storage. +func (s *tenderService) buildRecommendedTenderListResponses(items []rankedRecommendedTender, language string) []TenderResponse { if len(items) == 0 { return nil } out := make([]TenderResponse, len(items)) - if len(items) == 1 { - out[0] = s.buildRecommendedTenderResponse(ctx, items[0].tender, items[0].rank, items[0].analysis, items[0].tenderRef, language, newRecommendationTranslationCache()) - return out + for i, item := range items { + resp := item.tender.ToResponseWithLanguage(language) + resp.Rank = item.rank + resp.Analysis = item.analysis + resp.ProcedureRef = item.tenderRef + out[i] = *resp } - - translationCache := newRecommendationTranslationCache() - group, groupCtx := errgroup.WithContext(ctx) - group.SetLimit(recommendationTranslationConcurrency) - - for i := range items { - i := i - item := items[i] - group.Go(func() error { - out[i] = s.buildRecommendedTenderResponse( - groupCtx, - item.tender, - item.rank, - item.analysis, - item.tenderRef, - language, - translationCache, - ) - return nil - }) - } - - if err := group.Wait(); err != nil { - s.logger.Warn("Parallel recommendation response build degraded to sequential", map[string]interface{}{ - "error": err.Error(), - "count": len(items), - }) - for i, item := range items { - out[i] = s.buildRecommendedTenderResponse(ctx, item.tender, item.rank, item.analysis, item.tenderRef, language, translationCache) - } - } - return out } diff --git a/internal/tender/recommendation_response_test.go b/internal/tender/recommendation_response_test.go index 9545dc5..46c0af0 100644 --- a/internal/tender/recommendation_response_test.go +++ b/internal/tender/recommendation_response_test.go @@ -5,7 +5,6 @@ import ( "io" "sync/atomic" "testing" - "time" "go.mongodb.org/mongo-driver/v2/bson" @@ -26,7 +25,6 @@ func (s *countingTranslationStorage) GetDocumentSummariesFromStorage(context.Con func (s *countingTranslationStorage) GetTranslationFromStorage(context.Context, string, string, string) (ai_summarizer.StoredTranslation, error) { s.calls.Add(1) - time.Sleep(20 * time.Millisecond) return ai_summarizer.StoredTranslation{ Title: "Translated title", Description: "Translated description", @@ -41,7 +39,7 @@ func (s *countingTranslationStorage) DownloadDocument(context.Context, string) ( return nil, ai_summarizer.ErrObjectNotFound } -func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { +func TestBuildRecommendedTenderListResponsesPreservesOrderWithoutStorage(t *testing.T) { storage := &countingTranslationStorage{} service := &tenderService{ aiSummarizerStorage: storage, @@ -71,7 +69,7 @@ func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { }) } - got := service.buildRecommendedTenderResponsesParallel(context.Background(), items, "fr") + got := service.buildRecommendedTenderListResponses(items, "fr") if len(got) != len(items) { t.Fatalf("len = %d, want %d", len(got), len(items)) } @@ -80,11 +78,11 @@ func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { if resp.Rank != wantRank { t.Fatalf("got[%d].Rank = %d, want %d", i, resp.Rank, wantRank) } - if resp.Title != "Translated title" { - t.Fatalf("got[%d].Title = %q, want translated title", i, resp.Title) + if resp.Title != "Original" { + t.Fatalf("got[%d].Title = %q, want MongoDB title without MinIO lookup", i, resp.Title) } } - if storage.calls.Load() != int32(len(items)) { - t.Fatalf("translation lookups = %d, want %d", storage.calls.Load(), len(items)) + if storage.calls.Load() != 0 { + t.Fatalf("translation lookups = %d, want 0", storage.calls.Load()) } } diff --git a/internal/tender/repository.go b/internal/tender/repository.go index d3416d5..6176e42 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -392,11 +392,21 @@ func (r *tenderRepository) GetByProcedureReferences(ctx context.Context, refs [] } func mapProcedureReferencesToTenders(candidates []Tender, refs []ProcedureReference) map[string]Tender { - out := make(map[string]Tender) + byFolder := make(map[string][]Tender) + for i := range candidates { + folder := strings.TrimSpace(candidates[i].ContractFolderID) + if folder == "" { + continue + } + byFolder[folder] = append(byFolder[folder], candidates[i]) + } + + out := make(map[string]Tender, len(refs)) for _, ref := range refs { - for i := range candidates { - if tenderMatchesProcedureRef(candidates[i], ref.ContractFolderID, ref.NoticePublicationID) { - out[ref.Ref] = candidates[i] + folder := strings.TrimSpace(ref.ContractFolderID) + for _, candidate := range byFolder[folder] { + if tenderMatchesProcedureRef(candidate, ref.ContractFolderID, ref.NoticePublicationID) { + out[ref.Ref] = candidate break } } diff --git a/internal/tender/service.go b/internal/tender/service.go index 4cae58d..5b362e0 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -947,8 +947,13 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination nil } +const recommendRequestTimeout = 15 * time.Second + // Recommend retrieves AI-ranked tenders for the authenticated customer's company. func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { + ctx, cancel := context.WithTimeout(ctx, recommendRequestTimeout) + defer cancel() + companyIDs := normalizeRecommendationCompanyScope(form.CompanyIDs) companyID := "" if form.CompanyID != nil { @@ -1055,7 +1060,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat end = len(ordered) } - paged := s.buildRecommendedTenderResponsesParallel(ctx, ordered[start:end], lang) + paged := s.buildRecommendedTenderListResponses(ordered[start:end], lang) return &SearchResponse{ Tenders: paged,