Commit ca91352aa97b0a95479a570f0278d840f5709b20
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)
Showing
8 changed files
with
560 additions
and
176 deletions
Show diff stats
api/models.py
| @@ -145,6 +145,12 @@ class SearchRequest(BaseModel): | @@ -145,6 +145,12 @@ class SearchRequest(BaseModel): | ||
| 145 | highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)") | 145 | highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)") |
| 146 | debug: bool = Field(False, description="是否返回调试信息") | 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 | user_id: Optional[str] = Field(None, description="用户ID,用于个性化搜索和推荐") | 155 | user_id: Optional[str] = Field(None, description="用户ID,用于个性化搜索和推荐") |
| 150 | session_id: Optional[str] = Field(None, description="会话ID,用于搜索分析") | 156 | session_id: Optional[str] = Field(None, description="会话ID,用于搜索分析") |
api/result_formatter.py
| @@ -13,7 +13,8 @@ class ResultFormatter: | @@ -13,7 +13,8 @@ class ResultFormatter: | ||
| 13 | def format_search_results( | 13 | def format_search_results( |
| 14 | es_hits: List[Dict[str, Any]], | 14 | es_hits: List[Dict[str, Any]], |
| 15 | max_score: float = 1.0, | 15 | max_score: float = 1.0, |
| 16 | - language: str = "zh" | 16 | + language: str = "zh", |
| 17 | + sku_filter_dimension: Optional[str] = None | ||
| 17 | ) -> List[SpuResult]: | 18 | ) -> List[SpuResult]: |
| 18 | """ | 19 | """ |
| 19 | Convert ES hits to SpuResult list. | 20 | Convert ES hits to SpuResult list. |
| @@ -72,11 +73,29 @@ class ResultFormatter: | @@ -72,11 +73,29 @@ class ResultFormatter: | ||
| 72 | price=sku_entry.get('price'), | 73 | price=sku_entry.get('price'), |
| 73 | compare_at_price=sku_entry.get('compare_at_price'), | 74 | compare_at_price=sku_entry.get('compare_at_price'), |
| 74 | sku=sku_entry.get('sku'), | 75 | sku=sku_entry.get('sku'), |
| 76 | + sku_code=sku_entry.get('sku_code'), | ||
| 75 | stock=sku_entry.get('stock', 0), | 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 | options=sku_entry.get('options') | 84 | options=sku_entry.get('options') |
| 77 | ) | 85 | ) |
| 78 | skus.append(sku) | 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 | # Determine in_stock (any sku has stock > 0) | 99 | # Determine in_stock (any sku has stock > 0) |
| 81 | in_stock = any(sku.stock > 0 for sku in skus) if skus else True | 100 | in_stock = any(sku.stock > 0 for sku in skus) if skus else True |
| 82 | 101 | ||
| @@ -119,6 +138,81 @@ class ResultFormatter: | @@ -119,6 +138,81 @@ class ResultFormatter: | ||
| 119 | return results | 138 | return results |
| 120 | 139 | ||
| 121 | @staticmethod | 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 | def format_facets( | 216 | def format_facets( |
| 123 | es_aggregations: Dict[str, Any], | 217 | es_aggregations: Dict[str, Any], |
| 124 | facet_configs: Optional[List[Any]] = None | 218 | facet_configs: Optional[List[Any]] = None |
api/routes/search.py
| @@ -96,6 +96,7 @@ async def search(request: SearchRequest, http_request: Request): | @@ -96,6 +96,7 @@ async def search(request: SearchRequest, http_request: Request): | ||
| 96 | sort_order=request.sort_order, | 96 | sort_order=request.sort_order, |
| 97 | debug=request.debug, | 97 | debug=request.debug, |
| 98 | language=request.language, | 98 | language=request.language, |
| 99 | + sku_filter_dimension=request.sku_filter_dimension, | ||
| 99 | ) | 100 | ) |
| 100 | 101 | ||
| 101 | # Include performance summary in response | 102 | # Include performance summary in response |
| @@ -291,7 +292,8 @@ async def instant_search( | @@ -291,7 +292,8 @@ async def instant_search( | ||
| 291 | query=q, | 292 | query=q, |
| 292 | tenant_id=tenant_id, | 293 | tenant_id=tenant_id, |
| 293 | size=size, | 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 | return SearchResponse( | 299 | return SearchResponse( |
docs/搜索API对接指南.md
| @@ -105,6 +105,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -105,6 +105,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 105 | "sort_by": "string", | 105 | "sort_by": "string", |
| 106 | "sort_order": "desc", | 106 | "sort_order": "desc", |
| 107 | "min_score": 0.0, | 107 | "min_score": 0.0, |
| 108 | + "sku_filter_dimension": "string", | ||
| 108 | "debug": false, | 109 | "debug": false, |
| 109 | "user_id": "string", | 110 | "user_id": "string", |
| 110 | "session_id": "string" | 111 | "session_id": "string" |
| @@ -127,6 +128,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -127,6 +128,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 127 | | `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | | 128 | | `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | |
| 128 | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | 129 | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | |
| 129 | | `min_score` | float | N | null | 最小相关性分数阈值 | | 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 | | `debug` | boolean | N | false | 是否返回调试信息 | | 132 | | `debug` | boolean | N | false | 是否返回调试信息 | |
| 131 | | `user_id` | string | N | null | 用户ID(用于个性化,预留) | | 133 | | `user_id` | string | N | null | 用户ID(用于个性化,预留) | |
| 132 | | `session_id` | string | N | null | 会话ID(用于分析,预留) | | 134 | | `session_id` | string | N | null | 会话ID(用于分析,预留) | |
| @@ -326,6 +328,51 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -326,6 +328,51 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 326 | - `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) | 328 | - `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) |
| 327 | - `ranges`: 范围定义(仅当 type='range' 时需要) | 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,7 +514,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 467 | | `results[].spu_id` | string | SPU ID | | 514 | | `results[].spu_id` | string | SPU ID | |
| 468 | | `results[].title` | string | 商品标题 | | 515 | | `results[].title` | string | 商品标题 | |
| 469 | | `results[].price` | float | 价格(min_price) | | 516 | | `results[].price` | float | 价格(min_price) | |
| 470 | -| `results[].skus` | array | SKU列表 | | 517 | +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) | |
| 471 | | `results[].relevance_score` | float | 相关性分数 | | 518 | | `results[].relevance_score` | float | 相关性分数 | |
| 472 | | `total` | integer | 匹配的总文档数 | | 519 | | `total` | integer | 匹配的总文档数 | |
| 473 | | `max_score` | float | 最高相关性分数 | | 520 | | `max_score` | float | 最高相关性分数 | |
| @@ -548,7 +595,24 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -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 | **需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 | 617 | **需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 |
| 554 | 618 | ||
| @@ -569,7 +633,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -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,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,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 | **需求**: 搜索"手机",筛选color为"white"的商品 | 695 | **需求**: 搜索"手机",筛选color为"white"的商品 |
| 632 | 696 | ||
| @@ -644,7 +708,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -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 | **需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品 | 713 | **需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品 |
| 650 | 714 | ||
| @@ -662,7 +726,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -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,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,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,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 | **需求**: 获取第2页结果(每页20条) | 791 | **需求**: 获取第2页结果(每页20条) |
| 728 | 792 |
docs/搜索API速查表.md
| @@ -121,6 +121,37 @@ POST /search/ | @@ -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 | ```bash | 157 | ```bash |
| @@ -180,7 +211,8 @@ Headers: X-Tenant-ID: 2 | @@ -180,7 +211,8 @@ Headers: X-Tenant-ID: 2 | ||
| 180 | "specifications.size" | 211 | "specifications.size" |
| 181 | ], | 212 | ], |
| 182 | "sort_by": "min_price", | 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,7 +243,7 @@ Headers: X-Tenant-ID: 2 | ||
| 211 | "specifications": [ | 243 | "specifications": [ |
| 212 | {"sku_id": "sku_001", "name": "color", "value": "white"} | 244 | {"sku_id": "sku_001", "name": "color", "value": "white"} |
| 213 | ], | 245 | ], |
| 214 | - "skus": [...], | 246 | + "skus": [...], // 如果指定了sku_filter_dimension,则返回过滤后的SKU(每个维度值一个) |
| 215 | "relevance_score": 8.5 | 247 | "relevance_score": 8.5 |
| 216 | } | 248 | } |
| 217 | ], | 249 | ], |
docs/系统设计文档.md
| @@ -31,124 +31,154 @@ | @@ -31,124 +31,154 @@ | ||
| 31 | { | 31 | { |
| 32 | "tenant_id": "1", | 32 | "tenant_id": "1", |
| 33 | "spu_id": "123", | 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 | "skus": [ | 62 | "skus": [ |
| 36 | { | 63 | { |
| 37 | "sku_id": "456", | 64 | "sku_id": "456", |
| 38 | - "title": "黑色", | ||
| 39 | "price": 199.99, | 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 | ### 2.2 索引结构配置(查询域配置) | 183 | ### 2.2 索引结构配置(查询域配置) |
| 154 | 184 | ||
| @@ -217,6 +247,7 @@ indexes: | @@ -217,6 +247,7 @@ indexes: | ||
| 217 | **实现模块**: | 247 | **实现模块**: |
| 218 | - `search/es_query_builder.py` - ES 查询构建器(单层架构) | 248 | - `search/es_query_builder.py` - ES 查询构建器(单层架构) |
| 219 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) | 249 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) |
| 250 | +- `search/query_config.py` - 查询配置(字段 boost、查询域等) | ||
| 220 | 251 | ||
| 221 | --- | 252 | --- |
| 222 | 253 | ||
| @@ -233,26 +264,47 @@ indexes: | @@ -233,26 +264,47 @@ indexes: | ||
| 233 | 264 | ||
| 234 | ### 3.2 数据导入方式 | 265 | ### 3.2 数据导入方式 |
| 235 | 266 | ||
| 236 | -**Pipeline层决定数据源**: | ||
| 237 | -- 数据导入流程是写死的脚本,不依赖配置 | ||
| 238 | -- 配置只关注ES搜索相关的内容 | ||
| 239 | -- 数据源映射逻辑写死在转换器代码中 | 267 | +**数据源统一**: |
| 268 | +- 所有租户使用相同的MySQL表结构(店匠标准表) | ||
| 269 | +- 数据转换逻辑写死在转换器代码中 | ||
| 270 | +- 索引结构硬编码,不依赖配置 | ||
| 240 | 271 | ||
| 241 | -#### Base配置数据导入(店匠通用) | 272 | +#### 数据导入流程(店匠通用) |
| 242 | 273 | ||
| 243 | **脚本**:`scripts/ingest_shoplazza.py` | 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 | 2. **数据转换**(`indexer/spu_transformer.py`): | 282 | 2. **数据转换**(`indexer/spu_transformer.py`): |
| 248 | - 按`spu_id`和`tenant_id`关联SPU和SKU数据 | 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 | - 注入`tenant_id`字段 | 302 | - 注入`tenant_id`字段 |
| 303 | + | ||
| 253 | 3. **索引创建**: | 304 | 3. **索引创建**: |
| 254 | - - 根据配置生成ES mapping | 305 | + - 从`mappings/search_products.json`加载ES mapping |
| 255 | - 创建或更新`search_products`索引 | 306 | - 创建或更新`search_products`索引 |
| 307 | + | ||
| 256 | 4. **批量入库**: | 308 | 4. **批量入库**: |
| 257 | - 批量写入ES(默认每批500条) | 309 | - 批量写入ES(默认每批500条) |
| 258 | - 错误处理和重试机制 | 310 | - 错误处理和重试机制 |
| @@ -424,12 +476,19 @@ laptop AND (gaming OR professional) ANDNOT cheap | @@ -424,12 +476,19 @@ laptop AND (gaming OR professional) ANDNOT cheap | ||
| 424 | 2. **查询构建**(简化架构): | 476 | 2. **查询构建**(简化架构): |
| 425 | - **结构**: `filters AND (text_recall OR embedding_recall)` | 477 | - **结构**: `filters AND (text_recall OR embedding_recall)` |
| 426 | - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) | 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 | - **text_recall**: 文本相关性召回 | 485 | - **text_recall**: 文本相关性召回 |
| 428 | - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`) | 486 | - 同时搜索中英文字段(`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 | 487 | - 使用 `multi_match` 查询,支持字段 boost |
| 488 | + - 中文字段使用中文分词器,英文字段使用英文分析器 | ||
| 430 | - **embedding_recall**: 向量召回(KNN) | 489 | - **embedding_recall**: 向量召回(KNN) |
| 431 | - 使用 `title_embedding` 字段进行 KNN 搜索 | 490 | - 使用 `title_embedding` 字段进行 KNN 搜索 |
| 432 | - - ES 自动与文本召回合并 | 491 | + - ES 自动与文本召回合并(OR逻辑) |
| 433 | - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) | 492 | - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) |
| 434 | 493 | ||
| 435 | #### 查询结构示例 | 494 | #### 查询结构示例 |
| @@ -530,10 +589,13 @@ ranking: | @@ -530,10 +589,13 @@ ranking: | ||
| 530 | 589 | ||
| 531 | ### 6.4 搜索功能 | 590 | ### 6.4 搜索功能 |
| 532 | - ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) | 591 | - ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) |
| 533 | -- ✅ 多语言查询构建(语言路由、字段映射) | 592 | +- ✅ 多语言查询构建(同时搜索中英文字段) |
| 534 | - ✅ 语义搜索(KNN 检索) | 593 | - ✅ 语义搜索(KNN 检索) |
| 535 | - ✅ 相关性排序(BM25 + 向量相似度) | 594 | - ✅ 相关性排序(BM25 + 向量相似度) |
| 536 | - ✅ 结果聚合(Faceted Search) | 595 | - ✅ 结果聚合(Faceted Search) |
| 596 | +- ✅ Specifications嵌套过滤(单个和多个规格,OR逻辑) | ||
| 597 | +- ✅ Specifications嵌套分面(所有规格名称和指定规格名称) | ||
| 598 | +- ✅ SKU筛选(按维度过滤,应用层实现) | ||
| 537 | 599 | ||
| 538 | ### 6.5 API 服务 | 600 | ### 6.5 API 服务 |
| 539 | - ✅ RESTful API(FastAPI) | 601 | - ✅ RESTful API(FastAPI) |
| @@ -542,12 +604,16 @@ ranking: | @@ -542,12 +604,16 @@ ranking: | ||
| 542 | - ✅ 前端界面(HTML + JavaScript) | 604 | - ✅ 前端界面(HTML + JavaScript) |
| 543 | - ✅ 租户隔离(tenant_id过滤) | 605 | - ✅ 租户隔离(tenant_id过滤) |
| 544 | 606 | ||
| 545 | -### 6.6 Base配置(店匠通用) | 607 | +### 6.6 索引结构(店匠通用) |
| 546 | - ✅ SPU级别索引结构 | 608 | - ✅ SPU级别索引结构 |
| 547 | -- ✅ 嵌套skus字段 | 609 | +- ✅ 多语言字段支持(中英文) |
| 610 | +- ✅ 嵌套字段(skus, specifications, image_embedding) | ||
| 611 | +- ✅ 规格字段(specifications)支持过滤和分面 | ||
| 612 | +- ✅ 扁平化字段(价格、库存等)用于过滤和排序 | ||
| 548 | - ✅ 统一索引(search_products) | 613 | - ✅ 统一索引(search_products) |
| 549 | - ✅ 租户隔离(tenant_id) | 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,25 +702,30 @@ Elasticsearch | ||
| 636 | 702 | ||
| 637 | - **简单模式**:字符串列表(字段名),使用默认配置 | 703 | - **简单模式**:字符串列表(字段名),使用默认配置 |
| 638 | ```json | 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 | - **高级模式**:FacetConfig 对象列表,支持自定义配置 | 712 | - **高级模式**:FacetConfig 对象列表,支持自定义配置 |
| 643 | ```json | 713 | ```json |
| 644 | [ | 714 | [ |
| 645 | { | 715 | { |
| 646 | - "field": "category.keyword", | 716 | + "field": "category1_name", |
| 647 | "size": 15, | 717 | "size": 15, |
| 648 | "type": "terms" | 718 | "type": "terms" |
| 649 | }, | 719 | }, |
| 650 | { | 720 | { |
| 651 | - "field": "price", | 721 | + "field": "min_price", |
| 652 | "type": "range", | 722 | "type": "range", |
| 653 | "ranges": [ | 723 | "ranges": [ |
| 654 | {"key": "0-50", "to": 50}, | 724 | {"key": "0-50", "to": 50}, |
| 655 | {"key": "50-100", "from": 50, "to": 100} | 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,7 +733,10 @@ Elasticsearch | ||
| 662 | 1. API 层:接收 `List[Union[str, FacetConfig]]` | 733 | 1. API 层:接收 `List[Union[str, FacetConfig]]` |
| 663 | 2. Searcher 层:透传,不做转换 | 734 | 2. Searcher 层:透传,不做转换 |
| 664 | 3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 | 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 | #### 8.3.3 Range Filters 数据流 | 741 | #### 8.3.3 Range Filters 数据流 |
| 668 | 742 | ||
| @@ -680,8 +754,8 @@ class RangeFilter(BaseModel): | @@ -680,8 +754,8 @@ class RangeFilter(BaseModel): | ||
| 680 | **示例**: | 754 | **示例**: |
| 681 | ```json | 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,6 +770,38 @@ class RangeFilter(BaseModel): | ||
| 696 | - 类型支持:支持数值(float)和日期时间字符串(ISO 格式) | 770 | - 类型支持:支持数值(float)和日期时间字符串(ISO 格式) |
| 697 | - 统一约定:所有范围过滤都使用 RangeFilter 模型 | 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 | #### 8.3.4 响应 Facets 数据流 | 805 | #### 8.3.4 响应 Facets 数据流 |
| 700 | 806 | ||
| 701 | **输出格式**:`List[FacetResult]` | 807 | **输出格式**:`List[FacetResult]` |
| @@ -711,16 +817,63 @@ class FacetResult(BaseModel): | @@ -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 | - 类型安全:使用 Pydantic 模型确保数据结构一致性 | 831 | - 类型安全:使用 Pydantic 模型确保数据结构一致性 |
| 720 | - 自动序列化:模型自动转换为 JSON,无需手动处理 | 832 | - 自动序列化:模型自动转换为 JSON,无需手动处理 |
| 721 | - 统一约定:所有响应都使用标准化的 Pydantic 模型 | 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 | 1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 | 878 | 1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 |
| 726 | 2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 | 879 | 2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 |
| @@ -729,14 +882,14 @@ class FacetResult(BaseModel): | @@ -729,14 +882,14 @@ class FacetResult(BaseModel): | ||
| 729 | 5. **数据验证**:自动验证输入数据,减少错误处理代码 | 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,7 +24,7 @@ | ||
| 24 | - 所有客户共享同一个Elasticsearch索引:`search_products` | 24 | - 所有客户共享同一个Elasticsearch索引:`search_products` |
| 25 | - 索引粒度:SPU级别(每个文档代表一个SPU) | 25 | - 索引粒度:SPU级别(每个文档代表一个SPU) |
| 26 | - 数据隔离:通过`tenant_id`字段实现租户隔离 | 26 | - 数据隔离:通过`tenant_id`字段实现租户隔离 |
| 27 | -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组(SKU变体) | 27 | +- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 |
| 28 | 28 | ||
| 29 | **索引文档结构**: | 29 | **索引文档结构**: |
| 30 | ```json | 30 | ```json |
| @@ -215,7 +215,7 @@ indexes: | @@ -215,7 +215,7 @@ indexes: | ||
| 215 | 4. 组合多个语言查询的结果,提高召回率 | 215 | 4. 组合多个语言查询的结果,提高召回率 |
| 216 | 216 | ||
| 217 | **实现模块**: | 217 | **实现模块**: |
| 218 | -- `search/multilang_query_builder.py` - 多语言查询构建器 | 218 | +- `search/es_query_builder.py` - ES 查询构建器(单层架构) |
| 219 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) | 219 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) |
| 220 | 220 | ||
| 221 | --- | 221 | --- |
| @@ -345,7 +345,7 @@ query_config: | @@ -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,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap | ||
| 421 | - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) | 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 | ### 5.3 相关性计算(Ranking) | 476 | ### 5.3 相关性计算(Ranking) |
| 451 | 477 | ||
| @@ -610,14 +636,14 @@ Elasticsearch | @@ -610,14 +636,14 @@ Elasticsearch | ||
| 610 | 636 | ||
| 611 | - **简单模式**:字符串列表(字段名),使用默认配置 | 637 | - **简单模式**:字符串列表(字段名),使用默认配置 |
| 612 | ```json | 638 | ```json |
| 613 | - ["categoryName_keyword", "brandName_keyword"] | 639 | + ["category.keyword", "vendor.keyword"] |
| 614 | ``` | 640 | ``` |
| 615 | 641 | ||
| 616 | - **高级模式**:FacetConfig 对象列表,支持自定义配置 | 642 | - **高级模式**:FacetConfig 对象列表,支持自定义配置 |
| 617 | ```json | 643 | ```json |
| 618 | [ | 644 | [ |
| 619 | { | 645 | { |
| 620 | - "field": "categoryName_keyword", | 646 | + "field": "category.keyword", |
| 621 | "size": 15, | 647 | "size": 15, |
| 622 | "type": "terms" | 648 | "type": "terms" |
| 623 | }, | 649 | }, |
search/searcher.py
| @@ -135,6 +135,7 @@ class Searcher: | @@ -135,6 +135,7 @@ class Searcher: | ||
| 135 | sort_order: Optional[str] = "desc", | 135 | sort_order: Optional[str] = "desc", |
| 136 | debug: bool = False, | 136 | debug: bool = False, |
| 137 | language: str = "zh", | 137 | language: str = "zh", |
| 138 | + sku_filter_dimension: Optional[str] = None, | ||
| 138 | ) -> SearchResult: | 139 | ) -> SearchResult: |
| 139 | """ | 140 | """ |
| 140 | Execute search query (外部友好格式). | 141 | Execute search query (外部友好格式). |
| @@ -376,7 +377,8 @@ class Searcher: | @@ -376,7 +377,8 @@ class Searcher: | ||
| 376 | formatted_results = ResultFormatter.format_search_results( | 377 | formatted_results = ResultFormatter.format_search_results( |
| 377 | es_hits, | 378 | es_hits, |
| 378 | max_score, | 379 | max_score, |
| 379 | - language=language | 380 | + language=language, |
| 381 | + sku_filter_dimension=sku_filter_dimension | ||
| 380 | ) | 382 | ) |
| 381 | 383 | ||
| 382 | # Format facets | 384 | # Format facets |
| @@ -542,7 +544,12 @@ class Searcher: | @@ -542,7 +544,12 @@ class Searcher: | ||
| 542 | max_score = es_response.get('hits', {}).get('max_score') or 0.0 | 544 | max_score = es_response.get('hits', {}).get('max_score') or 0.0 |
| 543 | 545 | ||
| 544 | # Format results using ResultFormatter | 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 | return SearchResult( | 554 | return SearchResult( |
| 548 | results=formatted_results, | 555 | results=formatted_results, |