Commit 6d524cb4993a3530ae4e99378afb20e77a6bb528

Authored by tangwang
1 parent c581becd

docs优化

docs/multi_select_faceting.md deleted
... ... @@ -1,399 +0,0 @@
1   -# Multi-Select Faceting 功能说明
2   -
3   -## 概述
4   -
5   -Multi-Select Faceting(多选分面)是业界标准的 Faceted Search 功能,允许用户在选中某个分面筛选项后,仍然能看到该分面的其他可选项,提供更好的探索式搜索体验。
6   -
7   -## 功能特性
8   -
9   -### 1. 两种 Faceting 模式
10   -
11   -#### 标准模式(Conjunctive Faceting)
12   -- **设置**: `multi_select: false`(默认)
13   -- **行为**: 选中某个分面值后,该分面只显示选中的值
14   -- **适用场景**: 层级下钻、逐步精炼
15   -- **ES 实现**: 过滤器应用在 `query.bool.filter`
16   -
17   -#### Multi-Select 模式(Disjunctive Faceting)
18   -- **设置**: `multi_select: true`
19   -- **行为**: 选中某个分面值后,该分面仍显示所有可选项
20   -- **适用场景**: 颜色、品牌、尺码等可切换属性
21   -- **ES 实现**: 过滤器应用在 `post_filter`
22   -
23   -### 2. Selected 状态标记
24   -
25   -所有 facet 值都包含 `selected` 字段,标记当前是否被选中:
26   -- `selected: true` - 当前筛选项已被选中
27   -- `selected: false` - 当前筛选项未被选中
28   -
29   -## 使用示例
30   -
31   -### 示例 1: 标准 Category Faceting
32   -
33   -```json
34   -{
35   - "query": "T恤",
36   - "filters": {
37   - "category1_name": "服装"
38   - },
39   - "facets": [
40   - {
41   - "field": "category1_name",
42   - "size": 10,
43   - "type": "terms",
44   - "multi_select": false
45   - }
46   - ]
47   -}
48   -```
49   -
50   -**响应**:
51   -```json
52   -{
53   - "results": [...],
54   - "facets": [
55   - {
56   - "field": "category1_name",
57   - "values": [
58   - {"value": "服装", "count": 150, "selected": true}
59   - ]
60   - }
61   - ]
62   -}
63   -```
64   -
65   -### 示例 2: Multi-Select Brand Faceting
66   -
67   -```json
68   -{
69   - "query": "手机",
70   - "filters": {
71   - "brand_name": "苹果"
72   - },
73   - "facets": [
74   - {
75   - "field": "brand_name",
76   - "size": 10,
77   - "type": "terms",
78   - "multi_select": true
79   - }
80   - ]
81   -}
82   -```
83   -
84   -**响应**:
85   -```json
86   -{
87   - "results": [...只包含苹果手机...],
88   - "facets": [
89   - {
90   - "field": "brand_name",
91   - "values": [
92   - {"value": "苹果", "count": 150, "selected": true},
93   - {"value": "华为", "count": 120, "selected": false},
94   - {"value": "小米", "count": 98, "selected": false}
95   - ]
96   - }
97   - ]
98   -}
99   -```
100   -
101   -### 示例 3: Specifications Multi-Select
102   -
103   -```json
104   -{
105   - "query": "衬衫",
106   - "filters": {
107   - "specifications": {
108   - "name": "颜色",
109   - "value": "白色"
110   - }
111   - },
112   - "facets": [
113   - {
114   - "field": "specifications.颜色",
115   - "size": 10,
116   - "type": "terms",
117   - "multi_select": true
118   - },
119   - {
120   - "field": "specifications.尺码",
121   - "size": 10,
122   - "type": "terms",
123   - "multi_select": false
124   - }
125   - ]
126   -}
127   -```
128   -
129   -**响应**:
130   -```json
131   -{
132   - "results": [...只包含白色衬衫...],
133   - "facets": [
134   - {
135   - "field": "specifications.颜色",
136   - "label": "颜色",
137   - "values": [
138   - {"value": "白色", "count": 50, "selected": true},
139   - {"value": "蓝色", "count": 35, "selected": false},
140   - {"value": "黑色", "count": 28, "selected": false}
141   - ]
142   - },
143   - {
144   - "field": "specifications.尺码",
145   - "label": "尺码",
146   - "values": [
147   - {"value": "M", "count": 20, "selected": false},
148   - {"value": "L", "count": 18, "selected": false},
149   - {"value": "XL", "count": 12, "selected": false}
150   - ]
151   - }
152   - ]
153   -}
154   -```
155   -
156   -注意:尺码分面(`multi_select: false`)的统计是基于白色衬衫的。
157   -
158   -### 示例 4: 混合多个 Multi-Select Facets
159   -
160   -```json
161   -{
162   - "query": "*",
163   - "filters": {
164   - "category1_name": "玩具",
165   - "specifications": [
166   - {"name": "颜色", "value": "红色"},
167   - {"name": "材质", "value": "塑料"}
168   - ]
169   - },
170   - "facets": [
171   - {
172   - "field": "category1_name",
173   - "size": 10,
174   - "multi_select": true
175   - },
176   - {
177   - "field": "specifications.颜色",
178   - "size": 10,
179   - "multi_select": true
180   - },
181   - {
182   - "field": "specifications.材质",
183   - "size": 10,
184   - "multi_select": true
185   - },
186   - {
187   - "field": "specifications.年龄段",
188   - "size": 10,
189   - "multi_select": false
190   - }
191   - ]
192   -}
193   -```
194   -
195   -**行为说明**:
196   -- `category1_name`: 显示所有类目选项(玩具被标记为 selected)
197   -- `specifications.颜色`: 显示所有颜色选项(红色被标记为 selected)
198   -- `specifications.材质`: 显示所有材质选项(塑料被标记为 selected)
199   -- `specifications.年龄段`: 只显示符合当前过滤条件的年龄段选项
200   -
201   -## 前端集成建议
202   -
203   -### React 示例
204   -
205   -```jsx
206   -function FacetComponent({ facet }) {
207   - return (
208   - <div className="facet">
209   - <h3>{facet.label}</h3>
210   - {facet.values.map(value => (
211   - <label key={value.value} className={value.selected ? 'active' : ''}>
212   - <input
213   - type="checkbox"
214   - checked={value.selected}
215   - onChange={() => toggleFilter(facet.field, value.value)}
216   - />
217   - {value.value} ({value.count})
218   - </label>
219   - ))}
220   - </div>
221   - );
222   -}
223   -```
224   -
225   -### Vue 示例
226   -
227   -```vue
228   -<template>
229   - <div class="facet">
230   - <h3>{{ facet.label }}</h3>
231   - <label
232   - v-for="value in facet.values"
233   - :key="value.value"
234   - :class="{ active: value.selected }"
235   - >
236   - <input
237   - type="checkbox"
238   - :checked="value.selected"
239   - @change="toggleFilter(facet.field, value.value)"
240   - />
241   - {{ value.value }} ({{ value.count }})
242   - </label>
243   - </div>
244   -</template>
245   -```
246   -
247   -## 技术实现细节
248   -
249   -### Elasticsearch Query 结构
250   -
251   -**Multi-Select Faceting** 使用 `post_filter` 实现:
252   -
253   -```json
254   -{
255   - "query": {
256   - "bool": {
257   - "must": [...],
258   - "filter": [
259   - // 只包含 multi_select=false 的过滤器
260   - {"term": {"category2_name": "短袖T恤"}}
261   - ]
262   - }
263   - },
264   - "post_filter": {
265   - "bool": {
266   - "filter": [
267   - // 包含 multi_select=true 的过滤器
268   - {"term": {"brand_name": "苹果"}},
269   - {
270   - "nested": {
271   - "path": "specifications",
272   - "query": {
273   - "bool": {
274   - "must": [
275   - {"term": {"specifications.name": "颜色"}},
276   - {"term": {"specifications.value": "白色"}}
277   - ]
278   - }
279   - }
280   - }
281   - }
282   - ]
283   - }
284   - },
285   - "aggs": {
286   - // 所有聚合都基于 query 的结果(不受 post_filter 影响)
287   - "brand_name_facet": {...},
288   - "specifications_颜色_facet": {...}
289   - }
290   -}
291   -```
292   -
293   -**关键点**:
294   -- `query.bool.filter`: 影响结果和聚合
295   -- `post_filter`: 只影响结果,不影响聚合
296   -- 聚合统计基于 `query` 的结果,因此 multi-select facet 可以显示多个选项
297   -
298   -## 最佳实践
299   -
300   -### 1. 何时使用 Multi-Select
301   -
302   -| Facet 类型 | 推荐模式 | 原因 |
303   -|-----------|---------|------|
304   -| 颜色 | `multi_select: true` | 用户需要切换颜色 |
305   -| 品牌 | `multi_select: true` | 用户需要比较不同品牌 |
306   -| 尺码 | `multi_select: true` | 用户需要查看其他尺码 |
307   -| 类目 | `multi_select: false` | 层级下钻 |
308   -| 价格区间 | `multi_select: false` | 互斥选择 |
309   -| 是否有货 | `multi_select: false` | 布尔值筛选 |
310   -
311   -### 2. 性能考虑
312   -
313   -- **过多 Multi-Select**: 会增加 ES 查询复杂度
314   -- **建议**: 最多 3-5 个 multi-select facets
315   -- **优化**: 对于不常用的属性使用标准模式
316   -
317   -### 3. UI 设计建议
318   -
319   -- **Multi-Select Facets**: 使用复选框(Checkbox)
320   -- **Standard Facets**: 使用单选框(Radio)或链接
321   -- **Selected 状态**: 使用不同颜色或图标标识
322   -
323   -## API 变更说明
324   -
325   -### 新增字段
326   -
327   -**FacetConfig**:
328   -```json
329   -{
330   - "field": "brand_name",
331   - "size": 10,
332   - "type": "terms",
333   - "multi_select": true // 新增字段
334   -}
335   -```
336   -
337   -**FacetValue**:
338   -```json
339   -{
340   - "value": "苹果",
341   - "count": 150,
342   - "selected": true // 现在由后端返回真实状态
343   -}
344   -```
345   -
346   -### 兼容性
347   -
348   -- `multi_select` 默认为 `false`,保持向后兼容
349   -- 旧版 API 调用仍然有效(使用标准模式)
350   -
351   -## 测试验证
352   -
353   -运行测试脚本:
354   -```bash
355   -python test_multi_select_facet.py
356   -```
357   -
358   -测试覆盖:
359   -1. ✓ 标准 Faceting (multi_select=false)
360   -2. ✓ Multi-Select Faceting (multi_select=true)
361   -3. ✓ Specifications Multi-Select
362   -4. ✓ ES Query 结构验证
363   -
364   -## 故障排查
365   -
366   -### 问题 1: Multi-Select 不生效
367   -
368   -**症状**: 设置了 `multi_select: true`,但仍然只返回一个值
369   -
370   -**检查**:
371   -1. 确认 `multi_select` 字段在请求中正确设置
372   -2. 检查 ES query 是否包含 `post_filter`(开启 `debug: true`)
373   -3. 验证 Elasticsearch 版本支持 `post_filter`
374   -
375   -### 问题 2: Selected 标记不正确
376   -
377   -**症状**: `selected` 字段没有正确标记
378   -
379   -**检查**:
380   -1. 确认 `filters` 中的字段名与 facet 字段名一致
381   -2. 对于 specifications,检查 `name` 和 `value` 是否匹配
382   -3. 检查 `filters` 的值类型(字符串、数组等)
383   -
384   -### 问题 3: 性能问题
385   -
386   -**症状**: 启用 Multi-Select 后查询变慢
387   -
388   -**优化**:
389   -1. 减少 multi-select facets 数量
390   -2. 降低 facet `size` 参数
391   -3. 考虑使用缓存
392   -4. 为常用字段建立索引
393   -
394   -## 参考资料
395   -
396   -- [Elasticsearch Post Filter](https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#post-filter)
397   -- [Algolia Disjunctive Faceting](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#conjunctive-and-disjunctive-facets)
398   -- [Amazon Product Search](https://www.amazon.com) - 业界最佳实践示例
399   -
docs/搜索API对接指南.md
... ... @@ -318,38 +318,17 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
318 318  
319 319 **重要特性**: `multi_select` 字段控制分面的行为模式。
320 320  
  321 +
321 322 ##### 标准模式 (multi_select: false)
322 323 - **行为**: 选中某个分面值后,该分面只显示选中的值
323 324 - **适用场景**: 层级类目、互斥选择
324 325 - **示例**: 类目下钻(玩具 > 娃娃 > 芭比)
325 326  
326   -```json
327   -{
328   - "filters": {"category1_name": "玩具"},
329   - "facets": [
330   - {"field": "category1_name", "size": 10, "multi_select": false}
331   - ]
332   -}
333   -```
334   -**响应**: 只返回 "玩具" 一个选项
335   -
336 327 ##### Multi-Select 模式 (multi_select: true) ⭐
337 328 - **行为**: 选中某个分面值后,该分面仍显示所有可选项
338 329 - **适用场景**: 颜色、品牌、尺码等可切换属性
339 330 - **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项
340 331  
341   -```json
342   -{
343   - "filters": {
344   - "specifications": {"name": "颜色", "value": "红色"}
345   - },
346   - "facets": [
347   - {"field": "specifications.颜色", "size": 10, "multi_select": true}
348   - ]
349   -}
350   -```
351   -**响应**: 返回所有颜色选项,"红色" 被标记为 `selected: true`
352   -
353 332 ##### 推荐配置
354 333  
355 334 | 分面类型 | multi_select | 原因 |
... ...
frontend/index.html
... ... @@ -23,7 +23,7 @@
23 23 <div class="search-bar">
24 24 <div class="tenant-input-wrapper">
25 25 <label for="tenantInput">tenant ID:</label>
26   - <input type="text" id="tenantInput" placeholder="请输入租户ID" value="1">
  26 + <input type="text" id="tenantInput" placeholder="请输入租户ID" value="162">
27 27 </div>
28 28 <div class="tenant-input-wrapper">
29 29 <label for="skuFilterDimension">sku_filter_dimension:</label>
... ...
test_multi_select_facet.py deleted
... ... @@ -1,288 +0,0 @@
1   -#!/usr/bin/env python3
2   -"""
3   -测试 Multi-Select Faceting 和 Selected 标记功能
4   -
5   -验证:
6   -1. multi_select=False 时,选中某个 facet 值后,该 facet 只返回一个值(标准模式)
7   -2. multi_select=True 时,选中某个 facet 值后,该 facet 仍返回多个值(disjunctive 模式)
8   -3. selected 字段正确标记
9   -"""
10   -
11   -import requests
12   -import json
13   -
14   -API_URL = "http://localhost:8000/api/search"
15   -TENANT_ID = "test_tenant"
16   -
17   -def test_standard_faceting():
18   - """测试标准 faceting(multi_select=False)"""
19   - print("\n" + "="*80)
20   - print("测试 1: 标准 Faceting (multi_select=False)")
21   - print("="*80)
22   -
23   - # 选择 category1_name = "玩具"
24   - payload = {
25   - "query": "*",
26   - "size": 5,
27   - "filters": {
28   - "category1_name": "玩具"
29   - },
30   - "facets": [
31   - {
32   - "field": "category1_name",
33   - "size": 10,
34   - "type": "terms",
35   - "multi_select": False # 标准模式
36   - }
37   - ]
38   - }
39   -
40   - headers = {"X-Tenant-ID": TENANT_ID}
41   -
42   - try:
43   - response = requests.post(API_URL, json=payload, headers=headers)
44   - response.raise_for_status()
45   - result = response.json()
46   -
47   - print(f"\n✓ 请求成功 (HTTP {response.status_code})")
48   - print(f" 总结果数: {result.get('total', 0)}")
49   -
50   - if result.get('facets'):
51   - for facet in result['facets']:
52   - if facet['field'] == 'category1_name':
53   - print(f"\n Facet: {facet['field']}")
54   - print(f" Values 数量: {len(facet['values'])}")
55   - for val in facet['values']:
56   - selected_mark = "✓" if val.get('selected') else " "
57   - print(f" [{selected_mark}] {val['value']}: {val['count']}")
58   -
59   - # 验证
60   - if len(facet['values']) == 1:
61   - print("\n ✓ 验证通过: multi_select=False 时,只返回选中的值")
62   - else:
63   - print("\n ⚠ 警告: multi_select=False 时应只返回一个值")
64   -
65   - selected_values = [v['value'] for v in facet['values'] if v.get('selected')]
66   - if selected_values == ['玩具']:
67   - print(" ✓ 验证通过: selected 字段正确标记")
68   - else:
69   - print(f" ✗ 错误: selected 标记不正确,期望 ['玩具'],实际 {selected_values}")
70   - else:
71   - print("\n ⚠ 警告: 没有返回 facets")
72   -
73   - except requests.exceptions.RequestException as e:
74   - print(f"\n✗ 请求失败: {e}")
75   - except Exception as e:
76   - print(f"\n✗ 错误: {e}")
77   -
78   -
79   -def test_multi_select_faceting():
80   - """测试 Multi-Select Faceting (multi_select=True)"""
81   - print("\n" + "="*80)
82   - print("测试 2: Multi-Select Faceting (multi_select=True)")
83   - print("="*80)
84   -
85   - # 选择 category1_name = "玩具"
86   - payload = {
87   - "query": "*",
88   - "size": 5,
89   - "filters": {
90   - "category1_name": "玩具"
91   - },
92   - "facets": [
93   - {
94   - "field": "category1_name",
95   - "size": 10,
96   - "type": "terms",
97   - "multi_select": True # Multi-select 模式
98   - }
99   - ]
100   - }
101   -
102   - headers = {"X-Tenant-ID": TENANT_ID}
103   -
104   - try:
105   - response = requests.post(API_URL, json=payload, headers=headers)
106   - response.raise_for_status()
107   - result = response.json()
108   -
109   - print(f"\n✓ 请求成功 (HTTP {response.status_code})")
110   - print(f" 总结果数: {result.get('total', 0)}")
111   -
112   - if result.get('facets'):
113   - for facet in result['facets']:
114   - if facet['field'] == 'category1_name':
115   - print(f"\n Facet: {facet['field']}")
116   - print(f" Values 数量: {len(facet['values'])}")
117   - for val in facet['values'][:5]: # 只显示前5个
118   - selected_mark = "✓" if val.get('selected') else " "
119   - print(f" [{selected_mark}] {val['value']}: {val['count']}")
120   -
121   - # 验证
122   - if len(facet['values']) > 1:
123   - print(f"\n ✓ 验证通过: multi_select=True 时,返回多个值 ({len(facet['values'])} 个)")
124   - else:
125   - print("\n ✗ 错误: multi_select=True 时应返回多个值")
126   -
127   - selected_values = [v['value'] for v in facet['values'] if v.get('selected')]
128   - if selected_values == ['玩具']:
129   - print(" ✓ 验证通过: selected 字段正确标记")
130   - else:
131   - print(f" ⚠ 警告: selected 标记可能不正确,期望 ['玩具'],实际 {selected_values}")
132   - else:
133   - print("\n ⚠ 警告: 没有返回 facets")
134   -
135   - except requests.exceptions.RequestException as e:
136   - print(f"\n✗ 请求失败: {e}")
137   - except Exception as e:
138   - print(f"\n✗ 错误: {e}")
139   -
140   -
141   -def test_specifications_multi_select():
142   - """测试 Specifications 的 Multi-Select Faceting"""
143   - print("\n" + "="*80)
144   - print("测试 3: Specifications Multi-Select Faceting")
145   - print("="*80)
146   -
147   - # 选择 specifications.颜色 = "白色"
148   - payload = {
149   - "query": "*",
150   - "size": 5,
151   - "filters": {
152   - "specifications": {
153   - "name": "颜色",
154   - "value": "白色"
155   - }
156   - },
157   - "facets": [
158   - {
159   - "field": "specifications.颜色",
160   - "size": 10,
161   - "type": "terms",
162   - "multi_select": True
163   - },
164   - {
165   - "field": "specifications.尺寸",
166   - "size": 10,
167   - "type": "terms",
168   - "multi_select": False
169   - }
170   - ]
171   - }
172   -
173   - headers = {"X-Tenant-ID": TENANT_ID}
174   -
175   - try:
176   - response = requests.post(API_URL, json=payload, headers=headers)
177   - response.raise_for_status()
178   - result = response.json()
179   -
180   - print(f"\n✓ 请求成功 (HTTP {response.status_code})")
181   - print(f" 总结果数: {result.get('total', 0)}")
182   -
183   - if result.get('facets'):
184   - for facet in result['facets']:
185   - print(f"\n Facet: {facet['field']} (multi_select={facet.get('multi_select', 'N/A')})")
186   - print(f" Values 数量: {len(facet['values'])}")
187   - for val in facet['values'][:5]:
188   - selected_mark = "✓" if val.get('selected') else " "
189   - print(f" [{selected_mark}] {val['value']}: {val['count']}")
190   -
191   - # 验证 specifications.颜色
192   - if facet['field'] == 'specifications.颜色':
193   - if len(facet['values']) > 1:
194   - print(f" ✓ 验证通过: multi_select=True,返回多个颜色选项")
195   - selected_values = [v['value'] for v in facet['values'] if v.get('selected')]
196   - if '白色' in selected_values:
197   - print(" ✓ 验证通过: '白色' 被正确标记为 selected")
198   -
199   - # 验证 specifications.尺寸(基于白色商品的尺寸分布)
200   - if facet['field'] == 'specifications.尺寸':
201   - print(f" ℹ 尺寸分布基于已选的颜色过滤器")
202   - else:
203   - print("\n ⚠ 警告: 没有返回 facets")
204   -
205   - except requests.exceptions.RequestException as e:
206   - print(f"\n✗ 请求失败: {e}")
207   - except Exception as e:
208   - print(f"\n✗ 错误: {e}")
209   -
210   -
211   -def test_es_query_structure():
212   - """测试 ES Query 结构(需要 debug=True)"""
213   - print("\n" + "="*80)
214   - print("测试 4: ES Query 结构验证 (debug=True)")
215   - print("="*80)
216   -
217   - payload = {
218   - "query": "手机",
219   - "size": 1,
220   - "filters": {
221   - "category1_name": "电子产品",
222   - "specifications": {"name": "颜色", "value": "白色"}
223   - },
224   - "facets": [
225   - {
226   - "field": "category1_name",
227   - "size": 5,
228   - "multi_select": True
229   - },
230   - {
231   - "field": "specifications.颜色",
232   - "size": 5,
233   - "multi_select": True
234   - }
235   - ],
236   - "debug": True
237   - }
238   -
239   - headers = {"X-Tenant-ID": TENANT_ID}
240   -
241   - try:
242   - response = requests.post(API_URL, json=payload, headers=headers)
243   - response.raise_for_status()
244   - result = response.json()
245   -
246   - print(f"\n✓ 请求成功")
247   -
248   - if result.get('debug_info') and result['debug_info'].get('es_query'):
249   - es_query = result['debug_info']['es_query']
250   -
251   - # 检查 post_filter
252   - if 'post_filter' in es_query:
253   - print("\n ✓ ES Query 包含 post_filter:")
254   - print(f" {json.dumps(es_query['post_filter'], indent=4, ensure_ascii=False)[:200]}...")
255   - else:
256   - print("\n ℹ ES Query 不包含 post_filter(可能没有 multi-select 过滤器)")
257   -
258   - # 检查 query.bool.filter
259   - if 'query' in es_query and 'bool' in es_query['query']:
260   - filters = es_query['query']['bool'].get('filter', [])
261   - print(f"\n ✓ Query filters 数量: {len(filters)}")
262   -
263   - else:
264   - print("\n ⚠ 警告: debug_info 中没有 es_query")
265   -
266   - except requests.exceptions.RequestException as e:
267   - print(f"\n✗ 请求失败: {e}")
268   - except Exception as e:
269   - print(f"\n✗ 错误: {e}")
270   -
271   -
272   -if __name__ == "__main__":
273   - print("\n" + "="*80)
274   - print("Multi-Select Faceting 功能测试")
275   - print("="*80)
276   - print(f"\nAPI URL: {API_URL}")
277   - print(f"Tenant ID: {TENANT_ID}")
278   -
279   - # 运行测试
280   - test_standard_faceting()
281   - test_multi_select_faceting()
282   - test_specifications_multi_select()
283   - test_es_query_structure()
284   -
285   - print("\n" + "="*80)
286   - print("测试完成")
287   - print("="*80)
288   -