Commit 93be98cb287bd879e5e7e6c495e3d505154d5053

Authored by tangwang
1 parent 7a013ca7

清理过时的文档

config/config.yaml
@@ -278,7 +278,7 @@ services: @@ -278,7 +278,7 @@ services:
278 glossary_id: "" 278 glossary_id: ""
279 use_cache: true 279 use_cache: true
280 nllb-200-distilled-600m: 280 nllb-200-distilled-600m:
281 - enabled: false 281 + enabled: true
282 backend: "local_nllb" 282 backend: "local_nllb"
283 model_id: "facebook/nllb-200-distilled-600M" 283 model_id: "facebook/nllb-200-distilled-600M"
284 model_dir: "./models/translation/facebook/nllb-200-distilled-600M" 284 model_dir: "./models/translation/facebook/nllb-200-distilled-600M"
context/request_context.py
@@ -41,7 +41,6 @@ class QueryAnalysisResult: @@ -41,7 +41,6 @@ class QueryAnalysisResult:
41 translations: Dict[str, str] = field(default_factory=dict) 41 translations: Dict[str, str] = field(default_factory=dict)
42 query_vector: Optional[List[float]] = None 42 query_vector: Optional[List[float]] = None
43 boolean_ast: Optional[str] = None 43 boolean_ast: Optional[str] = None
44 - is_simple_query: bool = True  
45 44
46 45
47 @dataclass 46 @dataclass
@@ -280,7 +279,6 @@ class RequestContext: @@ -280,7 +279,6 @@ class RequestContext:
280 'query_normalized': self.query_analysis.query_normalized, 279 'query_normalized': self.query_analysis.query_normalized,
281 'rewritten_query': self.query_analysis.rewritten_query, 280 'rewritten_query': self.query_analysis.rewritten_query,
282 'detected_language': self.query_analysis.detected_language, 281 'detected_language': self.query_analysis.detected_language,
283 - 'is_simple_query': self.query_analysis.is_simple_query  
284 }, 282 },
285 'performance': { 283 'performance': {
286 'total_duration_ms': round(self.performance_metrics.total_duration, 2), 284 'total_duration_ms': round(self.performance_metrics.total_duration, 2),
docs/亚马逊到店匠格式转换分析.md renamed to data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md
docs/亚马逊格式数据转店匠商品导入模板.md renamed to data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md
docs/Search-API-Examples.md deleted
@@ -1,1350 +0,0 @@ @@ -1,1350 +0,0 @@
1 -# API 使用示例  
2 -  
3 -本文档提供了搜索引擎 API 的详细使用示例,包括各种常见场景和最佳实践。  
4 -  
5 ----  
6 -  
7 -## 目录  
8 -  
9 -1. [基础搜索](#基础搜索)  
10 -2. [过滤器使用](#过滤器使用)  
11 -3. [分面搜索](#分面搜索)  
12 -4. [排序](#排序)  
13 -5. [图片搜索](#图片搜索)  
14 -6. [布尔表达式](#布尔表达式)  
15 -7. [完整示例](#完整示例)  
16 -  
17 ----  
18 -  
19 -## 基础搜索  
20 -  
21 -### 示例 1:最简单的搜索  
22 -  
23 -```bash  
24 -curl -X POST "http://localhost:6002/search/" \  
25 - -H "Content-Type: application/json" \  
26 - -H "X-Tenant-ID: 162" \  
27 - -d '{  
28 - "query": "芭比娃娃"  
29 - }'  
30 -```  
31 -  
32 -**响应**:  
33 -```json  
34 -{  
35 - "hits": [...],  
36 - "total": 118,  
37 - "max_score": 8.5,  
38 - "took_ms": 45,  
39 - "query_info": {  
40 - "original_query": "芭比娃娃",  
41 - "detected_language": "zh",  
42 - "translations": {"en": "barbie doll"}  
43 - }  
44 -}  
45 -```  
46 -  
47 -### 示例 2:指定返回数量  
48 -  
49 -```bash  
50 -curl -X POST "http://localhost:6002/search/" \  
51 - -H "Content-Type: application/json" \  
52 - -H "X-Tenant-ID: 162" \  
53 - -d '{  
54 - "query": "手机",  
55 - "language": "zh",  
56 - "size": 50  
57 - }'  
58 -```  
59 -  
60 -### 示例 3:分页查询  
61 -  
62 -```bash  
63 -# 第1页(0-19)  
64 -curl -X POST "http://localhost:6002/search/" \  
65 - -H "Content-Type: application/json" \  
66 - -H "X-Tenant-ID: 162" \  
67 - -d '{  
68 - "query": "手机",  
69 - "language": "zh",  
70 - "size": 20,  
71 - "from": 0  
72 - }'  
73 -  
74 -# 第2页(20-39)  
75 -curl -X POST "http://localhost:6002/search/" \  
76 - -H "Content-Type: application/json" \  
77 - -H "X-Tenant-ID: 162" \  
78 - -d '{  
79 - "query": "手机",  
80 - "language": "zh",  
81 - "size": 20,  
82 - "from": 20  
83 - }'  
84 -```  
85 -  
86 ----  
87 -  
88 -## 过滤器使用  
89 -  
90 -### 精确匹配过滤器  
91 -  
92 -#### 示例 1:单值过滤  
93 -  
94 -```bash  
95 -curl -X POST "http://localhost:6002/search/" \  
96 - -H "Content-Type: application/json" \  
97 - -H "X-Tenant-ID: 162" \  
98 - -d '{  
99 - "query": "手机",  
100 - "language": "zh",  
101 - "filters": {  
102 - "category_name": "手机"  
103 - }  
104 - }'  
105 -```  
106 -  
107 -#### 示例 2:多值过滤(OR 逻辑)  
108 -  
109 -```bash  
110 -curl -X POST "http://localhost:6002/search/" \  
111 - -H "Content-Type: application/json" \  
112 - -H "X-Tenant-ID: 162" \  
113 - -d '{  
114 - "query": "手机",  
115 - "language": "zh",  
116 - "filters": {  
117 - "category_name": ["手机", "电子产品"]  
118 - }  
119 - }'  
120 -```  
121 -  
122 -说明:匹配类目为"玩具" **或** "益智玩具" **或** "儿童玩具"的商品。  
123 -  
124 -#### 示例 3:多字段过滤(AND 逻辑)  
125 -  
126 -```bash  
127 -curl -X POST "http://localhost:6002/search/" \  
128 - -H "Content-Type: application/json" \  
129 - -H "X-Tenant-ID: 162" \  
130 - -d '{  
131 - "query": "手机",  
132 - "language": "zh",  
133 - "filters": {  
134 - "category_name": "手机",  
135 - "vendor.zh.keyword": "奇乐"  
136 - }  
137 - }'  
138 -```  
139 -  
140 -说明:必须同时满足"类目=手机" **并且** "品牌=奇乐"。  
141 -  
142 -#### 示例 4:Specifications 嵌套过滤(单个规格)  
143 -  
144 -```bash  
145 -curl -X POST "http://localhost:6002/search/" \  
146 - -H "Content-Type: application/json" \  
147 - -H "X-Tenant-ID: 162" \  
148 - -d '{  
149 - "query": "手机",  
150 - "language": "zh",  
151 - "filters": {  
152 - "specifications": {  
153 - "name": "color",  
154 - "value": "white"  
155 - }  
156 - }  
157 - }'  
158 -```  
159 -  
160 -说明:查询规格名称为"color"且值为"white"的商品。  
161 -  
162 -#### 示例 5:Specifications 嵌套过滤(多个规格,OR逻辑)  
163 -  
164 -```bash  
165 -curl -X POST "http://localhost:6002/search/" \  
166 - -H "Content-Type: application/json" \  
167 - -H "X-Tenant-ID: 162" \  
168 - -d '{  
169 - "query": "手机",  
170 - "language": "zh",  
171 - "filters": {  
172 - "specifications": [  
173 - {"name": "color", "value": "white"},  
174 - {"name": "size", "value": "256GB"}  
175 - ]  
176 - }  
177 - }'  
178 -```  
179 -  
180 -说明:查询满足任意一个规格的商品(color=white **或** size=256GB)。  
181 -  
182 -#### 示例 6:组合过滤(包含 specifications)  
183 -  
184 -```bash  
185 -curl -X POST "http://localhost:6002/search/" \  
186 - -H "Content-Type: application/json" \  
187 - -H "X-Tenant-ID: 162" \  
188 - -d '{  
189 - "query": "手机",  
190 - "language": "zh",  
191 - "filters": {  
192 - "category_name": "手机",  
193 - "specifications": {  
194 - "name": "color",  
195 - "value": "white"  
196 - }  
197 - }  
198 - }'  
199 -```  
200 -  
201 -说明:同时满足类目=手机 **并且** color=white。  
202 -  
203 -### 范围过滤器  
204 -  
205 -#### 示例 1:价格范围  
206 -  
207 -```bash  
208 -curl -X POST "http://localhost:6002/search/" \  
209 - -H "Content-Type: application/json" \  
210 - -H "X-Tenant-ID: 162" \  
211 - -d '{  
212 - "query": "手机",  
213 - "language": "zh",  
214 - "range_filters": {  
215 - "min_price": {  
216 - "gte": 50,  
217 - "lte": 200  
218 - }  
219 - }  
220 - }'  
221 -```  
222 -  
223 -说明:价格在 50-200 元之间(包含边界)。  
224 -  
225 -#### 示例 2:只设置下限  
226 -  
227 -```bash  
228 -curl -X POST "http://localhost:6002/search/" \  
229 - -H "Content-Type: application/json" \  
230 - -H "X-Tenant-ID: 162" \  
231 - -d '{  
232 - "query": "手机",  
233 - "language": "zh",  
234 - "range_filters": {  
235 - "min_price": {  
236 - "gte": 100  
237 - }  
238 - }  
239 - }'  
240 -```  
241 -  
242 -说明:价格 ≥ 100 元。  
243 -  
244 -#### 示例 3:只设置上限  
245 -  
246 -```bash  
247 -curl -X POST "http://localhost:6002/search/" \  
248 - -H "Content-Type: application/json" \  
249 - -H "X-Tenant-ID: 162" \  
250 - -d '{  
251 - "query": "手机",  
252 - "language": "zh",  
253 - "range_filters": {  
254 - "min_price": {  
255 - "lt": 50  
256 - }  
257 - }  
258 - }'  
259 -```  
260 -  
261 -说明:价格 < 50 元(不包含50)。  
262 -  
263 -#### 示例 4:多字段范围过滤  
264 -  
265 -```bash  
266 -curl -X POST "http://localhost:6002/search/" \  
267 - -H "Content-Type: application/json" \  
268 - -H "X-Tenant-ID: 162" \  
269 - -d '{  
270 - "query": "手机",  
271 - "language": "zh",  
272 - "range_filters": {  
273 - "min_price": {  
274 - "gte": 50,  
275 - "lte": 200  
276 - },  
277 - "days_since_last_update": {  
278 - "lte": 30  
279 - }  
280 - }  
281 - }'  
282 -```  
283 -  
284 -说明:价格在 50-200 元 **并且** 最近30天内更新过。  
285 -  
286 -### 组合过滤器  
287 -  
288 -```bash  
289 -curl -X POST "http://localhost:6002/search/" \  
290 - -H "Content-Type: application/json" \  
291 - -H "X-Tenant-ID: 162" \  
292 - -d '{  
293 - "query": "手机",  
294 - "language": "zh",  
295 - "filters": {  
296 - "category_name": ["手机", "电子产品"],  
297 - "vendor.zh.keyword": "品牌A"  
298 - },  
299 - "range_filters": {  
300 - "min_price": {  
301 - "gte": 50,  
302 - "lte": 500  
303 - }  
304 - }  
305 - }'  
306 -```  
307 -  
308 -说明:类目是"玩具"或"益智玩具" **并且** 品牌是"乐高" **并且** 价格在 50-500 元之间。  
309 -  
310 ----  
311 -  
312 -## 分面搜索  
313 -  
314 -### 简单模式  
315 -  
316 -#### 示例 1:基础分面  
317 -  
318 -```bash  
319 -curl -X POST "http://localhost:6002/search/" \  
320 - -H "Content-Type: application/json" \  
321 - -H "X-Tenant-ID: 162" \  
322 - -d '{  
323 - "query": "手机",  
324 - "language": "zh",  
325 - "size": 20,  
326 - "facets": ["category1_name", "category2_name", "specifications"]  
327 - }'  
328 -```  
329 -  
330 -**响应**:  
331 -```json  
332 -{  
333 - "results": [...],  
334 - "total": 118,  
335 - "facets": [  
336 - {  
337 - "field": "category1_name",  
338 - "label": "category1_name",  
339 - "type": "terms",  
340 - "values": [  
341 - {"value": "手机", "count": 85, "selected": false},  
342 - {"value": "电子产品", "count": 33, "selected": false}  
343 - ]  
344 - },  
345 - {  
346 - "field": "specifications.color",  
347 - "label": "color",  
348 - "type": "terms",  
349 - "values": [  
350 - {"value": "white", "count": 50, "selected": false},  
351 - {"value": "black", "count": 30, "selected": false}  
352 - ]  
353 - },  
354 - {  
355 - "field": "specifications.size",  
356 - "label": "size",  
357 - "type": "terms",  
358 - "values": [  
359 - {"value": "256GB", "count": 40, "selected": false},  
360 - {"value": "512GB", "count": 20, "selected": false}  
361 - ]  
362 - }  
363 - ]  
364 -}  
365 -```  
366 -  
367 -#### 示例 2:Specifications 分面(所有规格名称)  
368 -  
369 -```bash  
370 -curl -X POST "http://localhost:6002/search/" \  
371 - -H "Content-Type: application/json" \  
372 - -H "X-Tenant-ID: 162" \  
373 - -d '{  
374 - "query": "手机",  
375 - "language": "zh",  
376 - "facets": ["specifications"]  
377 - }'  
378 -```  
379 -  
380 -说明:返回所有规格名称(name)及其对应的值(value)列表。  
381 -  
382 -#### 示例 3:Specifications 分面(指定规格名称)  
383 -  
384 -```bash  
385 -curl -X POST "http://localhost:6002/search/" \  
386 - -H "Content-Type: application/json" \  
387 - -H "X-Tenant-ID: 162" \  
388 - -d '{  
389 - "query": "手机",  
390 - "language": "zh",  
391 - "facets": ["specifications.color", "specifications.size"]  
392 - }'  
393 -```  
394 -  
395 -说明:只返回指定规格名称的值列表。  
396 -  
397 -### 高级模式  
398 -  
399 -#### 示例 1:自定义分面大小  
400 -  
401 -```bash  
402 -curl -X POST "http://localhost:6002/search/" \  
403 - -H "Content-Type: application/json" \  
404 - -H "X-Tenant-ID: 162" \  
405 - -d '{  
406 - "query": "手机",  
407 - "language": "zh",  
408 - "facets": [  
409 - {  
410 - "field": "category1_name",  
411 - "size": 20,  
412 - "type": "terms"  
413 - },  
414 - {  
415 - "field": "category2_name",  
416 - "size": 30,  
417 - "type": "terms"  
418 - }  
419 - ]  
420 - }'  
421 -```  
422 -  
423 -#### 示例 2:范围分面  
424 -  
425 -```bash  
426 -curl -X POST "http://localhost:6002/search/" \  
427 - -H "Content-Type: application/json" \  
428 - -H "X-Tenant-ID: 162" \  
429 - -d '{  
430 - "query": "手机",  
431 - "language": "zh",  
432 - "facets": [  
433 - {  
434 - "field": "price",  
435 - "type": "range",  
436 - "ranges": [  
437 - {"key": "0-50", "to": 50},  
438 - {"key": "50-100", "from": 50, "to": 100},  
439 - {"key": "100-200", "from": 100, "to": 200},  
440 - {"key": "200+", "from": 200}  
441 - ]  
442 - }  
443 - ]  
444 - }'  
445 -```  
446 -  
447 -**响应**:  
448 -```json  
449 -{  
450 - "facets": [  
451 - {  
452 - "field": "price",  
453 - "label": "price",  
454 - "type": "range",  
455 - "values": [  
456 - {"value": "0-50", "count": 23, "selected": false},  
457 - {"value": "50-100", "count": 45, "selected": false},  
458 - {"value": "100-200", "count": 38, "selected": false},  
459 - {"value": "200+", "count": 12, "selected": false}  
460 - ]  
461 - }  
462 - ]  
463 -}  
464 -```  
465 -  
466 -#### 示例 3:混合分面(Terms + Range)  
467 -  
468 -```bash  
469 -curl -X POST "http://localhost:6002/search/" \  
470 - -H "Content-Type: application/json" \  
471 - -H "X-Tenant-ID: 162" \  
472 - -d '{  
473 - "query": "手机",  
474 - "language": "zh",  
475 - "facets": [  
476 - {"field": "category.keyword", "size": 15},  
477 - {"field": "vendor.keyword", "size": 15},  
478 - {  
479 - "field": "price",  
480 - "type": "range",  
481 - "ranges": [  
482 - {"key": "低价", "to": 50},  
483 - {"key": "中价", "from": 50, "to": 200},  
484 - {"key": "高价", "from": 200}  
485 - ]  
486 - }  
487 - ]  
488 - }'  
489 -```  
490 -  
491 ----  
492 -  
493 -## 排序  
494 -  
495 -### 示例 1:按价格排序(升序)  
496 -  
497 -```bash  
498 -curl -X POST "http://localhost:6002/search/" \  
499 - -H "Content-Type: application/json" \  
500 - -H "X-Tenant-ID: 162" \  
501 - -d '{  
502 - "query": "手机",  
503 - "language": "zh",  
504 - "size": 20,  
505 - "sort_by": "min_price",  
506 - "sort_order": "asc"  
507 - }'  
508 -```  
509 -  
510 -### 示例 2:按创建时间排序(降序)  
511 -  
512 -```bash  
513 -curl -X POST "http://localhost:6002/search/" \  
514 - -H "Content-Type: application/json" \  
515 - -H "X-Tenant-ID: 162" \  
516 - -d '{  
517 - "query": "手机",  
518 - "language": "zh",  
519 - "size": 20,  
520 - "sort_by": "create_time",  
521 - "sort_order": "desc"  
522 - }'  
523 -```  
524 -  
525 -### 示例 3:排序+过滤  
526 -  
527 -```bash  
528 -curl -X POST "http://localhost:6002/search/" \  
529 - -H "Content-Type: application/json" \  
530 - -H "X-Tenant-ID: 162" \  
531 - -d '{  
532 - "query": "手机",  
533 - "language": "zh",  
534 - "filters": {  
535 - "category.keyword": "益智玩具"  
536 - },  
537 - "sort_by": "min_price",  
538 - "sort_order": "asc"  
539 - }'  
540 -```  
541 -  
542 ----  
543 -  
544 -## 图片搜索  
545 -  
546 -### 示例 1:基础图片搜索  
547 -  
548 -```bash  
549 -curl -X POST "http://localhost:6002/search/image" \  
550 - -H "Content-Type: application/json" \  
551 - -H "X-Tenant-ID: 162" \  
552 - -d '{  
553 - "image_url": "https://example.com/barbie.jpg",  
554 - "size": 20  
555 - }'  
556 -```  
557 -  
558 -### 示例 2:图片搜索+过滤器  
559 -  
560 -```bash  
561 -curl -X POST "http://localhost:6002/search/image" \  
562 - -H "Content-Type: application/json" \  
563 - -H "X-Tenant-ID: 162" \  
564 - -d '{  
565 - "image_url": "https://example.com/barbie.jpg",  
566 - "size": 20,  
567 - "filters": {  
568 - "category_name": "手机"  
569 - },  
570 - "range_filters": {  
571 - "min_price": {  
572 - "lte": 100  
573 - }  
574 - }  
575 - }'  
576 -```  
577 -  
578 ----  
579 -  
580 -## 布尔表达式  
581 -  
582 -### 示例 1:AND 查询  
583 -  
584 -```bash  
585 -curl -X POST "http://localhost:6002/search/" \  
586 - -H "Content-Type: application/json" \  
587 - -H "X-Tenant-ID: 162" \  
588 - -d '{  
589 - "query": "玩具 AND 乐高"  
590 - }'  
591 -```  
592 -  
593 -说明:必须同时包含"玩具"和"乐高"。  
594 -  
595 -### 示例 2:OR 查询  
596 -  
597 -```bash  
598 -curl -X POST "http://localhost:6002/search/" \  
599 - -H "Content-Type: application/json" \  
600 - -H "X-Tenant-ID: 162" \  
601 - -d '{  
602 - "query": "芭比 OR 娃娃"  
603 - }'  
604 -```  
605 -  
606 -说明:包含"芭比"或"娃娃"即可。  
607 -  
608 -### 示例 3:ANDNOT 查询(排除)  
609 -  
610 -```bash  
611 -curl -X POST "http://localhost:6002/search/" \  
612 - -H "Content-Type: application/json" \  
613 - -H "X-Tenant-ID: 162" \  
614 - -d '{  
615 - "query": "玩具 ANDNOT 电动"  
616 - }'  
617 -```  
618 -  
619 -说明:包含"玩具"但不包含"电动"。  
620 -  
621 -### 示例 4:复杂布尔表达式  
622 -  
623 -```bash  
624 -curl -X POST "http://localhost:6002/search/" \  
625 - -H "Content-Type: application/json" \  
626 - -H "X-Tenant-ID: 162" \  
627 - -d '{  
628 - "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动"  
629 - }'  
630 -```  
631 -  
632 -说明:必须包含"玩具",并且包含"乐高"或"芭比",但不包含"电动"。  
633 -  
634 -### 示例 5:域查询  
635 -  
636 -```bash  
637 -curl -X POST "http://localhost:6002/search/" \  
638 - -H "Content-Type: application/json" \  
639 - -H "X-Tenant-ID: 162" \  
640 - -d '{  
641 - "query": "brand:乐高"  
642 - }'  
643 -```  
644 -  
645 -说明:在品牌域中搜索"乐高"。  
646 -  
647 ----  
648 -  
649 -## 完整示例  
650 -  
651 -### Python 完整示例  
652 -  
653 -```python  
654 -#!/usr/bin/env python3  
655 -import requests  
656 -import json  
657 -  
658 -API_URL = "http://localhost:6002/search/"  
659 -  
660 -def search_products(  
661 - query,  
662 - size=20,  
663 - from_=0,  
664 - filters=None,  
665 - range_filters=None,  
666 - facets=None,  
667 - sort_by=None,  
668 - sort_order="desc",  
669 - debug=False  
670 -):  
671 - """执行搜索查询"""  
672 - payload = {  
673 - "query": query,  
674 - "size": size,  
675 - "from": from_  
676 - }  
677 -  
678 - if filters:  
679 - payload["filters"] = filters  
680 - if range_filters:  
681 - payload["range_filters"] = range_filters  
682 - if facets:  
683 - payload["facets"] = facets  
684 - if sort_by:  
685 - payload["sort_by"] = sort_by  
686 - payload["sort_order"] = sort_order  
687 - if debug:  
688 - payload["debug"] = debug  
689 -  
690 - response = requests.post(API_URL, json=payload)  
691 - response.raise_for_status()  
692 - return response.json()  
693 -  
694 -  
695 -# 示例 1:简单搜索  
696 -result = search_products("芭比娃娃", size=10)  
697 -print(f"找到 {result['total']} 个结果")  
698 -for hit in result['hits'][:3]:  
699 - product = hit['_source']  
700 - print(f" - {product['name']}: ¥{product.get('price', 'N/A')}")  
701 -  
702 -# 示例 2:带过滤和分面的搜索  
703 -result = search_products(  
704 - query="手机",  
705 - size=20,  
706 - language="zh",  
707 - filters={  
708 - "category_name": "手机",  
709 - "specifications": {"name": "color", "value": "white"}  
710 - },  
711 - range_filters={  
712 - "min_price": {"gte": 50, "lte": 200}  
713 - },  
714 - facets=[  
715 - {"field": "category1_name", "size": 15},  
716 - {"field": "category2_name", "size": 15},  
717 - "specifications.color",  
718 - "specifications.size",  
719 - {  
720 - "field": "min_price",  
721 - "type": "range",  
722 - "ranges": [  
723 - {"key": "0-50", "to": 50},  
724 - {"key": "50-100", "from": 50, "to": 100},  
725 - {"key": "100-200", "from": 100, "to": 200},  
726 - {"key": "200+", "from": 200}  
727 - ]  
728 - }  
729 - ],  
730 - sort_by="min_price",  
731 - sort_order="asc"  
732 -)  
733 -  
734 -# 显示分面结果  
735 -print(f"\n分面统计:")  
736 -for facet in result.get('facets', []):  
737 - print(f"\n{facet['label']} ({facet['type']}):")  
738 - for value in facet['values'][:5]:  
739 - selected_mark = "✓" if value['selected'] else " "  
740 - print(f" [{selected_mark}] {value['label']}: {value['count']}")  
741 -  
742 -# 示例 3:分页查询  
743 -page = 1  
744 -page_size = 20  
745 -total_pages = 5  
746 -  
747 -for page in range(1, total_pages + 1):  
748 - result = search_products(  
749 - query="玩具",  
750 - size=page_size,  
751 - from_=(page - 1) * page_size  
752 - )  
753 - print(f"\n第 {page} 页:")  
754 - for hit in result['hits']:  
755 - product = hit['_source']  
756 - print(f" - {product['name']}")  
757 -```  
758 -  
759 -### JavaScript 完整示例  
760 -  
761 -```javascript  
762 -// 搜索引擎客户端  
763 -class SearchClient {  
764 - constructor(baseUrl) {  
765 - this.baseUrl = baseUrl;  
766 - }  
767 -  
768 - async search({  
769 - query,  
770 - size = 20,  
771 - from = 0,  
772 - filters = null,  
773 - rangeFilters = null,  
774 - facets = null,  
775 - sortBy = null,  
776 - sortOrder = 'desc',  
777 - debug = false  
778 - }) {  
779 - const payload = {  
780 - query,  
781 - size,  
782 - from  
783 - };  
784 -  
785 - if (filters) payload.filters = filters;  
786 - if (rangeFilters) payload.range_filters = rangeFilters;  
787 - if (facets) payload.facets = facets;  
788 - if (sortBy) {  
789 - payload.sort_by = sortBy;  
790 - payload.sort_order = sortOrder;  
791 - }  
792 - if (debug) payload.debug = debug;  
793 -  
794 - const response = await fetch(`${this.baseUrl}/search/`, {  
795 - method: 'POST',  
796 - headers: {  
797 - 'Content-Type': 'application/json'  
798 - },  
799 - body: JSON.stringify(payload)  
800 - });  
801 -  
802 - if (!response.ok) {  
803 - throw new Error(`HTTP ${response.status}: ${response.statusText}`);  
804 - }  
805 -  
806 - return await response.json();  
807 - }  
808 -  
809 - async searchByImage(imageUrl, options = {}) {  
810 - const payload = {  
811 - image_url: imageUrl,  
812 - size: options.size || 20,  
813 - filters: options.filters || null,  
814 - range_filters: options.rangeFilters || null  
815 - };  
816 -  
817 - const response = await fetch(`${this.baseUrl}/search/image`, {  
818 - method: 'POST',  
819 - headers: {  
820 - 'Content-Type': 'application/json'  
821 - },  
822 - body: JSON.stringify(payload)  
823 - });  
824 -  
825 - if (!response.ok) {  
826 - throw new Error(`HTTP ${response.status}: ${response.statusText}`);  
827 - }  
828 -  
829 - return await response.json();  
830 - }  
831 -}  
832 -  
833 -// 使用示例  
834 -const client = new SearchClient('http://localhost:6002');  
835 -  
836 -// 简单搜索  
837 -const result1 = await client.search({  
838 - query: "芭比娃娃",  
839 - size: 20  
840 -});  
841 -console.log(`找到 ${result1.total} 个结果`);  
842 -  
843 -// 带过滤和分面的搜索(包含规格)  
844 -const result2 = await client.search({  
845 - query: "手机",  
846 - language: "zh",  
847 - size: 20,  
848 - filters: {  
849 - category_name: "手机",  
850 - specifications: { name: "color", value: "white" }  
851 - },  
852 - rangeFilters: {  
853 - min_price: { gte: 50, lte: 200 }  
854 - },  
855 - facets: [  
856 - "category1_name",  
857 - "specifications.color",  
858 - "specifications.size"  
859 - ],  
860 - sortBy: "min_price",  
861 - sortOrder: "asc"  
862 -});  
863 -  
864 -// 显示分面结果  
865 -result2.facets.forEach(facet => {  
866 - console.log(`\n${facet.label}:`);  
867 - facet.values.forEach(value => {  
868 - const selected = value.selected ? '✓' : ' ';  
869 - console.log(` [${selected}] ${value.label}: ${value.count}`);  
870 - });  
871 -});  
872 -  
873 -// 显示商品  
874 -result2.hits.forEach(hit => {  
875 - const product = hit._source;  
876 - console.log(`${product.name} - ¥${product.price}`);  
877 -});  
878 -```  
879 -  
880 -### 前端完整示例(Vue.js 风格)  
881 -  
882 -```javascript  
883 -// 搜索组件  
884 -const SearchComponent = {  
885 - data() {  
886 - return {  
887 - query: '',  
888 - results: [],  
889 - facets: [],  
890 - filters: {},  
891 - rangeFilters: {},  
892 - total: 0,  
893 - currentPage: 1,  
894 - pageSize: 20  
895 - };  
896 - },  
897 - methods: {  
898 - async search() {  
899 - const response = await fetch('http://localhost:6002/search/', {  
900 - method: 'POST',  
901 - headers: { 'Content-Type': 'application/json' },  
902 - body: JSON.stringify({  
903 - query: this.query,  
904 - size: this.pageSize,  
905 - from: (this.currentPage - 1) * this.pageSize,  
906 - filters: this.filters,  
907 - range_filters: this.rangeFilters,  
908 - facets: [  
909 - { field: 'category.keyword', size: 15 },  
910 - { field: 'vendor.keyword', size: 15 }  
911 - ]  
912 - })  
913 - });  
914 -  
915 - const data = await response.json();  
916 - this.results = data.hits;  
917 - this.facets = data.facets || [];  
918 - this.total = data.total;  
919 - },  
920 -  
921 - toggleFilter(field, value) {  
922 - if (!this.filters[field]) {  
923 - this.filters[field] = [];  
924 - }  
925 -  
926 - const index = this.filters[field].indexOf(value);  
927 - if (index > -1) {  
928 - this.filters[field].splice(index, 1);  
929 - if (this.filters[field].length === 0) {  
930 - delete this.filters[field];  
931 - }  
932 - } else {  
933 - this.filters[field].push(value);  
934 - }  
935 -  
936 - this.currentPage = 1;  
937 - this.search();  
938 - },  
939 -  
940 - setPriceRange(min, max) {  
941 - if (min !== null || max !== null) {  
942 - this.rangeFilters.price = {};  
943 - if (min !== null) this.rangeFilters.price.gte = min;  
944 - if (max !== null) this.rangeFilters.price.lte = max;  
945 - } else {  
946 - delete this.rangeFilters.price;  
947 - }  
948 - this.currentPage = 1;  
949 - this.search();  
950 - }  
951 - }  
952 -};  
953 -```  
954 -  
955 ----  
956 -  
957 -## 调试与优化  
958 -  
959 -### 启用调试模式  
960 -  
961 -```bash  
962 -curl -X POST "http://localhost:6002/search/" \  
963 - -H "Content-Type: application/json" \  
964 - -H "X-Tenant-ID: 162" \  
965 - -d '{  
966 - "query": "手机",  
967 - "language": "zh",  
968 - "debug": true  
969 - }'  
970 -```  
971 -  
972 -**响应包含调试信息**:  
973 -```json  
974 -{  
975 - "hits": [...],  
976 - "total": 118,  
977 - "debug_info": {  
978 - "query_analysis": {  
979 - "original_query": "玩具",  
980 - "query_normalized": "玩具",  
981 - "rewritten_query": "玩具",  
982 - "detected_language": "zh",  
983 - "translations": {"en": "toy"}  
984 - },  
985 - "es_query": {  
986 - "query": {...},  
987 - "size": 10  
988 - },  
989 - "stage_timings": {  
990 - "query_parsing": 5.3,  
991 - "elasticsearch_search": 35.1,  
992 - "result_processing": 4.8  
993 - }  
994 - }  
995 -}  
996 -```  
997 -  
998 -### 设置最小分数阈值  
999 -  
1000 -```bash  
1001 -curl -X POST "http://localhost:6002/search/" \  
1002 - -H "Content-Type: application/json" \  
1003 - -H "X-Tenant-ID: 162" \  
1004 - -d '{  
1005 - "query": "手机",  
1006 - "language": "zh",  
1007 - "min_score": 5.0  
1008 - }'  
1009 -```  
1010 -  
1011 -说明:只返回相关性分数 ≥ 5.0 的结果。  
1012 -  
1013 ----  
1014 -  
1015 -## 常见使用场景  
1016 -  
1017 -### 场景 1:电商分类页  
1018 -  
1019 -```bash  
1020 -# 显示某个类目下的所有商品,按价格排序,提供品牌筛选  
1021 -curl -X POST "http://localhost:6002/search/" \  
1022 - -H "Content-Type: application/json" \  
1023 - -H "X-Tenant-ID: 162" \  
1024 - -d '{  
1025 - "query": "*",  
1026 - "filters": {  
1027 - "category_name": "手机"  
1028 - },  
1029 - "facets": [  
1030 - {"field": "vendor.keyword", "size": 20},  
1031 - {  
1032 - "field": "price",  
1033 - "type": "range",  
1034 - "ranges": [  
1035 - {"key": "0-50", "to": 50},  
1036 - {"key": "50-100", "from": 50, "to": 100},  
1037 - {"key": "100-200", "from": 100, "to": 200},  
1038 - {"key": "200+", "from": 200}  
1039 - ]  
1040 - }  
1041 - ],  
1042 - "sort_by": "min_price",  
1043 - "sort_order": "asc",  
1044 - "size": 24  
1045 - }'  
1046 -```  
1047 -  
1048 -### 场景 2:搜索结果页  
1049 -  
1050 -```bash  
1051 -# 用户搜索关键词,提供筛选和排序(包含规格分面)  
1052 -curl -X POST "http://localhost:6002/search/" \  
1053 - -H "Content-Type: application/json" \  
1054 - -H "X-Tenant-ID: 162" \  
1055 - -d '{  
1056 - "query": "手机",  
1057 - "language": "zh",  
1058 - "facets": [  
1059 - {"field": "category1_name", "size": 10},  
1060 - {"field": "category2_name", "size": 10},  
1061 - "specifications.color",  
1062 - "specifications.size",  
1063 - {  
1064 - "field": "min_price",  
1065 - "type": "range",  
1066 - "ranges": [  
1067 - {"key": "0-50", "to": 50},  
1068 - {"key": "50-100", "from": 50, "to": 100},  
1069 - {"key": "100+", "from": 100}  
1070 - ]  
1071 - }  
1072 - ],  
1073 - "size": 20  
1074 - }'  
1075 -```  
1076 -  
1077 -### 场景 2.1:带规格过滤的搜索结果页  
1078 -  
1079 -```bash  
1080 -# 用户搜索并选择了规格筛选条件  
1081 -curl -X POST "http://localhost:6002/search/" \  
1082 - -H "Content-Type: application/json" \  
1083 - -H "X-Tenant-ID: 162" \  
1084 - -d '{  
1085 - "query": "手机",  
1086 - "language": "zh",  
1087 - "filters": {  
1088 - "category_name": "手机",  
1089 - "specifications": {  
1090 - "name": "color",  
1091 - "value": "white"  
1092 - }  
1093 - },  
1094 - "facets": [  
1095 - "category1_name",  
1096 - "specifications.color",  
1097 - "specifications.size"  
1098 - ],  
1099 - "size": 20  
1100 - }'  
1101 -```  
1102 -  
1103 -### 场景 3:促销专区  
1104 -  
1105 -```bash  
1106 -# 显示特定价格区间的商品  
1107 -curl -X POST "http://localhost:6002/search/" \  
1108 - -H "Content-Type: application/json" \  
1109 - -H "X-Tenant-ID: 162" \  
1110 - -d '{  
1111 - "query": "*",  
1112 - "range_filters": {  
1113 - "min_price": {  
1114 - "gte": 50,  
1115 - "lte": 100  
1116 - }  
1117 - },  
1118 - "facets": ["category1_name", "category2_name", "specifications"],  
1119 - "sort_by": "min_price",  
1120 - "sort_order": "asc",  
1121 - "size": 50  
1122 - }'  
1123 -```  
1124 -  
1125 -### 场景 4:新品推荐  
1126 -  
1127 -```bash  
1128 -# 最近更新的商品  
1129 -curl -X POST "http://localhost:6002/search/" \  
1130 - -H "Content-Type: application/json" \  
1131 - -H "X-Tenant-ID: 162" \  
1132 - -d '{  
1133 - "query": "*",  
1134 - "range_filters": {  
1135 - "days_since_last_update": {  
1136 - "lte": 7  
1137 - }  
1138 - },  
1139 - "sort_by": "create_time",  
1140 - "sort_order": "desc",  
1141 - "size": 20  
1142 - }'  
1143 -```  
1144 -  
1145 ----  
1146 -  
1147 -## 错误处理  
1148 -  
1149 -### 示例 1:参数错误  
1150 -  
1151 -```bash  
1152 -# 错误:range_filters 缺少操作符  
1153 -curl -X POST "http://localhost:6002/search/" \  
1154 - -H "Content-Type: application/json" \  
1155 - -H "X-Tenant-ID: 162" \  
1156 - -d '{  
1157 - "query": "手机",  
1158 - "language": "zh",  
1159 - "range_filters": {  
1160 - "min_price": {}  
1161 - }  
1162 - }'  
1163 -```  
1164 -  
1165 -**响应**:  
1166 -```json  
1167 -{  
1168 - "error": "Validation error",  
1169 - "detail": "至少需要指定一个范围边界(gte, gt, lte, lt)",  
1170 - "timestamp": 1699800000  
1171 -}  
1172 -```  
1173 -  
1174 -### 示例 2:空查询  
1175 -  
1176 -```bash  
1177 -# 错误:query 为空  
1178 -curl -X POST "http://localhost:6002/search/" \  
1179 - -H "Content-Type: application/json" \  
1180 - -H "X-Tenant-ID: 162" \  
1181 - -d '{  
1182 - "query": ""  
1183 - }'  
1184 -```  
1185 -  
1186 -**响应**:  
1187 -```json  
1188 -{  
1189 - "error": "Validation error",  
1190 - "detail": "query field required",  
1191 - "timestamp": 1699800000  
1192 -}  
1193 -```  
1194 -  
1195 ----  
1196 -  
1197 -## 性能优化建议  
1198 -  
1199 -### 1. 合理使用分面  
1200 -  
1201 -```bash  
1202 -# ❌ 不推荐:请求太多分面  
1203 -{  
1204 - "facets": [  
1205 - {"field": "field1", "size": 100},  
1206 - {"field": "field2", "size": 100},  
1207 - {"field": "field3", "size": 100},  
1208 - // ... 10+ facets  
1209 - ]  
1210 -}  
1211 -  
1212 -# ✅ 推荐:只请求必要的分面  
1213 -{  
1214 - "facets": [  
1215 - {"field": "category.keyword", "size": 15},  
1216 - {"field": "vendor.keyword", "size": 15}  
1217 - ]  
1218 -}  
1219 -```  
1220 -  
1221 -### 2. 控制返回数量  
1222 -  
1223 -```bash  
1224 -# ❌ 不推荐:一次返回太多  
1225 -{  
1226 - "size": 100  
1227 -}  
1228 -  
1229 -# ✅ 推荐:分页查询  
1230 -{  
1231 - "size": 20,  
1232 - "from": 0  
1233 -}  
1234 -```  
1235 -  
1236 -### 3. 使用适当的过滤器  
1237 -  
1238 -```bash  
1239 -# ✅ 推荐:先过滤后搜索  
1240 -{  
1241 - "query": "玩具",  
1242 - "filters": {  
1243 - "category.keyword": "玩具"  
1244 - }  
1245 -}  
1246 -```  
1247 -  
1248 ----  
1249 -  
1250 -## 高级技巧  
1251 -  
1252 -### 技巧 1:获取所有类目  
1253 -  
1254 -```bash  
1255 -# 使用通配符查询 + 分面  
1256 -curl -X POST "http://localhost:6002/search/" \  
1257 - -H "Content-Type: application/json" \  
1258 - -H "X-Tenant-ID: 162" \  
1259 - -d '{  
1260 - "query": "*",  
1261 - "size": 0,  
1262 - "facets": [  
1263 - {"field": "category.keyword", "size": 100}  
1264 - ]  
1265 - }'  
1266 -```  
1267 -  
1268 -### 技巧 2:价格分布统计  
1269 -  
1270 -```bash  
1271 -curl -X POST "http://localhost:6002/search/" \  
1272 - -H "Content-Type: application/json" \  
1273 - -H "X-Tenant-ID: 162" \  
1274 - -d '{  
1275 - "query": "手机",  
1276 - "language": "zh",  
1277 - "size": 0,  
1278 - "facets": [  
1279 - {  
1280 - "field": "price",  
1281 - "type": "range",  
1282 - "ranges": [  
1283 - {"key": "0-50", "to": 50},  
1284 - {"key": "50-100", "from": 50, "to": 100},  
1285 - {"key": "100-200", "from": 100, "to": 200},  
1286 - {"key": "200-500", "from": 200, "to": 500},  
1287 - {"key": "500+", "from": 500}  
1288 - ]  
1289 - }  
1290 - ]  
1291 - }'  
1292 -```  
1293 -  
1294 -### 技巧 3:组合多种查询类型  
1295 -  
1296 -```bash  
1297 -# 布尔表达式 + 过滤器 + 分面 + 排序  
1298 -curl -X POST "http://localhost:6002/search/" \  
1299 - -H "Content-Type: application/json" \  
1300 - -H "X-Tenant-ID: 162" \  
1301 - -d '{  
1302 - "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子",  
1303 - "filters": {  
1304 - "category.keyword": ["玩具", "益智玩具"]  
1305 - },  
1306 - "range_filters": {  
1307 - "min_price": {"gte": 20, "lte": 100},  
1308 - "days_since_last_update": {"lte": 30}  
1309 - },  
1310 - "facets": [  
1311 - {"field": "vendor.keyword", "size": 20}  
1312 - ],  
1313 - "sort_by": "min_price",  
1314 - "sort_order": "asc",  
1315 - "size": 20  
1316 - }'  
1317 -```  
1318 -  
1319 ----  
1320 -  
1321 -## 测试数据  
1322 -  
1323 -如果你需要测试数据,可以使用以下查询:  
1324 -  
1325 -```bash  
1326 -# 测试类目:玩具  
1327 -curl -X POST "http://localhost:6002/search/" \  
1328 - -H "Content-Type: application/json" \  
1329 - -H "X-Tenant-ID: 162" \  
1330 - -d '{"query": "玩具", "size": 5}'  
1331 -  
1332 -# 测试品牌:乐高  
1333 -curl -X POST "http://localhost:6002/search/" \  
1334 - -H "Content-Type: application/json" \  
1335 - -H "X-Tenant-ID: 162" \  
1336 - -d '{"query": "brand:乐高", "size": 5}'  
1337 -  
1338 -# 测试布尔表达式  
1339 -curl -X POST "http://localhost:6002/search/" \  
1340 - -H "Content-Type: application/json" \  
1341 - -H "X-Tenant-ID: 162" \  
1342 - -d '{"query": "玩具 AND 乐高", "size": 5}'  
1343 -```  
1344 -  
1345 ----  
1346 -  
1347 -**文档版本**: 3.0  
1348 -**最后更新**: 2024-11-12  
1349 -**相关文档**: `API_DOCUMENTATION.md`  
1350 -  
docs/TODO-ES能力提升.md
@@ -67,3 +67,4 @@ text_similarity_reranker 用 NLP 模型对 top-k ç»“æžœæŒ‰è¯­ä¹‰ç›¸ä¼¼åº¦é‡æ–°æ @@ -67,3 +67,4 @@ text_similarity_reranker 用 NLP 模型对 top-k ç»“æžœæŒ‰è¯­ä¹‰ç›¸ä¼¼åº¦é‡æ–°æ
67 67
68 68
69 69
  70 +
docs/TODO-prompts.md deleted
@@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
1 -  
2 -1. debug信息展示方面  
3 -在这次重构过程中,你基本上了解了从query析到 ES 搜索,再到重排的全流程。  
4 -在这些环节中,有哪些重要的信息对搜索结果产生较大影响,先回顾,适当的完善、优化日志。  
5 -debug_info也给前端更充分的调试信息,包括,丰富每条结果的ranking debug。目前只有这些  
6 -Ranking Debug  
7 -spu_id  
8 -ES score  
9 -ES normalized  
10 -ES norm (rerank input):  
11 -Rerank score  
12 -Fused score  
13 -title.en  
14 -title.zh  
15 -  
16 -还不够,需要详细思考,设计一个更加完整的展示的体系  
17 -比如:  
18 -position(重排前的,重排后最终的)  
19 -ES打分,从ES检索原始打分、相关的因子(minmax归一化的因子。到底用了哪些因子,你自己关注下,用到的要体现出来,让人知道从原始打分怎么得到的ES norm (rerank input))。另外ES normalized,ES norm,要梳理清楚,如果代码不清晰则改代码,展示不清晰则要梳理展示。  
20 -Rerank这一块,除了rerank_score,输入的信息比如输入的doc模板,被选中的sku及其用于辅助reranker相关性的、加到title后面的后缀  
21 -融合公式这一块,除了fused_score,还要包含rerank_factor, text_factor, knn_factor等因子,以及text_score, knn_score, text_source_score, text_translation_score, … 等变量,要清晰的展示。  
22 -证据 matched_queries  
23 -  
24 -  
25 -耗时记录方面:  
26 -1. Stage Timings: 为每个阶段耗时补充起止时间戳。  
27 -2, 我看到total_search大幅度大于上面各个Stage 的总和,可能漏了一些重要的stage,比如「款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage  
28 -  
29 -但是也要注意,不要影响不带debug_info的请求的性能。  
30 -  
31 -  
32 -2. 日志方面也需要完善  
33 -从 query 到 ES 再到重排,对结果影响大的信息,语种检测 + index_languages,Query 向量,ES查询的构建过程中所使用的关键的变量、关键的中间结果,rerank相关信息(intput/window,query/doc 模板,融合公式的详细信息和关键的因子、输入原始值以及中间变量、到最终的融合公式的因子,Page fill,sku_filter_dimension等  
34 -  
35 -  
36 -3. 测试用例方面  
37 -子串匹配(Direct 阶段)是否可能产生误判 — 与尺码/短词强相关  
38 -_is_direct_match 使用 normalized_value in query_text(见 sku_intent_selector.py)。  
39 -词表里 l、m、s、xl,会在常见英文 query 里产生字符级子串命中,例如"l" 是否会匹配 "large …" 里的 l,注意要用分词匹配,检查分词匹配逻辑是否容易产生badcase。  
docs/TODO-意图判断-2.md deleted
@@ -1,40 +0,0 @@ @@ -1,40 +0,0 @@
1 -  
2 -一、 增加款式意图识别模块  
3 -意图类型: 颜色,尺码(目前只需要支持这两种)  
4 -  
5 -  
6 -二、 意图判断  
7 -- 意图召回层:  
8 -每种意图,有一个召回词集合  
9 -对query(包括原始query、各种翻译query 都做匹配)  
10 -- 以颜色意图为例:  
11 -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词  
12 -query匹配了其中任何一个词,都认为,具有颜色意图  
13 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。  
14 -  
15 -  
16 -三、 意图使用:  
17 - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后  
18 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter  
19 - 要改为:  
20 - 1. 有款式意图的时候,才做sku筛选  
21 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选  
22 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。  
23 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用:  
24 - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url  
25 - 2. 用来做rerank doc的title补充,从而参与rerank  
26 - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀)  
27 -  
28 -- sku筛选的规则也要优化:  
29 -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。  
30 -改为:  
31 - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。  
32 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。  
33 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的  
34 - 这个sku筛选也需要提取为一个独立的模块。  
35 -  
36 -细节备注:  
37 -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name  
38 -  
39 -5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。)  
40 -  
docs/TODO-意图判断.md renamed to docs/TODO-意图判断-done.md
@@ -97,3 +97,172 @@ ption_value的匹é…。æ„图检测的时候,有匹é…çš„query中的命中的è @@ -97,3 +97,172 @@ ption_value的匹é…。æ„图检测的时候,有匹é…çš„query中的命中的è
97 验è¯è¿‡ï¼š 97 验è¯è¿‡ï¼š
98 - `pytest tests/test_search_rerank_window.py -q` 通过 98 - `pytest tests/test_search_rerank_window.py -q` 通过
99 - å˜æ›´æ–‡ä»¶ lint 无报错 99 - å˜æ›´æ–‡ä»¶ lint 无报错
  100 +
  101 +
  102 +------------------------------
  103 +
  104 +
  105 +
  106 +---
  107 +
  108 +## 1. 现状(与需求的差è·ï¼‰
  109 +
  110 +**æµæ°´çº¿**(`search/searcher.py`)大致是:
  111 +
  112 +1. `QueryParser.parse` → `ParsedQuery`ï¼ˆå« `translations`ã€`query_tokens` 等)
  113 +2. 组 ES æŸ¥è¯¢ï¼›è‹¥åœ¨é‡æŽ’çª—å£å†…,第一次查询把 `_source` è£æˆã€Œé‡æŽ’æ¨¡æ¿æ‰€éœ€å­—段ã€ï¼ˆ`_resolve_rerank_source_filter`)
  114 +3. ES æœç´¢ → `run_rerank`(`rerank_client.build_docs_from_hits` 用 `{title}` 等拼 doc)
  115 +4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当å‰é¡µ `_source` 补全
  116 +5. `_apply_sku_sorting_for_page_hits`(仅 **option1**,先å­ä¸²åŒ…å«å‘½ä¸­ç¬¬ä¸€ä¸ªï¼Œå¦åˆ™å…¨é‡ option1 embedding)
  117 +6. `ResultFormatter`(`sku_filter_dimension` åªåš**展示层**æŒ‰ç»´åº¦æŠ˜å  SKU,与置顶逻辑独立)
  118 +
  119 +
  120 +**与需求冲çªä½†å¿…须一起解决的一点**:page fill 会用 ES 拉回æ¥çš„ `_source`**整份覆盖**å½“å‰ hit(约 841–842 行)。若在 rerank **之å‰**åªæ”¹å†…存里的 `skus` 顺åº/`image_url`,**ä¸**在 fill åŽå†å¤„ç†ä¸€æ¬¡ï¼Œæœ€ç»ˆå“应会被覆盖掉。因此「rerank å‰å¯¹æ‰€æœ‰ window 内 hit åš SKU 决策ã€å’Œã€Œç”¨æˆ·çœ‹åˆ°çš„æœ€ç»ˆåˆ—表ã€ä¹‹é—´ï¼Œå¿…须有一æ¡**明确的数æ®å¥‘约**(è§ä¸‹æ–‡ §4)。
  121 +
  122 +---
  123 +
  124 +## 2. 模å—划分(建议:`intent` + `sku_intent` 两层)
  125 +
  126 +é¿å…继续在 `Searcher` 里堆方法,建议新建å°åŒ…,èŒè´£æ¸…æ™°ã€ç”± `Searcher` 编排。
  127 +
  128 +| æ¨¡å— | èŒè´£ |
  129 +|------|------|
  130 +| **`query/intent/`**(或 `search/intent/`ï¼ŒäºŒé€‰ä¸€ä»¥ã€Œç¦»è°æ›´è¿‘ã€ä¸ºå‡†ï¼›æ›´æŽ¨è **`query/intent`**,因为输入完全是 query 侧事实) | 加载è¯è¡¨ã€**æ„图å¬å›ž**ã€å¤š query å˜ä½“ + 粗细分è¯ã€è¾“出结构化 **`IntentProfile`** |
  131 +| **`search/sku_intent/`**(或 `intent/sku_selection.py`) | æ ¹æ® `IntentProfile` è§£æž **option1/2/3** 哪一维ã€ç”Ÿæˆæ¯ SKU çš„**åŒ¹é…æ–‡æœ¬**ã€ä¸‰è½®åŒ¹é…规则ã€embedding 批处ç†ã€å¯¹ `_source` åš **promote + image_url** |
  132 +| **`search/rerank_client.py`(薄扩展)** | 支æŒã€Œæ¯æ¡ hit çš„ doc 文本ã€ï¼šæ¨¡æ¿æ‰©å±•或 **显å¼ä¼ å…¥ per-hit 字符串列表**,é¿å…把业务塞进 format 字符串 |
  133 +
  134 +**`IntentProfile`(概念模型)建议包å«**:
  135 +
  136 +- `active_intents: Set[Literal["color","size"]]`ï¼ˆå¯æ‰©å±•)
  137 +- æ¯ç§æ„图:`canonical_terms`(命中行的标准è¯ï¼‰ã€`matched_surface_forms`(å¯é€‰ï¼Œç”¨äºŽ debug)
  138 +- **维度别å**:如 color → `{"color","颜色","colour",...}`(é…置或独立å°è¯è¡¨ï¼‰
  139 +- 原始用于匹é…çš„ token 集åˆï¼šæ¯ä¸ª query å˜ä½“ ×(细粒度 | ç²—ç²’åº¦ï¼‰ï¼Œä¾¿äºŽæ—¥å¿—ä¸Žå•æµ‹
  140 +
  141 +**è¯è¡¨**:
  142 +
  143 +- **æ„图å¬å›žè¡¨**:æ¯è¡Œé€—å·åˆ†éš”åŒä¹‰è¯ï¼Œé¦–è¯æ ‡å‡†åŒ–;颜色ã€å°ºç å„一份(路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径)。
  144 +- **SKU 第二轮「泛化ã€è¡¨**(对 **option å–值** åšåŒä¹‰æ‰©å±•):与æ„图å¬å›žè¡¨åˆ†å¼€ï¼Œé¿å…语义混在一起。
  145 +
  146 +---
  147 +
  148 +## 3. æ„图判断(与 `QueryParser` 的衔接)
  149 +
  150 +需求:对 **原始 query + å„类翻译** 都åšåŒ¹é…ï¼›**细粒度 + 粗粒度** 分è¯ã€‚
  151 +
  152 +现状:
  153 +
  154 +- `ParsedQuery` 里 **`query_tokens` åªå¯¹ rewritten åŽçš„ `query_text` 跑了一次 HanLP**(`query_parser.py` 269–274 行附近),**没有**对 `original_query`ã€å„ `translations` çš„ token 缓存。
  155 +- 已有 **`simple_tokenize_query`**(粗粒度)在 `query_parser.py`。
  156 +
  157 +**建议**:
  158 +
  159 +- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生æˆã€Œquery å˜ä½“列表ã€ï¼šè‡³å°‘åŒ…å« `original_query`ã€`query_normalized`ã€`rewritten_query`ã€`translations` çš„å€¼ï¼ˆä¸Žå½“å‰ `_build_sku_query_texts` æ€è·¯ä¸€è‡´ï¼Œä½†å‡çº§ä¸º**结构化**)。
  160 +- 细粒度:å¤ç”¨ `QueryParser._get_query_tokens`(需把该方法暴露为公开 API 或注入åŒä¸€ HanLP callable),对æ¯ä¸ªå˜ä½“字符串调用。
  161 +- 粗粒度:对æ¯ä¸ªå˜ä½“调用 `simple_tokenize_query`。
  162 +- 匹é…逻辑:**ä»»æ„å˜ä½“ × ä»»æ„粒度** çš„ token è½åœ¨ã€Œæ ‡å‡†åŒ– → åŒä¹‰è¯é—­åŒ…ã€ä¸Šå³è§†ä¸ºå‘½ä¸­è¯¥æ„图(与你æè¿°çš„行内åŒä¹‰ä¸€è‡´ï¼‰ã€‚
  163 +
  164 +**å¯é€‰ä¼˜åŒ–**:在 `parse()` 里顺带产出 `intent_profile`,å‡å°‘一次é历;但为控制 `QueryParser` 体积,更稳妥的是 **parse 之åŽ**å•独调 `IntentDetector`,ä¾èµ–清晰。
  165 +
  166 +---
  167 +
  168 +## 4. æµæ°´çº¿æ”¹é€ ï¼ˆä¸Ž page fill 的契约)
  169 +
  170 +目标顺åºå˜ä¸ºï¼š
  171 +
  172 +`ES(window)`(有æ„图时 `_source` å« `skus` + `option*_name`)
  173 +→ **对æ¯ä¸ª hit:SKU 决策 + ç”Ÿæˆ rerank 用åŽç¼€/全文**
  174 +→ `run_rerank`(doc = 标题 + åŽç¼€ï¼‰
  175 +→ 切片
  176 +→ page fill
  177 +→ **最终å“应å‰å†åº”用一次 SKU 决策(或与 prefetch 结果åˆå¹¶ï¼‰**
  178 +→ `ResultFormatter`
  179 +
  180 +**为何最åŽè¿˜è¦ä¸€æ¬¡ï¼Ÿ** 因为 page fill 会覆盖 `_source`,rerank å‰å†…存里的 `skus` 顺åºä¸èƒ½å½“作最终真相。
  181 +
  182 +**推è契约(é™ä½Žå¤æ‚度)**:
  183 +
  184 +1. **Rerank å‰**:对 window 内æ¯ä¸ª hit 计算 `SkuIntentDecision`(至少包å«ï¼š`option_slot` 1/2/3ã€`candidate_sku_index` 或 `sku_id`ã€`rerank_suffix` å­—ç¬¦ä¸²ï¼‰ã€‚å¯æŒ‚在 hit çš„**éž ES 字段**上,例如 `hit["_intent_sku"] = {...}`(或åªå­˜ `rerank_doc_text` 全文)。
  185 +2. **`run_rerank`**:`build_docs_from_hits` è‹¥å‘现 hit 上已有 `rerank_doc_text`(或 `style_suffix` + 模æ¿ï¼‰ï¼Œåˆ™ä¼˜å…ˆä½¿ç”¨ï¼Œå¦åˆ™èµ°åŽŸæ¨¡æ¿ã€‚
  186 +3. **Page fill 之åŽ**:对**当å‰é¡µ** hit å†è°ƒç”¨**åŒä¸€** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`ï¼ˆæˆ–æ ¹æ® `_id` åˆå¹¶ prefetch 决策)。这样最终 `image_url` / SKU 顺åºä¸Ž rerank 一致,且ä¸è¢« fill 冲掉。
  187 +
  188 +若担心算两次 embedding:**第一次**在 window å…¨é‡ä¸Šç®— query å‘é‡ + option å‘é‡ï¼›ç¬¬äºŒæ¬¡ä»…对当å‰é¡µä¸”å¯å¸¦ç¼“存(按 `embed_key` 去é‡ï¼‰ï¼Œä¸€èˆ¬é‡å¾ˆå°ã€‚
  189 +
  190 +**ä¸åœ¨é‡æŽ’窗å£å†…**:没有「rerank å‰å…¨ windowã€è¿™ä¸€æ­¥ï¼›å¯åœ¨ **ResultFormatter å‰**对当å‰é¡µ `es_hits` 用åŒä¸€ `SkuIntentSelector`(仅当有æ„图时),与「有æ„图æ‰åš SKU 筛选ã€ä¸€è‡´ã€‚
  191 +
  192 +---
  193 +
  194 +## 5. `_resolve_rerank_source_filter` 与 ES 字段
  195 +
  196 +需求:有æ„图时预å–éœ€åŒ…å« `skus`ã€`option1_name`ã€`option2_name`ã€`option3_name`。
  197 +
  198 +å»ºè®®ç­¾åæ‰©å±•为:
  199 +
  200 +`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])`
  201 +
  202 +- è‹¥ `intent_profile` éžç©ºä¸”å« color/size(或任æ„ã€Œæ¬¾å¼æ„图ã€ï¼‰ï¼Œåœ¨ `includes` 中**åˆå¹¶**上述字段(并与模æ¿è§£æžå‡ºçš„ `title` ç­‰å–并集)。
  203 +- 注æ„与全局 `source_fields` çš„ tri-state 语义(`_apply_source_filter`)是å¦å†²çªï¼šè‹¥ç§Ÿæˆ·é…ç½® `_source` 白åå•且ä¸å« `skus`,需定义优先级——**建议**ï¼šã€Œæ¬¾å¼æ„图所需字段ã€ä½œä¸º**最低ä¿è¯**åˆå¹¶è¿›æœ¬æ¬¡è¯·æ±‚çš„ fetch includes,或在文档中写明é™åˆ¶ã€‚
  204 +
  205 +---
  206 +
  207 +## 6. 多维度 option 与「未匹é…维度åã€
  208 +
  209 +需求逻辑å¯è½åˆ°çº¯å‡½æ•°ï¼š
  210 +
  211 +1. 对æ¯ä¸ªæ„图类型,有 **维度别å集åˆ**(如 color)。
  212 +2. 便¬¡ä¸Ž `option1_name`ã€`option2_name`ã€`option3_name`(字符串,注æ„多语言:与 indexer 一致,å¯èƒ½æ˜¯çº¯è‹±æ–‡æˆ–ä¸­æ–‡ï¼‰åš **casefold / 规范化** åŽåŒ¹é…别å表。
  213 +3. 命中则该 SKU 行的匹é…字段为 `option{k}_value`;用于 embedding key 时继续用 `name:value` å½¢å¼ï¼ˆæ²¿ç”¨çŽ°æœ‰ `_sku_option1_embedding_key` æ€è·¯ï¼Œæ³›åŒ–为 `option_slot`)。
  214 +4. **若三个 name 都ä¸åŒ¹é…æ„图维度**:用 `option1_value`ã€`option2_value`ã€`option3_value` **空格拼接**æˆä¸€æ¡ã€Œå…œåº•æè¿°å­—符串ã€ï¼Œä¾›ï¼š
  215 + - 与 query 的包å«/泛化/embedding 比较;
  216 + - 作为 `rerank_suffix` 的一部分(若你希望无明确维度时ä»åŠ å¼º rerank)。
  217 +
  218 +**多æ„å›¾åŒæ—¶å­˜åœ¨**ï¼ˆå¦‚åŒæ—¶é¢œè‰²+å°ºç ï¼‰ï¼šéœ€è¦åœ¨äº§å“层定规则,例如:
  219 +
  220 +- åªå¯¹ã€Œä¸»æ„å›¾ã€æŽ’åºï¼ˆé…置优先级 color > size),或
  221 +- è¦æ±‚两个维度都满足的 SKU 优先,å¦åˆ™é€€åŒ–ä¸ºå•æ„图。
  222 +
  223 +实现上å¯åœ¨ `SkuIntentSelector` 输入 `List[IntentType]` 与策略枚举,é¿å…写死 if-else æ•£è½ã€‚
  224 +
  225 +---
  226 +
  227 +## 7. 三轮 SKU 匹é…规则(独立模å—内)
  228 +
  229 +从当å‰ã€Œç¬¬ä¸€ä¸ªåŒ…å«å°±è¿”å›žã€æ”¹ä¸ºï¼š
  230 +
  231 +1. **第一轮**:统计「option åŒ¹é…æ–‡æœ¬è¢« **æ•´æ¡ query 文本** **包å«**ã€çš„ SKU(或对æ¯ä¸ª query å˜ä½“分别计,å†åˆå¹¶â€”—建议与你现有 `_build_sku_query_texts` 对é½ï¼‰ï¼›**è‹¥æ°å¥½ 1 个** → 选中。
  232 +2. **第二轮**:若 0 个,对æ¯ä¸ª SKU 的候选è¯èµ° **å–值泛化表**(åŒä¹‰è¯è¡Œï¼‰ï¼Œå†è·‘包å«åˆ¤æ–­ï¼›ä»ç»Ÿè®¡ã€Œå¤šä¸ª / 零个ã€ã€‚
  233 +3. **第三轮**:
  234 + - è‹¥ **多个** 满足包å«ï¼ˆç¬¬ä¸€è½®æˆ–第二轮)→ 仅在这多个上算 embedding,å–相似度最高;
  235 + - è‹¥ **ä» 0 个** → 对 **全部** SKU ç®— embeddingï¼Œå–æœ€é«˜ã€‚
  236 +
  237 +å®žçŽ°ä¸Šä¿æŒ **æ‰¹é‡ encode**ï¼ˆä¸Žå½“å‰ `option1_values_to_encode` 去é‡é€»è¾‘ç±»ä¼¼ï¼‰ï¼Œåªæ˜¯æŠŠã€Œembed_keyã€ä»Žå›ºå®š option1 改为按 slot 动æ€ç”Ÿæˆã€‚
  238 +
  239 +---
  240 +
  241 +## 8. `sku_filter_dimension`(API)与æ„图的关系
  242 +
  243 +- **`sku_filter_dimension`**:客户端指定「结果里 SKU 列表如何按维度折å ã€ï¼Œåœ¨ `ResultFormatter._filter_skus_by_dimensions` 中实现。
  244 +- **æ„图 SKU 置顶**:æœåŠ¡ç«¯æ ¹æ® query 推断维度与å–值,改顺åºä¸Žä¸»å›¾ã€‚
  245 +
  246 +建议约定:
  247 +
  248 +- **置顶 / æ¢å›¾**仅在æ„图开坿—¶æ‰§è¡Œï¼›
  249 +- **`sku_filter_dimension` ä»åªå½±å“返回 SKU æ¡æ•°ç»“æž„**;若与æ„图维度冲çªï¼ˆä¾‹å¦‚æ„图命中 colorï¼Œå®¢æˆ·ç«¯åªæŒ‰ size 折å ï¼‰ï¼Œåº”用**文档说明优先级**:常è§åšæ³•æ˜¯ **å…ˆæ„å›¾ç½®é¡¶ï¼Œå† filter**(或相å,需在 PRD 写清)。
  250 +
  251 +é¿å…在 `ResultFormatter` 里å†çŒœæ„图;æ„图结论由上游传入或在 Formatter å‰å·²å®Œæˆ `_source` 调整。
  252 +
  253 +---
  254 +
  255 +## 9. é…置与观测
  256 +
  257 +- `config.yaml`:`intent.enabled`ã€`intent.lexicon_paths`ã€`intent.dimension_aliases`(或按类型分å—)。
  258 +- `RequestContext` / `debug`:写入 `intent_profile`ã€`sku_intent_decision`ã€rerank 用的 doc 摘è¦ï¼Œä¾¿äºŽä¸Ž `docs/TODO-æ„图判断.md` 对é½ã€‚
  259 +
  260 +---
  261 +
  262 +## 10. å°ç»“
  263 +
  264 +- **核心架构**:**`IntentDetector`(query 侧)** + **`SkuIntentSelector`(search 侧)** + **`run_rerank` çš„ per-hit doc 覆盖** + **`_resolve_rerank_source_filter` æ¡ä»¶ includes**。
  265 +- **å¿…é¡»å¤„ç† page fill 覆盖 `_source`**:rerank å‰å†³ç­–与 **fill åŽå† apply 一次**(或等价åˆå¹¶ç­–略),å¦åˆ™ä¼šå‡ºçŽ°ã€Œé‡æŽ’ç”¨äº†å¸¦åŽç¼€çš„ docã€è¿”å›žç»“æžœå´æ˜¯æœªç½®é¡¶ SKUã€çš„ä¸ä¸€è‡´ã€‚
  266 +- **与现有系统èžåˆç‚¹**:`ParsedQuery` å˜ä½“列表ã€HanLP + `simple_tokenize_query`ã€`TextEmbeddingEncoder`ã€`ResultFormatter` / `sku_filter_dimension` 的边界清晰,é¿å…把æ„图逻辑å¤åˆ¶åˆ° `api/` 层。
  267 +
  268 +若你åŽç»­å¸Œæœ›æŠŠã€Œå¤šæ„å›¾ä¼˜å…ˆçº§ã€æˆ–「rerank åŽç¼€æ ¼å¼ã€å®šæˆå”¯ä¸€äº§å“规则,å¯ä»¥åœ¨å®žçްå‰å†™è¿›åŒä¸€ä»½ specï¼Œæ¨¡å—æŽ¥å£ä¼šå¾ˆå¥½ç¨³å®šä¸‹æ¥ã€‚
100 \ No newline at end of file 269 \ No newline at end of file
docs/Untitled deleted
@@ -1,38 +0,0 @@ @@ -1,38 +0,0 @@
1 -  
2 -一、 增加款式意图识别模块  
3 -意图类型: 颜色,尺码(目前只需要支持这两种)  
4 -  
5 -  
6 -二、 意图判断  
7 -- 意图召回层:  
8 -每种意图,有一个召回词集合  
9 -对query(包括原始query、各种翻译query 都做匹配)  
10 -- 以颜色意图为例:  
11 -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词  
12 -query匹配了其中任何一个词,都认为,具有颜色意图  
13 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。  
14 -  
15 -  
16 -三、 意图使用:  
17 - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后  
18 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter  
19 - 要改为:  
20 - 1. 有款式意图的时候,才做sku筛选  
21 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选  
22 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。  
23 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用:  
24 - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url  
25 - 2. 用来做rerank doc的title补充,从而参与rerank  
26 - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀)  
27 -  
28 -- sku筛选的规则也要优化:  
29 -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。  
30 -改为:  
31 - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。  
32 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。  
33 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的  
34 - 这个sku筛选也需要提取为一个独立的模块。  
35 -  
36 -细节备注:  
37 -intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。  
38 -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name  
docs/系统设计文档.md deleted
@@ -1,894 +0,0 @@ @@ -1,894 +0,0 @@
1 -# 搜索引擎通用化开发进度  
2 -  
3 -## 项目概述  
4 -  
5 -对后端搜索技术 做通用化。  
6 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。  
7 -  
8 -  
9 -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。  
10 -  
11 ----  
12 -  
13 -## 1. 原始数据层的约定  
14 -  
15 -### 1.1 店匠主表  
16 -  
17 -所有租户共用以下主表:  
18 -- `shoplazza_product_sku` - SKU级别商品数据  
19 -- `shoplazza_product_spu` - SPU级别商品数据  
20 -  
21 -### 1.2 索引结构(SPU维度)  
22 -  
23 -**索引架构**:  
24 -- 每个租户使用独立的 Elasticsearch 索引(索引名称由 `get_tenant_index_name(tenant_id)` 动态生成)  
25 -- 索引粒度:SPU级别(每个文档代表一个SPU)  
26 -- 数据隔离:通过“分索引 + `tenant_id` 字段”双重隔离(索引级 + 文档级)  
27 -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组  
28 -  
29 -**索引文档结构**:  
30 -```json  
31 -{  
32 - "tenant_id": "1",  
33 - "spu_id": "123",  
34 - "title.zh": "蓝牙耳机",  
35 - "title.en": "Bluetooth Headphones",  
36 - "brief.zh": "高品质蓝牙耳机",  
37 - "brief.en": "High-quality Bluetooth headphones",  
38 - "category_name": "电子产品",  
39 - "category_path.zh": "电子产品/音频设备/耳机",  
40 - "category_path.en": "Electronics/Audio/Headphones",  
41 - "category1_name": "电子产品",  
42 - "category2_name": "音频设备",  
43 - "category3_name": "耳机",  
44 - "vendor.zh": "品牌A",  
45 - "vendor.en": "Brand A",  
46 - "min_price": 199.99,  
47 - "max_price": 299.99,  
48 - "option1_name": "color",  
49 - "option2_name": "size",  
50 - "specifications": [  
51 - {  
52 - "sku_id": "456",  
53 - "name": "color",  
54 - "value": "black"  
55 - },  
56 - {  
57 - "sku_id": "456",  
58 - "name": "size",  
59 - "value": "large"  
60 - }  
61 - ],  
62 - "skus": [  
63 - {  
64 - "sku_id": "456",  
65 - "price": 199.99,  
66 - "compare_at_price": 249.99,  
67 - "sku_code": "SKU-123-1",  
68 - "stock": 50,  
69 - "weight": 0.2,  
70 - "weight_unit": "kg",  
71 - "option1_value": "black",  
72 - "option2_value": "large",  
73 - "option3_value": null,  
74 - "image_src": "https://example.com/image.jpg"  
75 - }  
76 - ],  
77 - "title_embedding": [0.1, 0.2, ...], // 1024维向量  
78 - "image_embedding": [  
79 - {  
80 - "vector": [0.1, 0.2, ...], // 1024维向量  
81 - "url": "https://example.com/image.jpg"  
82 - }  
83 - ]  
84 -}  
85 -```  
86 -  
87 -### 1.3 索引结构简化方案  
88 -  
89 -**简化原则**:  
90 -- **硬编码映射**:ES mapping 结构直接定义在 JSON 文件中(`mappings/search_products.json`),所有租户索引共享相同结构  
91 -- **分索引 + 公共结构**:每个租户拥有独立索引,但索引结构一致  
92 -- **数据源统一**:所有租户使用相同的 MySQL 表结构(店匠标准表)  
93 -- **查询配置集中**:查询相关配置(字段 boost、查询域等)集中在配置文件中管理  
94 -  
95 -**索引结构特点**:  
96 -1. **多语言字段**:所有文本字段支持中英文(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`)  
97 -2. **嵌套字段**:  
98 - - `skus`: SKU 嵌套数组(包含价格、库存、选项值等)  
99 - - `specifications`: 规格嵌套数组(包含 name、value、sku_id)  
100 - - `image_embedding`: 图片向量嵌套数组  
101 -3. **扁平化字段**:`sku_prices`, `sku_weights`, `total_inventory` 等用于过滤和排序  
102 -4. **向量字段**:`title_embedding`(1024维)用于语义搜索  
103 -  
104 -**实现文件**:  
105 -- `mappings/search_products.json` - ES mapping 定义(硬编码)  
106 -- `search/query_config.py` - 查询配置(硬编码)  
107 -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引  
108 -  
109 ----  
110 -  
111 -## 2. 索引结构实现  
112 -  
113 -### 2.1 硬编码映射方案  
114 -  
115 -**实现方式**:  
116 -- ES mapping 直接定义在 `mappings/search_products.json` 文件中  
117 -- 所有租户索引共享相同的 mapping 结构  
118 -- 查询配置集中在配置系统中(如 `config/config.yaml` 等)  
119 -  
120 -**索引字段结构**:  
121 -  
122 -#### 基础字段  
123 -- `tenant_id` (keyword): 租户ID,用于数据隔离  
124 -- `spu_id` (keyword): SPU唯一标识  
125 -- `create_time`, `update_time` (date): 创建和更新时间  
126 -  
127 -#### 多语言文本字段  
128 -- `title.zh/en` (text): 标题(中英文)  
129 -- `brief.zh/en` (text): 短描述(中英文)  
130 -- `description.zh/en` (text): 详细描述(中英文)  
131 -- `vendor.zh/en` (text): 供应商/品牌(中英文)  
132 -- `category_path.zh/en` (text): 类目路径(中英文)  
133 -- `category_name_text.zh/en` (text): 类目名称(中英文)  
134 -  
135 -**分析器配置**:  
136 -- 中文字段:`hanlp_index`(索引时)/ `hanlp_standard`(查询时)  
137 -- 英文字段:`english`  
138 -- `vendor` 字段包含 `keyword` 子字段(normalizer: lowercase)  
139 -  
140 -#### 分类字段  
141 -- `category_id` (keyword): 类目ID  
142 -- `category_name` (keyword): 类目名称  
143 -- `category_level` (integer): 类目层级  
144 -- `category1_name`, `category2_name`, `category3_name` (keyword): 多级类目名称  
145 -  
146 -#### 规格字段(Specifications)  
147 -- `specifications` (nested): 规格嵌套数组  
148 - - `sku_id` (keyword): SKU ID  
149 - - `name` (keyword): 规格名称(如 "color", "size")  
150 - - `value` (keyword): 规格值(如 "white", "256GB")  
151 -  
152 -**用途**:  
153 -- 支持按规格过滤:`{"specifications": {"name": "color", "value": "white"}}`  
154 -- 支持规格分面:`["specifications"]` 或 `["specifications.color"]`  
155 -  
156 -#### SKU嵌套字段  
157 -- `skus` (nested): SKU嵌套数组  
158 - - `sku_id`, `price`, `compare_at_price`, `sku_code`  
159 - - `stock`, `weight`, `weight_unit`  
160 - - `option1_value`, `option2_value`, `option3_value`  
161 - - `image_src` (index: false)  
162 -  
163 -#### 选项名称字段  
164 -- `option1_name`, `option2_name`, `option3_name` (keyword): 选项名称(如 "color", "size")  
165 -  
166 -#### 扁平化字段  
167 -- `min_price`, `max_price`, `compare_at_price` (float): 价格字段  
168 -- `sku_prices` (float[]): 所有SKU价格数组  
169 -- `sku_weights` (long[]): 所有SKU重量数组  
170 -- `total_inventory` (long): 总库存  
171 -  
172 -#### 向量字段  
173 -- `title_embedding` (dense_vector, 1024维): 标题向量,用于语义搜索  
174 -- `image_embedding` (nested): 图片向量数组  
175 - - `vector` (dense_vector, 1024维)  
176 - - `url` (text)  
177 -  
178 -**实现模块**:  
179 -- `mappings/search_products.json` - ES mapping 定义  
180 -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引  
181 -- `search/query_config.py` - 查询配置(字段 boost、查询域等)  
182 -  
183 -### 2.2 索引结构配置(查询域配置)  
184 -  
185 -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。  
186 -  
187 -**实现情况**:  
188 -  
189 -#### 域(Domain)配置  
190 -每个域定义了:  
191 -- 域名称(如 `default`, `title`, `category`, `brand`)  
192 -- 域标签(中文描述)  
193 -- 搜索字段列表  
194 -- 默认分析器  
195 -- 权重(boost)  
196 -- **多语言字段映射**(`language_field_mapping`)  
197 -  
198 -#### 多语言字段映射  
199 -  
200 -支持将不同语言的查询路由到对应的字段:  
201 -  
202 -```yaml  
203 -indexes:  
204 - - name: "default"  
205 - label: "默认索引"  
206 - fields:  
207 - - "name"  
208 - - "enSpuName"  
209 - - "ruSkuName"  
210 - - "categoryName"  
211 - - "brandName"  
212 - analyzer: "chinese_ecommerce"  
213 - boost: 1.0  
214 - language_field_mapping:  
215 - zh:  
216 - - "name"  
217 - - "categoryName"  
218 - - "brandName"  
219 - en:  
220 - - "enSpuName"  
221 - ru:  
222 - - "ruSkuName"  
223 -  
224 - - name: "title"  
225 - label: "标题索引"  
226 - fields:  
227 - - "name"  
228 - - "enSpuName"  
229 - - "ruSkuName"  
230 - analyzer: "chinese_ecommerce"  
231 - boost: 2.0  
232 - language_field_mapping:  
233 - zh:  
234 - - "name"  
235 - en:  
236 - - "enSpuName"  
237 - ru:  
238 - - "ruSkuName"  
239 -```  
240 -  
241 -**工作原理**:  
242 -1. 检测查询语言(中文、英文、俄文等)  
243 -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段  
244 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段  
245 -4. 组合多个语言查询的结果,提高召回率  
246 -  
247 -**实现模块**:  
248 -- `search/es_query_builder.py` - ES 查询构建器(单层架构)  
249 -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译)  
250 -- `search/query_config.py` - 查询配置(字段 boost、查询域等)  
251 -  
252 ----  
253 -  
254 -## 3. 数据导入流程  
255 -  
256 -### 3.1 数据源  
257 -  
258 -**店匠标准表**(Base配置使用):  
259 -- `shoplazza_product_spu` - SPU级别商品数据  
260 -- `shoplazza_product_sku` - SKU级别商品数据  
261 -  
262 -**其他客户表**(tenant1等):  
263 -- 使用各自的数据源表和扩展表  
264 -  
265 -### 3.2 数据导入方式  
266 -  
267 -**数据源统一**:  
268 -- 所有租户使用相同的MySQL表结构(店匠标准表)  
269 -- 数据转换逻辑写死在转换器代码中  
270 -- 索引结构硬编码,不依赖配置  
271 -  
272 -#### 数据导入流程(店匠通用)  
273 -  
274 -**脚本**:`scripts/ingest_shoplazza.py`  
275 -  
276 -**数据流程**:  
277 -1. **数据加载**:  
278 - - 从MySQL读取`shoplazza_product_spu`表(SPU数据)  
279 - - 从MySQL读取`shoplazza_product_sku`表(SKU数据)  
280 - - 从MySQL读取`shoplazza_product_option`表(选项定义)  
281 -  
282 -2. **数据转换**(`indexer/spu_transformer.py`):  
283 - - 按`spu_id`和`tenant_id`关联SPU和SKU数据  
284 - - **多语言字段映射**:  
285 - - MySQL的`title` → ES的`title.zh`(英文字段设为空)  
286 - - 其他文本字段类似处理  
287 - - **分类字段映射**:  
288 - - 从SPU表的`category_path`解析多级类目(`category1_name`, `category2_name`, `category3_name`)  
289 - - 映射`category_id`, `category_name`, `category_level`  
290 - - **规格字段构建**(`specifications`):  
291 - - 从`shoplazza_product_option`表获取选项名称(`name`)  
292 - - 从SKU的`option1/2/3`字段获取选项值(`value`)  
293 - - 构建嵌套数组:`[{"sku_id": "...", "name": "color", "value": "white"}, ...]`  
294 - - **选项名称映射**:  
295 - - 从`shoplazza_product_option`表获取`option1_name`, `option2_name`, `option3_name`  
296 - - **SKU嵌套数组构建**:  
297 - - 包含所有SKU字段(价格、库存、选项值、图片等)  
298 - - **扁平化字段计算**:  
299 - - `min_price`, `max_price`: 从所有SKU价格计算  
300 - - `sku_prices`: 所有SKU价格数组  
301 - - `total_inventory`: SKU库存总和  
302 - - 注入`tenant_id`字段  
303 -  
304 -3. **索引创建**:  
305 - - 从`mappings/search_products.json`加载ES mapping  
306 - - 创建或更新`search_products`索引  
307 -  
308 -4. **批量入库**:  
309 - - 批量写入ES(默认每批500条)  
310 - - 错误处理和重试机制  
311 -  
312 -**命令行工具**:  
313 -```bash  
314 -python scripts/ingest_shoplazza.py \  
315 - --db-host localhost \  
316 - --db-port 3306 \  
317 - --db-database saas \  
318 - --db-username root \  
319 - --db-password password \  
320 - --tenant-id "1" \  
321 - --config base \  
322 - --es-host http://localhost:9200 \  
323 - --recreate \  
324 - --batch-size 500  
325 -```  
326 -  
327 -#### 其他客户数据导入  
328 -  
329 -- 使用各自的数据转换器(如`indexer/data_transformer.py`)  
330 -- 数据源映射逻辑写死在各自的转换器中  
331 -- 共享`search_products`索引,通过`tenant_id`隔离  
332 -  
333 -**实现模块**:  
334 -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置)  
335 -- `indexer/data_transformer.py` - 通用数据转换器(其他客户)  
336 -- `indexer/bulk_indexer.py` - 批量索引器  
337 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本  
338 -  
339 ----  
340 -  
341 -## 4. QueryParser 实现  
342 -  
343 -  
344 -### 4.1 查询改写(Query Rewriting)  
345 -  
346 -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。  
347 -**实现情况**:  
348 -  
349 -#### 配置方式  
350 -在 `query_config.rewrite_dictionary` 中配置查询改写规则:  
351 -  
352 -```yaml  
353 -query_config:  
354 - enable_query_rewrite: true  
355 - rewrite_dictionary:  
356 - "芭比": "brand:芭比 OR name:芭比娃娃"  
357 - "玩具": "category:玩具"  
358 - "消防": "category:消防 OR name:消防"  
359 -```  
360 -  
361 -#### 功能特性  
362 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value  
363 -- **部分匹配**:查询包含词典 key 时,替换该部分  
364 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等)  
365 -  
366 -#### 实现模块  
367 -- `query/query_rewriter.py` - 查询改写器  
368 -- `query/query_parser.py` - 查询解析器(集成改写功能)  
369 -  
370 -### 4.2 翻译(Translation)  
371 -  
372 -**实现情况**:  
373 -  
374 -#### 配置方式(示意)  
375 -翻译能力通过统一的 provider 配置管理,支持多种后端实现(如本地服务或外部 API):  
376 -```yaml  
377 -query_config:  
378 - supported_languages:  
379 - - "zh"  
380 - - "en"  
381 - default_language: "en"  
382 - # 实际翻译 provider 与模型在通用 services 配置中定义  
383 -```  
384 -  
385 -实际代码中,翻译已改为统一的 translator service 架构:业务侧通过 `translation.create_translation_client()` 访问 6006,由 `translation/service.py` 在服务内按 `model + scene` 路由到具体 backend。scene 集合、语言码映射、LLM prompt 模板、本地模型方向约束等翻译域知识位于 `translation/` 内部,不再通过外部 provider 抽象分散管理。  
386 -  
387 -#### 功能特性  
388 -1. **语言检测**:自动检测查询语言  
389 -2. **智能翻译**:  
390 - - 将源语言 query 翻译到当前租户配置的索引语言集合  
391 -3. **域感知翻译**:  
392 - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言  
393 - - 避免不必要的翻译,提高效率  
394 -4. **翻译缓存**:缓存翻译结果,避免重复调用翻译后端  
395 -  
396 -#### 工作流程  
397 -```  
398 -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall))  
399 -```  
400 -  
401 -#### 实现模块  
402 -- `query/language_detector.py` - 语言检测器  
403 -- `query/translator.py` - 翻译 provider 抽象与调用  
404 -- `query/query_parser.py` - 查询解析器(集成翻译功能)  
405 -  
406 -### 4.3 文本向量化(Text Embedding)  
407 -  
408 -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。  
409 -  
410 -**实现情况**:  
411 -  
412 -#### 配置方式  
413 -```yaml  
414 -query_config:  
415 - enable_text_embedding: true  
416 -```  
417 -  
418 -#### 功能特性  
419 -1. **条件生成**:  
420 - - 仅当 `enable_text_embedding=true` 时生成向量  
421 - - 仅对 `default` 域查询生成向量  
422 -2. **向量模型**:使用可配置的文本向量模型(例如 1024 维通用语义 embedding),具体模型通过配置与 embedding 服务选择,不在文档中固定写死  
423 -3. **用途**:用于语义搜索(KNN 检索)  
424 -  
425 -#### 实现模块  
426 -- `embeddings/text_encoder.py` + `embeddings/server.py` - 文本向量服务客户端与服务端实现(支持可配置的模型后端)  
427 -- `query/query_parser.py` - 查询解析器(集成向量生成)  
428 -  
429 ----  
430 -  
431 -## 5. Searcher 实现  
432 -  
433 -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。  
434 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。  
435 -  
436 -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。  
437 -  
438 -### 5.1 布尔表达式解析  
439 -  
440 -**实现情况**:  
441 -  
442 -#### 支持的运算符  
443 -- **AND**:所有项必须匹配  
444 -- **OR**:任意项匹配  
445 -- **RANK**:排序增强(类似 OR 但影响排序)  
446 -- **ANDNOT**:排除(第一项匹配,第二项不匹配)  
447 -- **()**:括号分组  
448 -  
449 -#### 优先级(从高到低)  
450 -1. `()` - 括号  
451 -2. `ANDNOT` - 排除  
452 -3. `AND` - 与  
453 -4. `OR` - 或  
454 -5. `RANK` - 排序  
455 -  
456 -#### 示例  
457 -```  
458 -laptop AND (gaming OR professional) ANDNOT cheap  
459 -```  
460 -  
461 -#### 实现模块  
462 -- `search/boolean_parser.py` - 布尔表达式解析器  
463 -- `search/searcher.py` - 搜索器(集成布尔解析)  
464 -  
465 -### 5.2 多语言搜索  
466 -  
467 -**实现情况**:  
468 -  
469 -#### 工作原理  
470 -1. **查询解析**:  
471 - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`)  
472 - - 检测查询语言  
473 - - 生成翻译  
474 -2. **查询构建**(简化架构):  
475 - - **结构**: `filters AND (text_recall OR embedding_recall)`  
476 - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中)  
477 - - 普通字段过滤:`{"category_name": "手机"}`  
478 - - 范围过滤:`{"min_price": {"gte": 50, "lte": 200}}`  
479 - - **Specifications嵌套过滤**:  
480 - - 单个规格:`{"specifications": {"name": "color", "value": "white"}}`  
481 - - 多个规格:`{"specifications": [{"name": "color", "value": "white"}, {"name": "size", "value": "256GB"}]}`  
482 - - 过滤逻辑:不同维度(不同name)是AND关系,相同维度(相同name)的多个值是OR关系  
483 - - 使用ES的`nested`查询实现  
484 - - **text_recall**: 文本相关性召回  
485 - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`)  
486 - - 使用 `multi_match` 查询,支持字段 boost  
487 - - 中文字段使用中文分词器,英文字段使用英文分析器  
488 - - **embedding_recall**: 向量召回(KNN)  
489 - - 使用 `title_embedding` 字段进行 KNN 搜索  
490 - - ES 自动与文本召回合并(OR逻辑)  
491 - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等)  
492 -  
493 -#### 查询结构示例  
494 -```json  
495 -{  
496 - "query": {  
497 - "bool": {  
498 - "must": [  
499 - {  
500 - "function_score": {  
501 - "query": {  
502 - "multi_match": {  
503 - "query": "手机",  
504 - "fields": [  
505 - "title.zh^3.0", "title.en^3.0",  
506 - "brief.zh^1.5", "brief.en^1.5",  
507 - ...  
508 - ]  
509 - }  
510 - },  
511 - "functions": [...]  
512 - }  
513 - }  
514 - ],  
515 - "filter": [  
516 - {"term": {"tenant_id": "2"}},  
517 - {"term": {"category_name": "手机"}}  
518 - ]  
519 - }  
520 - },  
521 - "knn": {  
522 - "field": "title_embedding",  
523 - "query_vector": [...],  
524 - "k": 50,  
525 - "boost": 0.2  
526 - }  
527 -}  
528 -```  
529 -> **KNN 自适应策略**:`k`、`num_candidates`、`boost` 会根据 `query_tokens` 动态调整:短查询(≤2 token)减少召回和权重,长查询(≥5 token)增加召回和权重。详见 `docs/相关性检索优化说明.md` 3.6 节。  
530 -  
531 -#### 实现模块  
532 -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法)  
533 -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`)  
534 -  
535 -### 5.3 相关性计算(Ranking)  
536 -  
537 -**实现情况**:  
538 -  
539 -#### 当前实现  
540 -**公式**:`bm25() + 0.2 * text_embedding_relevance()`  
541 -  
542 -- **bm25()**:BM25 文本相关性得分  
543 - - 包括多语言打分  
544 - - 内部通过配置翻译为多种语言  
545 - - 分别到对应的字段搜索  
546 - - 中文字段使用中文分词器,英文字段使用英文分词器  
547 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分)  
548 - - 权重:0.2  
549 -  
550 -#### 配置方式  
551 -```yaml  
552 -ranking:  
553 - expression: "bm25() + 0.2*text_embedding_relevance()"  
554 - description: "BM25 text relevance combined with semantic embedding similarity"  
555 -```  
556 -  
557 -#### 扩展性  
558 -- 支持表达式配置(未来可扩展)  
559 -- 支持自定义函数(如 `timeliness()`, `field_value()`)  
560 -  
561 -#### 实现模块  
562 -- `search/ranking_engine.py` - 排序引擎  
563 -- `search/searcher.py` - 搜索器(集成排序功能)  
564 -  
565 ----  
566 -  
567 -## 6. 已完成功能总结  
568 -  
569 -### 6.1 配置系统  
570 -- ✅ 字段定义配置(类型、分析器、来源表/列)  
571 -- ✅ 索引域配置(多域查询、多语言映射)  
572 -- ✅ 查询配置(改写词典、翻译配置)  
573 -- ✅ 排序配置(表达式配置)  
574 -- ✅ 配置验证(字段存在性、类型检查、分析器匹配)  
575 -  
576 -### 6.2 数据索引  
577 -- ✅ 数据转换(字段映射、类型转换)  
578 -- ✅ 向量生成(文本向量、图片向量)  
579 -- ✅ 向量缓存(避免重复计算)  
580 -- ✅ 批量索引(错误处理、重试机制)  
581 -- ✅ ES mapping 自动生成  
582 -  
583 -### 6.3 查询处理  
584 -- ✅ 查询改写(词典配置)  
585 -- ✅ 语言检测  
586 -- ✅ 多语言翻译(DeepL API)  
587 -- ✅ 文本向量化(Qwen3-Embedding-0.6B)  
588 -- ✅ 域提取(支持 `domain:query` 语法)  
589 -  
590 -### 6.4 搜索功能  
591 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号)  
592 -- ✅ 多语言查询构建(同时搜索中英文字段)  
593 -- ✅ 语义搜索(KNN 检索)  
594 -- ✅ 相关性排序(BM25 + 向量相似度)  
595 -- ✅ 结果聚合(Faceted Search)  
596 -- ✅ Specifications嵌套过滤(单个和多个规格,按维度分组:不同维度AND,相同维度OR)  
597 -- ✅ Specifications嵌套分面(所有规格名称和指定规格名称)  
598 -- ✅ SKU筛选(按维度过滤,应用层实现)  
599 -  
600 -### 6.5 API 服务  
601 -- ✅ RESTful API(FastAPI)  
602 -- ✅ 搜索接口(文本搜索、图片搜索)  
603 -- ✅ 文档查询接口  
604 -- ✅ 前端界面(HTML + JavaScript)  
605 -- ✅ 租户隔离(tenant_id过滤)  
606 -  
607 -### 6.6 索引结构(店匠通用)  
608 -- ✅ SPU级别索引结构  
609 -- ✅ 多语言字段支持(中英文)  
610 -- ✅ 嵌套字段(skus, specifications, image_embedding)  
611 -- ✅ 规格字段(specifications)支持过滤和分面  
612 -- ✅ 扁平化字段(价格、库存等)用于过滤和排序  
613 -- ✅ 按租户分索引(索引名通过 `get_tenant_index_name` 生成),各租户索引结构一致  
614 -- ✅ 文档内仍包含 `tenant_id` 字段,可用于额外校验或跨索引场景  
615 -- ✅ 硬编码映射(mappings/search_products.json)  
616 -- ✅ 集中查询配置(config/config.yaml 等)  
617 -  
618 ----  
619 -  
620 -## 7. 技术栈  
621 -  
622 -- **后端**:Python 3.6+  
623 -- **搜索引擎**:Elasticsearch  
624 -- **数据库**:MySQL(Shoplazza)  
625 -- **向量服务**:可配置的文本/图像向量模型(通过 embedding 服务与配置选择具体模型)  
626 -- **翻译服务**:可配置的翻译 provider(通过 translation 服务与配置选择具体后端与模型)  
627 -- **API 框架**:FastAPI  
628 -- **前端**:HTML + JavaScript  
629 -  
630 ----  
631 -  
632 -## 8. API响应格式  
633 -  
634 -### 8.1 外部友好格式  
635 -  
636 -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式:  
637 -  
638 -**响应结构**:  
639 -```json  
640 -{  
641 - "results": [  
642 - {  
643 - "spu_id": "123",  
644 - "title": "蓝牙耳机",  
645 - "skus": [  
646 - {  
647 - "sku_id": "456",  
648 - "price": 199.99,  
649 - "sku": "SKU-123-1",  
650 - "stock": 50  
651 - }  
652 - ],  
653 - "relevance_score": 0.95  
654 - }  
655 - ],  
656 - "total": 10,  
657 - "facets": [...],  
658 - "suggestions": [],  
659 - "related_searches": []  
660 -}  
661 -```  
662 -  
663 -**主要变化**:  
664 -- 结构化结果(`SpuResult`和`SkuResult`)  
665 -- 嵌套skus数组  
666 -- 无ES内部字段  
667 -  
668 -### 8.2 租户隔离  
669 -  
670 -所有API请求必须提供`tenant_id`:  
671 -- 请求头:`X-Tenant-ID: 1`  
672 -- 或查询参数:`?tenant_id=1`  
673 -  
674 -搜索时自动添加`tenant_id`过滤,确保数据隔离。  
675 -  
676 -### 8.3 数据接口约定  
677 -  
678 -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。  
679 -  
680 -#### 8.3.1 数据流模式  
681 -  
682 -系统采用统一的数据流模式,确保数据在各层之间的一致性:  
683 -  
684 -**数据流转路径**:  
685 -```  
686 -API Request (JSON)  
687 - ↓  
688 -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等)  
689 - ↓  
690 -Searcher(透传)  
691 - ↓  
692 -ES Query Builder → model_dump() 转换为字典  
693 - ↓  
694 -ES Query (字典)  
695 - ↓  
696 -Elasticsearch  
697 -```  
698 -  
699 -#### 8.3.2 Facets 配置数据流  
700 -  
701 -**输入格式**:`List[FacetConfig]`  
702 -  
703 -**配置对象列表**:所有分面配置必须使用 FacetConfig 对象  
704 -```json  
705 -[  
706 - {  
707 - "field": "category1_name",  
708 - "size": 15,  
709 - "type": "terms"  
710 - },  
711 - {  
712 - "field": "specifications.color",  
713 - "size": 20,  
714 - "type": "terms"  
715 - },  
716 - {  
717 - "field": "min_price",  
718 - "type": "range",  
719 - "ranges": [  
720 - {"key": "0-50", "to": 50},  
721 - {"key": "50-100", "from": 50, "to": 100}  
722 - ]  
723 - }  
724 -]  
725 -```  
726 -  
727 -**Specifications 分面支持**:  
728 -- 所有规格名称:`field: "specifications"` - 返回所有 name 及其 value 列表  
729 -- 指定规格名称:`field: "specifications.color"` - 只返回指定 name 的 value 列表  
730 -  
731 -**数据流**:  
732 -1. API 层:接收 `List[FacetConfig]`,Pydantic 验证参数  
733 -2. Searcher 层:透传 FacetConfig 对象列表  
734 -3. ES Query Builder:解析 FacetConfig 对象  
735 - - 检测 `"specifications"` 或 `"specifications.{name}"` 格式  
736 - - 构建对应的嵌套聚合查询或普通聚合查询  
737 -4. 输出:转换为 ES 聚合查询(包括 specifications 嵌套聚合)  
738 -5. Result Formatter:格式化 ES 聚合结果,处理 specifications 嵌套结构  
739 -  
740 -#### 8.3.3 Range Filters 数据流  
741 -  
742 -**输入格式**:`Dict[str, RangeFilter]`  
743 -  
744 -**RangeFilter 模型**:  
745 -```python  
746 -class RangeFilter(BaseModel):  
747 - gte: Optional[Union[float, str]] # 大于等于  
748 - gt: Optional[Union[float, str]] # 大于  
749 - lte: Optional[Union[float, str]] # 小于等于  
750 - lt: Optional[Union[float, str]] # 小于  
751 -```  
752 -  
753 -**示例**:  
754 -```json  
755 -{  
756 - "min_price": {"gte": 50, "lte": 200},  
757 - "create_time": {"gte": "2023-01-01T00:00:00Z"}  
758 -}  
759 -```  
760 -  
761 -**数据流**:  
762 -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证  
763 -2. Searcher 层:透传 `Dict[str, RangeFilter]`  
764 -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典  
765 -4. 输出:ES range 查询(支持数值和日期)  
766 -  
767 -**特性**:  
768 -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt)  
769 -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式)  
770 -- 统一约定:所有范围过滤都使用 RangeFilter 模型  
771 -  
772 -#### 8.3.3.1 Specifications 过滤数据流  
773 -  
774 -**输入格式**:`Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]`  
775 -  
776 -**单个规格过滤**:  
777 -```json  
778 -{  
779 - "specifications": {  
780 - "name": "color",  
781 - "value": "white"  
782 - }  
783 -}  
784 -```  
785 -  
786 -**多个规格过滤(按维度分组)**:  
787 -```json  
788 -{  
789 - "specifications": [  
790 - {"name": "color", "value": "white"},  
791 - {"name": "size", "value": "256GB"}  
792 - ]  
793 -}  
794 -```  
795 -  
796 -**数据流**:  
797 -1. API 层:接收 `filters` 字典,检测 `specifications` 键  
798 -2. Searcher 层:透传 `filters` 字典  
799 -3. ES Query Builder:检测 `specifications` 键,构建ES `nested` 查询  
800 - - 单个规格:构建单个 `nested` 查询  
801 - - 多个规格:按 name 维度分组,相同维度内使用 `should` 组合(OR逻辑),不同维度之间使用 `must` 组合(AND逻辑)  
802 -4. 输出:ES nested 查询(`nested.path=specifications` + `bool.must=[term(name), term(value)]`)  
803 -  
804 -#### 8.3.4 响应 Facets 数据流  
805 -  
806 -**输出格式**:`List[FacetResult]`  
807 -  
808 -**FacetResult 模型**:  
809 -```python  
810 -class FacetResult(BaseModel):  
811 - field: str # 字段名  
812 - label: str # 显示标签  
813 - type: Literal["terms", "range"] # 分面类型  
814 - values: List[FacetValue] # 分面值列表  
815 - total_count: Optional[int] # 总文档数  
816 -```  
817 -  
818 -**数据流**:  
819 -1. ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合)  
820 -2. Result Formatter:格式化ES聚合结果  
821 - - 处理普通terms聚合  
822 - - 处理range聚合  
823 - - **处理specifications嵌套聚合**:  
824 - - 所有规格名称:解析 `by_name` 聚合结构  
825 - - 指定规格名称:解析 `filter_by_name` 聚合结构  
826 -3. Searcher 层:构建 `List[FacetResult]` 对象  
827 -4. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON)  
828 -  
829 -**优势**:  
830 -- 类型安全:使用 Pydantic 模型确保数据结构一致性  
831 -- 自动序列化:模型自动转换为 JSON,无需手动处理  
832 -- 统一约定:所有响应都使用标准化的 Pydantic 模型  
833 -  
834 -#### 8.3.5 SKU筛选数据流  
835 -  
836 -**输入格式**:`Optional[str]`  
837 -  
838 -**支持的维度值**:  
839 -- `option1`, `option2`, `option3`: 直接使用选项字段  
840 -- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配  
841 -  
842 -**示例**:  
843 -```json  
844 -{  
845 - "query": "手机",  
846 - "sku_filter_dimension": "color"  
847 -}  
848 -```  
849 -  
850 -**数据流**:  
851 -1. API 层:接收 `sku_filter_dimension` 字符串参数  
852 -2. Searcher 层:透传到 Result Formatter  
853 -3. Result Formatter:在格式化结果时,按指定维度对SKU进行分组  
854 - - 如果维度是 `option1/2/3`,直接使用对应的 `option1_value/2/3` 字段  
855 - - 如果维度是规格名称,通过 `option1_name/2/3` 匹配找到对应的 `option1_value/2/3`  
856 - - 每个分组选择第一个SKU返回  
857 -4. 输出:过滤后的SKU列表(每个维度值一个SKU)  
858 -  
859 -**工作原理**:  
860 -1. 系统从ES返回所有SKU(不改变ES查询,保持性能)  
861 -2. 在结果格式化阶段,按指定维度对SKU进行分组  
862 -3. 每个分组选择第一个SKU返回  
863 -4. 如果维度不匹配或未找到,返回所有SKU(不进行过滤)  
864 -  
865 -**性能说明**:  
866 -- ✅ **推荐方案**: 在应用层过滤(当前实现)  
867 - - ES查询简单,不需要nested查询和join  
868 - - 只对返回的结果(通常10-20个SPU)进行过滤,数据量小  
869 - - 实现简单,性能开销小  
870 -- ❌ **不推荐**: 在ES查询时过滤  
871 - - 需要nested查询和join,性能开销大  
872 - - 实现复杂  
873 - - 只对返回的结果需要过滤,不需要在ES层面过滤  
874 -  
875 -#### 8.3.6 统一约定的好处  
876 -  
877 -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证  
878 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误  
879 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型)  
880 -4. **易于维护**:修改数据结构只需更新模型定义  
881 -5. **数据验证**:自动验证输入数据,减少错误处理代码  
882 -  
883 -**实现模块**:  
884 -- `api/models.py` - 所有 Pydantic 模型定义(包括 `SearchRequest`, `FacetConfig`, `RangeFilter` 等)  
885 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型,包括specifications分面处理和SKU筛选)  
886 -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询,包括specifications过滤和分面)  
887 -  
888 -## 9. 索引结构文件  
889 -  
890 -**硬编码映射**(店匠通用):`mappings/search_products.json`  
891 -  
892 -**查询配置**(硬编码):`search/query_config.py`  
893 -  
894 ----  
docs/系统设计文档v1.md deleted
@@ -1,743 +0,0 @@ @@ -1,743 +0,0 @@
1 -# 搜索引擎通用化开发进度  
2 -  
3 -> 历史版本说明:本文件为 v1 归档文档,部分模型与实现细节(如 BGE-M3)已过时。当前以 `docs/系统设计文档.md` 与 `docs/QUICKSTART.md` 为准。  
4 -  
5 -## 项目概述  
6 -  
7 -对后端搜索技术 做通用化。  
8 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。  
9 -  
10 -  
11 -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。  
12 -  
13 ----  
14 -  
15 -## 1. 原始数据层的约定  
16 -  
17 -### 1.1 店匠主表  
18 -  
19 -所有租户共用以下主表:  
20 -- `shoplazza_product_sku` - SKU级别商品数据  
21 -- `shoplazza_product_spu` - SPU级别商品数据  
22 -  
23 -### 1.2 索引结构(SPU维度)  
24 -  
25 -**统一索引架构**:  
26 -- 所有客户共享同一个Elasticsearch索引:`search_products`  
27 -- 索引粒度:SPU级别(每个文档代表一个SPU)  
28 -- 数据隔离:通过`tenant_id`字段实现租户隔离  
29 -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组  
30 -  
31 -**索引文档结构**:  
32 -```json  
33 -{  
34 - "tenant_id": "1",  
35 - "spu_id": "123",  
36 - "title": "蓝牙耳机",  
37 - "skus": [  
38 - {  
39 - "sku_id": "456",  
40 - "title": "黑色",  
41 - "price": 199.99,  
42 - "sku": "SKU-123-1",  
43 - "stock": 50  
44 - }  
45 - ],  
46 - "min_price": 199.99,  
47 - "max_price": 299.99  
48 -}  
49 -```  
50 -  
51 -### 1.3 配置化方案  
52 -  
53 -**配置分离原则**:  
54 -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置  
55 -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定  
56 -- **数据导入流程**:写死的脚本,不依赖配置  
57 -  
58 -统一通过配置文件定义:  
59 -1. ES 字段定义(字段类型、分析器、boost等)  
60 -2. ES mapping 结构生成  
61 -3. 查询域配置(indexes)  
62 -4. 排序和打分配置(function_score)  
63 -  
64 -**注意**:配置中**不包含**以下内容:  
65 -- `mysql_config` - MySQL数据库配置  
66 -- `main_table` / `extension_table` - 数据表配置  
67 -- `source_table` / `source_column` - 字段数据源映射  
68 -  
69 ----  
70 -  
71 -## 2. 配置系统实现  
72 -  
73 -### 2.1 应用结构配置(字段定义)  
74 -  
75 -**配置文件位置**:`config/schema/{tenant_id}_config.yaml`  
76 -  
77 -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。  
78 -  
79 -**实现情况**:  
80 -  
81 -#### 字段类型支持  
82 -- **TEXT**:文本字段,支持多语言分析器  
83 -- **KEYWORD**:关键词字段,用于精确匹配和聚合  
84 -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度)  
85 -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度)  
86 -- **INT/LONG**:整数类型  
87 -- **FLOAT/DOUBLE**:浮点数类型  
88 -- **DATE**:日期类型  
89 -- **BOOLEAN**:布尔类型  
90 -  
91 -#### 分析器支持  
92 -- **chinese_ecommerce**:中文电商分词器(index_ik/query_ik)  
93 -- **english**:英文分析器  
94 -- **russian**:俄文分析器  
95 -- **arabic**:阿拉伯文分析器  
96 -- **spanish**:西班牙文分析器  
97 -- **japanese**:日文分析器  
98 -- **standard**:标准分析器  
99 -- **keyword**:关键词分析器  
100 -  
101 -#### 字段配置示例(Base配置)  
102 -  
103 -```yaml  
104 -fields:  
105 - # 租户隔离字段(必需)  
106 - - name: "tenant_id"  
107 - type: "KEYWORD"  
108 - required: true  
109 - index: true  
110 - store: true  
111 -  
112 - # 商品标识字段  
113 - - name: "spu_id"  
114 - type: "KEYWORD"  
115 - required: true  
116 - index: true  
117 - store: true  
118 -  
119 - # 文本搜索字段  
120 - - name: "title"  
121 - type: "TEXT"  
122 - analyzer: "chinese_ecommerce"  
123 - boost: 3.0  
124 - index: true  
125 - store: true  
126 -  
127 - - name: "seo_keywords"  
128 - type: "TEXT"  
129 - analyzer: "chinese_ecommerce"  
130 - boost: 2.0  
131 - index: true  
132 - store: true  
133 -  
134 - # 嵌套skus字段  
135 - - name: "skus"  
136 - type: "JSON"  
137 - nested: true  
138 - nested_properties:  
139 - sku_id:  
140 - type: "keyword"  
141 - price:  
142 - type: "float"  
143 - sku:  
144 - type: "keyword"  
145 -```  
146 -  
147 -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。  
148 -  
149 -**实现模块**:  
150 -- `config/config_loader.py` - 配置加载器  
151 -- `config/field_types.py` - 字段类型定义  
152 -- `indexer/mapping_generator.py` - ES mapping 生成器  
153 -- `indexer/data_transformer.py` - 数据转换器  
154 -  
155 -### 2.2 索引结构配置(查询域配置)  
156 -  
157 -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。  
158 -  
159 -**实现情况**:  
160 -  
161 -#### 域(Domain)配置  
162 -每个域定义了:  
163 -- 域名称(如 `default`, `title`, `category`, `brand`)  
164 -- 域标签(中文描述)  
165 -- 搜索字段列表  
166 -- 默认分析器  
167 -- 权重(boost)  
168 -- **多语言字段映射**(`language_field_mapping`)  
169 -  
170 -#### 多语言字段映射  
171 -  
172 -支持将不同语言的查询路由到对应的字段:  
173 -  
174 -```yaml  
175 -indexes:  
176 - - name: "default"  
177 - label: "默认索引"  
178 - fields:  
179 - - "name"  
180 - - "enSpuName"  
181 - - "ruSkuName"  
182 - - "categoryName"  
183 - - "brandName"  
184 - analyzer: "chinese_ecommerce"  
185 - boost: 1.0  
186 - language_field_mapping:  
187 - zh:  
188 - - "name"  
189 - - "categoryName"  
190 - - "brandName"  
191 - en:  
192 - - "enSpuName"  
193 - ru:  
194 - - "ruSkuName"  
195 -  
196 - - name: "title"  
197 - label: "标题索引"  
198 - fields:  
199 - - "name"  
200 - - "enSpuName"  
201 - - "ruSkuName"  
202 - analyzer: "chinese_ecommerce"  
203 - boost: 2.0  
204 - language_field_mapping:  
205 - zh:  
206 - - "name"  
207 - en:  
208 - - "enSpuName"  
209 - ru:  
210 - - "ruSkuName"  
211 -```  
212 -  
213 -**工作原理**:  
214 -1. 检测查询语言(中文、英文、俄文等)  
215 -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段  
216 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段  
217 -4. 组合多个语言查询的结果,提高召回率  
218 -  
219 -**实现模块**:  
220 -- `search/es_query_builder.py` - ES 查询构建器(单层架构)  
221 -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译)  
222 -  
223 ----  
224 -  
225 -## 3. 数据导入流程  
226 -  
227 -### 3.1 数据源  
228 -  
229 -**店匠标准表**(Base配置使用):  
230 -- `shoplazza_product_spu` - SPU级别商品数据  
231 -- `shoplazza_product_sku` - SKU级别商品数据  
232 -  
233 -**其他客户表**(tenant1等):  
234 -- 使用各自的数据源表和扩展表  
235 -  
236 -### 3.2 数据导入方式  
237 -  
238 -**Pipeline层决定数据源**:  
239 -- 数据导入流程是写死的脚本,不依赖配置  
240 -- 配置只关注ES搜索相关的内容  
241 -- 数据源映射逻辑写死在转换器代码中  
242 -  
243 -#### Base配置数据导入(店匠通用)  
244 -  
245 -**脚本**:`scripts/ingest_shoplazza.py`  
246 -  
247 -**数据流程**:  
248 -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表  
249 -2. **数据转换**(`indexer/spu_transformer.py`):  
250 - - 按`spu_id`和`tenant_id`关联SPU和SKU数据  
251 - - 将SKU数据聚合为嵌套的`skus`数组  
252 - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`)  
253 - - 字段映射(写死在代码中,不依赖配置)  
254 - - 注入`tenant_id`字段  
255 -3. **索引创建**:  
256 - - 根据配置生成ES mapping  
257 - - 创建或更新`search_products`索引  
258 -4. **批量入库**:  
259 - - 批量写入ES(默认每批500条)  
260 - - 错误处理和重试机制  
261 -  
262 -**命令行工具**:  
263 -```bash  
264 -python scripts/ingest_shoplazza.py \  
265 - --db-host localhost \  
266 - --db-port 3306 \  
267 - --db-database saas \  
268 - --db-username root \  
269 - --db-password password \  
270 - --tenant-id "1" \  
271 - --config base \  
272 - --es-host http://localhost:9200 \  
273 - --recreate \  
274 - --batch-size 500  
275 -```  
276 -  
277 -#### 其他客户数据导入  
278 -  
279 -- 使用各自的数据转换器(如`indexer/data_transformer.py`)  
280 -- 数据源映射逻辑写死在各自的转换器中  
281 -- 共享`search_products`索引,通过`tenant_id`隔离  
282 -  
283 -**实现模块**:  
284 -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置)  
285 -- `indexer/data_transformer.py` - 通用数据转换器(其他客户)  
286 -- `indexer/bulk_indexer.py` - 批量索引器  
287 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本  
288 -  
289 ----  
290 -  
291 -## 4. QueryParser 实现  
292 -  
293 -  
294 -### 4.1 查询改写(Query Rewriting)  
295 -  
296 -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。  
297 -**实现情况**:  
298 -  
299 -#### 配置方式  
300 -在 `query_config.rewrite_dictionary` 中配置查询改写规则:  
301 -  
302 -```yaml  
303 -query_config:  
304 - enable_query_rewrite: true  
305 - rewrite_dictionary:  
306 - "芭比": "brand:芭比 OR name:芭比娃娃"  
307 - "玩具": "category:玩具"  
308 - "消防": "category:消防 OR name:消防"  
309 -```  
310 -  
311 -#### 功能特性  
312 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value  
313 -- **部分匹配**:查询包含词典 key 时,替换该部分  
314 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等)  
315 -  
316 -#### 实现模块  
317 -- `query/query_rewriter.py` - 查询改写器  
318 -- `query/query_parser.py` - 查询解析器(集成改写功能)  
319 -  
320 -### 4.2 翻译(Translation)  
321 -  
322 -**实现情况**:  
323 -  
324 -#### 配置方式  
325 -```yaml  
326 -query_config:  
327 - supported_languages:  
328 - - "zh"  
329 - - "en"  
330 - - "ru"  
331 - default_language: "zh"  
332 - translation_service: "deepl"  
333 - translation_api_key: null # 通过环境变量设置  
334 -```  
335 -  
336 -#### 功能特性  
337 -1. **语言检测**:自动检测查询语言  
338 -2. **智能翻译**:  
339 - - 如果查询是中文,翻译为英文、俄文  
340 - - 如果查询是英文,翻译为中文、俄文  
341 - - 如果查询是其他语言,翻译为所有支持的语言  
342 -3. **域感知翻译**:  
343 - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言  
344 - - 避免不必要的翻译,提高效率  
345 -4. **翻译缓存**:缓存翻译结果,避免重复调用 API  
346 -  
347 -#### 工作流程  
348 -```  
349 -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall))  
350 -```  
351 -  
352 -#### 实现模块  
353 -- `query/language_detector.py` - 语言检测器  
354 -- `query/translator.py` - 翻译器(DeepL API)  
355 -- `query/query_parser.py` - 查询解析器(集成翻译功能)  
356 -  
357 -### 4.3 文本向量化(Text Embedding)  
358 -  
359 -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。  
360 -  
361 -**实现情况**:  
362 -  
363 -#### 配置方式  
364 -```yaml  
365 -query_config:  
366 - enable_text_embedding: true  
367 -```  
368 -  
369 -#### 功能特性  
370 -1. **条件生成**:  
371 - - 仅当 `enable_text_embedding=true` 时生成向量  
372 - - 仅对 `default` 域查询生成向量  
373 -2. **向量模型**:BGE-M3 模型(1024维向量)  
374 -3. **用途**:用于语义搜索(KNN 检索)  
375 -  
376 -#### 实现模块  
377 -- `embeddings/bge_encoder.py` - BGE 文本编码器  
378 -- `query/query_parser.py` - 查询解析器(集成向量生成)  
379 -  
380 ----  
381 -  
382 -## 5. Searcher 实现  
383 -  
384 -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。  
385 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。  
386 -  
387 -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。  
388 -  
389 -### 5.1 布尔表达式解析  
390 -  
391 -**实现情况**:  
392 -  
393 -#### 支持的运算符  
394 -- **AND**:所有项必须匹配  
395 -- **OR**:任意项匹配  
396 -- **RANK**:排序增强(类似 OR 但影响排序)  
397 -- **ANDNOT**:排除(第一项匹配,第二项不匹配)  
398 -- **()**:括号分组  
399 -  
400 -#### 优先级(从高到低)  
401 -1. `()` - 括号  
402 -2. `ANDNOT` - 排除  
403 -3. `AND` - 与  
404 -4. `OR` - 或  
405 -5. `RANK` - 排序  
406 -  
407 -#### 示例  
408 -```  
409 -laptop AND (gaming OR professional) ANDNOT cheap  
410 -```  
411 -  
412 -#### 实现模块  
413 -- `search/boolean_parser.py` - 布尔表达式解析器  
414 -- `search/searcher.py` - 搜索器(集成布尔解析)  
415 -  
416 -### 5.2 多语言搜索  
417 -  
418 -**实现情况**:  
419 -  
420 -#### 工作原理  
421 -1. **查询解析**:  
422 - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`)  
423 - - 检测查询语言  
424 - - 生成翻译  
425 -2. **查询构建**(简化架构):  
426 - - **结构**: `filters AND (text_recall OR embedding_recall)`  
427 - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中)  
428 - - **text_recall**: 文本相关性召回  
429 - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`)  
430 - - 使用 `multi_match` 查询,支持字段 boost  
431 - - **embedding_recall**: 向量召回(KNN)  
432 - - 使用 `title_embedding` 字段进行 KNN 搜索  
433 - - ES 自动与文本召回合并  
434 - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等)  
435 -  
436 -#### 查询结构示例  
437 -```json  
438 -{  
439 - "query": {  
440 - "bool": {  
441 - "must": [  
442 - {  
443 - "function_score": {  
444 - "query": {  
445 - "multi_match": {  
446 - "query": "手机",  
447 - "fields": [  
448 - "title.zh^3.0", "title.en^3.0",  
449 - "brief.zh^1.5", "brief.en^1.5",  
450 - ...  
451 - ]  
452 - }  
453 - },  
454 - "functions": [...]  
455 - }  
456 - }  
457 - ],  
458 - "filter": [  
459 - {"term": {"tenant_id": "2"}},  
460 - {"term": {"category_name": "手机"}}  
461 - ]  
462 - }  
463 - },  
464 - "knn": {  
465 - "field": "title_embedding",  
466 - "query_vector": [...],  
467 - "k": 50,  
468 - "boost": 0.2  
469 - }  
470 -}  
471 -```  
472 -  
473 -#### 实现模块  
474 -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法)  
475 -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`)  
476 -  
477 -### 5.3 相关性计算(Ranking)  
478 -  
479 -**实现情况**:  
480 -  
481 -#### 当前实现  
482 -**公式**:`bm25() + 0.2 * text_embedding_relevance()`  
483 -  
484 -- **bm25()**:BM25 文本相关性得分  
485 - - 包括多语言打分  
486 - - 内部通过配置翻译为多种语言  
487 - - 分别到对应的字段搜索  
488 - - 中文字段使用中文分词器,英文字段使用英文分词器  
489 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分)  
490 - - 权重:0.2  
491 -  
492 -#### 配置方式  
493 -```yaml  
494 -ranking:  
495 - expression: "bm25() + 0.2*text_embedding_relevance()"  
496 - description: "BM25 text relevance combined with semantic embedding similarity"  
497 -```  
498 -  
499 -#### 扩展性  
500 -- 支持表达式配置(未来可扩展)  
501 -- 支持自定义函数(如 `timeliness()`, `field_value()`)  
502 -  
503 -#### 实现模块  
504 -- `search/ranking_engine.py` - 排序引擎  
505 -- `search/searcher.py` - 搜索器(集成排序功能)  
506 -  
507 ----  
508 -  
509 -## 6. 已完成功能总结  
510 -  
511 -### 6.1 配置系统  
512 -- ✅ 字段定义配置(类型、分析器、来源表/列)  
513 -- ✅ 索引域配置(多域查询、多语言映射)  
514 -- ✅ 查询配置(改写词典、翻译配置)  
515 -- ✅ 排序配置(表达式配置)  
516 -- ✅ 配置验证(字段存在性、类型检查、分析器匹配)  
517 -  
518 -### 6.2 数据索引  
519 -- ✅ 数据转换(字段映射、类型转换)  
520 -- ✅ 向量生成(文本向量、图片向量)  
521 -- ✅ 向量缓存(避免重复计算)  
522 -- ✅ 批量索引(错误处理、重试机制)  
523 -- ✅ ES mapping 自动生成  
524 -  
525 -### 6.3 查询处理  
526 -- ✅ 查询改写(词典配置)  
527 -- ✅ 语言检测  
528 -- ✅ 多语言翻译(DeepL API)  
529 -- ✅ 文本向量化(BGE-M3)  
530 -- ✅ 域提取(支持 `domain:query` 语法)  
531 -  
532 -### 6.4 搜索功能  
533 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号)  
534 -- ✅ 多语言查询构建(语言路由、字段映射)  
535 -- ✅ 语义搜索(KNN 检索)  
536 -- ✅ 相关性排序(BM25 + 向量相似度)  
537 -- ✅ 结果聚合(Faceted Search)  
538 -  
539 -### 6.5 API 服务  
540 -- ✅ RESTful API(FastAPI)  
541 -- ✅ 搜索接口(文本搜索、图片搜索)  
542 -- ✅ 文档查询接口  
543 -- ✅ 前端界面(HTML + JavaScript)  
544 -- ✅ 租户隔离(tenant_id过滤)  
545 -  
546 -### 6.6 Base配置(店匠通用)  
547 -- ✅ SPU级别索引结构  
548 -- ✅ 嵌套skus字段  
549 -- ✅ 统一索引(search_products)  
550 -- ✅ 租户隔离(tenant_id)  
551 -- ✅ 配置简化(移除MySQL相关配置)  
552 -  
553 ----  
554 -  
555 -## 7. 技术栈  
556 -  
557 -- **后端**:Python 3.6+  
558 -- **搜索引擎**:Elasticsearch  
559 -- **数据库**:MySQL(Shoplazza)  
560 -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片)  
561 -- **翻译服务**:DeepL API  
562 -- **API 框架**:FastAPI  
563 -- **前端**:HTML + JavaScript  
564 -  
565 ----  
566 -  
567 -## 8. API响应格式  
568 -  
569 -### 8.1 外部友好格式  
570 -  
571 -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式:  
572 -  
573 -**响应结构**:  
574 -```json  
575 -{  
576 - "results": [  
577 - {  
578 - "spu_id": "123",  
579 - "title": "蓝牙耳机",  
580 - "skus": [  
581 - {  
582 - "sku_id": "456",  
583 - "price": 199.99,  
584 - "sku": "SKU-123-1",  
585 - "stock": 50  
586 - }  
587 - ],  
588 - "relevance_score": 0.95  
589 - }  
590 - ],  
591 - "total": 10,  
592 - "facets": [...],  
593 - "suggestions": [],  
594 - "related_searches": []  
595 -}  
596 -```  
597 -  
598 -**主要变化**:  
599 -- 结构化结果(`SpuResult`和`SkuResult`)  
600 -- 嵌套skus数组  
601 -- 无ES内部字段  
602 -  
603 -### 8.2 租户隔离  
604 -  
605 -所有API请求必须提供`tenant_id`:  
606 -- 请求头:`X-Tenant-ID: 1`  
607 -- 或查询参数:`?tenant_id=1`  
608 -  
609 -搜索时自动添加`tenant_id`过滤,确保数据隔离。  
610 -  
611 -### 8.3 数据接口约定  
612 -  
613 -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。  
614 -  
615 -#### 8.3.1 数据流模式  
616 -  
617 -系统采用统一的数据流模式,确保数据在各层之间的一致性:  
618 -  
619 -**数据流转路径**:  
620 -```  
621 -API Request (JSON)  
622 - ↓  
623 -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等)  
624 - ↓  
625 -Searcher(透传)  
626 - ↓  
627 -ES Query Builder → model_dump() 转换为字典  
628 - ↓  
629 -ES Query (字典)  
630 - ↓  
631 -Elasticsearch  
632 -```  
633 -  
634 -#### 8.3.2 Facets 配置数据流  
635 -  
636 -**输入格式**:`List[Union[str, FacetConfig]]`  
637 -  
638 -- **简单模式**:字符串列表(字段名),使用默认配置  
639 - ```json  
640 - ["category.keyword", "vendor.keyword"]  
641 - ```  
642 -  
643 -- **高级模式**:FacetConfig 对象列表,支持自定义配置  
644 - ```json  
645 - [  
646 - {  
647 - "field": "category.keyword",  
648 - "size": 15,  
649 - "type": "terms"  
650 - },  
651 - {  
652 - "field": "price",  
653 - "type": "range",  
654 - "ranges": [  
655 - {"key": "0-50", "to": 50},  
656 - {"key": "50-100", "from": 50, "to": 100}  
657 - ]  
658 - }  
659 - ]  
660 - ```  
661 -  
662 -**数据流**:  
663 -1. API 层:接收 `List[Union[str, FacetConfig]]`  
664 -2. Searcher 层:透传,不做转换  
665 -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式  
666 -4. 输出:转换为 ES 聚合查询  
667 -  
668 -#### 8.3.3 Range Filters 数据流  
669 -  
670 -**输入格式**:`Dict[str, RangeFilter]`  
671 -  
672 -**RangeFilter 模型**:  
673 -```python  
674 -class RangeFilter(BaseModel):  
675 - gte: Optional[Union[float, str]] # 大于等于  
676 - gt: Optional[Union[float, str]] # 大于  
677 - lte: Optional[Union[float, str]] # 小于等于  
678 - lt: Optional[Union[float, str]] # 小于  
679 -```  
680 -  
681 -**示例**:  
682 -```json  
683 -{  
684 - "price": {"gte": 50, "lte": 200},  
685 - "created_at": {"gte": "2023-01-01T00:00:00Z"}  
686 -}  
687 -```  
688 -  
689 -**数据流**:  
690 -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证  
691 -2. Searcher 层:透传 `Dict[str, RangeFilter]`  
692 -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典  
693 -4. 输出:ES range 查询(支持数值和日期)  
694 -  
695 -**特性**:  
696 -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt)  
697 -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式)  
698 -- 统一约定:所有范围过滤都使用 RangeFilter 模型  
699 -  
700 -#### 8.3.4 响应 Facets 数据流  
701 -  
702 -**输出格式**:`List[FacetResult]`  
703 -  
704 -**FacetResult 模型**:  
705 -```python  
706 -class FacetResult(BaseModel):  
707 - field: str # 字段名  
708 - label: str # 显示标签  
709 - type: Literal["terms", "range"] # 分面类型  
710 - values: List[FacetValue] # 分面值列表  
711 - total_count: Optional[int] # 总文档数  
712 -```  
713 -  
714 -**数据流**:  
715 -1. ES Response:返回聚合结果(字典格式)  
716 -2. Searcher 层:构建 `List[FacetResult]` 对象  
717 -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON)  
718 -  
719 -**优势**:  
720 -- 类型安全:使用 Pydantic 模型确保数据结构一致性  
721 -- 自动序列化:模型自动转换为 JSON,无需手动处理  
722 -- 统一约定:所有响应都使用标准化的 Pydantic 模型  
723 -  
724 -#### 8.3.5 统一约定的好处  
725 -  
726 -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证  
727 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误  
728 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型)  
729 -4. **易于维护**:修改数据结构只需更新模型定义  
730 -5. **数据验证**:自动验证输入数据,减少错误处理代码  
731 -  
732 -**实现模块**:  
733 -- `api/models.py` - 所有 Pydantic 模型定义  
734 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型)  
735 -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询)  
736 -  
737 -## 9. 配置文件示例  
738 -  
739 -**Base配置**(店匠通用):`config/schema/base/config.yaml`  
740 -  
741 -**其他客户配置**:`config/schema/tenant1/config.yaml`  
742 -  
743 ----  
@@ -38,13 +38,7 @@ def example_basic_usage(): @@ -38,13 +38,7 @@ def example_basic_usage():
38 translations={"en": "red dress"} 38 translations={"en": "red dress"}
39 ) 39 )
40 40
41 - # 步骤2: 布尔解析  
42 - if not context.query_analysis.is_simple_query:  
43 - context.start_stage(RequestContextStage.BOOLEAN_PARSING)  
44 - time.sleep(0.02)  
45 - context.end_stage(RequestContextStage.BOOLEAN_PARSING)  
46 -  
47 - # 步骤3: ES查询构建 41 + # 步骤2: ES查询构建
48 context.start_stage(RequestContextStage.QUERY_BUILDING) 42 context.start_stage(RequestContextStage.QUERY_BUILDING)
49 time.sleep(0.03) 43 time.sleep(0.03)
50 context.end_stage(RequestContextStage.QUERY_BUILDING) 44 context.end_stage(RequestContextStage.QUERY_BUILDING)
@@ -53,7 +47,7 @@ def example_basic_usage(): @@ -53,7 +47,7 @@ def example_basic_usage():
53 "size": 10 47 "size": 10
54 }) 48 })
55 49
56 - # 步骤4: ES搜索 50 + # 步骤3: ES搜索
57 context.start_stage(RequestContextStage.ELASTICSEARCH_SEARCH) 51 context.start_stage(RequestContextStage.ELASTICSEARCH_SEARCH)
58 time.sleep(0.1) # 模拟ES响应时间 52 time.sleep(0.1) # 模拟ES响应时间
59 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH) 53 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH)
@@ -62,7 +56,7 @@ def example_basic_usage(): @@ -62,7 +56,7 @@ def example_basic_usage():
62 "took": 45 56 "took": 45
63 }) 57 })
64 58
65 - # 步骤5: 结果处理 59 + # 步骤4: 结果处理
66 context.start_stage(RequestContextStage.RESULT_PROCESSING) 60 context.start_stage(RequestContextStage.RESULT_PROCESSING)
67 time.sleep(0.02) 61 time.sleep(0.02)
68 context.end_stage(RequestContextStage.RESULT_PROCESSING) 62 context.end_stage(RequestContextStage.RESULT_PROCESSING)
frontend/static/js/app.js
@@ -950,7 +950,6 @@ function displayDebugInfo(data) { @@ -950,7 +950,6 @@ function displayDebugInfo(data) {
950 html += `<div>detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}</div>`; 950 html += `<div>detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}</div>`;
951 html += `<div>index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}</div>`; 951 html += `<div>index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}</div>`;
952 html += `<div>query_tokens: ${escapeHtml((debugInfo.query_analysis.query_tokens || []).join(', ') || 'N/A')}</div>`; 952 html += `<div>query_tokens: ${escapeHtml((debugInfo.query_analysis.query_tokens || []).join(', ') || 'N/A')}</div>`;
953 - html += `<div>is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}</div>`;  
954 953
955 if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { 954 if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) {
956 html += '<div>translations: '; 955 html += '<div>translations: ';
docs/reranker-共享前缀批量推理.md renamed to reranker/reranker-共享前缀批量推理.md
search/searcher.py
@@ -396,7 +396,6 @@ class Searcher: @@ -396,7 +396,6 @@ class Searcher:
396 detected_language=parsed_query.detected_language, 396 detected_language=parsed_query.detected_language,
397 translations=parsed_query.translations, 397 translations=parsed_query.translations,
398 query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None, 398 query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None,
399 - is_simple_query=True  
400 ) 399 )
401 context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query) 400 context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query)
402 401
@@ -865,7 +864,6 @@ class Searcher: @@ -865,7 +864,6 @@ class Searcher:
865 "translations": context.query_analysis.translations, 864 "translations": context.query_analysis.translations,
866 "has_vector": context.query_analysis.query_vector is not None, 865 "has_vector": context.query_analysis.query_vector is not None,
867 "query_tokens": getattr(parsed_query, "query_tokens", []), 866 "query_tokens": getattr(parsed_query, "query_tokens", []),
868 - "is_simple_query": context.query_analysis.is_simple_query,  
869 "intent_detection": context.get_intermediate_result("style_intent_profile"), 867 "intent_detection": context.get_intermediate_result("style_intent_profile"),
870 }, 868 },
871 "es_query": context.get_intermediate_result('es_query', {}), 869 "es_query": context.get_intermediate_result('es_query', {}),