Skip to content

Commit 970a021

Browse files
author
spf7000
committed
refactor(image-filter): clean up redundant code and optimize performance
1 parent 0855884 commit 970a021

6 files changed

Lines changed: 242 additions & 288 deletions

File tree

spx-backend/docs/SESSION_LEVEL_FILTERING_DESIGN.md

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
好的,遵命。这是您提供的原文内容,未做任何修改和补充,直接转换为 Markdown 格式。
2-
3-
---
4-
51
# **图片推荐过滤功能设计文档**
62

73
## 1. 概述
@@ -57,33 +53,93 @@ graph TD
5753

5854
### 3.2 数据库设计
5955

60-
#### 3.2.2 用户过滤配置表
56+
#### 3.2.1 用户图片过滤配置表 (user_image_filter_config)
6157
```sql
6258
CREATE TABLE `user_image_filter_config` (
6359
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
6460
`user_id` bigint NOT NULL COMMENT '用户ID',
65-
`filter_window_days` int DEFAULT 30 COMMENT '过滤窗口期(天)',
6661
`max_filter_ratio` decimal(3,2) DEFAULT 0.80 COMMENT '最大过滤比例(0-1)',
62+
`session_enabled` tinyint(1) DEFAULT 1 COMMENT '会话级过滤开关',
6763
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
6864
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
65+
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳',
6966
PRIMARY KEY (`id`),
7067
UNIQUE KEY `uk_user_id` (`user_id`)
7168
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户图片过滤配置表';
7269
```
7370

74-
### 3.4.2 过滤流程
75-
1. 获取用户过滤配置和历史记录
76-
2. 执行带过滤的图片搜索(扩大搜索量补偿过滤损失)
77-
3. 检测过滤饱和度,触发降级策略
78-
4. AI生成补充(避免重复生成)
79-
5. 记录推荐历史用于后续过滤
71+
#### 3.2.2 用户图片推荐历史表 (user_image_recommendation_history)
72+
```sql
73+
CREATE TABLE `user_image_recommendation_history` (
74+
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
75+
`user_id` bigint NOT NULL COMMENT '用户ID',
76+
`image_id` bigint NOT NULL COMMENT '图片ID',
77+
`query_id` varchar(36) NOT NULL COMMENT '查询ID',
78+
`session_id` varchar(36) DEFAULT NULL COMMENT '会话ID(用于会话级过滤)',
79+
`query` text COMMENT '用户原始查询',
80+
`source` varchar(20) NOT NULL COMMENT '图片来源(search/generated)',
81+
`similarity` decimal(5,3) COMMENT '相似度分数',
82+
`rank` int NOT NULL COMMENT '推荐结果中的排名',
83+
`selected` boolean DEFAULT false COMMENT '是否被用户选择',
84+
`selected_at` timestamp NULL COMMENT '选择时间',
85+
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
86+
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
87+
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳',
88+
PRIMARY KEY (`id`),
89+
KEY `idx_user_image` (`user_id`, `image_id`),
90+
KEY `idx_query_id` (`query_id`),
91+
KEY `idx_user_session` (`user_id`, `session_id`),
92+
KEY `idx_selected` (`selected`)
93+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户图片推荐历史表';
94+
```
95+
96+
#### 3.2.3 图片过滤指标表 (image_filter_metrics)
97+
```sql
98+
CREATE TABLE `image_filter_metrics` (
99+
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
100+
`user_id` bigint NOT NULL COMMENT '用户ID',
101+
`query_id` varchar(36) NOT NULL COMMENT '查询ID',
102+
`total_candidates` int NOT NULL COMMENT '总候选数量',
103+
`filtered_count` int NOT NULL COMMENT '过滤数量',
104+
`filter_ratio` decimal(5,3) COMMENT '过滤比例',
105+
`degradation_level` int DEFAULT 0 COMMENT '降级等级(0=无降级,1-4=降级等级)',
106+
`degradation_strategy` varchar(100) COMMENT '降级策略',
107+
`final_result_count` int NOT NULL COMMENT '最终结果数量',
108+
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
109+
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
110+
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳',
111+
PRIMARY KEY (`id`),
112+
KEY `idx_user_id` (`user_id`),
113+
KEY `idx_query_id` (`query_id`)
114+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='图片过滤指标表';
115+
```
116+
117+
### 3.4 会话级过滤实现
118+
119+
#### 3.4.1 核心服务组件
120+
- **ImageFilterService**: 核心过滤服务 (`internal/controller/image_filter_service.go`)
121+
- **UserImageFilterConfig**: 用户过滤配置模型 (`internal/model/image_filter.go`)
122+
- **UserImageRecommendationHistory**: 推荐历史记录模型
123+
- **ImageFilterMetrics**: 过滤指标监控模型
124+
125+
#### 3.4.2 过滤流程
126+
1. **配置获取**: 从数据库获取用户过滤配置或使用默认配置
127+
2. **会话过滤**: 若启用会话级过滤,先过滤当前会话已推荐的图片
128+
3. **历史过滤**: 基于时间窗口过滤历史推荐图片
129+
4. **降级检测**: 检测过滤率,超过阈值时触发降级策略
130+
5. **结果记录**: 异步记录推荐历史和过滤指标
131+
132+
#### 3.4.3 关键方法
133+
- `FilterWithSession()`: 会话级过滤主方法
134+
- `applySessionFiltering()`: 应用会话过滤逻辑
135+
- `applyDegradationStrategies()`: 应用降级策略
136+
- `RecordRecommendationHistoryWithSession()`: 记录推荐历史(支持会话ID)
80137

81138
### 3.5 降级策略
82139
当过滤率过高时(默认>80%),按优先级依次尝试:
83-
1. 时间窗口扩展:7天→15天→30天→60天→无限制
84-
2. 主题范围扩展:搜索相似主题(cartoon→anime→cute)
85-
3. 相似度阈值降低:0.8→0.7→0.6→0.5
86-
4. 新用户内容混合:混入30%新用户喜欢的内容
140+
1. **时间窗口缩减**:15天→7天→3天→1天(减少历史窗口降低过滤率)
141+
2. **相似度阈值混合**:混入高相似度(>0.23)的已推荐图片
142+
3. **降级策略标记**:记录使用的降级策略和等级用于监控
87143

88144
### 3.6 性能优化
89145
- 缓存策略:Redis缓存用户历史ID + 本地缓存配置
@@ -92,11 +148,24 @@ CREATE TABLE `user_image_filter_config` (
92148

93149
## 4. 配置参数
94150

95-
### 4.1 关键配置
151+
### 4.1 ImageFilterConfig 结构配置
152+
153+
基于 `internal/config/config.go` 中的实际实现:
154+
155+
| 配置字段 | 类型 | 默认值 | 描述 |
156+
|----------------------------|---------|--------|--------------------------------------------|
157+
| `Enabled` | bool | `true` | 过滤功能全局开关 |
158+
| `DefaultWindowDays` | int | `30` | 默认过滤窗口期(30天) |
159+
| `DefaultMaxFilterRatio` | float64 | `0.8` | 默认最大过滤比例(0.8) |
160+
| `SearchExpansionRatio` | float64 | `2.0` | 搜索量扩大倍数(2.0) |
161+
| `EnableDegradation` | bool | `true` | 是否启用降级策略 |
162+
| `EnableMetrics` | bool | `true` | 是否存储过滤指标数据 |
163+
164+
### 4.2 用户级配置
165+
166+
用户可以在 `user_image_filter_config` 表中配置个性化参数:
96167

97-
| 参数名 | 默认值 | 描述 |
98-
|------------------------------------|--------|--------------------------------------------|
99-
| `IMAGE_FILTER_ENABLED` | `true` | 过滤功能开关 |
100-
| `IMAGE_FILTER_DEFAULT_WINDOW_DAYS` | `7` | 默认过滤窗口期(7天) |
101-
| `IMAGE_FILTER_DEFAULT_MAX_RATIO` | `0.8` | 默认最大过滤比例(0.8) |
102-
| `IMAGE_FILTER_SEARCH_EXPANSION_RATIO` | `2.0` | 搜索量扩大倍数(2.0) |
168+
| 字段名 | 类型 | 默认值 | 描述 |
169+
|-----------------------|---------|--------|--------------------------|
170+
| `max_filter_ratio` | decimal | `0.80` | 用户个性化最大过滤比例 |
171+
| `session_enabled` | boolean | `true` | 是否启用会话级过滤 |

spx-backend/internal/controller/image_filter_service.go

Lines changed: 57 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,9 @@ func (s *ImageFilterService) DefaultFilterConfig() *FilterConfig {
5757
type DegradationStrategy string
5858

5959
const (
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
8988
func (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
312224
func (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
470421
func (s *ImageFilterService) getSessionRecommendationHistory(ctx context.Context, userID int64, sessionID string) ([]int64, error) {
471422
// If no database is available, return empty history

spx-backend/internal/controller/image_filter_service_simple_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,8 @@ func TestDegradationStrategy_String(t *testing.T) {
9898
expected string
9999
}{
100100
{DegradationNone, "none"},
101-
{DegradationTimeWindow, "time_window_expansion"},
102-
{DegradationThemeExpansion, "theme_expansion"},
103-
{DegradationSimilarityThreshold, "similarity_threshold_reduction"},
104-
{DegradationNewUserMix, "new_user_content_mix"},
101+
{DegradationTimeWindow, "time_window_reduction"},
102+
{DegradationSimilarityThreshold, "similarity_threshold_mixing"},
105103
}
106104

107105
for _, test := range tests {
@@ -186,7 +184,7 @@ func TestFilterMetrics_Validation(t *testing.T) {
186184
FilteredCount: 20,
187185
FilterRatio: 0.2,
188186
DegradationLevel: 1,
189-
DegradationStrategy: "time_window_expansion",
187+
DegradationStrategy: "time_window_reduction",
190188
FinalResultCount: 80,
191189
}
192190

0 commit comments

Comments
 (0)