Commit 13320ac6b0d8b3ca7a554e3d9923b019faaf7fb9

Authored by tangwang
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 的组装格式 完全一样。前端不需要感知 属性分面 和 类别等其他字段分面的差异。
activate.sh 0 → 100644
... ... @@ -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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
650 709  
651 710 ### 场景4:带分面的商品搜索
652 711  
653   -**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器
  712 +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器
654 713  
655 714 ```json
656 715 {
... ... @@ -658,9 +717,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://120.76.41.98:6002/search/&quot; \
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 &quot;http://localhost:6002/search/12345&quot;
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 &quot;http://localhost:6002/search/12345&quot;
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,
... ...