Commit ff32d8945e2d83e4937b40241a902da6a4018820

Authored by tangwang
1 parent 7746376c

rerank

@@ -151,9 +151,23 @@ class SearchRequest(BaseModel): @@ -151,9 +151,23 @@ class SearchRequest(BaseModel):
151 min_score: Optional[float] = Field(None, ge=0, description="最小相关性分数阈值") 151 min_score: Optional[float] = Field(None, ge=0, description="最小相关性分数阈值")
152 highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)") 152 highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)")
153 debug: bool = Field(False, description="是否返回调试信息") 153 debug: bool = Field(False, description="是否返回调试信息")
154 - ai_search: bool = Field( 154 + enable_rerank: bool = Field(
155 False, 155 False,
156 - description="是否开启 AI 搜索(调用本地重排服务对 ES 结果进行二次排序)" 156 + description="是否开启重排(调用外部重排服务对 ES 结果进行二次排序)"
  157 + )
  158 + rerank_query_template: Optional[str] = Field(
  159 + None,
  160 + description=(
  161 + "重排 query 模板(可选)。支持 {query} 占位符。"
  162 + "不传则使用服务端配置的 rerank_query_template。"
  163 + ),
  164 + )
  165 + rerank_doc_template: Optional[str] = Field(
  166 + None,
  167 + description=(
  168 + "重排 doc 模板(可选)。支持 {title} {brief} {vendor} {description} {category_path} 占位符。"
  169 + "不传则使用服务端配置的 rerank_doc_template。"
  170 + ),
157 ) 171 )
158 172
159 # SKU筛选参数 173 # SKU筛选参数
api/routes/search.py
@@ -84,7 +84,9 @@ async def search(request: SearchRequest, http_request: Request): @@ -84,7 +84,9 @@ async def search(request: SearchRequest, http_request: Request):
84 f"min_score: {request.min_score} | " 84 f"min_score: {request.min_score} | "
85 f"language: {request.language} | " 85 f"language: {request.language} | "
86 f"debug: {request.debug} | " 86 f"debug: {request.debug} | "
87 - f"ai_search: {request.ai_search} | " 87 + f"enable_rerank: {request.enable_rerank} | "
  88 + f"rerank_query_template: {request.rerank_query_template} | "
  89 + f"rerank_doc_template: {request.rerank_doc_template} | "
88 f"sku_filter_dimension: {request.sku_filter_dimension} | " 90 f"sku_filter_dimension: {request.sku_filter_dimension} | "
89 f"filters: {request.filters} | " 91 f"filters: {request.filters} | "
90 f"range_filters: {request.range_filters} | " 92 f"range_filters: {request.range_filters} | "
@@ -112,7 +114,9 @@ async def search(request: SearchRequest, http_request: Request): @@ -112,7 +114,9 @@ async def search(request: SearchRequest, http_request: Request):
112 debug=request.debug, 114 debug=request.debug,
113 language=request.language, 115 language=request.language,
114 sku_filter_dimension=request.sku_filter_dimension, 116 sku_filter_dimension=request.sku_filter_dimension,
115 - ai_search=request.ai_search, 117 + enable_rerank=request.enable_rerank,
  118 + rerank_query_template=request.rerank_query_template,
  119 + rerank_doc_template=request.rerank_doc_template,
116 ) 120 )
117 121
118 # Include performance summary in response 122 # Include performance summary in response
config/config.yaml
@@ -133,14 +133,19 @@ function_score: @@ -133,14 +133,19 @@ function_score:
133 boost_mode: "multiply" 133 boost_mode: "multiply"
134 functions: [] 134 functions: []
135 135
136 -# 重排配置(唯一实现:外部 BGE 重排服务,由请求参数 ai_search 控制是否执行)  
137 -# ai_search 且 from+size<=rerank_window 时:从 ES 取前 rerank_window 条、重排后再按 from/size 分页 136 +# 重排配置(唯一实现:外部 BGE 重排服务,由请求参数 enable_rerank 控制是否执行)
  137 +# enable_rerank 且 from+size<=rerank_window 时:从 ES 取前 rerank_window 条、重排后再按 from/size 分页
138 rerank: 138 rerank:
139 rerank_window: 1000 139 rerank_window: 1000
140 # service_url: "http://127.0.0.1:6007/rerank" # 可选,不填则用默认端口 6007 140 # service_url: "http://127.0.0.1:6007/rerank" # 可选,不填则用默认端口 6007
141 timeout_sec: 15.0 # 文档多时重排耗时长,可按需调大 141 timeout_sec: 15.0 # 文档多时重排耗时长,可按需调大
142 weight_es: 0.4 142 weight_es: 0.4
143 weight_ai: 0.6 143 weight_ai: 0.6
  144 + # 模板:用于将搜索请求/文档字段组装成重排服务输入
  145 + # - rerank_query_template:支持 {query}
  146 + # - rerank_doc_template:支持 {title} {brief} {vendor} {description} {category_path}
  147 + rerank_query_template: "{query}"
  148 + rerank_doc_template: "{title}"
