Commit 6d524cb4993a3530ae4e99378afb20e77a6bb528
1 parent
c581becd
docs优化
Showing
4 changed files
with
2 additions
and
710 deletions
Show diff stats
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 "http://120.76.41.98:6002/search/" \ |
| 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 | - |