Commit 13320ac6b0d8b3ca7a554e3d9923b019faaf7fb9
1 parent
e7ad2b4a
分面接口修改:
{
"facets": [
{
"field": "category1_name",
"size": 15,
"type": "terms"
},
"specifications.color",
"specifications.size"
]
}
{
"facets": [
{"field": "category1_name", "size": 15, "type": "terms"},
{"field": "specifications.color", "size": 10, "type": "terms"},
{"field": "specifications.size", "size": 10, "type": "terms"}
]
}
之前是上面的接口形式,主要是考虑 属性的分面, 因为 款式都是有限的 不需要设定 "size": 10, "type": "terms" 这些参数。
但是从接口设计层面,最好按下面这样,这样的话 specifications.color 和 category1_name 的组装格式 完全一样。前端不需要感知 属性分面 和 类别等其他字段分面的差异。
Showing
10 changed files
with
273 additions
and
175 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | +#!/bin/bash | |
| 2 | +source /home/tw/miniconda3/etc/profile.d/conda.sh | |
| 3 | +conda activate searchengine | |
| 4 | + | |
| 5 | +# 如果需要加载 .env 中的环境变量 | |
| 6 | +if [ -f .env ]; then | |
| 7 | + set -a # 自动导出所有变量 | |
| 8 | + source <(grep -v '^#' .env | grep -v '^$' | sed 's/#.*$//' | sed 's/\r$//') | |
| 9 | + set +a # 关闭自动导出 | |
| 10 | +fi | |
| 11 | + | |
| 12 | +echo "Environment activated: searchengine" | ... | ... |
api/models.py
| ... | ... | @@ -110,31 +110,34 @@ class SearchRequest(BaseModel): |
| 110 | 110 | ) |
| 111 | 111 | |
| 112 | 112 | # 排序 |
| 113 | - sort_by: Optional[str] = Field(None, description="排序字段名(如 'min_price', 'max_price', 'title')") | |
| 114 | - sort_order: Optional[str] = Field("desc", description="排序方向: 'asc'(升序)或 'desc'(降序)") | |
| 113 | + sort_by: Optional[str] = Field(None, description="排序字段名。支持:'price'(价格,自动根据sort_order选择min_price或max_price)、'sales'(销量)、'create_time'(创建时间)、'update_time'(更新时间)") | |
| 114 | + sort_order: Optional[str] = Field("desc", description="排序方向: 'asc'(升序)或 'desc'(降序)。注意:price+asc=价格从低到高,price+desc=价格从高到低") | |
| 115 | 115 | |
| 116 | - # 分面搜索 - 简化接口 | |
| 117 | - facets: Optional[List[Union[str, FacetConfig]]] = Field( | |
| 116 | + # 分面搜索 | |
| 117 | + facets: Optional[List[FacetConfig]] = Field( | |
| 118 | 118 | None, |
| 119 | - description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象。支持 specifications 分面:\"specifications\"(所有name)或 \"specifications.color\"(指定name)", | |
| 119 | + description="分面配置对象列表。支持 specifications 分面:field=\"specifications\"(所有规格名称)或 field=\"specifications.color\"(指定规格名称)", | |
| 120 | 120 | json_schema_extra={ |
| 121 | 121 | "examples": [ |
| 122 | - # 简单模式:只指定字段名,使用默认配置 | |
| 123 | - ["category1_name", "category2_name", "specifications"], | |
| 124 | - # 指定specifications的某个name | |
| 125 | - ["specifications.颜色", "specifications.尺寸"], | |
| 126 | - # 高级模式:详细配置 | |
| 127 | 122 | [ |
| 128 | - {"field": "category1_name", "size": 15}, | |
| 123 | + {"field": "category1_name", "size": 15, "type": "terms"}, | |
| 124 | + {"field": "category2_name", "size": 10, "type": "terms"}, | |
| 125 | + {"field": "specifications.color", "size": 20, "type": "terms"}, | |
| 126 | + {"field": "specifications.size", "size": 15, "type": "terms"} | |
| 127 | + ], | |
| 128 | + [ | |
| 129 | + {"field": "category1_name", "size": 15, "type": "terms"}, | |
| 129 | 130 | { |
| 130 | 131 | "field": "min_price", |
| 131 | 132 | "type": "range", |
| 132 | 133 | "ranges": [ |
| 133 | 134 | {"key": "0-50", "to": 50}, |
| 134 | - {"key": "50-100", "from": 50, "to": 100} | |
| 135 | + {"key": "50-100", "from": 50, "to": 100}, | |
| 136 | + {"key": "100-200", "from": 100, "to": 200}, | |
| 137 | + {"key": "200+", "from": 200} | |
| 135 | 138 | ] |
| 136 | 139 | }, |
| 137 | - "specifications" # 所有specifications name的分面 | |
| 140 | + {"field": "specifications", "size": 10, "type": "terms"} | |
| 138 | 141 | ] |
| 139 | 142 | ] |
| 140 | 143 | } | ... | ... |
docs/搜索API对接指南.md
| ... | ... | @@ -50,7 +50,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 50 | 50 | "gte": "2020-01-01T00:00:00Z" |
| 51 | 51 | } |
| 52 | 52 | }, |
| 53 | - "sort_by": "min_price", | |
| 53 | + "sort_by": "price", | |
| 54 | 54 | "sort_order": "asc" |
| 55 | 55 | }' |
| 56 | 56 | ``` |
| ... | ... | @@ -63,7 +63,11 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 63 | 63 | -H "X-Tenant-ID: 162" \ |
| 64 | 64 | -d '{ |
| 65 | 65 | "query": "芭比娃娃", |
| 66 | - "facets": ["category1_name", "specifications.color", "specifications.size", "specifications.material"], | |
| 66 | + "facets": [ | |
| 67 | + {"field": "category1_name", "size": 10, "type": "terms"}, | |
| 68 | + {"field": "specifications.color", "size": 10, "type": "terms"}, | |
| 69 | + {"field": "specifications.size", "size": 10, "type": "terms"} | |
| 70 | + ], | |
| 67 | 71 | "min_score": 0.2 |
| 68 | 72 | }' |
| 69 | 73 | ``` |
| ... | ... | @@ -124,8 +128,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 124 | 128 | | `filters` | object | N | null | 精确匹配过滤器(见下文) | |
| 125 | 129 | | `range_filters` | object | N | null | 数值范围过滤器(见下文) | |
| 126 | 130 | | `facets` | array | N | null | 分面配置(见下文) | |
| 127 | -| `sort_by` | string | N | null | 排序字段名(如 `min_price`, `max_price`) | | |
| 128 | -| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | |
| 131 | +| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 | | |
| 132 | +| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) | | |
| 129 | 133 | | `min_score` | float | N | null | 最小相关性分数阈值 | |
| 130 | 134 | | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(店铺配置)。指定后,每个SPU下的SKU将按这些维度的组合进行分组,每个组合只返回第一个SKU。支持的值:`option1`、`option2`、`option3` 或选项名称(如 `color`、`size`)。详见下文说明 | |
| 131 | 135 | | `debug` | boolean | N | false | 是否返回调试信息 | |
| ... | ... | @@ -263,14 +267,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 263 | 267 | |
| 264 | 268 | 用于生成分面统计(分组聚合),常用于构建筛选器UI。 |
| 265 | 269 | |
| 266 | -**简单模式**(字符串数组): | |
| 267 | -```json | |
| 268 | -{ | |
| 269 | - "facets": ["category1_name", "category2_name", "category3_name", "specifications"] | |
| 270 | -} | |
| 271 | -``` | |
| 270 | +**配置格式**(对象数组): | |
| 272 | 271 | |
| 273 | -**高级模式**(配置对象数组): | |
| 274 | 272 | ```json |
| 275 | 273 | { |
| 276 | 274 | "facets": [ |
| ... | ... | @@ -280,6 +278,16 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 280 | 278 | "type": "terms" |
| 281 | 279 | }, |
| 282 | 280 | { |
| 281 | + "field": "category2_name", | |
| 282 | + "size": 10, | |
| 283 | + "type": "terms" | |
| 284 | + }, | |
| 285 | + { | |
| 286 | + "field": "specifications.color", | |
| 287 | + "size": 20, | |
| 288 | + "type": "terms" | |
| 289 | + }, | |
| 290 | + { | |
| 283 | 291 | "field": "min_price", |
| 284 | 292 | "type": "range", |
| 285 | 293 | "ranges": [ |
| ... | ... | @@ -288,8 +296,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 288 | 296 | {"key": "100-200", "from": 100, "to": 200}, |
| 289 | 297 | {"key": "200+", "from": 200} |
| 290 | 298 | ] |
| 291 | - }, | |
| 292 | - "specifications" // 规格分面(特殊处理:嵌套聚合,按name分组,然后按value聚合) | |
| 299 | + } | |
| 293 | 300 | ] |
| 294 | 301 | } |
| 295 | 302 | ``` |
| ... | ... | @@ -298,18 +305,35 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 298 | 305 | |
| 299 | 306 | `specifications` 是嵌套字段,支持两种分面模式: |
| 300 | 307 | |
| 301 | -**模式1:所有规格名称的分面** (`"specifications"`): | |
| 308 | +**模式1:所有规格名称的分面**: | |
| 302 | 309 | ```json |
| 303 | 310 | { |
| 304 | - "facets": ["specifications"] | |
| 311 | + "facets": [ | |
| 312 | + { | |
| 313 | + "field": "specifications", | |
| 314 | + "size": 10, | |
| 315 | + "type": "terms" | |
| 316 | + } | |
| 317 | + ] | |
| 305 | 318 | } |
| 306 | 319 | ``` |
| 307 | 320 | 返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 |
| 308 | 321 | |
| 309 | -**模式2:指定规格名称的分面** (`"specifications.color"`): | |
| 322 | +**模式2:指定规格名称的分面**: | |
| 310 | 323 | ```json |
| 311 | 324 | { |
| 312 | - "facets": ["specifications.color", "specifications.size", "specifications.material"] | |
| 325 | + "facets": [ | |
| 326 | + { | |
| 327 | + "field": "specifications.color", | |
| 328 | + "size": 20, | |
| 329 | + "type": "terms" | |
| 330 | + }, | |
| 331 | + { | |
| 332 | + "field": "specifications.size", | |
| 333 | + "size": 15, | |
| 334 | + "type": "terms" | |
| 335 | + } | |
| 336 | + ] | |
| 313 | 337 | } |
| 314 | 338 | ``` |
| 315 | 339 | 只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。 |
| ... | ... | @@ -564,6 +588,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 564 | 588 | | `sku_weights` | array[integer] | 所有SKU重量列表 | |
| 565 | 589 | | `sku_weight_units` | array[string] | 所有SKU重量单位列表 | |
| 566 | 590 | | `total_inventory` | integer | 总库存 | |
| 591 | +| `sales` | integer | 销量(展示销量) | | |
| 567 | 592 | | `option1_name` | string | 选项1名称(如"color") | |
| 568 | 593 | | `option2_name` | string | 选项2名称(如"size") | |
| 569 | 594 | | `option3_name` | string | 选项3名称 | |
| ... | ... | @@ -605,11 +630,45 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 605 | 630 | "query": "玩具", |
| 606 | 631 | "size": 20, |
| 607 | 632 | "from": 0, |
| 608 | - "sort_by": "min_price", | |
| 633 | + "sort_by": "price", | |
| 609 | 634 | "sort_order": "asc" |
| 610 | 635 | } |
| 611 | 636 | ``` |
| 612 | 637 | |
| 638 | +**需求**: 搜索"玩具",按价格从高到低排序 | |
| 639 | + | |
| 640 | +```json | |
| 641 | +{ | |
| 642 | + "query": "玩具", | |
| 643 | + "size": 20, | |
| 644 | + "from": 0, | |
| 645 | + "sort_by": "price", | |
| 646 | + "sort_order": "desc" | |
| 647 | +} | |
| 648 | +``` | |
| 649 | + | |
| 650 | +**需求**: 搜索"玩具",按销量从高到低排序 | |
| 651 | + | |
| 652 | +```json | |
| 653 | +{ | |
| 654 | + "query": "玩具", | |
| 655 | + "size": 20, | |
| 656 | + "from": 0, | |
| 657 | + "sort_by": "sales", | |
| 658 | + "sort_order": "desc" | |
| 659 | +} | |
| 660 | +``` | |
| 661 | + | |
| 662 | +**需求**: 搜索"玩具",按默认(相关性)排序 | |
| 663 | + | |
| 664 | +```json | |
| 665 | +{ | |
| 666 | + "query": "玩具", | |
| 667 | + "size": 20, | |
| 668 | + "from": 0 | |
| 669 | +} | |
| 670 | +``` | |
| 671 | + | |
| 613 | 672 | ### 场景2:SKU筛选(按维度过滤) |
| 614 | 673 | |
| 615 | 674 | **需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU |
| ... | ... | @@ -650,7 +709,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 650 | 709 | |
| 651 | 710 | ### 场景4:带分面的商品搜索 |
| 652 | 711 | |
| 653 | -**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 | |
| 712 | +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器 | |
| 654 | 713 | |
| 655 | 714 | ```json |
| 656 | 715 | { |
| ... | ... | @@ -658,9 +717,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 658 | 717 | "size": 20, |
| 659 | 718 | "language": "zh", |
| 660 | 719 | "facets": [ |
| 661 | - "category1_name", | |
| 662 | - "category2_name", | |
| 663 | - "specifications" | |
| 720 | + {"field": "category1_name", "size": 15, "type": "terms"}, | |
| 721 | + {"field": "category2_name", "size": 10, "type": "terms"}, | |
| 722 | + {"field": "specifications", "size": 10, "type": "terms"} | |
| 664 | 723 | ] |
| 665 | 724 | } |
| 666 | 725 | ``` |
| ... | ... | @@ -686,7 +745,8 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 686 | 745 | "facets": [ |
| 687 | 746 | { |
| 688 | 747 | "field": "category1_name", |
| 689 | - "size": 15 | |
| 748 | + "size": 15, | |
| 749 | + "type": "terms" | |
| 690 | 750 | }, |
| 691 | 751 | { |
| 692 | 752 | "field": "min_price", |
| ... | ... | @@ -698,7 +758,11 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 698 | 758 | {"key": "200+", "from": 200} |
| 699 | 759 | ] |
| 700 | 760 | }, |
| 701 | - "specifications" | |
| 761 | + { | |
| 762 | + "field": "specifications", | |
| 763 | + "size": 10, | |
| 764 | + "type": "terms" | |
| 765 | + } | |
| 702 | 766 | ], |
| 703 | 767 | "sort_by": "min_price", |
| 704 | 768 | "sort_order": "asc" |
| ... | ... | @@ -768,18 +832,23 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 768 | 832 | "query": "手机", |
| 769 | 833 | "size": 20, |
| 770 | 834 | "language": "zh", |
| 771 | - "facets": ["specifications"] | |
| 835 | + "facets": [ | |
| 836 | + {"field": "specifications", "size": 10, "type": "terms"} | |
| 837 | + ] | |
| 772 | 838 | } |
| 773 | 839 | ``` |
| 774 | 840 | |
| 775 | -**需求**: 只获取"color"规格的分面统计 | |
| 841 | +**需求**: 只获取"color"和"size"规格的分面统计 | |
| 776 | 842 | |
| 777 | 843 | ```json |
| 778 | 844 | { |
| 779 | 845 | "query": "手机", |
| 780 | 846 | "size": 20, |
| 781 | 847 | "language": "zh", |
| 782 | - "facets": ["specifications.color", "specifications.size"] | |
| 848 | + "facets": [ | |
| 849 | + {"field": "specifications.color", "size": 20, "type": "terms"}, | |
| 850 | + {"field": "specifications.size", "size": 15, "type": "terms"} | |
| 851 | + ] | |
| 783 | 852 | } |
| 784 | 853 | ``` |
| 785 | 854 | |
| ... | ... | @@ -800,10 +869,10 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 800 | 869 | } |
| 801 | 870 | }, |
| 802 | 871 | "facets": [ |
| 803 | - "category1_name", | |
| 804 | - "category2_name", | |
| 805 | - "specifications.color", | |
| 806 | - "specifications.size" | |
| 872 | + {"field": "category1_name", "size": 15, "type": "terms"}, | |
| 873 | + {"field": "category2_name", "size": 10, "type": "terms"}, | |
| 874 | + {"field": "specifications.color", "size": 20, "type": "terms"}, | |
| 875 | + {"field": "specifications.size", "size": 15, "type": "terms"} | |
| 807 | 876 | ] |
| 808 | 877 | } |
| 809 | 878 | ``` |
| ... | ... | @@ -1006,6 +1075,7 @@ curl "http://localhost:6002/search/12345" |
| 1006 | 1075 | | `sku_weights` | long | SKU重量列表(数组) | |
| 1007 | 1076 | | `sku_weight_units` | keyword | SKU重量单位列表(数组) | |
| 1008 | 1077 | | `total_inventory` | long | 总库存 | |
| 1078 | +| `sales` | long | 销量(展示销量) | | |
| 1009 | 1079 | | `skus` | nested | SKU详细信息(嵌套对象数组) | |
| 1010 | 1080 | | `create_time`, `update_time` | date | 创建/更新时间 | |
| 1011 | 1081 | | `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | |
| ... | ... | @@ -1038,11 +1108,15 @@ curl "http://localhost:6002/search/12345" |
| 1038 | 1108 | |
| 1039 | 1109 | #### 排序字段 |
| 1040 | 1110 | |
| 1041 | -- `min_price`: 最低价格 | |
| 1042 | -- `max_price`: 最高价格 | |
| 1111 | +- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price) | |
| 1112 | +- `sales`: 销量 | |
| 1043 | 1113 | - `create_time`: 创建时间 |
| 1044 | 1114 | - `update_time`: 更新时间 |
| 1045 | -- `relevance_score`: 相关性分数(默认) | |
| 1115 | +- `relevance_score`: 相关性分数(默认,不指定sort_by时使用) | |
| 1116 | + | |
| 1117 | +**注意**: 前端只需传 `price`,后端会自动处理: | |
| 1118 | +- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高) | |
| 1119 | +- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低) | |
| 1046 | 1120 | |
| 1047 | 1121 | ### 支持的分析器 |
| 1048 | 1122 | ... | ... |
docs/系统设计文档.md
| ... | ... | @@ -699,45 +699,44 @@ Elasticsearch |
| 699 | 699 | |
| 700 | 700 | #### 8.3.2 Facets 配置数据流 |
| 701 | 701 | |
| 702 | -**输入格式**:`List[Union[str, FacetConfig]]` | |
| 702 | +**输入格式**:`List[FacetConfig]` | |
| 703 | 703 | |
| 704 | -- **简单模式**:字符串列表(字段名),使用默认配置 | |
| 705 | - ```json | |
| 706 | - ["category1_name", "category2_name", "specifications"] | |
| 707 | - ``` | |
| 708 | - | |
| 709 | -- **Specifications分面**: | |
| 710 | - - 所有规格名称:`"specifications"` - 返回所有name及其value列表 | |
| 711 | - - 指定规格名称:`"specifications.color"` - 只返回指定name的value列表 | |
| 704 | +**配置对象列表**:所有分面配置必须使用 FacetConfig 对象 | |
| 705 | +```json | |
| 706 | +[ | |
| 707 | + { | |
| 708 | + "field": "category1_name", | |
| 709 | + "size": 15, | |
| 710 | + "type": "terms" | |
| 711 | + }, | |
| 712 | + { | |
| 713 | + "field": "specifications.color", | |
| 714 | + "size": 20, | |
| 715 | + "type": "terms" | |
| 716 | + }, | |
| 717 | + { | |
| 718 | + "field": "min_price", | |
| 719 | + "type": "range", | |
| 720 | + "ranges": [ | |
| 721 | + {"key": "0-50", "to": 50}, | |
| 722 | + {"key": "50-100", "from": 50, "to": 100} | |
| 723 | + ] | |
| 724 | + } | |
| 725 | +] | |
| 726 | +``` | |
| 712 | 727 | |
| 713 | -- **高级模式**:FacetConfig 对象列表,支持自定义配置 | |
| 714 | - ```json | |
| 715 | - [ | |
| 716 | - { | |
| 717 | - "field": "category1_name", | |
| 718 | - "size": 15, | |
| 719 | - "type": "terms" | |
| 720 | - }, | |
| 721 | - { | |
| 722 | - "field": "min_price", | |
| 723 | - "type": "range", | |
| 724 | - "ranges": [ | |
| 725 | - {"key": "0-50", "to": 50}, | |
| 726 | - {"key": "50-100", "from": 50, "to": 100} | |
| 727 | - ] | |
| 728 | - }, | |
| 729 | - "specifications.color" // 指定规格名称的分面 | |
| 730 | - ] | |
| 731 | - ``` | |
| 728 | +**Specifications 分面支持**: | |
| 729 | +- 所有规格名称:`field: "specifications"` - 返回所有 name 及其 value 列表 | |
| 730 | +- 指定规格名称:`field: "specifications.color"` - 只返回指定 name 的 value 列表 | |
| 732 | 731 | |
| 733 | 732 | **数据流**: |
| 734 | -1. API 层:接收 `List[Union[str, FacetConfig]]` | |
| 735 | -2. Searcher 层:透传,不做转换 | |
| 736 | -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 | |
| 733 | +1. API 层:接收 `List[FacetConfig]`,Pydantic 验证参数 | |
| 734 | +2. Searcher 层:透传 FacetConfig 对象列表 | |
| 735 | +3. ES Query Builder:解析 FacetConfig 对象 | |
| 737 | 736 | - 检测 `"specifications"` 或 `"specifications.{name}"` 格式 |
| 738 | - - 构建对应的嵌套聚合查询 | |
| 739 | -4. 输出:转换为 ES 聚合查询(包括specifications嵌套聚合) | |
| 740 | -5. Result Formatter:格式化ES聚合结果,处理specifications嵌套结构 | |
| 737 | + - 构建对应的嵌套聚合查询或普通聚合查询 | |
| 738 | +4. 输出:转换为 ES 聚合查询(包括 specifications 嵌套聚合) | |
| 739 | +5. Result Formatter:格式化 ES 聚合结果,处理 specifications 嵌套结构 | |
| 741 | 740 | |
| 742 | 741 | #### 8.3.3 Range Filters 数据流 |
| 743 | 742 | ... | ... |
docs/索引字段说明v2.md
| ... | ... | @@ -284,11 +284,12 @@ |
| 284 | 284 | | `sku_weights` | long | 所有 SKU 重量列表(数组) | 从所有 SKU 重量汇总 | |
| 285 | 285 | | `sku_weight_units` | keyword | 所有 SKU 重量单位列表(数组) | 从所有 SKU 重量单位汇总 | |
| 286 | 286 | |
| 287 | -### 9. 库存字段 | |
| 287 | +### 9. 库存与销量字段 | |
| 288 | 288 | |
| 289 | 289 | | 字段名 | ES类型 | 说明 | 数据来源 | |
| 290 | 290 | |--------|--------|------|----------| |
| 291 | 291 | | `total_inventory` | long | 总库存(所有 SKU 库存之和) | 从所有 SKU 库存汇总 | |
| 292 | +| `sales` | long | 销量(展示销量) | MySQL: `shoplazza_product_spu.fake_sales` | | |
| 292 | 293 | |
| 293 | 294 | ### 10. SKU 嵌套字段 |
| 294 | 295 | |
| ... | ... | @@ -363,6 +364,16 @@ |
| 363 | 364 | - `vendor_zh.keyword`, `vendor_en.keyword` |
| 364 | 365 | - `specifications` (嵌套查询) |
| 365 | 366 | - `min_price`, `max_price` (范围过滤) |
| 367 | +- `sales` (范围过滤) | |
| 368 | +- `total_inventory` (范围过滤) | |
| 369 | + | |
| 370 | +### 排序字段 | |
| 371 | + | |
| 372 | +- `price`: 价格(前端传入,后端自动映射:asc→min_price,desc→max_price) | |
| 373 | +- `sales`: 销量 | |
| 374 | +- `create_time`: 创建时间 | |
| 375 | +- `update_time`: 更新时间 | |
| 376 | +- `relevance_score`: 相关性分数(默认) | |
| 366 | 377 | |
| 367 | 378 | ### 分面字段(聚合统计) |
| 368 | 379 | ... | ... |
frontend/index.html
| ... | ... | @@ -94,11 +94,18 @@ |
| 94 | 94 | <span class="arrow-down" data-field="create_time" data-order="asc" onclick="sortByField('create_time', 'asc')">▼</span> |
| 95 | 95 | </span> |
| 96 | 96 | </button> |
| 97 | - <button class="sort-btn" data-sort="min_price"> | |
| 97 | + <button class="sort-btn" data-sort="price"> | |
| 98 | 98 | By Price |
| 99 | 99 | <span class="sort-arrows"> |
| 100 | - <span class="arrow-up" data-field="min_price" data-order="asc" onclick="sortByField('min_price', 'asc')">▲</span> | |
| 101 | - <span class="arrow-down" data-field="min_price" data-order="desc" onclick="sortByField('min_price', 'desc')">▼</span> | |
| 100 | + <span class="arrow-up" data-field="price" data-order="asc" onclick="sortByField('price', 'asc')">▲</span> | |
| 101 | + <span class="arrow-down" data-field="price" data-order="desc" onclick="sortByField('price', 'desc')">▼</span> | |
| 102 | + </span> | |
| 103 | + </button> | |
| 104 | + <button class="sort-btn" data-sort="sales"> | |
| 105 | + By Sales | |
| 106 | + <span class="sort-arrows"> | |
| 107 | + <span class="arrow-up" data-field="sales" data-order="desc" onclick="sortByField('sales', 'desc')">▲</span> | |
| 108 | + <span class="arrow-down" data-field="sales" data-order="asc" onclick="sortByField('sales', 'asc')">▼</span> | |
| 102 | 109 | </span> |
| 103 | 110 | </button> |
| 104 | 111 | |
| ... | ... | @@ -135,6 +142,6 @@ |
| 135 | 142 | <p>SearchEngine © 2025 | API: <span id="apiUrl">Loading...</span></p> |
| 136 | 143 | </footer> |
| 137 | 144 | |
| 138 | - <script src="/static/js/app.js?v=3.4"></script> | |
| 145 | + <script src="/static/js/app.js?v=3.7"></script> | |
| 139 | 146 | </body> |
| 140 | 147 | </html> | ... | ... |
frontend/static/js/app.js
| ... | ... | @@ -86,10 +86,10 @@ async function performSearch(page = 1) { |
| 86 | 86 | |
| 87 | 87 | // Define facets (一级分类 + 三个属性分面) |
| 88 | 88 | const facets = [ |
| 89 | - "category1_name", // 一级分类 | |
| 90 | - "specifications.color", // 颜色属性 | |
| 91 | - "specifications.size", // 尺寸属性 | |
| 92 | - "specifications.material" // 材质属性 | |
| 89 | + { field: "category1_name", size: 15, type: "terms" }, // 一级分类 | |
| 90 | + { field: "specifications.color", size: 20, type: "terms" }, // 颜色属性 | |
| 91 | + { field: "specifications.size", size: 15, type: "terms" }, // 尺寸属性 | |
| 92 | + { field: "specifications.material", size: 10, type: "terms" } // 材质属性 | |
| 93 | 93 | ]; |
| 94 | 94 | |
| 95 | 95 | // Show loading | ... | ... |
indexer/spu_transformer.py
| ... | ... | @@ -98,6 +98,7 @@ class SPUTransformer: |
| 98 | 98 | image_src, image_width, image_height, image_path, image_alt, |
| 99 | 99 | tags, note, category, category_id, category_google_id, |
| 100 | 100 | category_level, category_path, |
| 101 | + fake_sales, display_fake_sales, | |
| 101 | 102 | tenant_id, creator, create_time, updater, update_time, deleted |
| 102 | 103 | FROM shoplazza_product_spu |
| 103 | 104 | WHERE tenant_id = :tenant_id AND deleted = 0 |
| ... | ... | @@ -447,6 +448,15 @@ class SPUTransformer: |
| 447 | 448 | image_src = f"//{image_src}" if image_src.startswith('//') else image_src |
| 448 | 449 | doc['image_url'] = image_src |
| 449 | 450 | |
| 451 | + # Sales (fake_sales) | |
| 452 | + if pd.notna(spu_row.get('fake_sales')): | |
| 453 | + try: | |
| 454 | + doc['sales'] = int(spu_row['fake_sales']) | |
| 455 | + except (ValueError, TypeError): | |
| 456 | + doc['sales'] = 0 | |
| 457 | + else: | |
| 458 | + doc['sales'] = 0 | |
| 459 | + | |
| 450 | 460 | # Process SKUs and build specifications |
| 451 | 461 | skus_list = [] |
| 452 | 462 | prices = [] | ... | ... |
search/es_query_builder.py
| ... | ... | @@ -519,7 +519,7 @@ class ESQueryBuilder: |
| 519 | 519 | |
| 520 | 520 | Args: |
| 521 | 521 | es_query: Existing ES query |
| 522 | - sort_by: Field name for sorting | |
| 522 | + sort_by: Field name for sorting (支持 'price' 自动映射) | |
| 523 | 523 | sort_order: Sort order: 'asc' or 'desc' |
| 524 | 524 | |
| 525 | 525 | Returns: |
| ... | ... | @@ -531,6 +531,13 @@ class ESQueryBuilder: |
| 531 | 531 | if not sort_order: |
| 532 | 532 | sort_order = "desc" |
| 533 | 533 | |
| 534 | + # Auto-map 'price' to 'min_price' or 'max_price' based on sort_order | |
| 535 | + if sort_by == "price": | |
| 536 | + if sort_order.lower() == "asc": | |
| 537 | + sort_by = "min_price" # 价格从低到高 | |
| 538 | + else: | |
| 539 | + sort_by = "max_price" # 价格从高到低 | |
| 540 | + | |
| 534 | 541 | if "sort" not in es_query: |
| 535 | 542 | es_query["sort"] = [] |
| 536 | 543 | |
| ... | ... | @@ -546,20 +553,18 @@ class ESQueryBuilder: |
| 546 | 553 | |
| 547 | 554 | def build_facets( |
| 548 | 555 | self, |
| 549 | - facet_configs: Optional[List[Union[str, 'FacetConfig']]] = None | |
| 556 | + facet_configs: Optional[List['FacetConfig']] = None | |
| 550 | 557 | ) -> Dict[str, Any]: |
| 551 | 558 | """ |
| 552 | 559 | 构建分面聚合。 |
| 553 | 560 | |
| 554 | - 支持: | |
| 555 | - 1. 分类分面:category1_name, category2_name, category3_name, category_name | |
| 556 | - 2. specifications分面:嵌套聚合,按name聚合,然后按value聚合 | |
| 557 | - | |
| 558 | 561 | Args: |
| 559 | - facet_configs: 分面配置列表(标准格式): | |
| 560 | - - str: 字段名,使用默认 terms 配置 | |
| 561 | - - FacetConfig: 详细的分面配置对象 | |
| 562 | - - 特殊值 "specifications": 构建specifications嵌套分面 | |
| 562 | + facet_configs: 分面配置对象列表 | |
| 563 | + | |
| 564 | + 支持的字段类型: | |
| 565 | + - 普通字段: 如 "category1_name"(terms 或 range 类型) | |
| 566 | + - specifications: "specifications"(返回所有规格名称及其值) | |
| 567 | + - specifications.{name}: 如 "specifications.color"(返回指定规格名称的值) | |
| 563 | 568 | |
| 564 | 569 | Returns: |
| 565 | 570 | ES aggregations 字典 |
| ... | ... | @@ -570,99 +575,76 @@ class ESQueryBuilder: |
| 570 | 575 | aggs = {} |
| 571 | 576 | |
| 572 | 577 | for config in facet_configs: |
| 573 | - # 特殊处理:specifications嵌套分面 | |
| 574 | - if isinstance(config, str): | |
| 575 | - # 格式1: "specifications" - 返回所有name的分面 | |
| 576 | - if config == "specifications": | |
| 577 | - aggs["specifications_facet"] = { | |
| 578 | - "nested": { | |
| 579 | - "path": "specifications" | |
| 580 | - }, | |
| 581 | - "aggs": { | |
| 582 | - "by_name": { | |
| 583 | - "terms": { | |
| 584 | - "field": "specifications.name", | |
| 585 | - "size": 20, | |
| 586 | - "order": {"_count": "desc"} | |
| 587 | - }, | |
| 588 | - "aggs": { | |
| 589 | - "value_counts": { | |
| 590 | - "terms": { | |
| 591 | - "field": "specifications.value", | |
| 592 | - "size": 10, | |
| 593 | - "order": {"_count": "desc"} | |
| 594 | - } | |
| 578 | + field = config.field | |
| 579 | + size = config.size | |
| 580 | + facet_type = config.type | |
| 581 | + | |
| 582 | + # 处理 specifications(所有规格名称) | |
| 583 | + if field == "specifications": | |
| 584 | + aggs["specifications_facet"] = { | |
| 585 | + "nested": {"path": "specifications"}, | |
| 586 | + "aggs": { | |
| 587 | + "by_name": { | |
| 588 | + "terms": { | |
| 589 | + "field": "specifications.name", | |
| 590 | + "size": 20, | |
| 591 | + "order": {"_count": "desc"} | |
| 592 | + }, | |
| 593 | + "aggs": { | |
| 594 | + "value_counts": { | |
| 595 | + "terms": { | |
| 596 | + "field": "specifications.value", | |
| 597 | + "size": size, | |
| 598 | + "order": {"_count": "desc"} | |
| 595 | 599 | } |
| 596 | 600 | } |
| 597 | 601 | } |
| 598 | 602 | } |
| 599 | 603 | } |
| 600 | - continue | |
| 601 | - | |
| 602 | - # 格式2: "specifications.color" 或 "specifications.颜色" - 只返回指定name的value列表 | |
| 603 | - if config.startswith("specifications."): | |
| 604 | - name = config[len("specifications."):] | |
| 605 | - agg_name = f"specifications_{name}_facet" | |
| 606 | - aggs[agg_name] = { | |
| 607 | - "nested": { | |
| 608 | - "path": "specifications" | |
| 609 | - }, | |
| 610 | - "aggs": { | |
| 611 | - "filter_by_name": { | |
| 612 | - "filter": { | |
| 613 | - "term": {"specifications.name": name} | |
| 614 | - }, | |
| 615 | - "aggs": { | |
| 616 | - "value_counts": { | |
| 617 | - "terms": { | |
| 618 | - "field": "specifications.value", | |
| 619 | - "size": 10, | |
| 620 | - "order": {"_count": "desc"} | |
| 621 | - } | |
| 604 | + } | |
| 605 | + continue | |
| 606 | + | |
| 607 | + # 处理 specifications.{name}(指定规格名称) | |
| 608 | + if field.startswith("specifications."): | |
| 609 | + name = field[len("specifications."):] | |
| 610 | + agg_name = f"specifications_{name}_facet" | |
| 611 | + aggs[agg_name] = { | |
| 612 | + "nested": {"path": "specifications"}, | |
| 613 | + "aggs": { | |
| 614 | + "filter_by_name": { | |
| 615 | + "filter": {"term": {"specifications.name": name}}, | |
| 616 | + "aggs": { | |
| 617 | + "value_counts": { | |
| 618 | + "terms": { | |
| 619 | + "field": "specifications.value", | |
| 620 | + "size": size, | |
| 621 | + "order": {"_count": "desc"} | |
| 622 | 622 | } |
| 623 | 623 | } |
| 624 | 624 | } |
| 625 | 625 | } |
| 626 | 626 | } |
| 627 | - continue | |
| 627 | + } | |
| 628 | + continue | |
| 629 | + | |
| 630 | + # 处理普通字段 | |
| 631 | + agg_name = f"{field}_facet" | |
| 628 | 632 | |
| 629 | - # 简单模式:只有字段名(字符串,非specifications) | |
| 630 | - if isinstance(config, str): | |
| 631 | - field = config | |
| 632 | - agg_name = f"{field}_facet" | |
| 633 | + if facet_type == 'terms': | |
| 633 | 634 | aggs[agg_name] = { |
| 634 | 635 | "terms": { |
| 635 | 636 | "field": field, |
| 636 | - "size": 10, | |
| 637 | + "size": size, | |
| 637 | 638 | "order": {"_count": "desc"} |
| 638 | 639 | } |
| 639 | 640 | } |
| 640 | - continue | |
| 641 | - | |
| 642 | - # 高级模式:FacetConfig 对象 | |
| 643 | - else: | |
| 644 | - # 此时 config 应该是 FacetConfig 对象 | |
| 645 | - field = config.field | |
| 646 | - facet_type = config.type | |
| 647 | - size = config.size | |
| 648 | - agg_name = f"{field}_facet" | |
| 649 | - | |
| 650 | - if facet_type == 'terms': | |
| 641 | + elif facet_type == 'range': | |
| 642 | + if config.ranges: | |
| 651 | 643 | aggs[agg_name] = { |
| 652 | - "terms": { | |
| 644 | + "range": { | |
| 653 | 645 | "field": field, |
| 654 | - "size": size, | |
| 655 | - "order": {"_count": "desc"} | |
| 646 | + "ranges": config.ranges | |
| 656 | 647 | } |
| 657 | 648 | } |
| 658 | - | |
| 659 | - elif facet_type == 'range': | |
| 660 | - if config.ranges: | |
| 661 | - aggs[agg_name] = { | |
| 662 | - "range": { | |
| 663 | - "field": field, | |
| 664 | - "ranges": config.ranges | |
| 665 | - } | |
| 666 | - } | |
| 667 | 649 | |
| 668 | 650 | return aggs | ... | ... |
search/searcher.py
| ... | ... | @@ -17,7 +17,7 @@ from .rerank_engine import RerankEngine |
| 17 | 17 | from config import SearchConfig |
| 18 | 18 | from config.utils import get_match_fields_for_index |
| 19 | 19 | from context.request_context import RequestContext, RequestContextStage, create_request_context |
| 20 | -from api.models import FacetResult, FacetValue | |
| 20 | +from api.models import FacetResult, FacetValue, FacetConfig | |
| 21 | 21 | from api.result_formatter import ResultFormatter |
| 22 | 22 | |
| 23 | 23 | logger = logging.getLogger(__name__) |
| ... | ... | @@ -123,7 +123,7 @@ class Searcher: |
| 123 | 123 | from_: int = 0, |
| 124 | 124 | filters: Optional[Dict[str, Any]] = None, |
| 125 | 125 | range_filters: Optional[Dict[str, Any]] = None, |
| 126 | - facets: Optional[List[Any]] = None, | |
| 126 | + facets: Optional[List[FacetConfig]] = None, | |
| 127 | 127 | min_score: Optional[float] = None, |
| 128 | 128 | context: Optional[RequestContext] = None, |
| 129 | 129 | sort_by: Optional[str] = None, | ... | ... |