144 149
145 # SPU配置(已启用,使用嵌套skus) 150 # SPU配置(已启用,使用嵌套skus)
146 spu_config: 151 spu_config:
config/config_loader.py
@@ -88,14 +88,19 @@ class RankingConfig: @@ -88,14 +88,19 @@ class RankingConfig:
88 88
89 @dataclass 89 @dataclass
90 class RerankConfig: 90 class RerankConfig:
91 - """重排配置(唯一实现:调用外部 BGE 重排服务,由请求参数 ai_search 控制是否执行)"""  
92 - # 重排窗口:ai_search 且 from+size<=rerank_window 时,从 ES 取前 rerank_window 条重排后再分页 91 + """重排配置(唯一实现:调用外部 BGE 重排服务,由请求参数 enable_rerank 控制是否执行)"""
  92 + # 重排窗口:enable_rerank 且 from+size<=rerank_window 时,从 ES 取前 rerank_window 条重排后再分页
93 rerank_window: int = 1000 93 rerank_window: int = 1000
94 # 可选:重排服务 URL,为空时使用 reranker 模块默认端口 6007 94 # 可选:重排服务 URL,为空时使用 reranker 模块默认端口 6007
95 service_url: Optional[str] = None 95 service_url: Optional[str] = None
96 timeout_sec: float = 15.0 96 timeout_sec: float = 15.0
97 weight_es: float = 0.4 97 weight_es: float = 0.4
98 weight_ai: float = 0.6 98 weight_ai: float = 0.6
  99 + # 模板:用于将搜索请求/文档字段组装成重排服务输入
  100 + # - rerank_query_template:支持 {query}
  101 + # - rerank_doc_template:支持 {title} {brief} {vendor} {description} {category_path}
  102 + rerank_query_template: str = "{query}"
  103 + rerank_doc_template: str = "{title}"
99 104
100 105
101 @dataclass 106 @dataclass
@@ -267,7 +272,7 @@ class ConfigLoader: @@ -267,7 +272,7 @@ class ConfigLoader:
267 functions=fs_data.get("functions") or [] 272 functions=fs_data.get("functions") or []
268 ) 273 )
269 274
270 - # Parse Rerank configuration(唯一实现:外部重排服务,由 ai_search 控制) 275 + # Parse Rerank configuration(唯一实现:外部重排服务,由 enable_rerank 控制)
271 rerank_data = config_data.get("rerank", {}) 276 rerank_data = config_data.get("rerank", {})
272 rerank = RerankConfig( 277 rerank = RerankConfig(
273 rerank_window=int(rerank_data.get("rerank_window", 1000)), 278 rerank_window=int(rerank_data.get("rerank_window", 1000)),
@@ -275,6 +280,8 @@ class ConfigLoader: @@ -275,6 +280,8 @@ class ConfigLoader:
275 timeout_sec=float(rerank_data.get("timeout_sec", 15.0)), 280 timeout_sec=float(rerank_data.get("timeout_sec", 15.0)),
276 weight_es=float(rerank_data.get("weight_es", 0.4)), 281 weight_es=float(rerank_data.get("weight_es", 0.4)),
277 weight_ai=float(rerank_data.get("weight_ai", 0.6)), 282 weight_ai=float(rerank_data.get("weight_ai", 0.6)),
  283 + rerank_query_template=str(rerank_data.get("rerank_query_template") or "{query}"),
  284 + rerank_doc_template=str(rerank_data.get("rerank_doc_template") or "{title}"),
278 ) 285 )
279 286
280 # Parse SPU config 287 # Parse SPU config
@@ -410,6 +417,8 @@ class ConfigLoader: @@ -410,6 +417,8 @@ class ConfigLoader:
410 "timeout_sec": config.rerank.timeout_sec, 417 "timeout_sec": config.rerank.timeout_sec,
411 "weight_es": config.rerank.weight_es, 418 "weight_es": config.rerank.weight_es,
412 "weight_ai": config.rerank.weight_ai, 419 "weight_ai": config.rerank.weight_ai,
  420 + "rerank_query_template": config.rerank.rerank_query_template,
  421 + "rerank_doc_template": config.rerank.rerank_doc_template,
