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 查询结构
添加了两种分面模式的说明和示例
更新了“分面字段”说明,明确支持指定规格名称的分面
@@ -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
@@ -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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -64,7 +64,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -95,10 +95,10 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -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 | `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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -139,9 +141,17 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -151,11 +161,46 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -201,7 +246,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -210,7 +255,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -223,6 +268,53 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -276,28 +368,47 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -307,8 +418,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -318,6 +429,19 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -356,31 +480,55 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -408,8 +556,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -428,23 +577,26 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -454,7 +606,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \ @@ -466,31 +618,117 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://localhost:6002/search/12345&quot; @@ -650,25 +888,33 @@ curl &quot;http://localhost:6002/search/12345&quot;
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 &quot;http://localhost:6002/search/12345&quot; @@ -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,7 +943,6 @@ curl &quot;http://localhost:6002/search/12345&quot;
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 &quot;http://localhost:6002/search/12345&quot; @@ -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 | 类型 | 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&quot;找到 {result[&#39;total&#39;]} 个结果&quot;) @@ -211,12 +302,16 @@ print(f&quot;找到 {result[&#39;total&#39;]} 个结果&quot;)
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: