Commit f7d3cf70b2b2a3d50e8e4c3ce5aceaad157d9daa

Authored by tangwang
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 查询结构
添加了两种分面模式的说明和示例
更新了“分面字段”说明,明确支持指定规格名称的分面
README.md
... ... @@ -54,11 +54,11 @@ curl -X POST http://localhost:6002/search/ \
54 54 |------|----------|----------|
55 55 | `环境配置说明.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 |
56 56 | `Usage-Guide.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 |
57   -| `基础配置指南.md` | 租户字段、索引域、排序表达式配置流程 | 新租户开通、配置变更 |
  57 +| `基础配置指南.md` | 统一硬编码配置说明、索引结构、查询配置 | 了解系统配置、修改配置 |
58 58 | `测试数据指南.md` | 两个租户的模拟/CSV 数据构造 & MySQL→ES 流程 | 数据准备、联调 |
59 59 | `测试Pipeline说明.md` | 测试流水线、CI 脚本、上下文说明 | 自动化测试、追踪流水线 |
60 60 | `系统设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 |
61   -| `索引字段说明.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 |
  61 +| `索引字段说明v2.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 |
62 62 | `搜索API对接指南.md` | REST API(文本/图片/管理)详解、示例、响应格式 | API 使用、测试 |
63 63 | `搜索API速查表.md` | 常用请求体、过滤器、分面速查表 | 支持团队快速查阅 |
64 64 | `Search-API-Examples.md` | Python/JS/cURL 端到端示例 | 客户工程、SDK 参考 |
... ... @@ -82,9 +82,11 @@ curl -X POST http://localhost:6002/search/ \
82 82 - API、分页、过滤、Facet、KNN 等:`搜索API对接指南.md`
83 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 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 79 None,
80   - description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)",
  80 + description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)。支持 specifications 嵌套过滤:{\"specifications\": {\"name\": \"color\", \"value\": \"green\"}} 或 [{\"name\": \"color\", \"value\": \"green\"}, ...]",
81 81 json_schema_extra={
82 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 116 # 分面搜索 - 简化接口
111 117 facets: Optional[List[Union[str, FacetConfig]]] = Field(
112 118 None,
113   - description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象",
  119 + description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象。支持 specifications 分面:\"specifications\"(所有name)或 \"specifications.color\"(指定name)",
114 120 json_schema_extra={
115 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 131 "type": "range",
124 132 "ranges": [
125 133 {"key": "0-50", "to": 50},
126 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 143 for field_name, agg_data in es_aggregations.items():
144 144 display_field = field_name[:-6] if field_name.endswith("_facet") else field_name
145 145  
146   - # 处理specifications嵌套分面
  146 + # 处理specifications嵌套分面(所有name)
147 147 if field_name == "specifications_facet" and 'by_name' in agg_data:
148 148 # specifications嵌套聚合:按name分组,每个name下有value_counts
149 149 by_name_agg = agg_data['by_name']
... ... @@ -174,6 +174,35 @@ class ResultFormatter:
174 174 facets.append(facet)
175 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 206 # Handle terms aggregation
178 207 if 'buckets' in agg_data:
179 208 values = []
... ...
docs/Search-API-Examples.md
... ... @@ -23,6 +23,7 @@
23 23 ```bash
24 24 curl -X POST "http://localhost:6002/search/" \
25 25 -H "Content-Type: application/json" \
  26 + -H "X-Tenant-ID: 2" \
26 27 -d '{
27 28 "query": "芭比娃娃"
28 29 }'
... ... @@ -48,8 +49,10 @@ curl -X POST "http://localhost:6002/search/" \
48 49 ```bash
49 50 curl -X POST "http://localhost:6002/search/" \
50 51 -H "Content-Type: application/json" \
  52 + -H "X-Tenant-ID: 2" \
51 53 -d '{
52   - "query": "玩具",
  54 + "query": "手机",
  55 + "language": "zh",
53 56 "size": 50
54 57 }'
55 58 ```
... ... @@ -60,8 +63,10 @@ curl -X POST "http://localhost:6002/search/" \
60 63 # 第1页(0-19)
61 64 curl -X POST "http://localhost:6002/search/" \
62 65 -H "Content-Type: application/json" \
  66 + -H "X-Tenant-ID: 2" \
63 67 -d '{
64   - "query": "玩具",
  68 + "query": "手机",
  69 + "language": "zh",
65 70 "size": 20,
66 71 "from": 0
67 72 }'
... ... @@ -69,8 +74,10 @@ curl -X POST "http://localhost:6002/search/" \
69 74 # 第2页(20-39)
70 75 curl -X POST "http://localhost:6002/search/" \
71 76 -H "Content-Type: application/json" \
  77 + -H "X-Tenant-ID: 2" \
72 78 -d '{
73   - "query": "玩具",
  79 + "query": "手机",
  80 + "language": "zh",