413 }, 422 },
414 "spu_config": { 423 "spu_config": {
415 "enabled": config.spu_config.enabled, 424 "enabled": config.spu_config.enabled,
docs/搜索API对接指南.md
@@ -167,7 +167,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -167,7 +167,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
167 "min_score": 0.0, 167 "min_score": 0.0,
168 "sku_filter_dimension": ["string"], 168 "sku_filter_dimension": ["string"],
169 "debug": false, 169 "debug": false,
170 - "ai_search": false, 170 + "enable_rerank": false,
  171 + "rerank_query_template": "{query}",
  172 + "rerank_doc_template": "{title}",
171 "user_id": "string", 173 "user_id": "string",
172 "session_id": "string" 174 "session_id": "string"
173 } 175 }
@@ -189,7 +191,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -189,7 +191,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
189 | `min_score` | float | N | null | 最小相关性分数阈值 | 191 | `min_score` | float | N | null | 最小相关性分数阈值 |
190 | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) | 192 | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) |
191 | `debug` | boolean | N | false | 是否返回调试信息 | 193 | `debug` | boolean | N | false | 是否返回调试信息 |
192 -| `ai_search` | boolean | N | false | 是否开启 AI 搜索(调用本地重排服务对 ES 结果进行二次排序) | 194 +| `enable_rerank` | boolean | N | false | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。开启后若 `from+size<=rerank_window` 才会触发重排 |
  195 +| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 |
  196 +| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 |
193 | `user_id` | string | N | null | 用户ID(用于个性化,预留) | 197 | `user_id` | string | N | null | 用户ID(用于个性化,预留) |
194 | `session_id` | string | N | null | 会话ID(用于分析,预留) | 198 | `session_id` | string | N | null | 会话ID(用于分析,预留) |
195 199
query/test_translation.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 """ 2 """
3 -翻译功能测试脚本。 3 +Translation function test script.
4 4
5 -测试内容:  
6 -1. 翻译提示词配置加载  
7 -2. 同步翻译(索引场景)  
8 -3. 异步翻译(查询场景)  
9 -4. 不同提示词的使用  
10 -5. 缓存功能  
11 -6. DeepL Context参数使用 5 +Test content:
  6 +1. Translation prompt configuration loading
  7 +2. Synchronous translation (indexing scenario)
  8 +3. Asynchronous translation (query scenario)
  9 +4. Usage of different prompts
  10 +5. Cache functionality
  11 +6. DeepL Context parameter usage
12 """ 12 """
13 13
14 import sys 14 import sys
@@ -31,37 +31,37 @@ logger = logging.getLogger(__name__) @@ -31,37 +31,37 @@ logger = logging.getLogger(__name__)
31 31
32 32
33 def test_config_loading(): 33 def test_config_loading():
34 - """测试配置加载""" 34 + """Test configuration loading"""
35 print("\n" + "="*60) 35 print("\n" + "="*60)
36 - print("测试1: 配置加载") 36 + print("Test 1: Configuration loading")
37 print("="*60) 37 print("="*60)
38 38
39 try: 39 try:
40 config_loader = ConfigLoader() 40 config_loader = ConfigLoader()
41 config = config_loader.load_config() 41 config = config_loader.load_config()
42 42
43 - print(f"✓ 配置加载成功")  
44 - print(f" 翻译服务: {config.query_config.translation_service}")  
45 - print(f" 翻译提示词配置:") 43 + print(f"✓ Configuration loaded successfully")
  44 + print(f" Translation service: {config.query_config.translation_service}")
  45 + print(f" Translation prompt configuration:")
46 for key, value in config.query_config.translation_prompts.items(): 46 for key, value in config.query_config.translation_prompts.items():
47 print(f" {key}: {value[:60]}..." if len(value) > 60 else f" {key}: {value}") 47 print(f" {key}: {value[:60]}..." if len(value) > 60 else f" {key}: {value}")
48 48
49 return config 49 return config
50 except Exception as e: 50 except Exception as e:
51 - print(f"✗ 配置加载失败: {e}") 51 + print(f"✗ Configuration loading failed: {e}")
52 import traceback 52 import traceback
53 traceback.print_exc() 53 traceback.print_exc()
54 return None 54 return None
55 55
56 56
57 def test_translator_sync(config): 57 def test_translator_sync(config):
58 - """测试同步翻译(索引场景)""" 58 + """Test synchronous translation (indexing scenario)"""
59 print("\n" + "="*60) 59 print("\n" + "="*60)
60 - print("测试2: 同步翻译(索引场景)") 60 + print("Test 2: Synchronous translation (indexing scenario)")
61 print("="*60) 61 print("="*60)
62 62
63 if not config: 63 if not config:
64 - print("✗ 跳过:配置未加载") 64 + print("✗ Skipped: Configuration not loaded")
65 return None 65 return None
66 66
67 try: 67 try:
@@ -90,10 +90,10 @@ def test_translator_sync(config): @@ -90,10 +90,10 @@ def test_translator_sync(config):
90 else: 90 else:
91 prompt = config.query_config.translation_prompts.get('default_en') 91 prompt = config.query_config.translation_prompts.get('default_en')
92 92
93 - print(f"\n翻译测试:")  
94 - print(f" 原文 ({source_lang}): {text}")  
95 - print(f" 目标语言: {target_lang}")  
96 - print(f" 提示词: {prompt[:50] if prompt else 'None'}...") 93 + print(f"\nTranslation test:")
  94 + print(f" Original text ({source_lang}): {text}")
  95 + print(f" Target language: {target_lang}")
  96 + print(f" Prompt: {prompt[:50] if prompt else 'None'}...")
