From ca91352aa97b0a95479a570f0278d840f5709b20 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 27 Nov 2025 09:55:30 +0800 Subject: [PATCH] 更新文档 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 查询结构 添加了两种分面模式的说明和示例 更新了“分面字段”说明,明确支持指定规格名称的分面 --- api/models.py | 6 ++++++ api/result_formatter.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- api/routes/search.py | 4 +++- docs/搜索API对接指南.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- docs/搜索API速查表.md | 36 ++++++++++++++++++++++++++++++++++-- docs/系统设计文档.md | 419 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------- docs/系统设计文档v1.md | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/设计文档.md | 716 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- search/searcher.py | 11 +++++++++-- 9 files changed, 1249 insertions(+), 865 deletions(-) create mode 100644 docs/系统设计文档v1.md delete mode 100644 docs/设计文档.md diff --git a/api/models.py b/api/models.py index aab3071..375d2e9 100644 --- a/api/models.py +++ b/api/models.py @@ -145,6 +145,12 @@ class SearchRequest(BaseModel): highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)") debug: bool = Field(False, description="是否返回调试信息") + # SKU筛选参数 + sku_filter_dimension: Optional[str] = Field( + None, + description="子SKU筛选维度(店铺配置)。指定后,每个SPU下的SKU将按该维度分组,每组选择第一个SKU返回。例如:'color'表示按颜色分组,每种颜色选一款。支持的值:'option1'、'option2'、'option3'或specifications中的name(如'color'、'size')" + ) + # 个性化参数(预留) user_id: Optional[str] = Field(None, description="用户ID,用于个性化搜索和推荐") session_id: Optional[str] = Field(None, description="会话ID,用于搜索分析") diff --git a/api/result_formatter.py b/api/result_formatter.py index 39af909..790aac1 100644 --- a/api/result_formatter.py +++ b/api/result_formatter.py @@ -13,7 +13,8 @@ class ResultFormatter: def format_search_results( es_hits: List[Dict[str, Any]], max_score: float = 1.0, - language: str = "zh" + language: str = "zh", + sku_filter_dimension: Optional[str] = None ) -> List[SpuResult]: """ Convert ES hits to SpuResult list. @@ -72,11 +73,29 @@ class ResultFormatter: price=sku_entry.get('price'), compare_at_price=sku_entry.get('compare_at_price'), sku=sku_entry.get('sku'), + sku_code=sku_entry.get('sku_code'), stock=sku_entry.get('stock', 0), + weight=sku_entry.get('weight'), + weight_unit=sku_entry.get('weight_unit'), + option1_value=sku_entry.get('option1_value'), + option2_value=sku_entry.get('option2_value'), + option3_value=sku_entry.get('option3_value'), + image_src=sku_entry.get('image_src'), options=sku_entry.get('options') ) skus.append(sku) + # Apply SKU filtering if dimension is specified + if sku_filter_dimension and skus: + skus = ResultFormatter._filter_skus_by_dimension( + skus, + sku_filter_dimension, + source.get('option1_name'), + source.get('option2_name'), + source.get('option3_name'), + source.get('specifications', []) + ) + # Determine in_stock (any sku has stock > 0) in_stock = any(sku.stock > 0 for sku in skus) if skus else True @@ -119,6 +138,81 @@ class ResultFormatter: return results @staticmethod + def _filter_skus_by_dimension( + skus: List[SkuResult], + dimension: str, + option1_name: Optional[str] = None, + option2_name: Optional[str] = None, + option3_name: Optional[str] = None, + specifications: Optional[List[Dict[str, Any]]] = None + ) -> List[SkuResult]: + """ + Filter SKUs by dimension, keeping only one SKU per dimension value. + + Args: + skus: List of SKU results to filter + dimension: Filter dimension, can be: + - 'option1', 'option2', 'option3': Direct option field + - A specification name (e.g., 'color', 'size'): Match by option name + option1_name: Name of option1 (e.g., 'color') + option2_name: Name of option2 (e.g., 'size') + option3_name: Name of option3 + specifications: List of specifications (for reference) + + Returns: + Filtered list of SKUs (one per dimension value) + """ + if not skus: + return skus + + # Determine which field to use for filtering + filter_field = None + + # Direct option field (option1, option2, option3) + if dimension.lower() == 'option1': + filter_field = 'option1_value' + elif dimension.lower() == 'option2': + filter_field = 'option2_value' + elif dimension.lower() == 'option3': + filter_field = 'option3_value' + else: + # Try to match by option name + dimension_lower = dimension.lower() + if option1_name and option1_name.lower() == dimension_lower: + filter_field = 'option1_value' + elif option2_name and option2_name.lower() == dimension_lower: + filter_field = 'option2_value' + elif option3_name and option3_name.lower() == dimension_lower: + filter_field = 'option3_value' + + # If no matching field found, return all SKUs (no filtering) + if not filter_field: + return skus + + # Group SKUs by dimension value and select first one from each group + dimension_groups: Dict[str, SkuResult] = {} + + for sku in skus: + # Get dimension value from the determined field + dimension_value = None + if filter_field == 'option1_value': + dimension_value = sku.option1_value + elif filter_field == 'option2_value': + dimension_value = sku.option2_value + elif filter_field == 'option3_value': + dimension_value = sku.option3_value + + # Use empty string as key for None values + key = str(dimension_value) if dimension_value is not None else '' + + # Keep first SKU for each dimension value + if key not in dimension_groups: + dimension_groups[key] = sku + + # Return filtered SKUs (one per dimension value) + return list(dimension_groups.values()) + + @staticmethod def format_facets( es_aggregations: Dict[str, Any], facet_configs: Optional[List[Any]] = None diff --git a/api/routes/search.py b/api/routes/search.py index 5cce887..3d28122 100644 --- a/api/routes/search.py +++ b/api/routes/search.py @@ -96,6 +96,7 @@ async def search(request: SearchRequest, http_request: Request): sort_order=request.sort_order, debug=request.debug, language=request.language, + sku_filter_dimension=request.sku_filter_dimension, ) # Include performance summary in response @@ -291,7 +292,8 @@ async def instant_search( query=q, tenant_id=tenant_id, size=size, - from_=0 + from_=0, + sku_filter_dimension=None # Instant search doesn't support SKU filtering ) return SearchResponse( diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md index 7929749..95225f1 100644 --- a/docs/搜索API对接指南.md +++ b/docs/搜索API对接指南.md @@ -105,6 +105,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ "sort_by": "string", "sort_order": "desc", "min_score": 0.0, + "sku_filter_dimension": "string", "debug": false, "user_id": "string", "session_id": "string" @@ -127,6 +128,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | `min_score` | float | N | null | 最小相关性分数阈值 | +| `sku_filter_dimension` | string | N | null | 子SKU筛选维度(店铺配置)。指定后,每个SPU下的SKU将按该维度分组,每组选择第一个SKU返回。支持的值:`option1`、`option2`、`option3` 或 specifications 中的 name(如 `color`、`size`)。详见下文说明 | | `debug` | boolean | N | false | 是否返回调试信息 | | `user_id` | string | N | null | 用户ID(用于个性化,预留) | | `session_id` | string | N | null | 会话ID(用于分析,预留) | @@ -326,6 +328,51 @@ curl -X POST "http://120.76.41.98:6002/search/" \ - `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) - `ranges`: 范围定义(仅当 type='range' 时需要) +### SKU筛选维度 (sku_filter_dimension) + +**功能说明**: +`sku_filter_dimension` 用于控制每个SPU下返回的SKU数量。当指定此参数后,系统会按指定维度对SKU进行分组,每个分组只返回第一个SKU(从简实现,选择该维度下的第一款)。 + +**使用场景**: +- 店铺配置了SKU筛选维度(如 `color`),希望每个SPU下每种颜色只显示一个SKU +- 减少前端展示的SKU数量,提升页面加载性能 +- 避免展示过多重复的SKU选项 + +**支持的维度值**: +1. **直接选项字段**: `option1`、`option2`、`option3` + - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组 + +2. **规格名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配 + - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: "color"` 来按颜色分组 + +**示例**: + +**按颜色筛选(假设 option1_name = "color")**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": "color" +} +``` + +**按选项1筛选**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": "option1" +} +``` + +**按选项2筛选**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": "option2" +} +``` + +--- + ### 布尔表达式语法 搜索查询支持布尔表达式,提供更灵活的搜索能力。 @@ -467,7 +514,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | `results[].spu_id` | string | SPU ID | | `results[].title` | string | 商品标题 | | `results[].price` | float | 价格(min_price) | -| `results[].skus` | array | SKU列表 | +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) | | `results[].relevance_score` | float | 相关性分数 | | `total` | integer | 匹配的总文档数 | | `max_score` | float | 最高相关性分数 | @@ -548,7 +595,24 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景2:带筛选的商品搜索 +### 场景2:SKU筛选(按维度过滤) + +**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU + +```json +{ + "query": "芭比娃娃", + "size": 20, + "sku_filter_dimension": "color" +} +``` + +**说明**: +- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: "color"` 可以按颜色分组 +- 每个SPU下,每种颜色只会返回第一个SKU +- 如果维度不匹配,返回所有SKU(不进行过滤) + +### 场景3:带筛选的商品搜索 **需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 @@ -569,7 +633,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景3:带分面的商品搜索 +### 场景4:带分面的商品搜索 **需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 @@ -586,7 +650,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景4:多条件组合搜索 +### 场景5:多条件组合搜索 **需求**: 搜索"手机",筛选多个品牌,价格范围,并获取分面统计 @@ -626,7 +690,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景5:规格过滤搜索 +### 场景6:规格过滤搜索 **需求**: 搜索"手机",筛选color为"white"的商品 @@ -644,7 +708,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景6:多个规格过滤(OR逻辑) +### 场景7:多个规格过滤(OR逻辑) **需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品 @@ -662,7 +726,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景7:规格分面搜索 +### 场景8:规格分面搜索 **需求**: 搜索"手机",获取所有规格的分面统计 @@ -686,7 +750,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景8:组合过滤和分面 +### 场景9:组合过滤和分面 **需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 @@ -711,7 +775,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景9:布尔表达式搜索 +### 场景10:布尔表达式搜索 **需求**: 搜索包含"手机"和"智能"的商品,排除"二手" @@ -722,7 +786,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` -### 场景10:分页查询 +### 场景11:分页查询 **需求**: 获取第2页结果(每页20条) diff --git a/docs/搜索API速查表.md b/docs/搜索API速查表.md index 4393f4d..e40a189 100644 --- a/docs/搜索API速查表.md +++ b/docs/搜索API速查表.md @@ -121,6 +121,37 @@ POST /search/ --- +## SKU筛选维度 + +**功能**: 按指定维度对每个SPU下的SKU进行分组,每组只返回第一个SKU。 + +```bash +{ + "query": "芭比娃娃", + "sku_filter_dimension": "color" // 按颜色筛选(假设option1_name="color") +} +``` + +**支持的维度值**: +- `option1`, `option2`, `option3`: 直接使用选项字段 +- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配 + +**示例**: +```bash +// 按选项1筛选 +{"sku_filter_dimension": "option1"} + +// 按颜色筛选(如果option1_name="color") +{"sku_filter_dimension": "color"} + +// 按尺寸筛选(如果option2_name="size") +{"sku_filter_dimension": "size"} +``` + +**性能说明**: 在应用层过滤,不影响ES查询性能,只对返回结果进行过滤。 + +--- + ## 排序 ```bash @@ -180,7 +211,8 @@ Headers: X-Tenant-ID: 2 "specifications.size" ], "sort_by": "min_price", - "sort_order": "asc" + "sort_order": "asc", + "sku_filter_dimension": "color" // 可选:按颜色筛选SKU } ``` @@ -211,7 +243,7 @@ Headers: X-Tenant-ID: 2 "specifications": [ {"sku_id": "sku_001", "name": "color", "value": "white"} ], - "skus": [...], + "skus": [...], // 如果指定了sku_filter_dimension,则返回过滤后的SKU(每个维度值一个) "relevance_score": 8.5 } ], diff --git a/docs/系统设计文档.md b/docs/系统设计文档.md index 076dddc..7286d9a 100644 --- a/docs/系统设计文档.md +++ b/docs/系统设计文档.md @@ -31,124 +31,154 @@ { "tenant_id": "1", "spu_id": "123", - "title": "蓝牙耳机", + "title_zh": "蓝牙耳机", + "title_en": "Bluetooth Headphones", + "brief_zh": "高品质蓝牙耳机", + "brief_en": "High-quality Bluetooth headphones", + "category_name": "电子产品", + "category_path_zh": "电子产品/音频设备/耳机", + "category_path_en": "Electronics/Audio/Headphones", + "category1_name": "电子产品", + "category2_name": "音频设备", + "category3_name": "耳机", + "vendor_zh": "品牌A", + "vendor_en": "Brand A", + "min_price": 199.99, + "max_price": 299.99, + "option1_name": "color", + "option2_name": "size", + "specifications": [ + { + "sku_id": "456", + "name": "color", + "value": "black" + }, + { + "sku_id": "456", + "name": "size", + "value": "large" + } + ], "skus": [ { "sku_id": "456", - "title": "黑色", "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 + "compare_at_price": 249.99, + "sku_code": "SKU-123-1", + "stock": 50, + "weight": 0.2, + "weight_unit": "kg", + "option1_value": "black", + "option2_value": "large", + "option3_value": null, + "image_src": "https://example.com/image.jpg" } ], - "min_price": 199.99, - "max_price": 299.99 + "title_embedding": [0.1, 0.2, ...], // 1024维向量 + "image_embedding": [ + { + "vector": [0.1, 0.2, ...], // 1024维向量 + "url": "https://example.com/image.jpg" + } + ] } ``` -### 1.3 配置化方案 +### 1.3 索引结构简化方案 -**配置分离原则**: -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 -- **数据导入流程**:写死的脚本,不依赖配置 +**简化原则**: +- **硬编码映射**:ES mapping 结构直接定义在 JSON 文件中(`mappings/search_products.json`) +- **统一索引结构**:所有租户共享相同的索引结构,通过 `tenant_id` 隔离数据 +- **数据源统一**:所有租户使用相同的 MySQL 表结构(店匠标准表) +- **查询配置硬编码**:查询相关配置(字段 boost、查询域等)硬编码在 `search/query_config.py` -统一通过配置文件定义: -1. ES 字段定义(字段类型、分析器、boost等) -2. ES mapping 结构生成 -3. 查询域配置(indexes) -4. 排序和打分配置(function_score) +**索引结构特点**: +1. **多语言字段**:所有文本字段支持中英文(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`) +2. **嵌套字段**: + - `skus`: SKU 嵌套数组(包含价格、库存、选项值等) + - `specifications`: 规格嵌套数组(包含 name、value、sku_id) + - `image_embedding`: 图片向量嵌套数组 +3. **扁平化字段**:`sku_prices`, `sku_weights`, `total_inventory` 等用于过滤和排序 +4. **向量字段**:`title_embedding`(1024维)用于语义搜索 -**注意**:配置中**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` / `extension_table` - 数据表配置 -- `source_table` / `source_column` - 字段数据源映射 +**实现文件**: +- `mappings/search_products.json` - ES mapping 定义(硬编码) +- `search/query_config.py` - 查询配置(硬编码) +- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 --- -## 2. 配置系统实现 - -### 2.1 应用结构配置(字段定义) - -**配置文件位置**:`config/schema/{tenant_id}_config.yaml` - -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 - -**实现情况**: - -#### 字段类型支持 -- **TEXT**:文本字段,支持多语言分析器 -- **KEYWORD**:关键词字段,用于精确匹配和聚合 -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) -- **INT/LONG**:整数类型 -- **FLOAT/DOUBLE**:浮点数类型 -- **DATE**:日期类型 -- **BOOLEAN**:布尔类型 - -#### 分析器支持 -- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj) -- **english**:英文分析器 -- **russian**:俄文分析器 -- **arabic**:阿拉伯文分析器 -- **spanish**:西班牙文分析器 -- **japanese**:日文分析器 -- **standard**:标准分析器 -- **keyword**:关键词分析器 - -#### 字段配置示例(Base配置) - -```yaml -fields: - # 租户隔离字段(必需) - - name: "tenant_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 商品标识字段 - - name: "spu_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 文本搜索字段 - - name: "title" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 3.0 - index: true - store: true - - - name: "seo_keywords" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 2.0 - index: true - store: true - - # 嵌套skus字段 - - name: "skus" - type: "JSON" - nested: true - nested_properties: - sku_id: - type: "keyword" - price: - type: "float" - sku: - type: "keyword" -``` - -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 +## 2. 索引结构实现 + +### 2.1 硬编码映射方案 + +**实现方式**: +- ES mapping 直接定义在 `mappings/search_products.json` 文件中 +- 所有租户共享相同的索引结构 +- 查询配置硬编码在 `search/query_config.py` + +**索引字段结构**: + +#### 基础字段 +- `tenant_id` (keyword): 租户ID,用于数据隔离 +- `spu_id` (keyword): SPU唯一标识 +- `create_time`, `update_time` (date): 创建和更新时间 + +#### 多语言文本字段 +- `title_zh/en` (text): 标题(中英文) +- `brief_zh/en` (text): 短描述(中英文) +- `description_zh/en` (text): 详细描述(中英文) +- `vendor_zh/en` (text): 供应商/品牌(中英文) +- `category_path_zh/en` (text): 类目路径(中英文) +- `category_name_zh/en` (text): 类目名称(中英文) + +**分析器配置**: +- 中文字段:`hanlp_index`(索引时)/ `hanlp_standard`(查询时) +- 英文字段:`english` +- `vendor` 字段包含 `keyword` 子字段(normalizer: lowercase) + +#### 分类字段 +- `category_id` (keyword): 类目ID +- `category_name` (keyword): 类目名称 +- `category_level` (integer): 类目层级 +- `category1_name`, `category2_name`, `category3_name` (keyword): 多级类目名称 + +#### 规格字段(Specifications) +- `specifications` (nested): 规格嵌套数组 + - `sku_id` (keyword): SKU ID + - `name` (keyword): 规格名称(如 "color", "size") + - `value` (keyword): 规格值(如 "white", "256GB") + +**用途**: +- 支持按规格过滤:`{"specifications": {"name": "color", "value": "white"}}` +- 支持规格分面:`["specifications"]` 或 `["specifications.color"]` + +#### SKU嵌套字段 +- `skus` (nested): SKU嵌套数组 + - `sku_id`, `price`, `compare_at_price`, `sku_code` + - `stock`, `weight`, `weight_unit` + - `option1_value`, `option2_value`, `option3_value` + - `image_src` (index: false) + +#### 选项名称字段 +- `option1_name`, `option2_name`, `option3_name` (keyword): 选项名称(如 "color", "size") + +#### 扁平化字段 +- `min_price`, `max_price`, `compare_at_price` (float): 价格字段 +- `sku_prices` (float[]): 所有SKU价格数组 +- `sku_weights` (long[]): 所有SKU重量数组 +- `total_inventory` (long): 总库存 + +#### 向量字段 +- `title_embedding` (dense_vector, 1024维): 标题向量,用于语义搜索 +- `image_embedding` (nested): 图片向量数组 + - `vector` (dense_vector, 1024维) + - `url` (text) **实现模块**: -- `config/config_loader.py` - 配置加载器 -- `config/field_types.py` - 字段类型定义 -- `indexer/mapping_generator.py` - ES mapping 生成器 -- `indexer/data_transformer.py` - 数据转换器 +- `mappings/search_products.json` - ES mapping 定义 +- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 +- `search/query_config.py` - 查询配置(字段 boost、查询域等) ### 2.2 索引结构配置(查询域配置) @@ -217,6 +247,7 @@ indexes: **实现模块**: - `search/es_query_builder.py` - ES 查询构建器(单层架构) - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) +- `search/query_config.py` - 查询配置(字段 boost、查询域等) --- @@ -233,26 +264,47 @@ indexes: ### 3.2 数据导入方式 -**Pipeline层决定数据源**: -- 数据导入流程是写死的脚本,不依赖配置 -- 配置只关注ES搜索相关的内容 -- 数据源映射逻辑写死在转换器代码中 +**数据源统一**: +- 所有租户使用相同的MySQL表结构(店匠标准表) +- 数据转换逻辑写死在转换器代码中 +- 索引结构硬编码,不依赖配置 -#### Base配置数据导入(店匠通用) +#### 数据导入流程(店匠通用) **脚本**:`scripts/ingest_shoplazza.py` **数据流程**: -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 +1. **数据加载**: + - 从MySQL读取`shoplazza_product_spu`表(SPU数据) + - 从MySQL读取`shoplazza_product_sku`表(SKU数据) + - 从MySQL读取`shoplazza_product_option`表(选项定义) + 2. **数据转换**(`indexer/spu_transformer.py`): - 按`spu_id`和`tenant_id`关联SPU和SKU数据 - - 将SKU数据聚合为嵌套的`skus`数组 - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) - - 字段映射(写死在代码中,不依赖配置) + - **多语言字段映射**: + - MySQL的`title` → ES的`title_zh`(英文字段设为空) + - 其他文本字段类似处理 + - **分类字段映射**: + - 从SPU表的`category_path`解析多级类目(`category1_name`, `category2_name`, `category3_name`) + - 映射`category_id`, `category_name`, `category_level` + - **规格字段构建**(`specifications`): + - 从`shoplazza_product_option`表获取选项名称(`name`) + - 从SKU的`option1/2/3`字段获取选项值(`value`) + - 构建嵌套数组:`[{"sku_id": "...", "name": "color", "value": "white"}, ...]` + - **选项名称映射**: + - 从`shoplazza_product_option`表获取`option1_name`, `option2_name`, `option3_name` + - **SKU嵌套数组构建**: + - 包含所有SKU字段(价格、库存、选项值、图片等) + - **扁平化字段计算**: + - `min_price`, `max_price`: 从所有SKU价格计算 + - `sku_prices`: 所有SKU价格数组 + - `total_inventory`: SKU库存总和 - 注入`tenant_id`字段 + 3. **索引创建**: - - 根据配置生成ES mapping + - 从`mappings/search_products.json`加载ES mapping - 创建或更新`search_products`索引 + 4. **批量入库**: - 批量写入ES(默认每批500条) - 错误处理和重试机制 @@ -424,12 +476,19 @@ laptop AND (gaming OR professional) ANDNOT cheap 2. **查询构建**(简化架构): - **结构**: `filters AND (text_recall OR embedding_recall)` - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) + - 普通字段过滤:`{"category_name": "手机"}` + - 范围过滤:`{"min_price": {"gte": 50, "lte": 200}}` + - **Specifications嵌套过滤**: + - 单个规格:`{"specifications": {"name": "color", "value": "white"}}` + - 多个规格(OR):`{"specifications": [{"name": "color", "value": "white"}, {"name": "size", "value": "256GB"}]}` + - 使用ES的`nested`查询实现 - **text_recall**: 文本相关性召回 - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`) - 使用 `multi_match` 查询,支持字段 boost + - 中文字段使用中文分词器,英文字段使用英文分析器 - **embedding_recall**: 向量召回(KNN) - 使用 `title_embedding` 字段进行 KNN 搜索 - - ES 自动与文本召回合并 + - ES 自动与文本召回合并(OR逻辑) - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) #### 查询结构示例 @@ -530,10 +589,13 @@ ranking: ### 6.4 搜索功能 - ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) -- ✅ 多语言查询构建(语言路由、字段映射) +- ✅ 多语言查询构建(同时搜索中英文字段) - ✅ 语义搜索(KNN 检索) - ✅ 相关性排序(BM25 + 向量相似度) - ✅ 结果聚合(Faceted Search) +- ✅ Specifications嵌套过滤(单个和多个规格,OR逻辑) +- ✅ Specifications嵌套分面(所有规格名称和指定规格名称) +- ✅ SKU筛选(按维度过滤,应用层实现) ### 6.5 API 服务 - ✅ RESTful API(FastAPI) @@ -542,12 +604,16 @@ ranking: - ✅ 前端界面(HTML + JavaScript) - ✅ 租户隔离(tenant_id过滤) -### 6.6 Base配置(店匠通用) +### 6.6 索引结构(店匠通用) - ✅ SPU级别索引结构 -- ✅ 嵌套skus字段 +- ✅ 多语言字段支持(中英文) +- ✅ 嵌套字段(skus, specifications, image_embedding) +- ✅ 规格字段(specifications)支持过滤和分面 +- ✅ 扁平化字段(价格、库存等)用于过滤和排序 - ✅ 统一索引(search_products) - ✅ 租户隔离(tenant_id) -- ✅ 配置简化(移除MySQL相关配置) +- ✅ 硬编码映射(mappings/search_products.json) +- ✅ 硬编码查询配置(search/query_config.py) --- @@ -636,25 +702,30 @@ Elasticsearch - **简单模式**:字符串列表(字段名),使用默认配置 ```json - ["category.keyword", "vendor.keyword"] + ["category1_name", "category2_name", "specifications"] ``` +- **Specifications分面**: + - 所有规格名称:`"specifications"` - 返回所有name及其value列表 + - 指定规格名称:`"specifications.color"` - 只返回指定name的value列表 + - **高级模式**:FacetConfig 对象列表,支持自定义配置 ```json [ { - "field": "category.keyword", + "field": "category1_name", "size": 15, "type": "terms" }, { - "field": "price", + "field": "min_price", "type": "range", "ranges": [ {"key": "0-50", "to": 50}, {"key": "50-100", "from": 50, "to": 100} ] - } + }, + "specifications.color" // 指定规格名称的分面 ] ``` @@ -662,7 +733,10 @@ Elasticsearch 1. API 层:接收 `List[Union[str, FacetConfig]]` 2. Searcher 层:透传,不做转换 3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 -4. 输出:转换为 ES 聚合查询 + - 检测 `"specifications"` 或 `"specifications.{name}"` 格式 + - 构建对应的嵌套聚合查询 +4. 输出:转换为 ES 聚合查询(包括specifications嵌套聚合) +5. Result Formatter:格式化ES聚合结果,处理specifications嵌套结构 #### 8.3.3 Range Filters 数据流 @@ -680,8 +754,8 @@ class RangeFilter(BaseModel): **示例**: ```json { - "price": {"gte": 50, "lte": 200}, - "created_at": {"gte": "2023-01-01T00:00:00Z"} + "min_price": {"gte": 50, "lte": 200}, + "create_time": {"gte": "2023-01-01T00:00:00Z"} } ``` @@ -696,6 +770,38 @@ class RangeFilter(BaseModel): - 类型支持:支持数值(float)和日期时间字符串(ISO 格式) - 统一约定:所有范围过滤都使用 RangeFilter 模型 +#### 8.3.3.1 Specifications 过滤数据流 + +**输入格式**:`Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]` + +**单个规格过滤**: +```json +{ + "specifications": { + "name": "color", + "value": "white" + } +} +``` + +**多个规格过滤(OR逻辑)**: +```json +{ + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] +} +``` + +**数据流**: +1. API 层:接收 `filters` 字典,检测 `specifications` 键 +2. Searcher 层:透传 `filters` 字典 +3. ES Query Builder:检测 `specifications` 键,构建ES `nested` 查询 + - 单个规格:构建单个 `nested` 查询 + - 多个规格:构建多个 `nested` 查询,使用 `should` 组合(OR逻辑) +4. 输出:ES nested 查询(`nested.path=specifications` + `bool.must=[term(name), term(value)]`) + #### 8.3.4 响应 Facets 数据流 **输出格式**:`List[FacetResult]` @@ -711,16 +817,63 @@ class FacetResult(BaseModel): ``` **数据流**: -1. ES Response:返回聚合结果(字典格式) -2. Searcher 层:构建 `List[FacetResult]` 对象 -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) +1. ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合) +2. Result Formatter:格式化ES聚合结果 + - 处理普通terms聚合 + - 处理range聚合 + - **处理specifications嵌套聚合**: + - 所有规格名称:解析 `by_name` 聚合结构 + - 指定规格名称:解析 `filter_by_name` 聚合结构 +3. Searcher 层:构建 `List[FacetResult]` 对象 +4. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) **优势**: - 类型安全:使用 Pydantic 模型确保数据结构一致性 - 自动序列化:模型自动转换为 JSON,无需手动处理 - 统一约定:所有响应都使用标准化的 Pydantic 模型 -#### 8.3.5 统一约定的好处 +#### 8.3.5 SKU筛选数据流 + +**输入格式**:`Optional[str]` + +**支持的维度值**: +- `option1`, `option2`, `option3`: 直接使用选项字段 +- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配 + +**示例**: +```json +{ + "query": "手机", + "sku_filter_dimension": "color" +} +``` + +**数据流**: +1. API 层:接收 `sku_filter_dimension` 字符串参数 +2. Searcher 层:透传到 Result Formatter +3. Result Formatter:在格式化结果时,按指定维度对SKU进行分组 + - 如果维度是 `option1/2/3`,直接使用对应的 `option1_value/2/3` 字段 + - 如果维度是规格名称,通过 `option1_name/2/3` 匹配找到对应的 `option1_value/2/3` + - 每个分组选择第一个SKU返回 +4. 输出:过滤后的SKU列表(每个维度值一个SKU) + +**工作原理**: +1. 系统从ES返回所有SKU(不改变ES查询,保持性能) +2. 在结果格式化阶段,按指定维度对SKU进行分组 +3. 每个分组选择第一个SKU返回 +4. 如果维度不匹配或未找到,返回所有SKU(不进行过滤) + +**性能说明**: +- ✅ **推荐方案**: 在应用层过滤(当前实现) + - ES查询简单,不需要nested查询和join + - 只对返回的结果(通常10-20个SPU)进行过滤,数据量小 + - 实现简单,性能开销小 +- ❌ **不推荐**: 在ES查询时过滤 + - 需要nested查询和join,性能开销大 + - 实现复杂 + - 只对返回的结果需要过滤,不需要在ES层面过滤 + +#### 8.3.6 统一约定的好处 1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 @@ -729,14 +882,14 @@ class FacetResult(BaseModel): 5. **数据验证**:自动验证输入数据,减少错误处理代码 **实现模块**: -- `api/models.py` - 所有 Pydantic 模型定义 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) +- `api/models.py` - 所有 Pydantic 模型定义(包括 `SearchRequest`, `FacetConfig`, `RangeFilter` 等) +- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型,包括specifications分面处理和SKU筛选) +- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询,包括specifications过滤和分面) -## 9. 配置文件示例 +## 9. 索引结构文件 -**Base配置**(店匠通用):`config/schema/base/config.yaml` +**硬编码映射**(店匠通用):`mappings/search_products.json` -**其他客户配置**:`config/schema/tenant1/config.yaml` +**查询配置**(硬编码):`search/query_config.py` --- diff --git a/docs/系统设计文档v1.md b/docs/系统设计文档v1.md new file mode 100644 index 0000000..076dddc --- /dev/null +++ b/docs/系统设计文档v1.md @@ -0,0 +1,742 @@ +# 搜索引擎通用化开发进度 + +## 项目概述 + +对后端搜索技术 做通用化。 +通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 + + +**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 + +--- + +## 1. 原始数据层的约定 + +### 1.1 店匠主表 + +所有租户共用以下主表: +- `shoplazza_product_sku` - SKU级别商品数据 +- `shoplazza_product_spu` - SPU级别商品数据 + +### 1.2 索引结构(SPU维度) + +**统一索引架构**: +- 所有客户共享同一个Elasticsearch索引:`search_products` +- 索引粒度:SPU级别(每个文档代表一个SPU) +- 数据隔离:通过`tenant_id`字段实现租户隔离 +- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 + +**索引文档结构**: +```json +{ + "tenant_id": "1", + "spu_id": "123", + "title": "蓝牙耳机", + "skus": [ + { + "sku_id": "456", + "title": "黑色", + "price": 199.99, + "sku": "SKU-123-1", + "stock": 50 + } + ], + "min_price": 199.99, + "max_price": 299.99 +} +``` + +### 1.3 配置化方案 + +**配置分离原则**: +- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 +- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 +- **数据导入流程**:写死的脚本,不依赖配置 + +统一通过配置文件定义: +1. ES 字段定义(字段类型、分析器、boost等) +2. ES mapping 结构生成 +3. 查询域配置(indexes) +4. 排序和打分配置(function_score) + +**注意**:配置中**不包含**以下内容: +- `mysql_config` - MySQL数据库配置 +- `main_table` / `extension_table` - 数据表配置 +- `source_table` / `source_column` - 字段数据源映射 + +--- + +## 2. 配置系统实现 + +### 2.1 应用结构配置(字段定义) + +**配置文件位置**:`config/schema/{tenant_id}_config.yaml` + +**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 + +**实现情况**: + +#### 字段类型支持 +- **TEXT**:文本字段,支持多语言分析器 +- **KEYWORD**:关键词字段,用于精确匹配和聚合 +- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) +- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) +- **INT/LONG**:整数类型 +- **FLOAT/DOUBLE**:浮点数类型 +- **DATE**:日期类型 +- **BOOLEAN**:布尔类型 + +#### 分析器支持 +- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj) +- **english**:英文分析器 +- **russian**:俄文分析器 +- **arabic**:阿拉伯文分析器 +- **spanish**:西班牙文分析器 +- **japanese**:日文分析器 +- **standard**:标准分析器 +- **keyword**:关键词分析器 + +#### 字段配置示例(Base配置) + +```yaml +fields: + # 租户隔离字段(必需) + - name: "tenant_id" + type: "KEYWORD" + required: true + index: true + store: true + + # 商品标识字段 + - name: "spu_id" + type: "KEYWORD" + required: true + index: true + store: true + + # 文本搜索字段 + - name: "title" + type: "TEXT" + analyzer: "chinese_ecommerce" + boost: 3.0 + index: true + store: true + + - name: "seo_keywords" + type: "TEXT" + analyzer: "chinese_ecommerce" + boost: 2.0 + index: true + store: true + + # 嵌套skus字段 + - name: "skus" + type: "JSON" + nested: true + nested_properties: + sku_id: + type: "keyword" + price: + type: "float" + sku: + type: "keyword" +``` + +**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 + +**实现模块**: +- `config/config_loader.py` - 配置加载器 +- `config/field_types.py` - 字段类型定义 +- `indexer/mapping_generator.py` - ES mapping 生成器 +- `indexer/data_transformer.py` - 数据转换器 + +### 2.2 索引结构配置(查询域配置) + +**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 + +**实现情况**: + +#### 域(Domain)配置 +每个域定义了: +- 域名称(如 `default`, `title`, `category`, `brand`) +- 域标签(中文描述) +- 搜索字段列表 +- 默认分析器 +- 权重(boost) +- **多语言字段映射**(`language_field_mapping`) + +#### 多语言字段映射 + +支持将不同语言的查询路由到对应的字段: + +```yaml +indexes: + - name: "default" + label: "默认索引" + fields: + - "name" + - "enSpuName" + - "ruSkuName" + - "categoryName" + - "brandName" + analyzer: "chinese_ecommerce" + boost: 1.0 + language_field_mapping: + zh: + - "name" + - "categoryName" + - "brandName" + en: + - "enSpuName" + ru: + - "ruSkuName" + + - name: "title" + label: "标题索引" + fields: + - "name" + - "enSpuName" + - "ruSkuName" + analyzer: "chinese_ecommerce" + boost: 2.0 + language_field_mapping: + zh: + - "name" + en: + - "enSpuName" + ru: + - "ruSkuName" +``` + +**工作原理**: +1. 检测查询语言(中文、英文、俄文等) +2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 +3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 +4. 组合多个语言查询的结果,提高召回率 + +**实现模块**: +- `search/es_query_builder.py` - ES 查询构建器(单层架构) +- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) + +--- + +## 3. 数据导入流程 + +### 3.1 数据源 + +**店匠标准表**(Base配置使用): +- `shoplazza_product_spu` - SPU级别商品数据 +- `shoplazza_product_sku` - SKU级别商品数据 + +**其他客户表**(tenant1等): +- 使用各自的数据源表和扩展表 + +### 3.2 数据导入方式 + +**Pipeline层决定数据源**: +- 数据导入流程是写死的脚本,不依赖配置 +- 配置只关注ES搜索相关的内容 +- 数据源映射逻辑写死在转换器代码中 + +#### Base配置数据导入(店匠通用) + +**脚本**:`scripts/ingest_shoplazza.py` + +**数据流程**: +1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 +2. **数据转换**(`indexer/spu_transformer.py`): + - 按`spu_id`和`tenant_id`关联SPU和SKU数据 + - 将SKU数据聚合为嵌套的`skus`数组 + - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) + - 字段映射(写死在代码中,不依赖配置) + - 注入`tenant_id`字段 +3. **索引创建**: + - 根据配置生成ES mapping + - 创建或更新`search_products`索引 +4. **批量入库**: + - 批量写入ES(默认每批500条) + - 错误处理和重试机制 + +**命令行工具**: +```bash +python scripts/ingest_shoplazza.py \ + --db-host localhost \ + --db-port 3306 \ + --db-database saas \ + --db-username root \ + --db-password password \ + --tenant-id "1" \ + --config base \ + --es-host http://localhost:9200 \ + --recreate \ + --batch-size 500 +``` + +#### 其他客户数据导入 + +- 使用各自的数据转换器(如`indexer/data_transformer.py`) +- 数据源映射逻辑写死在各自的转换器中 +- 共享`search_products`索引,通过`tenant_id`隔离 + +**实现模块**: +- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) +- `indexer/data_transformer.py` - 通用数据转换器(其他客户) +- `indexer/bulk_indexer.py` - 批量索引器 +- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 + +--- + +## 4. QueryParser 实现 + + +### 4.1 查询改写(Query Rewriting) + +配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 +**实现情况**: + +#### 配置方式 +在 `query_config.rewrite_dictionary` 中配置查询改写规则: + +```yaml +query_config: + enable_query_rewrite: true + rewrite_dictionary: + "芭比": "brand:芭比 OR name:芭比娃娃" + "玩具": "category:玩具" + "消防": "category:消防 OR name:消防" +``` + +#### 功能特性 +- **精确匹配**:查询完全匹配词典 key 时,替换为 value +- **部分匹配**:查询包含词典 key 时,替换该部分 +- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) + +#### 实现模块 +- `query/query_rewriter.py` - 查询改写器 +- `query/query_parser.py` - 查询解析器(集成改写功能) + +### 4.2 翻译(Translation) + +**实现情况**: + +#### 配置方式 +```yaml +query_config: + supported_languages: + - "zh" + - "en" + - "ru" + default_language: "zh" + enable_translation: true + translation_service: "deepl" + translation_api_key: null # 通过环境变量设置 +``` + +#### 功能特性 +1. **语言检测**:自动检测查询语言 +2. **智能翻译**: + - 如果查询是中文,翻译为英文、俄文 + - 如果查询是英文,翻译为中文、俄文 + - 如果查询是其他语言,翻译为所有支持的语言 +3. **域感知翻译**: + - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 + - 避免不必要的翻译,提高效率 +4. **翻译缓存**:缓存翻译结果,避免重复调用 API + +#### 工作流程 +``` +查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) +``` + +#### 实现模块 +- `query/language_detector.py` - 语言检测器 +- `query/translator.py` - 翻译器(DeepL API) +- `query/query_parser.py` - 查询解析器(集成翻译功能) + +### 4.3 文本向量化(Text Embedding) + +如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 + +**实现情况**: + +#### 配置方式 +```yaml +query_config: + enable_text_embedding: true +``` + +#### 功能特性 +1. **条件生成**: + - 仅当 `enable_text_embedding=true` 时生成向量 + - 仅对 `default` 域查询生成向量 +2. **向量模型**:BGE-M3 模型(1024维向量) +3. **用途**:用于语义搜索(KNN 检索) + +#### 实现模块 +- `embeddings/bge_encoder.py` - BGE 文本编码器 +- `query/query_parser.py` - 查询解析器(集成向量生成) + +--- + +## 5. Searcher 实现 + +参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 +比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 + +查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 + +### 5.1 布尔表达式解析 + +**实现情况**: + +#### 支持的运算符 +- **AND**:所有项必须匹配 +- **OR**:任意项匹配 +- **RANK**:排序增强(类似 OR 但影响排序) +- **ANDNOT**:排除(第一项匹配,第二项不匹配) +- **()**:括号分组 + +#### 优先级(从高到低) +1. `()` - 括号 +2. `ANDNOT` - 排除 +3. `AND` - 与 +4. `OR` - 或 +5. `RANK` - 排序 + +#### 示例 +``` +laptop AND (gaming OR professional) ANDNOT cheap +``` + +#### 实现模块 +- `search/boolean_parser.py` - 布尔表达式解析器 +- `search/searcher.py` - 搜索器(集成布尔解析) + +### 5.2 多语言搜索 + +**实现情况**: + +#### 工作原理 +1. **查询解析**: + - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) + - 检测查询语言 + - 生成翻译 +2. **查询构建**(简化架构): + - **结构**: `filters AND (text_recall OR embedding_recall)` + - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) + - **text_recall**: 文本相关性召回 + - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`) + - 使用 `multi_match` 查询,支持字段 boost + - **embedding_recall**: 向量召回(KNN) + - 使用 `title_embedding` 字段进行 KNN 搜索 + - ES 自动与文本召回合并 + - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) + +#### 查询结构示例 +```json +{ + "query": { + "bool": { + "must": [ + { + "function_score": { + "query": { + "multi_match": { + "query": "手机", + "fields": [ + "title_zh^3.0", "title_en^3.0", + "brief_zh^1.5", "brief_en^1.5", + ... + ] + } + }, + "functions": [...] + } + } + ], + "filter": [ + {"term": {"tenant_id": "2"}}, + {"term": {"category_name": "手机"}} + ] + } + }, + "knn": { + "field": "title_embedding", + "query_vector": [...], + "k": 50, + "boost": 0.2 + } +} +``` + +#### 实现模块 +- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) +- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) + +### 5.3 相关性计算(Ranking) + +**实现情况**: + +#### 当前实现 +**公式**:`bm25() + 0.2 * text_embedding_relevance()` + +- **bm25()**:BM25 文本相关性得分 + - 包括多语言打分 + - 内部通过配置翻译为多种语言 + - 分别到对应的字段搜索 + - 中文字段使用中文分词器,英文字段使用英文分词器 +- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) + - 权重:0.2 + +#### 配置方式 +```yaml +ranking: + expression: "bm25() + 0.2*text_embedding_relevance()" + description: "BM25 text relevance combined with semantic embedding similarity" +``` + +#### 扩展性 +- 支持表达式配置(未来可扩展) +- 支持自定义函数(如 `timeliness()`, `field_value()`) + +#### 实现模块 +- `search/ranking_engine.py` - 排序引擎 +- `search/searcher.py` - 搜索器(集成排序功能) + +--- + +## 6. 已完成功能总结 + +### 6.1 配置系统 +- ✅ 字段定义配置(类型、分析器、来源表/列) +- ✅ 索引域配置(多域查询、多语言映射) +- ✅ 查询配置(改写词典、翻译配置) +- ✅ 排序配置(表达式配置) +- ✅ 配置验证(字段存在性、类型检查、分析器匹配) + +### 6.2 数据索引 +- ✅ 数据转换(字段映射、类型转换) +- ✅ 向量生成(文本向量、图片向量) +- ✅ 向量缓存(避免重复计算) +- ✅ 批量索引(错误处理、重试机制) +- ✅ ES mapping 自动生成 + +### 6.3 查询处理 +- ✅ 查询改写(词典配置) +- ✅ 语言检测 +- ✅ 多语言翻译(DeepL API) +- ✅ 文本向量化(BGE-M3) +- ✅ 域提取(支持 `domain:query` 语法) + +### 6.4 搜索功能 +- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) +- ✅ 多语言查询构建(语言路由、字段映射) +- ✅ 语义搜索(KNN 检索) +- ✅ 相关性排序(BM25 + 向量相似度) +- ✅ 结果聚合(Faceted Search) + +### 6.5 API 服务 +- ✅ RESTful API(FastAPI) +- ✅ 搜索接口(文本搜索、图片搜索) +- ✅ 文档查询接口 +- ✅ 前端界面(HTML + JavaScript) +- ✅ 租户隔离(tenant_id过滤) + +### 6.6 Base配置(店匠通用) +- ✅ SPU级别索引结构 +- ✅ 嵌套skus字段 +- ✅ 统一索引(search_products) +- ✅ 租户隔离(tenant_id) +- ✅ 配置简化(移除MySQL相关配置) + +--- + +## 7. 技术栈 + +- **后端**:Python 3.6+ +- **搜索引擎**:Elasticsearch +- **数据库**:MySQL(Shoplazza) +- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) +- **翻译服务**:DeepL API +- **API 框架**:FastAPI +- **前端**:HTML + JavaScript + +--- + +## 8. API响应格式 + +### 8.1 外部友好格式 + +API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: + +**响应结构**: +```json +{ + "results": [ + { + "spu_id": "123", + "title": "蓝牙耳机", + "skus": [ + { + "sku_id": "456", + "price": 199.99, + "sku": "SKU-123-1", + "stock": 50 + } + ], + "relevance_score": 0.95 + } + ], + "total": 10, + "facets": [...], + "suggestions": [], + "related_searches": [] +} +``` + +**主要变化**: +- 结构化结果(`SpuResult`和`SkuResult`) +- 嵌套skus数组 +- 无ES内部字段 + +### 8.2 租户隔离 + +所有API请求必须提供`tenant_id`: +- 请求头:`X-Tenant-ID: 1` +- 或查询参数:`?tenant_id=1` + +搜索时自动添加`tenant_id`过滤,确保数据隔离。 + +### 8.3 数据接口约定 + +**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 + +#### 8.3.1 数据流模式 + +系统采用统一的数据流模式,确保数据在各层之间的一致性: + +**数据流转路径**: +``` +API Request (JSON) + ↓ +Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) + ↓ +Searcher(透传) + ↓ +ES Query Builder → model_dump() 转换为字典 + ↓ +ES Query (字典) + ↓ +Elasticsearch +``` + +#### 8.3.2 Facets 配置数据流 + +**输入格式**:`List[Union[str, FacetConfig]]` + +- **简单模式**:字符串列表(字段名),使用默认配置 + ```json + ["category.keyword", "vendor.keyword"] + ``` + +- **高级模式**:FacetConfig 对象列表,支持自定义配置 + ```json + [ + { + "field": "category.keyword", + "size": 15, + "type": "terms" + }, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100} + ] + } + ] + ``` + +**数据流**: +1. API 层:接收 `List[Union[str, FacetConfig]]` +2. Searcher 层:透传,不做转换 +3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 +4. 输出:转换为 ES 聚合查询 + +#### 8.3.3 Range Filters 数据流 + +**输入格式**:`Dict[str, RangeFilter]` + +**RangeFilter 模型**: +```python +class RangeFilter(BaseModel): + gte: Optional[Union[float, str]] # 大于等于 + gt: Optional[Union[float, str]] # 大于 + lte: Optional[Union[float, str]] # 小于等于 + lt: Optional[Union[float, str]] # 小于 +``` + +**示例**: +```json +{ + "price": {"gte": 50, "lte": 200}, + "created_at": {"gte": "2023-01-01T00:00:00Z"} +} +``` + +**数据流**: +1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 +2. Searcher 层:透传 `Dict[str, RangeFilter]` +3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 +4. 输出:ES range 查询(支持数值和日期) + +**特性**: +- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) +- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) +- 统一约定:所有范围过滤都使用 RangeFilter 模型 + +#### 8.3.4 响应 Facets 数据流 + +**输出格式**:`List[FacetResult]` + +**FacetResult 模型**: +```python +class FacetResult(BaseModel): + field: str # 字段名 + label: str # 显示标签 + type: Literal["terms", "range"] # 分面类型 + values: List[FacetValue] # 分面值列表 + total_count: Optional[int] # 总文档数 +``` + +**数据流**: +1. ES Response:返回聚合结果(字典格式) +2. Searcher 层:构建 `List[FacetResult]` 对象 +3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) + +**优势**: +- 类型安全:使用 Pydantic 模型确保数据结构一致性 +- 自动序列化:模型自动转换为 JSON,无需手动处理 +- 统一约定:所有响应都使用标准化的 Pydantic 模型 + +#### 8.3.5 统一约定的好处 + +1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 +2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 +3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) +4. **易于维护**:修改数据结构只需更新模型定义 +5. **数据验证**:自动验证输入数据,减少错误处理代码 + +**实现模块**: +- `api/models.py` - 所有 Pydantic 模型定义 +- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) +- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) + +## 9. 配置文件示例 + +**Base配置**(店匠通用):`config/schema/base/config.yaml` + +**其他客户配置**:`config/schema/tenant1/config.yaml` + +--- diff --git a/docs/设计文档.md b/docs/设计文档.md deleted file mode 100644 index a790b97..0000000 --- a/docs/设计文档.md +++ /dev/null @@ -1,716 +0,0 @@ -# 搜索引擎通用化开发进度 - -## 项目概述 - -对后端搜索技术 做通用化。 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 - - -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 - ---- - -## 1. 原始数据层的约定 - -### 1.1 店匠主表 - -所有租户共用以下主表: -- `shoplazza_product_sku` - SKU级别商品数据 -- `shoplazza_product_spu` - SPU级别商品数据 - -### 1.2 索引结构(SPU维度) - -**统一索引架构**: -- 所有客户共享同一个Elasticsearch索引:`search_products` -- 索引粒度:SPU级别(每个文档代表一个SPU) -- 数据隔离:通过`tenant_id`字段实现租户隔离 -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组(SKU变体) - -**索引文档结构**: -```json -{ - "tenant_id": "1", - "spu_id": "123", - "title": "蓝牙耳机", - "skus": [ - { - "sku_id": "456", - "title": "黑色", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "min_price": 199.99, - "max_price": 299.99 -} -``` - -### 1.3 配置化方案 - -**配置分离原则**: -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 -- **数据导入流程**:写死的脚本,不依赖配置 - -统一通过配置文件定义: -1. ES 字段定义(字段类型、分析器、boost等) -2. ES mapping 结构生成 -3. 查询域配置(indexes) -4. 排序和打分配置(function_score) - -**注意**:配置中**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` / `extension_table` - 数据表配置 -- `source_table` / `source_column` - 字段数据源映射 - ---- - -## 2. 配置系统实现 - -### 2.1 应用结构配置(字段定义) - -**配置文件位置**:`config/schema/{tenant_id}_config.yaml` - -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 - -**实现情况**: - -#### 字段类型支持 -- **TEXT**:文本字段,支持多语言分析器 -- **KEYWORD**:关键词字段,用于精确匹配和聚合 -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) -- **INT/LONG**:整数类型 -- **FLOAT/DOUBLE**:浮点数类型 -- **DATE**:日期类型 -- **BOOLEAN**:布尔类型 - -#### 分析器支持 -- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj) -- **english**:英文分析器 -- **russian**:俄文分析器 -- **arabic**:阿拉伯文分析器 -- **spanish**:西班牙文分析器 -- **japanese**:日文分析器 -- **standard**:标准分析器 -- **keyword**:关键词分析器 - -#### 字段配置示例(Base配置) - -```yaml -fields: - # 租户隔离字段(必需) - - name: "tenant_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 商品标识字段 - - name: "spu_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 文本搜索字段 - - name: "title" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 3.0 - index: true - store: true - - - name: "seo_keywords" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 2.0 - index: true - store: true - - # 嵌套skus字段 - - name: "skus" - type: "JSON" - nested: true - nested_properties: - sku_id: - type: "keyword" - price: - type: "float" - sku: - type: "keyword" -``` - -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 - -**实现模块**: -- `config/config_loader.py` - 配置加载器 -- `config/field_types.py` - 字段类型定义 -- `indexer/mapping_generator.py` - ES mapping 生成器 -- `indexer/data_transformer.py` - 数据转换器 - -### 2.2 索引结构配置(查询域配置) - -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 - -**实现情况**: - -#### 域(Domain)配置 -每个域定义了: -- 域名称(如 `default`, `title`, `category`, `brand`) -- 域标签(中文描述) -- 搜索字段列表 -- 默认分析器 -- 权重(boost) -- **多语言字段映射**(`language_field_mapping`) - -#### 多语言字段映射 - -支持将不同语言的查询路由到对应的字段: - -```yaml -indexes: - - name: "default" - label: "默认索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - - "categoryName" - - "brandName" - analyzer: "chinese_ecommerce" - boost: 1.0 - language_field_mapping: - zh: - - "name" - - "categoryName" - - "brandName" - en: - - "enSpuName" - ru: - - "ruSkuName" - - - name: "title" - label: "标题索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - analyzer: "chinese_ecommerce" - boost: 2.0 - language_field_mapping: - zh: - - "name" - en: - - "enSpuName" - ru: - - "ruSkuName" -``` - -**工作原理**: -1. 检测查询语言(中文、英文、俄文等) -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 -4. 组合多个语言查询的结果,提高召回率 - -**实现模块**: -- `search/multilang_query_builder.py` - 多语言查询构建器 -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) - ---- - -## 3. 数据导入流程 - -### 3.1 数据源 - -**店匠标准表**(Base配置使用): -- `shoplazza_product_spu` - SPU级别商品数据 -- `shoplazza_product_sku` - SKU级别商品数据 - -**其他客户表**(tenant1等): -- 使用各自的数据源表和扩展表 - -### 3.2 数据导入方式 - -**Pipeline层决定数据源**: -- 数据导入流程是写死的脚本,不依赖配置 -- 配置只关注ES搜索相关的内容 -- 数据源映射逻辑写死在转换器代码中 - -#### Base配置数据导入(店匠通用) - -**脚本**:`scripts/ingest_shoplazza.py` - -**数据流程**: -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 -2. **数据转换**(`indexer/spu_transformer.py`): - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 - - 将SKU数据聚合为嵌套的`skus`数组 - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) - - 字段映射(写死在代码中,不依赖配置) - - 注入`tenant_id`字段 -3. **索引创建**: - - 根据配置生成ES mapping - - 创建或更新`search_products`索引 -4. **批量入库**: - - 批量写入ES(默认每批500条) - - 错误处理和重试机制 - -**命令行工具**: -```bash -python scripts/ingest_shoplazza.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --tenant-id "1" \ - --config base \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -#### 其他客户数据导入 - -- 使用各自的数据转换器(如`indexer/data_transformer.py`) -- 数据源映射逻辑写死在各自的转换器中 -- 共享`search_products`索引,通过`tenant_id`隔离 - -**实现模块**: -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) -- `indexer/bulk_indexer.py` - 批量索引器 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 - ---- - -## 4. QueryParser 实现 - - -### 4.1 查询改写(Query Rewriting) - -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 -**实现情况**: - -#### 配置方式 -在 `query_config.rewrite_dictionary` 中配置查询改写规则: - -```yaml -query_config: - enable_query_rewrite: true - rewrite_dictionary: - "芭比": "brand:芭比 OR name:芭比娃娃" - "玩具": "category:玩具" - "消防": "category:消防 OR name:消防" -``` - -#### 功能特性 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value -- **部分匹配**:查询包含词典 key 时,替换该部分 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) - -#### 实现模块 -- `query/query_rewriter.py` - 查询改写器 -- `query/query_parser.py` - 查询解析器(集成改写功能) - -### 4.2 翻译(Translation) - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - supported_languages: - - "zh" - - "en" - - "ru" - default_language: "zh" - enable_translation: true - translation_service: "deepl" - translation_api_key: null # 通过环境变量设置 -``` - -#### 功能特性 -1. **语言检测**:自动检测查询语言 -2. **智能翻译**: - - 如果查询是中文,翻译为英文、俄文 - - 如果查询是英文,翻译为中文、俄文 - - 如果查询是其他语言,翻译为所有支持的语言 -3. **域感知翻译**: - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 - - 避免不必要的翻译,提高效率 -4. **翻译缓存**:缓存翻译结果,避免重复调用 API - -#### 工作流程 -``` -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建 -``` - -#### 实现模块 -- `query/language_detector.py` - 语言检测器 -- `query/translator.py` - 翻译器(DeepL API) -- `query/query_parser.py` - 查询解析器(集成翻译功能) - -### 4.3 文本向量化(Text Embedding) - -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - enable_text_embedding: true -``` - -#### 功能特性 -1. **条件生成**: - - 仅当 `enable_text_embedding=true` 时生成向量 - - 仅对 `default` 域查询生成向量 -2. **向量模型**:BGE-M3 模型(1024维向量) -3. **用途**:用于语义搜索(KNN 检索) - -#### 实现模块 -- `embeddings/bge_encoder.py` - BGE 文本编码器 -- `query/query_parser.py` - 查询解析器(集成向量生成) - ---- - -## 5. Searcher 实现 - -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 - -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 - -### 5.1 布尔表达式解析 - -**实现情况**: - -#### 支持的运算符 -- **AND**:所有项必须匹配 -- **OR**:任意项匹配 -- **RANK**:排序增强(类似 OR 但影响排序) -- **ANDNOT**:排除(第一项匹配,第二项不匹配) -- **()**:括号分组 - -#### 优先级(从高到低) -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -#### 示例 -``` -laptop AND (gaming OR professional) ANDNOT cheap -``` - -#### 实现模块 -- `search/boolean_parser.py` - 布尔表达式解析器 -- `search/searcher.py` - 搜索器(集成布尔解析) - -### 5.2 多语言搜索 - -**实现情况**: - -#### 工作原理 -1. **查询解析**: - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) - - 检测查询语言 - - 生成翻译 -2. **多语言查询构建**: - - 如果域有 `language_field_mapping`: - - 使用检测到的语言查询对应字段(boost * 1.5) - - 使用翻译后的查询搜索其他语言字段(boost * 1.0) - - 如果域没有 `language_field_mapping`: - - 使用所有字段进行搜索 -3. **查询组合**: - - 多个语言查询组合为 `should` 子句 - - 提高召回率 - -#### 示例 -``` -查询: "芭比娃娃" -域: default -检测语言: zh - -生成的查询: -- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5) -- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0) -- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0) -``` - -#### 实现模块 -- `search/multilang_query_builder.py` - 多语言查询构建器 -- `search/searcher.py` - 搜索器(使用多语言构建器) - -### 5.3 相关性计算(Ranking) - -**实现情况**: - -#### 当前实现 -**公式**:`bm25() + 0.2 * text_embedding_relevance()` - -- **bm25()**:BM25 文本相关性得分 - - 包括多语言打分 - - 内部通过配置翻译为多种语言 - - 分别到对应的字段搜索 - - 中文字段使用中文分词器,英文字段使用英文分词器 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) - - 权重:0.2 - -#### 配置方式 -```yaml -ranking: - expression: "bm25() + 0.2*text_embedding_relevance()" - description: "BM25 text relevance combined with semantic embedding similarity" -``` - -#### 扩展性 -- 支持表达式配置(未来可扩展) -- 支持自定义函数(如 `timeliness()`, `field_value()`) - -#### 实现模块 -- `search/ranking_engine.py` - 排序引擎 -- `search/searcher.py` - 搜索器(集成排序功能) - ---- - -## 6. 已完成功能总结 - -### 6.1 配置系统 -- ✅ 字段定义配置(类型、分析器、来源表/列) -- ✅ 索引域配置(多域查询、多语言映射) -- ✅ 查询配置(改写词典、翻译配置) -- ✅ 排序配置(表达式配置) -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) - -### 6.2 数据索引 -- ✅ 数据转换(字段映射、类型转换) -- ✅ 向量生成(文本向量、图片向量) -- ✅ 向量缓存(避免重复计算) -- ✅ 批量索引(错误处理、重试机制) -- ✅ ES mapping 自动生成 - -### 6.3 查询处理 -- ✅ 查询改写(词典配置) -- ✅ 语言检测 -- ✅ 多语言翻译(DeepL API) -- ✅ 文本向量化(BGE-M3) -- ✅ 域提取(支持 `domain:query` 语法) - -### 6.4 搜索功能 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) -- ✅ 多语言查询构建(语言路由、字段映射) -- ✅ 语义搜索(KNN 检索) -- ✅ 相关性排序(BM25 + 向量相似度) -- ✅ 结果聚合(Faceted Search) - -### 6.5 API 服务 -- ✅ RESTful API(FastAPI) -- ✅ 搜索接口(文本搜索、图片搜索) -- ✅ 文档查询接口 -- ✅ 前端界面(HTML + JavaScript) -- ✅ 租户隔离(tenant_id过滤) - -### 6.6 Base配置(店匠通用) -- ✅ SPU级别索引结构 -- ✅ 嵌套skus字段 -- ✅ 统一索引(search_products) -- ✅ 租户隔离(tenant_id) -- ✅ 配置简化(移除MySQL相关配置) - ---- - -## 7. 技术栈 - -- **后端**:Python 3.6+ -- **搜索引擎**:Elasticsearch -- **数据库**:MySQL(Shoplazza) -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) -- **翻译服务**:DeepL API -- **API 框架**:FastAPI -- **前端**:HTML + JavaScript - ---- - -## 8. API响应格式 - -### 8.1 外部友好格式 - -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: - -**响应结构**: -```json -{ - "results": [ - { - "spu_id": "123", - "title": "蓝牙耳机", - "skus": [ - { - "sku_id": "456", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "facets": [...], - "suggestions": [], - "related_searches": [] -} -``` - -**主要变化**: -- 结构化结果(`SpuResult`和`SkuResult`) -- 嵌套skus数组 -- 无ES内部字段 - -### 8.2 租户隔离 - -所有API请求必须提供`tenant_id`: -- 请求头:`X-Tenant-ID: 1` -- 或查询参数:`?tenant_id=1` - -搜索时自动添加`tenant_id`过滤,确保数据隔离。 - -### 8.3 数据接口约定 - -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 - -#### 8.3.1 数据流模式 - -系统采用统一的数据流模式,确保数据在各层之间的一致性: - -**数据流转路径**: -``` -API Request (JSON) - ↓ -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) - ↓ -Searcher(透传) - ↓ -ES Query Builder → model_dump() 转换为字典 - ↓ -ES Query (字典) - ↓ -Elasticsearch -``` - -#### 8.3.2 Facets 配置数据流 - -**输入格式**:`List[Union[str, FacetConfig]]` - -- **简单模式**:字符串列表(字段名),使用默认配置 - ```json - ["categoryName_keyword", "brandName_keyword"] - ``` - -- **高级模式**:FacetConfig 对象列表,支持自定义配置 - ```json - [ - { - "field": "categoryName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - } - ] - ``` - -**数据流**: -1. API 层:接收 `List[Union[str, FacetConfig]]` -2. Searcher 层:透传,不做转换 -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 -4. 输出:转换为 ES 聚合查询 - -#### 8.3.3 Range Filters 数据流 - -**输入格式**:`Dict[str, RangeFilter]` - -**RangeFilter 模型**: -```python -class RangeFilter(BaseModel): - gte: Optional[Union[float, str]] # 大于等于 - gt: Optional[Union[float, str]] # 大于 - lte: Optional[Union[float, str]] # 小于等于 - lt: Optional[Union[float, str]] # 小于 -``` - -**示例**: -```json -{ - "price": {"gte": 50, "lte": 200}, - "created_at": {"gte": "2023-01-01T00:00:00Z"} -} -``` - -**数据流**: -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 -2. Searcher 层:透传 `Dict[str, RangeFilter]` -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 -4. 输出:ES range 查询(支持数值和日期) - -**特性**: -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) -- 统一约定:所有范围过滤都使用 RangeFilter 模型 - -#### 8.3.4 响应 Facets 数据流 - -**输出格式**:`List[FacetResult]` - -**FacetResult 模型**: -```python -class FacetResult(BaseModel): - field: str # 字段名 - label: str # 显示标签 - type: Literal["terms", "range"] # 分面类型 - values: List[FacetValue] # 分面值列表 - total_count: Optional[int] # 总文档数 -``` - -**数据流**: -1. ES Response:返回聚合结果(字典格式) -2. Searcher 层:构建 `List[FacetResult]` 对象 -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) - -**优势**: -- 类型安全:使用 Pydantic 模型确保数据结构一致性 -- 自动序列化:模型自动转换为 JSON,无需手动处理 -- 统一约定:所有响应都使用标准化的 Pydantic 模型 - -#### 8.3.5 统一约定的好处 - -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) -4. **易于维护**:修改数据结构只需更新模型定义 -5. **数据验证**:自动验证输入数据,减少错误处理代码 - -**实现模块**: -- `api/models.py` - 所有 Pydantic 模型定义 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) - -## 9. 配置文件示例 - -**Base配置**(店匠通用):`config/schema/base/config.yaml` - -**其他客户配置**:`config/schema/tenant1/config.yaml` - ---- diff --git a/search/searcher.py b/search/searcher.py index 76375b9..dd79fb5 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -135,6 +135,7 @@ class Searcher: sort_order: Optional[str] = "desc", debug: bool = False, language: str = "zh", + sku_filter_dimension: Optional[str] = None, ) -> SearchResult: """ Execute search query (外部友好格式). @@ -376,7 +377,8 @@ class Searcher: formatted_results = ResultFormatter.format_search_results( es_hits, max_score, - language=language + language=language, + sku_filter_dimension=sku_filter_dimension ) # Format facets @@ -542,7 +544,12 @@ class Searcher: max_score = es_response.get('hits', {}).get('max_score') or 0.0 # Format results using ResultFormatter - formatted_results = ResultFormatter.format_search_results(es_hits, max_score) + formatted_results = ResultFormatter.format_search_results( + es_hits, + max_score, + language="zh", # Default language for image search + sku_filter_dimension=None # Image search doesn't support SKU filtering + ) return SearchResult( results=formatted_results, -- libgit2 0.21.2