74 81 "size": 20,
75 82 "from": 20
76 83 }'
... ... @@ -87,10 +94,12 @@ curl -X POST "http://localhost:6002/search/" \
87 94 ```bash
88 95 curl -X POST "http://localhost:6002/search/" \
89 96 -H "Content-Type: application/json" \
  97 + -H "X-Tenant-ID: 2" \
90 98 -d '{
91   - "query": "玩具",
  99 + "query": "手机",
  100 + "language": "zh",
92 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 109 ```bash
101 110 curl -X POST "http://localhost:6002/search/" \
102 111 -H "Content-Type: application/json" \
  112 + -H "X-Tenant-ID: 2" \
103 113 -d '{
104   - "query": "娃娃",
  114 + "query": "手机",
  115 + "language": "zh",
105 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 126 ```bash
116 127 curl -X POST "http://localhost:6002/search/" \
117 128 -H "Content-Type: application/json" \
  129 + -H "X-Tenant-ID: 2" \
118 130 -d '{
119   - "query": "娃娃",
  131 + "query": "手机",
  132 + "language": "zh",
120 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 207 ```bash
134 208 curl -X POST "http://localhost:6002/search/" \
135 209 -H "Content-Type: application/json" \
  210 + -H "X-Tenant-ID: 2" \
136 211 -d '{
137   - "query": "玩具",
  212 + "query": "手机",
  213 + "language": "zh",