97 97
98 result = translator.translate( 98 result = translator.translate(
99 text, 99 text,
@@ -103,28 +103,28 @@ def test_translator_sync(config): @@ -103,28 +103,28 @@ def test_translator_sync(config):
103 ) 103 )
104 104
105 if result: 105 if result:
106 - print(f" 结果: {result}")  
107 - print(f" ✓ 翻译成功") 106 + print(f" Result: {result}")
  107 + print(f" ✓ Translation successful")
108 else: 108 else:
109 - print(f" ⚠ 翻译返回None(可能是mock模式或无API key)") 109 + print(f" ⚠ Translation returned None (possibly mock mode or no API key)")
110 110
111 return translator 111 return translator
112 112
113 except Exception as e: 113 except Exception as e:
114 - print(f"✗ 同步翻译测试失败: {e}") 114 + print(f"✗ Synchronous translation test failed: {e}")
115 import traceback 115 import traceback
116 traceback.print_exc() 116 traceback.print_exc()
117 return None 117 return None
118 118
119 119
120 def test_translator_async(config, translator): 120 def test_translator_async(config, translator):
121 - """测试异步翻译(查询场景)""" 121 + """Test asynchronous translation (query scenario)"""
122 print("\n" + "="*60) 122 print("\n" + "="*60)
123 - print("测试3: 异步翻译(查询场景)") 123 + print("Test 3: Asynchronous translation (query scenario)")
124 print("="*60) 124 print("="*60)
125 125
126 if not config or not translator: 126 if not config or not translator:
127 - print("✗ 跳过:配置或翻译器未初始化") 127 + print("✗ Skipped: Configuration or translator not initialized")
128 return 128 return
129 129
130 try: 130 try:
@@ -134,9 +134,9 @@ def test_translator_async(config, translator): @@ -134,9 +134,9 @@ def test_translator_async(config, translator):
134 134
135 query_prompt = config.query_config.translation_prompts.get('query_zh') 135 query_prompt = config.query_config.translation_prompts.get('query_zh')
136 136
137 - print(f"查询文本: {query_text}")  
138 - print(f"目标语言: {target_langs}")  
139 - print(f"提示词: {query_prompt}") 137 + print(f"Query text: {query_text}")
  138 + print(f"Target languages: {target_langs}")
  139 + print(f"Prompt: {query_prompt}")
