Commit ca91352aa97b0a95479a570f0278d840f5709b20

Authored by tangwang
1 parent f7d3cf70

更新文档

1. 搜索API对接指南.md
在“精确匹配过滤器”部分添加了 specifications 嵌套过滤说明
支持单个规格过滤和多个规格过滤(OR 逻辑)
在“分面配置”部分完善了 specifications 分面说明
添加了两种分面模式:所有规格名称和指定规格名称
在“常见场景示例”部分添加了场景5-8,包含规格过滤和分面的完整示例
2. 搜索API速查表.md
在“精确匹配过滤”部分添加了 specifications 过滤的快速参考
在“分面搜索”部分添加了 specifications 分面的快速参考
更新了完整示例,包含 specifications 的使用
3. Search-API-Examples.md
在“过滤器使用”部分添加了示例4-6,展示 specifications 过滤
在“分面搜索”部分添加了示例2-3,展示 specifications 分面
更新了 Python 和 JavaScript 完整示例,包含 specifications 的使用
在“常见使用场景”部分添加了场景2.1,展示带规格过滤的搜索结果页
4. 索引字段说明v2.md
更新了 specifications 字段的查询示例,包含 API 格式和 ES 查询结构
添加了两种分面模式的说明和示例
更新了“分面字段”说明,明确支持指定规格名称的分面

5. 补充参数
参数说明:sku_filter_dimension 是可选参数,用于按指定维度过滤每个SPU下的SKU
支持的维度:
直接选项字段:option1、option2、option3
规格名称:通过 option1_name、option2_name、option3_name 匹配(如 color、size)
api/models.py
... ... @@ -145,6 +145,12 @@ class SearchRequest(BaseModel):
145 145 highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)")
146 146 debug: bool = Field(False, description="是否返回调试信息")
147 147  
  148 + # SKU筛选参数
  149 + sku_filter_dimension: Optional[str] = Field(
  150 + None,
  151 + description="子SKU筛选维度(店铺配置)。指定后,每个SPU下的SKU将按该维度分组,每组选择第一个SKU返回。例如:'color'表示按颜色分组,每种颜色选一款。支持的值:'option1'、'option2'、'option3'或specifications中的name(如'color'、'size')"
  152 + )
  153 +
