Commit f7d3cf70b2b2a3d50e8e4c3ce5aceaad157d9daa
1 parent
f0d020c3
更新文档
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 查询结构 添加了两种分面模式的说明和示例 更新了“分面字段”说明,明确支持指定规格名称的分面
Showing
10 changed files
with
1441 additions
and
614 deletions
Show diff stats
README.md
| @@ -54,11 +54,11 @@ curl -X POST http://localhost:6002/search/ \ | @@ -54,11 +54,11 @@ curl -X POST http://localhost:6002/search/ \ | ||
| 54 | |------|----------|----------| | 54 | |------|----------|----------| |
| 55 | | `环境配置说明.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 | | 55 | | `环境配置说明.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 | |
| 56 | | `Usage-Guide.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 | | 56 | | `Usage-Guide.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 | |
| 57 | -| `基础配置指南.md` | 租户字段、索引域、排序表达式配置流程 | 新租户开通、配置变更 | | 57 | +| `基础配置指南.md` | 统一硬编码配置说明、索引结构、查询配置 | 了解系统配置、修改配置 | |
| 58 | | `测试数据指南.md` | 两个租户的模拟/CSV 数据构造 & MySQL→ES 流程 | 数据准备、联调 | | 58 | | `测试数据指南.md` | 两个租户的模拟/CSV 数据构造 & MySQL→ES 流程 | 数据准备、联调 | |
| 59 | | `测试Pipeline说明.md` | 测试流水线、CI 脚本、上下文说明 | 自动化测试、追踪流水线 | | 59 | | `测试Pipeline说明.md` | 测试流水线、CI 脚本、上下文说明 | 自动化测试、追踪流水线 | |
| 60 | | `系统设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 | | 60 | | `系统设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 | |
| 61 | -| `索引字段说明.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | | 61 | +| `索引字段说明v2.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | |
| 62 | | `搜索API对接指南.md` | REST API(文本/图片/管理)详解、示例、响应格式 | API 使用、测试 | | 62 | | `搜索API对接指南.md` | REST API(文本/图片/管理)详解、示例、响应格式 | API 使用、测试 | |
| 63 | | `搜索API速查表.md` | 常用请求体、过滤器、分面速查表 | 支持团队快速查阅 | | 63 | | `搜索API速查表.md` | 常用请求体、过滤器、分面速查表 | 支持团队快速查阅 | |
| 64 | | `Search-API-Examples.md` | Python/JS/cURL 端到端示例 | 客户工程、SDK 参考 | | 64 | | `Search-API-Examples.md` | Python/JS/cURL 端到端示例 | 客户工程、SDK 参考 | |
| @@ -82,9 +82,11 @@ curl -X POST http://localhost:6002/search/ \ | @@ -82,9 +82,11 @@ curl -X POST http://localhost:6002/search/ \ | ||
| 82 | - API、分页、过滤、Facet、KNN 等:`搜索API对接指南.md` | 82 | - API、分页、过滤、Facet、KNN 等:`搜索API对接指南.md` |
| 83 | - 对接案例、示例与错误码:`搜索API对接指南.md`、`Search-API-Examples.md` | 83 | - 对接案例、示例与错误码:`搜索API对接指南.md`、`Search-API-Examples.md` |
| 84 | 84 | ||
| 85 | -- **配置驱动能力** | ||
| 86 | - - `config/schema/{tenant_id}/config.yaml`:字段定义、索引域、排序表达式、SPU 聚合 | ||
| 87 | - - 详解与设计理念:`设计文档.md`、`INDEX_FIELDS_DOCUMENTATION.md` | 85 | +- **统一配置** |
| 86 | + - 所有租户共享统一的索引结构和查询配置(硬编码) | ||
| 87 | + - 索引 mapping: `mappings/search_products.json` | ||
| 88 | + - 查询配置: `search/query_config.py` | ||
| 89 | + - 详解:`基础配置指南.md`、`索引字段说明v2.md` | ||
| 88 | 90 | ||
| 89 | ## 仓库结构(概览) | 91 | ## 仓库结构(概览) |
| 90 | 92 |
api/models.py
| @@ -75,15 +75,21 @@ class SearchRequest(BaseModel): | @@ -75,15 +75,21 @@ class SearchRequest(BaseModel): | ||
| 75 | ) | 75 | ) |
| 76 | 76 | ||
| 77 | # 过滤器 - 精确匹配和多值匹配 | 77 | # 过滤器 - 精确匹配和多值匹配 |
| 78 | - filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = Field( | 78 | + filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]], Dict[str, Any], List[Dict[str, Any]]]]] = Field( |
| 79 | None, | 79 | None, |
| 80 | - description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)", | 80 | + description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)。支持 specifications 嵌套过滤:{\"specifications\": {\"name\": \"color\", \"value\": \"green\"}} 或 [{\"name\": \"color\", \"value\": \"green\"}, ...]", |
| 81 | json_schema_extra={ | 81 | json_schema_extra={ |
| 82 | "examples": [ | 82 | "examples": [ |
| 83 | { | 83 | { |
| 84 | - "category.keyword": ["玩具", "益智玩具"], | ||
| 85 | - "vendor.keyword": "乐高", | ||
| 86 | - "in_stock": True | 84 | + "category_name": ["手机", "电子产品"], |
| 85 | + "vendor_zh.keyword": "奇乐", | ||
| 86 | + "specifications": {"name": "颜色", "value": "白色"} | ||
| 87 | + }, | ||
| 88 | + { | ||
| 89 | + "specifications": [ | ||
| 90 | + {"name": "颜色", "value": "白色"}, | ||
| 91 | + {"name": "尺寸", "value": "256GB"} | ||
| 92 | + ] | ||
| 87 | } | 93 | } |
| 88 | ] | 94 | ] |
| 89 | } | 95 | } |
| @@ -110,22 +116,25 @@ class SearchRequest(BaseModel): | @@ -110,22 +116,25 @@ class SearchRequest(BaseModel): | ||
| 110 | # 分面搜索 - 简化接口 | 116 | # 分面搜索 - 简化接口 |
| 111 | facets: Optional[List[Union[str, FacetConfig]]] = Field( | 117 | facets: Optional[List[Union[str, FacetConfig]]] = Field( |
| 112 | None, | 118 | None, |
| 113 | - description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象", | 119 | + description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象。支持 specifications 分面:\"specifications\"(所有name)或 \"specifications.color\"(指定name)", |
| 114 | json_schema_extra={ | 120 | json_schema_extra={ |
| 115 | "examples": [ | 121 | "examples": [ |
| 116 | # 简单模式:只指定字段名,使用默认配置 | 122 | # 简单模式:只指定字段名,使用默认配置 |
| 117 | - ["category.keyword", "vendor.keyword"], | 123 | + ["category1_name", "category2_name", "specifications"], |
| 124 | + # 指定specifications的某个name | ||
| 125 | + ["specifications.颜色", "specifications.尺寸"], | ||
| 118 | # 高级模式:详细配置 | 126 | # 高级模式:详细配置 |
| 119 | [ | 127 | [ |
| 120 | - {"field": "category.keyword", "size": 15}, | 128 | + {"field": "category1_name", "size": 15}, |
| 121 | { | 129 | { |
| 122 | - "field": "price", | 130 | + "field": "min_price", |
| 123 | "type": "range", | 131 | "type": "range", |
| 124 | "ranges": [ | 132 | "ranges": [ |
| 125 | {"key": "0-50", "to": 50}, | 133 | {"key": "0-50", "to": 50}, |
| 126 | {"key": "50-100", "from": 50, "to": 100} | 134 | {"key": "50-100", "from": 50, "to": 100} |
| 127 | ] | 135 | ] |
| 128 | - } | 136 | + }, |
| 137 | + "specifications" # 所有specifications name的分面 | ||
| 129 | ] | 138 | ] |
| 130 | ] | 139 | ] |
| 131 | } | 140 | } |
api/result_formatter.py
| @@ -143,7 +143,7 @@ class ResultFormatter: | @@ -143,7 +143,7 @@ class ResultFormatter: | ||
| 143 | for field_name, agg_data in es_aggregations.items(): | 143 | for field_name, agg_data in es_aggregations.items(): |
| 144 | display_field = field_name[:-6] if field_name.endswith("_facet") else field_name | 144 | display_field = field_name[:-6] if field_name.endswith("_facet") else field_name |
| 145 | 145 | ||
| 146 | - # 处理specifications嵌套分面 | 146 | + # 处理specifications嵌套分面(所有name) |
| 147 | if field_name == "specifications_facet" and 'by_name' in agg_data: | 147 | if field_name == "specifications_facet" and 'by_name' in agg_data: |
| 148 | # specifications嵌套聚合:按name分组,每个name下有value_counts | 148 | # specifications嵌套聚合:按name分组,每个name下有value_counts |
| 149 | by_name_agg = agg_data['by_name'] | 149 | by_name_agg = agg_data['by_name'] |
| @@ -174,6 +174,35 @@ class ResultFormatter: | @@ -174,6 +174,35 @@ class ResultFormatter: | ||
| 174 | facets.append(facet) | 174 | facets.append(facet) |
| 175 | continue | 175 | continue |
| 176 | 176 | ||
| 177 | + # 处理specifications嵌套分面(指定name) | ||
| 178 | + if field_name.startswith("specifications_") and field_name.endswith("_facet") and 'filter_by_name' in agg_data: | ||
| 179 | + # 提取name(从 "specifications_颜色_facet" 提取 "颜色") | ||
| 180 | + name = field_name[len("specifications_"):-len("_facet")] | ||
| 181 | + filter_by_name_agg = agg_data.get('filter_by_name', {}) | ||
| 182 | + value_counts = filter_by_name_agg.get('value_counts', {}) | ||
| 183 | + | ||
| 184 | + values = [] | ||
| 185 | + if 'buckets' in value_counts: | ||
| 186 | + for value_bucket in value_counts['buckets']: | ||
| 187 | + value = FacetValue( | ||
| 188 | + value=value_bucket['key'], | ||
| 189 | + label=str(value_bucket['key']), | ||
| 190 | + count=value_bucket['doc_count'], | ||
| 191 | + selected=False | ||
| 192 | + ) | ||
| 193 | + values.append(value) | ||
| 194 | + | ||
| 195 | + # 创建分面结果 | ||
| 196 | + facet = FacetResult( | ||
| 197 | + field=f"specifications.{name}", | ||
| 198 | + label=str(name), | ||
| 199 | + type="terms", | ||
| 200 | + values=values, | ||
| 201 | + total_count=filter_by_name_agg.get('doc_count', 0) | ||
| 202 | + ) | ||
| 203 | + facets.append(facet) | ||
| 204 | + continue | ||
| 205 | + | ||
| 177 | # Handle terms aggregation | 206 | # Handle terms aggregation |
| 178 | if 'buckets' in agg_data: | 207 | if 'buckets' in agg_data: |
| 179 | values = [] | 208 | values = [] |
docs/Search-API-Examples.md
| @@ -23,6 +23,7 @@ | @@ -23,6 +23,7 @@ | ||
| 23 | ```bash | 23 | ```bash |
| 24 | curl -X POST "http://localhost:6002/search/" \ | 24 | curl -X POST "http://localhost:6002/search/" \ |
| 25 | -H "Content-Type: application/json" \ | 25 | -H "Content-Type: application/json" \ |
| 26 | + -H "X-Tenant-ID: 2" \ | ||
| 26 | -d '{ | 27 | -d '{ |
| 27 | "query": "芭比娃娃" | 28 | "query": "芭比娃娃" |
| 28 | }' | 29 | }' |
| @@ -48,8 +49,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -48,8 +49,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 48 | ```bash | 49 | ```bash |
| 49 | curl -X POST "http://localhost:6002/search/" \ | 50 | curl -X POST "http://localhost:6002/search/" \ |
| 50 | -H "Content-Type: application/json" \ | 51 | -H "Content-Type: application/json" \ |
| 52 | + -H "X-Tenant-ID: 2" \ | ||
| 51 | -d '{ | 53 | -d '{ |
| 52 | - "query": "玩具", | 54 | + "query": "手机", |
| 55 | + "language": "zh", | ||
| 53 | "size": 50 | 56 | "size": 50 |
| 54 | }' | 57 | }' |
| 55 | ``` | 58 | ``` |
| @@ -60,8 +63,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -60,8 +63,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 60 | # 第1页(0-19) | 63 | # 第1页(0-19) |
| 61 | curl -X POST "http://localhost:6002/search/" \ | 64 | curl -X POST "http://localhost:6002/search/" \ |
| 62 | -H "Content-Type: application/json" \ | 65 | -H "Content-Type: application/json" \ |
| 66 | + -H "X-Tenant-ID: 2" \ | ||
| 63 | -d '{ | 67 | -d '{ |
| 64 | - "query": "玩具", | 68 | + "query": "手机", |
| 69 | + "language": "zh", | ||
| 65 | "size": 20, | 70 | "size": 20, |
| 66 | "from": 0 | 71 | "from": 0 |
| 67 | }' | 72 | }' |
| @@ -69,8 +74,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -69,8 +74,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 69 | # 第2页(20-39) | 74 | # 第2页(20-39) |
| 70 | curl -X POST "http://localhost:6002/search/" \ | 75 | curl -X POST "http://localhost:6002/search/" \ |
| 71 | -H "Content-Type: application/json" \ | 76 | -H "Content-Type: application/json" \ |
| 77 | + -H "X-Tenant-ID: 2" \ | ||
| 72 | -d '{ | 78 | -d '{ |
| 73 | - "query": "玩具", | 79 | + "query": "手机", |
| 80 | + "language": "zh", | ||
| 74 | "size": 20, | 81 | "size": 20, |
| 75 | "from": 20 | 82 | "from": 20 |
| 76 | }' | 83 | }' |
| @@ -87,10 +94,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -87,10 +94,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 87 | ```bash | 94 | ```bash |
| 88 | curl -X POST "http://localhost:6002/search/" \ | 95 | curl -X POST "http://localhost:6002/search/" \ |
| 89 | -H "Content-Type: application/json" \ | 96 | -H "Content-Type: application/json" \ |
| 97 | + -H "X-Tenant-ID: 2" \ | ||
| 90 | -d '{ | 98 | -d '{ |
| 91 | - "query": "玩具", | 99 | + "query": "手机", |
| 100 | + "language": "zh", | ||
| 92 | "filters": { | 101 | "filters": { |
| 93 | - "category.keyword": "玩具" | 102 | + "category_name": "手机" |
| 94 | } | 103 | } |
| 95 | }' | 104 | }' |
| 96 | ``` | 105 | ``` |
| @@ -100,10 +109,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -100,10 +109,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 100 | ```bash | 109 | ```bash |
| 101 | curl -X POST "http://localhost:6002/search/" \ | 110 | curl -X POST "http://localhost:6002/search/" \ |
| 102 | -H "Content-Type: application/json" \ | 111 | -H "Content-Type: application/json" \ |
| 112 | + -H "X-Tenant-ID: 2" \ | ||
| 103 | -d '{ | 113 | -d '{ |
| 104 | - "query": "娃娃", | 114 | + "query": "手机", |
| 115 | + "language": "zh", | ||
| 105 | "filters": { | 116 | "filters": { |
| 106 | - "category.keyword": ["玩具", "益智玩具", "儿童玩具"] | 117 | + "category_name": ["手机", "电子产品"] |
| 107 | } | 118 | } |
| 108 | }' | 119 | }' |
| 109 | ``` | 120 | ``` |
| @@ -115,16 +126,79 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -115,16 +126,79 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 115 | ```bash | 126 | ```bash |
| 116 | curl -X POST "http://localhost:6002/search/" \ | 127 | curl -X POST "http://localhost:6002/search/" \ |
| 117 | -H "Content-Type: application/json" \ | 128 | -H "Content-Type: application/json" \ |
| 129 | + -H "X-Tenant-ID: 2" \ | ||
| 118 | -d '{ | 130 | -d '{ |
| 119 | - "query": "娃娃", | 131 | + "query": "手机", |
| 132 | + "language": "zh", | ||
| 120 | "filters": { | 133 | "filters": { |
| 121 | - "category.keyword": "玩具", | ||
| 122 | - "vendor.keyword": "美泰" | 134 | + "category_name": "手机", |
| 135 | + "vendor_zh.keyword": "奇乐" | ||
| 123 | } | 136 | } |
| 124 | }' | 137 | }' |
| 125 | ``` | 138 | ``` |
| 126 | 139 | ||
| 127 | -说明:必须同时满足"类目=玩具" **并且** "品牌=美泰"。 | 140 | +说明:必须同时满足"类目=手机" **并且** "品牌=奇乐"。 |
| 141 | + | ||
| 142 | +#### 示例 4:Specifications 嵌套过滤(单个规格) | ||
| 143 | + | ||
| 144 | +```bash | ||
| 145 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 146 | + -H "Content-Type: application/json" \ | ||
| 147 | + -H "X-Tenant-ID: 2" \ | ||
| 148 | + -d '{ | ||
| 149 | + "query": "手机", | ||
| 150 | + "language": "zh", | ||
| 151 | + "filters": { | ||
| 152 | + "specifications": { | ||
| 153 | + "name": "color", | ||
| 154 | + "value": "white" | ||
| 155 | + } | ||
| 156 | + } | ||
| 157 | + }' | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +说明:查询规格名称为"color"且值为"white"的商品。 | ||
| 161 | + | ||
| 162 | +#### 示例 5:Specifications 嵌套过滤(多个规格,OR逻辑) | ||
| 163 | + | ||
| 164 | +```bash | ||
| 165 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 166 | + -H "Content-Type: application/json" \ | ||
| 167 | + -H "X-Tenant-ID: 2" \ | ||
| 168 | + -d '{ | ||
| 169 | + "query": "手机", | ||
| 170 | + "language": "zh", | ||
| 171 | + "filters": { | ||
| 172 | + "specifications": [ | ||
| 173 | + {"name": "color", "value": "white"}, | ||
| 174 | + {"name": "size", "value": "256GB"} | ||
| 175 | + ] | ||
| 176 | + } | ||
| 177 | + }' | ||
| 178 | +``` | ||
| 179 | + | ||
| 180 | +说明:查询满足任意一个规格的商品(color=white **或** size=256GB)。 | ||
| 181 | + | ||
| 182 | +#### 示例 6:组合过滤(包含 specifications) | ||
| 183 | + | ||
| 184 | +```bash | ||
| 185 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 186 | + -H "Content-Type: application/json" \ | ||
| 187 | + -H "X-Tenant-ID: 2" \ | ||
| 188 | + -d '{ | ||
| 189 | + "query": "手机", | ||
| 190 | + "language": "zh", | ||
| 191 | + "filters": { | ||
| 192 | + "category_name": "手机", | ||
| 193 | + "specifications": { | ||
| 194 | + "name": "color", | ||
| 195 | + "value": "white" | ||
| 196 | + } | ||
| 197 | + } | ||
| 198 | + }' | ||
| 199 | +``` | ||
| 200 | + | ||
| 201 | +说明:同时满足类目=手机 **并且** color=white。 | ||
| 128 | 202 | ||
| 129 | ### 范围过滤器 | 203 | ### 范围过滤器 |
| 130 | 204 | ||
| @@ -133,10 +207,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -133,10 +207,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 133 | ```bash | 207 | ```bash |
| 134 | curl -X POST "http://localhost:6002/search/" \ | 208 | curl -X POST "http://localhost:6002/search/" \ |
| 135 | -H "Content-Type: application/json" \ | 209 | -H "Content-Type: application/json" \ |
| 210 | + -H "X-Tenant-ID: 2" \ | ||
| 136 | -d '{ | 211 | -d '{ |
| 137 | - "query": "玩具", | 212 | + "query": "手机", |
| 213 | + "language": "zh", | ||
| 138 | "range_filters": { | 214 | "range_filters": { |
| 139 | - "price": { | 215 | + "min_price": { |
| 140 | "gte": 50, | 216 | "gte": 50, |
| 141 | "lte": 200 | 217 | "lte": 200 |
| 142 | } | 218 | } |
| @@ -151,10 +227,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -151,10 +227,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 151 | ```bash | 227 | ```bash |
| 152 | curl -X POST "http://localhost:6002/search/" \ | 228 | curl -X POST "http://localhost:6002/search/" \ |
| 153 | -H "Content-Type: application/json" \ | 229 | -H "Content-Type: application/json" \ |
| 230 | + -H "X-Tenant-ID: 2" \ | ||
| 154 | -d '{ | 231 | -d '{ |
| 155 | - "query": "玩具", | 232 | + "query": "手机", |
| 233 | + "language": "zh", | ||
| 156 | "range_filters": { | 234 | "range_filters": { |
| 157 | - "price": { | 235 | + "min_price": { |
| 158 | "gte": 100 | 236 | "gte": 100 |
| 159 | } | 237 | } |
| 160 | } | 238 | } |
| @@ -168,10 +246,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -168,10 +246,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 168 | ```bash | 246 | ```bash |
| 169 | curl -X POST "http://localhost:6002/search/" \ | 247 | curl -X POST "http://localhost:6002/search/" \ |
| 170 | -H "Content-Type: application/json" \ | 248 | -H "Content-Type: application/json" \ |
| 249 | + -H "X-Tenant-ID: 2" \ | ||
| 171 | -d '{ | 250 | -d '{ |
| 172 | - "query": "玩具", | 251 | + "query": "手机", |
| 252 | + "language": "zh", | ||
| 173 | "range_filters": { | 253 | "range_filters": { |
| 174 | - "price": { | 254 | + "min_price": { |
| 175 | "lt": 50 | 255 | "lt": 50 |
| 176 | } | 256 | } |
| 177 | } | 257 | } |
| @@ -185,10 +265,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -185,10 +265,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 185 | ```bash | 265 | ```bash |
| 186 | curl -X POST "http://localhost:6002/search/" \ | 266 | curl -X POST "http://localhost:6002/search/" \ |
| 187 | -H "Content-Type: application/json" \ | 267 | -H "Content-Type: application/json" \ |
| 268 | + -H "X-Tenant-ID: 2" \ | ||
| 188 | -d '{ | 269 | -d '{ |
| 189 | - "query": "玩具", | 270 | + "query": "手机", |
| 271 | + "language": "zh", | ||
| 190 | "range_filters": { | 272 | "range_filters": { |
| 191 | - "price": { | 273 | + "min_price": { |
| 192 | "gte": 50, | 274 | "gte": 50, |
| 193 | "lte": 200 | 275 | "lte": 200 |
| 194 | }, | 276 | }, |
| @@ -206,14 +288,16 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -206,14 +288,16 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 206 | ```bash | 288 | ```bash |
| 207 | curl -X POST "http://localhost:6002/search/" \ | 289 | curl -X POST "http://localhost:6002/search/" \ |
| 208 | -H "Content-Type: application/json" \ | 290 | -H "Content-Type: application/json" \ |
| 291 | + -H "X-Tenant-ID: 2" \ | ||
| 209 | -d '{ | 292 | -d '{ |
| 210 | - "query": "玩具", | 293 | + "query": "手机", |
| 294 | + "language": "zh", | ||
| 211 | "filters": { | 295 | "filters": { |
| 212 | - "category.keyword": ["玩具", "益智玩具"], | ||
| 213 | - "vendor.keyword": "乐高" | 296 | + "category_name": ["手机", "电子产品"], |
| 297 | + "vendor_zh.keyword": "品牌A" | ||
| 214 | }, | 298 | }, |
| 215 | "range_filters": { | 299 | "range_filters": { |
| 216 | - "price": { | 300 | + "min_price": { |
| 217 | "gte": 50, | 301 | "gte": 50, |
| 218 | "lte": 500 | 302 | "lte": 500 |
| 219 | } | 303 | } |
| @@ -234,41 +318,82 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -234,41 +318,82 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 234 | ```bash | 318 | ```bash |
| 235 | curl -X POST "http://localhost:6002/search/" \ | 319 | curl -X POST "http://localhost:6002/search/" \ |
| 236 | -H "Content-Type: application/json" \ | 320 | -H "Content-Type: application/json" \ |
| 321 | + -H "X-Tenant-ID: 2" \ | ||
| 237 | -d '{ | 322 | -d '{ |
| 238 | - "query": "玩具", | 323 | + "query": "手机", |
| 324 | + "language": "zh", | ||
| 239 | "size": 20, | 325 | "size": 20, |
| 240 | - "facets": ["category.keyword", "vendor.keyword"] | 326 | + "facets": ["category1_name", "category2_name", "specifications"] |
| 241 | }' | 327 | }' |
| 242 | ``` | 328 | ``` |
| 243 | 329 | ||
| 244 | **响应**: | 330 | **响应**: |
| 245 | ```json | 331 | ```json |
| 246 | { | 332 | { |
| 247 | - "hits": [...], | 333 | + "results": [...], |
| 248 | "total": 118, | 334 | "total": 118, |
| 249 | "facets": [ | 335 | "facets": [ |
| 250 | { | 336 | { |
| 251 | - "field": "category.keyword", | ||
| 252 | - "label": "category.keyword", | 337 | + "field": "category1_name", |
| 338 | + "label": "category1_name", | ||
| 253 | "type": "terms", | 339 | "type": "terms", |
| 254 | "values": [ | 340 | "values": [ |
| 255 | - {"value": "玩具", "count": 85, "selected": false}, | ||
| 256 | - {"value": "益智玩具", "count": 33, "selected": false} | 341 | + {"value": "手机", "count": 85, "selected": false}, |
| 342 | + {"value": "电子产品", "count": 33, "selected": false} | ||
| 257 | ] | 343 | ] |
| 258 | }, | 344 | }, |
| 259 | { | 345 | { |
| 260 | - "field": "vendor.keyword", | ||
| 261 | - "label": "vendor.keyword", | 346 | + "field": "specifications.color", |
| 347 | + "label": "color", | ||
| 262 | "type": "terms", | 348 | "type": "terms", |
| 263 | "values": [ | 349 | "values": [ |
| 264 | - {"value": "乐高", "count": 42, "selected": false}, | ||
| 265 | - {"value": "美泰", "count": 28, "selected": false} | 350 | + {"value": "white", "count": 50, "selected": false}, |
| 351 | + {"value": "black", "count": 30, "selected": false} | ||
| 352 | + ] | ||
| 353 | + }, | ||
| 354 | + { | ||
| 355 | + "field": "specifications.size", | ||
| 356 | + "label": "size", | ||
| 357 | + "type": "terms", | ||
| 358 | + "values": [ | ||
| 359 | + {"value": "256GB", "count": 40, "selected": false}, | ||
| 360 | + {"value": "512GB", "count": 20, "selected": false} | ||
| 266 | ] | 361 | ] |
| 267 | } | 362 | } |
| 268 | ] | 363 | ] |
| 269 | } | 364 | } |
| 270 | ``` | 365 | ``` |
| 271 | 366 | ||
| 367 | +#### 示例 2:Specifications 分面(所有规格名称) | ||
| 368 | + | ||
| 369 | +```bash | ||
| 370 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 371 | + -H "Content-Type: application/json" \ | ||
| 372 | + -H "X-Tenant-ID: 2" \ | ||
| 373 | + -d '{ | ||
| 374 | + "query": "手机", | ||
| 375 | + "language": "zh", | ||
| 376 | + "facets": ["specifications"] | ||
| 377 | + }' | ||
| 378 | +``` | ||
| 379 | + | ||
| 380 | +说明:返回所有规格名称(name)及其对应的值(value)列表。 | ||
| 381 | + | ||
| 382 | +#### 示例 3:Specifications 分面(指定规格名称) | ||
| 383 | + | ||
| 384 | +```bash | ||
| 385 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 386 | + -H "Content-Type: application/json" \ | ||
| 387 | + -H "X-Tenant-ID: 2" \ | ||
| 388 | + -d '{ | ||
| 389 | + "query": "手机", | ||
| 390 | + "language": "zh", | ||
| 391 | + "facets": ["specifications.color", "specifications.size"] | ||
| 392 | + }' | ||
| 393 | +``` | ||
| 394 | + | ||
| 395 | +说明:只返回指定规格名称的值列表。 | ||
| 396 | + | ||
| 272 | ### 高级模式 | 397 | ### 高级模式 |
| 273 | 398 | ||
| 274 | #### 示例 1:自定义分面大小 | 399 | #### 示例 1:自定义分面大小 |
| @@ -276,16 +401,18 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -276,16 +401,18 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 276 | ```bash | 401 | ```bash |
| 277 | curl -X POST "http://localhost:6002/search/" \ | 402 | curl -X POST "http://localhost:6002/search/" \ |
| 278 | -H "Content-Type: application/json" \ | 403 | -H "Content-Type: application/json" \ |
| 404 | + -H "X-Tenant-ID: 2" \ | ||
| 279 | -d '{ | 405 | -d '{ |
| 280 | - "query": "玩具", | 406 | + "query": "手机", |
| 407 | + "language": "zh", | ||
| 281 | "facets": [ | 408 | "facets": [ |
| 282 | { | 409 | { |
| 283 | - "field": "category.keyword", | 410 | + "field": "category1_name", |
| 284 | "size": 20, | 411 | "size": 20, |
| 285 | "type": "terms" | 412 | "type": "terms" |
| 286 | }, | 413 | }, |
| 287 | { | 414 | { |
| 288 | - "field": "vendor.keyword", | 415 | + "field": "category2_name", |
| 289 | "size": 30, | 416 | "size": 30, |
| 290 | "type": "terms" | 417 | "type": "terms" |
| 291 | } | 418 | } |
| @@ -298,8 +425,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -298,8 +425,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 298 | ```bash | 425 | ```bash |
| 299 | curl -X POST "http://localhost:6002/search/" \ | 426 | curl -X POST "http://localhost:6002/search/" \ |
| 300 | -H "Content-Type: application/json" \ | 427 | -H "Content-Type: application/json" \ |
| 428 | + -H "X-Tenant-ID: 2" \ | ||
| 301 | -d '{ | 429 | -d '{ |
| 302 | - "query": "玩具", | 430 | + "query": "手机", |
| 431 | + "language": "zh", | ||
| 303 | "facets": [ | 432 | "facets": [ |
| 304 | { | 433 | { |
| 305 | "field": "price", | 434 | "field": "price", |
| @@ -339,8 +468,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -339,8 +468,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 339 | ```bash | 468 | ```bash |
| 340 | curl -X POST "http://localhost:6002/search/" \ | 469 | curl -X POST "http://localhost:6002/search/" \ |
| 341 | -H "Content-Type: application/json" \ | 470 | -H "Content-Type: application/json" \ |
| 471 | + -H "X-Tenant-ID: 2" \ | ||
| 342 | -d '{ | 472 | -d '{ |
| 343 | - "query": "玩具", | 473 | + "query": "手机", |
| 474 | + "language": "zh", | ||
| 344 | "facets": [ | 475 | "facets": [ |
| 345 | {"field": "category.keyword", "size": 15}, | 476 | {"field": "category.keyword", "size": 15}, |
| 346 | {"field": "vendor.keyword", "size": 15}, | 477 | {"field": "vendor.keyword", "size": 15}, |
| @@ -366,8 +497,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -366,8 +497,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 366 | ```bash | 497 | ```bash |
| 367 | curl -X POST "http://localhost:6002/search/" \ | 498 | curl -X POST "http://localhost:6002/search/" \ |
| 368 | -H "Content-Type: application/json" \ | 499 | -H "Content-Type: application/json" \ |
| 500 | + -H "X-Tenant-ID: 2" \ | ||
| 369 | -d '{ | 501 | -d '{ |
| 370 | - "query": "玩具", | 502 | + "query": "手机", |
| 503 | + "language": "zh", | ||
| 371 | "size": 20, | 504 | "size": 20, |
| 372 | "sort_by": "min_price", | 505 | "sort_by": "min_price", |
| 373 | "sort_order": "asc" | 506 | "sort_order": "asc" |
| @@ -379,8 +512,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -379,8 +512,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 379 | ```bash | 512 | ```bash |
| 380 | curl -X POST "http://localhost:6002/search/" \ | 513 | curl -X POST "http://localhost:6002/search/" \ |
| 381 | -H "Content-Type: application/json" \ | 514 | -H "Content-Type: application/json" \ |
| 515 | + -H "X-Tenant-ID: 2" \ | ||
| 382 | -d '{ | 516 | -d '{ |
| 383 | - "query": "玩具", | 517 | + "query": "手机", |
| 518 | + "language": "zh", | ||
| 384 | "size": 20, | 519 | "size": 20, |
| 385 | "sort_by": "create_time", | 520 | "sort_by": "create_time", |
| 386 | "sort_order": "desc" | 521 | "sort_order": "desc" |
| @@ -392,8 +527,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -392,8 +527,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 392 | ```bash | 527 | ```bash |
| 393 | curl -X POST "http://localhost:6002/search/" \ | 528 | curl -X POST "http://localhost:6002/search/" \ |
| 394 | -H "Content-Type: application/json" \ | 529 | -H "Content-Type: application/json" \ |
| 530 | + -H "X-Tenant-ID: 2" \ | ||
| 395 | -d '{ | 531 | -d '{ |
| 396 | - "query": "玩具", | 532 | + "query": "手机", |
| 533 | + "language": "zh", | ||
| 397 | "filters": { | 534 | "filters": { |
| 398 | "category.keyword": "益智玩具" | 535 | "category.keyword": "益智玩具" |
| 399 | }, | 536 | }, |
| @@ -411,6 +548,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -411,6 +548,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 411 | ```bash | 548 | ```bash |
| 412 | curl -X POST "http://localhost:6002/search/image" \ | 549 | curl -X POST "http://localhost:6002/search/image" \ |
| 413 | -H "Content-Type: application/json" \ | 550 | -H "Content-Type: application/json" \ |
| 551 | + -H "X-Tenant-ID: 2" \ | ||
| 414 | -d '{ | 552 | -d '{ |
| 415 | "image_url": "https://example.com/barbie.jpg", | 553 | "image_url": "https://example.com/barbie.jpg", |
| 416 | "size": 20 | 554 | "size": 20 |
| @@ -422,14 +560,15 @@ curl -X POST "http://localhost:6002/search/image" \ | @@ -422,14 +560,15 @@ curl -X POST "http://localhost:6002/search/image" \ | ||
| 422 | ```bash | 560 | ```bash |
| 423 | curl -X POST "http://localhost:6002/search/image" \ | 561 | curl -X POST "http://localhost:6002/search/image" \ |
| 424 | -H "Content-Type: application/json" \ | 562 | -H "Content-Type: application/json" \ |
| 563 | + -H "X-Tenant-ID: 2" \ | ||
| 425 | -d '{ | 564 | -d '{ |
| 426 | "image_url": "https://example.com/barbie.jpg", | 565 | "image_url": "https://example.com/barbie.jpg", |
| 427 | "size": 20, | 566 | "size": 20, |
| 428 | "filters": { | 567 | "filters": { |
| 429 | - "category.keyword": "玩具" | 568 | + "category_name": "手机" |
| 430 | }, | 569 | }, |
| 431 | "range_filters": { | 570 | "range_filters": { |
| 432 | - "price": { | 571 | + "min_price": { |
| 433 | "lte": 100 | 572 | "lte": 100 |
| 434 | } | 573 | } |
| 435 | } | 574 | } |
| @@ -445,6 +584,7 @@ curl -X POST "http://localhost:6002/search/image" \ | @@ -445,6 +584,7 @@ curl -X POST "http://localhost:6002/search/image" \ | ||
| 445 | ```bash | 584 | ```bash |
| 446 | curl -X POST "http://localhost:6002/search/" \ | 585 | curl -X POST "http://localhost:6002/search/" \ |
| 447 | -H "Content-Type: application/json" \ | 586 | -H "Content-Type: application/json" \ |
| 587 | + -H "X-Tenant-ID: 2" \ | ||
| 448 | -d '{ | 588 | -d '{ |
| 449 | "query": "玩具 AND 乐高" | 589 | "query": "玩具 AND 乐高" |
| 450 | }' | 590 | }' |
| @@ -457,6 +597,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -457,6 +597,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 457 | ```bash | 597 | ```bash |
| 458 | curl -X POST "http://localhost:6002/search/" \ | 598 | curl -X POST "http://localhost:6002/search/" \ |
| 459 | -H "Content-Type: application/json" \ | 599 | -H "Content-Type: application/json" \ |
| 600 | + -H "X-Tenant-ID: 2" \ | ||
| 460 | -d '{ | 601 | -d '{ |
| 461 | "query": "芭比 OR 娃娃" | 602 | "query": "芭比 OR 娃娃" |
| 462 | }' | 603 | }' |
| @@ -469,6 +610,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -469,6 +610,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 469 | ```bash | 610 | ```bash |
| 470 | curl -X POST "http://localhost:6002/search/" \ | 611 | curl -X POST "http://localhost:6002/search/" \ |
| 471 | -H "Content-Type: application/json" \ | 612 | -H "Content-Type: application/json" \ |
| 613 | + -H "X-Tenant-ID: 2" \ | ||
| 472 | -d '{ | 614 | -d '{ |
| 473 | "query": "玩具 ANDNOT 电动" | 615 | "query": "玩具 ANDNOT 电动" |
| 474 | }' | 616 | }' |
| @@ -481,6 +623,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -481,6 +623,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 481 | ```bash | 623 | ```bash |
| 482 | curl -X POST "http://localhost:6002/search/" \ | 624 | curl -X POST "http://localhost:6002/search/" \ |
| 483 | -H "Content-Type: application/json" \ | 625 | -H "Content-Type: application/json" \ |
| 626 | + -H "X-Tenant-ID: 2" \ | ||
| 484 | -d '{ | 627 | -d '{ |
| 485 | "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" | 628 | "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" |
| 486 | }' | 629 | }' |
| @@ -493,6 +636,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -493,6 +636,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 493 | ```bash | 636 | ```bash |
| 494 | curl -X POST "http://localhost:6002/search/" \ | 637 | curl -X POST "http://localhost:6002/search/" \ |
| 495 | -H "Content-Type: application/json" \ | 638 | -H "Content-Type: application/json" \ |
| 639 | + -H "X-Tenant-ID: 2" \ | ||
| 496 | -d '{ | 640 | -d '{ |
| 497 | "query": "brand:乐高" | 641 | "query": "brand:乐高" |
| 498 | }' | 642 | }' |
| @@ -557,19 +701,23 @@ for hit in result['hits'][:3]: | @@ -557,19 +701,23 @@ for hit in result['hits'][:3]: | ||
| 557 | 701 | ||
| 558 | # 示例 2:带过滤和分面的搜索 | 702 | # 示例 2:带过滤和分面的搜索 |
| 559 | result = search_products( | 703 | result = search_products( |
| 560 | - query="玩具", | 704 | + query="手机", |
| 561 | size=20, | 705 | size=20, |
| 706 | + language="zh", | ||
| 562 | filters={ | 707 | filters={ |
| 563 | - "category.keyword": ["玩具", "益智玩具"] | 708 | + "category_name": "手机", |
| 709 | + "specifications": {"name": "color", "value": "white"} | ||
| 564 | }, | 710 | }, |
| 565 | range_filters={ | 711 | range_filters={ |
| 566 | - "price": {"gte": 50, "lte": 200} | 712 | + "min_price": {"gte": 50, "lte": 200} |
| 567 | }, | 713 | }, |
| 568 | facets=[ | 714 | facets=[ |
| 569 | - {"field": "vendor.keyword", "size": 15}, | ||
| 570 | - {"field": "category.keyword", "size": 15}, | 715 | + {"field": "category1_name", "size": 15}, |
| 716 | + {"field": "category2_name", "size": 15}, | ||
| 717 | + "specifications.color", | ||
| 718 | + "specifications.size", | ||
| 571 | { | 719 | { |
| 572 | - "field": "price", | 720 | + "field": "min_price", |
| 573 | "type": "range", | 721 | "type": "range", |
| 574 | "ranges": [ | 722 | "ranges": [ |
| 575 | {"key": "0-50", "to": 50}, | 723 | {"key": "0-50", "to": 50}, |
| @@ -692,21 +840,24 @@ const result1 = await client.search({ | @@ -692,21 +840,24 @@ const result1 = await client.search({ | ||
| 692 | }); | 840 | }); |
| 693 | console.log(`找到 ${result1.total} 个结果`); | 841 | console.log(`找到 ${result1.total} 个结果`); |
| 694 | 842 | ||
| 695 | -// 带过滤和分面的搜索 | 843 | +// 带过滤和分面的搜索(包含规格) |
| 696 | const result2 = await client.search({ | 844 | const result2 = await client.search({ |
| 697 | - query: "玩具", | 845 | + query: "手机", |
| 846 | + language: "zh", | ||
| 698 | size: 20, | 847 | size: 20, |
| 699 | filters: { | 848 | filters: { |
| 700 | - category.keyword: ["玩具", "益智玩具"] | 849 | + category_name: "手机", |
| 850 | + specifications: { name: "color", value: "white" } | ||
| 701 | }, | 851 | }, |
| 702 | rangeFilters: { | 852 | rangeFilters: { |
| 703 | - price: { gte: 50, lte: 200 } | 853 | + min_price: { gte: 50, lte: 200 } |
| 704 | }, | 854 | }, |
| 705 | facets: [ | 855 | facets: [ |
| 706 | - { field: "vendor.keyword", size: 15 }, | ||
| 707 | - { field: "category.keyword", size: 15 } | 856 | + "category1_name", |
| 857 | + "specifications.color", | ||
| 858 | + "specifications.size" | ||
| 708 | ], | 859 | ], |
| 709 | - sortBy: "price", | 860 | + sortBy: "min_price", |
| 710 | sortOrder: "asc" | 861 | sortOrder: "asc" |
| 711 | }); | 862 | }); |
| 712 | 863 | ||
| @@ -810,8 +961,10 @@ const SearchComponent = { | @@ -810,8 +961,10 @@ const SearchComponent = { | ||
| 810 | ```bash | 961 | ```bash |
| 811 | curl -X POST "http://localhost:6002/search/" \ | 962 | curl -X POST "http://localhost:6002/search/" \ |
| 812 | -H "Content-Type: application/json" \ | 963 | -H "Content-Type: application/json" \ |
| 964 | + -H "X-Tenant-ID: 2" \ | ||
| 813 | -d '{ | 965 | -d '{ |
| 814 | - "query": "玩具", | 966 | + "query": "手机", |
| 967 | + "language": "zh", | ||
| 815 | "debug": true | 968 | "debug": true |
| 816 | }' | 969 | }' |
| 817 | ``` | 970 | ``` |
| @@ -847,8 +1000,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -847,8 +1000,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 847 | ```bash | 1000 | ```bash |
| 848 | curl -X POST "http://localhost:6002/search/" \ | 1001 | curl -X POST "http://localhost:6002/search/" \ |
| 849 | -H "Content-Type: application/json" \ | 1002 | -H "Content-Type: application/json" \ |
| 1003 | + -H "X-Tenant-ID: 2" \ | ||
| 850 | -d '{ | 1004 | -d '{ |
| 851 | - "query": "玩具", | 1005 | + "query": "手机", |
| 1006 | + "language": "zh", | ||
| 852 | "min_score": 5.0 | 1007 | "min_score": 5.0 |
| 853 | }' | 1008 | }' |
| 854 | ``` | 1009 | ``` |
| @@ -865,10 +1020,11 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -865,10 +1020,11 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 865 | # 显示某个类目下的所有商品,按价格排序,提供品牌筛选 | 1020 | # 显示某个类目下的所有商品,按价格排序,提供品牌筛选 |
| 866 | curl -X POST "http://localhost:6002/search/" \ | 1021 | curl -X POST "http://localhost:6002/search/" \ |
| 867 | -H "Content-Type: application/json" \ | 1022 | -H "Content-Type: application/json" \ |
| 1023 | + -H "X-Tenant-ID: 2" \ | ||
| 868 | -d '{ | 1024 | -d '{ |
| 869 | "query": "*", | 1025 | "query": "*", |
| 870 | "filters": { | 1026 | "filters": { |
| 871 | - "category.keyword": "玩具" | 1027 | + "category_name": "手机" |
| 872 | }, | 1028 | }, |
| 873 | "facets": [ | 1029 | "facets": [ |
| 874 | {"field": "vendor.keyword", "size": 20}, | 1030 | {"field": "vendor.keyword", "size": 20}, |
| @@ -892,19 +1048,53 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -892,19 +1048,53 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 892 | ### 场景 2:搜索结果页 | 1048 | ### 场景 2:搜索结果页 |
| 893 | 1049 | ||
| 894 | ```bash | 1050 | ```bash |
| 895 | -# 用户搜索关键词,提供筛选和排序 | 1051 | +# 用户搜索关键词,提供筛选和排序(包含规格分面) |
| 1052 | +curl -X POST "http://localhost:6002/search/" \ | ||
| 1053 | + -H "Content-Type: application/json" \ | ||
| 1054 | + -H "X-Tenant-ID: 2" \ | ||
| 1055 | + -d '{ | ||
| 1056 | + "query": "手机", | ||
| 1057 | + "language": "zh", | ||
| 1058 | + "facets": [ | ||
| 1059 | + {"field": "category1_name", "size": 10}, | ||
| 1060 | + {"field": "category2_name", "size": 10}, | ||
| 1061 | + "specifications.color", | ||
| 1062 | + "specifications.size", | ||
| 1063 | + { | ||
| 1064 | + "field": "min_price", | ||
| 1065 | + "type": "range", | ||
| 1066 | + "ranges": [ | ||
| 1067 | + {"key": "0-50", "to": 50}, | ||
| 1068 | + {"key": "50-100", "from": 50, "to": 100}, | ||
| 1069 | + {"key": "100+", "from": 100} | ||
| 1070 | + ] | ||
| 1071 | + } | ||
| 1072 | + ], | ||
| 1073 | + "size": 20 | ||
| 1074 | + }' | ||
| 1075 | +``` | ||
| 1076 | + | ||
| 1077 | +### 场景 2.1:带规格过滤的搜索结果页 | ||
| 1078 | + | ||
| 1079 | +```bash | ||
| 1080 | +# 用户搜索并选择了规格筛选条件 | ||
| 896 | curl -X POST "http://localhost:6002/search/" \ | 1081 | curl -X POST "http://localhost:6002/search/" \ |
| 897 | -H "Content-Type: application/json" \ | 1082 | -H "Content-Type: application/json" \ |
| 1083 | + -H "X-Tenant-ID: 2" \ | ||
| 898 | -d '{ | 1084 | -d '{ |
| 899 | - "query": "芭比娃娃", | 1085 | + "query": "手机", |
| 1086 | + "language": "zh", | ||
| 1087 | + "filters": { | ||
| 1088 | + "category_name": "手机", | ||
| 1089 | + "specifications": { | ||
| 1090 | + "name": "color", | ||
| 1091 | + "value": "white" | ||
| 1092 | + } | ||
| 1093 | + }, | ||
| 900 | "facets": [ | 1094 | "facets": [ |
| 901 | - {"field": "category.keyword", "size": 10}, | ||
| 902 | - {"field": "vendor.keyword", "size": 10}, | ||
| 903 | - {"field": "price", "type": "range", "ranges": [ | ||
| 904 | - {"key": "0-50", "to": 50}, | ||
| 905 | - {"key": "50-100", "from": 50, "to": 100}, | ||
| 906 | - {"key": "100+", "from": 100} | ||
| 907 | - ]} | 1095 | + "category1_name", |
| 1096 | + "specifications.color", | ||
| 1097 | + "specifications.size" | ||
| 908 | ], | 1098 | ], |
| 909 | "size": 20 | 1099 | "size": 20 |
| 910 | }' | 1100 | }' |
| @@ -916,15 +1106,16 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -916,15 +1106,16 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 916 | # 显示特定价格区间的商品 | 1106 | # 显示特定价格区间的商品 |
| 917 | curl -X POST "http://localhost:6002/search/" \ | 1107 | curl -X POST "http://localhost:6002/search/" \ |
| 918 | -H "Content-Type: application/json" \ | 1108 | -H "Content-Type: application/json" \ |
| 1109 | + -H "X-Tenant-ID: 2" \ | ||
| 919 | -d '{ | 1110 | -d '{ |
| 920 | "query": "*", | 1111 | "query": "*", |
| 921 | "range_filters": { | 1112 | "range_filters": { |
| 922 | - "price": { | 1113 | + "min_price": { |
| 923 | "gte": 50, | 1114 | "gte": 50, |
| 924 | "lte": 100 | 1115 | "lte": 100 |
| 925 | } | 1116 | } |
| 926 | }, | 1117 | }, |
| 927 | - "facets": ["category.keyword", "vendor.keyword"], | 1118 | + "facets": ["category1_name", "category2_name", "specifications"], |
| 928 | "sort_by": "min_price", | 1119 | "sort_by": "min_price", |
| 929 | "sort_order": "asc", | 1120 | "sort_order": "asc", |
| 930 | "size": 50 | 1121 | "size": 50 |
| @@ -937,6 +1128,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -937,6 +1128,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 937 | # 最近更新的商品 | 1128 | # 最近更新的商品 |
| 938 | curl -X POST "http://localhost:6002/search/" \ | 1129 | curl -X POST "http://localhost:6002/search/" \ |
| 939 | -H "Content-Type: application/json" \ | 1130 | -H "Content-Type: application/json" \ |
| 1131 | + -H "X-Tenant-ID: 2" \ | ||
| 940 | -d '{ | 1132 | -d '{ |
| 941 | "query": "*", | 1133 | "query": "*", |
| 942 | "range_filters": { | 1134 | "range_filters": { |
| @@ -960,10 +1152,12 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -960,10 +1152,12 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 960 | # 错误:range_filters 缺少操作符 | 1152 | # 错误:range_filters 缺少操作符 |
| 961 | curl -X POST "http://localhost:6002/search/" \ | 1153 | curl -X POST "http://localhost:6002/search/" \ |
| 962 | -H "Content-Type: application/json" \ | 1154 | -H "Content-Type: application/json" \ |
| 1155 | + -H "X-Tenant-ID: 2" \ | ||
| 963 | -d '{ | 1156 | -d '{ |
| 964 | - "query": "玩具", | 1157 | + "query": "手机", |
| 1158 | + "language": "zh", | ||
| 965 | "range_filters": { | 1159 | "range_filters": { |
| 966 | - "price": {} | 1160 | + "min_price": {} |
| 967 | } | 1161 | } |
| 968 | }' | 1162 | }' |
| 969 | ``` | 1163 | ``` |
| @@ -983,6 +1177,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -983,6 +1177,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 983 | # 错误:query 为空 | 1177 | # 错误:query 为空 |
| 984 | curl -X POST "http://localhost:6002/search/" \ | 1178 | curl -X POST "http://localhost:6002/search/" \ |
| 985 | -H "Content-Type: application/json" \ | 1179 | -H "Content-Type: application/json" \ |
| 1180 | + -H "X-Tenant-ID: 2" \ | ||
| 986 | -d '{ | 1181 | -d '{ |
| 987 | "query": "" | 1182 | "query": "" |
| 988 | }' | 1183 | }' |
| @@ -1060,6 +1255,7 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -1060,6 +1255,7 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 1060 | # 使用通配符查询 + 分面 | 1255 | # 使用通配符查询 + 分面 |
| 1061 | curl -X POST "http://localhost:6002/search/" \ | 1256 | curl -X POST "http://localhost:6002/search/" \ |
| 1062 | -H "Content-Type: application/json" \ | 1257 | -H "Content-Type: application/json" \ |
| 1258 | + -H "X-Tenant-ID: 2" \ | ||
| 1063 | -d '{ | 1259 | -d '{ |
| 1064 | "query": "*", | 1260 | "query": "*", |
| 1065 | "size": 0, | 1261 | "size": 0, |
| @@ -1074,8 +1270,10 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -1074,8 +1270,10 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 1074 | ```bash | 1270 | ```bash |
| 1075 | curl -X POST "http://localhost:6002/search/" \ | 1271 | curl -X POST "http://localhost:6002/search/" \ |
| 1076 | -H "Content-Type: application/json" \ | 1272 | -H "Content-Type: application/json" \ |
| 1273 | + -H "X-Tenant-ID: 2" \ | ||
| 1077 | -d '{ | 1274 | -d '{ |
| 1078 | - "query": "玩具", | 1275 | + "query": "手机", |
| 1276 | + "language": "zh", | ||
| 1079 | "size": 0, | 1277 | "size": 0, |
| 1080 | "facets": [ | 1278 | "facets": [ |
| 1081 | { | 1279 | { |
| @@ -1099,13 +1297,14 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -1099,13 +1297,14 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 1099 | # 布尔表达式 + 过滤器 + 分面 + 排序 | 1297 | # 布尔表达式 + 过滤器 + 分面 + 排序 |
| 1100 | curl -X POST "http://localhost:6002/search/" \ | 1298 | curl -X POST "http://localhost:6002/search/" \ |
| 1101 | -H "Content-Type: application/json" \ | 1299 | -H "Content-Type: application/json" \ |
| 1300 | + -H "X-Tenant-ID: 2" \ | ||
| 1102 | -d '{ | 1301 | -d '{ |
| 1103 | "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", | 1302 | "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", |
| 1104 | "filters": { | 1303 | "filters": { |
| 1105 | "category.keyword": ["玩具", "益智玩具"] | 1304 | "category.keyword": ["玩具", "益智玩具"] |
| 1106 | }, | 1305 | }, |
| 1107 | "range_filters": { | 1306 | "range_filters": { |
| 1108 | - "price": {"gte": 20, "lte": 100}, | 1307 | + "min_price": {"gte": 20, "lte": 100}, |
| 1109 | "days_since_last_update": {"lte": 30} | 1308 | "days_since_last_update": {"lte": 30} |
| 1110 | }, | 1309 | }, |
| 1111 | "facets": [ | 1310 | "facets": [ |
| @@ -1127,16 +1326,19 @@ curl -X POST "http://localhost:6002/search/" \ | @@ -1127,16 +1326,19 @@ curl -X POST "http://localhost:6002/search/" \ | ||
| 1127 | # 测试类目:玩具 | 1326 | # 测试类目:玩具 |
| 1128 | curl -X POST "http://localhost:6002/search/" \ | 1327 | curl -X POST "http://localhost:6002/search/" \ |
| 1129 | -H "Content-Type: application/json" \ | 1328 | -H "Content-Type: application/json" \ |
| 1329 | + -H "X-Tenant-ID: 2" \ | ||
| 1130 | -d '{"query": "玩具", "size": 5}' | 1330 | -d '{"query": "玩具", "size": 5}' |
| 1131 | 1331 | ||
| 1132 | # 测试品牌:乐高 | 1332 | # 测试品牌:乐高 |
| 1133 | curl -X POST "http://localhost:6002/search/" \ | 1333 | curl -X POST "http://localhost:6002/search/" \ |
| 1134 | -H "Content-Type: application/json" \ | 1334 | -H "Content-Type: application/json" \ |
| 1335 | + -H "X-Tenant-ID: 2" \ | ||
| 1135 | -d '{"query": "brand:乐高", "size": 5}' | 1336 | -d '{"query": "brand:乐高", "size": 5}' |
| 1136 | 1337 | ||
| 1137 | # 测试布尔表达式 | 1338 | # 测试布尔表达式 |
| 1138 | curl -X POST "http://localhost:6002/search/" \ | 1339 | curl -X POST "http://localhost:6002/search/" \ |
| 1139 | -H "Content-Type: application/json" \ | 1340 | -H "Content-Type: application/json" \ |
| 1341 | + -H "X-Tenant-ID: 2" \ | ||
| 1140 | -d '{"query": "玩具 AND 乐高", "size": 5}' | 1342 | -d '{"query": "玩具 AND 乐高", "size": 5}' |
| 1141 | ``` | 1343 | ``` |
| 1142 | 1344 |
docs/基础配置指南.md
| 1 | -# Base Configuration Guide | ||
| 2 | - | ||
| 3 | -店匠通用配置(Base Configuration)使用指南 | 1 | +# 基础配置指南 |
| 4 | 2 | ||
| 5 | ## 概述 | 3 | ## 概述 |
| 6 | 4 | ||
| 7 | -Base配置是店匠(Shoplazza)通用配置,适用于所有使用店匠标准表的客户。该配置采用SPU级别的索引结构,所有客户共享同一个Elasticsearch索引(`search_products`),通过`tenant_id`字段实现数据隔离。 | 5 | +搜索引擎采用**统一硬编码配置**方案,所有租户共享相同的索引结构和查询配置,无需单独配置。 |
| 8 | 6 | ||
| 9 | ## 核心特性 | 7 | ## 核心特性 |
| 10 | 8 | ||
| 11 | -- **SPU级别索引**:每个ES文档代表一个SPU,包含嵌套的skus数组 | ||
| 12 | -- **统一索引**:所有客户共享`search_products`索引 | ||
| 13 | -- **租户隔离**:通过`tenant_id`字段实现数据隔离 | ||
| 14 | -- **配置简化**:配置只包含ES搜索相关配置,不包含MySQL数据源配置 | ||
| 15 | -- **外部友好格式**:API返回格式不包含ES内部字段(`_id`, `_score`, `_source`) | 9 | +- **统一索引结构**: 所有租户共享 `search_products` 索引 |
| 10 | +- **硬编码配置**: 索引 mapping 和查询配置直接硬编码在代码中,无需配置文件 | ||
| 11 | +- **SPU级别索引**: 每个ES文档代表一个SPU,包含嵌套的 `skus` 和 `specifications` 数组 | ||
| 12 | +- **租户隔离**: 通过 `tenant_id` 字段实现数据隔离 | ||
| 13 | +- **多语言支持**: 文本字段支持中英文双语,后端根据 `language` 参数自动选择 | ||
| 16 | 14 | ||
| 17 | -## 配置说明 | 15 | +## 索引结构 |
| 18 | 16 | ||
| 19 | -### 配置文件位置 | 17 | +### Mapping 文件位置 |
| 20 | 18 | ||
| 21 | -`config/schema/base/config.yaml` | 19 | +`mappings/search_products.json` |
| 22 | 20 | ||
| 23 | -### 配置内容 | 21 | +### 主要字段 |
| 24 | 22 | ||
| 25 | -Base配置**不包含**以下内容: | ||
| 26 | -- `mysql_config` - MySQL数据库配置 | ||
| 27 | -- `main_table` - 主表配置 | ||
| 28 | -- `extension_table` - 扩展表配置 | ||
| 29 | -- `source_table` / `source_column` - 字段数据源映射 | 23 | +#### 基础标识 |
| 24 | +- `tenant_id` (keyword) - 租户ID(必需,用于隔离) | ||
| 25 | +- `spu_id` (keyword) - SPU ID | ||
| 26 | +- `create_time`, `update_time` (date) - 时间字段 | ||
| 30 | 27 | ||
| 31 | -Base配置**只包含**: | ||
| 32 | -- ES字段定义(字段类型、分析器、boost等) | ||
| 33 | -- 查询域(indexes)配置 | ||
| 34 | -- 查询处理配置(query_config) | ||
| 35 | -- 排序和打分配置(function_score) | ||
| 36 | -- SPU配置(spu_config) | 28 | +#### 多语言文本字段 |
| 29 | +- `title_zh`, `title_en` (text) - 标题(中英文) | ||
| 30 | +- `brief_zh`, `brief_en` (text) - 短描述(中英文) | ||
| 31 | +- `description_zh`, `description_en` (text) - 详细描述(中英文) | ||
| 32 | +- `vendor_zh`, `vendor_en` (text) - 供应商/品牌(中英文,含keyword子字段) | ||
| 33 | +- `category_path_zh`, `category_path_en` (text) - 类目路径(中英文) | ||
| 34 | +- `category_name_zh`, `category_name_en` (text) - 类目名称(中英文) | ||
| 37 | 35 | ||
| 38 | -### 必需字段 | 36 | +#### 类目字段 |
| 37 | +- `category_id` (keyword) - 类目ID | ||
| 38 | +- `category_name` (keyword) - 类目名称 | ||
| 39 | +- `category_level` (integer) - 类目层级 | ||
| 40 | +- `category1_name`, `category2_name`, `category3_name` (keyword) - 多级类目 | ||
| 39 | 41 | ||
| 40 | -- `tenant_id` (KEYWORD, required) - 租户隔离字段 | 42 | +#### 规格和选项 |
| 43 | +- `specifications` (nested) - 规格列表(name, value, sku_id) | ||
| 44 | +- `option1_name`, `option2_name`, `option3_name` (keyword) - 选项名称 | ||
| 41 | 45 | ||
| 42 | -### 主要字段 | 46 | +#### 价格和库存 |
| 47 | +- `min_price`, `max_price`, `compare_at_price` (float) - 价格字段 | ||
| 48 | +- `sku_prices` (float) - SKU价格列表(数组) | ||
| 49 | +- `sku_weights` (long) - SKU重量列表(数组) | ||
| 50 | +- `sku_weight_units` (keyword) - SKU重量单位列表(数组) | ||
| 51 | +- `total_inventory` (long) - 总库存 | ||
| 43 | 52 | ||
| 44 | -- `spu_id` - SPU ID | ||
| 45 | -- `title`, `brief`, `description` - 文本搜索字段 | ||
| 46 | -- `seo_title`, `seo_description`, `seo_keywords` - SEO字段 | ||
| 47 | -- `vendor`, `tags`, `category` - 分类和标签字段(HKText,支持 `.keyword` 精确匹配) | ||
| 48 | -- `min_price`, `max_price`, `compare_at_price` - 价格字段 | ||
| 49 | -- `skus` (nested) - 嵌套SKU数组 | 53 | +#### 嵌套字段 |
| 54 | +- `skus` (nested) - SKU详细信息数组 | ||
| 55 | +- `image_embedding` (nested) - 图片向量(仅用于搜索) | ||
| 50 | 56 | ||
| 51 | -## 数据导入流程 | 57 | +#### 其他 |
| 58 | +- `tags` (keyword) - 标签列表(数组) | ||
| 59 | +- `image_url` (keyword, index: false) - 主图URL | ||
| 60 | +- `title_embedding` (dense_vector) - 标题向量(仅用于搜索,不返回) | ||
| 52 | 61 | ||
| 53 | -### 1. 生成测试数据 | 62 | +## 查询配置 |
| 54 | 63 | ||
| 55 | -```bash | ||
| 56 | -python scripts/generate_test_data.py \ | ||
| 57 | - --num-spus 100 \ | ||
| 58 | - --tenant-id "1" \ | ||
| 59 | - --start-spu-id 1 \ | ||
| 60 | - --start-sku-id 1 \ | ||
| 61 | - --output test_data.sql | ||
| 62 | -``` | 64 | +### 文本召回字段 |
| 63 | 65 | ||
| 64 | -### 2. 导入测试数据到MySQL | ||
| 65 | - | ||
| 66 | -```bash | ||
| 67 | -python scripts/import_test_data.py \ | ||
| 68 | - --db-host localhost \ | ||
| 69 | - --db-port 3306 \ | ||
| 70 | - --db-database saas \ | ||
| 71 | - --db-username root \ | ||
| 72 | - --db-password password \ | ||
| 73 | - --sql-file test_data.sql \ | ||
| 74 | - --tenant-id "1" | ||
| 75 | -``` | 66 | +默认同时搜索以下字段(中英文都包含): |
| 67 | +- `title_zh^3.0`, `title_en^3.0` | ||
| 68 | +- `brief_zh^1.5`, `brief_en^1.5` | ||
| 69 | +- `description_zh^1.0`, `description_en^1.0` | ||
| 70 | +- `vendor_zh^1.5`, `vendor_en^1.5` | ||
| 71 | +- `category_path_zh^1.5`, `category_path_en^1.5` | ||
| 72 | +- `category_name_zh^1.5`, `category_name_en^1.5` | ||
| 73 | +- `tags^1.0` | ||
| 76 | 74 | ||
| 77 | -### 3. 导入数据到Elasticsearch | ||
| 78 | - | ||
| 79 | -```bash | ||
| 80 | -python scripts/ingest_shoplazza.py \ | ||
| 81 | - --db-host localhost \ | ||
| 82 | - --db-port 3306 \ | ||
| 83 | - --db-database saas \ | ||
| 84 | - --db-username root \ | ||
| 85 | - --db-password password \ | ||
| 86 | - --tenant-id "1" \ | ||
| 87 | - --config base \ | ||
| 88 | - --es-host http://localhost:9200 \ | ||
| 89 | - --recreate \ | ||
| 90 | - --batch-size 500 | ||
| 91 | -``` | 75 | +### 查询架构 |
| 92 | 76 | ||
| 93 | -## API使用 | 77 | +**结构**: `filters AND (text_recall OR embedding_recall)` |
| 94 | 78 | ||
| 95 | -### 搜索接口 | 79 | +- **filters**: 前端传递的过滤条件(永远起作用) |
| 80 | +- **text_recall**: 文本相关性召回(同时搜索中英文字段) | ||
| 81 | +- **embedding_recall**: 向量召回(KNN,使用 `title_embedding`) | ||
| 82 | +- **function_score**: 包装召回部分,支持提权字段 | ||
| 96 | 83 | ||
| 97 | -**端点**: `POST /search/` | 84 | +### Function Score 配置 |
| 98 | 85 | ||
| 99 | -**请求头**: | ||
| 100 | -``` | ||
| 101 | -X-Tenant-ID: 1 | ||
| 102 | -Content-Type: application/json | ||
| 103 | -``` | 86 | +位置: `search/query_config.py` 中的 `FUNCTION_SCORE_CONFIG` |
| 104 | 87 | ||
| 105 | -**请求体**: | ||
| 106 | -```json | ||
| 107 | -{ | ||
| 108 | - "query": "耳机", | ||
| 109 | - "size": 10, | ||
| 110 | - "from": 0, | ||
| 111 | - "filters": { | ||
| 112 | - "category.keyword": "电子产品" | ||
| 113 | - }, | ||
| 114 | - "facets": ["category.keyword", "vendor.keyword"] | ||
| 115 | -} | ||
| 116 | -``` | 88 | +支持的类型: |
| 89 | +- `filter_weight`: 条件权重(如新品提权) | ||
| 90 | +- `field_value_factor`: 字段值因子(如销量因子) | ||
| 91 | +- `decay`: 衰减函数(如时间衰减) | ||
| 92 | + | ||
| 93 | +## 分面配置 | ||
| 94 | + | ||
| 95 | +### 默认分面字段 | ||
| 96 | + | ||
| 97 | +- `category1_name` - 一级类目 | ||
| 98 | +- `category2_name` - 二级类目 | ||
| 99 | +- `category3_name` - 三级类目 | ||
| 100 | +- `specifications` - 规格分面(嵌套聚合,按name分组,然后按value聚合) | ||
| 101 | + | ||
| 102 | +### 规格分面说明 | ||
| 103 | + | ||
| 104 | +`specifications` 使用特殊的嵌套聚合: | ||
| 105 | +- 按 `specifications.name` 分组(如"color"、"size") | ||
| 106 | +- 每个 `name` 下按 `specifications.value` 聚合(如"white"、"black") | ||
| 117 | 107 | ||
| 118 | -**响应格式**: | 108 | +返回格式: |
| 119 | ```json | 109 | ```json |
| 120 | { | 110 | { |
| 121 | - "results": [ | ||
| 122 | - { | ||
| 123 | - "spu_id": "1", | ||
| 124 | - "title": "蓝牙耳机 Sony", | ||
| 125 | - "handle": "product-1", | ||
| 126 | - "description": "高品质无线蓝牙耳机", | ||
| 127 | - "vendor": "Sony", | ||
| 128 | - "category": "电子产品", | ||
| 129 | - "price": 199.99, | ||
| 130 | - "compare_at_price": 299.99, | ||
| 131 | - "currency": "USD", | ||
| 132 | - "image_url": "//cdn.example.com/products/1.jpg", | ||
| 133 | - "in_stock": true, | ||
| 134 | - "skus": [ | ||
| 135 | - { | ||
| 136 | - "sku_id": "1", | ||
| 137 | - "title": "黑色", | ||
| 138 | - "price": 199.99, | ||
| 139 | - "compare_at_price": 299.99, | ||
| 140 | - "sku": "SKU-1-1", | ||
| 141 | - "stock": 50, | ||
| 142 | - "options": { | ||
| 143 | - "option1": "黑色" | ||
| 144 | - } | ||
| 145 | - } | ||
| 146 | - ], | ||
| 147 | - "relevance_score": 0.95 | ||
| 148 | - } | ||
| 149 | - ], | ||
| 150 | - "total": 10, | ||
| 151 | - "max_score": 1.0, | ||
| 152 | - "facets": [ | ||
| 153 | - { | ||
| 154 | - "field": "category.keyword", | ||
| 155 | - "label": "category.keyword", | ||
| 156 | - "type": "terms", | ||
| 157 | - "values": [ | ||
| 158 | - { | ||
| 159 | - "value": "电子产品", | ||
| 160 | - "label": "电子产品", | ||
| 161 | - "count": 5, | ||
| 162 | - "selected": false | ||
| 163 | - } | ||
| 164 | - ] | ||
| 165 | - } | ||
| 166 | - ], | ||
| 167 | - "suggestions": [], | ||
| 168 | - "related_searches": [], | ||
| 169 | - "took_ms": 15, | ||
| 170 | - "query_info": {} | 111 | + "field": "specifications.color", |
| 112 | + "label": "color", | ||
| 113 | + "type": "terms", | ||
| 114 | + "values": [ | ||
| 115 | + {"value": "white", "count": 50}, | ||
| 116 | + {"value": "black", "count": 30} | ||
| 117 | + ] | ||
| 171 | } | 118 | } |
| 172 | ``` | 119 | ``` |
| 173 | 120 | ||
| 174 | -### 响应格式说明 | ||
| 175 | - | ||
| 176 | -#### 主要变化 | ||
| 177 | - | ||
| 178 | -1. **`results`替代`hits`**:返回字段从`hits`改为`results` | ||
| 179 | -2. **结构化结果**:每个结果包含`spu_id`, `title`, `skus`, `relevance_score`等字段 | ||
| 180 | -3. **无ES内部字段**:不包含`_id`, `_score`, `_source`等ES内部字段 | ||
| 181 | -4. **嵌套skus**:每个商品包含skus数组,每个sku包含完整的变体信息 | ||
| 182 | -5. **相关性分数**:`relevance_score`是ES原始分数(不进行归一化) | ||
| 183 | - | ||
| 184 | -#### SpuResult字段 | ||
| 185 | - | ||
| 186 | -- `spu_id` - SPU ID | ||
| 187 | -- `title` - 商品标题 | ||
| 188 | -- `handle` - 商品handle | ||
| 189 | -- `description` - 商品描述 | ||
| 190 | -- `vendor` - 供应商/品牌 | ||
| 191 | -- `category` - 类目 | ||
| 192 | -- `tags` - 标签 | ||
| 193 | -- `price` - 最低价格(min_price) | ||
| 194 | -- `compare_at_price` - 原价 | ||
| 195 | -- `currency` - 货币单位(默认USD) | ||
| 196 | -- `image_url` - 主图URL | ||
| 197 | -- `in_stock` - 是否有库存 | ||
| 198 | -- `skus` - SKU列表 | ||
| 199 | -- `relevance_score` - 相关性分数(ES原始分数) | ||
| 200 | - | ||
| 201 | -#### SkuResult字段 | ||
| 202 | - | ||
| 203 | -- `sku_id` - SKU ID | ||
| 204 | -- `title` - 变体标题 | ||
| 205 | -- `price` - 价格 | ||
| 206 | -- `compare_at_price` - 原价 | ||
| 207 | -- `sku` - SKU编码 | ||
| 208 | -- `stock` - 库存数量 | ||
| 209 | -- `options` - 选项(颜色、尺寸等) | ||
| 210 | - | ||
| 211 | -## 测试 | ||
| 212 | - | ||
| 213 | -### 运行测试脚本 | ||
| 214 | - | ||
| 215 | -```bash | ||
| 216 | -python scripts/test_base.py \ | ||
| 217 | - --api-url http://localhost:8000 \ | ||
| 218 | - --tenant-id "1" \ | ||
| 219 | - --test-tenant-2 "2" | ||
| 220 | -``` | ||
| 221 | - | ||
| 222 | -### 测试内容 | ||
| 223 | - | ||
| 224 | -1. **基本搜索**:测试搜索API基本功能 | ||
| 225 | -2. **响应格式验证**:验证返回格式是否符合要求 | ||
| 226 | -3. **Facets聚合**:测试分面搜索功能 | ||
| 227 | -4. **租户隔离**:验证不同租户的数据隔离 | 121 | +## 返回字段映射 |
| 228 | 122 | ||
| 229 | -## 常见问题 | 123 | +后端根据请求的 `language` 参数(`zh` 或 `en`)自动选择对应的中英文字段: |
| 230 | 124 | ||
| 231 | -### Q: 为什么配置中没有MySQL相关配置? | 125 | +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 |
| 126 | +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 | ||
| 232 | 127 | ||
| 233 | -A: 数据源配置和数据导入流程是写死的脚本,不在搜索配置中。搜索配置只关注ES搜索相关的内容。 | 128 | +映射规则: |
| 129 | +- `title_zh/en` → `title` | ||
| 130 | +- `brief_zh/en` → `brief` | ||
| 131 | +- `description_zh/en` → `description` | ||
| 132 | +- `vendor_zh/en` → `vendor` | ||
| 133 | +- `category_path_zh/en` → `category_path` | ||
| 134 | +- `category_name_zh/en` → `category_name` | ||
| 234 | 135 | ||
| 235 | -### Q: 如何为新的租户导入数据? | 136 | +## 配置修改 |
| 236 | 137 | ||
| 237 | -A: 使用`ingest_shoplazza.py`脚本,指定不同的`--tenant-id`参数即可。 | 138 | +### 修改索引结构 |
| 238 | 139 | ||
| 239 | -### Q: 如何验证租户隔离是否生效? | 140 | +编辑 `mappings/search_products.json`,然后: |
| 141 | +1. 删除旧索引: `scripts/recreate_and_import.py --recreate` | ||
| 142 | +2. 重新导入数据: `scripts/ingest.sh <tenant_id> true` | ||
| 240 | 143 | ||
| 241 | -A: 使用`test_base.py`脚本,指定两个不同的`--tenant-id`,检查搜索结果是否隔离。 | 144 | +### 修改查询配置 |
| 242 | 145 | ||
| 243 | -### Q: API返回格式中为什么没有`_id`和`_score`? | 146 | +编辑 `search/query_config.py`: |
| 147 | +- `DEFAULT_MATCH_FIELDS`: 文本召回字段列表 | ||
| 148 | +- `FUNCTION_SCORE_CONFIG`: Function score 配置 | ||
| 149 | +- `DEFAULT_FACETS`: 默认分面字段 | ||
| 244 | 150 | ||
| 245 | -A: 为了提供外部友好的API格式,我们移除了ES内部字段,使用`spu_id`和`relevance_score`替代。 | 151 | +### 修改返回字段 |
| 246 | 152 | ||
| 247 | -### Q: 如何添加新的搜索字段? | ||
| 248 | - | ||
| 249 | -A: 在`config/schema/base/config.yaml`中添加字段定义,然后重新生成索引映射并重新导入数据。 | 153 | +编辑 `search/query_config.py` 中的 `SOURCE_FIELDS` 列表。 |
| 250 | 154 | ||
| 251 | ## 注意事项 | 155 | ## 注意事项 |
| 252 | 156 | ||
| 253 | -1. **tenant_id必需**:所有API请求必须提供`tenant_id`(通过请求头`X-Tenant-ID`或查询参数`tenant_id`) | ||
| 254 | -2. **索引共享**:所有客户共享`search_products`索引,确保`tenant_id`字段正确设置 | ||
| 255 | -3. **数据导入**:数据导入脚本是写死的,不依赖配置中的MySQL设置 | ||
| 256 | -4. **配置分离**:搜索配置和数据源配置完全分离,提高可维护性 | 157 | +1. **无需配置文件**: 所有配置都是硬编码的,不需要为每个租户创建配置文件 |
| 158 | +2. **统一结构**: 所有租户共享相同的索引结构和查询逻辑 | ||
| 159 | +3. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 | ||
| 160 | +4. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不会返回给前端 | ||
| 161 | + | ||
| 162 | +## 相关文档 | ||
| 257 | 163 | ||
| 164 | +- `索引字段说明v2.md` - 详细的字段说明 | ||
| 165 | +- `搜索API对接指南.md` - API使用说明 | ||
| 166 | +- `mappings/search_products.json` - 索引 mapping 定义 |
docs/搜索API对接指南.md
| @@ -64,7 +64,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -64,7 +64,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 64 | -d '{ | 64 | -d '{ |
| 65 | "tenant_id": "demo-tenant", | 65 | "tenant_id": "demo-tenant", |
| 66 | "query": "芭比娃娃", | 66 | "query": "芭比娃娃", |
| 67 | - "facets": ["category.keyword", "vendor.keyword"], | 67 | + "facets": ["category.keyword", "specifications.color", "specifications.size"], |
| 68 | "min_score": 0.2 | 68 | "min_score": 0.2 |
| 69 | }' | 69 | }' |
| 70 | ``` | 70 | ``` |
| @@ -95,10 +95,10 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -95,10 +95,10 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 95 | 95 | ||
| 96 | ```json | 96 | ```json |
| 97 | { | 97 | { |
| 98 | - "tenant_id": "string (required)", | ||
| 99 | "query": "string (required)", | 98 | "query": "string (required)", |
| 100 | "size": 10, | 99 | "size": 10, |
| 101 | "from": 0, | 100 | "from": 0, |
| 101 | + "language": "zh", | ||
| 102 | "filters": {}, | 102 | "filters": {}, |
| 103 | "range_filters": {}, | 103 | "range_filters": {}, |
| 104 | "facets": [], | 104 | "facets": [], |
| @@ -111,18 +111,20 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -111,18 +111,20 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 111 | } | 111 | } |
| 112 | ``` | 112 | ``` |
| 113 | 113 | ||
| 114 | +**注意**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。 | ||
| 115 | + | ||
| 114 | #### 参数详细说明 | 116 | #### 参数详细说明 |
| 115 | 117 | ||
| 116 | | 参数 | 类型 | 必填 | 默认值 | 说明 | | 118 | | 参数 | 类型 | 必填 | 默认值 | 说明 | |
| 117 | |------|------|------|--------|------| | 119 | |------|------|------|--------|------| |
| 118 | -| `tenant_id` | string | Y | - | 租户ID,用于隔离不同站点或客户的数据 | | ||
| 119 | | `query` | string | Y | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | | 120 | | `query` | string | Y | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | |
| 120 | | `size` | integer | N | 10 | 返回结果数量(1-100) | | 121 | | `size` | integer | N | 10 | 返回结果数量(1-100) | |
| 121 | | `from` | integer | N | 0 | 分页偏移量(用于分页) | | 122 | | `from` | integer | N | 0 | 分页偏移量(用于分页) | |
| 123 | +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 | | ||
| 122 | | `filters` | object | N | null | 精确匹配过滤器(见下文) | | 124 | | `filters` | object | N | null | 精确匹配过滤器(见下文) | |
| 123 | | `range_filters` | object | N | null | 数值范围过滤器(见下文) | | 125 | | `range_filters` | object | N | null | 数值范围过滤器(见下文) | |
| 124 | | `facets` | array | N | null | 分面配置(见下文) | | 126 | | `facets` | array | N | null | 分面配置(见下文) | |
| 125 | -| `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`, `title`) | | 127 | +| `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | |
| 126 | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | 128 | | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | |
| 127 | | `min_score` | float | N | null | 最小相关性分数阈值 | | 129 | | `min_score` | float | N | null | 最小相关性分数阈值 | |
| 128 | | `debug` | boolean | N | false | 是否返回调试信息 | | 130 | | `debug` | boolean | N | false | 是否返回调试信息 | |
| @@ -139,9 +141,17 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -139,9 +141,17 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 139 | ```json | 141 | ```json |
| 140 | { | 142 | { |
| 141 | "filters": { | 143 | "filters": { |
| 142 | - "category.keyword": "玩具", // 单值:精确匹配 | ||
| 143 | - "vendor.keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) | ||
| 144 | - "tags.keyword": "益智玩具" // 单值:精确匹配 | 144 | + "category_name": "手机", // 单值:精确匹配 |
| 145 | + "category1_name": "服装", // 一级类目 | ||
| 146 | + "category2_name": "男装", // 二级类目 | ||
| 147 | + "category3_name": "衬衫", // 三级类目 | ||
| 148 | + "vendor_zh.keyword": ["奇乐", "品牌A"], // 数组:匹配任意值(OR) | ||
| 149 | + "tags": "手机", // 标签(keyword类型) | ||
| 150 | + // specifications 嵌套过滤(特殊格式) | ||
| 151 | + "specifications": { | ||
| 152 | + "name": "color", | ||
| 153 | + "value": "white" | ||
| 154 | + } | ||
| 145 | } | 155 | } |
| 146 | } | 156 | } |
| 147 | ``` | 157 | ``` |
| @@ -151,11 +161,46 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -151,11 +161,46 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 151 | - 整数:精确匹配 | 161 | - 整数:精确匹配 |
| 152 | - 布尔值:精确匹配 | 162 | - 布尔值:精确匹配 |
| 153 | - 数组:匹配任意值(OR 逻辑) | 163 | - 数组:匹配任意值(OR 逻辑) |
| 164 | +- 对象:specifications 嵌套过滤(见下文) | ||
| 165 | + | ||
| 166 | +**Specifications 嵌套过滤**: | ||
| 167 | + | ||
| 168 | +`specifications` 是嵌套字段,支持按规格名称和值进行过滤。 | ||
| 169 | + | ||
| 170 | +**单个规格过滤**: | ||
| 171 | +```json | ||
| 172 | +{ | ||
| 173 | + "filters": { | ||
| 174 | + "specifications": { | ||
| 175 | + "name": "color", | ||
| 176 | + "value": "white" | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | +} | ||
| 180 | +``` | ||
| 181 | +查询规格名称为"color"且值为"white"的商品。 | ||
| 182 | + | ||
| 183 | +**多个规格过滤(OR 逻辑)**: | ||
| 184 | +```json | ||
| 185 | +{ | ||
| 186 | + "filters": { | ||
| 187 | + "specifications": [ | ||
| 188 | + {"name": "color", "value": "white"}, | ||
| 189 | + {"name": "size", "value": "256GB"} | ||
| 190 | + ] | ||
| 191 | + } | ||
| 192 | +} | ||
| 193 | +``` | ||
| 194 | +查询满足任意一个规格的商品(color=white **或** size=256GB)。 | ||
| 154 | 195 | ||
| 155 | **常用过滤字段**: | 196 | **常用过滤字段**: |
| 156 | -- `category.keyword`: 类目 | ||
| 157 | -- `vendor.keyword`: 品牌/供应商 | ||
| 158 | -- `tags.keyword`: 标签 | 197 | +- `category_name`: 类目名称 |
| 198 | +- `category1_name`, `category2_name`, `category3_name`: 多级类目 | ||
| 199 | +- `category_id`: 类目ID | ||
| 200 | +- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌(使用keyword子字段) | ||
| 201 | +- `tags`: 标签(keyword类型,支持数组) | ||
| 202 | +- `option1_name`, `option2_name`, `option3_name`: 选项名称 | ||
| 203 | +- `specifications`: 规格过滤(嵌套字段,格式见上文) | ||
| 159 | 204 | ||
| 160 | #### 2. 范围过滤器 (range_filters) | 205 | #### 2. 范围过滤器 (range_filters) |
| 161 | 206 | ||
| @@ -201,7 +246,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -201,7 +246,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 201 | **简单模式**(字符串数组): | 246 | **简单模式**(字符串数组): |
| 202 | ```json | 247 | ```json |
| 203 | { | 248 | { |
| 204 | - "facets": ["category.keyword", "vendor.keyword"] | 249 | + "facets": ["category1_name", "category2_name", "category3_name", "specifications"] |
| 205 | } | 250 | } |
| 206 | ``` | 251 | ``` |
| 207 | 252 | ||
| @@ -210,7 +255,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -210,7 +255,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 210 | { | 255 | { |
| 211 | "facets": [ | 256 | "facets": [ |
| 212 | { | 257 | { |
| 213 | - "field": "category.keyword", | 258 | + "field": "category1_name", |
| 214 | "size": 15, | 259 | "size": 15, |
| 215 | "type": "terms" | 260 | "type": "terms" |
| 216 | }, | 261 | }, |
| @@ -223,6 +268,53 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -223,6 +268,53 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 223 | {"key": "100-200", "from": 100, "to": 200}, | 268 | {"key": "100-200", "from": 100, "to": 200}, |
| 224 | {"key": "200+", "from": 200} | 269 | {"key": "200+", "from": 200} |
| 225 | ] | 270 | ] |
| 271 | + }, | ||
| 272 | + "specifications" // 规格分面(特殊处理:嵌套聚合,按name分组,然后按value聚合) | ||
| 273 | + ] | ||
| 274 | +} | ||
| 275 | +``` | ||
| 276 | + | ||
| 277 | +**规格分面说明**: | ||
| 278 | + | ||
| 279 | +`specifications` 是嵌套字段,支持两种分面模式: | ||
| 280 | + | ||
| 281 | +**模式1:所有规格名称的分面** (`"specifications"`): | ||
| 282 | +```json | ||
| 283 | +{ | ||
| 284 | + "facets": ["specifications"] | ||
| 285 | +} | ||
| 286 | +``` | ||
| 287 | +返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 | ||
| 288 | + | ||
| 289 | +**模式2:指定规格名称的分面** (`"specifications.color"`): | ||
| 290 | +```json | ||
| 291 | +{ | ||
| 292 | + "facets": ["specifications.color", "specifications.size"] | ||
| 293 | +} | ||
| 294 | +``` | ||
| 295 | +只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size")。 | ||
| 296 | + | ||
| 297 | +**返回格式示例**: | ||
| 298 | +```json | ||
| 299 | +{ | ||
| 300 | + "facets": [ | ||
| 301 | + { | ||
| 302 | + "field": "specifications.color", | ||
| 303 | + "label": "color", | ||
| 304 | + "type": "terms", | ||
| 305 | + "values": [ | ||
| 306 | + {"value": "white", "count": 50, "selected": false}, | ||
| 307 | + {"value": "black", "count": 30, "selected": false} | ||
| 308 | + ] | ||
| 309 | + }, | ||
| 310 | + { | ||
| 311 | + "field": "specifications.size", | ||
| 312 | + "label": "size", | ||
| 313 | + "type": "terms", | ||
| 314 | + "values": [ | ||
| 315 | + {"value": "256GB", "count": 40, "selected": false}, | ||
| 316 | + {"value": "512GB", "count": 20, "selected": false} | ||
| 317 | + ] | ||
| 226 | } | 318 | } |
| 227 | ] | 319 | ] |
| 228 | } | 320 | } |
| @@ -276,28 +368,47 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -276,28 +368,47 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 276 | { | 368 | { |
| 277 | "spu_id": "12345", | 369 | "spu_id": "12345", |
| 278 | "title": "芭比时尚娃娃", | 370 | "title": "芭比时尚娃娃", |
| 279 | - "handle": "barbie-doll", | ||
| 280 | - "description": "高品质芭比娃娃", | 371 | + "brief": "高品质芭比娃娃", |
| 372 | + "description": "详细描述...", | ||
| 281 | "vendor": "美泰", | 373 | "vendor": "美泰", |
| 282 | "category": "玩具", | 374 | "category": "玩具", |
| 283 | - "tags": "娃娃, 玩具, 女孩", | 375 | + "category_path": "玩具/娃娃/时尚", |
| 376 | + "category_name": "时尚", | ||
| 377 | + "category_id": "cat_001", | ||
| 378 | + "category_level": 3, | ||
| 379 | + "category1_name": "玩具", | ||
| 380 | + "category2_name": "娃娃", | ||
| 381 | + "category3_name": "时尚", | ||
| 382 | + "tags": ["娃娃", "玩具", "女孩"], | ||
| 284 | "price": 89.99, | 383 | "price": 89.99, |
| 285 | "compare_at_price": 129.99, | 384 | "compare_at_price": 129.99, |
| 286 | "currency": "USD", | 385 | "currency": "USD", |
| 287 | "image_url": "https://example.com/image.jpg", | 386 | "image_url": "https://example.com/image.jpg", |
| 288 | "in_stock": true, | 387 | "in_stock": true, |
| 388 | + "sku_prices": [89.99, 99.99, 109.99], | ||
| 389 | + "sku_weights": [100, 150, 200], | ||
| 390 | + "sku_weight_units": ["g", "g", "g"], | ||
| 391 | + "total_inventory": 500, | ||
| 392 | + "option1_name": "color", | ||
| 393 | + "option2_name": "size", | ||
| 394 | + "option3_name": null, | ||
| 395 | + "specifications": [ | ||
| 396 | + {"sku_id": "sku_001", "name": "color", "value": "pink"}, | ||
| 397 | + {"sku_id": "sku_001", "name": "size", "value": "standard"} | ||
| 398 | + ], | ||
| 289 | "skus": [ | 399 | "skus": [ |
| 290 | { | 400 | { |
| 291 | "sku_id": "67890", | 401 | "sku_id": "67890", |
| 292 | - "title": "粉色款", | ||
| 293 | "price": 89.99, | 402 | "price": 89.99, |
| 294 | "compare_at_price": 129.99, | 403 | "compare_at_price": 129.99, |
| 295 | "sku": "BARBIE-001", | 404 | "sku": "BARBIE-001", |
| 296 | "stock": 100, | 405 | "stock": 100, |
| 297 | - "options": { | ||
| 298 | - "option1": "粉色", | ||
| 299 | - "option2": "标准款" | ||
| 300 | - } | 406 | + "weight": 0.1, |
| 407 | + "weight_unit": "kg", | ||
| 408 | + "option1_value": "pink", | ||
| 409 | + "option2_value": "standard", | ||
| 410 | + "option3_value": null, | ||
| 411 | + "image_src": "https://example.com/sku1.jpg" | ||
| 301 | } | 412 | } |
| 302 | ], | 413 | ], |
| 303 | "relevance_score": 8.5 | 414 | "relevance_score": 8.5 |
| @@ -307,8 +418,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -307,8 +418,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 307 | "max_score": 8.5, | 418 | "max_score": 8.5, |
| 308 | "facets": [ | 419 | "facets": [ |
| 309 | { | 420 | { |
| 310 | - "field": "category.keyword", | ||
| 311 | - "label": "category.keyword", | 421 | + "field": "category1_name", |
| 422 | + "label": "category1_name", | ||
| 312 | "type": "terms", | 423 | "type": "terms", |
| 313 | "values": [ | 424 | "values": [ |
| 314 | { | 425 | { |
| @@ -318,6 +429,19 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -318,6 +429,19 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 318 | "selected": false | 429 | "selected": false |
| 319 | } | 430 | } |
| 320 | ] | 431 | ] |
| 432 | + }, | ||
| 433 | + { | ||
| 434 | + "field": "specifications.color", | ||
| 435 | + "label": "color", | ||
| 436 | + "type": "terms", | ||
| 437 | + "values": [ | ||
| 438 | + { | ||
| 439 | + "value": "pink", | ||
| 440 | + "label": "pink", | ||
| 441 | + "count": 30, | ||
| 442 | + "selected": false | ||
| 443 | + } | ||
| 444 | + ] | ||
| 321 | } | 445 | } |
| 322 | ], | 446 | ], |
| 323 | "query_info": { | 447 | "query_info": { |
| @@ -356,31 +480,55 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -356,31 +480,55 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 356 | | 字段 | 类型 | 说明 | | 480 | | 字段 | 类型 | 说明 | |
| 357 | |------|------|------| | 481 | |------|------|------| |
| 358 | | `spu_id` | string | SPU ID | | 482 | | `spu_id` | string | SPU ID | |
| 359 | -| `title` | string | 商品标题 | | ||
| 360 | -| `handle` | string | 商品URL handle | | ||
| 361 | -| `description` | string | 商品描述 | | ||
| 362 | -| `vendor` | string | 供应商/品牌 | | ||
| 363 | -| `category` | string | 类目 | | ||
| 364 | -| `tags` | string | 标签 | | 483 | +| `title` | string | 商品标题(根据language参数自动选择title_zh或title_en) | |
| 484 | +| `brief` | string | 商品短描述(根据language参数自动选择) | | ||
| 485 | +| `description` | string | 商品详细描述(根据language参数自动选择) | | ||
| 486 | +| `vendor` | string | 供应商/品牌(根据language参数自动选择) | | ||
| 487 | +| `category` | string | 类目(兼容字段,等同于category_name) | | ||
| 488 | +| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) | | ||
| 489 | +| `category_name` | string | 类目名称(展示用,根据language参数自动选择) | | ||
| 490 | +| `category_id` | string | 类目ID | | ||
| 491 | +| `category_level` | integer | 类目层级(1/2/3) | | ||
| 492 | +| `category1_name` | string | 一级类目名称 | | ||
| 493 | +| `category2_name` | string | 二级类目名称 | | ||
| 494 | +| `category3_name` | string | 三级类目名称 | | ||
| 495 | +| `tags` | array[string] | 标签列表 | | ||
| 365 | | `price` | float | 价格(min_price) | | 496 | | `price` | float | 价格(min_price) | |
| 366 | | `compare_at_price` | float | 原价 | | 497 | | `compare_at_price` | float | 原价 | |
| 367 | | `currency` | string | 货币单位(默认USD) | | 498 | | `currency` | string | 货币单位(默认USD) | |
| 368 | | `image_url` | string | 主图URL | | 499 | | `image_url` | string | 主图URL | |
| 369 | | `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | | 500 | | `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | |
| 501 | +| `sku_prices` | array[float] | 所有SKU价格列表 | | ||
| 502 | +| `sku_weights` | array[integer] | 所有SKU重量列表 | | ||
| 503 | +| `sku_weight_units` | array[string] | 所有SKU重量单位列表 | | ||
| 504 | +| `total_inventory` | integer | 总库存 | | ||
| 505 | +| `option1_name` | string | 选项1名称(如"color") | | ||
| 506 | +| `option2_name` | string | 选项2名称(如"size") | | ||
| 507 | +| `option3_name` | string | 选项3名称 | | ||
| 508 | +| `specifications` | array[object] | 规格列表(与ES specifications字段对应) | | ||
| 370 | | `skus` | array | SKU 列表 | | 509 | | `skus` | array | SKU 列表 | |
| 371 | | `relevance_score` | float | 相关性分数 | | 510 | | `relevance_score` | float | 相关性分数 | |
| 372 | 511 | ||
| 512 | +**多语言字段说明**: | ||
| 513 | +- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段 | ||
| 514 | +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 | ||
| 515 | +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 | ||
| 516 | + | ||
| 373 | ### SkuResult字段说明 | 517 | ### SkuResult字段说明 |
| 374 | 518 | ||
| 375 | | 字段 | 类型 | 说明 | | 519 | | 字段 | 类型 | 说明 | |
| 376 | |------|------|------| | 520 | |------|------|------| |
| 377 | | `sku_id` | string | SKU ID | | 521 | | `sku_id` | string | SKU ID | |
| 378 | -| `title` | string | SKU标题 | | ||
| 379 | | `price` | float | 价格 | | 522 | | `price` | float | 价格 | |
| 380 | | `compare_at_price` | float | 原价 | | 523 | | `compare_at_price` | float | 原价 | |
| 381 | -| `sku` | string | SKU编码 | | 524 | +| `sku` | string | SKU编码(sku_code) | |
| 382 | | `stock` | integer | 库存数量 | | 525 | | `stock` | integer | 库存数量 | |
| 383 | -| `options` | object | 选项(颜色、尺寸等) | | 526 | +| `weight` | float | 重量 | |
| 527 | +| `weight_unit` | string | 重量单位 | | ||
| 528 | +| `option1_value` | string | 选项1取值(如color值) | | ||
| 529 | +| `option2_value` | string | 选项2取值(如size值) | | ||
| 530 | +| `option3_value` | string | 选项3取值 | | ||
| 531 | +| `image_src` | string | SKU图片地址 | | ||
| 384 | 532 | ||
| 385 | --- | 533 | --- |
| 386 | 534 | ||
| @@ -408,8 +556,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -408,8 +556,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 408 | { | 556 | { |
| 409 | "query": "玩具", | 557 | "query": "玩具", |
| 410 | "size": 20, | 558 | "size": 20, |
| 559 | + "language": "zh", | ||
| 411 | "filters": { | 560 | "filters": { |
| 412 | - "category.keyword": "益智玩具" | 561 | + "category_name": "益智玩具" |
| 413 | }, | 562 | }, |
| 414 | "range_filters": { | 563 | "range_filters": { |
| 415 | "min_price": { | 564 | "min_price": { |
| @@ -428,23 +577,26 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -428,23 +577,26 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 428 | { | 577 | { |
| 429 | "query": "玩具", | 578 | "query": "玩具", |
| 430 | "size": 20, | 579 | "size": 20, |
| 580 | + "language": "zh", | ||
| 431 | "facets": [ | 581 | "facets": [ |
| 432 | - "category.keyword", | ||
| 433 | - "vendor.keyword" | 582 | + "category1_name", |
| 583 | + "category2_name", | ||
| 584 | + "specifications" | ||
| 434 | ] | 585 | ] |
| 435 | } | 586 | } |
| 436 | ``` | 587 | ``` |
| 437 | 588 | ||
| 438 | ### 场景4:多条件组合搜索 | 589 | ### 场景4:多条件组合搜索 |
| 439 | 590 | ||
| 440 | -**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计 | 591 | +**需求**: 搜索"手机",筛选多个品牌,价格范围,并获取分面统计 |
| 441 | 592 | ||
| 442 | ```json | 593 | ```json |
| 443 | { | 594 | { |
| 444 | - "query": "玩具", | 595 | + "query": "手机", |
| 445 | "size": 20, | 596 | "size": 20, |
| 597 | + "language": "zh", | ||
| 446 | "filters": { | 598 | "filters": { |
| 447 | - "vendor.keyword": ["乐高", "孩之宝", "美泰"] | 599 | + "vendor_zh.keyword": ["品牌A", "品牌B"] |
| 448 | }, | 600 | }, |
| 449 | "range_filters": { | 601 | "range_filters": { |
| 450 | "min_price": { | 602 | "min_price": { |
| @@ -454,7 +606,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -454,7 +606,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 454 | }, | 606 | }, |
| 455 | "facets": [ | 607 | "facets": [ |
| 456 | { | 608 | { |
| 457 | - "field": "category.keyword", | 609 | + "field": "category1_name", |
| 458 | "size": 15 | 610 | "size": 15 |
| 459 | }, | 611 | }, |
| 460 | { | 612 | { |
| @@ -466,31 +618,117 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | @@ -466,31 +618,117 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | ||
| 466 | {"key": "100-200", "from": 100, "to": 200}, | 618 | {"key": "100-200", "from": 100, "to": 200}, |
| 467 | {"key": "200+", "from": 200} | 619 | {"key": "200+", "from": 200} |
| 468 | ] | 620 | ] |
| 469 | - } | 621 | + }, |
| 622 | + "specifications" | ||
| 470 | ], | 623 | ], |
| 471 | "sort_by": "min_price", | 624 | "sort_by": "min_price", |
| 472 | "sort_order": "asc" | 625 | "sort_order": "asc" |
| 473 | } | 626 | } |
| 474 | ``` | 627 | ``` |
| 475 | 628 | ||
| 476 | -### 场景5:布尔表达式搜索 | 629 | +### 场景5:规格过滤搜索 |
| 630 | + | ||
| 631 | +**需求**: 搜索"手机",筛选color为"white"的商品 | ||
| 632 | + | ||
| 633 | +```json | ||
| 634 | +{ | ||
| 635 | + "query": "手机", | ||
| 636 | + "size": 20, | ||
| 637 | + "language": "zh", | ||
| 638 | + "filters": { | ||
| 639 | + "specifications": { | ||
| 640 | + "name": "color", | ||
| 641 | + "value": "white" | ||
| 642 | + } | ||
| 643 | + } | ||
| 644 | +} | ||
| 645 | +``` | ||
| 646 | + | ||
| 647 | +### 场景6:多个规格过滤(OR逻辑) | ||
| 648 | + | ||
| 649 | +**需求**: 搜索"手机",筛选color为"white"或size为"256GB"的商品 | ||
| 650 | + | ||
| 651 | +```json | ||
| 652 | +{ | ||
| 653 | + "query": "手机", | ||
| 654 | + "size": 20, | ||
| 655 | + "language": "zh", | ||
| 656 | + "filters": { | ||
| 657 | + "specifications": [ | ||
| 658 | + {"name": "color", "value": "white"}, | ||
| 659 | + {"name": "size", "value": "256GB"} | ||
| 660 | + ] | ||
| 661 | + } | ||
| 662 | +} | ||
| 663 | +``` | ||
| 664 | + | ||
| 665 | +### 场景7:规格分面搜索 | ||
| 666 | + | ||
| 667 | +**需求**: 搜索"手机",获取所有规格的分面统计 | ||
| 668 | + | ||
| 669 | +```json | ||
| 670 | +{ | ||
| 671 | + "query": "手机", | ||
| 672 | + "size": 20, | ||
| 673 | + "language": "zh", | ||
| 674 | + "facets": ["specifications"] | ||
| 675 | +} | ||
| 676 | +``` | ||
| 677 | + | ||
| 678 | +**需求**: 只获取"color"规格的分面统计 | ||
| 679 | + | ||
| 680 | +```json | ||
| 681 | +{ | ||
| 682 | + "query": "手机", | ||
| 683 | + "size": 20, | ||
| 684 | + "language": "zh", | ||
| 685 | + "facets": ["specifications.color", "specifications.size"] | ||
| 686 | +} | ||
| 687 | +``` | ||
| 688 | + | ||
| 689 | +### 场景8:组合过滤和分面 | ||
| 690 | + | ||
| 691 | +**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 | ||
| 692 | + | ||
| 693 | +```json | ||
| 694 | +{ | ||
| 695 | + "query": "手机", | ||
| 696 | + "size": 20, | ||
| 697 | + "language": "zh", | ||
| 698 | + "filters": { | ||
| 699 | + "category_name": "手机", | ||
| 700 | + "specifications": { | ||
| 701 | + "name": "color", | ||
| 702 | + "value": "white" | ||
| 703 | + } | ||
| 704 | + }, | ||
| 705 | + "facets": [ | ||
| 706 | + "category1_name", | ||
| 707 | + "category2_name", | ||
| 708 | + "specifications.color", | ||
| 709 | + "specifications.size" | ||
| 710 | + ] | ||
| 711 | +} | ||
| 712 | +``` | ||
| 713 | + | ||
| 714 | +### 场景9:布尔表达式搜索 | ||
| 477 | 715 | ||
| 478 | -**需求**: 搜索包含"玩具"和"乐高"的商品,排除"电动" | 716 | +**需求**: 搜索包含"手机"和"智能"的商品,排除"二手" |
| 479 | 717 | ||
| 480 | ```json | 718 | ```json |
| 481 | { | 719 | { |
| 482 | - "query": "玩具 AND 乐高 ANDNOT 电动", | 720 | + "query": "手机 AND 智能 ANDNOT 二手", |
| 483 | "size": 20 | 721 | "size": 20 |
| 484 | } | 722 | } |
| 485 | ``` | 723 | ``` |
| 486 | 724 | ||
| 487 | -### 场景6:分页查询 | 725 | +### 场景10:分页查询 |
| 488 | 726 | ||
| 489 | **需求**: 获取第2页结果(每页20条) | 727 | **需求**: 获取第2页结果(每页20条) |
| 490 | 728 | ||
| 491 | ```json | 729 | ```json |
| 492 | { | 730 | { |
| 493 | - "query": "玩具", | 731 | + "query": "手机", |
| 494 | "size": 20, | 732 | "size": 20, |
| 495 | "from": 20 | 733 | "from": 20 |
| 496 | } | 734 | } |
| @@ -650,25 +888,33 @@ curl "http://localhost:6002/search/12345" | @@ -650,25 +888,33 @@ curl "http://localhost:6002/search/12345" | ||
| 650 | 888 | ||
| 651 | | 字段名 | 类型 | 描述 | | 889 | | 字段名 | 类型 | 描述 | |
| 652 | |--------|------|------| | 890 | |--------|------|------| |
| 891 | +| `tenant_id` | keyword | 租户ID(多租户隔离) | | ||
| 653 | | `spu_id` | keyword | SPU ID | | 892 | | `spu_id` | keyword | SPU ID | |
| 654 | -| `sku_id` | keyword/long | SKU ID(主键) | | ||
| 655 | -| `title` | text | 商品名称(中文) | | ||
| 656 | -| `en_title` | text | 商品名称(英文) | | ||
| 657 | -| `ru_title` | text | 商品名称(俄文) | | ||
| 658 | -| `category.keyword` | keyword | 类目(精确匹配) | | ||
| 659 | -| `vendor.keyword` | keyword | 品牌/供应商(精确匹配) | | ||
| 660 | -| `category` | HKText | 类目(支持 `category.keyword` 精确匹配) | | ||
| 661 | -| `tags.keyword` | keyword | 标签 | | ||
| 662 | -| `min_price` | double | 最低价格 | | ||
| 663 | -| `max_price` | double | 最高价格 | | ||
| 664 | -| `compare_at_price` | double | 原价 | | ||
| 665 | -| `create_time` | date | 创建时间 | | ||
| 666 | -| `update_time` | date | 更新时间 | | ||
| 667 | -| `in_stock` | boolean | 是否有库存 | | ||
| 668 | -| `text_embedding` | dense_vector | 文本向量(1024 维) | | ||
| 669 | -| `image_embedding` | dense_vector | 图片向量(1024 维) | | ||
| 670 | - | ||
| 671 | -> 不同租户可自定义字段名称。推荐将可过滤的文本字段配置为 HKText,这样即可同时支持全文检索和 `field.keyword` 精确过滤;数值字段单独建索引以用于排序/Range。 | 893 | +| `title_zh`, `title_en` | text | 商品标题(中英文) | |
| 894 | +| `brief_zh`, `brief_en` | text | 商品短描述(中英文) | | ||
| 895 | +| `description_zh`, `description_en` | text | 商品详细描述(中英文) | | ||
| 896 | +| `vendor_zh`, `vendor_en` | text | 供应商/品牌(中英文,含keyword子字段) | | ||
| 897 | +| `category_path_zh`, `category_path_en` | text | 类目路径(中英文,用于搜索) | | ||
| 898 | +| `category_name_zh`, `category_name_en` | text | 类目名称(中英文,用于搜索) | | ||
| 899 | +| `category_id` | keyword | 类目ID | | ||
| 900 | +| `category_name` | keyword | 类目名称(用于过滤) | | ||
| 901 | +| `category_level` | integer | 类目层级 | | ||
| 902 | +| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) | | ||
| 903 | +| `tags` | keyword | 标签(数组) | | ||
| 904 | +| `specifications` | nested | 规格(嵌套对象数组) | | ||
| 905 | +| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 | | ||
| 906 | +| `min_price`, `max_price` | float | 最低/最高价格 | | ||
| 907 | +| `compare_at_price` | float | 原价 | | ||
| 908 | +| `sku_prices` | float | SKU价格列表(数组) | | ||
| 909 | +| `sku_weights` | long | SKU重量列表(数组) | | ||
| 910 | +| `sku_weight_units` | keyword | SKU重量单位列表(数组) | | ||
| 911 | +| `total_inventory` | long | 总库存 | | ||
| 912 | +| `skus` | nested | SKU详细信息(嵌套对象数组) | | ||
| 913 | +| `create_time`, `update_time` | date | 创建/更新时间 | | ||
| 914 | +| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | | ||
| 915 | +| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) | | ||
| 916 | + | ||
| 917 | +> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。 | ||
| 672 | 918 | ||
| 673 | --- | 919 | --- |
| 674 | 920 | ||
| @@ -676,11 +922,14 @@ curl "http://localhost:6002/search/12345" | @@ -676,11 +922,14 @@ curl "http://localhost:6002/search/12345" | ||
| 676 | 922 | ||
| 677 | ### 常用字段列表 | 923 | ### 常用字段列表 |
| 678 | 924 | ||
| 679 | -#### 过滤字段(使用 HKText 的 keyword 子字段) | 925 | +#### 过滤字段 |
| 680 | 926 | ||
| 681 | -- `category.keyword`: 类目 | ||
| 682 | -- `vendor.keyword`: 品牌/供应商 | ||
| 683 | -- `tags.keyword`: 标签 | 927 | +- `category_name`: 类目名称 |
| 928 | +- `category1_name`, `category2_name`, `category3_name`: 多级类目 | ||
| 929 | +- `category_id`: 类目ID | ||
| 930 | +- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌(使用keyword子字段) | ||
| 931 | +- `tags`: 标签(keyword类型) | ||
| 932 | +- `option1_name`, `option2_name`, `option3_name`: 选项名称 | ||
| 684 | 933 | ||
| 685 | #### 范围字段 | 934 | #### 范围字段 |
| 686 | 935 | ||
| @@ -694,7 +943,6 @@ curl "http://localhost:6002/search/12345" | @@ -694,7 +943,6 @@ curl "http://localhost:6002/search/12345" | ||
| 694 | 943 | ||
| 695 | - `min_price`: 最低价格 | 944 | - `min_price`: 最低价格 |
| 696 | - `max_price`: 最高价格 | 945 | - `max_price`: 最高价格 |
| 697 | -- `title`: 标题(字母序) | ||
| 698 | - `create_time`: 创建时间 | 946 | - `create_time`: 创建时间 |
| 699 | - `update_time`: 更新时间 | 947 | - `update_time`: 更新时间 |
| 700 | - `relevance_score`: 相关性分数(默认) | 948 | - `relevance_score`: 相关性分数(默认) |
| @@ -703,23 +951,21 @@ curl "http://localhost:6002/search/12345" | @@ -703,23 +951,21 @@ curl "http://localhost:6002/search/12345" | ||
| 703 | 951 | ||
| 704 | | 分析器 | 语言 | 描述 | | 952 | | 分析器 | 语言 | 描述 | |
| 705 | |--------|------|------| | 953 | |--------|------|------| |
| 706 | -| `chinese_ecommerce` | 中文 | 基于 Ansj 的电商优化中文分析器 | | ||
| 707 | -| `english` | 英文 | 标准英文分析器 | | ||
| 708 | -| `russian` | 俄文 | 俄文分析器 | | ||
| 709 | -| `arabic` | 阿拉伯文 | 阿拉伯文分析器 | | ||
| 710 | -| `spanish` | 西班牙文 | 西班牙文分析器 | | ||
| 711 | -| `japanese` | 日文 | 日文分析器 | | 954 | +| `hanlp_index` | 中文 | 中文索引分析器(用于中文字段) | |
| 955 | +| `hanlp_standard` | 中文 | 中文查询分析器(用于中文字段) | | ||
| 956 | +| `english` | 英文 | 标准英文分析器(用于英文字段) | | ||
| 957 | +| `lowercase` | - | 小写标准化器(用于keyword子字段) | | ||
| 712 | 958 | ||
| 713 | ### 字段类型速查 | 959 | ### 字段类型速查 |
| 714 | 960 | ||
| 715 | | 类型 | ES Mapping | 用途 | | 961 | | 类型 | ES Mapping | 用途 | |
| 716 | |------|------------|------| | 962 | |------|------------|------| |
| 717 | -| `TEXT` | `text` | 全文检索 | | ||
| 718 | -| `KEYWORD` | `keyword` | 精确匹配、聚合、排序 | | ||
| 719 | -| `LONG` | `long` | 整数 | | ||
| 720 | -| `DOUBLE` | `double` | 浮点数 | | ||
| 721 | -| `DATE` | `date` | 日期时间 | | ||
| 722 | -| `BOOLEAN` | `boolean` | 布尔值 | | ||
| 723 | -| `TEXT_EMBEDDING` | `dense_vector` | 文本语义向量 | | ||
| 724 | -| `IMAGE_EMBEDDING` | `dense_vector` | 图片语义向量 | | 963 | +| `text` | `text` | 全文检索(支持中英文分析器) | |
| 964 | +| `keyword` | `keyword` | 精确匹配、聚合、排序 | | ||
| 965 | +| `integer` | `integer` | 整数 | | ||
| 966 | +| `long` | `long` | 长整数 | | ||
| 967 | +| `float` | `float` | 浮点数 | | ||
| 968 | +| `date` | `date` | 日期时间 | | ||
| 969 | +| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) | | ||
| 970 | +| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) | | ||
| 725 | 971 |
docs/搜索API速查表.md
| @@ -17,8 +17,38 @@ POST /search/ | @@ -17,8 +17,38 @@ POST /search/ | ||
| 17 | ```bash | 17 | ```bash |
| 18 | { | 18 | { |
| 19 | "filters": { | 19 | "filters": { |
| 20 | - "category.keyword": "玩具", // 单值 | ||
| 21 | - "vendor.keyword": ["乐高", "美泰"] // 多值(OR) | 20 | + "category_name": "手机", // 单值 |
| 21 | + "category1_name": "服装", // 一级类目 | ||
| 22 | + "vendor_zh.keyword": ["奇乐", "品牌A"], // 多值(OR) | ||
| 23 | + "tags": "手机", // 标签 | ||
| 24 | + // specifications 嵌套过滤 | ||
| 25 | + "specifications": { | ||
| 26 | + "name": "color", | ||
| 27 | + "value": "white" | ||
| 28 | + } | ||
| 29 | + } | ||
| 30 | +} | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +### Specifications 过滤 | ||
| 34 | + | ||
| 35 | +**单个规格**: | ||
| 36 | +```bash | ||
| 37 | +{ | ||
| 38 | + "filters": { | ||
| 39 | + "specifications": {"name": "color", "value": "white"} | ||
| 40 | + } | ||
| 41 | +} | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +**多个规格(OR)**: | ||
| 45 | +```bash | ||
| 46 | +{ | ||
| 47 | + "filters": { | ||
| 48 | + "specifications": [ | ||
| 49 | + {"name": "color", "value": "white"}, | ||
| 50 | + {"name": "size", "value": "256GB"} | ||
| 51 | + ] | ||
| 22 | } | 52 | } |
| 23 | } | 53 | } |
| 24 | ``` | 54 | ``` |
| @@ -48,7 +78,23 @@ POST /search/ | @@ -48,7 +78,23 @@ POST /search/ | ||
| 48 | 78 | ||
| 49 | ```bash | 79 | ```bash |
| 50 | { | 80 | { |
| 51 | - "facets": ["category.keyword", "vendor.keyword"] | 81 | + "facets": ["category1_name", "category2_name", "category3_name", "specifications"] |
| 82 | +} | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +### Specifications 分面 | ||
| 86 | + | ||
| 87 | +**所有规格名称**: | ||
| 88 | +```bash | ||
| 89 | +{ | ||
| 90 | + "facets": ["specifications"] // 返回所有name及其value列表 | ||
| 91 | +} | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +**指定规格名称**: | ||
| 95 | +```bash | ||
| 96 | +{ | ||
| 97 | + "facets": ["specifications.color", "specifications.size"] // 只返回指定name的value列表 | ||
| 52 | } | 98 | } |
| 53 | ``` | 99 | ``` |
| 54 | 100 | ||
| @@ -57,15 +103,18 @@ POST /search/ | @@ -57,15 +103,18 @@ POST /search/ | ||
| 57 | ```bash | 103 | ```bash |
| 58 | { | 104 | { |
| 59 | "facets": [ | 105 | "facets": [ |
| 60 | - {"field": "category.keyword", "size": 15}, | 106 | + {"field": "category1_name", "size": 15}, |
| 61 | { | 107 | { |
| 62 | - "field": "price", | 108 | + "field": "min_price", |
| 63 | "type": "range", | 109 | "type": "range", |
| 64 | "ranges": [ | 110 | "ranges": [ |
| 65 | {"key": "0-50", "to": 50}, | 111 | {"key": "0-50", "to": 50}, |
| 66 | {"key": "50-100", "from": 50, "to": 100} | 112 | {"key": "50-100", "from": 50, "to": 100} |
| 67 | ] | 113 | ] |
| 68 | - } | 114 | + }, |
| 115 | + "specifications", // 所有规格名称 | ||
| 116 | + "specifications.color", // 指定规格名称 | ||
| 117 | + "specifications.size" | ||
| 69 | ] | 118 | ] |
| 70 | } | 119 | } |
| 71 | ``` | 120 | ``` |
| @@ -110,19 +159,25 @@ POST /search/ | @@ -110,19 +159,25 @@ POST /search/ | ||
| 110 | 159 | ||
| 111 | ```bash | 160 | ```bash |
| 112 | POST /search/ | 161 | POST /search/ |
| 162 | +Headers: X-Tenant-ID: 2 | ||
| 113 | { | 163 | { |
| 114 | - "query": "玩具", | 164 | + "query": "手机", |
| 115 | "size": 20, | 165 | "size": 20, |
| 116 | "from": 0, | 166 | "from": 0, |
| 167 | + "language": "zh", | ||
| 117 | "filters": { | 168 | "filters": { |
| 118 | - "category.keyword": ["玩具", "益智玩具"] | 169 | + "category_name": "手机", |
| 170 | + "category1_name": "电子产品", | ||
| 171 | + "specifications": {"name": "color", "value": "white"} | ||
| 119 | }, | 172 | }, |
| 120 | "range_filters": { | 173 | "range_filters": { |
| 121 | - "price": {"gte": 50, "lte": 200} | 174 | + "min_price": {"gte": 50, "lte": 200} |
| 122 | }, | 175 | }, |
| 123 | "facets": [ | 176 | "facets": [ |
| 124 | - {"field": "vendor.keyword", "size": 15}, | ||
| 125 | - {"field": "category.keyword", "size": 15} | 177 | + {"field": "category1_name", "size": 15}, |
| 178 | + {"field": "category2_name", "size": 15}, | ||
| 179 | + "specifications.color", | ||
| 180 | + "specifications.size" | ||
| 126 | ], | 181 | ], |
| 127 | "sort_by": "min_price", | 182 | "sort_by": "min_price", |
| 128 | "sort_order": "asc" | 183 | "sort_order": "asc" |
| @@ -135,11 +190,29 @@ POST /search/ | @@ -135,11 +190,29 @@ POST /search/ | ||
| 135 | 190 | ||
| 136 | ```json | 191 | ```json |
| 137 | { | 192 | { |
| 138 | - "hits": [ | 193 | + "results": [ |
| 139 | { | 194 | { |
| 140 | - "_id": "12345", | ||
| 141 | - "_score": 8.5, | ||
| 142 | - "_source": {...} | 195 | + "spu_id": "12345", |
| 196 | + "title": "商品标题", | ||
| 197 | + "brief": "短描述", | ||
| 198 | + "description": "详细描述", | ||
| 199 | + "vendor": "供应商", | ||
| 200 | + "category": "类目", | ||
| 201 | + "category_path": "类目/路径", | ||
| 202 | + "category_name": "类目名称", | ||
| 203 | + "category1_name": "一级类目", | ||
| 204 | + "category2_name": "二级类目", | ||
| 205 | + "category3_name": "三级类目", | ||
| 206 | + "tags": ["标签1", "标签2"], | ||
| 207 | + "price": 99.99, | ||
| 208 | + "compare_at_price": 149.99, | ||
| 209 | + "sku_prices": [99.99, 109.99], | ||
| 210 | + "total_inventory": 500, | ||
| 211 | + "specifications": [ | ||
| 212 | + {"sku_id": "sku_001", "name": "color", "value": "white"} | ||
| 213 | + ], | ||
| 214 | + "skus": [...], | ||
| 215 | + "relevance_score": 8.5 | ||
| 143 | } | 216 | } |
| 144 | ], | 217 | ], |
| 145 | "total": 118, | 218 | "total": 118, |
| @@ -147,17 +220,30 @@ POST /search/ | @@ -147,17 +220,30 @@ POST /search/ | ||
| 147 | "took_ms": 45, | 220 | "took_ms": 45, |
| 148 | "facets": [ | 221 | "facets": [ |
| 149 | { | 222 | { |
| 150 | - "field": "category.keyword", | ||
| 151 | - "label": "商品类目", | 223 | + "field": "category1_name", |
| 224 | + "label": "category1_name", | ||
| 152 | "type": "terms", | 225 | "type": "terms", |
| 153 | "values": [ | 226 | "values": [ |
| 154 | { | 227 | { |
| 155 | - "value": "玩具", | ||
| 156 | - "label": "玩具", | 228 | + "value": "手机", |
| 229 | + "label": "手机", | ||
| 157 | "count": 85, | 230 | "count": 85, |
| 158 | "selected": false | 231 | "selected": false |
| 159 | } | 232 | } |
| 160 | ] | 233 | ] |
| 234 | + }, | ||
| 235 | + { | ||
| 236 | + "field": "specifications.color", | ||
| 237 | + "label": "color", | ||
| 238 | + "type": "terms", | ||
| 239 | + "values": [ | ||
| 240 | + { | ||
| 241 | + "value": "white", | ||
| 242 | + "label": "white", | ||
| 243 | + "count": 30, | ||
| 244 | + "selected": false | ||
| 245 | + } | ||
| 246 | + ] | ||
| 161 | } | 247 | } |
| 162 | ] | 248 | ] |
| 163 | } | 249 | } |
| @@ -192,14 +278,19 @@ GET /admin/stats | @@ -192,14 +278,19 @@ GET /admin/stats | ||
| 192 | ```python | 278 | ```python |
| 193 | import requests | 279 | import requests |
| 194 | 280 | ||
| 195 | -result = requests.post('http://localhost:6002/search/', json={ | ||
| 196 | - "query": "玩具", | ||
| 197 | - "filters": {"category.keyword": "玩具"}, | ||
| 198 | - "range_filters": {"price": {"gte": 50, "lte": 200}}, | ||
| 199 | - "facets": ["vendor.keyword"], | ||
| 200 | - "sort_by": "min_price", | ||
| 201 | - "sort_order": "asc" | ||
| 202 | -}).json() | 281 | +result = requests.post( |
| 282 | + 'http://localhost:6002/search/', | ||
| 283 | + headers={'X-Tenant-ID': '2'}, | ||
| 284 | + json={ | ||
| 285 | + "query": "手机", | ||
| 286 | + "language": "zh", | ||
| 287 | + "filters": {"category_name": "手机"}, | ||
| 288 | + "range_filters": {"min_price": {"gte": 50, "lte": 200}}, | ||
| 289 | + "facets": ["category1_name", "specifications"], | ||
| 290 | + "sort_by": "min_price", | ||
| 291 | + "sort_order": "asc" | ||
| 292 | + } | ||
| 293 | +).json() | ||
| 203 | 294 | ||
| 204 | print(f"找到 {result['total']} 个结果") | 295 | print(f"找到 {result['total']} 个结果") |
| 205 | ``` | 296 | ``` |
| @@ -211,12 +302,16 @@ print(f"找到 {result['total']} 个结果") | @@ -211,12 +302,16 @@ print(f"找到 {result['total']} 个结果") | ||
| 211 | ```javascript | 302 | ```javascript |
| 212 | const result = await fetch('http://localhost:6002/search/', { | 303 | const result = await fetch('http://localhost:6002/search/', { |
| 213 | method: 'POST', | 304 | method: 'POST', |
| 214 | - headers: {'Content-Type': 'application/json'}, | 305 | + headers: { |
| 306 | + 'Content-Type': 'application/json', | ||
| 307 | + 'X-Tenant-ID': '2' | ||
| 308 | + }, | ||
| 215 | body: JSON.stringify({ | 309 | body: JSON.stringify({ |
| 216 | - query: "玩具", | ||
| 217 | - filters: {category.keyword: "玩具"}, | ||
| 218 | - range_filters: {price: {gte: 50, lte: 200}}, | ||
| 219 | - facets: ["vendor.keyword"], | 310 | + query: "手机", |
| 311 | + language: "zh", | ||
| 312 | + filters: {category_name: "手机"}, | ||
| 313 | + range_filters: {min_price: {gte: 50, lte: 200}}, | ||
| 314 | + facets: ["category1_name", "specifications"], | ||
| 220 | sort_by: "min_price", | 315 | sort_by: "min_price", |
| 221 | sort_order: "asc" | 316 | sort_order: "asc" |
| 222 | }) | 317 | }) |
docs/系统设计文档.md
| @@ -215,7 +215,7 @@ indexes: | @@ -215,7 +215,7 @@ indexes: | ||
| 215 | 4. 组合多个语言查询的结果,提高召回率 | 215 | 4. 组合多个语言查询的结果,提高召回率 |
| 216 | 216 | ||
| 217 | **实现模块**: | 217 | **实现模块**: |
| 218 | -- `search/multilang_query_builder.py` - 多语言查询构建器 | 218 | +- `search/es_query_builder.py` - ES 查询构建器(单层架构) |
| 219 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) | 219 | - `query/query_parser.py` - 查询解析器(支持语言检测和翻译) |
| 220 | 220 | ||
| 221 | --- | 221 | --- |
| @@ -345,7 +345,7 @@ query_config: | @@ -345,7 +345,7 @@ query_config: | ||
| 345 | 345 | ||
| 346 | #### 工作流程 | 346 | #### 工作流程 |
| 347 | ``` | 347 | ``` |
| 348 | -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建 | 348 | +查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) |
| 349 | ``` | 349 | ``` |
| 350 | 350 | ||
| 351 | #### 实现模块 | 351 | #### 实现模块 |
| @@ -421,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap | @@ -421,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap | ||
| 421 | - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) | 421 | - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) |
| 422 | - 检测查询语言 | 422 | - 检测查询语言 |
| 423 | - 生成翻译 | 423 | - 生成翻译 |
| 424 | -2. **多语言查询构建**: | ||
| 425 | - - 如果域有 `language_field_mapping`: | ||
| 426 | - - 使用检测到的语言查询对应字段(boost * 1.5) | ||
| 427 | - - 使用翻译后的查询搜索其他语言字段(boost * 1.0) | ||
| 428 | - - 如果域没有 `language_field_mapping`: | ||
| 429 | - - 使用所有字段进行搜索 | ||
| 430 | -3. **查询组合**: | ||
| 431 | - - 多个语言查询组合为 `should` 子句 | ||
| 432 | - - 提高召回率 | ||
| 433 | - | ||
| 434 | -#### 示例 | ||
| 435 | -``` | ||
| 436 | -查询: "芭比娃娃" | ||
| 437 | -域: default | ||
| 438 | -检测语言: zh | ||
| 439 | - | ||
| 440 | -生成的查询: | ||
| 441 | -- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5) | ||
| 442 | -- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0) | ||
| 443 | -- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0) | 424 | +2. **查询构建**(简化架构): |
| 425 | + - **结构**: `filters AND (text_recall OR embedding_recall)` | ||
| 426 | + - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) | ||
| 427 | + - **text_recall**: 文本相关性召回 | ||
| 428 | + - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`) | ||
| 429 | + - 使用 `multi_match` 查询,支持字段 boost | ||
| 430 | + - **embedding_recall**: 向量召回(KNN) | ||
| 431 | + - 使用 `title_embedding` 字段进行 KNN 搜索 | ||
| 432 | + - ES 自动与文本召回合并 | ||
| 433 | + - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) | ||
| 434 | + | ||
| 435 | +#### 查询结构示例 | ||
| 436 | +```json | ||
| 437 | +{ | ||
| 438 | + "query": { | ||
| 439 | + "bool": { | ||
| 440 | + "must": [ | ||
| 441 | + { | ||
| 442 | + "function_score": { | ||
| 443 | + "query": { | ||
| 444 | + "multi_match": { | ||
| 445 | + "query": "手机", | ||
| 446 | + "fields": [ | ||
| 447 | + "title_zh^3.0", "title_en^3.0", | ||
| 448 | + "brief_zh^1.5", "brief_en^1.5", | ||
| 449 | + ... | ||
| 450 | + ] | ||
| 451 | + } | ||
| 452 | + }, | ||
| 453 | + "functions": [...] | ||
| 454 | + } | ||
| 455 | + } | ||
| 456 | + ], | ||
| 457 | + "filter": [ | ||
| 458 | + {"term": {"tenant_id": "2"}}, | ||
| 459 | + {"term": {"category_name": "手机"}} | ||
| 460 | + ] | ||
| 461 | + } | ||
| 462 | + }, | ||
| 463 | + "knn": { | ||
| 464 | + "field": "title_embedding", | ||
| 465 | + "query_vector": [...], | ||
| 466 | + "k": 50, | ||
| 467 | + "boost": 0.2 | ||
| 468 | + } | ||
| 469 | +} | ||
| 444 | ``` | 470 | ``` |
| 445 | 471 | ||
| 446 | #### 实现模块 | 472 | #### 实现模块 |
| 447 | -- `search/multilang_query_builder.py` - 多语言查询构建器 | ||
| 448 | -- `search/searcher.py` - 搜索器(使用多语言构建器) | 473 | +- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) |
| 474 | +- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) | ||
| 449 | 475 | ||
| 450 | ### 5.3 相关性计算(Ranking) | 476 | ### 5.3 相关性计算(Ranking) |
| 451 | 477 |
docs/索引字段说明v2.md
| 1 | -SPU-SKU索引方案选型 | ||
| 2 | -1. spu为单位。SKU字段展开作为SPU属性 | ||
| 3 | -1.1 索引方案 | ||
| 4 | -除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有款式、价格、重量、库存等相关属性。所以,可以以spu为单位建立索引。 | ||
| 5 | -sku中需要参与搜索的属性(比如价格、库存)展开到spu。 | ||
| 6 | -sku的所有需要返回的字段作为nested字段,仅用于返回。 | ||
| 7 | -# 写入 spu 级别索引 | ||
| 8 | -def build_product_document(product, variants): | ||
| 9 | - return { | ||
| 10 | - "spu_id": str(product.id), | ||
| 11 | - "title": product.title, | ||
| 12 | - | ||
| 13 | - # Variant搜索字段(展开) | ||
| 14 | - # 价格(int)、重量(int)、重量单位拼接重量(keyword),都以list形式灌入 | ||
| 15 | - # TODO 按要求补充 | ||
| 16 | - | ||
| 17 | - # 库存总和 将sku的库存加起来作为一个值灌入 | ||
| 18 | - # 售价,灌入3个字段,一个 sku价格 以list形式灌入,一个最高价一个最低价 | ||
| 19 | - # TODO 按要求补充 | ||
| 20 | - | ||
| 21 | - # Variant详细信息(用于返回) | ||
| 22 | - "variants": [ | ||
| 23 | - { | ||
| 24 | - "sku_id": str(v.id), | ||
| 25 | - "price": float(v.price), | ||
| 26 | - "options": v.options | ||
| 27 | - } | ||
| 28 | - for v in variants | ||
| 29 | - ], | 1 | +# 索引字段说明 v2 |
| 2 | + | ||
| 3 | +本文档详细说明 `search_products` 索引的字段结构、类型、数据来源和用途。 | ||
| 4 | + | ||
| 5 | +## 索引概述 | ||
| 6 | + | ||
| 7 | +- **索引名称**: `search_products` | ||
| 8 | +- **索引维度**: SPU(Standard Product Unit)级别 | ||
| 9 | +- **多租户隔离**: 通过 `tenant_id` 字段实现 | ||
| 10 | +- **Mapping 文件**: `mappings/search_products.json` | ||
| 11 | + | ||
| 12 | +## 字段分类 | ||
| 13 | + | ||
| 14 | +### 1. 基础标识字段 | ||
| 15 | + | ||
| 16 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 17 | +|--------|--------|------|----------| | ||
| 18 | +| `tenant_id` | keyword | 租户ID,用于多租户隔离 | MySQL: `shoplazza_product_spu.tenant_id` | | ||
| 19 | +| `spu_id` | keyword | SPU唯一标识 | MySQL: `shoplazza_product_spu.id` | | ||
| 20 | +| `create_time` | date | 创建时间 | MySQL: `shoplazza_product_spu.created_at` | | ||
| 21 | +| `update_time` | date | 更新时间 | MySQL: `shoplazza_product_spu.updated_at` | | ||
| 22 | + | ||
| 23 | +### 2. 多语言文本字段 | ||
| 24 | + | ||
| 25 | +所有文本字段都支持中英文双语,后端根据请求的 `language` 参数自动选择对应语言字段返回。 | ||
| 26 | + | ||
| 27 | +#### 2.1 标题字段 | ||
| 28 | + | ||
| 29 | +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | | ||
| 30 | +|--------|--------|--------|------|----------| | ||
| 31 | +| `title_zh` | text | hanlp_index / hanlp_standard | 中文标题 | MySQL: `shoplazza_product_spu.title` | | ||
| 32 | +| `title_en` | text | english | 英文标题 | 暂为空(待翻译服务填充) | | ||
| 33 | + | ||
| 34 | +#### 2.2 描述字段 | ||
| 35 | + | ||
| 36 | +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | | ||
| 37 | +|--------|--------|--------|------|----------| | ||
| 38 | +| `brief_zh` | text | hanlp_index / hanlp_standard | 中文短描述 | MySQL: `shoplazza_product_spu.brief` | | ||
| 39 | +| `brief_en` | text | english | 英文短描述 | 暂为空 | | ||
| 40 | +| `description_zh` | text | hanlp_index / hanlp_standard | 中文详细描述 | MySQL: `shoplazza_product_spu.description` | | ||
| 41 | +| `description_en` | text | english | 英文详细描述 | 暂为空 | | ||
| 42 | + | ||
| 43 | +#### 2.3 供应商/品牌字段 | ||
| 44 | + | ||
| 45 | +| 字段名 | ES类型 | 分析器 | 子字段 | 说明 | 数据来源 | | ||
| 46 | +|--------|--------|--------|--------|------|----------| | ||
| 47 | +| `vendor_zh` | text | hanlp_index / hanlp_standard | `vendor_zh.keyword` (keyword, normalizer: lowercase) | 中文供应商/品牌 | MySQL: `shoplazza_product_spu.vendor` | | ||
| 48 | +| `vendor_en` | text | english | `vendor_en.keyword` (keyword, normalizer: lowercase) | 英文供应商/品牌 | 暂为空 | | ||
| 49 | + | ||
| 50 | +**用途**: | ||
| 51 | +- `text` 类型:用于全文搜索(支持模糊匹配) | ||
| 52 | +- `keyword` 子字段:用于精确匹配过滤和分面聚合 | ||
| 53 | + | ||
| 54 | +### 3. 标签字段 | ||
| 55 | + | ||
| 56 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 57 | +|--------|--------|------|----------| | ||
| 58 | +| `tags` | keyword | 标签列表(数组) | MySQL: `shoplazza_product_spu.tags`(逗号分隔字符串,转换为数组) | | ||
| 59 | + | ||
| 60 | +**数据格式**: `["新品", "热卖", "爆款"]` | ||
| 61 | + | ||
| 62 | +### 4. 类目字段 | ||
| 63 | + | ||
| 64 | +#### 4.1 类目路径(用于搜索) | ||
| 65 | + | ||
| 66 | +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | | ||
| 67 | +|--------|--------|--------|------|----------| | ||
| 68 | +| `category_path_zh` | text | hanlp_index / hanlp_standard | 中文类目路径(如"服装/男装/衬衫") | MySQL: `shoplazza_product_spu.category_path` | | ||
| 69 | +| `category_path_en` | text | english | 英文类目路径 | 暂为空 | | ||
| 70 | + | ||
| 71 | +#### 4.2 类目名称(用于搜索) | ||
| 72 | + | ||
| 73 | +| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 | | ||
| 74 | +|--------|--------|--------|------|----------| | ||
| 75 | +| `category_name_zh` | text | hanlp_index / hanlp_standard | 中文类目名称 | MySQL: `shoplazza_product_spu.category` | | ||
| 76 | +| `category_name_en` | text | english | 英文类目名称 | 暂为空 | | ||
| 77 | + | ||
| 78 | +#### 4.3 类目标识(用于过滤和分面) | ||
| 79 | + | ||
| 80 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 81 | +|--------|--------|------|----------| | ||
| 82 | +| `category_id` | keyword | 类目ID | MySQL: `shoplazza_product_spu.category_id` | | ||
| 83 | +| `category_name` | keyword | 类目名称(用于过滤) | MySQL: `shoplazza_product_spu.category` | | ||
| 84 | +| `category_level` | integer | 类目层级(1/2/3) | MySQL: `shoplazza_product_spu.category_level` | | ||
| 85 | +| `category1_name` | keyword | 一级类目名称 | 从 `category_path` 解析 | | ||
| 86 | +| `category2_name` | keyword | 二级类目名称 | 从 `category_path` 解析 | | ||
| 87 | +| `category3_name` | keyword | 三级类目名称 | 从 `category_path` 解析 | | ||
| 88 | + | ||
| 89 | +**用途**: | ||
| 90 | +- `category_path_zh/en`, `category_name_zh/en`: 用于全文搜索,支持模糊匹配 | ||
| 91 | +- `category_id`, `category_name`, `category_level`, `category1/2/3_name`: 用于精确过滤和分面聚合 | ||
| 92 | + | ||
| 93 | +### 5. 规格字段(Specifications) | ||
| 30 | 94 | ||
| 31 | - | ||
| 32 | - "min_price": min(v.price for v in variants), | ||
| 33 | - "max_price": max(v.price for v in variants) | 95 | +| 字段名 | ES类型 | 说明 | 数据来源 | |
| 96 | +|--------|--------|------|----------| | ||
| 97 | +| `specifications` | nested | 规格列表(嵌套对象数组) | MySQL: `shoplazza_product_option` + `shoplazza_product_sku.option1/2/3` | | ||
| 98 | + | ||
| 99 | +**嵌套结构**: | ||
| 100 | +```json | ||
| 101 | +{ | ||
| 102 | + "specifications": [ | ||
| 103 | + { | ||
| 104 | + "sku_id": "sku_123", | ||
| 105 | + "name": "color", | ||
| 106 | + "value": "white" | ||
| 107 | + }, | ||
| 108 | + { | ||
| 109 | + "sku_id": "sku_123", | ||
| 110 | + "name": "size", | ||
| 111 | + "value": "256GB" | ||
| 34 | } | 112 | } |
| 35 | -1.2 查询方案 | ||
| 36 | -对数组字段使用 dis_max,只取最高分,避免累加。 | ||
| 37 | -其他重点字段 | ||
| 38 | -1. Sku title | ||
| 39 | -2. category | ||
| 40 | -2.1 Mysql | ||
| 41 | -在spu表中: | ||
| 42 | -Field Type | ||
| 43 | -category varchar(255) | ||
| 44 | -category_id bigint(20) | ||
| 45 | -category_google_id bigint(20) | ||
| 46 | -category_level int(11) | ||
| 47 | -category_path varchar(500) | ||
| 48 | -2.2 ES索引 | ||
| 49 | -2.2.1 输入数据 | ||
| 50 | - 设计 1,2,3级分类 三个字段,的 category (原始文本) | ||
| 51 | -2.2.2 索引方法 | ||
| 52 | - 设计要求: | ||
| 53 | - 1. 支持facet(精确过滤、keyword聚合),并且性能需要足够高。 | ||
| 54 | - 2. 支持普通搜索模糊匹配(用户原始query可能包括分类词)。 | ||
| 55 | - 3. 模糊匹配要考虑多语言 | ||
| 56 | -方案:采用方案2 | ||
| 57 | - 1. categoryPath索引 + Prefix 查询(categoryPath.keyword: "服装/男装")(如果满足条件的key太多的则性能较差,比如 查询的是一级类目,类目树叶子节点太多时性能较差) | ||
| 58 | - 2. categoryPath支撑模糊查询 和 多级cate keyword索引支撑精确查询。 索引阶段冗余,查询性能高。 | ||
| 59 | - "category_path_zh": { // 提供模糊查询功能,辅助相关性计算 | ||
| 60 | - "type": "text", | ||
| 61 | - "analyzer": "hanlp_index", | ||
| 62 | - "search_analyzer": "hanlp_standard" | ||
| 63 | - }, | ||
| 64 | - "category_path_en": { // 提供模糊查询功能,辅助相关性计算 | ||
| 65 | - "type": "text", | ||
| 66 | - "analyzer": "english", | ||
| 67 | - "search_analyzer": "english" | ||
| 68 | - }, | ||
| 69 | - "category_path": { // 用于多层级的筛选、精确匹配 | ||
| 70 | - "type": "keyword", | ||
| 71 | - "normalizer": "lowercase" | ||
| 72 | - }, | ||
| 73 | - "category_id": { | ||
| 74 | - "type": "keyword" | ||
| 75 | - }, | ||
| 76 | - "category_name": { | ||
| 77 | - "type": "keyword" | ||
| 78 | - }, | ||
| 79 | - "category_level": { | ||
| 80 | - "type": "integer" | ||
| 81 | - }, | ||
| 82 | - "category1_name": { // 不同层级下 可能有同名的情况,因此提供一二三级分开的查询方式 | ||
| 83 | - "type": "keyword" | ||
| 84 | - }, | ||
| 85 | - "category2_name": { | ||
| 86 | - "type": "keyword" | ||
| 87 | - }, | ||
| 88 | - "category3_name": { | ||
| 89 | - "type": "keyword" | ||
| 90 | - }, | ||
| 91 | - | ||
| 92 | -3. tags | ||
| 93 | -3.1 数据源 | ||
| 94 | -多值 | ||
| 95 | -标签 | ||
| 96 | -最多输入250个标签,每个不得超过500字符,多个标签请用「英文逗号」隔开 | ||
| 97 | -新品,热卖,爆款 | ||
| 98 | -耳机,头戴式,爆款 | ||
| 99 | - | ||
| 100 | -分割后 list形式灌入 | ||
| 101 | -3.2 Mysql | ||
| 102 | -3.3 ES索引 | ||
| 103 | -3.3.1 输入数据 | ||
| 104 | -3.3.2 索引方法 | ||
| 105 | -4. 供应商 | ||
| 106 | -4.1 数据源 | ||
| 107 | -4.2 Mysql | ||
| 108 | -4.3 ES索引 | ||
| 109 | -4.3.1 输入数据 | ||
| 110 | -4.3.2 索引方法 | ||
| 111 | -5. 款式/选项值(options) | ||
| 112 | -5.1 数据源 | ||
| 113 | -以下区域字段,商品属性为M(商品主体)的行需填写款式名称,商品属性为P(子款式)的行需填写款式值信息,商品属性为S(单一款式商品)的行无需填写 | ||
| 114 | -款式1 款式2 款式3 | ||
| 115 | -最多255字符 最多255字符 最多255字符 | ||
| 116 | -SIZE COLOR | ||
| 117 | -S red | ||
| 118 | -... | ||
| 119 | -5.2 Mysql | ||
| 120 | -1. API 在 SPU 的维度直接返回3个属性定义,存储在 shoplazza_product_option 中: | ||
| 121 | -1. API在 SKU的维度直接返回3个属性值,存储在 shoplazza_product_sku 表的 option 相关的字段中: | ||
| 122 | -5.3 ES索引 | ||
| 123 | - | ||
| 124 | - "specifications": { | ||
| 125 | - "type": "nested", | ||
| 126 | - "properties": { | ||
| 127 | - "name": { "type": "keyword" }, // "颜色", "容量" | ||
| 128 | - "value": { "type": "keyword" } // "白色", "256GB" | 113 | + ] |
| 114 | +} | ||
| 115 | +``` | ||
| 116 | + | ||
| 117 | +**数据来源**: | ||
| 118 | +- `name`: 从 `shoplazza_product_option` 表获取(选项名称,如"color"、"size") | ||
| 119 | +- `value`: 从 `shoplazza_product_sku` 表的 `option1`, `option2`, `option3` 字段获取(选项值,如"white"、"256GB") | ||
| 120 | +- `sku_id`: SKU ID,用于关联 | ||
| 121 | + | ||
| 122 | +**API 过滤示例**: | ||
| 123 | +```json | ||
| 124 | +{ | ||
| 125 | + "query": "手机", | ||
| 126 | + "filters": { | ||
| 127 | + "specifications": { | ||
| 128 | + "name": "color", | ||
| 129 | + "value": "white" | ||
| 129 | } | 130 | } |
| 130 | - }, | ||
| 131 | - | ||
| 132 | - 另外还需要包含一个单独的字段,main_option (即店铺主题装修里面配置的 颜色切换 - 变体名称,也就是列表页商品的子sku显示维度) | ||
| 133 | - "main_option": { "type": "keyword" } | ||
| 134 | -查询指定款式 | 131 | + } |
| 132 | +} | ||
| 133 | +``` | ||
| 134 | + | ||
| 135 | +**多个规格过滤(OR逻辑)**: | ||
| 136 | +```json | ||
| 135 | { | 137 | { |
| 136 | - "query": { | ||
| 137 | - "nested": { | ||
| 138 | - "path": "specifications", | ||
| 139 | - "query": { | ||
| 140 | - "bool": { | ||
| 141 | - "must": [ | ||
| 142 | - { "term": { "specifications.name ": "颜色" } }, | ||
| 143 | - { "term": { "specifications.value": "绿色" } } | ||
| 144 | - ] | ||
| 145 | - } | 138 | + "query": "手机", |
| 139 | + "filters": { | ||
| 140 | + "specifications": [ | ||
| 141 | + {"name": "color", "value": "white"}, | ||
| 142 | + {"name": "size", "value": "256GB"} | ||
| 143 | + ] | ||
| 144 | + } | ||
| 145 | +} | ||
| 146 | +``` | ||
| 147 | + | ||
| 148 | +**ES 查询结构**(后端自动生成): | ||
| 149 | +```json | ||
| 150 | +{ | ||
| 151 | + "nested": { | ||
| 152 | + "path": "specifications", | ||
| 153 | + "query": { | ||
| 154 | + "bool": { | ||
| 155 | + "must": [ | ||
| 156 | + { "term": { "specifications.name": "color" } }, | ||
| 157 | + { "term": { "specifications.value": "white" } } | ||
| 158 | + ] | ||
| 146 | } | 159 | } |
| 147 | } | 160 | } |
| 148 | } | 161 | } |
| 149 | } | 162 | } |
| 150 | -按 name 做分面搜索(聚合) | ||
| 151 | - | 163 | +``` |
| 164 | + | ||
| 165 | +**API 分面示例**: | ||
| 166 | + | ||
| 167 | +所有规格名称的分面: | ||
| 168 | +```json | ||
| 169 | +{ | ||
| 170 | + "query": "手机", | ||
| 171 | + "facets": ["specifications"] | ||
| 172 | +} | ||
| 173 | +``` | ||
| 174 | + | ||
| 175 | +指定规格名称的分面: | ||
| 176 | +```json | ||
| 177 | +{ | ||
| 178 | + "query": "手机", | ||
| 179 | + "facets": ["specifications.color", "specifications.size"] | ||
| 180 | +} | ||
| 181 | +``` | ||
| 182 | + | ||
| 183 | +**ES 聚合结构**(后端自动生成): | ||
| 184 | + | ||
| 185 | +所有规格名称: | ||
| 186 | +```json | ||
| 152 | { | 187 | { |
| 153 | "aggs": { | 188 | "aggs": { |
| 154 | - "specs": { | 189 | + "specifications_facet": { |
| 155 | "nested": { "path": "specifications" }, | 190 | "nested": { "path": "specifications" }, |
| 156 | "aggs": { | 191 | "aggs": { |
| 157 | "by_name": { | 192 | "by_name": { |
| 158 | - "terms": { | ||
| 159 | - "field": "specifications.name", | ||
| 160 | - "size": 20 | ||
| 161 | - }, | 193 | + "terms": { "field": "specifications.name", "size": 20 }, |
| 162 | "aggs": { | 194 | "aggs": { |
| 163 | "value_counts": { | 195 | "value_counts": { |
| 164 | - "terms": { | ||
| 165 | - "field": "specifications.value", | ||
| 166 | - "size": 10 | ||
| 167 | - } | 196 | + "terms": { "field": "specifications.value", "size": 10 } |
| 168 | } | 197 | } |
| 169 | } | 198 | } |
| 170 | } | 199 | } |
| @@ -172,4 +201,204 @@ S red | @@ -172,4 +201,204 @@ S red | ||
| 172 | } | 201 | } |
| 173 | } | 202 | } |
| 174 | } | 203 | } |
| 175 | - | ||
| 176 | \ No newline at end of file | 204 | \ No newline at end of file |
| 205 | +``` | ||
| 206 | + | ||
| 207 | +指定规格名称: | ||
| 208 | +```json | ||
| 209 | +{ | ||
| 210 | + "aggs": { | ||
| 211 | + "specifications_color_facet": { | ||
| 212 | + "nested": { "path": "specifications" }, | ||
| 213 | + "aggs": { | ||
| 214 | + "filter_by_name": { | ||
| 215 | + "filter": { "term": { "specifications.name": "color" } }, | ||
| 216 | + "aggs": { | ||
| 217 | + "value_counts": { | ||
| 218 | + "terms": { "field": "specifications.value", "size": 10 } | ||
| 219 | + } | ||
| 220 | + } | ||
| 221 | + } | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | +} | ||
| 226 | +``` | ||
| 227 | + | ||
| 228 | +### 6. 选项名称字段 | ||
| 229 | + | ||
| 230 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 231 | +|--------|--------|------|----------| | ||
| 232 | +| `option1_name` | keyword | 选项1名称(如"color") | MySQL: `shoplazza_product_option` | | ||
| 233 | +| `option2_name` | keyword | 选项2名称(如"size") | MySQL: `shoplazza_product_option` | | ||
| 234 | +| `option3_name` | keyword | 选项3名称 | MySQL: `shoplazza_product_option` | | ||
| 235 | + | ||
| 236 | +### 7. 价格字段 | ||
| 237 | + | ||
| 238 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 239 | +|--------|--------|------|----------| | ||
| 240 | +| `min_price` | float | 最低价格 | 从所有 SKU 价格计算 | | ||
| 241 | +| `max_price` | float | 最高价格 | 从所有 SKU 价格计算 | | ||
| 242 | +| `compare_at_price` | float | 原价/对比价 | MySQL: `shoplazza_product_spu.compare_at_price` | | ||
| 243 | +| `sku_prices` | float | 所有 SKU 价格列表(数组) | 从所有 SKU 价格汇总 | | ||
| 244 | + | ||
| 245 | +### 8. 重量字段 | ||
| 246 | + | ||
| 247 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 248 | +|--------|--------|------|----------| | ||
| 249 | +| `sku_weights` | long | 所有 SKU 重量列表(数组) | 从所有 SKU 重量汇总 | | ||
| 250 | +| `sku_weight_units` | keyword | 所有 SKU 重量单位列表(数组) | 从所有 SKU 重量单位汇总 | | ||
| 251 | + | ||
| 252 | +### 9. 库存字段 | ||
| 253 | + | ||
| 254 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 255 | +|--------|--------|------|----------| | ||
| 256 | +| `total_inventory` | long | 总库存(所有 SKU 库存之和) | 从所有 SKU 库存汇总 | | ||
| 257 | + | ||
| 258 | +### 10. SKU 嵌套字段 | ||
| 259 | + | ||
| 260 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 261 | +|--------|--------|------|----------| | ||
| 262 | +| `skus` | nested | SKU 详细信息列表(嵌套对象数组) | MySQL: `shoplazza_product_sku` | | ||
| 263 | + | ||
| 264 | +**嵌套结构**: | ||
| 265 | +```json | ||
| 266 | +{ | ||
| 267 | + "skus": [ | ||
| 268 | + { | ||
| 269 | + "sku_id": "sku_123", | ||
| 270 | + "price": 99.99, | ||
| 271 | + "compare_at_price": 149.99, | ||
| 272 | + "sku_code": "SKU001", | ||
| 273 | + "stock": 100, | ||
| 274 | + "weight": 0.5, | ||
| 275 | + "weight_unit": "kg", | ||
| 276 | + "option1_value": "white", | ||
| 277 | + "option2_value": "256GB", | ||
| 278 | + "option3_value": null, | ||
| 279 | + "image_src": "https://example.com/image.jpg" | ||
| 280 | + } | ||
| 281 | + ] | ||
| 282 | +} | ||
| 283 | +``` | ||
| 284 | + | ||
| 285 | +**字段说明**: | ||
| 286 | +- `sku_id`: SKU 唯一标识 | ||
| 287 | +- `price`: SKU 价格 | ||
| 288 | +- `compare_at_price`: SKU 原价 | ||
| 289 | +- `sku_code`: SKU 编码 | ||
| 290 | +- `stock`: 库存数量 | ||
| 291 | +- `weight`: 重量 | ||
| 292 | +- `weight_unit`: 重量单位 | ||
| 293 | +- `option1_value`, `option2_value`, `option3_value`: 选项值(对应 `option1_name`, `option2_name`, `option3_name`) | ||
| 294 | +- `image_src`: SKU 图片地址(`index: false`,仅用于返回) | ||
| 295 | + | ||
| 296 | +### 11. 图片字段 | ||
| 297 | + | ||
| 298 | +| 字段名 | ES类型 | 说明 | 数据来源 | | ||
| 299 | +|--------|--------|------|----------| | ||
| 300 | +| `image_url` | keyword | 主图URL(`index: false`,仅用于返回) | MySQL: `shoplazza_product_spu.image_url` | | ||
| 301 | + | ||
| 302 | +### 12. 向量字段(不返回给前端) | ||
| 303 | + | ||
| 304 | +| 字段名 | ES类型 | 维度 | 说明 | 数据来源 | | ||
| 305 | +|--------|--------|------|------|----------| | ||
| 306 | +| `title_embedding` | dense_vector | 1024 | 标题向量(用于语义搜索) | 由 BGE-M3 模型生成 | | ||
| 307 | +| `image_embedding` | nested | - | 图片向量(用于图片搜索) | 由 CN-CLIP 模型生成 | | ||
| 308 | + | ||
| 309 | +**注意**: 这些字段仅用于搜索,不会返回给前端。 | ||
| 310 | + | ||
| 311 | +## 字段用途总结 | ||
| 312 | + | ||
| 313 | +### 搜索字段(参与相关性计算) | ||
| 314 | + | ||
| 315 | +- `title_zh`, `title_en` (boost: 3.0) | ||
| 316 | +- `brief_zh`, `brief_en` (boost: 1.5) | ||
| 317 | +- `description_zh`, `description_en` (boost: 1.0) | ||
| 318 | +- `vendor_zh`, `vendor_en` (boost: 1.5) | ||
| 319 | +- `tags` (boost: 1.0) | ||
| 320 | +- `category_path_zh`, `category_path_en` (boost: 1.5) | ||
| 321 | +- `category_name_zh`, `category_name_en` (boost: 1.5) | ||
| 322 | +- `title_embedding` (向量召回,boost: 0.2) | ||
| 323 | + | ||
| 324 | +### 过滤字段(精确匹配) | ||
| 325 | + | ||
| 326 | +- `tenant_id` (必需,多租户隔离) | ||
| 327 | +- `category_id`, `category_name`, `category1_name`, `category2_name`, `category3_name` | ||
| 328 | +- `vendor_zh.keyword`, `vendor_en.keyword` | ||
| 329 | +- `specifications` (嵌套查询) | ||
| 330 | +- `min_price`, `max_price` (范围过滤) | ||
| 331 | + | ||
| 332 | +### 分面字段(聚合统计) | ||
| 333 | + | ||
| 334 | +- `category1_name`, `category2_name`, `category3_name` | ||
| 335 | +- `specifications` (所有规格名称的分面,嵌套聚合,按 name 分组,然后按 value 聚合) | ||
| 336 | +- `specifications.{name}` (指定规格名称的分面,如 `specifications.color`,只返回该 name 的 value 列表) | ||
| 337 | + | ||
| 338 | +### 返回字段(前端展示) | ||
| 339 | + | ||
| 340 | +除 `title_embedding` 和 `image_embedding` 外,所有字段都会根据 `language` 参数自动选择对应的中英文字段返回。 | ||
| 341 | + | ||
| 342 | +## 数据映射规则 | ||
| 343 | + | ||
| 344 | +### 多语言字段映射 | ||
| 345 | + | ||
| 346 | +后端根据请求的 `language` 参数(`zh` 或 `en`)自动选择: | ||
| 347 | + | ||
| 348 | +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 | ||
| 349 | +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 | ||
| 350 | + | ||
| 351 | +映射到前端字段: | ||
| 352 | +- `title_zh/en` → `title` | ||
| 353 | +- `brief_zh/en` → `brief` | ||
| 354 | +- `description_zh/en` → `description` | ||
| 355 | +- `vendor_zh/en` → `vendor` | ||
| 356 | +- `category_path_zh/en` → `category_path` | ||
| 357 | +- `category_name_zh/en` → `category_name` | ||
| 358 | + | ||
| 359 | +### 规格数据构建 | ||
| 360 | + | ||
| 361 | +1. 从 `shoplazza_product_option` 表获取选项名称(`option1_name`, `option2_name`, `option3_name`) | ||
| 362 | +2. 从 `shoplazza_product_sku` 表获取选项值(`option1`, `option2`, `option3`) | ||
| 363 | +3. 将每个 SKU 的选项组合构建为 `specifications` 数组: | ||
| 364 | + ```python | ||
| 365 | + for sku in skus: | ||
| 366 | + if sku.option1 and option1_name: | ||
| 367 | + specifications.append({ | ||
| 368 | + "sku_id": sku.id, | ||
| 369 | + "name": option1_name, # 如"color" | ||
| 370 | + "value": sku.option1 # 如"white" | ||
| 371 | + }) | ||
| 372 | + # 同样处理 option2, option3 | ||
| 373 | + ``` | ||
| 374 | + | ||
| 375 | +## 查询架构 | ||
| 376 | + | ||
| 377 | +### 查询结构 | ||
| 378 | + | ||
| 379 | +``` | ||
| 380 | +filters AND (text_recall OR embedding_recall) | ||
| 381 | +``` | ||
| 382 | + | ||
| 383 | +- **filters**: 前端传递的过滤条件(永远起作用) | ||
| 384 | +- **text_recall**: 文本相关性召回(同时搜索中英文字段) | ||
| 385 | +- **embedding_recall**: 向量召回(KNN) | ||
| 386 | +- **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) | ||
| 387 | + | ||
| 388 | +### 文本召回字段 | ||
| 389 | + | ||
| 390 | +默认同时搜索以下字段(中英文都包含): | ||
| 391 | +- `title_zh^3.0`, `title_en^3.0` | ||
| 392 | +- `brief_zh^1.5`, `brief_en^1.5` | ||
| 393 | +- `description_zh^1.0`, `description_en^1.0` | ||
| 394 | +- `vendor_zh^1.5`, `vendor_en^1.5` | ||
| 395 | +- `category_path_zh^1.5`, `category_path_en^1.5` | ||
| 396 | +- `category_name_zh^1.5`, `category_name_en^1.5` | ||
| 397 | +- `tags^1.0` | ||
| 398 | + | ||
| 399 | +## 注意事项 | ||
| 400 | + | ||
| 401 | +1. **索引维度**: 所有数据以 SPU 为单位索引,SKU 信息作为嵌套字段存储 | ||
| 402 | +2. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 | ||
| 403 | +3. **多语言支持**: 文本字段支持中英文,后端根据 `language` 参数自动选择 | ||
| 404 | +4. **规格分面**: `specifications` 使用嵌套聚合,按 `name` 分组,然后按 `value` 聚合 | ||
| 405 | +5. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不返回给前端 |
search/es_query_builder.py
| @@ -352,6 +352,57 @@ class ESQueryBuilder: | @@ -352,6 +352,57 @@ class ESQueryBuilder: | ||
| 352 | # 1. 处理精确匹配过滤 | 352 | # 1. 处理精确匹配过滤 |
| 353 | if filters: | 353 | if filters: |
| 354 | for field, value in filters.items(): | 354 | for field, value in filters.items(): |
| 355 | + # 特殊处理:specifications 嵌套过滤 | ||
| 356 | + if field == "specifications": | ||
| 357 | + if isinstance(value, dict): | ||
| 358 | + # 单个规格过滤:{"name": "color", "value": "green"} | ||
| 359 | + name = value.get("name") | ||
| 360 | + spec_value = value.get("value") | ||
| 361 | + if name and spec_value: | ||
| 362 | + filter_clauses.append({ | ||
| 363 | + "nested": { | ||
| 364 | + "path": "specifications", | ||
| 365 | + "query": { | ||
| 366 | + "bool": { | ||
| 367 | + "must": [ | ||
| 368 | + {"term": {"specifications.name": name}}, | ||
| 369 | + {"term": {"specifications.value": spec_value}} | ||
| 370 | + ] | ||
| 371 | + } | ||
| 372 | + } | ||
| 373 | + } | ||
| 374 | + }) | ||
| 375 | + elif isinstance(value, list): | ||
| 376 | + # 多个规格过滤(OR逻辑):[{"name": "color", "value": "green"}, ...] | ||
| 377 | + should_clauses = [] | ||
| 378 | + for spec in value: | ||
| 379 | + if isinstance(spec, dict): | ||
| 380 | + name = spec.get("name") | ||
| 381 | + spec_value = spec.get("value") | ||
| 382 | + if name and spec_value: | ||
| 383 | + should_clauses.append({ | ||
| 384 | + "nested": { | ||
| 385 | + "path": "specifications", | ||
| 386 | + "query": { | ||
| 387 | + "bool": { | ||
| 388 | + "must": [ | ||
| 389 | + {"term": {"specifications.name": name}}, | ||
| 390 | + {"term": {"specifications.value": spec_value}} | ||
| 391 | + ] | ||
| 392 | + } | ||
| 393 | + } | ||
| 394 | + } | ||
| 395 | + }) | ||
| 396 | + if should_clauses: | ||
| 397 | + filter_clauses.append({ | ||
| 398 | + "bool": { | ||
| 399 | + "should": should_clauses, | ||
| 400 | + "minimum_should_match": 1 | ||
| 401 | + } | ||
| 402 | + }) | ||
| 403 | + continue | ||
| 404 | + | ||
| 405 | + # 普通字段过滤 | ||
| 355 | if isinstance(value, list): | 406 | if isinstance(value, list): |
| 356 | # 多值匹配(OR) | 407 | # 多值匹配(OR) |
| 357 | filter_clauses.append({ | 408 | filter_clauses.append({ |
| @@ -486,34 +537,62 @@ class ESQueryBuilder: | @@ -486,34 +537,62 @@ class ESQueryBuilder: | ||
| 486 | 537 | ||
| 487 | for config in facet_configs: | 538 | for config in facet_configs: |
| 488 | # 特殊处理:specifications嵌套分面 | 539 | # 特殊处理:specifications嵌套分面 |
| 489 | - if isinstance(config, str) and config == "specifications": | ||
| 490 | - # 构建specifications嵌套分面(按name聚合,然后按value聚合) | ||
| 491 | - aggs["specifications_facet"] = { | ||
| 492 | - "nested": { | ||
| 493 | - "path": "specifications" | ||
| 494 | - }, | ||
| 495 | - "aggs": { | ||
| 496 | - "by_name": { | ||
| 497 | - "terms": { | ||
| 498 | - "field": "specifications.name", | ||
| 499 | - "size": 20, | ||
| 500 | - "order": {"_count": "desc"} | ||
| 501 | - }, | ||
| 502 | - "aggs": { | ||
| 503 | - "value_counts": { | ||
| 504 | - "terms": { | ||
| 505 | - "field": "specifications.value", | ||
| 506 | - "size": 10, | ||
| 507 | - "order": {"_count": "desc"} | 540 | + if isinstance(config, str): |
| 541 | + # 格式1: "specifications" - 返回所有name的分面 | ||
| 542 | + if config == "specifications": | ||
| 543 | + aggs["specifications_facet"] = { | ||
| 544 | + "nested": { | ||
| 545 | + "path": "specifications" | ||
| 546 | + }, | ||
| 547 | + "aggs": { | ||
| 548 | + "by_name": { | ||
| 549 | + "terms": { | ||
| 550 | + "field": "specifications.name", | ||
| 551 | + "size": 20, | ||
| 552 | + "order": {"_count": "desc"} | ||
| 553 | + }, | ||
| 554 | + "aggs": { | ||
| 555 | + "value_counts": { | ||
| 556 | + "terms": { | ||
| 557 | + "field": "specifications.value", | ||
| 558 | + "size": 10, | ||
| 559 | + "order": {"_count": "desc"} | ||
| 560 | + } | ||
| 508 | } | 561 | } |
| 509 | } | 562 | } |
| 510 | } | 563 | } |
| 511 | } | 564 | } |
| 512 | } | 565 | } |
| 513 | - } | ||
| 514 | - continue | 566 | + continue |
| 567 | + | ||
| 568 | + # 格式2: "specifications.color" 或 "specifications.颜色" - 只返回指定name的value列表 | ||
| 569 | + if config.startswith("specifications."): | ||
| 570 | + name = config[len("specifications."):] | ||
| 571 | + agg_name = f"specifications_{name}_facet" | ||
| 572 | + aggs[agg_name] = { | ||
| 573 | + "nested": { | ||
| 574 | + "path": "specifications" | ||
| 575 | + }, | ||
| 576 | + "aggs": { | ||
| 577 | + "filter_by_name": { | ||
| 578 | + "filter": { | ||
| 579 | + "term": {"specifications.name": name} | ||
| 580 | + }, | ||
| 581 | + "aggs": { | ||
| 582 | + "value_counts": { | ||
| 583 | + "terms": { | ||
| 584 | + "field": "specifications.value", | ||
| 585 | + "size": 10, | ||
| 586 | + "order": {"_count": "desc"} | ||
| 587 | + } | ||
| 588 | + } | ||
| 589 | + } | ||
| 590 | + } | ||
| 591 | + } | ||
| 592 | + } | ||
| 593 | + continue | ||
| 515 | 594 | ||
| 516 | - # 简单模式:只有字段名(字符串) | 595 | + # 简单模式:只有字段名(字符串,非specifications) |
| 517 | if isinstance(config, str): | 596 | if isinstance(config, str): |
| 518 | field = config | 597 | field = config |
| 519 | agg_name = f"{field}_facet" | 598 | agg_name = f"{field}_facet" |
| @@ -524,6 +603,7 @@ class ESQueryBuilder: | @@ -524,6 +603,7 @@ class ESQueryBuilder: | ||
| 524 | "order": {"_count": "desc"} | 603 | "order": {"_count": "desc"} |
| 525 | } | 604 | } |
| 526 | } | 605 | } |
| 606 | + continue | ||
| 527 | 607 | ||
| 528 | # 高级模式:FacetConfig 对象 | 608 | # 高级模式:FacetConfig 对象 |
| 529 | else: | 609 | else: |