140 140
141 # 异步模式(立即返回,后台翻译) 141 # 异步模式(立即返回,后台翻译)
142 results = translator.translate_multi( 142 results = translator.translate_multi(
@@ -148,15 +148,15 @@ def test_translator_async(config, translator): @@ -148,15 +148,15 @@ def test_translator_async(config, translator):
148 prompt=query_prompt 148 prompt=query_prompt
149 ) 149 )
150 150
151 - print(f"\n异步翻译结果:") 151 + print(f"\nAsynchronous translation results:")
152 for lang, translation in results.items(): 152 for lang, translation in results.items():
153 if translation: 153 if translation:
154 - print(f" {lang}: {translation} (缓存命中)") 154 + print(f" {lang}: {translation} (cache hit)")
155 else: 155 else:
156 - print(f" {lang}: None (后台翻译中...)") 156 + print(f" {lang}: None (translating in background...)")
157 157
158 # 同步模式(等待完成) 158 # 同步模式(等待完成)
159 - print(f"\n同步翻译(等待完成):") 159 + print(f"\nSynchronous translation (waiting for completion):")
160 results_sync = translator.translate_multi( 160 results_sync = translator.translate_multi(
161 query_text, 161 query_text,
162 target_langs, 162 target_langs,
@@ -170,7 +170,7 @@ def test_translator_async(config, translator): @@ -170,7 +170,7 @@ def test_translator_async(config, translator):
170 print(f" {lang}: {translation}") 170 print(f" {lang}: {translation}")
171 171
172 except Exception as e: 172 except Exception as e:
173 - print(f"✗ 异步翻译测试失败: {e}") 173 + print(f"✗ Asynchronous translation test failed: {e}")
174 import traceback 174 import traceback
175 traceback.print_exc() 175 traceback.print_exc()
176 176
@@ -178,7 +178,7 @@ def test_translator_async(config, translator): @@ -178,7 +178,7 @@ def test_translator_async(config, translator):
178 def test_cache(): 178 def test_cache():
179 """测试缓存功能""" 179 """测试缓存功能"""
180 print("\n" + "="*60) 180 print("\n" + "="*60)
181 - print("测试4: 缓存功能") 181 + print("Test 4: Cache functionality")
182 print("="*60) 182 print("="*60)
183 183
184 try: 184 try:
@@ -195,29 +195,29 @@ def test_cache(): @@ -195,29 +195,29 @@ def test_cache():
195 source_lang = "zh" 195 source_lang = "zh"
196 prompt = config.query_config.translation_prompts.get('default_zh') 196 prompt = config.query_config.translation_prompts.get('default_zh')
197 197
198 - print(f"第一次翻译(应该调用API或返回mock):") 198 + print(f"First translation (should call API or return mock):")
199 result1 = translator.translate(test_text, target_lang, source_lang, prompt=prompt) 199 result1 = translator.translate(test_text, target_lang, source_lang, prompt=prompt)
200 - print(f" 结果: {result1}")  
201 -  
202 - print(f"\n第二次翻译(应该使用缓存):") 200 + print(f" Result: {result1}")
  201 +
  202 + print(f"\nSecond translation (should use cache):")
203 result2 = translator.translate(test_text, target_lang, source_lang, prompt=prompt) 203 result2 = translator.translate(test_text, target_lang, source_lang, prompt=prompt)
204 - print(f" 结果: {result2}")  
205 - 204 + print(f" Result: {result2}")
  205 +
206 if result1 == result2: 206 if result1 == result2:
207 - print(f" ✓ 缓存功能正常") 207 + print(f" ✓ Cache functionality working properly")
208 else: 208 else:
209 - print(f" ⚠ 缓存可能有问题") 209 + print(f" ⚠ Cache might have issues")
210 210
211 except Exception as e: 211 except Exception as e:
212 - print(f"✗ 缓存测试失败: {e}") 212 + print(f"✗ Cache test failed: {e}")
213 import traceback 213 import traceback
214 traceback.print_exc() 214 traceback.print_exc()
215 215
216 216
217 def test_context_parameter(): 217 def test_context_parameter():
218 - """测试DeepL Context参数使用""" 218 + """Test DeepL Context parameter usage"""
219 print("\n" + "="*60) 219 print("\n" + "="*60)
220 - print("测试5: DeepL Context参数") 220 + print("Test 5: DeepL Context parameter")
221 print("="*60) 221 print("="*60)
222 222
223 try: 223 try:
@@ -233,8 +233,8 @@ def test_context_parameter(): @@ -233,8 +233,8 @@ def test_context_parameter():
233 text = "手机" 233 text = "手机"
234 prompt = config.query_config.translation_prompts.get('query_zh') 234 prompt = config.query_config.translation_prompts.get('query_zh')
235 235
236 - print(f"测试文本: {text}")  
237 - print(f"提示词(作为context): {prompt}") 236 + print(f"Test text: {text}")
  237 + print(f"Prompt (as context): {prompt}")
238 238
239 # 带context的翻译 239 # 带context的翻译
240 result_with_context = translator.translate( 240 result_with_context = translator.translate(
@@ -243,7 +243,7 @@ def test_context_parameter(): @@ -243,7 +243,7 @@ def test_context_parameter():
243 source_lang='zh', 243 source_lang='zh',
244 prompt=prompt 244 prompt=prompt
245 ) 245 )
246 - print(f"\n带context翻译结果: {result_with_context}") 246 + print(f"\nTranslation result with context: {result_with_context}")
247 247
248 # 不带context的翻译 248 # 不带context的翻译
249 result_without_context = translator.translate( 249 result_without_context = translator.translate(
@@ -252,21 +252,21 @@ def test_context_parameter(): @@ -252,21 +252,21 @@ def test_context_parameter():
252 source_lang='zh', 252 source_lang='zh',
253 prompt=None 253 prompt=None
254 ) 254 )
255 - print(f"不带context翻译结果: {result_without_context}") 255 + print(f"Translation result without context: {result_without_context}")
256 256
257 - print(f"\n✓ Context参数测试完成")  
258 - print(f" 注意:根据DeepL API,context参数影响翻译但不参与翻译本身") 257 + print(f"\n✓ Context parameter test completed")
  258 + print(f" Note: According to DeepL API, context parameter affects translation but does not participate in translation itself")
259 259
260 except Exception as e: 260 except Exception as e:
261 - print(f"✗ Context参数测试失败: {e}") 261 + print(f"✗ Context parameter test failed: {e}")
262 import traceback 262 import traceback
263 traceback.print_exc() 263 traceback.print_exc()
264 264
265 265
266 def main(): 266 def main():
267 - """主测试函数""" 267 + """Main test function"""
268 print("="*60) 268 print("="*60)
269 - print("翻译功能测试") 269 + print("Translation function test")
270 print("="*60) 270 print("="*60)
271 271
272 # 测试1: 配置加载 272 # 测试1: 配置加载
@@ -285,7 +285,7 @@ def main(): @@ -285,7 +285,7 @@ def main():
285 test_context_parameter() 285 test_context_parameter()
286 286
287 print("\n" + "="*60) 287 print("\n" + "="*60)
288 - print("测试完成") 288 + print("Test completed")
289 print("="*60) 289 print("="*60)
290 290
291 291
reranker/bge_reranker.py
@@ -112,6 +112,17 @@ class BGEReranker: @@ -112,6 +112,17 @@ class BGEReranker:
112 total_docs = len(docs) 112 total_docs = len(docs)
113 output_scores: List[float] = [0.0] * total_docs 113 output_scores: List[float] = [0.0] * total_docs
114 114
  115 + # Log request summary (query + first 3 docs preview)
  116 + preview_docs: List[str] = []
  117 + for d in docs[:3]:
  118 + preview_docs.append("" if d is None else str(d))
  119 + logger.info(
  120 + "[BGE_RERANKER] Request | query=%r | docs=%d | docs_preview=%s",
  121 + query,
  122 + total_docs,
  123 + preview_docs,
  124 + )
  125 +
115 indexed_docs: List[Tuple[int, str]] = [] 126 indexed_docs: List[Tuple[int, str]] = []
116 for i, doc in enumerate(docs): 127 for i, doc in enumerate(docs):
117 if doc is None: 128 if doc is None:
@@ -158,6 +169,15 @@ class BGEReranker: @@ -158,6 +169,15 @@ class BGEReranker:
158 for (orig_idx, _text), unique_idx in zip(indexed_docs, position_to_unique): 169 for (orig_idx, _text), unique_idx in zip(indexed_docs, position_to_unique):
159 output_scores[orig_idx] = float(unique_scores[unique_idx]) 170 output_scores[orig_idx] = float(unique_scores[unique_idx])
160 171
  172 + # Log per-doc scores (aligned to original docs order)
  173 + try:
  174 + lines = []
  175 + for i, d in enumerate(docs[:100]):
  176 + lines.append(f"{output_scores[i]},{'' if d is None else str(d)}")
  177 + logger.info("[BGE_RERANKER] query:%s Scores (score,doc):\n%s", query, "\n".join(lines))
  178 + except Exception:
  179 + pass
  180 +
161 elapsed_ms = (time.time() - start_ts) * 1000.0 181 elapsed_ms = (time.time() - start_ts) * 1000.0
162 dedup_ratio = 0.0 182 dedup_ratio = 0.0
163 if indexed_docs: 183 if indexed_docs:
search/rerank_client.py
@@ -22,12 +22,13 @@ DEFAULT_TIMEOUT_SEC = 15.0 @@ -22,12 +22,13 @@ DEFAULT_TIMEOUT_SEC = 15.0
22 def build_docs_from_hits( 22 def build_docs_from_hits(
23 es_hits: List[Dict[str, Any]], 23 es_hits: List[Dict[str, Any]],
24 language: str = "zh", 24 language: str = "zh",
  25 + doc_template: str = "{title}",
25 ) -> List[str]: 26 ) -> List[str]:
26 """ 27 """
27 从 ES 命中结果构造重排服务所需的文档文本列表(与 hits 一一对应)。 28 从 ES 命中结果构造重排服务所需的文档文本列表(与 hits 一一对应)。
28 29
29 - 文本由 title、brief、description、vendor、category_path 等多语言字段拼接,  
30 - 按 language 优先选取对应语言;若无内容则用 spu_id 兜底。 30 + 使用 doc_template 将文档字段组装为重排服务输入。
  31 + 支持占位符:{title} {brief} {vendor} {description} {category_path}
31 32
32 Args: 33 Args:
33 es_hits: ES 返回的 hits 列表,每项含 _source 34 es_hits: ES 返回的 hits 列表,每项含 _source
@@ -47,16 +48,29 @@ def build_docs_from_hits( @@ -47,16 +48,29 @@ def build_docs_from_hits(
47 return str(obj.get(lang) or obj.get("zh") or obj.get("en") or "").strip() 48 return str(obj.get(lang) or obj.get("zh") or obj.get("en") or "").strip()
48 return str(obj).strip() 49 return str(obj).strip()
49 50
  51 + class _SafeDict(dict):
  52 + def __missing__(self, key: str) -> str:
  53 + return ""
  54 +
50 docs: List[str] = [] 55 docs: List[str] = []
  56 + only_title = "{title}" == doc_template
  57 + need_brief = "{brief}" in doc_template
  58 + need_vendor = "{vendor}" in doc_template
  59 + need_description = "{description}" in doc_template
  60 + need_category_path = "{category_path}" in doc_template
51 for hit in es_hits: 61 for hit in es_hits:
52 src = hit.get("_source") or {} 62 src = hit.get("_source") or {}
53 - parts: List[str] = []  
54 - for key in ("title", "brief", "description", "vendor", "category_path"):  
55 - parts.append(pick_lang_text(src.get(key)))  
56 - text = " ".join(p for p in parts if p).strip()  
57 - if not text:  
58 - text = str(src.get("spu_id", ""))  
59 - docs.append(text) 63 + if only_title:
  64 + docs.append(pick_lang_text(src.get("title")))
  65 + else:
  66 + values = _SafeDict(
  67 + title=pick_lang_text(src.get("title")),
  68 + brief=pick_lang_text(src.get("brief")) if need_brief else "",
  69 + vendor=pick_lang_text(src.get("vendor")) if need_vendor else "",
  70 + description=pick_lang_text(src.get("description")) if need_description else "",
  71 + category_path=pick_lang_text(src.get("category_path")) if need_category_path else "",
  72 + )
  73 + docs.append(str(doc_template).format_map(values))
60 return docs 74 return docs
61 75
62 76
@@ -188,6 +202,8 @@ def run_rerank( @@ -188,6 +202,8 @@ def run_rerank(
188 timeout_sec: float = DEFAULT_TIMEOUT_SEC, 202 timeout_sec: float = DEFAULT_TIMEOUT_SEC,
189 weight_es: float = DEFAULT_WEIGHT_ES, 203 weight_es: float = DEFAULT_WEIGHT_ES,
190 weight_ai: float = DEFAULT_WEIGHT_AI, 204 weight_ai: float = DEFAULT_WEIGHT_AI,
  205 + rerank_query_template: str = "{query}",
  206 + rerank_doc_template: str = "{title}",
191 ) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]], List[Dict[str, Any]]]: 207 ) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]], List[Dict[str, Any]]]:
192 """ 208 """
193 完整重排流程:从 es_response 取 hits -> 构造 docs -> 调服务 -> 融合分数并重排 -> 更新 max_score。 209 完整重排流程:从 es_response 取 hits -> 构造 docs -> 调服务 -> 融合分数并重排 -> 更新 max_score。
@@ -222,8 +238,10 @@ def run_rerank( @@ -222,8 +238,10 @@ def run_rerank(
222 if not hits: 238 if not hits:
223 return es_response, None, [] 239 return es_response, None, []
224 240
225 - docs = build_docs_from_hits(hits, language=language)  
226 - scores, meta = call_rerank_service(query, docs, url, timeout_sec=timeout_sec) 241 + # Apply query template (supports {query})
  242 + query_text = str(rerank_query_template).format_map({"query": query})
  243 + docs = build_docs_from_hits(hits, language=language, doc_template=rerank_doc_template)
  244 + scores, meta = call_rerank_service(query_text, docs, url, timeout_sec=timeout_sec)
227 245
228 if scores is None or len(scores) != len(hits): 246 if scores is None or len(scores) != len(hits):
229 return es_response, None, [] 247 return es_response, None, []
search/searcher.py
@@ -135,7 +135,9 @@ class Searcher: @@ -135,7 +135,9 @@ class Searcher:
135 debug: bool = False, 135 debug: bool = False,
136 language: str = "en", 136 language: str = "en",
137 sku_filter_dimension: Optional[List[str]] = None, 137 sku_filter_dimension: Optional[List[str]] = None,
138 - ai_search: bool = False, 138 + enable_rerank: bool = False,
  139 + rerank_query_template: Optional[str] = None,
  140 + rerank_doc_template: Optional[str] = None,
139 ) -> SearchResult: 141 ) -> SearchResult:
140 """ 142 """
141 Execute search query (外部友好格式). 143 Execute search query (外部友好格式).
@@ -167,11 +169,11 @@ class Searcher: @@ -167,11 +169,11 @@ class Searcher:
167 index_langs = tenant_cfg.get("index_languages") or [] 169 index_langs = tenant_cfg.get("index_languages") or []
168 enable_translation = len(index_langs) > 0 170 enable_translation = len(index_langs) > 0
169 enable_embedding = self.config.query_config.enable_text_embedding 171 enable_embedding = self.config.query_config.enable_text_embedding
170 - # 重排仅由请求参数 ai_search 控制,唯一实现为调用外部 BGE 重排服务  
171 - enable_rerank = bool(ai_search) 172 + # 重排仅由请求参数 enable_rerank 控制,唯一实现为调用外部 BGE 重排服务
  173 + do_rerank = bool(enable_rerank)