148 154 # 个性化参数(预留)
149 155 user_id: Optional[str] = Field(None, description="用户ID,用于个性化搜索和推荐")
150 156 session_id: Optional[str] = Field(None, description="会话ID,用于搜索分析")
... ...
api/result_formatter.py
... ... @@ -13,7 +13,8 @@ class ResultFormatter:
13 13 def format_search_results(
14 14 es_hits: List[Dict[str, Any]],
15 15 max_score: float = 1.0,
16   - language: str = "zh"
  16 + language: str = "zh",
  17 + sku_filter_dimension: Optional[str] = None
17 18 ) -> List[SpuResult]:
18 19 """
19 20 Convert ES hits to SpuResult list.
... ... @@ -72,11 +73,29 @@ class ResultFormatter:
72 73 price=sku_entry.get('price'),
73 74 compare_at_price=sku_entry.get('compare_at_price'),
74 75 sku=sku_entry.get('sku'),
  76 + sku_code=sku_entry.get('sku_code'),
75 77 stock=sku_entry.get('stock', 0),
  78 + weight=sku_entry.get('weight'),
  79 + weight_unit=sku_entry.get('weight_unit'),
  80 + option1_value=sku_entry.get('option1_value'),
  81 + option2_value=sku_entry.get('option2_value'),
  82 + option3_value=sku_entry.get('option3_value'),
  83 + image_src=sku_entry.get('image_src'),
76 84 options=sku_entry.get('options')
77 85 )
78 86 skus.append(sku)
79 87  
  88 + # Apply SKU filtering if dimension is specified
  89 + if sku_filter_dimension and skus:
  90 + skus = ResultFormatter._filter_skus_by_dimension(
  91 + skus,
  92 + sku_filter_dimension,
  93 + source.get('option1_name'),
  94 + source.get('option2_name'),
  95 + source.get('option3_name'),
  96 + source.get('specifications', [])
  97 + )
  98 +
80 99 # Determine in_stock (any sku has stock > 0)
81 100 in_stock = any(sku.stock > 0 for sku in skus) if skus else True
82 101  
... ... @@ -119,6 +138,81 @@ class ResultFormatter:
119 138 return results
120 139  
121 140 @staticmethod
  141 + def _filter_skus_by_dimension(
  142 + skus: List[SkuResult],
  143 + dimension: str,
  144 + option1_name: Optional[str] = None,
  145 + option2_name: Optional[str] = None,
  146 + option3_name: Optional[str] = None,
  147 + specifications: Optional[List[Dict[str, Any]]] = None
  148 + ) -> List[SkuResult]:
  149 + """
  150 + Filter SKUs by dimension, keeping only one SKU per dimension value.
  151 +
  152 + Args:
  153 + skus: List of SKU results to filter
  154 + dimension: Filter dimension, can be:
  155 + - 'option1', 'option2', 'option3': Direct option field
  156 + - A specification name (e.g., 'color', 'size'): Match by option name
  157 + option1_name: Name of option1 (e.g., 'color')
  158 + option2_name: Name of option2 (e.g., 'size')
  159 + option3_name: Name of option3
  160 + specifications: List of specifications (for reference)
  161 +
  162 + Returns:
  163 + Filtered list of SKUs (one per dimension value)
  164 + """
  165 + if not skus:
  166 + return skus
  167 +
  168 + # Determine which field to use for filtering
  169 + filter_field = None
  170 +
  171 + # Direct option field (option1, option2, option3)
  172 + if dimension.lower() == 'option1':
  173 + filter_field = 'option1_value'
  174 + elif dimension.lower() == 'option2':
  175 + filter_field = 'option2_value'
  176 + elif dimension.lower() == 'option3':
  177 + filter_field = 'option3_value'
  178 + else:
  179 + # Try to match by option name
  180 + dimension_lower = dimension.lower()
  181 + if option1_name and option1_name.lower() == dimension_lower:
  182 + filter_field = 'option1_value'
  183 + elif option2_name and option2_name.lower() == dimension_lower:
  184 + filter_field = 'option2_value'
  185 + elif option3_name and option3_name.lower() == dimension_lower:
  186 + filter_field = 'option3_value'
  187 +
  188 + # If no matching field found, return all SKUs (no filtering)
  189 + if not filter_field:
  190 + return skus
  191 +
  192 + # Group SKUs by dimension value and select first one from each group
  193 + dimension_groups: Dict[str, SkuResult] = {}
  194 +
  195 + for sku in skus:
  196 + # Get dimension value from the determined field
  197 + dimension_value = None
  198 + if filter_field == 'option1_value':
  199 + dimension_value = sku.option1_value
  200 + elif filter_field == 'option2_value':
  201 + dimension_value = sku.option2_value
  202 + elif filter_field == 'option3_value':
  203 + dimension_value = sku.option3_value
  204 +
  205 + # Use empty string as key for None values
  206 + key = str(dimension_value) if dimension_value is not None else ''
  207 +
  208 + # Keep first SKU for each dimension value
  209 + if key not in dimension_groups:
  210 + dimension_groups[key] = sku
  211 +
  212 + # Return filtered SKUs (one per dimension value)
  213 + return list(dimension_groups.values())
  214 +
  215 + @staticmethod
122 216 def format_facets(
123 217 es_aggregations: Dict[str, Any],
124 218 facet_configs: Optional[List[Any]] = None
... ...
api/routes/search.py
... ... @@ -96,6 +96,7 @@ async def search(request: SearchRequest, http_request: Request):
96 96 sort_order=request.sort_order,
97 97 debug=request.debug,
98 98 language=request.language,
  99 + sku_filter_dimension=request.sku_filter_dimension,
99 100 )
100 101  
101 102 # Include performance summary in response
... ... @@ -291,7 +292,8 @@ async def instant_search(
291 292 query=q,
292 293 tenant_id=tenant_id,
293 294 size=size,
294   - from_=0
  295 + from_=0,
  296 + sku_filter_dimension=None # Instant search doesn't support SKU filtering
295 297 )
296 298  
297 299 return SearchResponse(
... ...
docs/搜索API对接指南.md
... ... @@ -105,6 +105,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
105 105 "sort_by": "string",
106 106 "sort_order": "desc",
107 107 "min_score": 0.0,
  108 + "sku_filter_dimension": "string",
108 109 "debug": false,
109 110 "user_id": "string",
110 111 "session_id": "string"
... ... @@ -127,6 +128,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
127 128 | `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) |
128 129 | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) |
129 130 | `min_score` | float | N | null | 最小相关性分数阈值 |
  131 +| `sku_filter_dimension` | string | N | null | 子SKU筛选维度(店铺配置)。指定后,每个SPU下的SKU将按该维度分组,每组选择第一个SKU返回。支持的值:`option1`、`option2`、`option3` 或 specifications 中的 name(如 `color`、`size`)。详见下文说明 |
130 132 | `debug` | boolean | N | false | 是否返回调试信息 |
131 133 | `user_id` | string | N | null | 用户ID(用于个性化,预留) |
132 134 | `session_id` | string | N | null | 会话ID(用于分析,预留) |
... ... @@ -326,6 +328,51 @@ curl -X POST "http://120.76.41.98:6002/search/" \
326 328 - `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计)
327 329 - `ranges`: 范围定义(仅当 type='range' 时需要)
328 330  
  331 +### SKU筛选维度 (sku_filter_dimension)
  332 +
  333 +**功能说明**:
  334 +`sku_filter_dimension` 用于控制每个SPU下返回的SKU数量。当指定此参数后,系统会按指定维度对SKU进行分组,每个分组只返回第一个SKU(从简实现,选择该维度下的第一款)。
  335 +
  336 +**使用场景**:
  337 +- 店铺配置了SKU筛选维度(如 `color`),希望每个SPU下每种颜色只显示一个SKU
  338 +- 减少前端展示的SKU数量,提升页面加载性能
  339 +- 避免展示过多重复的SKU选项
  340 +
  341 +**支持的维度值**:
  342 +1. **直接选项字段**: `option1`、`option2`、`option3`
  343 + - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组
  344 +
  345 +2. **规格名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配
  346 + - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: "color"` 来按颜色分组
  347 +
  348 +**示例**:
  349 +
  350 +**按颜色筛选(假设 option1_name = "color")**:
  351 +```json
  352 +{
  353 + "query": "芭比娃娃",
  354 + "sku_filter_dimension": "color"
  355 +}
  356 +```
  357 +
  358 +**按选项1筛选**:
  359 +```json
  360 +{
  361 + "query": "芭比娃娃",
  362 + "sku_filter_dimension": "option1"
  363 +}
  364 +```
  365 +
  366 +**按选项2筛选**:
  367 +```json
  368 +{
  369 + "query": "芭比娃娃",
  370 + "sku_filter_dimension": "option2"
  371 +}
  372 +```
  373 +
  374 +---
  375 +
329 376 ### 布尔表达式语法
330 377  
331 378 搜索查询支持布尔表达式,提供更灵活的搜索能力。
... ... @@ -467,7 +514,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
467 514 | `results[].spu_id` | string | SPU ID |
468 515 | `results[].title` | string | 商品标题 |
469 516 | `results[].price` | float | 价格(min_price) |
470   -| `results[].skus` | array | SKU列表 |
  517 +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) |
471 518 | `results[].relevance_score` | float | 相关性分数 |
472 519 | `total` | integer | 匹配的总文档数 |
473 520 | `max_score` | float | 最高相关性分数 |
... ... @@ -548,7 +595,24 @@ curl -X POST "http://120.76.41.98:6002/search/" \
548 595 }
549 596 ```
550 597  
551   -### 场景2:带筛选的商品搜索
  598 +### 场景2:SKU筛选(按维度过滤)
  599 +
  600 +**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU
  601 +
  602 +```json
  603 +{
  604 + "query": "芭比娃娃",
  605 + "size": 20,
  606 + "sku_filter_dimension": "color"
  607 +}
  608 +```
  609 +
  610 +**说明**:
  611 +- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: "color"` 可以按颜色分组
  612 +- 每个SPU下,每种颜色只会返回第一个SKU
  613 +- 如果维度不匹配,返回所有SKU(不进行过滤)
  614 +
  615 +### 场景3:带筛选的商品搜索
552 616  
553 617 **需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间
554 618  
... ... @@ -569,7 +633,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
569 633 }
570 634 ```
571 635  
572   -### 场景3:带分面的商品搜索
  636 +### 场景4:带分面的商品搜索
573 637  
574 638 **需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器
575 639  
... ... @@ -586,7 +650,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
586 650 }
587 651 ```
588 652  
589   -### 场景4:多条件组合搜索
  653 +### 场景5:多条件组合搜索
590 654  
591 655 **需求**: 搜索"手机",筛选多个品牌,价格范围,并获取分面统计
592 656  
... ... @@ -626,7 +690,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
626 690 }
627 691 ```
628 692  
629   -### 场景5:规格过滤搜索
  693 +### 场景6:规格过滤搜索
630 694  
631 695 **需求**: 搜索"手机",筛选color为"white"的商品
632 696  
... ... @@ -644,7 +708,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
644 708 }
645 709 ```
646 710  
647   -### 场景6:多个规格过滤(OR逻辑)
  711 +### 场景7:多个规格过滤(OR逻辑)
648 712  
649 713 **需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品
650 714  
... ... @@ -662,7 +726,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
662 726 }
663 727 ```
664 728  
665   -### 场景7:规格分面搜索
  729 +### 场景8:规格分面搜索
666 730  
667 731 **需求**: 搜索"手机",获取所有规格的分面统计
668 732  
... ... @@ -686,7 +750,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
686 750 }
687 751 ```
688 752  
689   -### 场景8:组合过滤和分面
  753 +### 场景9:组合过滤和分面
690 754  
691 755 **需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计
692 756  
... ... @@ -711,7 +775,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
711 775 }
712 776 ```
713 777  
714   -### 场景9:布尔表达式搜索
  778 +### 场景10:布尔表达式搜索
715 779  
716 780 **需求**: 搜索包含"手机"和"智能"的商品,排除"二手"
717 781  
... ... @@ -722,7 +786,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
722 786 }
723 787 ```
724 788  
725   -### 场景10:分页查询
  789 +### 场景11:分页查询
726 790  
727 791 **需求**: 获取第2页结果(每页20条)
728 792  
... ...
docs/搜索API速查表.md
... ... @@ -121,6 +121,37 @@ POST /search/
121 121  
122 122 ---
123 123  
  124 +## SKU筛选维度
  125 +
  126 +**功能**: 按指定维度对每个SPU下的SKU进行分组,每组只返回第一个SKU。
  127 +
  128 +```bash
  129 +{
  130 + "query": "芭比娃娃",
  131 + "sku_filter_dimension": "color" // 按颜色筛选(假设option1_name="color")
  132 +}
  133 +```
  134 +
  135 +**支持的维度值**:
  136 +- `option1`, `option2`, `option3`: 直接使用选项字段
  137 +- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配
  138 +
  139 +**示例**:
  140 +```bash
  141 +// 按选项1筛选
  142 +{"sku_filter_dimension": "option1"}
  143 +
  144 +// 按颜色筛选(如果option1_name="color")
  145 +{"sku_filter_dimension": "color"}
  146 +
  147 +// 按尺寸筛选(如果option2_name="size")
  148 +{"sku_filter_dimension": "size"}
  149 +```
  150 +
  151 +**性能说明**: 在应用层过滤,不影响ES查询性能,只对返回结果进行过滤。
  152 +
  153 +---
  154 +
124 155 ## 排序
125 156  
126 157 ```bash
... ... @@ -180,7 +211,8 @@ Headers: X-Tenant-ID: 2
180 211 "specifications.size"
181 212 ],
182 213 "sort_by": "min_price",
183   - "sort_order": "asc"
  214 + "sort_order": "asc",
  215 + "sku_filter_dimension": "color" // 可选:按颜色筛选SKU
184 216 }
185 217 ```
186 218  
... ... @@ -211,7 +243,7 @@ Headers: X-Tenant-ID: 2
211 243 "specifications": [
212 244 {"sku_id": "sku_001", "name": "color", "value": "white"}
213 245 ],
214   - "skus": [...],
  246 + "skus": [...], // 如果指定了sku_filter_dimension,则返回过滤后的SKU(每个维度值一个)
215 247 "relevance_score": 8.5
216 248 }
217 249 ],
... ...
docs/系统设计文档.md
... ... @@ -31,124 +31,154 @@
31 31 {
32 32 "tenant_id": "1",
33 33 "spu_id": "123",
34   - "title": "蓝牙耳机",
  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 + ],
35 62 "skus": [
36 63 {
37 64 "sku_id": "456",
38   - "title": "黑色",
39 65 "price": 199.99,
40   - "sku": "SKU-123-1",
41   - "stock": 50
  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"
42 75 }
43 76 ],
44   - "min_price": 199.99,
45   - "max_price": 299.99
  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 + ]
46 84 }
47 85 ```
48 86  
49   -### 1.3 配置化方案
  87 +### 1.3 索引结构简化方案
50 88  
51   -**配置分离原则**:
52   -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置
53   -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定
54   -- **数据导入流程**:写死的脚本,不依赖配置
  89 +**简化原则**:
  90 +- **硬编码映射**:ES mapping 结构直接定义在 JSON 文件中(`mappings/search_products.json`)
  91 +- **统一索引结构**:所有租户共享相同的索引结构,通过 `tenant_id` 隔离数据
  92 +- **数据源统一**:所有租户使用相同的 MySQL 表结构(店匠标准表)
  93 +- **查询配置硬编码**:查询相关配置(字段 boost、查询域等)硬编码在 `search/query_config.py`
55 94  
56   -统一通过配置文件定义:
57   -1. ES 字段定义(字段类型、分析器、boost等)
58   -2. ES mapping 结构生成
59   -3. 查询域配置(indexes)
60   -4. 排序和打分配置(function_score)
  95 +**索引结构特点**:
  96 +1. **多语言字段**:所有文本字段支持中英文(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_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维)用于语义搜索
61 103  
62   -**注意**:配置中**不包含**以下内容:
63   -- `mysql_config` - MySQL数据库配置
64   -- `main_table` / `extension_table` - 数据表配置
65   -- `source_table` / `source_column` - 字段数据源映射
  104 +**实现文件**:
  105 +- `mappings/search_products.json` - ES mapping 定义(硬编码)
  106 +- `search/query_config.py` - 查询配置(硬编码)
  107 +- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引
66 108  
67 109 ---
68 110  
69   -## 2. 配置系统实现
70   -
71   -### 2.1 应用结构配置(字段定义)
72   -
73   -**配置文件位置**:`config/schema/{tenant_id}_config.yaml`
74   -
75   -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。
76   -
77   -**实现情况**:
78   -
79   -#### 字段类型支持
80   -- **TEXT**:文本字段,支持多语言分析器
81   -- **KEYWORD**:关键词字段,用于精确匹配和聚合
82   -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度)
83   -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度)
84   -- **INT/LONG**:整数类型
85   -- **FLOAT/DOUBLE**:浮点数类型
86   -- **DATE**:日期类型
87   -- **BOOLEAN**:布尔类型
88   -
89   -#### 分析器支持
90   -- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj)
91   -- **english**:英文分析器
92   -- **russian**:俄文分析器
93   -- **arabic**:阿拉伯文分析器
94   -- **spanish**:西班牙文分析器
95   -- **japanese**:日文分析器
96   -- **standard**:标准分析器
97   -- **keyword**:关键词分析器
98   -
99   -#### 字段配置示例(Base配置)
100   -
101   -```yaml
102   -fields:
103   - # 租户隔离字段(必需)
104   - - name: "tenant_id"
105   - type: "KEYWORD"
106   - required: true
107   - index: true
108   - store: true
109   -
110   - # 商品标识字段
111   - - name: "spu_id"
112   - type: "KEYWORD"
113   - required: true
114   - index: true
115   - store: true
116   -
117   - # 文本搜索字段
118   - - name: "title"
119   - type: "TEXT"
120   - analyzer: "chinese_ecommerce"
121   - boost: 3.0
122   - index: true
123   - store: true
124   -
125   - - name: "seo_keywords"
126   - type: "TEXT"
127   - analyzer: "chinese_ecommerce"
128   - boost: 2.0
129   - index: true
130   - store: true
131   -
132   - # 嵌套skus字段
133   - - name: "skus"
134   - type: "JSON"
135   - nested: true
136   - nested_properties:
137   - sku_id:
138   - type: "keyword"
139   - price:
140   - type: "float"
141   - sku:
142   - type: "keyword"
143   -```
144   -
145   -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。
  111 +## 2. 索引结构实现
  112 +
  113 +### 2.1 硬编码映射方案
  114 +
  115 +**实现方式**:
  116 +- ES mapping 直接定义在 `mappings/search_products.json` 文件中
  117 +- 所有租户共享相同的索引结构
  118 +- 查询配置硬编码在 `search/query_config.py`
  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_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)
146 177  
147 178 **实现模块**:
148   -- `config/config_loader.py` - 配置加载器
149   -- `config/field_types.py` - 字段类型定义
150   -- `indexer/mapping_generator.py` - ES mapping 生成器
151   -- `indexer/data_transformer.py` - 数据转换器
  179 +- `mappings/search_products.json` - ES mapping 定义
  180 +- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引
  181 +- `search/query_config.py` - 查询配置(字段 boost、查询域等)
152 182  
153 183 ### 2.2 索引结构配置(查询域配置)
154 184  
... ... @@ -217,6 +247,7 @@ indexes:
217 247 **实现模块**:
218 248 - `search/es_query_builder.py` - ES 查询构建器(单层架构)
219 249 - `query/query_parser.py` - 查询解析器(支持语言检测和翻译)
  250 +- `search/query_config.py` - 查询配置(字段 boost、查询域等)
220 251  
221 252 ---
222 253  
... ... @@ -233,26 +264,47 @@ indexes:
233 264  
234 265 ### 3.2 数据导入方式
235 266  
236   -**Pipeline层决定数据源**:
237   -- 数据导入流程是写死的脚本,不依赖配置
238   -- 配置只关注ES搜索相关的内容
239   -- 数据源映射逻辑写死在转换器代码中
  267 +**数据源统一**:
  268 +- 所有租户使用相同的MySQL表结构(店匠标准表)
  269 +- 数据转换逻辑写死在转换器代码中
  270 +- 索引结构硬编码,不依赖配置
240 271  
241   -#### Base配置数据导入(店匠通用)
  272 +#### 数据导入流程(店匠通用)
242 273  
243 274 **脚本**:`scripts/ingest_shoplazza.py`
244 275  
245 276 **数据流程**:
246   -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表
  277 +1. **数据加载**:
  278 + - 从MySQL读取`shoplazza_product_spu`表(SPU数据)
  279 + - 从MySQL读取`shoplazza_product_sku`表(SKU数据)
  280 + - 从MySQL读取`shoplazza_product_option`表(选项定义)
  281 +
247 282 2. **数据转换**(`indexer/spu_transformer.py`):
248 283 - 按`spu_id`和`tenant_id`关联SPU和SKU数据
249   - - 将SKU数据聚合为嵌套的`skus`数组
250   - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`)
251   - - 字段映射(写死在代码中,不依赖配置)
  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库存总和
252 302 - 注入`tenant_id`字段
  303 +
253 304 3. **索引创建**:
254   - - 根据配置生成ES mapping
  305 + - 从`mappings/search_products.json`加载ES mapping
255 306 - 创建或更新`search_products`索引
  307 +
256 308 4. **批量入库**:
257 309 - 批量写入ES(默认每批500条)
258 310 - 错误处理和重试机制
... ... @@ -424,12 +476,19 @@ laptop AND (gaming OR professional) ANDNOT cheap
424 476 2. **查询构建**(简化架构):
425 477 - **结构**: `filters AND (text_recall OR embedding_recall)`
426 478 - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中)
  479 + - 普通字段过滤:`{"category_name": "手机"}`
  480 + - 范围过滤:`{"min_price": {"gte": 50, "lte": 200}}`
  481 + - **Specifications嵌套过滤**:
  482 + - 单个规格:`{"specifications": {"name": "color", "value": "white"}}`
  483 + - 多个规格(OR):`{"specifications": [{"name": "color", "value": "white"}, {"name": "size", "value": "256GB"}]}`
  484 + - 使用ES的`nested`查询实现
427 485 - **text_recall**: 文本相关性召回
428 486 - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`)
429 487 - 使用 `multi_match` 查询,支持字段 boost
  488 + - 中文字段使用中文分词器,英文字段使用英文分析器
430 489 - **embedding_recall**: 向量召回(KNN)
431 490 - 使用 `title_embedding` 字段进行 KNN 搜索
432   - - ES 自动与文本召回合并
  491 + - ES 自动与文本召回合并(OR逻辑)
433 492 - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等)
434 493  
435 494 #### 查询结构示例
... ... @@ -530,10 +589,13 @@ ranking:
530 589  
531 590 ### 6.4 搜索功能
532 591 - ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号)
533   -- ✅ 多语言查询构建(语言路由、字段映射
  592 +- ✅ 多语言查询构建(同时搜索中英文字段
534 593 - ✅ 语义搜索(KNN 检索)
535 594 - ✅ 相关性排序(BM25 + 向量相似度)
536 595 - ✅ 结果聚合(Faceted Search)
  596 +- ✅ Specifications嵌套过滤(单个和多个规格,OR逻辑)
  597 +- ✅ Specifications嵌套分面(所有规格名称和指定规格名称)
  598 +- ✅ SKU筛选(按维度过滤,应用层实现)
537 599  
538 600 ### 6.5 API 服务
539 601 - ✅ RESTful API(FastAPI)
... ... @@ -542,12 +604,16 @@ ranking:
542 604 - ✅ 前端界面(HTML + JavaScript)
543 605 - ✅ 租户隔离(tenant_id过滤)
544 606  
545   -### 6.6 Base配置(店匠通用)
  607 +### 6.6 索引结构(店匠通用)
546 608 - ✅ SPU级别索引结构
547   -- ✅ 嵌套skus字段
  609 +- ✅ 多语言字段支持(中英文)
  610 +- ✅ 嵌套字段(skus, specifications, image_embedding)
  611 +- ✅ 规格字段(specifications)支持过滤和分面
  612 +- ✅ 扁平化字段(价格、库存等)用于过滤和排序
548 613 - ✅ 统一索引(search_products)
549 614 - ✅ 租户隔离(tenant_id)
550   -- ✅ 配置简化(移除MySQL相关配置)
  615 +- ✅ 硬编码映射(mappings/search_products.json)
  616 +- ✅ 硬编码查询配置(search/query_config.py)
551 617  
552 618 ---
553 619  
... ... @@ -636,25 +702,30 @@ Elasticsearch
636 702  
637 703 - **简单模式**:字符串列表(字段名),使用默认配置
638 704 ```json
639   - ["category.keyword", "vendor.keyword"]
  705 + ["category1_name", "category2_name", "specifications"]
640 706 ```
641 707  
  708 +- **Specifications分面**:
  709 + - 所有规格名称:`"specifications"` - 返回所有name及其value列表
  710 + - 指定规格名称:`"specifications.color"` - 只返回指定name的value列表
  711 +
642 712 - **高级模式**:FacetConfig 对象列表,支持自定义配置
643 713 ```json
644 714 [
645 715 {
646   - "field": "category.keyword",
  716 + "field": "category1_name",
647 717 "size": 15,
648 718 "type": "terms"
649 719 },
650 720 {
651   - "field": "price",
  721 + "field": "min_price",
652 722 "type": "range",
653 723 "ranges": [
654 724 {"key": "0-50", "to": 50},
655 725 {"key": "50-100", "from": 50, "to": 100}
656 726 ]
657   - }
  727 + },
  728 + "specifications.color" // 指定规格名称的分面
658 729 ]
659 730 ```
660 731  
... ... @@ -662,7 +733,10 @@ Elasticsearch
662 733 1. API 层:接收 `List[Union[str, FacetConfig]]`
663 734 2. Searcher 层:透传,不做转换
664 735 3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式
665   -4. 输出:转换为 ES 聚合查询
  736 + - 检测 `"specifications"` 或 `"specifications.{name}"` 格式
  737 + - 构建对应的嵌套聚合查询
  738 +4. 输出:转换为 ES 聚合查询(包括specifications嵌套聚合)
  739 +5. Result Formatter:格式化ES聚合结果,处理specifications嵌套结构
666 740  
667 741 #### 8.3.3 Range Filters 数据流
668 742  
... ... @@ -680,8 +754,8 @@ class RangeFilter(BaseModel):
680 754 **示例**:
681 755 ```json
682 756 {
683   - "price": {"gte": 50, "lte": 200},
684   - "created_at": {"gte": "2023-01-01T00:00:00Z"}
  757 + "min_price": {"gte": 50, "lte": 200},
  758 + "create_time": {"gte": "2023-01-01T00:00:00Z"}
685 759 }
686 760 ```
687 761  
... ... @@ -696,6 +770,38 @@ class RangeFilter(BaseModel):
696 770 - 类型支持:支持数值(float)和日期时间字符串(ISO 格式)
697 771 - 统一约定:所有范围过滤都使用 RangeFilter 模型
698 772  
  773 +#### 8.3.3.1 Specifications 过滤数据流
  774 +
  775 +**输入格式**:`Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]`
  776 +
  777 +**单个规格过滤**:
  778 +```json
  779 +{
  780 + "specifications": {
  781 + "name": "color",
  782 + "value": "white"
  783 + }
  784 +}
  785 +```
  786 +
  787 +**多个规格过滤(OR逻辑)**:
  788 +```json
  789 +{
  790 + "specifications": [
  791 + {"name": "color", "value": "white"},
  792 + {"name": "size", "value": "256GB"}
  793 + ]
  794 +}
  795 +```
  796 +
  797 +**数据流**:
  798 +1. API 层:接收 `filters` 字典,检测 `specifications` 键
  799 +2. Searcher 层:透传 `filters` 字典
  800 +3. ES Query Builder:检测 `specifications` 键,构建ES `nested` 查询
  801 + - 单个规格:构建单个 `nested` 查询
  802 + - 多个规格:构建多个 `nested` 查询,使用 `should` 组合(OR逻辑)
  803 +4. 输出:ES nested 查询(`nested.path=specifications` + `bool.must=[term(name), term(value)]`)
  804 +
699 805 #### 8.3.4 响应 Facets 数据流
700 806  
701 807 **输出格式**:`List[FacetResult]`
... ... @@ -711,16 +817,63 @@ class FacetResult(BaseModel):
711 817 ```
712 818  
713 819 **数据流**:
714   -1. ES Response:返回聚合结果(字典格式)
715   -2. Searcher 层:构建 `List[FacetResult]` 对象
716   -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON)
  820 +1. ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合)
  821 +2. Result Formatter:格式化ES聚合结果
  822 + - 处理普通terms聚合
  823 + - 处理range聚合
  824 + - **处理specifications嵌套聚合**:
  825 + - 所有规格名称:解析 `by_name` 聚合结构
  826 + - 指定规格名称:解析 `filter_by_name` 聚合结构
  827 +3. Searcher 层:构建 `List[FacetResult]` 对象
  828 +4. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON)
717 829  
718 830 **优势**:
719 831 - 类型安全:使用 Pydantic 模型确保数据结构一致性
720 832 - 自动序列化:模型自动转换为 JSON,无需手动处理
721 833 - 统一约定:所有响应都使用标准化的 Pydantic 模型
722 834  
723   -#### 8.3.5 统一约定的好处
  835 +#### 8.3.5 SKU筛选数据流
  836 +
  837 +**输入格式**:`Optional[str]`
  838 +
  839 +**支持的维度值**:
  840 +- `option1`, `option2`, `option3`: 直接使用选项字段
  841 +- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配
  842 +
  843 +**示例**:
  844 +```json
  845 +{
  846 + "query": "手机",
  847 + "sku_filter_dimension": "color"
  848 +}
  849 +```
  850 +
  851 +**数据流**:
  852 +1. API 层:接收 `sku_filter_dimension` 字符串参数
  853 +2. Searcher 层:透传到 Result Formatter
  854 +3. Result Formatter:在格式化结果时,按指定维度对SKU进行分组
  855 + - 如果维度是 `option1/2/3`,直接使用对应的 `option1_value/2/3` 字段
  856 + - 如果维度是规格名称,通过 `option1_name/2/3` 匹配找到对应的 `option1_value/2/3`
  857 + - 每个分组选择第一个SKU返回
  858 +4. 输出:过滤后的SKU列表(每个维度值一个SKU)
  859 +
  860 +**工作原理**:
  861 +1. 系统从ES返回所有SKU(不改变ES查询,保持性能)
  862 +2. 在结果格式化阶段,按指定维度对SKU进行分组
  863 +3. 每个分组选择第一个SKU返回
  864 +4. 如果维度不匹配或未找到,返回所有SKU(不进行过滤)
  865 +
  866 +**性能说明**:
  867 +- ✅ **推荐方案**: 在应用层过滤(当前实现)
  868 + - ES查询简单,不需要nested查询和join
  869 + - 只对返回的结果(通常10-20个SPU)进行过滤,数据量小
  870 + - 实现简单,性能开销小
  871 +- ❌ **不推荐**: 在ES查询时过滤
  872 + - 需要nested查询和join,性能开销大
  873 + - 实现复杂
  874 + - 只对返回的结果需要过滤,不需要在ES层面过滤
  875 +
  876 +#### 8.3.6 统一约定的好处
724 877  
725 878 1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证
726 879 2. **代码一致性**:所有层使用相同的数据模型,减少转换错误
... ... @@ -729,14 +882,14 @@ class FacetResult(BaseModel):
729 882 5. **数据验证**:自动验证输入数据,减少错误处理代码
730 883  
731 884 **实现模块**:
732   -- `api/models.py` - 所有 Pydantic 模型定义
733   -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型)
734   -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询)
  885 +- `api/models.py` - 所有 Pydantic 模型定义(包括 `SearchRequest`, `FacetConfig`, `RangeFilter` 等)
  886 +- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型,包括specifications分面处理和SKU筛选)
  887 +- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询,包括specifications过滤和分面)
735 888  
736   -## 9. 配置文件示例
  889 +## 9. 索引结构文件
737 890  
738   -**Base配置**(店匠通用):`config/schema/base/config.yaml`
  891 +**硬编码映射**(店匠通用):`mappings/search_products.json`
739 892  
740   -**其他客户配置**:`config/schema/tenant1/config.yaml`
  893 +**查询配置**(硬编码):`search/query_config.py`
741 894  
742 895 ---
... ...
docs/设计文档.md renamed to docs/系统设计文档v1.md
... ... @@ -24,7 +24,7 @@
24 24 - 所有客户共享同一个Elasticsearch索引:`search_products`
25 25 - 索引粒度:SPU级别(每个文档代表一个SPU)
26 26 - 数据隔离:通过`tenant_id`字段实现租户隔离
27   -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组(SKU变体)
  27 +- 嵌套结构:每个SPU文档包含嵌套的`skus`数组
28 28  
29 29 **索引文档结构**:
30 30 ```json
... ... @@ -215,7 +215,7 @@ indexes:
215 215 4. 组合多个语言查询的结果,提高召回率
216 216  
217 217 **实现模块**:
218   -- `search/multilang_query_builder.py` - 多语言查询构建器
  218 +- `search/es_query_builder.py` - ES 查询构建器(单层架构)
219 219 - `query/query_parser.py` - 查询解析器(支持语言检测和翻译)
220 220  
221 221 ---
... ... @@ -345,7 +345,7 @@ query_config:
345 345  
346 346 #### 工作流程
347 347 ```
348   -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建
  348 +查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall))
349 349 ```
350 350  
351 351 #### 实现模块
... ... @@ -421,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap
421 421 - 提取域(如 `title:查询` → 域=`title`,查询=`查询`)
422 422 - 检测查询语言
423 423 - 生成翻译
424   -2. **多语言查询构建**:
425   - - 如果域有 `language_field_mapping`:
426   - - 使用检测到的语言查询对应字段(boost * 1.5)
427   - - 使用翻译后的查询搜索其他语言字段(boost * 1.0)
428   - - 如果域没有 `language_field_mapping`:
429   - - 使用所有字段进行搜索
430   -3. **查询组合**:
431   - - 多个语言查询组合为 `should` 子句
432   - - 提高召回率
433   -
434   -#### 示例
435   -```
436   -查询: "芭比娃娃"
437   -域: default
438   -检测语言: zh
439   -
440   -生成的查询:
441   -- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5)
442   -- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0)
443   -- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0)
  424 +2. **查询构建**(简化架构):
  425 + - **结构**: `filters AND (text_recall OR embedding_recall)`
  426 + - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中)
  427 + - **text_recall**: 文本相关性召回
  428 + - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`)
  429 + - 使用 `multi_match` 查询,支持字段 boost
  430 + - **embedding_recall**: 向量召回(KNN)
  431 + - 使用 `title_embedding` 字段进行 KNN 搜索
  432 + - ES 自动与文本召回合并
  433 + - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等)
  434 +
  435 +#### 查询结构示例
  436 +```json
  437 +{
  438 + "query": {
  439 + "bool": {
  440 + "must": [
  441 + {
  442 + "function_score": {
  443 + "query": {
  444 + "multi_match": {
  445 + "query": "手机",
  446 + "fields": [
  447 + "title_zh^3.0", "title_en^3.0",
  448 + "brief_zh^1.5", "brief_en^1.5",
  449 + ...
  450 + ]
  451 + }
  452 + },
  453 + "functions": [...]
  454 + }
  455 + }
  456 + ],
  457 + "filter": [
  458 + {"term": {"tenant_id": "2"}},
  459 + {"term": {"category_name": "手机"}}
  460 + ]
  461 + }
  462 + },
  463 + "knn": {
  464 + "field": "title_embedding",
  465 + "query_vector": [...],
  466 + "k": 50,
  467 + "boost": 0.2
  468 + }
  469 +}
444 470 ```
445 471  
446 472 #### 实现模块
447   -- `search/multilang_query_builder.py` - 多语言查询构建器
448   -- `search/searcher.py` - 搜索器(使用多语言构建器)
  473 +- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法)
  474 +- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`)
449 475  
450 476 ### 5.3 相关性计算(Ranking)
451 477  
... ... @@ -610,14 +636,14 @@ Elasticsearch
610 636  
611 637 - **简单模式**:字符串列表(字段名),使用默认配置
612 638 ```json
613   - ["categoryName_keyword", "brandName_keyword"]
  639 + ["category.keyword", "vendor.keyword"]
614 640 ```
615 641  
616 642 - **高级模式**:FacetConfig 对象列表,支持自定义配置
617 643 ```json
618 644 [
619 645 {
620   - "field": "categoryName_keyword",
  646 + "field": "category.keyword",
621 647 "size": 15,
622 648 "type": "terms"
623 649 },
... ...
search/searcher.py
... ... @@ -135,6 +135,7 @@ class Searcher:
135 135 sort_order: Optional[str] = "desc",
136 136 debug: bool = False,
137 137 language: str = "zh",
  138 + sku_filter_dimension: Optional[str] = None,
138 139 ) -> SearchResult:
139 140 """
140 141 Execute search query (外部友好格式).
... ... @@ -376,7 +377,8 @@ class Searcher:
376 377 formatted_results = ResultFormatter.format_search_results(
377 378 es_hits,
378 379 max_score,
379   - language=language
  380 + language=language,
  381 + sku_filter_dimension=sku_filter_dimension
380 382 )
381 383  
382 384 # Format facets
... ... @@ -542,7 +544,12 @@ class Searcher:
542 544 max_score = es_response.get('hits', {}).get('max_score') or 0.0
543 545  
544 546 # Format results using ResultFormatter
545   - formatted_results = ResultFormatter.format_search_results(es_hits, max_score)
  547 + formatted_results = ResultFormatter.format_search_results(
  548 + es_hits,
  549 + max_score,
  550 + language="zh", # Default language for image search
  551 + sku_filter_dimension=None # Image search doesn't support SKU filtering
  552 + )
546 553  
547 554 return SearchResult(
548 555 results=formatted_results,
... ...