@@ -57,11 +57,9 @@ func (s *ImageFilterService) DefaultFilterConfig() *FilterConfig {
5757type DegradationStrategy string
5858
5959const (
60- DegradationNone DegradationStrategy = "none"
61- DegradationTimeWindow DegradationStrategy = "time_window_expansion"
62- DegradationThemeExpansion DegradationStrategy = "theme_expansion"
63- DegradationSimilarityThreshold DegradationStrategy = "similarity_threshold_reduction"
64- DegradationNewUserMix DegradationStrategy = "new_user_content_mix"
60+ DegradationNone DegradationStrategy = "none"
61+ DegradationTimeWindow DegradationStrategy = "time_window_reduction"
62+ DegradationSimilarityThreshold DegradationStrategy = "similarity_threshold_mixing"
6563)
6664
6765// FilterContext holds context information for filtering operation
@@ -86,67 +84,10 @@ type FilterMetrics struct {
8684}
8785
8886// FilterResults applies filtering to search results with degradation strategies
87+ // This is a convenience method that calls FilterWithSession with empty sessionID
8988func (s * ImageFilterService ) FilterResults (ctx context.Context , userID int64 , queryID string , query string ,
9089 candidates []RecommendedImageResult , requestedCount int ) ([]RecommendedImageResult , * FilterMetrics , error ) {
91-
92- logger := log .GetReqLogger (ctx )
93-
94- // Get user filter configuration
95- config , err := s .getUserFilterConfig (ctx , userID )
96- if err != nil {
97- logger .Printf ("Failed to get user filter config, using default: %v" , err )
98- config = s .DefaultFilterConfig ()
99- }
100-
101- // If filtering is disabled globally, return original results
102- if ! s .config .Enabled || ! config .Enabled {
103- return candidates , & FilterMetrics {
104- TotalCandidates : len (candidates ),
105- FilteredCount : 0 ,
106- FilterRatio : 0 ,
107- FinalResultCount : len (candidates ),
108- }, nil
109- }
110-
111- // Create filter context
112- filterCtx := & FilterContext {
113- UserID : userID ,
114- QueryID : queryID ,
115- Query : query ,
116- RequestedCount : requestedCount ,
117- Config : config ,
118- DegradationUsed : DegradationNone ,
119- Metrics : & FilterMetrics {
120- TotalCandidates : len (candidates ),
121- },
122- }
123-
124- // Apply filtering with degradation strategies
125- filteredResults , err := s .applyFilteringWithDegradation (ctx , filterCtx , candidates )
126- if err != nil {
127- return candidates , filterCtx .Metrics , fmt .Errorf ("filtering failed: %w" , err )
128- }
129-
130- // Update final metrics
131- filterCtx .Metrics .FinalResultCount = len (filteredResults )
132- filterCtx .Metrics .FilteredCount = filterCtx .Metrics .TotalCandidates - len (filteredResults )
133- if filterCtx .Metrics .TotalCandidates > 0 {
134- filterCtx .Metrics .FilterRatio = float64 (filterCtx .Metrics .FilteredCount ) / float64 (filterCtx .Metrics .TotalCandidates )
135- }
136-
137- // Store metrics asynchronously if enabled
138- if s .config .EnableMetrics {
139- go func () {
140- if err := s .storeFilterMetrics (context .Background (), filterCtx ); err != nil {
141- logger .Printf ("Failed to store filter metrics: %v" , err )
142- }
143- }()
144- }
145-
146- logger .Printf ("Filtering completed: %d candidates -> %d results (%.1f%% filtered, degradation: %s)" ,
147- filterCtx .Metrics .TotalCandidates , len (filteredResults ), filterCtx .Metrics .FilterRatio * 100 , filterCtx .DegradationUsed )
148-
149- return filteredResults , filterCtx .Metrics , nil
90+ return s .FilterWithSession (ctx , userID , "" , queryID , query , candidates , requestedCount )
15091}
15192
15293// FilterWithSession applies session-level filtering to search results with degradation strategies
@@ -246,36 +187,6 @@ func (s *ImageFilterService) FilterWithSession(ctx context.Context, userID int64
246187 return filteredResults , filterCtx .Metrics , nil
247188}
248189
249- // applyFilteringWithDegradation applies filtering with progressive degradation strategies
250- func (s * ImageFilterService ) applyFilteringWithDegradation (ctx context.Context , filterCtx * FilterContext ,
251- candidates []RecommendedImageResult ) ([]RecommendedImageResult , error ) {
252-
253- logger := log .GetReqLogger (ctx )
254-
255- // Try normal filtering first
256- filtered , err := s .applyBasicFiltering (ctx , filterCtx , candidates )
257- if err != nil {
258- return candidates , err
259- }
260-
261- // Check if we have enough results
262- if len (filtered ) >= filterCtx .RequestedCount {
263- return filtered [:filterCtx .RequestedCount ], nil
264- }
265-
266- // Calculate filter ratio
267- filterRatio := float64 (len (candidates )- len (filtered )) / float64 (len (candidates ))
268-
269- // Check if we need degradation (filter ratio too high) and degradation is enabled
270- if s .config .EnableDegradation && filterRatio > filterCtx .Config .MaxFilterRatio {
271- logger .Printf ("High filter ratio detected (%.1f%% > %.1f%%), applying degradation strategies" ,
272- filterRatio * 100 , filterCtx .Config .MaxFilterRatio * 100 )
273-
274- return s .applyDegradationStrategies (ctx , filterCtx , candidates )
275- }
276-
277- return filtered , nil
278- }
279190
280191// applyBasicFiltering applies basic filtering based on user history (fallback method)
281192// This is kept for backward compatibility with degradation strategies
@@ -309,12 +220,21 @@ func (s *ImageFilterService) applyBasicFiltering(ctx context.Context, filterCtx
309220}
310221
311222// applyDegradationStrategies applies progressive degradation strategies when filtering is too aggressive
223+ // PERFORMANCE OPTIMIZATION: Uses single database query with in-memory filtering for all degradation levels
312224func (s * ImageFilterService ) applyDegradationStrategies (ctx context.Context , filterCtx * FilterContext ,
313225 candidates []RecommendedImageResult ) ([]RecommendedImageResult , error ) {
314226
315227 logger := log .GetReqLogger (ctx )
316228
317- // Strategy 1: Time window expansion
229+ // Fetch once with max window (15 days) to avoid N+1 queries
230+ maxWindowDays := 15
231+ histories , err := s .getUserRecommendationHistoryWithTimestamps (ctx , filterCtx .UserID , maxWindowDays )
232+ if err != nil {
233+ logger .Printf ("Failed to get user history with timestamps, falling back to basic filtering: %v" , err )
234+ return s .applyBasicFiltering (ctx , filterCtx , candidates )
235+ }
236+
237+ // Strategy 1: Time window expansion - filter in-memory for each level
318238 degradationLevels := []int {15 , 7 , 3 , 1 } // Reduce window to 15, 7, 3, 1 days
319239 for level , windowDays := range degradationLevels {
320240 filterCtx .Metrics .DegradationLevel = level + 1
@@ -323,17 +243,9 @@ func (s *ImageFilterService) applyDegradationStrategies(ctx context.Context, fil
323243
324244 logger .Printf ("Applying degradation level %d: reducing time window to %d days" , level + 1 , windowDays )
325245
326- // Try filtering with reduced time window using getUserRecommendationHistory directly
327- historyImageIDs , err := s .getUserRecommendationHistory (ctx , filterCtx .UserID , windowDays )
328- if err != nil {
329- continue
330- }
331-
332- // Create a map for fast lookup
333- seenImages := make (map [int64 ]bool , len (historyImageIDs ))
334- for _ , imageID := range historyImageIDs {
335- seenImages [imageID ] = true
336- }
246+ // Filter in-memory by cutoff time
247+ cutoff := time .Now ().AddDate (0 , 0 , - windowDays )
248+ seenImages := filterByTimestamp (histories , cutoff )
337249
338250 // Filter out already recommended images
339251 var filtered []RecommendedImageResult
@@ -466,6 +378,45 @@ func (s *ImageFilterService) getUserRecommendationHistory(ctx context.Context, u
466378 return imageIDs , err
467379}
468380
381+ // RecommendationHistoryWithTimestamp holds recommendation history with timestamps for in-memory filtering
382+ type RecommendationHistoryWithTimestamp struct {
383+ ImageID int64 `json:"image_id"`
384+ CreatedAt time.Time `json:"created_at"`
385+ }
386+
387+ // getUserRecommendationHistoryWithTimestamps gets user's recommendation history with timestamps
388+ // This allows for efficient in-memory filtering for multiple time windows
389+ func (s * ImageFilterService ) getUserRecommendationHistoryWithTimestamps (ctx context.Context , userID int64 , maxWindowDays int ) ([]RecommendationHistoryWithTimestamp , error ) {
390+ // If no database is available, return empty history
391+ if s .db == nil {
392+ return []RecommendationHistoryWithTimestamp {}, nil
393+ }
394+
395+ var histories []RecommendationHistoryWithTimestamp
396+
397+ cutoffTime := time .Now ().AddDate (0 , 0 , - maxWindowDays )
398+
399+ err := s .db .WithContext (ctx ).
400+ Model (& model.UserImageRecommendationHistory {}).
401+ Select ("image_id, created_at" ).
402+ Where ("user_id = ? AND created_at > ?" , userID , cutoffTime ).
403+ Order ("created_at DESC" ).
404+ Scan (& histories ).Error
405+
406+ return histories , err
407+ }
408+
409+ // filterByTimestamp filters history records by cutoff time and returns unique image IDs
410+ func filterByTimestamp (histories []RecommendationHistoryWithTimestamp , cutoff time.Time ) map [int64 ]bool {
411+ seenImages := make (map [int64 ]bool )
412+ for _ , history := range histories {
413+ if history .CreatedAt .After (cutoff ) {
414+ seenImages [history .ImageID ] = true
415+ }
416+ }
417+ return seenImages
418+ }
419+
469420// getSessionRecommendationHistory gets image IDs that have been recommended to user within the session
470421func (s * ImageFilterService ) getSessionRecommendationHistory (ctx context.Context , userID int64 , sessionID string ) ([]int64 , error ) {
471422 // If no database is available, return empty history
0 commit comments