172 rerank_window = self.config.rerank.rerank_window or 1000 174 rerank_window = self.config.rerank.rerank_window or 1000
173 # 若开启重排且请求范围在窗口内:从 ES 取前 rerank_window 条、重排后再按 from/size 分页;否则不重排,按原 from/size 查 ES 175 # 若开启重排且请求范围在窗口内:从 ES 取前 rerank_window 条、重排后再按 from/size 分页;否则不重排,按原 from/size 查 ES
174 - in_rerank_window = enable_rerank and (from_ + size) <= rerank_window 176 + in_rerank_window = do_rerank and (from_ + size) <= rerank_window
175 es_fetch_from = 0 if in_rerank_window else from_ 177 es_fetch_from = 0 if in_rerank_window else from_
176 es_fetch_size = rerank_window if in_rerank_window else size 178 es_fetch_size = rerank_window if in_rerank_window else size
177 179
@@ -180,7 +182,7 @@ class Searcher: @@ -180,7 +182,7 @@ class Searcher:
180 182
181 context.logger.info( 183 context.logger.info(
182 f"开始搜索请求 | 查询: '{query}' | 参数: size={size}, from_={from_}, " 184 f"开始搜索请求 | 查询: '{query}' | 参数: size={size}, from_={from_}, "
183 - f"enable_rerank={enable_rerank}, in_rerank_window={in_rerank_window}, es_fetch=({es_fetch_from},{es_fetch_size}) | " 185 + f"enable_rerank={do_rerank}, in_rerank_window={in_rerank_window}, es_fetch=({es_fetch_from},{es_fetch_size}) | "
184 f"enable_translation={enable_translation}, enable_embedding={enable_embedding}, min_score={min_score}", 186 f"enable_translation={enable_translation}, enable_embedding={enable_embedding}, min_score={min_score}",
185 extra={'reqid': context.reqid, 'uid': context.uid} 187 extra={'reqid': context.reqid, 'uid': context.uid}
186 ) 188 )
@@ -192,12 +194,14 @@ class Searcher: @@ -192,12 +194,14 @@ class Searcher:
192 'es_fetch_from': es_fetch_from, 194 'es_fetch_from': es_fetch_from,
193 'es_fetch_size': es_fetch_size, 195 'es_fetch_size': es_fetch_size,
194 'in_rerank_window': in_rerank_window, 196 'in_rerank_window': in_rerank_window,
  197 + 'rerank_query_template': rerank_query_template,
  198 + 'rerank_doc_template': rerank_doc_template,