138 214 "range_filters": {
139   - "price": {
  215 + "min_price": {
140 216 "gte": 50,
141 217 "lte": 200
142 218 }
... ... @@ -151,10 +227,12 @@ curl -X POST "http://localhost:6002/search/" \
151 227 ```bash
152 228 curl -X POST "http://localhost:6002/search/" \
153 229 -H "Content-Type: application/json" \
  230 + -H "X-Tenant-ID: 2" \
154 231 -d '{
155   - "query": "玩具",
  232 + "query": "手机",
  233 + "language": "zh",
156 234 "range_filters": {
157   - "price": {
  235 + "min_price": {
158 236 "gte": 100
159 237 }
160 238 }
... ... @@ -168,10 +246,12 @@ curl -X POST "http://localhost:6002/search/" \
168 246 ```bash
169 247 curl -X POST "http://localhost:6002/search/" \
170 248 -H "Content-Type: application/json" \
  249 + -H "X-Tenant-ID: 2" \
171 250 -d '{
172   - "query": "玩具",
  251 + "query": "手机",
  252 + "language": "zh",
173 253 "range_filters": {
174   - "price": {
  254 + "min_price": {
175 255 "lt": 50
176 256 }
177 257 }
... ... @@ -185,10 +265,12 @@ curl -X POST "http://localhost:6002/search/" \
185 265 ```bash
186 266 curl -X POST "http://localhost:6002/search/" \
187 267 -H "Content-Type: application/json" \
  268 + -H "X-Tenant-ID: 2" \
188 269 -d '{
189   - "query": "玩具",
  270 + "query": "手机",
  271 + "language": "zh",
190 272 "range_filters": {
191   - "price": {
  273 + "min_price": {
192 274 "gte": 50,
193 275 "lte": 200
194 276 },
... ... @@ -206,14 +288,16 @@ curl -X POST "http://localhost:6002/search/" \
206 288 ```bash
207 289 curl -X POST "http://localhost:6002/search/" \
208 290 -H "Content-Type: application/json" \
  291 + -H "X-Tenant-ID: 2" \
209 292 -d '{
210   - "query": "玩具",
  293 + "query": "手机",
  294 + "language": "zh",
211 295 "filters": {
212   - "category.keyword": ["玩具", "益智玩具"],
213   - "vendor.keyword": "乐高"
  296 + "category_name": ["手机", "电子产品"],
  297 + "vendor_zh.keyword": "品牌A"
214 298 },
215 299 "range_filters": {
216   - "price": {
  300 + "min_price": {
217 301 "gte": 50,
218 302 "lte": 500
219 303 }
... ... @@ -234,41 +318,82 @@ curl -X POST "http://localhost:6002/search/" \
234 318 ```bash
235 319 curl -X POST "http://localhost:6002/search/" \
236 320 -H "Content-Type: application/json" \
  321 + -H "X-Tenant-ID: 2" \
237 322 -d '{
238   - "query": "玩具",
  323 + "query": "手机",
  324 + "language": "zh",
239 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 331 ```json
246 332 {
247   - "hits": [...],
  333 + "results": [...],
248 334 "total": 118,
249 335 "facets": [
250 336 {
251   - "field": "category.keyword",
252   - "label": "category.keyword",
  337 + "field": "category1_name",
  338 + "label": "category1_name",
253 339 "type": "terms",
254 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 348 "type": "terms",
263 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 399 #### 示例 1:自定义分面大小
... ... @@ -276,16 +401,18 @@ curl -X POST "http://localhost:6002/search/" \
276 401 ```bash
277 402 curl -X POST "http://localhost:6002/search/" \
278 403 -H "Content-Type: application/json" \
  404 + -H "X-Tenant-ID: 2" \
279 405 -d '{
280   - "query": "玩具",
  406 + "query": "手机",
  407 + "language": "zh",
281 408 "facets": [
282 409 {
283   - "field": "category.keyword",
  410 + "field": "category1_name",
284 411 "size": 20,
285 412 "type": "terms"
286 413 },
287 414 {
288   - "field": "vendor.keyword",
  415 + "field": "category2_name",
289 416 "size": 30,
290 417 "type": "terms"
291 418 }
... ... @@ -298,8 +425,10 @@ curl -X POST "http://localhost:6002/search/" \
298 425 ```bash
299 426 curl -X POST "http://localhost:6002/search/" \
300 427 -H "Content-Type: application/json" \
  428 + -H "X-Tenant-ID: 2" \
301 429 -d '{
302   - "query": "玩具",
  430 + "query": "手机",
  431 + "language": "zh",
303 432 "facets": [
304 433 {
305 434 "field": "price",
... ... @@ -339,8 +468,10 @@ curl -X POST "http://localhost:6002/search/" \
339 468 ```bash
340 469 curl -X POST "http://localhost:6002/search/" \
341 470 -H "Content-Type: application/json" \
  471 + -H "X-Tenant-ID: 2" \
342 472 -d '{
343   - "query": "玩具",
  473 + "query": "手机",
  474 + "language": "zh",
344 475 "facets": [
345 476 {"field": "category.keyword", "size": 15},
346 477 {"field": "vendor.keyword", "size": 15},
... ... @@ -366,8 +497,10 @@ curl -X POST "http://localhost:6002/search/" \
366 497 ```bash
367 498 curl -X POST "http://localhost:6002/search/" \
368 499 -H "Content-Type: application/json" \
  500 + -H "X-Tenant-ID: 2" \
369 501 -d '{
370   - "query": "玩具",
  502 + "query": "手机",
  503 + "language": "zh",
371 504 "size": 20,
372 505 "sort_by": "min_price",
373 506 "sort_order": "asc"
... ... @@ -379,8 +512,10 @@ curl -X POST "http://localhost:6002/search/" \
379 512 ```bash
380 513 curl -X POST "http://localhost:6002/search/" \
381 514 -H "Content-Type: application/json" \
  515 + -H "X-Tenant-ID: 2" \
382 516 -d '{
383   - "query": "玩具",
  517 + "query": "手机",
  518 + "language": "zh",
384 519 "size": 20,
385 520 "sort_by": "create_time",
386 521 "sort_order": "desc"
... ... @@ -392,8 +527,10 @@ curl -X POST "http://localhost:6002/search/" \
392 527 ```bash
393 528 curl -X POST "http://localhost:6002/search/" \
394 529 -H "Content-Type: application/json" \
  530 + -H "X-Tenant-ID: 2" \
395 531 -d '{
396   - "query": "玩具",
  532 + "query": "手机",
  533 + "language": "zh",
397 534 "filters": {
398 535 "category.keyword": "益智玩具"
399 536 },
... ... @@ -411,6 +548,7 @@ curl -X POST "http://localhost:6002/search/" \
411 548 ```bash
412 549 curl -X POST "http://localhost:6002/search/image" \
413 550 -H "Content-Type: application/json" \
  551 + -H "X-Tenant-ID: 2" \
414 552 -d '{
415 553 "image_url": "https://example.com/barbie.jpg",
416 554 "size": 20
... ... @@ -422,14 +560,15 @@ curl -X POST "http://localhost:6002/search/image" \
422 560 ```bash
423 561 curl -X POST "http://localhost:6002/search/image" \
424 562 -H "Content-Type: application/json" \
  563 + -H "X-Tenant-ID: 2" \
425 564 -d '{
426 565 "image_url": "https://example.com/barbie.jpg",
427 566 "size": 20,
428 567 "filters": {
429   - "category.keyword": "玩具"
  568 + "category_name": "手机"
430 569 },
431 570 "range_filters": {
432   - "price": {
  571 + "min_price": {
433 572 "lte": 100
434 573 }
435 574 }
... ... @@ -445,6 +584,7 @@ curl -X POST "http://localhost:6002/search/image" \
445 584 ```bash
446 585 curl -X POST "http://localhost:6002/search/" \
447 586 -H "Content-Type: application/json" \
  587 + -H "X-Tenant-ID: 2" \
448 588 -d '{
449 589 "query": "玩具 AND 乐高"
450 590 }'
... ... @@ -457,6 +597,7 @@ curl -X POST "http://localhost:6002/search/" \
457 597 ```bash
458 598 curl -X POST "http://localhost:6002/search/" \
459 599 -H "Content-Type: application/json" \
  600 + -H "X-Tenant-ID: 2" \
460 601 -d '{
461 602 "query": "芭比 OR 娃娃"
462 603 }'
... ... @@ -469,6 +610,7 @@ curl -X POST "http://localhost:6002/search/" \
469 610 ```bash
470 611 curl -X POST "http://localhost:6002/search/" \
471 612 -H "Content-Type: application/json" \
  613 + -H "X-Tenant-ID: 2" \
472 614 -d '{
473 615 "query": "玩具 ANDNOT 电动"
474 616 }'
... ... @@ -481,6 +623,7 @@ curl -X POST "http://localhost:6002/search/" \
481 623 ```bash
482 624 curl -X POST "http://localhost:6002/search/" \
483 625 -H "Content-Type: application/json" \
  626 + -H "X-Tenant-ID: 2" \
484 627 -d '{
485 628 "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动"
486 629 }'
... ... @@ -493,6 +636,7 @@ curl -X POST "http://localhost:6002/search/" \
493 636 ```bash
494 637 curl -X POST "http://localhost:6002/search/" \
495 638 -H "Content-Type: application/json" \
  639 + -H "X-Tenant-ID: 2" \
496 640 -d '{
497 641 "query": "brand:乐高"
498 642 }'
... ... @@ -557,19 +701,23 @@ for hit in result['hits'][:3]:
557 701  
558 702 # 示例 2:带过滤和分面的搜索
559 703 result = search_products(
560   - query="玩具",
  704 + query="手机",
561 705 size=20,
  706 + language="zh",
562 707 filters={
563   - "category.keyword": ["玩具", "益智玩具"]
  708 + "category_name": "手机",
  709 + "specifications": {"name": "color", "value": "white"}
564 710 },
565 711 range_filters={
566   - "price": {"gte": 50, "lte": 200}
  712 + "min_price": {"gte": 50, "lte": 200}
567 713 },
568 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 721 "type": "range",
574 722 "ranges": [
575 723 {"key": "0-50", "to": 50},
... ... @@ -692,21 +840,24 @@ const result1 = await client.search({
692 840 });
693 841 console.log(`找到 ${result1.total} 个结果`);
694 842  
695   -// 带过滤和分面的搜索
  843 +// 带过滤和分面的搜索(包含规格)
696 844 const result2 = await client.search({
697   - query: "玩具",
  845 + query: "手机",
  846 + language: "zh",
698 847 size: 20,
699 848 filters: {
700   - category.keyword: ["玩具", "益智玩具"]
  849 + category_name: "手机",
  850 + specifications: { name: "color", value: "white" }
701 851 },
702 852 rangeFilters: {
703   - price: { gte: 50, lte: 200 }
  853 + min_price: { gte: 50, lte: 200 }
704 854 },
705 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 861 sortOrder: "asc"
711 862 });
712 863  
... ... @@ -810,8 +961,10 @@ const SearchComponent = {
810 961 ```bash
811 962 curl -X POST "http://localhost:6002/search/" \
812 963 -H "Content-Type: application/json" \
  964 + -H "X-Tenant-ID: 2" \
813 965 -d '{
814   - "query": "玩具",
  966 + "query": "手机",
  967 + "language": "zh",
815 968 "debug": true
816 969 }'
817 970 ```
... ... @@ -847,8 +1000,10 @@ curl -X POST "http://localhost:6002/search/" \
847 1000 ```bash
848 1001 curl -X POST "http://localhost:6002/search/" \
849 1002 -H "Content-Type: application/json" \
  1003 + -H "X-Tenant-ID: 2" \
850 1004 -d '{
851   - "query": "玩具",
  1005 + "query": "手机",
  1006 + "language": "zh",
852 1007 "min_score": 5.0
853 1008 }'
854 1009 ```
... ... @@ -865,10 +1020,11 @@ curl -X POST "http://localhost:6002/search/" \
865 1020 # 显示某个类目下的所有商品,按价格排序,提供品牌筛选
866 1021 curl -X POST "http://localhost:6002/search/" \
867 1022 -H "Content-Type: application/json" \
  1023 + -H "X-Tenant-ID: 2" \
868 1024 -d '{
869 1025 "query": "*",
870 1026 "filters": {
871   - "category.keyword": "玩具"
  1027 + "category_name": "手机"
872 1028 },
873 1029 "facets": [
874 1030 {"field": "vendor.keyword", "size": 20},
... ... @@ -892,19 +1048,53 @@ curl -X POST "http://localhost:6002/search/" \
892 1048 ### 场景 2:搜索结果页
893 1049  
894 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 1081 curl -X POST "http://localhost:6002/search/" \
897 1082 -H "Content-Type: application/json" \
  1083 + -H "X-Tenant-ID: 2" \
898 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 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 1099 "size": 20
910 1100 }'
... ... @@ -916,15 +1106,16 @@ curl -X POST "http://localhost:6002/search/" \
916 1106 # 显示特定价格区间的商品
917 1107 curl -X POST "http://localhost:6002/search/" \
918 1108 -H "Content-Type: application/json" \
  1109 + -H "X-Tenant-ID: 2" \
919 1110 -d '{
920 1111 "query": "*",
921 1112 "range_filters": {
922   - "price": {
  1113 + "min_price": {
923 1114 "gte": 50,
924 1115 "lte": 100
925 1116 }
926 1117 },
927   - "facets": ["category.keyword", "vendor.keyword"],
  1118 + "facets": ["category1_name", "category2_name", "specifications"],
928 1119 "sort_by": "min_price",
929 1120 "sort_order": "asc",
930 1121 "size": 50
... ... @@ -937,6 +1128,7 @@ curl -X POST "http://localhost:6002/search/" \
937 1128 # 最近更新的商品
938 1129 curl -X POST "http://localhost:6002/search/" \
939 1130 -H "Content-Type: application/json" \
  1131 + -H "X-Tenant-ID: 2" \
940 1132 -d '{
941 1133 "query": "*",
942 1134 "range_filters": {
... ... @@ -960,10 +1152,12 @@ curl -X POST "http://localhost:6002/search/" \
960 1152 # 错误:range_filters 缺少操作符
961 1153 curl -X POST "http://localhost:6002/search/" \
962 1154 -H "Content-Type: application/json" \
  1155 + -H "X-Tenant-ID: 2" \
963 1156 -d '{
964   - "query": "玩具",
  1157 + "query": "手机",
  1158 + "language": "zh",
965 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 1177 # 错误:query 为空
984 1178 curl -X POST "http://localhost:6002/search/" \
985 1179 -H "Content-Type: application/json" \
  1180 + -H "X-Tenant-ID: 2" \
986 1181 -d '{
987 1182 "query": ""
988 1183 }'
... ... @@ -1060,6 +1255,7 @@ curl -X POST "http://localhost:6002/search/" \
1060 1255 # 使用通配符查询 + 分面
1061 1256 curl -X POST "http://localhost:6002/search/" \
1062 1257 -H "Content-Type: application/json" \
  1258 + -H "X-Tenant-ID: 2" \
1063 1259 -d '{
1064 1260 "query": "*",
1065 1261 "size": 0,
... ... @@ -1074,8 +1270,10 @@ curl -X POST "http://localhost:6002/search/" \
1074 1270 ```bash
1075 1271 curl -X POST "http://localhost:6002/search/" \
1076 1272 -H "Content-Type: application/json" \
  1273 + -H "X-Tenant-ID: 2" \
1077 1274 -d '{
1078   - "query": "玩具",
  1275 + "query": "手机",
  1276 + "language": "zh",
1079 1277 "size": 0,
1080 1278 "facets": [
1081 1279 {
... ... @@ -1099,13 +1297,14 @@ curl -X POST "http://localhost:6002/search/" \
1099 1297 # 布尔表达式 + 过滤器 + 分面 + 排序
1100 1298 curl -X POST "http://localhost:6002/search/" \
1101 1299 -H "Content-Type: application/json" \
  1300 + -H "X-Tenant-ID: 2" \
1102 1301 -d '{
1103 1302 "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子",
1104 1303 "filters": {
1105 1304 "category.keyword": ["玩具", "益智玩具"]
1106 1305 },
1107 1306 "range_filters": {
1108   - "price": {"gte": 20, "lte": 100},
  1307 + "min_price": {"gte": 20, "lte": 100},
1109 1308 "days_since_last_update": {"lte": 30}
1110 1309 },
1111 1310 "facets": [
... ... @@ -1127,16 +1326,19 @@ curl -X POST "http://localhost:6002/search/" \
1127 1326 # 测试类目:玩具
1128 1327 curl -X POST "http://localhost:6002/search/" \
1129 1328 -H "Content-Type: application/json" \
  1329 + -H "X-Tenant-ID: 2" \
1130 1330 -d '{"query": "玩具", "size": 5}'
1131 1331  
1132 1332 # 测试品牌:乐高
1133 1333 curl -X POST "http://localhost:6002/search/" \
1134 1334 -H "Content-Type: application/json" \
  1335 + -H "X-Tenant-ID: 2" \
1135 1336 -d '{"query": "brand:乐高", "size": 5}'
1136 1337  
1137 1338 # 测试布尔表达式
1138 1339 curl -X POST "http://localhost:6002/search/" \
1139 1340 -H "Content-Type: application/json" \
  1341 + -H "X-Tenant-ID: 2" \
1140 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 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 &quot;http://120.76.41.98:6002/search/&quot; \
64 64 -d '{
65 65 "tenant_id": "demo-tenant",
66 66 "query": "芭比娃娃",
67   - "facets": ["category.keyword", "vendor.keyword"],
  67 + "facets": ["category.keyword", "specifications.color", "specifications.size"],
68 68 "min_score": 0.2
69 69 }'
70 70 ```
... ... @@ -95,10 +95,10 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
95 95  
96 96 ```json
97 97 {
98   - "tenant_id": "string (required)",
99 98 "query": "string (required)",
100 99 "size": 10,
101 100 "from": 0,
  101 + "language": "zh",
102 102 "filters": {},
103 103 "range_filters": {},
104 104 "facets": [],
... ... @@ -111,18 +111,20 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 120 | `query` | string | Y | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) |
120 121 | `size` | integer | N | 10 | 返回结果数量(1-100) |
121 122 | `from` | integer | N | 0 | 分页偏移量(用于分页) |
  123 +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 |
122 124 | `filters` | object | N | null | 精确匹配过滤器(见下文) |
123 125 | `range_filters` | object | N | null | 数值范围过滤器(见下文) |
124 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 128 | `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) |
127 129 | `min_score` | float | N | null | 最小相关性分数阈值 |
128 130 | `debug` | boolean | N | false | 是否返回调试信息 |
... ... @@ -139,9 +141,17 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
139 141 ```json
140 142 {
141 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 &quot;http://120.76.41.98:6002/search/&quot; \
151 161 - 整数:精确匹配
152 162 - 布尔值:精确匹配
153 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 205 #### 2. 范围过滤器 (range_filters)
161 206  
... ... @@ -201,7 +246,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
201 246 **简单模式**(字符串数组):
202 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 &quot;http://120.76.41.98:6002/search/&quot; \
210 255 {
211 256 "facets": [
212 257 {
213   - "field": "category.keyword",
  258 + "field": "category1_name",
214 259 "size": 15,
215 260 "type": "terms"
216 261 },
... ... @@ -223,6 +268,53 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
223 268 {"key": "100-200", "from": 100, "to": 200},
224 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 &quot;http://120.76.41.98:6002/search/&quot; \
276 368 {
277 369 "spu_id": "12345",
278 370 "title": "芭比时尚娃娃",
279   - "handle": "barbie-doll",
280   - "description": "高品质芭比娃娃",
  371 + "brief": "高品质芭比娃娃",
  372 + "description": "详细描述...",
281 373 "vendor": "美泰",
282 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 383 "price": 89.99,
285 384 "compare_at_price": 129.99,
286 385 "currency": "USD",
287 386 "image_url": "https://example.com/image.jpg",
288 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 399 "skus": [
290 400 {
291 401 "sku_id": "67890",
292   - "title": "粉色款",
293 402 "price": 89.99,
294 403 "compare_at_price": 129.99,
295 404 "sku": "BARBIE-001",
296 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 414 "relevance_score": 8.5
... ... @@ -307,8 +418,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
307 418 "max_score": 8.5,
308 419 "facets": [
309 420 {
310   - "field": "category.keyword",
311   - "label": "category.keyword",
  421 + "field": "category1_name",
  422 + "label": "category1_name",
312 423 "type": "terms",
313 424 "values": [
314 425 {
... ... @@ -318,6 +429,19 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
318 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 447 "query_info": {
... ... @@ -356,31 +480,55 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
356 480 | 字段 | 类型 | 说明 |
357 481 |------|------|------|
358 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 496 | `price` | float | 价格(min_price) |
366 497 | `compare_at_price` | float | 原价 |
367 498 | `currency` | string | 货币单位(默认USD) |
368 499 | `image_url` | string | 主图URL |
369 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 509 | `skus` | array | SKU 列表 |
371 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 517 ### SkuResult字段说明
374 518  
375 519 | 字段 | 类型 | 说明 |
376 520 |------|------|------|
377 521 | `sku_id` | string | SKU ID |
378   -| `title` | string | SKU标题 |
379 522 | `price` | float | 价格 |
380 523 | `compare_at_price` | float | 原价 |
381   -| `sku` | string | SKU编码 |
  524 +| `sku` | string | SKU编码(sku_code) |
382 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 &quot;http://120.76.41.98:6002/search/&quot; \
408 556 {
409 557 "query": "玩具",
410 558 "size": 20,
  559 + "language": "zh",
411 560 "filters": {
412   - "category.keyword": "益智玩具"
  561 + "category_name": "益智玩具"
413 562 },
414 563 "range_filters": {
415 564 "min_price": {
... ... @@ -428,23 +577,26 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
428 577 {
429 578 "query": "玩具",
430 579 "size": 20,
  580 + "language": "zh",
431 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 589 ### 场景4:多条件组合搜索
439 590  
440   -**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计
  591 +**需求**: 搜索"手机",筛选多个品牌,价格范围,并获取分面统计
441 592  
442 593 ```json
443 594 {
444   - "query": "玩具",
  595 + "query": "手机",
445 596 "size": 20,
  597 + "language": "zh",
446 598 "filters": {
447   - "vendor.keyword": ["乐高", "孩之宝", "美泰"]
  599 + "vendor_zh.keyword": ["品牌A", "品牌B"]
448 600 },
449 601 "range_filters": {
450 602 "min_price": {
... ... @@ -454,7 +606,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
454 606 },
455 607 "facets": [
456 608 {
457   - "field": "category.keyword",
  609 + "field": "category1_name",
458 610 "size": 15
459 611 },
460 612 {
... ... @@ -466,31 +618,117 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
466 618 {"key": "100-200", "from": 100, "to": 200},
467 619 {"key": "200+", "from": 200}
468 620 ]
469   - }
  621 + },
  622 + "specifications"
470 623 ],
471 624 "sort_by": "min_price",
472 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 718 ```json
481 719 {
482   - "query": "玩具 AND 乐高 ANDNOT 电动",
  720 + "query": "手机 AND 智能 ANDNOT 二手",
483 721 "size": 20
484 722 }
485 723 ```
486 724  
487   -### 场景6:分页查询
  725 +### 场景10:分页查询
488 726  
489 727 **需求**: 获取第2页结果(每页20条)
490 728  
491 729 ```json
492 730 {
493   - "query": "玩具",
  731 + "query": "手机",
494 732 "size": 20,
495 733 "from": 20
496 734 }
... ... @@ -650,25 +888,33 @@ curl &quot;http://localhost:6002/search/12345&quot;
650 888  
651 889 | 字段名 | 类型 | 描述 |
652 890 |--------|------|------|
  891 +| `tenant_id` | keyword | 租户ID(多租户隔离) |
653 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 &quot;http://localhost:6002/search/12345&quot;
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 &quot;http://localhost:6002/search/12345&quot;
694 943  
695 944 - `min_price`: 最低价格
696 945 - `max_price`: 最高价格
697   -- `title`: 标题(字母序)
698 946 - `create_time`: 创建时间
699 947 - `update_time`: 更新时间
700 948 - `relevance_score`: 相关性分数(默认)
... ... @@ -703,23 +951,21 @@ curl &quot;http://localhost:6002/search/12345&quot;
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 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 17 ```bash
18 18 {
19 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 78  
49 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 103 ```bash
58 104 {
59 105 "facets": [
60   - {"field": "category.keyword", "size": 15},
  106 + {"field": "category1_name", "size": 15},
61 107 {
62   - "field": "price",
  108 + "field": "min_price",
63 109 "type": "range",
64 110 "ranges": [
65 111 {"key": "0-50", "to": 50},
66 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 159  
111 160 ```bash
112 161 POST /search/
  162 +Headers: X-Tenant-ID: 2
113 163 {
114   - "query": "玩具",
  164 + "query": "手机",
115 165 "size": 20,
116 166 "from": 0,
  167 + "language": "zh",
117 168 "filters": {
118   - "category.keyword": ["玩具", "益智玩具"]
  169 + "category_name": "手机",
  170 + "category1_name": "电子产品",
  171 + "specifications": {"name": "color", "value": "white"}
119 172 },
120 173 "range_filters": {
121   - "price": {"gte": 50, "lte": 200}
  174 + "min_price": {"gte": 50, "lte": 200}
122 175 },
123 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 182 "sort_by": "min_price",
128 183 "sort_order": "asc"
... ... @@ -135,11 +190,29 @@ POST /search/
135 190  
136 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 218 "total": 118,
... ... @@ -147,17 +220,30 @@ POST /search/
147 220 "took_ms": 45,
148 221 "facets": [
149 222 {
150   - "field": "category.keyword",
151   - "label": "商品类目",
  223 + "field": "category1_name",
  224 + "label": "category1_name",
152 225 "type": "terms",
153 226 "values": [
154 227 {
155   - "value": "玩具",
156   - "label": "玩具",
  228 + "value": "手机",
  229 + "label": "手机",
157 230 "count": 85,
158 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 278 ```python
193 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 295 print(f"找到 {result['total']} 个结果")
205 296 ```
... ... @@ -211,12 +302,16 @@ print(f&quot;找到 {result[&#39;total&#39;]} 个结果&quot;)
211 302 ```javascript
212 303 const result = await fetch('http://localhost:6002/search/', {
213 304 method: 'POST',
214   - headers: {'Content-Type': 'application/json'},
  305 + headers: {
  306 + 'Content-Type': 'application/json',
  307 + 'X-Tenant-ID': '2'
  308 + },
215 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 315 sort_by: "min_price",
221 316 sort_order: "asc"
222 317 })
... ...
docs/系统设计文档.md
... ... @@ -215,7 +215,7 @@ indexes:
215 215 4. 组合多个语言查询的结果,提高召回率
216 216  
217 217 **实现模块**:
218   -- `search/multilang_query_builder.py` - 多语言查询构建器
  218 +- `search/es_query_builder.py` - ES 查询构建器(单层架构)
219 219 - `query/query_parser.py` - 查询解析器(支持语言检测和翻译)
220 220  
221 221 ---
... ... @@ -345,7 +345,7 @@ query_config:
345 345  
346 346 #### 工作流程
347 347 ```
348   -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建
  348 +查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall))
349 349 ```
350 350  
351 351 #### 实现模块
... ... @@ -421,31 +421,57 @@ laptop AND (gaming OR professional) ANDNOT cheap
421 421 - 提取域(如 `title:查询` → 域=`title`,查询=`查询`)
422 422 - 检测查询语言
423 423 - 生成翻译
424   -2. **多语言查询构建**:
425   - - 如果域有 `language_field_mapping`:
426   - - 使用检测到的语言查询对应字段(boost * 1.5)
427   - - 使用翻译后的查询搜索其他语言字段(boost * 1.0)
428   - - 如果域没有 `language_field_mapping`:
429   - - 使用所有字段进行搜索
430   -3. **查询组合**:
431   - - 多个语言查询组合为 `should` 子句
432   - - 提高召回率
433   -
434   -#### 示例
435   -```
436   -查询: "芭比娃娃"
437   -域: default
438   -检测语言: zh
439   -
440   -生成的查询:
441   -- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5)
442   -- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0)
443   -- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0)
  424 +2. **查询构建**(简化架构):
  425 + - **结构**: `filters AND (text_recall OR embedding_recall)`
  426 + - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中)
  427 + - **text_recall**: 文本相关性召回
  428 + - 同时搜索中英文字段(`title_zh/en`, `brief_zh/en`, `description_zh/en`, `vendor_zh/en`, `category_path_zh/en`, `category_name_zh/en`, `tags`)
  429 + - 使用 `multi_match` 查询,支持字段 boost
  430 + - **embedding_recall**: 向量召回(KNN)
  431 + - 使用 `title_embedding` 字段进行 KNN 搜索
  432 + - ES 自动与文本召回合并
  433 + - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等)
  434 +
  435 +#### 查询结构示例
  436 +```json
  437 +{
  438 + "query": {
  439 + "bool": {
  440 + "must": [
  441 + {
  442 + "function_score": {
  443 + "query": {
  444 + "multi_match": {
  445 + "query": "手机",
  446 + "fields": [
  447 + "title_zh^3.0", "title_en^3.0",
  448 + "brief_zh^1.5", "brief_en^1.5",
  449 + ...
  450 + ]
  451 + }
  452 + },
  453 + "functions": [...]
  454 + }
  455 + }
  456 + ],
  457 + "filter": [
  458 + {"term": {"tenant_id": "2"}},
  459 + {"term": {"category_name": "手机"}}
  460 + ]
  461 + }
  462 + },
  463 + "knn": {
  464 + "field": "title_embedding",
  465 + "query_vector": [...],
  466 + "k": 50,
  467 + "boost": 0.2
  468 + }
  469 +}
444 470 ```
445 471  
446 472 #### 实现模块
447   -- `search/multilang_query_builder.py` - 多语言查询构建器
448   -- `search/searcher.py` - 搜索器(使用多语言构建器)
  473 +- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法)
  474 +- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`)
449 475  
450 476 ### 5.3 相关性计算(Ranking)
451 477  
... ...
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 188 "aggs": {
154   - "specs": {
  189 + "specifications_facet": {
155 190 "nested": { "path": "specifications" },
156 191 "aggs": {
157 192 "by_name": {
158   - "terms": {
159   - "field": "specifications.name",
160   - "size": 20
161   - },
  193 + "terms": { "field": "specifications.name", "size": 20 },
162 194 "aggs": {
163 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 201 }
173 202 }
174 203 }
175   -
176 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 352 # 1. 处理精确匹配过滤
353 353 if filters:
354 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 406 if isinstance(value, list):
356 407 # 多值匹配(OR)
357 408 filter_clauses.append({
... ... @@ -486,34 +537,62 @@ class ESQueryBuilder:
486 537  
487 538 for config in facet_configs:
488 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 596 if isinstance(config, str):
518 597 field = config
519 598 agg_name = f"{field}_facet"
... ... @@ -524,6 +603,7 @@ class ESQueryBuilder:
524 603 "order": {"_count": "desc"}
525 604 }
526 605 }
  606 + continue
527 607  
528 608 # 高级模式:FacetConfig 对象
529 609 else:
... ...