Refactor tender recommendation response handling and improve test coverage
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Renamed the test function to `TestBuildRecommendedTenderListResponsesPreservesOrderWithoutStorage` for clarity and updated its logic to reflect changes in the recommendation response building process. - Removed unnecessary sleep in the test to enhance performance. - Refactored the `buildRecommendedTenderResponsesParallel` method to `buildRecommendedTenderListResponses`, simplifying the response building without relying on MinIO translation lookups. - Updated the repository's `mapProcedureReferencesToTenders` function to improve efficiency by grouping candidates by their contract folder IDs, enhancing the mapping process. This update streamlines the recommendation response handling and enhances the test suite for better maintainability and performance.
This commit is contained in:
@@ -6,13 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
"tm/pkg/ai_summarizer"
|
"tm/pkg/ai_summarizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const recommendationTranslationConcurrency = 8
|
|
||||||
|
|
||||||
type rankedRecommendedTender struct {
|
type rankedRecommendedTender struct {
|
||||||
tender Tender
|
tender Tender
|
||||||
rank int
|
rank int
|
||||||
@@ -54,52 +50,21 @@ func (c *recommendationTranslationCache) set(tenderID, language string, stored a
|
|||||||
c.items[recommendationTranslationCacheKey(tenderID, language)] = stored
|
c.items[recommendationTranslationCacheKey(tenderID, language)] = stored
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *tenderService) buildRecommendedTenderResponsesParallel(
|
// buildRecommendedTenderListResponses builds list responses without MinIO translation lookups.
|
||||||
ctx context.Context,
|
// Recommendation pages use MongoDB text; tender detail endpoints still enrich from storage.
|
||||||
items []rankedRecommendedTender,
|
func (s *tenderService) buildRecommendedTenderListResponses(items []rankedRecommendedTender, language string) []TenderResponse {
|
||||||
language string,
|
|
||||||
) []TenderResponse {
|
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]TenderResponse, len(items))
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
for i, item := range items {
|
||||||
out[i] = s.buildRecommendedTenderResponse(ctx, item.tender, item.rank, item.analysis, item.tenderRef, language, translationCache)
|
resp := item.tender.ToResponseWithLanguage(language)
|
||||||
|
resp.Rank = item.rank
|
||||||
|
resp.Analysis = item.analysis
|
||||||
|
resp.ProcedureRef = item.tenderRef
|
||||||
|
out[i] = *resp
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"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) {
|
func (s *countingTranslationStorage) GetTranslationFromStorage(context.Context, string, string, string) (ai_summarizer.StoredTranslation, error) {
|
||||||
s.calls.Add(1)
|
s.calls.Add(1)
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
return ai_summarizer.StoredTranslation{
|
return ai_summarizer.StoredTranslation{
|
||||||
Title: "Translated title",
|
Title: "Translated title",
|
||||||
Description: "Translated description",
|
Description: "Translated description",
|
||||||
@@ -41,7 +39,7 @@ func (s *countingTranslationStorage) DownloadDocument(context.Context, string) (
|
|||||||
return nil, ai_summarizer.ErrObjectNotFound
|
return nil, ai_summarizer.ErrObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) {
|
func TestBuildRecommendedTenderListResponsesPreservesOrderWithoutStorage(t *testing.T) {
|
||||||
storage := &countingTranslationStorage{}
|
storage := &countingTranslationStorage{}
|
||||||
service := &tenderService{
|
service := &tenderService{
|
||||||
aiSummarizerStorage: storage,
|
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) {
|
if len(got) != len(items) {
|
||||||
t.Fatalf("len = %d, want %d", 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 {
|
if resp.Rank != wantRank {
|
||||||
t.Fatalf("got[%d].Rank = %d, want %d", i, resp.Rank, wantRank)
|
t.Fatalf("got[%d].Rank = %d, want %d", i, resp.Rank, wantRank)
|
||||||
}
|
}
|
||||||
if resp.Title != "Translated title" {
|
if resp.Title != "Original" {
|
||||||
t.Fatalf("got[%d].Title = %q, want translated title", i, resp.Title)
|
t.Fatalf("got[%d].Title = %q, want MongoDB title without MinIO lookup", i, resp.Title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if storage.calls.Load() != int32(len(items)) {
|
if storage.calls.Load() != 0 {
|
||||||
t.Fatalf("translation lookups = %d, want %d", storage.calls.Load(), len(items))
|
t.Fatalf("translation lookups = %d, want 0", storage.calls.Load())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,11 +392,21 @@ func (r *tenderRepository) GetByProcedureReferences(ctx context.Context, refs []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapProcedureReferencesToTenders(candidates []Tender, refs []ProcedureReference) map[string]Tender {
|
func mapProcedureReferencesToTenders(candidates []Tender, refs []ProcedureReference) map[string]Tender {
|
||||||
out := make(map[string]Tender)
|
byFolder := make(map[string][]Tender)
|
||||||
for _, ref := range refs {
|
|
||||||
for i := range candidates {
|
for i := range candidates {
|
||||||
if tenderMatchesProcedureRef(candidates[i], ref.ContractFolderID, ref.NoticePublicationID) {
|
folder := strings.TrimSpace(candidates[i].ContractFolderID)
|
||||||
out[ref.Ref] = candidates[i]
|
if folder == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byFolder[folder] = append(byFolder[folder], candidates[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]Tender, len(refs))
|
||||||
|
for _, ref := range refs {
|
||||||
|
folder := strings.TrimSpace(ref.ContractFolderID)
|
||||||
|
for _, candidate := range byFolder[folder] {
|
||||||
|
if tenderMatchesProcedureRef(candidate, ref.ContractFolderID, ref.NoticePublicationID) {
|
||||||
|
out[ref.Ref] = candidate
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -947,8 +947,13 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recommendRequestTimeout = 15 * time.Second
|
||||||
|
|
||||||
// Recommend retrieves AI-ranked tenders for the authenticated customer's company.
|
// 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) {
|
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)
|
companyIDs := normalizeRecommendationCompanyScope(form.CompanyIDs)
|
||||||
companyID := ""
|
companyID := ""
|
||||||
if form.CompanyID != nil {
|
if form.CompanyID != nil {
|
||||||
@@ -1055,7 +1060,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
|||||||
end = len(ordered)
|
end = len(ordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
paged := s.buildRecommendedTenderResponsesParallel(ctx, ordered[start:end], lang)
|
paged := s.buildRecommendedTenderListResponses(ordered[start:end], lang)
|
||||||
|
|
||||||
return &SearchResponse{
|
return &SearchResponse{
|
||||||
Tenders: paged,
|
Tenders: paged,
|
||||||
|
|||||||
Reference in New Issue
Block a user