195 'filters': filters, 199 'filters': filters,
196 'range_filters': range_filters, 200 'range_filters': range_filters,
197 'facets': facets, 201 'facets': facets,
198 'enable_translation': enable_translation, 202 'enable_translation': enable_translation,
199 'enable_embedding': enable_embedding, 203 'enable_embedding': enable_embedding,
200 - 'enable_rerank': enable_rerank, 204 + 'enable_rerank': do_rerank,
201 'min_score': min_score, 205 'min_score': min_score,
202 'sort_by': sort_by, 206 'sort_by': sort_by,
203 'sort_order': sort_order 207 'sort_order': sort_order
@@ -206,7 +210,7 @@ class Searcher: @@ -206,7 +210,7 @@ class Searcher:
206 context.metadata['feature_flags'] = { 210 context.metadata['feature_flags'] = {
207 'translation_enabled': enable_translation, 211 'translation_enabled': enable_translation,
208 'embedding_enabled': enable_embedding, 212 'embedding_enabled': enable_embedding,
209 - 'rerank_enabled': enable_rerank 213 + 'rerank_enabled': do_rerank
210 } 214 }
211 215
212 # Step 1: Parse query 216 # Step 1: Parse query
@@ -374,13 +378,15 @@ class Searcher: @@ -374,13 +378,15 @@ class Searcher:
374 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH) 378 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH)
375 379
376 # Optional Step 4.5: AI reranking(仅当请求范围在重排窗口内时执行) 380 # Optional Step 4.5: AI reranking(仅当请求范围在重排窗口内时执行)
377 - if enable_rerank and in_rerank_window: 381 + if do_rerank and in_rerank_window:
378 context.start_stage(RequestContextStage.RERANKING) 382 context.start_stage(RequestContextStage.RERANKING)
379 try: 383 try:
380 from .rerank_client import run_rerank 384 from .rerank_client import run_rerank
381 385
382 rerank_query = parsed_query.original_query if parsed_query else query 386 rerank_query = parsed_query.original_query if parsed_query else query
383 rc = self.config.rerank 387 rc = self.config.rerank
  388 + effective_query_template = rerank_query_template or rc.rerank_query_template
  389 + effective_doc_template = rerank_doc_template or rc.rerank_doc_template
384 es_response, rerank_meta, fused_debug = run_rerank( 390 es_response, rerank_meta, fused_debug = run_rerank(
385 query=rerank_query, 391 query=rerank_query,
386 es_response=es_response, 392 es_response=es_response,
@@ -389,6 +395,8 @@ class Searcher: @@ -389,6 +395,8 @@ class Searcher:
389 timeout_sec=rc.timeout_sec, 395 timeout_sec=rc.timeout_sec,
390 weight_es=rc.weight_es, 396 weight_es=rc.weight_es,
391 weight_ai=rc.weight_ai, 397 weight_ai=rc.weight_ai,
  398 + rerank_query_template=effective_query_template,
  399 + rerank_doc_template=effective_doc_template,
392 ) 400 )
393 401
394 if rerank_meta is not None: 402 if rerank_meta is not None: