Commit 93be98cb287bd879e5e7e6c495e3d505154d5053

Authored by tangwang
1 parent 7a013ca7

清理过时的文档

config/config.yaml
... ... @@ -278,7 +278,7 @@ services:
278 278 glossary_id: ""
279 279 use_cache: true
280 280 nllb-200-distilled-600m:
281   - enabled: false
  281 + enabled: true
282 282 backend: "local_nllb"
283 283 model_id: "facebook/nllb-200-distilled-600M"
284 284 model_dir: "./models/translation/facebook/nllb-200-distilled-600M"
... ...
context/request_context.py
... ... @@ -41,7 +41,6 @@ class QueryAnalysisResult:
41 41 translations: Dict[str, str] = field(default_factory=dict)
42 42 query_vector: Optional[List[float]] = None
43 43 boolean_ast: Optional[str] = None
44   - is_simple_query: bool = True
45 44  
46 45  
47 46 @dataclass
... ... @@ -280,7 +279,6 @@ class RequestContext:
280 279 'query_normalized': self.query_analysis.query_normalized,
281 280 'rewritten_query': self.query_analysis.rewritten_query,
282 281 'detected_language': self.query_analysis.detected_language,
283   - 'is_simple_query': self.query_analysis.is_simple_query
284 282 },
285 283 'performance': {
286 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   -# 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 67  
68 68  
69 69  
  70 +
... ...
docs/TODO-prompts.md deleted
... ... @@ -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   -
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 97 验è¯è¿‡ï¼š
98 98 - `pytest tests/test_search_rerank_window.py -q` 通过
99 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 269 \ No newline at end of file
... ...
docs/Untitled deleted
... ... @@ -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   -# 搜索引擎通用化开发进度
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   -# 搜索引擎通用化开发进度
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   ----
example_usage.py
... ... @@ -38,13 +38,7 @@ def example_basic_usage():
38 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 42 context.start_stage(RequestContextStage.QUERY_BUILDING)
49 43 time.sleep(0.03)
50 44 context.end_stage(RequestContextStage.QUERY_BUILDING)
... ... @@ -53,7 +47,7 @@ def example_basic_usage():
53 47 "size": 10
54 48 })
55 49  
56   - # 步骤4: ES搜索
  50 + # 步骤3: ES搜索
57 51 context.start_stage(RequestContextStage.ELASTICSEARCH_SEARCH)
58 52 time.sleep(0.1) # 模拟ES响应时间
59 53 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH)
... ... @@ -62,7 +56,7 @@ def example_basic_usage():
62 56 "took": 45
63 57 })
64 58  
65   - # 步骤5: 结果处理
  59 + # 步骤4: 结果处理
66 60 context.start_stage(RequestContextStage.RESULT_PROCESSING)
67 61 time.sleep(0.02)
68 62 context.end_stage(RequestContextStage.RESULT_PROCESSING)
... ...
frontend/static/js/app.js
... ... @@ -950,7 +950,6 @@ function displayDebugInfo(data) {
950 950 html += `<div>detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}</div>`;
951 951 html += `<div>index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}</div>`;
952 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 954 if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) {
956 955 html += '<div>translations: ';
... ...
docs/reranker-共享前缀批量推理.md renamed to reranker/reranker-共享前缀批量推理.md
search/searcher.py
... ... @@ -396,7 +396,6 @@ class Searcher:
396 396 detected_language=parsed_query.detected_language,
397 397 translations=parsed_query.translations,
398 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 400 context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query)
402 401  
... ... @@ -865,7 +864,6 @@ class Searcher:
865 864 "translations": context.query_analysis.translations,
866 865 "has_vector": context.query_analysis.query_vector is not None,
867 866 "query_tokens": getattr(parsed_query, "query_tokens", []),
868   - "is_simple_query": context.query_analysis.is_simple_query,
869 867 "intent_detection": context.get_intermediate_result("style_intent_profile"),
870 868 },
871 869 "es_query": context.get_intermediate_result('es_query', {}),
... ...