Enhance tender recommendation service with caching and parallel processing
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced a caching mechanism for translation responses in the tender recommendation service, improving efficiency by reducing redundant translation requests. - Refactored the `enrichWithTranslation` method to support an optional translation cache, allowing for faster retrieval of stored translations. - Implemented parallel processing for building recommended tender responses, utilizing goroutines to enhance performance and responsiveness. - Added unit tests to validate the new caching behavior and ensure the correct application of translations in various scenarios. This update significantly improves the performance and responsiveness of the tender recommendation service by integrating caching and parallel processing, enhancing the overall user experience.
This commit is contained in:
+36
-28
@@ -176,7 +176,7 @@ func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string]
|
||||
// buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary.
|
||||
func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse {
|
||||
resp := tender.ToResponseWithLanguage("")
|
||||
s.enrichWithTranslation(ctx, tender, resp, language)
|
||||
s.enrichWithTranslation(ctx, tender, resp, language, nil)
|
||||
s.enrichWithAISummary(ctx, tender, resp)
|
||||
return resp
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func (s *tenderService) enrichWithAISummary(ctx context.Context, t *Tender, resp
|
||||
|
||||
// enrichWithTranslation overlays title/description from MinIO when the pipeline has finished
|
||||
// for the requested language. Does not call the AI translate API on GET.
|
||||
func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, resp *TenderResponse, language string) {
|
||||
func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, resp *TenderResponse, language string, cache *recommendationTranslationCache) {
|
||||
if t == nil || resp == nil {
|
||||
return
|
||||
}
|
||||
@@ -215,6 +215,17 @@ func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, re
|
||||
}
|
||||
|
||||
targetLanguage := s.resolveDetailLanguage(language)
|
||||
if cache != nil {
|
||||
if stored, ok := cache.get(t.GetID(), targetLanguage); ok {
|
||||
if strings.TrimSpace(stored.Title) != "" {
|
||||
resp.Title = stored.Title
|
||||
resp.Description = stored.Description
|
||||
resp.Language = targetLanguage
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
enrichCtx, cancel := context.WithTimeout(ctx, aiTranslationEnrichmentTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -233,6 +244,10 @@ func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, re
|
||||
return
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
cache.set(t.GetID(), targetLanguage, stored)
|
||||
}
|
||||
|
||||
resp.Title = stored.Title
|
||||
resp.Description = stored.Description
|
||||
resp.Language = targetLanguage
|
||||
@@ -947,7 +962,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
||||
return nil, fmt.Errorf("%w: cursor pagination is not supported for recommendations", response.ErrInvalidPagination)
|
||||
}
|
||||
|
||||
s.logger.Info("Recommend tenders via AI service", map[string]interface{}{
|
||||
s.logger.Debug("Recommend tenders request", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"company_ids": companyIDs,
|
||||
})
|
||||
@@ -968,39 +983,35 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
||||
return s.emptySearchResponse(pagination), nil
|
||||
}
|
||||
|
||||
tenderByRef, err := s.resolveRecommendedTenders(ctx, recommendations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
tenderByRef map[string]Tender
|
||||
excluded map[string]struct{}
|
||||
excludedByCompany map[string]map[string]struct{}
|
||||
)
|
||||
if form.ExcludeRejectedTenders {
|
||||
if len(companyIDs) > 0 {
|
||||
excludedByCompany, err = s.buildRejectedTenderIDSetsForCompanies(ctx, companyIDs)
|
||||
} else {
|
||||
excluded, err = s.buildRejectedTenderIDSet(ctx, companyID)
|
||||
}
|
||||
if err != nil {
|
||||
loadGroup, loadCtx := errgroup.WithContext(ctx)
|
||||
loadGroup.Go(func() error {
|
||||
var resolveErr error
|
||||
tenderByRef, resolveErr = s.resolveRecommendedTenders(loadCtx, recommendations)
|
||||
return resolveErr
|
||||
})
|
||||
loadGroup.Go(func() error {
|
||||
var exclusionErr error
|
||||
excluded, excludedByCompany, exclusionErr = s.loadRecommendationExclusions(loadCtx, form, companyID, companyIDs)
|
||||
return exclusionErr
|
||||
})
|
||||
if err := loadGroup.Wait(); err != nil {
|
||||
if form.ExcludeRejectedTenders {
|
||||
s.logger.Error("Failed to build recommendation exclusions", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"company_ids": companyIDs,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to build recommendation exclusions: %w", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lang := s.pickResponseLanguage(form.Language)
|
||||
now := time.Now().Unix()
|
||||
type rankedRecommendedTender struct {
|
||||
tender Tender
|
||||
rank int
|
||||
analysis string
|
||||
tenderRef string
|
||||
}
|
||||
ordered := make([]rankedRecommendedTender, 0, len(recommendations))
|
||||
seenTenderIDs := make(map[string]struct{}, len(recommendations))
|
||||
for _, rec := range recommendations {
|
||||
@@ -1044,10 +1055,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
||||
end = len(ordered)
|
||||
}
|
||||
|
||||
paged := make([]TenderResponse, 0, end-start)
|
||||
for _, item := range ordered[start:end] {
|
||||
paged = append(paged, s.buildRecommendedTenderResponse(ctx, item.tender, item.rank, item.analysis, item.tenderRef, lang))
|
||||
}
|
||||
paged := s.buildRecommendedTenderResponsesParallel(ctx, ordered[start:end], lang)
|
||||
|
||||
return &SearchResponse{
|
||||
Tenders: paged,
|
||||
@@ -1055,9 +1063,9 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *tenderService) buildRecommendedTenderResponse(ctx context.Context, tender Tender, rank int, analysis, tenderRef, language string) TenderResponse {
|
||||
func (s *tenderService) buildRecommendedTenderResponse(ctx context.Context, tender Tender, rank int, analysis, tenderRef, language string, cache *recommendationTranslationCache) TenderResponse {
|
||||
resp := tender.ToResponseWithLanguage(language)
|
||||
s.enrichWithTranslation(ctx, &tender, resp, language)
|
||||
s.enrichWithTranslation(ctx, &tender, resp, language, cache)
|
||||
resp.Rank = rank
|
||||
resp.Analysis = analysis
|
||||
resp.ProcedureRef = tenderRef
|
||||
|
||||
Reference in New Issue
Block a user