diff --git a/README.md b/README.md index 1aa5d31..1ba08b7 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ curl -X POST http://localhost:6002/search/ \ |------|----------|----------| | `环境配置说明.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 | | `Usage-Guide.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 | -| `基础配置指南.md` | 租户字段、索引域、排序表达式配置流程 | 新租户开通、配置变更 | +| `基础配置指南.md` | 统一硬编码配置说明、索引结构、查询配置 | 了解系统配置、修改配置 | | `测试数据指南.md` | 两个租户的模拟/CSV 数据构造 & MySQL→ES 流程 | 数据准备、联调 | | `测试Pipeline说明.md` | 测试流水线、CI 脚本、上下文说明 | 自动化测试、追踪流水线 | | `系统设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 | -| `索引字段说明.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | +| `索引字段说明v2.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | | `搜索API对接指南.md` | REST API(文本/图片/管理)详解、示例、响应格式 | API 使用、测试 | | `搜索API速查表.md` | 常用请求体、过滤器、分面速查表 | 支持团队快速查阅 | | `Search-API-Examples.md` | Python/JS/cURL 端到端示例 | 客户工程、SDK 参考 | @@ -82,9 +82,11 @@ curl -X POST http://localhost:6002/search/ \ - API、分页、过滤、Facet、KNN 等:`搜索API对接指南.md` - 对接案例、示例与错误码:`搜索API对接指南.md`、`Search-API-Examples.md` -- **配置驱动能力** - - `config/schema/{tenant_id}/config.yaml`:字段定义、索引域、排序表达式、SPU 聚合 - - 详解与设计理念:`设计文档.md`、`INDEX_FIELDS_DOCUMENTATION.md` +- **统一配置** + - 所有租户共享统一的索引结构和查询配置(硬编码) + - 索引 mapping: `mappings/search_products.json` + - 查询配置: `search/query_config.py` + - 详解:`基础配置指南.md`、`索引字段说明v2.md` ## 仓库结构(概览) diff --git a/api/models.py b/api/models.py index 7ce52bc..aab3071 100644 --- a/api/models.py +++ b/api/models.py @@ -75,15 +75,21 @@ class SearchRequest(BaseModel): ) # 过滤器 - 精确匹配和多值匹配 - filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = Field( + filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]], Dict[str, Any], List[Dict[str, Any]]]]] = Field( None, - description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)", + description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)。支持 specifications 嵌套过滤:{\"specifications\": {\"name\": \"color\", \"value\": \"green\"}} 或 [{\"name\": \"color\", \"value\": \"green\"}, ...]", json_schema_extra={ "examples": [ { - "category.keyword": ["玩具", "益智玩具"], - "vendor.keyword": "乐高", - "in_stock": True + "category_name": ["手机", "电子产品"], + "vendor_zh.keyword": "奇乐", + "specifications": {"name": "颜色", "value": "白色"} + }, + { + "specifications": [ + {"name": "颜色", "value": "白色"}, + {"name": "尺寸", "value": "256GB"} + ] } ] } @@ -110,22 +116,25 @@ class SearchRequest(BaseModel): # 分面搜索 - 简化接口 facets: Optional[List[Union[str, FacetConfig]]] = Field( None, - description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象", + description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象。支持 specifications 分面:\"specifications\"(所有name)或 \"specifications.color\"(指定name)", json_schema_extra={ "examples": [ # 简单模式:只指定字段名,使用默认配置 - ["category.keyword", "vendor.keyword"], + ["category1_name", "category2_name", "specifications"], + # 指定specifications的某个name + ["specifications.颜色", "specifications.尺寸"], # 高级模式:详细配置 [ - {"field": "category.keyword", "size": 15}, + {"field": "category1_name", "size": 15}, { - "field": "price", + "field": "min_price", "type": "range", "ranges": [ {"key": "0-50", "to": 50}, {"key": "50-100", "from": 50, "to": 100} ] - } + }, + "specifications" # 所有specifications name的分面 ] ] } diff --git a/api/result_formatter.py b/api/result_formatter.py index a2a0146..39af909 100644 --- a/api/result_formatter.py +++ b/api/result_formatter.py @@ -143,7 +143,7 @@ class ResultFormatter: for field_name, agg_data in es_aggregations.items(): display_field = field_name[:-6] if field_name.endswith("_facet") else field_name - # 处理specifications嵌套分面 + # 处理specifications嵌套分面(所有name) if field_name == "specifications_facet" and 'by_name' in agg_data: # specifications嵌套聚合:按name分组,每个name下有value_counts by_name_agg = agg_data['by_name'] @@ -174,6 +174,35 @@ class ResultFormatter: facets.append(facet) continue + # 处理specifications嵌套分面(指定name) + if field_name.startswith("specifications_") and field_name.endswith("_facet") and 'filter_by_name' in agg_data: + # 提取name(从 "specifications_颜色_facet" 提取 "颜色") + name = field_name[len("specifications_"):-len("_facet")] + filter_by_name_agg = agg_data.get('filter_by_name', {}) + value_counts = filter_by_name_agg.get('value_counts', {}) + + values = [] + if 'buckets' in value_counts: + for value_bucket in value_counts['buckets']: + value = FacetValue( + value=value_bucket['key'], + label=str(value_bucket['key']), + count=value_bucket['doc_count'], + selected=False + ) + values.append(value) + + # 创建分面结果 + facet = FacetResult( + field=f"specifications.{name}", + label=str(name), + type="terms", + values=values, + total_count=filter_by_name_agg.get('doc_count', 0) + ) + facets.append(facet) + continue + # Handle terms aggregation if 'buckets' in agg_data: values = [] diff --git a/docs/Search-API-Examples.md b/docs/Search-API-Examples.md index 35f7e7f..3f0f7d0 100644 --- a/docs/Search-API-Examples.md +++ b/docs/Search-API-Examples.md @@ -23,6 +23,7 @@ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "芭比娃娃" }' @@ -48,8 +49,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 50 }' ``` @@ -60,8 +63,10 @@ curl -X POST "http://localhost:6002/search/" \ # 第1页(0-19) curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 20, "from": 0 }' @@ -69,8 +74,10 @@ curl -X POST "http://localhost:6002/search/" \ # 第2页(20-39) curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 20, "from": 20 }' @@ -87,10 +94,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "filters": { - "category.keyword": "玩具" + "category_name": "手机" } }' ``` @@ -100,10 +109,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "娃娃", + "query": "手机", + "language": "zh", "filters": { - "category.keyword": ["玩具", "益智玩具", "儿童玩具"] + "category_name": ["手机", "电子产品"] } }' ``` @@ -115,16 +126,79 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "娃娃", + "query": "手机", + "language": "zh", "filters": { - "category.keyword": "玩具", - "vendor.keyword": "美泰" + "category_name": "手机", + "vendor_zh.keyword": "奇乐" } }' ``` -说明:必须同时满足"类目=玩具" **并且** "品牌=美泰"。 +说明:必须同时满足"类目=手机" **并且** "品牌=奇乐"。 + +#### 示例 4:Specifications 嵌套过滤(单个规格) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } + }' +``` + +说明:查询规格名称为"color"且值为"white"的商品。 + +#### 示例 5:Specifications 嵌套过滤(多个规格,OR逻辑) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } + }' +``` + +说明:查询满足任意一个规格的商品(color=white **或** size=256GB)。 + +#### 示例 6:组合过滤(包含 specifications) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "filters": { + "category_name": "手机", + "specifications": { + "name": "color", + "value": "white" + } + } + }' +``` + +说明:同时满足类目=手机 **并且** color=white。 ### 范围过滤器 @@ -133,10 +207,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "range_filters": { - "price": { + "min_price": { "gte": 50, "lte": 200 } @@ -151,10 +227,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "range_filters": { - "price": { + "min_price": { "gte": 100 } } @@ -168,10 +246,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "range_filters": { - "price": { + "min_price": { "lt": 50 } } @@ -185,10 +265,12 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "range_filters": { - "price": { + "min_price": { "gte": 50, "lte": 200 }, @@ -206,14 +288,16 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "filters": { - "category.keyword": ["玩具", "益智玩具"], - "vendor.keyword": "乐高" + "category_name": ["手机", "电子产品"], + "vendor_zh.keyword": "品牌A" }, "range_filters": { - "price": { + "min_price": { "gte": 50, "lte": 500 } @@ -234,41 +318,82 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 20, - "facets": ["category.keyword", "vendor.keyword"] + "facets": ["category1_name", "category2_name", "specifications"] }' ``` **响应**: ```json { - "hits": [...], + "results": [...], "total": 118, "facets": [ { - "field": "category.keyword", - "label": "category.keyword", + "field": "category1_name", + "label": "category1_name", "type": "terms", "values": [ - {"value": "玩具", "count": 85, "selected": false}, - {"value": "益智玩具", "count": 33, "selected": false} + {"value": "手机", "count": 85, "selected": false}, + {"value": "电子产品", "count": 33, "selected": false} ] }, { - "field": "vendor.keyword", - "label": "vendor.keyword", + "field": "specifications.color", + "label": "color", "type": "terms", "values": [ - {"value": "乐高", "count": 42, "selected": false}, - {"value": "美泰", "count": 28, "selected": false} + {"value": "white", "count": 50, "selected": false}, + {"value": "black", "count": 30, "selected": false} + ] + }, + { + "field": "specifications.size", + "label": "size", + "type": "terms", + "values": [ + {"value": "256GB", "count": 40, "selected": false}, + {"value": "512GB", "count": 20, "selected": false} ] } ] } ``` +#### 示例 2:Specifications 分面(所有规格名称) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "facets": ["specifications"] + }' +``` + +说明:返回所有规格名称(name)及其对应的值(value)列表。 + +#### 示例 3:Specifications 分面(指定规格名称) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "facets": ["specifications.color", "specifications.size"] + }' +``` + +说明:只返回指定规格名称的值列表。 + ### 高级模式 #### 示例 1:自定义分面大小 @@ -276,16 +401,18 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "facets": [ { - "field": "category.keyword", + "field": "category1_name", "size": 20, "type": "terms" }, { - "field": "vendor.keyword", + "field": "category2_name", "size": 30, "type": "terms" } @@ -298,8 +425,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "facets": [ { "field": "price", @@ -339,8 +468,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "facets": [ {"field": "category.keyword", "size": 15}, {"field": "vendor.keyword", "size": 15}, @@ -366,8 +497,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 20, "sort_by": "min_price", "sort_order": "asc" @@ -379,8 +512,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 20, "sort_by": "create_time", "sort_order": "desc" @@ -392,8 +527,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "filters": { "category.keyword": "益智玩具" }, @@ -411,6 +548,7 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/image" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "image_url": "https://example.com/barbie.jpg", "size": 20 @@ -422,14 +560,15 @@ curl -X POST "http://localhost:6002/search/image" \ ```bash curl -X POST "http://localhost:6002/search/image" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "image_url": "https://example.com/barbie.jpg", "size": 20, "filters": { - "category.keyword": "玩具" + "category_name": "手机" }, "range_filters": { - "price": { + "min_price": { "lte": 100 } } @@ -445,6 +584,7 @@ curl -X POST "http://localhost:6002/search/image" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "玩具 AND 乐高" }' @@ -457,6 +597,7 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "芭比 OR 娃娃" }' @@ -469,6 +610,7 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "玩具 ANDNOT 电动" }' @@ -481,6 +623,7 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" }' @@ -493,6 +636,7 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "brand:乐高" }' @@ -557,19 +701,23 @@ for hit in result['hits'][:3]: # 示例 2:带过滤和分面的搜索 result = search_products( - query="玩具", + query="手机", size=20, + language="zh", filters={ - "category.keyword": ["玩具", "益智玩具"] + "category_name": "手机", + "specifications": {"name": "color", "value": "white"} }, range_filters={ - "price": {"gte": 50, "lte": 200} + "min_price": {"gte": 50, "lte": 200} }, facets=[ - {"field": "vendor.keyword", "size": 15}, - {"field": "category.keyword", "size": 15}, + {"field": "category1_name", "size": 15}, + {"field": "category2_name", "size": 15}, + "specifications.color", + "specifications.size", { - "field": "price", + "field": "min_price", "type": "range", "ranges": [ {"key": "0-50", "to": 50}, @@ -692,21 +840,24 @@ const result1 = await client.search({ }); console.log(`找到 ${result1.total} 个结果`); -// 带过滤和分面的搜索 +// 带过滤和分面的搜索(包含规格) const result2 = await client.search({ - query: "玩具", + query: "手机", + language: "zh", size: 20, filters: { - category.keyword: ["玩具", "益智玩具"] + category_name: "手机", + specifications: { name: "color", value: "white" } }, rangeFilters: { - price: { gte: 50, lte: 200 } + min_price: { gte: 50, lte: 200 } }, facets: [ - { field: "vendor.keyword", size: 15 }, - { field: "category.keyword", size: 15 } + "category1_name", + "specifications.color", + "specifications.size" ], - sortBy: "price", + sortBy: "min_price", sortOrder: "asc" }); @@ -810,8 +961,10 @@ const SearchComponent = { ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "debug": true }' ``` @@ -847,8 +1000,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "min_score": 5.0 }' ``` @@ -865,10 +1020,11 @@ curl -X POST "http://localhost:6002/search/" \ # 显示某个类目下的所有商品,按价格排序,提供品牌筛选 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "*", "filters": { - "category.keyword": "玩具" + "category_name": "手机" }, "facets": [ {"field": "vendor.keyword", "size": 20}, @@ -892,19 +1048,53 @@ curl -X POST "http://localhost:6002/search/" \ ### 场景 2:搜索结果页 ```bash -# 用户搜索关键词,提供筛选和排序 +# 用户搜索关键词,提供筛选和排序(包含规格分面) +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "手机", + "language": "zh", + "facets": [ + {"field": "category1_name", "size": 10}, + {"field": "category2_name", "size": 10}, + "specifications.color", + "specifications.size", + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100+", "from": 100} + ] + } + ], + "size": 20 + }' +``` + +### 场景 2.1:带规格过滤的搜索结果页 + +```bash +# 用户搜索并选择了规格筛选条件 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "芭比娃娃", + "query": "手机", + "language": "zh", + "filters": { + "category_name": "手机", + "specifications": { + "name": "color", + "value": "white" + } + }, "facets": [ - {"field": "category.keyword", "size": 10}, - {"field": "vendor.keyword", "size": 10}, - {"field": "price", "type": "range", "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100+", "from": 100} - ]} + "category1_name", + "specifications.color", + "specifications.size" ], "size": 20 }' @@ -916,15 +1106,16 @@ curl -X POST "http://localhost:6002/search/" \ # 显示特定价格区间的商品 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "*", "range_filters": { - "price": { + "min_price": { "gte": 50, "lte": 100 } }, - "facets": ["category.keyword", "vendor.keyword"], + "facets": ["category1_name", "category2_name", "specifications"], "sort_by": "min_price", "sort_order": "asc", "size": 50 @@ -937,6 +1128,7 @@ curl -X POST "http://localhost:6002/search/" \ # 最近更新的商品 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "*", "range_filters": { @@ -960,10 +1152,12 @@ curl -X POST "http://localhost:6002/search/" \ # 错误:range_filters 缺少操作符 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "range_filters": { - "price": {} + "min_price": {} } }' ``` @@ -983,6 +1177,7 @@ curl -X POST "http://localhost:6002/search/" \ # 错误:query 为空 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "" }' @@ -1060,6 +1255,7 @@ curl -X POST "http://localhost:6002/search/" \ # 使用通配符查询 + 分面 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "*", "size": 0, @@ -1074,8 +1270,10 @@ curl -X POST "http://localhost:6002/search/" \ ```bash curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ - "query": "玩具", + "query": "手机", + "language": "zh", "size": 0, "facets": [ { @@ -1099,13 +1297,14 @@ curl -X POST "http://localhost:6002/search/" \ # 布尔表达式 + 过滤器 + 分面 + 排序 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{ "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", "filters": { "category.keyword": ["玩具", "益智玩具"] }, "range_filters": { - "price": {"gte": 20, "lte": 100}, + "min_price": {"gte": 20, "lte": 100}, "days_since_last_update": {"lte": 30} }, "facets": [ @@ -1127,16 +1326,19 @@ curl -X POST "http://localhost:6002/search/" \ # 测试类目:玩具 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{"query": "玩具", "size": 5}' # 测试品牌:乐高 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{"query": "brand:乐高", "size": 5}' # 测试布尔表达式 curl -X POST "http://localhost:6002/search/" \ -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ -d '{"query": "玩具 AND 乐高", "size": 5}' ``` diff --git a/docs/基础配置指南.md b/docs/基础配置指南.md index 2feed7d..ca67edd 100644 --- a/docs/基础配置指南.md +++ b/docs/基础配置指南.md @@ -1,257 +1,166 @@ -# Base Configuration Guide - -店匠通用配置(Base Configuration)使用指南 +# 基础配置指南 ## 概述 -Base配置是店匠(Shoplazza)通用配置,适用于所有使用店匠标准表的客户。该配置采用SPU级别的索引结构,所有客户共享同一个Elasticsearch索引(`search_products`),通过`tenant_id`字段实现数据隔离。 +搜索引擎采用**统一硬编码配置**方案,所有租户共享相同的索引结构和查询配置,无需单独配置。 ## 核心特性 -- **SPU级别索引**:每个ES文档代表一个SPU,包含嵌套的skus数组 -- **统一索引**:所有客户共享`search_products`索引 -- **租户隔离**:通过`tenant_id`字段实现数据隔离 -- **配置简化**:配置只包含ES搜索相关配置,不包含MySQL数据源配置 -- **外部友好格式**:API返回格式不包含ES内部字段(`_id`, `_score`, `_source`) +- **统一索引结构**: 所有租户共享 `search_products` 索引 +- **硬编码配置**: 索引 mapping 和查询配置直接硬编码在代码中,无需配置文件 +- **SPU级别索引**: 每个ES文档代表一个SPU,包含嵌套的 `skus` 和 `specifications` 数组 +- **租户隔离**: 通过 `tenant_id` 字段实现数据隔离 +- **多语言支持**: 文本字段支持中英文双语,后端根据 `language` 参数自动选择 -## 配置说明 +## 索引结构 -### 配置文件位置 +### Mapping 文件位置 -`config/schema/base/config.yaml` +`mappings/search_products.json` -### 配置内容 +### 主要字段 -Base配置**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` - 主表配置 -- `extension_table` - 扩展表配置 -- `source_table` / `source_column` - 字段数据源映射 +#### 基础标识 +- `tenant_id` (keyword) - 租户ID(必需,用于隔离) +- `spu_id` (keyword) - SPU ID +- `create_time`, `update_time` (date) - 时间字段 -Base配置**只包含**: -- ES字段定义(字段类型、分析器、boost等) -- 查询域(indexes)配置 -- 查询处理配置(query_config) -- 排序和打分配置(function_score) -- SPU配置(spu_config) +#### 多语言文本字段 +- `title_zh`, `title_en` (text) - 标题(中英文) +- `brief_zh`, `brief_en` (text) - 短描述(中英文) +- `description_zh`, `description_en` (text) - 详细描述(中英文) +- `vendor_zh`, `vendor_en` (text) - 供应商/品牌(中英文,含keyword子字段) +- `category_path_zh`, `category_path_en` (text) - 类目路径(中英文) +- `category_name_zh`, `category_name_en` (text) - 类目名称(中英文) -### 必需字段 +#### 类目字段 +- `category_id` (keyword) - 类目ID +- `category_name` (keyword) - 类目名称 +- `category_level` (integer) - 类目层级 +- `category1_name`, `category2_name`, `category3_name` (keyword) - 多级类目 -- `tenant_id` (KEYWORD, required) - 租户隔离字段 +#### 规格和选项 +- `specifications` (nested) - 规格列表(name, value, sku_id) +- `option1_name`, `option2_name`, `option3_name` (keyword) - 选项名称 -### 主要字段 +#### 价格和库存 +- `min_price`, `max_price`, `compare_at_price` (float) - 价格字段 +- `sku_prices` (float) - SKU价格列表(数组) +- `sku_weights` (long) - SKU重量列表(数组) +- `sku_weight_units` (keyword) - SKU重量单位列表(数组) +- `total_inventory` (long) - 总库存 -- `spu_id` - SPU ID -- `title`, `brief`, `description` - 文本搜索字段 -- `seo_title`, `seo_description`, `seo_keywords` - SEO字段 -- `vendor`, `tags`, `category` - 分类和标签字段(HKText,支持 `.keyword` 精确匹配) -- `min_price`, `max_price`, `compare_at_price` - 价格字段 -- `skus` (nested) - 嵌套SKU数组 +#### 嵌套字段 +- `skus` (nested) - SKU详细信息数组 +- `image_embedding` (nested) - 图片向量(仅用于搜索) -## 数据导入流程 +#### 其他 +- `tags` (keyword) - 标签列表(数组) +- `image_url` (keyword, index: false) - 主图URL +- `title_embedding` (dense_vector) - 标题向量(仅用于搜索,不返回) -### 1. 生成测试数据 +## 查询配置 -```bash -python scripts/generate_test_data.py \ - --num-spus 100 \ - --tenant-id "1" \ - --start-spu-id 1 \ - --start-sku-id 1 \ - --output test_data.sql -``` +### 文本召回字段 -### 2. 导入测试数据到MySQL - -```bash -python scripts/import_test_data.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --sql-file test_data.sql \ - --tenant-id "1" -``` +默认同时搜索以下字段(中英文都包含): +- `title_zh^3.0`, `title_en^3.0` +- `brief_zh^1.5`, `brief_en^1.5` +- `description_zh^1.0`, `description_en^1.0` +- `vendor_zh^1.5`, `vendor_en^1.5` +- `category_path_zh^1.5`, `category_path_en^1.5` +- `category_name_zh^1.5`, `category_name_en^1.5` +- `tags^1.0` -### 3. 导入数据到Elasticsearch - -```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 -``` +### 查询架构 -## API使用 +**结构**: `filters AND (text_recall OR embedding_recall)` -### 搜索接口 +- **filters**: 前端传递的过滤条件(永远起作用) +- **text_recall**: 文本相关性召回(同时搜索中英文字段) +- **embedding_recall**: 向量召回(KNN,使用 `title_embedding`) +- **function_score**: 包装召回部分,支持提权字段 -**端点**: `POST /search/` +### Function Score 配置 -**请求头**: -``` -X-Tenant-ID: 1 -Content-Type: application/json -``` +位置: `search/query_config.py` 中的 `FUNCTION_SCORE_CONFIG` -**请求体**: -```json -{ - "query": "耳机", - "size": 10, - "from": 0, - "filters": { - "category.keyword": "电子产品" - }, - "facets": ["category.keyword", "vendor.keyword"] -} -``` +支持的类型: +- `filter_weight`: 条件权重(如新品提权) +- `field_value_factor`: 字段值因子(如销量因子) +- `decay`: 衰减函数(如时间衰减) + +## 分面配置 + +### 默认分面字段 + +- `category1_name` - 一级类目 +- `category2_name` - 二级类目 +- `category3_name` - 三级类目 +- `specifications` - 规格分面(嵌套聚合,按name分组,然后按value聚合) + +### 规格分面说明 + +`specifications` 使用特殊的嵌套聚合: +- 按 `specifications.name` 分组(如"color"、"size") +- 每个 `name` 下按 `specifications.value` 聚合(如"white"、"black") -**响应格式**: +返回格式: ```json { - "results": [ - { - "spu_id": "1", - "title": "蓝牙耳机 Sony", - "handle": "product-1", - "description": "高品质无线蓝牙耳机", - "vendor": "Sony", - "category": "电子产品", - "price": 199.99, - "compare_at_price": 299.99, - "currency": "USD", - "image_url": "//cdn.example.com/products/1.jpg", - "in_stock": true, - "skus": [ - { - "sku_id": "1", - "title": "黑色", - "price": 199.99, - "compare_at_price": 299.99, - "sku": "SKU-1-1", - "stock": 50, - "options": { - "option1": "黑色" - } - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "max_score": 1.0, - "facets": [ - { - "field": "category.keyword", - "label": "category.keyword", - "type": "terms", - "values": [ - { - "value": "电子产品", - "label": "电子产品", - "count": 5, - "selected": false - } - ] - } - ], - "suggestions": [], - "related_searches": [], - "took_ms": 15, - "query_info": {} + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + {"value": "white", "count": 50}, + {"value": "black", "count": 30} + ] } ``` -### 响应格式说明 - -#### 主要变化 - -1. **`results`替代`hits`**:返回字段从`hits`改为`results` -2. **结构化结果**:每个结果包含`spu_id`, `title`, `skus`, `relevance_score`等字段 -3. **无ES内部字段**:不包含`_id`, `_score`, `_source`等ES内部字段 -4. **嵌套skus**:每个商品包含skus数组,每个sku包含完整的变体信息 -5. **相关性分数**:`relevance_score`是ES原始分数(不进行归一化) - -#### SpuResult字段 - -- `spu_id` - SPU ID -- `title` - 商品标题 -- `handle` - 商品handle -- `description` - 商品描述 -- `vendor` - 供应商/品牌 -- `category` - 类目 -- `tags` - 标签 -- `price` - 最低价格(min_price) -- `compare_at_price` - 原价 -- `currency` - 货币单位(默认USD) -- `image_url` - 主图URL -- `in_stock` - 是否有库存 -- `skus` - SKU列表 -- `relevance_score` - 相关性分数(ES原始分数) - -#### SkuResult字段 - -- `sku_id` - SKU ID -- `title` - 变体标题 -- `price` - 价格 -- `compare_at_price` - 原价 -- `sku` - SKU编码 -- `stock` - 库存数量 -- `options` - 选项(颜色、尺寸等) - -## 测试 - -### 运行测试脚本 - -```bash -python scripts/test_base.py \ - --api-url http://localhost:8000 \ - --tenant-id "1" \ - --test-tenant-2 "2" -``` - -### 测试内容 - -1. **基本搜索**:测试搜索API基本功能 -2. **响应格式验证**:验证返回格式是否符合要求 -3. **Facets聚合**:测试分面搜索功能 -4. **租户隔离**:验证不同租户的数据隔离 +## 返回字段映射 -## 常见问题 +后端根据请求的 `language` 参数(`zh` 或 `en`)自动选择对应的中英文字段: -### Q: 为什么配置中没有MySQL相关配置? +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 -A: 数据源配置和数据导入流程是写死的脚本,不在搜索配置中。搜索配置只关注ES搜索相关的内容。 +映射规则: +- `title_zh/en` → `title` +- `brief_zh/en` → `brief` +- `description_zh/en` → `description` +- `vendor_zh/en` → `vendor` +- `category_path_zh/en` → `category_path` +- `category_name_zh/en` → `category_name` -### Q: 如何为新的租户导入数据? +## 配置修改 -A: 使用`ingest_shoplazza.py`脚本,指定不同的`--tenant-id`参数即可。 +### 修改索引结构 -### Q: 如何验证租户隔离是否生效? +编辑 `mappings/search_products.json`,然后: +1. 删除旧索引: `scripts/recreate_and_import.py --recreate` +2. 重新导入数据: `scripts/ingest.sh true` -A: 使用`test_base.py`脚本,指定两个不同的`--tenant-id`,检查搜索结果是否隔离。 +### 修改查询配置 -### Q: API返回格式中为什么没有`_id`和`_score`? +编辑 `search/query_config.py`: +- `DEFAULT_MATCH_FIELDS`: 文本召回字段列表 +- `FUNCTION_SCORE_CONFIG`: Function score 配置 +- `DEFAULT_FACETS`: 默认分面字段 -A: 为了提供外部友好的API格式,我们移除了ES内部字段,使用`spu_id`和`relevance_score`替代。 +### 修改返回字段 -### Q: 如何添加新的搜索字段? - -A: 在`config/schema/base/config.yaml`中添加字段定义,然后重新生成索引映射并重新导入数据。 +编辑 `search/query_config.py` 中的 `SOURCE_FIELDS` 列表。 ## 注意事项 -1. **tenant_id必需**:所有API请求必须提供`tenant_id`(通过请求头`X-Tenant-ID`或查询参数`tenant_id`) -2. **索引共享**:所有客户共享`search_products`索引,确保`tenant_id`字段正确设置 -3. **数据导入**:数据导入脚本是写死的,不依赖配置中的MySQL设置 -4. **配置分离**:搜索配置和数据源配置完全分离,提高可维护性 +1. **无需配置文件**: 所有配置都是硬编码的,不需要为每个租户创建配置文件 +2. **统一结构**: 所有租户共享相同的索引结构和查询逻辑 +3. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 +4. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不会返回给前端 + +## 相关文档 +- `索引字段说明v2.md` - 详细的字段说明 +- `搜索API对接指南.md` - API使用说明 +- `mappings/search_products.json` - 索引 mapping 定义 diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md index 1d21f08..7929749 100644 --- a/docs/搜索API对接指南.md +++ b/docs/搜索API对接指南.md @@ -64,7 +64,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ -d '{ "tenant_id": "demo-tenant", "query": "芭比娃娃", - "facets": ["category.keyword", "vendor.keyword"], + "facets": ["category.keyword", "specifications.color", "specifications.size"], "min_score": 0.2 }' ``` @@ -95,10 +95,10 @@ curl -X POST "http://120.76.41.98:6002/search/" \ ```json { - "tenant_id": "string (required)", "query": "string (required)", "size": 10, "from": 0, + "language": "zh", "filters": {}, "range_filters": {}, "facets": [], @@ -111,18 +111,20 @@ curl -X POST "http://120.76.41.98:6002/search/" \ } ``` +**注意**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。 + #### 参数详细说明 | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| -| `tenant_id` | string | Y | - | 租户ID,用于隔离不同站点或客户的数据 | | `query` | string | Y | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | | `size` | integer | N | 10 | 返回结果数量(1-100) | | `from` | integer | N | 0 | 分页偏移量(用于分页) | +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 | | `filters` | object | N | null | 精确匹配过滤器(见下文) | | `range_filters` | object | N | null | 数值范围过滤器(见下文) | | `facets` | array | N | null | 分面配置(见下文) | -| `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`, `title`) | +| `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | `min_score` | float | N | null | 最小相关性分数阈值 | | `debug` | boolean | N | false | 是否返回调试信息 | @@ -139,9 +141,17 @@ curl -X POST "http://120.76.41.98:6002/search/" \ ```json { "filters": { - "category.keyword": "玩具", // 单值:精确匹配 - "vendor.keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) - "tags.keyword": "益智玩具" // 单值:精确匹配 + "category_name": "手机", // 单值:精确匹配 + "category1_name": "服装", // 一级类目 + "category2_name": "男装", // 二级类目 + "category3_name": "衬衫", // 三级类目 + "vendor_zh.keyword": ["奇乐", "品牌A"], // 数组:匹配任意值(OR) + "tags": "手机", // 标签(keyword类型) + // specifications 嵌套过滤(特殊格式) + "specifications": { + "name": "color", + "value": "white" + } } } ``` @@ -151,11 +161,46 @@ curl -X POST "http://120.76.41.98:6002/search/" \ - 整数:精确匹配 - 布尔值:精确匹配 - 数组:匹配任意值(OR 逻辑) +- 对象:specifications 嵌套过滤(见下文) + +**Specifications 嵌套过滤**: + +`specifications` 是嵌套字段,支持按规格名称和值进行过滤。 + +**单个规格过滤**: +```json +{ + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` +查询规格名称为"color"且值为"white"的商品。 + +**多个规格过滤(OR 逻辑)**: +```json +{ + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` +查询满足任意一个规格的商品(color=white **或** size=256GB)。 **常用过滤字段**: -- `category.keyword`: 类目 -- `vendor.keyword`: 品牌/供应商 -- `tags.keyword`: 标签 +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型,支持数组) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 +- `specifications`: 规格过滤(嵌套字段,格式见上文) #### 2. 范围过滤器 (range_filters) @@ -201,7 +246,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ **简单模式**(字符串数组): ```json { - "facets": ["category.keyword", "vendor.keyword"] + "facets": ["category1_name", "category2_name", "category3_name", "specifications"] } ``` @@ -210,7 +255,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ { "facets": [ { - "field": "category.keyword", + "field": "category1_name", "size": 15, "type": "terms" }, @@ -223,6 +268,53 @@ curl -X POST "http://120.76.41.98:6002/search/" \ {"key": "100-200", "from": 100, "to": 200}, {"key": "200+", "from": 200} ] + }, + "specifications" // 规格分面(特殊处理:嵌套聚合,按name分组,然后按value聚合) + ] +} +``` + +**规格分面说明**: + +`specifications` 是嵌套字段,支持两种分面模式: + +**模式1:所有规格名称的分面** (`"specifications"`): +```json +{ + "facets": ["specifications"] +} +``` +返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 + +**模式2:指定规格名称的分面** (`"specifications.color"`): +```json +{ + "facets": ["specifications.color", "specifications.size"] +} +``` +只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size")。 + +**返回格式示例**: +```json +{ + "facets": [ + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + {"value": "white", "count": 50, "selected": false}, + {"value": "black", "count": 30, "selected": false} + ] + }, + { + "field": "specifications.size", + "label": "size", + "type": "terms", + "values": [ + {"value": "256GB", "count": 40, "selected": false}, + {"value": "512GB", "count": 20, "selected": false} + ] } ] } @@ -276,28 +368,47 @@ curl -X POST "http://120.76.41.98:6002/search/" \ { "spu_id": "12345", "title": "芭比时尚娃娃", - "handle": "barbie-doll", - "description": "高品质芭比娃娃", + "brief": "高品质芭比娃娃", + "description": "详细描述...", "vendor": "美泰", "category": "玩具", - "tags": "娃娃, 玩具, 女孩", + "category_path": "玩具/娃娃/时尚", + "category_name": "时尚", + "category_id": "cat_001", + "category_level": 3, + "category1_name": "玩具", + "category2_name": "娃娃", + "category3_name": "时尚", + "tags": ["娃娃", "玩具", "女孩"], "price": 89.99, "compare_at_price": 129.99, "currency": "USD", "image_url": "https://example.com/image.jpg", "in_stock": true, + "sku_prices": [89.99, 99.99, 109.99], + "sku_weights": [100, 150, 200], + "sku_weight_units": ["g", "g", "g"], + "total_inventory": 500, + "option1_name": "color", + "option2_name": "size", + "option3_name": null, + "specifications": [ + {"sku_id": "sku_001", "name": "color", "value": "pink"}, + {"sku_id": "sku_001", "name": "size", "value": "standard"} + ], "skus": [ { "sku_id": "67890", - "title": "粉色款", "price": 89.99, "compare_at_price": 129.99, "sku": "BARBIE-001", "stock": 100, - "options": { - "option1": "粉色", - "option2": "标准款" - } + "weight": 0.1, + "weight_unit": "kg", + "option1_value": "pink", + "option2_value": "standard", + "option3_value": null, + "image_src": "https://example.com/sku1.jpg" } ], "relevance_score": 8.5 @@ -307,8 +418,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ "max_score": 8.5, "facets": [ { - "field": "category.keyword", - "label": "category.keyword", + "field": "category1_name", + "label": "category1_name", "type": "terms", "values": [ { @@ -318,6 +429,19 @@ curl -X POST "http://120.76.41.98:6002/search/" \ "selected": false } ] + }, + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + { + "value": "pink", + "label": "pink", + "count": 30, + "selected": false + } + ] } ], "query_info": { @@ -356,31 +480,55 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | 字段 | 类型 | 说明 | |------|------|------| | `spu_id` | string | SPU ID | -| `title` | string | 商品标题 | -| `handle` | string | 商品URL handle | -| `description` | string | 商品描述 | -| `vendor` | string | 供应商/品牌 | -| `category` | string | 类目 | -| `tags` | string | 标签 | +| `title` | string | 商品标题(根据language参数自动选择title_zh或title_en) | +| `brief` | string | 商品短描述(根据language参数自动选择) | +| `description` | string | 商品详细描述(根据language参数自动选择) | +| `vendor` | string | 供应商/品牌(根据language参数自动选择) | +| `category` | string | 类目(兼容字段,等同于category_name) | +| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) | +| `category_name` | string | 类目名称(展示用,根据language参数自动选择) | +| `category_id` | string | 类目ID | +| `category_level` | integer | 类目层级(1/2/3) | +| `category1_name` | string | 一级类目名称 | +| `category2_name` | string | 二级类目名称 | +| `category3_name` | string | 三级类目名称 | +| `tags` | array[string] | 标签列表 | | `price` | float | 价格(min_price) | | `compare_at_price` | float | 原价 | | `currency` | string | 货币单位(默认USD) | | `image_url` | string | 主图URL | | `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | +| `sku_prices` | array[float] | 所有SKU价格列表 | +| `sku_weights` | array[integer] | 所有SKU重量列表 | +| `sku_weight_units` | array[string] | 所有SKU重量单位列表 | +| `total_inventory` | integer | 总库存 | +| `option1_name` | string | 选项1名称(如"color") | +| `option2_name` | string | 选项2名称(如"size") | +| `option3_name` | string | 选项3名称 | +| `specifications` | array[object] | 规格列表(与ES specifications字段对应) | | `skus` | array | SKU 列表 | | `relevance_score` | float | 相关性分数 | +**多语言字段说明**: +- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段 +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 + ### SkuResult字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `sku_id` | string | SKU ID | -| `title` | string | SKU标题 | | `price` | float | 价格 | | `compare_at_price` | float | 原价 | -| `sku` | string | SKU编码 | +| `sku` | string | SKU编码(sku_code) | | `stock` | integer | 库存数量 | -| `options` | object | 选项(颜色、尺寸等) | +| `weight` | float | 重量 | +| `weight_unit` | string | 重量单位 | +| `option1_value` | string | 选项1取值(如color值) | +| `option2_value` | string | 选项2取值(如size值) | +| `option3_value` | string | 选项3取值 | +| `image_src` | string | SKU图片地址 | --- @@ -408,8 +556,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ { "query": "玩具", "size": 20, + "language": "zh", "filters": { - "category.keyword": "益智玩具" + "category_name": "益智玩具" }, "range_filters": { "min_price": { @@ -428,23 +577,26 @@ curl -X POST "http://120.76.41.98:6002/search/" \ { "query": "玩具", "size": 20, + "language": "zh", "facets": [ - "category.keyword", - "vendor.keyword" + "category1_name", + "category2_name", + "specifications" ] } ``` ### 场景4:多条件组合搜索 -**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计 +**需求**: 搜索"手机",筛选多个品牌,价格范围,并获取分面统计 ```json { - "query": "玩具", + "query": "手机", "size": 20, + "language": "zh", "filters": { - "vendor.keyword": ["乐高", "孩之宝", "美泰"] + "vendor_zh.keyword": ["品牌A", "品牌B"] }, "range_filters": { "min_price": { @@ -454,7 +606,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ }, "facets": [ { - "field": "category.keyword", + "field": "category1_name", "size": 15 }, { @@ -466,31 +618,117 @@ curl -X POST "http://120.76.41.98:6002/search/" \ {"key": "100-200", "from": 100, "to": 200}, {"key": "200+", "from": 200} ] - } + }, + "specifications" ], "sort_by": "min_price", "sort_order": "asc" } ``` -### 场景5:布尔表达式搜索 +### 场景5:规格过滤搜索 + +**需求**: 搜索"手机",筛选color为"white"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +### 场景6:多个规格过滤(OR逻辑) + +**需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` + +### 场景7:规格分面搜索 + +**需求**: 搜索"手机",获取所有规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": ["specifications"] +} +``` + +**需求**: 只获取"color"规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": ["specifications.color", "specifications.size"] +} +``` + +### 场景8:组合过滤和分面 + +**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "category_name": "手机", + "specifications": { + "name": "color", + "value": "white" + } + }, + "facets": [ + "category1_name", + "category2_name", + "specifications.color", + "specifications.size" + ] +} +``` + +### 场景9:布尔表达式搜索 -**需求**: 搜索包含"玩具"和"乐高"的商品,排除"电动" +**需求**: 搜索包含"手机"和"智能"的商品,排除"二手" ```json { - "query": "玩具 AND 乐高 ANDNOT 电动", + "query": "手机 AND 智能 ANDNOT 二手", "size": 20 } ``` -### 场景6:分页查询 +### 场景10:分页查询 **需求**: 获取第2页结果(每页20条) ```json { - "query": "玩具", + "query": "手机", "size": 20, "from": 20 } @@ -650,25 +888,33 @@ curl "http://localhost:6002/search/12345" | 字段名 | 类型 | 描述 | |--------|------|------| +| `tenant_id` | keyword | 租户ID(多租户隔离) | | `spu_id` | keyword | SPU ID | -| `sku_id` | keyword/long | SKU ID(主键) | -| `title` | text | 商品名称(中文) | -| `en_title` | text | 商品名称(英文) | -| `ru_title` | text | 商品名称(俄文) | -| `category.keyword` | keyword | 类目(精确匹配) | -| `vendor.keyword` | keyword | 品牌/供应商(精确匹配) | -| `category` | HKText | 类目(支持 `category.keyword` 精确匹配) | -| `tags.keyword` | keyword | 标签 | -| `min_price` | double | 最低价格 | -| `max_price` | double | 最高价格 | -| `compare_at_price` | double | 原价 | -| `create_time` | date | 创建时间 | -| `update_time` | date | 更新时间 | -| `in_stock` | boolean | 是否有库存 | -| `text_embedding` | dense_vector | 文本向量(1024 维) | -| `image_embedding` | dense_vector | 图片向量(1024 维) | - -> 不同租户可自定义字段名称。推荐将可过滤的文本字段配置为 HKText,这样即可同时支持全文检索和 `field.keyword` 精确过滤;数值字段单独建索引以用于排序/Range。 +| `title_zh`, `title_en` | text | 商品标题(中英文) | +| `brief_zh`, `brief_en` | text | 商品短描述(中英文) | +| `description_zh`, `description_en` | text | 商品详细描述(中英文) | +| `vendor_zh`, `vendor_en` | text | 供应商/品牌(中英文,含keyword子字段) | +| `category_path_zh`, `category_path_en` | text | 类目路径(中英文,用于搜索) | +| `category_name_zh`, `category_name_en` | text | 类目名称(中英文,用于搜索) | +| `category_id` | keyword | 类目ID | +| `category_name` | keyword | 类目名称(用于过滤) | +| `category_level` | integer | 类目层级 | +| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) | +| `tags` | keyword | 标签(数组) | +| `specifications` | nested | 规格(嵌套对象数组) | +| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 | +| `min_price`, `max_price` | float | 最低/最高价格 | +| `compare_at_price` | float | 原价 | +| `sku_prices` | float | SKU价格列表(数组) | +| `sku_weights` | long | SKU重量列表(数组) | +| `sku_weight_units` | keyword | SKU重量单位列表(数组) | +| `total_inventory` | long | 总库存 | +| `skus` | nested | SKU详细信息(嵌套对象数组) | +| `create_time`, `update_time` | date | 创建/更新时间 | +| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | +| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) | + +> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。 --- @@ -676,11 +922,14 @@ curl "http://localhost:6002/search/12345" ### 常用字段列表 -#### 过滤字段(使用 HKText 的 keyword 子字段) +#### 过滤字段 -- `category.keyword`: 类目 -- `vendor.keyword`: 品牌/供应商 -- `tags.keyword`: 标签 +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 #### 范围字段 @@ -694,7 +943,6 @@ curl "http://localhost:6002/search/12345" - `min_price`: 最低价格 - `max_price`: 最高价格 -- `title`: 标题(字母序) - `create_time`: 创建时间 - `update_time`: 更新时间 - `relevance_score`: 相关性分数(默认) @@ -703,23 +951,21 @@ curl "http://localhost:6002/search/12345" | 分析器 | 语言 | 描述 | |--------|------|------| -| `chinese_ecommerce` | 中文 | 基于 Ansj 的电商优化中文分析器 | -| `english` | 英文 | 标准英文分析器 | -| `russian` | 俄文 | 俄文分析器 | -| `arabic` | 阿拉伯文 | 阿拉伯文分析器 | -| `spanish` | 西班牙文 | 西班牙文分析器 | -| `japanese` | 日文 | 日文分析器 | +| `hanlp_index` | 中文 | 中文索引分析器(用于中文字段) | +| `hanlp_standard` | 中文 | 中文查询分析器(用于中文字段) | +| `english` | 英文 | 标准英文分析器(用于英文字段) | +| `lowercase` | - | 小写标准化器(用于keyword子字段) | ### 字段类型速查 | 类型 | ES Mapping | 用途 | |------|------------|------| -| `TEXT` | `text` | 全文检索 | -| `KEYWORD` | `keyword` | 精确匹配、聚合、排序 | -| `LONG` | `long` | 整数 | -| `DOUBLE` | `double` | 浮点数 | -| `DATE` | `date` | 日期时间 | -| `BOOLEAN` | `boolean` | 布尔值 | -| `TEXT_EMBEDDING` | `dense_vector` | 文本语义向量 | -| `IMAGE_EMBEDDING` | `dense_vector` | 图片语义向量 | +| `text` | `text` | 全文检索(支持中英文分析器) | +| `keyword` | `keyword` | 精确匹配、聚合、排序 | +| `integer` | `integer` | 整数 | +| `long` | `long` | 长整数 | +| `float` | `float` | 浮点数 | +| `date` | `date` | 日期时间 | +| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) | +| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) | diff --git a/docs/搜索API速查表.md b/docs/搜索API速查表.md index 980adb7..4393f4d 100644 --- a/docs/搜索API速查表.md +++ b/docs/搜索API速查表.md @@ -17,8 +17,38 @@ POST /search/ ```bash { "filters": { - "category.keyword": "玩具", // 单值 - "vendor.keyword": ["乐高", "美泰"] // 多值(OR) + "category_name": "手机", // 单值 + "category1_name": "服装", // 一级类目 + "vendor_zh.keyword": ["奇乐", "品牌A"], // 多值(OR) + "tags": "手机", // 标签 + // specifications 嵌套过滤 + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +### Specifications 过滤 + +**单个规格**: +```bash +{ + "filters": { + "specifications": {"name": "color", "value": "white"} + } +} +``` + +**多个规格(OR)**: +```bash +{ + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] } } ``` @@ -48,7 +78,23 @@ POST /search/ ```bash { - "facets": ["category.keyword", "vendor.keyword"] + "facets": ["category1_name", "category2_name", "category3_name", "specifications"] +} +``` + +### Specifications 分面 + +**所有规格名称**: +```bash +{ + "facets": ["specifications"] // 返回所有name及其value列表 +} +``` + +**指定规格名称**: +```bash +{ + "facets": ["specifications.color", "specifications.size"] // 只返回指定name的value列表 } ``` @@ -57,15 +103,18 @@ POST /search/ ```bash { "facets": [ - {"field": "category.keyword", "size": 15}, + {"field": "category1_name", "size": 15}, { - "field": "price", + "field": "min_price", "type": "range", "ranges": [ {"key": "0-50", "to": 50}, {"key": "50-100", "from": 50, "to": 100} ] - } + }, + "specifications", // 所有规格名称 + "specifications.color", // 指定规格名称 + "specifications.size" ] } ``` @@ -110,19 +159,25 @@ POST /search/ ```bash POST /search/ +Headers: X-Tenant-ID: 2 { - "query": "玩具", + "query": "手机", "size": 20, "from": 0, + "language": "zh", "filters": { - "category.keyword": ["玩具", "益智玩具"] + "category_name": "手机", + "category1_name": "电子产品", + "specifications": {"name": "color", "value": "white"} }, "range_filters": { - "price": {"gte": 50, "lte": 200} + "min_price": {"gte": 50, "lte": 200} }, "facets": [ - {"field": "vendor.keyword", "size": 15}, - {"field": "category.keyword", "size": 15} + {"field": "category1_name", "size": 15}, + {"field": "category2_name", "size": 15}, + "specifications.color", + "specifications.size" ], "sort_by": "min_price", "sort_order": "asc" @@ -135,11 +190,29 @@ POST /search/ ```json { - "hits": [ + "results": [ { - "_id": "12345", - "_score": 8.5, - "_source": {...} + "spu_id": "12345", + "title": "商品标题", + "brief": "短描述", + "description": "详细描述", + "vendor": "供应商", + "category": "类目", + "category_path": "类目/路径", + "category_name": "类目名称", + "category1_name": "一级类目", + "category2_name": "二级类目", + "category3_name": "三级类目", + "tags": ["标签1", "标签2"], + "price": 99.99, + "compare_at_price": 149.99, + "sku_prices": [99.99, 109.99], + "total_inventory": 500, + "specifications": [ + {"sku_id": "sku_001", "name": "color", "value": "white"} + ], + "skus": [...], + "relevance_score": 8.5 } ], "total": 118, @@ -147,17 +220,30 @@ POST /search/ "took_ms": 45, "facets": [ { - "field": "category.keyword", - "label": "商品类目", + "field": "category1_name", + "label": "category1_name", "type": "terms", "values": [ { - "value": "玩具", - "label": "玩具", + "value": "手机", + "label": "手机", "count": 85, "selected": false } ] + }, + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + { + "value": "white", + "label": "white", + "count": 30, + "selected": false + } + ] } ] } @@ -192,14 +278,19 @@ GET /admin/stats ```python import requests -result = requests.post('http://localhost:6002/search/', json={ - "query": "玩具", - "filters": {"category.keyword": "玩具"}, - "range_filters": {"price": {"gte": 50, "lte": 200}}, - "facets": ["vendor.keyword"], - "sort_by": "min_price", - "sort_order": "asc" -}).json() +result = requests.post( + 'http://localhost:6002/search/', + headers={'X-Tenant-ID': '2'}, + json={ + "query": "手机", + "language": "zh", + "filters": {"category_name": "手机"}, + "range_filters": {"min_price": {"gte": 50, "lte": 200}}, + "facets": ["category1_name", "specifications"], + "sort_by": "min_price", + "sort_order": "asc" + } +).json() print(f"找到 {result['total']} 个结果") ``` @@ -211,12 +302,16 @@ print(f"找到 {result['total']} 个结果") ```javascript const result = await fetch('http://localhost:6002/search/', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + 'X-Tenant-ID': '2' + }, body: JSON.stringify({ - query: "玩具", - filters: {category.keyword: "玩具"}, - range_filters: {price: {gte: 50, lte: 200}}, - facets: ["vendor.keyword"], + query: "手机", + language: "zh", + filters: {category_name: "手机"}, + range_filters: {min_price: {gte: 50, lte: 200}}, + facets: ["category1_name", "specifications"], sort_by: "min_price", sort_order: "asc" }) diff --git a/docs/系统设计文档.md b/docs/系统设计文档.md index 6493f41..076dddc 100644 --- a/docs/系统设计文档.md +++ b/docs/系统设计文档.md @@ -215,7 +215,7 @@ indexes: 4. 组合多个语言查询的结果,提高召回率 **实现模块**: -- `search/multilang_query_builder.py` - 多语言查询构建器 +- `search/es_query_builder.py` - ES 查询构建器(单层架构) - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) --- @@ -345,7 +345,7 @@ query_config: #### 工作流程 ``` -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建 +查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) ``` #### 实现模块 @@ -421,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap - 提取域(如 `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) +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/multilang_query_builder.py` - 多语言查询构建器 -- `search/searcher.py` - 搜索器(使用多语言构建器) +- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) +- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) ### 5.3 相关性计算(Ranking) diff --git a/docs/索引字段说明v2.md b/docs/索引字段说明v2.md index 8593956..01f9fb4 100644 --- a/docs/索引字段说明v2.md +++ b/docs/索引字段说明v2.md @@ -1,170 +1,199 @@ -SPU-SKU索引方案选型 -1. spu为单位。SKU字段展开作为SPU属性 -1.1 索引方案 -除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有款式、价格、重量、库存等相关属性。所以,可以以spu为单位建立索引。 -sku中需要参与搜索的属性(比如价格、库存)展开到spu。 -sku的所有需要返回的字段作为nested字段,仅用于返回。 -# 写入 spu 级别索引 -def build_product_document(product, variants): - return { - "spu_id": str(product.id), - "title": product.title, - - # Variant搜索字段(展开) - # 价格(int)、重量(int)、重量单位拼接重量(keyword),都以list形式灌入 - # TODO 按要求补充 - - # 库存总和 将sku的库存加起来作为一个值灌入 - # 售价,灌入3个字段,一个 sku价格 以list形式灌入,一个最高价一个最低价 - # TODO 按要求补充 - - # Variant详细信息(用于返回) - "variants": [ - { - "sku_id": str(v.id), - "price": float(v.price), - "options": v.options - } - for v in variants - ], +# 索引字段说明 v2 + +本文档详细说明 `search_products` 索引的字段结构、类型、数据来源和用途。 + +## 索引概述 + +- **索引名称**: `search_products` +- **索引维度**: SPU(Standard Product Unit)级别 +- **多租户隔离**: 通过 `tenant_id` 字段实现 +- **Mapping 文件**: `mappings/search_products.json` + +## 字段分类 + +### 1. 基础标识字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `tenant_id` | keyword | 租户ID,用于多租户隔离 | MySQL: `shoplazza_product_spu.tenant_id` | +| `spu_id` | keyword | SPU唯一标识 | MySQL: `shoplazza_product_spu.id` | +| `create_time` | date | 创建时间 | MySQL: `shoplazza_product_spu.created_at` | +| `update_time` | date | 更新时间 | MySQL: `shoplazza_product_spu.updated_at` | + +### 2. 多语言文本字段 + +所有文本字段都支持中英文双语,后端根据请求的 `language` 参数自动选择对应语言字段返回。 + +#### 2.1 标题字段 + +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | +|--------|--------|--------|------|----------| +| `title_zh` | text | hanlp_index / hanlp_standard | 中文标题 | MySQL: `shoplazza_product_spu.title` | +| `title_en` | text | english | 英文标题 | 暂为空(待翻译服务填充) | + +#### 2.2 描述字段 + +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | +|--------|--------|--------|------|----------| +| `brief_zh` | text | hanlp_index / hanlp_standard | 中文短描述 | MySQL: `shoplazza_product_spu.brief` | +| `brief_en` | text | english | 英文短描述 | 暂为空 | +| `description_zh` | text | hanlp_index / hanlp_standard | 中文详细描述 | MySQL: `shoplazza_product_spu.description` | +| `description_en` | text | english | 英文详细描述 | 暂为空 | + +#### 2.3 供应商/品牌字段 + +| 字段名 | ES类型 | 分析器 | 子字段 | 说明 | 数据来源 | +|--------|--------|--------|--------|------|----------| +| `vendor_zh` | text | hanlp_index / hanlp_standard | `vendor_zh.keyword` (keyword, normalizer: lowercase) | 中文供应商/品牌 | MySQL: `shoplazza_product_spu.vendor` | +| `vendor_en` | text | english | `vendor_en.keyword` (keyword, normalizer: lowercase) | 英文供应商/品牌 | 暂为空 | + +**用途**: +- `text` 类型:用于全文搜索(支持模糊匹配) +- `keyword` 子字段:用于精确匹配过滤和分面聚合 + +### 3. 标签字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `tags` | keyword | 标签列表(数组) | MySQL: `shoplazza_product_spu.tags`(逗号分隔字符串,转换为数组) | + +**数据格式**: `["新品", "热卖", "爆款"]` + +### 4. 类目字段 + +#### 4.1 类目路径(用于搜索) + +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | +|--------|--------|--------|------|----------| +| `category_path_zh` | text | hanlp_index / hanlp_standard | 中文类目路径(如"服装/男装/衬衫") | MySQL: `shoplazza_product_spu.category_path` | +| `category_path_en` | text | english | 英文类目路径 | 暂为空 | + +#### 4.2 类目名称(用于搜索) + +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | +|--------|--------|--------|------|----------| +| `category_name_zh` | text | hanlp_index / hanlp_standard | 中文类目名称 | MySQL: `shoplazza_product_spu.category` | +| `category_name_en` | text | english | 英文类目名称 | 暂为空 | + +#### 4.3 类目标识(用于过滤和分面) + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `category_id` | keyword | 类目ID | MySQL: `shoplazza_product_spu.category_id` | +| `category_name` | keyword | 类目名称(用于过滤) | MySQL: `shoplazza_product_spu.category` | +| `category_level` | integer | 类目层级(1/2/3) | MySQL: `shoplazza_product_spu.category_level` | +| `category1_name` | keyword | 一级类目名称 | 从 `category_path` 解析 | +| `category2_name` | keyword | 二级类目名称 | 从 `category_path` 解析 | +| `category3_name` | keyword | 三级类目名称 | 从 `category_path` 解析 | + +**用途**: +- `category_path_zh/en`, `category_name_zh/en`: 用于全文搜索,支持模糊匹配 +- `category_id`, `category_name`, `category_level`, `category1/2/3_name`: 用于精确过滤和分面聚合 + +### 5. 规格字段(Specifications) - - "min_price": min(v.price for v in variants), - "max_price": max(v.price for v in variants) +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `specifications` | nested | 规格列表(嵌套对象数组) | MySQL: `shoplazza_product_option` + `shoplazza_product_sku.option1/2/3` | + +**嵌套结构**: +```json +{ + "specifications": [ + { + "sku_id": "sku_123", + "name": "color", + "value": "white" + }, + { + "sku_id": "sku_123", + "name": "size", + "value": "256GB" } -1.2 查询方案 -对数组字段使用 dis_max,只取最高分,避免累加。 -其他重点字段 -1. Sku title -2. category -2.1 Mysql -在spu表中: -Field Type -category varchar(255) -category_id bigint(20) -category_google_id bigint(20) -category_level int(11) -category_path varchar(500) -2.2 ES索引 -2.2.1 输入数据 - 设计 1,2,3级分类 三个字段,的 category (原始文本) -2.2.2 索引方法 - 设计要求: - 1. 支持facet(精确过滤、keyword聚合),并且性能需要足够高。 - 2. 支持普通搜索模糊匹配(用户原始query可能包括分类词)。 - 3. 模糊匹配要考虑多语言 -方案:采用方案2 - 1. categoryPath索引 + Prefix 查询(categoryPath.keyword: "服装/男装")(如果满足条件的key太多的则性能较差,比如 查询的是一级类目,类目树叶子节点太多时性能较差) - 2. categoryPath支撑模糊查询 和 多级cate keyword索引支撑精确查询。 索引阶段冗余,查询性能高。 - "category_path_zh": { // 提供模糊查询功能,辅助相关性计算 - "type": "text", - "analyzer": "hanlp_index", - "search_analyzer": "hanlp_standard" - }, - "category_path_en": { // 提供模糊查询功能,辅助相关性计算 - "type": "text", - "analyzer": "english", - "search_analyzer": "english" - }, - "category_path": { // 用于多层级的筛选、精确匹配 - "type": "keyword", - "normalizer": "lowercase" - }, - "category_id": { - "type": "keyword" - }, - "category_name": { - "type": "keyword" - }, - "category_level": { - "type": "integer" - }, - "category1_name": { // 不同层级下 可能有同名的情况,因此提供一二三级分开的查询方式 - "type": "keyword" - }, - "category2_name": { - "type": "keyword" - }, - "category3_name": { - "type": "keyword" - }, - -3. tags -3.1 数据源 -多值 -标签 -最多输入250个标签,每个不得超过500字符,多个标签请用「英文逗号」隔开 -新品,热卖,爆款 -耳机,头戴式,爆款 - -分割后 list形式灌入 -3.2 Mysql -3.3 ES索引 -3.3.1 输入数据 -3.3.2 索引方法 -4. 供应商 -4.1 数据源 -4.2 Mysql -4.3 ES索引 -4.3.1 输入数据 -4.3.2 索引方法 -5. 款式/选项值(options) -5.1 数据源 -以下区域字段,商品属性为M(商品主体)的行需填写款式名称,商品属性为P(子款式)的行需填写款式值信息,商品属性为S(单一款式商品)的行无需填写 -款式1 款式2 款式3 -最多255字符 最多255字符 最多255字符 -SIZE COLOR -S red -... -5.2 Mysql -1. API 在 SPU 的维度直接返回3个属性定义,存储在 shoplazza_product_option 中: -1. API在 SKU的维度直接返回3个属性值,存储在 shoplazza_product_sku 表的 option 相关的字段中: -5.3 ES索引 - - "specifications": { - "type": "nested", - "properties": { - "name": { "type": "keyword" }, // "颜色", "容量" - "value": { "type": "keyword" } // "白色", "256GB" + ] +} +``` + +**数据来源**: +- `name`: 从 `shoplazza_product_option` 表获取(选项名称,如"color"、"size") +- `value`: 从 `shoplazza_product_sku` 表的 `option1`, `option2`, `option3` 字段获取(选项值,如"white"、"256GB") +- `sku_id`: SKU ID,用于关联 + +**API 过滤示例**: +```json +{ + "query": "手机", + "filters": { + "specifications": { + "name": "color", + "value": "white" } - }, - - 另外还需要包含一个单独的字段,main_option (即店铺主题装修里面配置的 颜色切换 - 变体名称,也就是列表页商品的子sku显示维度) - "main_option": { "type": "keyword" } -查询指定款式 + } +} +``` + +**多个规格过滤(OR逻辑)**: +```json { - "query": { - "nested": { - "path": "specifications", - "query": { - "bool": { - "must": [ - { "term": { "specifications.name ": "颜色" } }, - { "term": { "specifications.value": "绿色" } } - ] - } + "query": "手机", + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` + +**ES 查询结构**(后端自动生成): +```json +{ + "nested": { + "path": "specifications", + "query": { + "bool": { + "must": [ + { "term": { "specifications.name": "color" } }, + { "term": { "specifications.value": "white" } } + ] } } } } -按 name 做分面搜索(聚合) - +``` + +**API 分面示例**: + +所有规格名称的分面: +```json +{ + "query": "手机", + "facets": ["specifications"] +} +``` + +指定规格名称的分面: +```json +{ + "query": "手机", + "facets": ["specifications.color", "specifications.size"] +} +``` + +**ES 聚合结构**(后端自动生成): + +所有规格名称: +```json { "aggs": { - "specs": { + "specifications_facet": { "nested": { "path": "specifications" }, "aggs": { "by_name": { - "terms": { - "field": "specifications.name", - "size": 20 - }, + "terms": { "field": "specifications.name", "size": 20 }, "aggs": { "value_counts": { - "terms": { - "field": "specifications.value", - "size": 10 - } + "terms": { "field": "specifications.value", "size": 10 } } } } @@ -172,4 +201,204 @@ S red } } } - \ No newline at end of file +``` + +指定规格名称: +```json +{ + "aggs": { + "specifications_color_facet": { + "nested": { "path": "specifications" }, + "aggs": { + "filter_by_name": { + "filter": { "term": { "specifications.name": "color" } }, + "aggs": { + "value_counts": { + "terms": { "field": "specifications.value", "size": 10 } + } + } + } + } + } + } +} +``` + +### 6. 选项名称字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `option1_name` | keyword | 选项1名称(如"color") | MySQL: `shoplazza_product_option` | +| `option2_name` | keyword | 选项2名称(如"size") | MySQL: `shoplazza_product_option` | +| `option3_name` | keyword | 选项3名称 | MySQL: `shoplazza_product_option` | + +### 7. 价格字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `min_price` | float | 最低价格 | 从所有 SKU 价格计算 | +| `max_price` | float | 最高价格 | 从所有 SKU 价格计算 | +| `compare_at_price` | float | 原价/对比价 | MySQL: `shoplazza_product_spu.compare_at_price` | +| `sku_prices` | float | 所有 SKU 价格列表(数组) | 从所有 SKU 价格汇总 | + +### 8. 重量字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `sku_weights` | long | 所有 SKU 重量列表(数组) | 从所有 SKU 重量汇总 | +| `sku_weight_units` | keyword | 所有 SKU 重量单位列表(数组) | 从所有 SKU 重量单位汇总 | + +### 9. 库存字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `total_inventory` | long | 总库存(所有 SKU 库存之和) | 从所有 SKU 库存汇总 | + +### 10. SKU 嵌套字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `skus` | nested | SKU 详细信息列表(嵌套对象数组) | MySQL: `shoplazza_product_sku` | + +**嵌套结构**: +```json +{ + "skus": [ + { + "sku_id": "sku_123", + "price": 99.99, + "compare_at_price": 149.99, + "sku_code": "SKU001", + "stock": 100, + "weight": 0.5, + "weight_unit": "kg", + "option1_value": "white", + "option2_value": "256GB", + "option3_value": null, + "image_src": "https://example.com/image.jpg" + } + ] +} +``` + +**字段说明**: +- `sku_id`: SKU 唯一标识 +- `price`: SKU 价格 +- `compare_at_price`: SKU 原价 +- `sku_code`: SKU 编码 +- `stock`: 库存数量 +- `weight`: 重量 +- `weight_unit`: 重量单位 +- `option1_value`, `option2_value`, `option3_value`: 选项值(对应 `option1_name`, `option2_name`, `option3_name`) +- `image_src`: SKU 图片地址(`index: false`,仅用于返回) + +### 11. 图片字段 + +| 字段名 | ES类型 | 说明 | 数据来源 | +|--------|--------|------|----------| +| `image_url` | keyword | 主图URL(`index: false`,仅用于返回) | MySQL: `shoplazza_product_spu.image_url` | + +### 12. 向量字段(不返回给前端) + +| 字段名 | ES类型 | 维度 | 说明 | 数据来源 | +|--------|--------|------|------|----------| +| `title_embedding` | dense_vector | 1024 | 标题向量(用于语义搜索) | 由 BGE-M3 模型生成 | +| `image_embedding` | nested | - | 图片向量(用于图片搜索) | 由 CN-CLIP 模型生成 | + +**注意**: 这些字段仅用于搜索,不会返回给前端。 + +## 字段用途总结 + +### 搜索字段(参与相关性计算) + +- `title_zh`, `title_en` (boost: 3.0) +- `brief_zh`, `brief_en` (boost: 1.5) +- `description_zh`, `description_en` (boost: 1.0) +- `vendor_zh`, `vendor_en` (boost: 1.5) +- `tags` (boost: 1.0) +- `category_path_zh`, `category_path_en` (boost: 1.5) +- `category_name_zh`, `category_name_en` (boost: 1.5) +- `title_embedding` (向量召回,boost: 0.2) + +### 过滤字段(精确匹配) + +- `tenant_id` (必需,多租户隔离) +- `category_id`, `category_name`, `category1_name`, `category2_name`, `category3_name` +- `vendor_zh.keyword`, `vendor_en.keyword` +- `specifications` (嵌套查询) +- `min_price`, `max_price` (范围过滤) + +### 分面字段(聚合统计) + +- `category1_name`, `category2_name`, `category3_name` +- `specifications` (所有规格名称的分面,嵌套聚合,按 name 分组,然后按 value 聚合) +- `specifications.{name}` (指定规格名称的分面,如 `specifications.color`,只返回该 name 的 value 列表) + +### 返回字段(前端展示) + +除 `title_embedding` 和 `image_embedding` 外,所有字段都会根据 `language` 参数自动选择对应的中英文字段返回。 + +## 数据映射规则 + +### 多语言字段映射 + +后端根据请求的 `language` 参数(`zh` 或 `en`)自动选择: + +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 + +映射到前端字段: +- `title_zh/en` → `title` +- `brief_zh/en` → `brief` +- `description_zh/en` → `description` +- `vendor_zh/en` → `vendor` +- `category_path_zh/en` → `category_path` +- `category_name_zh/en` → `category_name` + +### 规格数据构建 + +1. 从 `shoplazza_product_option` 表获取选项名称(`option1_name`, `option2_name`, `option3_name`) +2. 从 `shoplazza_product_sku` 表获取选项值(`option1`, `option2`, `option3`) +3. 将每个 SKU 的选项组合构建为 `specifications` 数组: + ```python + for sku in skus: + if sku.option1 and option1_name: + specifications.append({ + "sku_id": sku.id, + "name": option1_name, # 如"color" + "value": sku.option1 # 如"white" + }) + # 同样处理 option2, option3 + ``` + +## 查询架构 + +### 查询结构 + +``` +filters AND (text_recall OR embedding_recall) +``` + +- **filters**: 前端传递的过滤条件(永远起作用) +- **text_recall**: 文本相关性召回(同时搜索中英文字段) +- **embedding_recall**: 向量召回(KNN) +- **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) + +### 文本召回字段 + +默认同时搜索以下字段(中英文都包含): +- `title_zh^3.0`, `title_en^3.0` +- `brief_zh^1.5`, `brief_en^1.5` +- `description_zh^1.0`, `description_en^1.0` +- `vendor_zh^1.5`, `vendor_en^1.5` +- `category_path_zh^1.5`, `category_path_en^1.5` +- `category_name_zh^1.5`, `category_name_en^1.5` +- `tags^1.0` + +## 注意事项 + +1. **索引维度**: 所有数据以 SPU 为单位索引,SKU 信息作为嵌套字段存储 +2. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 +3. **多语言支持**: 文本字段支持中英文,后端根据 `language` 参数自动选择 +4. **规格分面**: `specifications` 使用嵌套聚合,按 `name` 分组,然后按 `value` 聚合 +5. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不返回给前端 diff --git a/search/es_query_builder.py b/search/es_query_builder.py index b661e40..7e7a793 100644 --- a/search/es_query_builder.py +++ b/search/es_query_builder.py @@ -352,6 +352,57 @@ class ESQueryBuilder: # 1. 处理精确匹配过滤 if filters: for field, value in filters.items(): + # 特殊处理:specifications 嵌套过滤 + if field == "specifications": + if isinstance(value, dict): + # 单个规格过滤:{"name": "color", "value": "green"} + name = value.get("name") + spec_value = value.get("value") + if name and spec_value: + filter_clauses.append({ + "nested": { + "path": "specifications", + "query": { + "bool": { + "must": [ + {"term": {"specifications.name": name}}, + {"term": {"specifications.value": spec_value}} + ] + } + } + } + }) + elif isinstance(value, list): + # 多个规格过滤(OR逻辑):[{"name": "color", "value": "green"}, ...] + should_clauses = [] + for spec in value: + if isinstance(spec, dict): + name = spec.get("name") + spec_value = spec.get("value") + if name and spec_value: + should_clauses.append({ + "nested": { + "path": "specifications", + "query": { + "bool": { + "must": [ + {"term": {"specifications.name": name}}, + {"term": {"specifications.value": spec_value}} + ] + } + } + } + }) + if should_clauses: + filter_clauses.append({ + "bool": { + "should": should_clauses, + "minimum_should_match": 1 + } + }) + continue + + # 普通字段过滤 if isinstance(value, list): # 多值匹配(OR) filter_clauses.append({ @@ -486,34 +537,62 @@ class ESQueryBuilder: for config in facet_configs: # 特殊处理:specifications嵌套分面 - if isinstance(config, str) and config == "specifications": - # 构建specifications嵌套分面(按name聚合,然后按value聚合) - aggs["specifications_facet"] = { - "nested": { - "path": "specifications" - }, - "aggs": { - "by_name": { - "terms": { - "field": "specifications.name", - "size": 20, - "order": {"_count": "desc"} - }, - "aggs": { - "value_counts": { - "terms": { - "field": "specifications.value", - "size": 10, - "order": {"_count": "desc"} + if isinstance(config, str): + # 格式1: "specifications" - 返回所有name的分面 + if config == "specifications": + aggs["specifications_facet"] = { + "nested": { + "path": "specifications" + }, + "aggs": { + "by_name": { + "terms": { + "field": "specifications.name", + "size": 20, + "order": {"_count": "desc"} + }, + "aggs": { + "value_counts": { + "terms": { + "field": "specifications.value", + "size": 10, + "order": {"_count": "desc"} + } } } } } } - } - continue + continue + + # 格式2: "specifications.color" 或 "specifications.颜色" - 只返回指定name的value列表 + if config.startswith("specifications."): + name = config[len("specifications."):] + agg_name = f"specifications_{name}_facet" + aggs[agg_name] = { + "nested": { + "path": "specifications" + }, + "aggs": { + "filter_by_name": { + "filter": { + "term": {"specifications.name": name} + }, + "aggs": { + "value_counts": { + "terms": { + "field": "specifications.value", + "size": 10, + "order": {"_count": "desc"} + } + } + } + } + } + } + continue - # 简单模式:只有字段名(字符串) + # 简单模式:只有字段名(字符串,非specifications) if isinstance(config, str): field = config agg_name = f"{field}_facet" @@ -524,6 +603,7 @@ class ESQueryBuilder: "order": {"_count": "desc"} } } + continue # 高级模式:FacetConfig 对象 else: -- libgit2 0.21.2