Compare View

switch
from
...
to
 
Commits (3)
  • 2. mysql->ES数据灌入脚本优化。修改了多个字段的处理方式,完善日志,为以后抽出来服务供java全量增量调用做准备
    tangwang
     
  • tangwang
     
  • {
      "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 的组装格式 完全一样。前端不需要感知 属性分面 和 类别等其他字段分面的差异。
    tangwang
     
.cursor/plans/api-interface-analysis-42918612.plan.0.md
@@ -632,7 +632,7 @@ def _standardize_facets( @@ -632,7 +632,7 @@ def _standardize_facets(
632 # 构建标准化分面结果 632 # 构建标准化分面结果
633 facet = { 633 facet = {
634 "field": field, 634 "field": field,
635 - "label": self._get_field_label(field), # 从配置获取 635 + "label": field, # 从配置获取
636 "type": facet_type, 636 "type": facet_type,
637 "values": [] 637 "values": []
638 } 638 }
.cursor/plans/api-interface-analysis-42918612.plan.1.md
@@ -632,7 +632,7 @@ def _standardize_facets( @@ -632,7 +632,7 @@ def _standardize_facets(
632 # 构建标准化分面结果 632 # 构建标准化分面结果
633 facet = { 633 facet = {
634 "field": field, 634 "field": field,
635 - "label": self._get_field_label(field), # 从配置获取 635 + "label": field, # 从配置获取
636 "type": facet_type, 636 "type": facet_type,
637 "values": [] 637 "values": []
638 } 638 }
.cursor/plans/api-interface-analysis-42918612.plan.2.md
@@ -632,7 +632,7 @@ def _standardize_facets( @@ -632,7 +632,7 @@ def _standardize_facets(
632 # 构建标准化分面结果 632 # 构建标准化分面结果
633 facet = { 633 facet = {
634 "field": field, 634 "field": field,
635 - "label": self._get_field_label(field), # 从配置获取 635 + "label": field, # 从配置获取
636 "type": facet_type, 636 "type": facet_type,
637 "values": [] 637 "values": []
638 } 638 }
.cursor/plans/api-interface-analysis-42918612.plan.3.最终执行.md
@@ -632,7 +632,7 @@ def _standardize_facets( @@ -632,7 +632,7 @@ def _standardize_facets(
632 # 构建标准化分面结果 632 # 构建标准化分面结果
633 facet = { 633 facet = {
634 "field": field, 634 "field": field,
635 - "label": self._get_field_label(field), 635 + "label": field,
636 "type": facet_type, 636 "type": facet_type,
637 "values": [] 637 "values": []
638 } 638 }
activate.sh 0 → 100644
@@ -0,0 +1,12 @@ @@ -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"
@@ -67,7 +67,7 @@ class SearchRequest(BaseModel): @@ -67,7 +67,7 @@ class SearchRequest(BaseModel):
67 67
68 # 基础搜索参数 68 # 基础搜索参数
69 query: str = Field(..., description="搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT)") 69 query: str = Field(..., description="搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT)")
70 - size: int = Field(10, ge=1, le=100, description="返回结果数量") 70 + size: int = Field(10, ge=1, le=1000, description="返回结果数量")
71 from_: int = Field(0, ge=0, alias="from", description="分页偏移量") 71 from_: int = Field(0, ge=0, alias="from", description="分页偏移量")
72 language: Literal["zh", "en"] = Field( 72 language: Literal["zh", "en"] = Field(
73 "zh", 73 "zh",
@@ -110,31 +110,34 @@ class SearchRequest(BaseModel): @@ -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 None, 118 None,
119 - description="分面配置。可以是字段名列表(使用默认配置)或详细的分面配置对象。支持 specifications 分面:\"specifications\"(所有name)或 \"specifications.color\"(指定name)", 119 + description="分面配置对象列表。支持 specifications 分面:field=\"specifications\"(所有规格名称)或 field=\"specifications.color\"(指定规格名称)",
120 json_schema_extra={ 120 json_schema_extra={
121 "examples": [ 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 "field": "min_price", 131 "field": "min_price",
131 "type": "range", 132 "type": "range",
132 "ranges": [ 133 "ranges": [
133 {"key": "0-50", "to": 50}, 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/常用查询 - sql.sql
@@ -361,4 +361,191 @@ SELECT @@ -361,4 +361,191 @@ SELECT
361 FROM shoplazza_product_option 361 FROM shoplazza_product_option
362 WHERE tenant_id = 162 AND deleted = 0 362 WHERE tenant_id = 162 AND deleted = 0
363 GROUP BY position, name 363 GROUP BY position, name
364 -ORDER BY position, spu_count DESC;  
365 \ No newline at end of file 364 \ No newline at end of file
  365 +ORDER BY position, spu_count DESC;
  366 +
  367 +
  368 +
  369 +-- ======================================
  370 +-- 10. SPU原始数据完整查询
  371 +-- ======================================
  372 +
  373 +-- 10.1 查询SPU表所有字段的原始数据
  374 +-- 用于全面检查数据质量
  375 +SELECT
  376 + id,
  377 + shop_id,
  378 + shoplazza_id,
  379 + handle,
  380 + title,
  381 + brief,
  382 + description,
  383 + spu,
  384 + vendor,
  385 + vendor_url,
  386 + seo_title,
  387 + seo_description,
  388 + seo_keywords,
  389 + image_src,
  390 + image_width,
  391 + image_height,
  392 + image_path,
  393 + image_alt,
  394 + inventory_policy,
  395 + inventory_quantity,
  396 + inventory_tracking,
  397 + published,
  398 + published_at,
  399 + requires_shipping,
  400 + taxable,
  401 + fake_sales,
  402 + display_fake_sales,
  403 + mixed_wholesale,
  404 + need_variant_image,
  405 + has_only_default_variant,
  406 + tags,
  407 + note,
  408 + category,
  409 + category_id,
  410 + category_google_id,
  411 + category_level,
  412 + category_path,
  413 + shoplazza_created_at,
  414 + shoplazza_updated_at,
  415 + tenant_id,
  416 + creator,
  417 + create_time,
  418 + updater,
  419 + update_time,
  420 + deleted
  421 +FROM shoplazza_product_spu
  422 +WHERE tenant_id = 162
  423 + AND deleted = 0
  424 +ORDER BY id DESC
  425 +LIMIT 10;
  426 +
  427 +-- 10.2 查询指定SPU的完整关联数据
  428 +-- 包含SPU、SKU、Option的完整信息
  429 +SELECT
  430 + '=== SPU基本信息 ===' AS section,
  431 + spu.id AS spu_id,
  432 + spu.title,
  433 + spu.category,
  434 + spu.category_path,
  435 + spu.category_id,
  436 + spu.category_level,
  437 + spu.vendor,
  438 + spu.tags,
  439 + NULL AS sku_id,
  440 + NULL AS option1,
  441 + NULL AS option2,
  442 + NULL AS option3,
  443 + NULL AS opt_position,
  444 + NULL AS opt_name
  445 +FROM shoplazza_product_spu spu
  446 +WHERE spu.id = 64001 -- 替换为实际的spu_id
  447 + AND spu.deleted = 0
  448 +
  449 +UNION ALL
  450 +
  451 +SELECT
  452 + '=== SKU信息 ===' AS section,
  453 + sku.spu_id,
  454 + NULL AS title,
  455 + NULL AS category,
  456 + NULL AS category_path,
  457 + NULL AS category_id,
  458 + NULL AS category_level,
  459 + NULL AS vendor,
  460 + NULL AS tags,
  461 + sku.id AS sku_id,
  462 + sku.option1,
  463 + sku.option2,
  464 + sku.option3,
  465 + NULL AS opt_position,
  466 + NULL AS opt_name
  467 +FROM shoplazza_product_sku sku
  468 +WHERE sku.spu_id = 64001 -- 替换为实际的spu_id
  469 + AND sku.deleted = 0
  470 +
  471 +UNION ALL
  472 +
  473 +SELECT
  474 + '=== Option定义 ===' AS section,
  475 + opt.spu_id,
  476 + NULL AS title,
  477 + NULL AS category,
  478 + NULL AS category_path,
  479 + NULL AS category_id,
  480 + NULL AS category_level,
  481 + NULL AS vendor,
  482 + NULL AS tags,
  483 + NULL AS sku_id,
  484 + NULL AS option1,
  485 + NULL AS option2,
  486 + NULL AS option3,
  487 + opt.position AS opt_position,
  488 + opt.name AS opt_name
  489 +FROM shoplazza_product_option opt
  490 +WHERE opt.spu_id = 64001 -- 替换为实际的spu_id
  491 + AND opt.deleted = 0;
  492 +
  493 +-- 10.3 导出SPU数据用于分析(简化版)
  494 +-- 适合导出到Excel进行分析
  495 +SELECT
  496 + id AS 'SPU_ID',
  497 + tenant_id AS '租户ID',
  498 + title AS '商品标题',
  499 + category AS '类目字段',
  500 + category_path AS '类目路径',
  501 + category_id AS '类目ID',
  502 + category_level AS '类目层级',
  503 + vendor AS '品牌',
  504 + tags AS '标签',
  505 + published AS '是否发布',
  506 + published_at AS '发布时间',
  507 + create_time AS '创建时间',
  508 + update_time AS '更新时间',
  509 + CASE
  510 + WHEN category REGEXP '^[0-9]+$' THEN 'YES-需要修复'
  511 + ELSE 'NO-正常'
  512 + END AS 'Category是数字ID'
  513 +FROM shoplazza_product_spu
  514 +WHERE tenant_id = 162
  515 + AND deleted = 0
  516 +ORDER BY id DESC
  517 +LIMIT 100;
  518 +
  519 +-- ======================================
  520 +-- 11. 租户数据概览
  521 +-- ======================================
  522 +
  523 +-- 11.1 所有租户的数据统计
  524 +SELECT
  525 + tenant_id,
  526 + COUNT(*) AS total_spu,
  527 + SUM(CASE WHEN category IS NOT NULL THEN 1 ELSE 0 END) AS has_category,
  528 + SUM(CASE WHEN category_path IS NOT NULL THEN 1 ELSE 0 END) AS has_category_path,
  529 + SUM(CASE WHEN category REGEXP '^[0-9]+$' THEN 1 ELSE 0 END) AS category_is_numeric,
  530 + MIN(create_time) AS earliest_data,
  531 + MAX(create_time) AS latest_data
  532 +FROM shoplazza_product_spu
  533 +WHERE deleted = 0
  534 +GROUP BY tenant_id
  535 +ORDER BY tenant_id;
  536 +
  537 +-- 11.2 查询某个租户的所有SPU(分页查看)
  538 +-- 用于逐条检查数据
  539 +SELECT
  540 + id,
  541 + title,
  542 + category,
  543 + category_path,
  544 + vendor,
  545 + tags,
  546 + create_time
  547 +FROM shoplazza_product_spu
  548 +WHERE tenant_id = 162
  549 + AND deleted = 0
  550 +ORDER BY id
  551 +LIMIT 50 OFFSET 0; -- 修改OFFSET查看不同页
  552 +
docs/店匠相关资料/SHOPLAZZA_INTEGRATION_GUIDE.md
@@ -3111,18 +3111,6 @@ search: @@ -3111,18 +3111,6 @@ search:
3111 url: http://localhost:6002 3111 url: http://localhost:6002
3112 timeout: 30000 3112 timeout: 30000
3113 3113
3114 -# 向量服务配置  
3115 -embedding:  
3116 - service:  
3117 - url: http://localhost:6003  
3118 - timeout: 60000  
3119 -  
3120 -# Elasticsearch 配置  
3121 -elasticsearch:  
3122 - hosts: localhost:9200  
3123 - username: elastic  
3124 - password: changeme  
3125 -  
3126 # 数据同步配置 3114 # 数据同步配置
3127 sync: 3115 sync:
3128 enabled: true 3116 enabled: true
@@ -3133,67 +3121,6 @@ sync: @@ -3133,67 +3121,6 @@ sync:
3133 tenants: "0 0 4 * * ?" # 每天凌晨4点 3121 tenants: "0 0 4 * * ?" # 每天凌晨4点
3134 ``` 3122 ```
3135 3123
3136 -### 12.4 故障排查  
3137 -  
3138 -#### 12.4.1 OAuth 认证失败  
3139 -  
3140 -**问题:** 授权回调时报错 "Invalid redirect_uri"  
3141 -  
3142 -**解决:**  
3143 -1. 检查 Partner 后台配置的 Redirect URI 是否与代码中一致  
3144 -2. 确保 Redirect URI 使用 HTTPS 协议  
3145 -3. 确保 Redirect URI 可公网访问  
3146 -  
3147 -#### 12.4.2 Token 过期  
3148 -  
3149 -**问题:** API 调用返回 401 Unauthorized  
3150 -  
3151 -**解决:**  
3152 -1. 检查数据库中的 `token_expires_at` 字段  
3153 -2. 使用 Refresh Token 刷新 Access Token  
3154 -3. 更新数据库中的 Token 信息  
3155 -  
3156 -#### 12.4.3 API 调用速率限制  
3157 -  
3158 -**问题:** API 返回 429 Too Many Requests  
3159 -  
3160 -**解决:**  
3161 -1. 降低请求频率  
3162 -2. 实现指数退避重试  
3163 -3. 解析响应头中的 `X-RateLimit-Reset` 字段,等待到指定时间后再重试  
3164 -  
3165 -#### 12.4.4 Webhook 接收失败  
3166 -  
3167 -**问题:** Webhook 事件未收到或签名验证失败  
3168 -  
3169 -**解决:**  
3170 -1. 检查 Webhook 地址是否可公网访问  
3171 -2. 检查签名验证逻辑是否正确使用 Client Secret  
3172 -3. 查看店匠后台的 Webhook 日志,确认发送状态  
3173 -4. 确保 Webhook 处理在 3 秒内返回 200 响应  
3174 -  
3175 -#### 12.4.5 商品搜索无结果  
3176 -  
3177 -**问题:** 搜索返回空结果  
3178 -  
3179 -**解决:**  
3180 -1. 检查 ES 索引是否存在:`GET /shoplazza_products_1/_count`  
3181 -2. 检查商品是否已索引:`GET /shoplazza_products_1/_search`  
3182 -3. 检查租户隔离参数是否正确  
3183 -4. 查看搜索服务日志,确认查询语句  
3184 -  
3185 -#### 12.4.6 向量生成失败  
3186 -  
3187 -**问题:** 图片或文本向量生成失败  
3188 -  
3189 -**解决:**  
3190 -1. 检查向量服务是否正常运行  
3191 -2. 检查向量服务的 GPU/CPU 资源是否充足  
3192 -3. 检查图片 URL 是否可访问  
3193 -4. 查看向量服务日志  
3194 -  
3195 ----  
3196 -  
3197 ## 13. 参考资料 3124 ## 13. 参考资料
3198 3125
3199 ### 13.1 官方文档 3126 ### 13.1 官方文档
docs/店匠相关资料/店匠官方参考文档.md
@@ -7,6 +7,19 @@ @@ -7,6 +7,19 @@
7 7
8 店匠API文档: https://www.shoplazza.dev/reference/overview-23; 8 店匠API文档: https://www.shoplazza.dev/reference/overview-23;
9 店匠应用市场: https://appstore.shoplazza.com/ 9 店匠应用市场: https://appstore.shoplazza.com/
  10 +通过主题装修集成的话,我们有相关的cli 和 主题扩展
  11 +https://www.shoplazza.dev/v2025.06/docs/app-command-usage-guide
  12 +主题扩展的相关文档
  13 +https://www.shoplazza.dev/v2025.06/docs/theme-extension-2
  14 +
  15 +[Quick Construction of Public Apps](https://www.shoplazza.dev/reference/quick-construction-of-public-apps)
  16 +[1.Create a developer platform login account and register as a partner](https://www.shoplazza.dev/reference/1create-a-developer-platform-login-account-and-register-as-a-partner)
  17 +[2.Create A Public APP](https://www.shoplazza.dev/reference/2create-a-public-app)
  18 +[3.Develop your application service](https://www.shoplazza.dev/reference/3develop-your-application-service)
  19 +[4.Test your app](https://www.shoplazza.dev/reference/4test-your-app)
  20 +[5.Submit App for review](https://www.shoplazza.dev/reference/5submit-app-for-review)
  21 +[Quick Construction of Private Apps](https://www.shoplazza.dev/reference/quick-construction-of-private-apps)
  22 +
10 23
11 ### 13.2 技术栈文档 24 ### 13.2 技术栈文档
12 25
docs/搜索API对接指南.md
@@ -50,7 +50,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -50,7 +50,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
50 "gte": "2020-01-01T00:00:00Z" 50 "gte": "2020-01-01T00:00:00Z"
51 } 51 }
52 }, 52 },
53 - "sort_by": "min_price", 53 + "sort_by": "price",
54 "sort_order": "asc" 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,7 +63,11 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
63 -H "X-Tenant-ID: 162" \ 63 -H "X-Tenant-ID: 162" \
64 -d '{ 64 -d '{
65 "query": "芭比娃娃", 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 "min_score": 0.2 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,8 +128,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
124 | `filters` | object | N | null | 精确匹配过滤器(见下文) | 128 | `filters` | object | N | null | 精确匹配过滤器(见下文) |
125 | `range_filters` | object | N | null | 数值范围过滤器(见下文) | 129 | `range_filters` | object | N | null | 数值范围过滤器(见下文) |
126 | `facets` | array | N | null | 分面配置(见下文) | 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 | `min_score` | float | N | null | 最小相关性分数阈值 | 133 | `min_score` | float | N | null | 最小相关性分数阈值 |
130 | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(店铺配置)。指定后,每个SPU下的SKU将按这些维度的组合进行分组,每个组合只返回第一个SKU。支持的值:`option1`、`option2`、`option3` 或选项名称(如 `color`、`size`)。详见下文说明 | 134 | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(店铺配置)。指定后,每个SPU下的SKU将按这些维度的组合进行分组,每个组合只返回第一个SKU。支持的值:`option1`、`option2`、`option3` 或选项名称(如 `color`、`size`)。详见下文说明 |
131 | `debug` | boolean | N | false | 是否返回调试信息 | 135 | `debug` | boolean | N | false | 是否返回调试信息 |
@@ -263,14 +267,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -263,14 +267,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
263 267
264 用于生成分面统计(分组聚合),常用于构建筛选器UI。 268 用于生成分面统计(分组聚合),常用于构建筛选器UI。
265 269
266 -**简单模式**(字符串数组):  
267 -```json  
268 -{  
269 - "facets": ["category1_name", "category2_name", "category3_name", "specifications"]  
270 -}  
271 -``` 270 +**配置格式**(对象数组):
272 271
273 -**高级模式**(配置对象数组):  
274 ```json 272 ```json
275 { 273 {
276 "facets": [ 274 "facets": [
@@ -280,6 +278,16 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -280,6 +278,16 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
280 "type": "terms" 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 "field": "min_price", 291 "field": "min_price",
284 "type": "range", 292 "type": "range",
285 "ranges": [ 293 "ranges": [
@@ -288,8 +296,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -288,8 +296,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
288 {"key": "100-200", "from": 100, "to": 200}, 296 {"key": "100-200", "from": 100, "to": 200},
289 {"key": "200+", "from": 200} 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,18 +305,35 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
298 305
299 `specifications` 是嵌套字段,支持两种分面模式: 306 `specifications` 是嵌套字段,支持两种分面模式:
300 307
301 -**模式1:所有规格名称的分面** (`"specifications"`): 308 +**模式1:所有规格名称的分面**:
302 ```json 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 返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 320 返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。
308 321
309 -**模式2:指定规格名称的分面** (`"specifications.color"`): 322 +**模式2:指定规格名称的分面**:
310 ```json 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 只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。 339 只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。
@@ -564,6 +588,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -564,6 +588,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
564 | `sku_weights` | array[integer] | 所有SKU重量列表 | 588 | `sku_weights` | array[integer] | 所有SKU重量列表 |
565 | `sku_weight_units` | array[string] | 所有SKU重量单位列表 | 589 | `sku_weight_units` | array[string] | 所有SKU重量单位列表 |
566 | `total_inventory` | integer | 总库存 | 590 | `total_inventory` | integer | 总库存 |
  591 +| `sales` | integer | 销量(展示销量) |
567 | `option1_name` | string | 选项1名称(如"color") | 592 | `option1_name` | string | 选项1名称(如"color") |
568 | `option2_name` | string | 选项2名称(如"size") | 593 | `option2_name` | string | 选项2名称(如"size") |
569 | `option3_name` | string | 选项3名称 | 594 | `option3_name` | string | 选项3名称 |
@@ -605,11 +630,45 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -605,11 +630,45 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
605 "query": "玩具", 630 "query": "玩具",
606 "size": 20, 631 "size": 20,
607 "from": 0, 632 "from": 0,
608 - "sort_by": "min_price", 633 + "sort_by": "price",
609 "sort_order": "asc" 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 ### 场景2:SKU筛选(按维度过滤) 672 ### 场景2:SKU筛选(按维度过滤)
614 673
615 **需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU 674 **需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU
@@ -650,7 +709,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -650,7 +709,7 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
650 709
651 ### 场景4:带分面的商品搜索 710 ### 场景4:带分面的商品搜索
652 711
653 -**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 712 +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器
654 713
655 ```json 714 ```json
656 { 715 {
@@ -658,9 +717,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -658,9 +717,9 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
658 "size": 20, 717 "size": 20,
659 "language": "zh", 718 "language": "zh",
660 "facets": [ 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,7 +745,8 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
686 "facets": [ 745 "facets": [
687 { 746 {
688 "field": "category1_name", 747 "field": "category1_name",
689 - "size": 15 748 + "size": 15,
  749 + "type": "terms"
690 }, 750 },
691 { 751 {
692 "field": "min_price", 752 "field": "min_price",
@@ -698,7 +758,11 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -698,7 +758,11 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
698 {"key": "200+", "from": 200} 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 "sort_by": "min_price", 767 "sort_by": "min_price",
704 "sort_order": "asc" 768 "sort_order": "asc"
@@ -768,18 +832,23 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \ @@ -768,18 +832,23 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
768 "query": "手机", 832 "query": "手机",
769 "size": 20, 833 "size": 20,
770 "language": "zh", 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 ```json 843 ```json
778 { 844 {
779 "query": "手机", 845 "query": "手机",
780 "size": 20, 846 "size": 20,
781 "language": "zh", 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,10 +869,10 @@ curl -X POST &quot;http://120.76.41.98:6002/search/&quot; \
800 } 869 }
801 }, 870 },
802 "facets": [ 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,6 +1075,7 @@ curl &quot;http://localhost:6002/search/12345&quot;
1006 | `sku_weights` | long | SKU重量列表(数组) | 1075 | `sku_weights` | long | SKU重量列表(数组) |
1007 | `sku_weight_units` | keyword | SKU重量单位列表(数组) | 1076 | `sku_weight_units` | keyword | SKU重量单位列表(数组) |
1008 | `total_inventory` | long | 总库存 | 1077 | `total_inventory` | long | 总库存 |
  1078 +| `sales` | long | 销量(展示销量) |
1009 | `skus` | nested | SKU详细信息(嵌套对象数组) | 1079 | `skus` | nested | SKU详细信息(嵌套对象数组) |
1010 | `create_time`, `update_time` | date | 创建/更新时间 | 1080 | `create_time`, `update_time` | date | 创建/更新时间 |
1011 | `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | 1081 | `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) |
@@ -1038,11 +1108,15 @@ curl &quot;http://localhost:6002/search/12345&quot; @@ -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 - `create_time`: 创建时间 1113 - `create_time`: 创建时间
1044 - `update_time`: 更新时间 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/竞品参考文档/prefixbox_api.md 0 → 100644
@@ -0,0 +1,700 @@ @@ -0,0 +1,700 @@
  1 +# Prefixbox Search API 完整文档
  2 +
  3 +*基于公开信息整理的 Prefixbox Search API 技术文档*
  4 +
  5 +*官方接口文档*
  6 +https://api-docs.prefixbox.com/#search-api
  7 +
  8 +
  9 +---
  10 +
  11 +## 概述
  12 +
  13 +Prefixbox Search API 是一个基于 AI 的电商搜索解决方案,提供全文搜索、向量搜索和混合搜索功能。该 API 支持自动完成、搜索建议、商品过滤、排序等核心电商搜索功能。
  14 +
  15 +**核心特性:**
  16 +- 混合搜索(文本 + 向量 + GPT 技术)
  17 +- 自然语言查询理解
  18 +- 动态重排序
  19 +- 同义词管理
  20 +- 多语言支持
  21 +
  22 +---
  23 +
  24 +## 1. 基础信息
  25 +
  26 +### 1.1 API 端点
  27 +
  28 +```
  29 +基础 URL: https://api.prefixbox.com
  30 +版本: v1/v2 (根据集成方式不同)
  31 +```
  32 +
  33 +### 1.2 认证方式
  34 +
  35 +**API Key 认证**
  36 +- 在 Prefixbox API Portal 的 Profile 页面获取 Search API Key
  37 +- 所有请求必须在 Header 中包含:`Authorization: Bearer YOUR_SEARCH_API_KEY`
  38 +- 同时需要提供 Search Engine Identifier
  39 +
  40 +**Header 示例:**
  41 +```http
  42 +Authorization: Bearer pb_live_abc123xyz...
  43 +X-Search-Engine-Id: your-engine-identifier
  44 +```
  45 +
  46 +---
  47 +
  48 +## 2. 核心 API 端点
  49 +
  50 +### 2.1 自动完成 API (Autocomplete API)
  51 +
  52 +**端点:** `GET /autocomplete`
  53 +
  54 +**功能:** 当用户在搜索框输入时提供实时查询建议、分类建议和商品建议。
  55 +
  56 +**请求参数:**
  57 +
  58 +| 参数 | 类型 | 必填 | 说明 |
  59 +|------|------|------|------|
  60 +| `q` | string | 是 | 用户输入的查询字符串 |
  61 +| `limit` | integer | 否 | 返回建议的最大数量 (默认: 10) |
  62 +| `type` | string | 否 | 建议类型: `all`, `queries`, `categories`, `products` (默认: all) |
  63 +| `lang` | string | 否 | ISO 639-1 语言代码 (如: en, de, fr) |
  64 +
  65 +**请求示例:**
  66 +```http
  67 +GET https://api.prefixbox.com/autocomplete?q=apple&limit=8&lang=en
  68 +Authorization: Bearer pb_live_abc123xyz...
  69 +X-Search-Engine-Id: your-engine-identifier
  70 +```
  71 +
  72 +**响应格式:**
  73 +```json
  74 +{
  75 + "query": "apple",
  76 + "suggestions": {
  77 + "queries": [
  78 + {"text": "apple iphone", "count": 1250},
  79 + {"text": "apple watch", "count": 890},
  80 + {"text": "apple airpods", "count": 650}
  81 + ],
  82 + "categories": [
  83 + {"id": "cat_001", "name": "Smartphones", "path": "Electronics/Mobile"},
  84 + {"id": "cat_002", "name": "Accessories", "path": "Electronics/Accessories"}
  85 + ],
  86 + "products": [
  87 + {
  88 + "id": "prod_12345",
  89 + "name": "Apple iPhone 15 Pro",
  90 + "image": "https://cdn.example.com/iphone15.jpg",
  91 + "price": 999.99,
  92 + "currency": "USD",
  93 + "url": "https://store.example.com/products/iphone-15-pro"
  94 + }
  95 + ]
  96 + },
  97 + "latency_ms": 45
  98 +}
  99 +```
  100 +
  101 +---
  102 +
  103 +### 2.2 搜索 API (Search API)
  104 +
  105 +**端点:** `GET /search`
  106 +
  107 +**功能:** 执行完整搜索并返回商品结果、过滤器、排序选项等。
  108 +
  109 +**请求参数:**
  110 +
  111 +| 参数 | 类型 | 必填 | 说明 |
  112 +|------|------|------|------|
  113 +| `q` | string | 是 | 搜索查询字符串 |
  114 +| `page` | integer | 否 | 页码 (默认: 1) |
  115 +| `page_size` | integer | 否 | 每页商品数量 (默认: 24, 最大: 100) |
  116 +| `sort` | string | 否 | 排序方式: `relevance`, `price_asc`, `price_desc`, `newest`, `popularity` |
  117 +| `filters` | object | 否 | 过滤器对象 `{"brand": ["Apple", "Samsung"], "price_range": {"min": 500, "max": 1000}}` |
  118 +| `lang` | string | 否 | 语言代码 |
  119 +| `user_id` | string | 否 | 用户ID(用于个性化)|
  120 +| `cdp_data` | object | 否 | 客户数据平台偏好数据 |
  121 +
  122 +**请求示例:**
  123 +```http
  124 +GET https://api.prefixbox.com/search?q=running+shoes&page=1&page_size=24&sort=relevance
  125 +Authorization: Bearer pb_live_abc123xyz...
  126 +X-Search-Engine-Id: your-engine-identifier
  127 +```
  128 +
  129 +**完整带过滤器的请求:**
  130 +```http
  131 +GET https://api.prefixbox.com/search?q=running+shoes&filters={"brand":["Nike","Adidas"],"price_range":{"min":50,"max":200},"size":["8","9","10"]}
  132 +Authorization: Bearer pb_live_abc123xyz...
  133 +X-Search-Engine-Id: your-engine-identifier
  134 +```
  135 +
  136 +**响应格式:**
  137 +```json
  138 +{
  139 + "search_id": "search_abc123xyz789",
  140 + "query": "running shoes",
  141 + "total_results": 1456,
  142 + "page": 1,
  143 + "page_size": 24,
  144 + "total_pages": 61,
  145 + "latency_ms": 120,
  146 + "products": [
  147 + {
  148 + "id": "prod_67890",
  149 + "name": "Nike Air Zoom Pegasus",
  150 + "description": "Responsive running shoes with Zoom Air cushioning",
  151 + "image": "https://cdn.example.com/nike-pegasus.jpg",
  152 + "price": 129.99,
  153 + "original_price": 159.99,
  154 + "currency": "USD",
  155 + "url": "https://store.example.com/products/nike-air-zoom-pegasus",
  156 + "availability": "in_stock",
  157 + "brand": "Nike",
  158 + "category": "Running Shoes",
  159 + "score": 0.987
  160 + }
  161 + ],
  162 + "filters": [
  163 + {
  164 + "name": "brand",
  165 + "type": "multiselect",
  166 + "values": [
  167 + {"value": "Nike", "count": 456},
  168 + {"value": "Adidas", "count": 389},
  169 + {"value": "Asics", "count": 234}
  170 + ]
  171 + },
  172 + {
  173 + "name": "price",
  174 + "type": "range",
  175 + "min": 29.99,
  176 + "max": 299.99
  177 + },
  178 + {
  179 + "name": "size",
  180 + "type": "multiselect",
  181 + "values": [
  182 + {"value": "7", "count": 156},
  183 + {"value": "8", "count": 234},
  184 + {"value": "9", "count": 289}
  185 + ]
  186 + }
  187 + ],
  188 + "sort_options": [
  189 + {"value": "relevance", "label": "Relevance"},
  190 + {"value": "price_asc", "label": "Price: Low to High"},
  191 + {"value": "price_desc", "label": "Price: High to Low"},
  192 + {"value": "newest", "label": "Newest First"},
  193 + {"value": "popularity", "label": "Most Popular"}
  194 + ],
  195 + "related_keywords": ["trail running", "marathon shoes", "lightweight trainers"],
  196 + "related_categories": [
  197 + {"id": "cat_sports", "name": "Sports & Outdoors"},
  198 + {"id": "cat_footwear", "name": "Footwear"}
  199 + ],
  200 + "personalized": false,
  201 + "ai_reranking": true
  202 +}
  203 +```
  204 +
  205 +---
  206 +
  207 +### 2.3 商品详情 API (Product Details API)
  208 +
  209 +**端点:** `GET /products/{product_id}`
  210 +
  211 +**功能:** 获取单个商品的详细信息。
  212 +
  213 +**请求示例:**
  214 +```http
  215 +GET https://api.prefixbox.com/products/prod_67890
  216 +Authorization: Bearer pb_live_abc123xyz...
  217 +X-Search-Engine-Id: your-engine-identifier
  218 +```
  219 +
  220 +**响应格式:**
  221 +```json
  222 +{
  223 + "id": "prod_67890",
  224 + "name": "Nike Air Zoom Pegasus",
  225 + "description": "Responsive running shoes with Zoom Air cushioning",
  226 + "long_description": "Detailed product description...",
  227 + "images": [
  228 + "https://cdn.example.com/nike-pegasus-1.jpg",
  229 + "https://cdn.example.com/nike-pegasus-2.jpg"
  230 + ],
  231 + "price": 129.99,
  232 + "original_price": 159.99,
  233 + "currency": "USD",
  234 + "url": "https://store.example.com/products/nike-air-zoom-pegasus",
  235 + "availability": "in_stock",
  236 + "stock_quantity": 45,
  237 + "brand": "Nike",
  238 + "category": "Running Shoes",
  239 + "attributes": {
  240 + "color": ["Black", "White", "Blue"],
  241 + "size": ["7", "8", "9", "10", "11", "12"],
  242 + "material": "Mesh and Rubber",
  243 + "weight": "280g"
  244 + },
  245 + "reviews": {
  246 + "average_rating": 4.5,
  247 + "count": 234
  248 + },
  249 + "tags": ["marathon", "neutral", "cushioned"]
  250 +}
  251 +```
  252 +
  253 +---
  254 +
  255 +### 2.4 分类页面 API (Collection API)
  256 +
  257 +**端点:** `GET /collections/{collection_id}`
  258 +
  259 +**功能:** 获取分类页面的商品列表。
  260 +
  261 +**请求参数:**
  262 +
  263 +| 参数 | 类型 | 说明 |
  264 +|------|------|------|
  265 +| `collection_id` | string | 分类ID |
  266 +| `page` | integer | 页码 |
  267 +| `page_size` | integer | 每页数量 |
  268 +| `sort` | string | 排序方式 |
  269 +| `filters` | object | 过滤器 |
  270 +
  271 +**响应格式:** 与 Search API 类似,但返回的是特定分类的商品
  272 +
  273 +---
  274 +
  275 +## 3. 高级功能 API
  276 +
  277 +### 3.1 个性化搜索 API
  278 +
  279 +**端点:** `GET /search/personalized`
  280 +
  281 +**功能:** 基于用户偏好数据返回个性化排序结果。
  282 +
  283 +**请求参数:**
  284 +```json
  285 +{
  286 + "q": "running shoes",
  287 + "user_id": "user_12345",
  288 + "cdp_data": {
  289 + "preferred_brands": ["Nike", "Adidas"],
  290 + "preferred_size": "9",
  291 + "preferred_color": "Black",
  292 + "past_purchases": ["prod_111", "prod_222"]
  293 + }
  294 +}
  295 +```
  296 +
  297 +---
  298 +
  299 +### 3.2 AI 重排序 API
  300 +
  301 +**端点:** `POST /search/rerank`
  302 +
  303 +**功能:** 基于用户行为数据动态重排搜索结果。
  304 +
  305 +**请求参数:**
  306 +```json
  307 +{
  308 + "search_id": "search_abc123xyz789",
  309 + "user_interactions": {
  310 + "clicks": ["prod_67890", "prod_67891"],
  311 + "hover_time_ms": 2500,
  312 + "previous_searches": ["trail running"]
  313 + }
  314 +}
  315 +```
  316 +
  317 +---
  318 +
  319 +## 4. 推荐 API (AI Recommend)
  320 +
  321 +### 4.1 相关产品推荐
  322 +
  323 +**端点:** `GET /recommendations/related`
  324 +
  325 +**请求参数:**
  326 +```http
  327 +GET /recommendations/related?product_id=prod_67890&limit=6
  328 +```
  329 +
  330 +**响应格式:**
  331 +```json
  332 +{
  333 + "product_id": "prod_67890",
  334 + "recommendations": [
  335 + {
  336 + "id": "prod_67891",
  337 + "name": "Nike Dri-FIT Running Socks",
  338 + "reason": "Frequently bought together"
  339 + },
  340 + {
  341 + "id": "prod_67892",
  342 + "name": "Nike Running Cap",
  343 + "reason": "Similar category"
  344 + }
  345 + ]
  346 +}
  347 +```
  348 +
  349 +---
  350 +
  351 +### 4.2 热门商品推荐
  352 +
  353 +**端点:** `GET /recommendations/trending`
  354 +
  355 +```http
  356 +GET /recommendations/trending?category=running&limit=8
  357 +```
  358 +
  359 +---
  360 +
  361 +## 5. 分析追踪 API
  362 +
  363 +### 5.1 搜索事件追踪
  364 +
  365 +**端点:** `POST /analytics/search-event`
  366 +
  367 +**功能:** 追踪用户搜索行为以优化搜索相关性。
  368 +
  369 +**事件类型:**
  370 +- `search_impression` - 搜索结果展示
  371 +- `search_click` - 用户点击结果
  372 +- `search_add_to_cart` - 添加商品到购物车
  373 +- `search_purchase` - 完成购买
  374 +
  375 +**请求示例:**
  376 +```json
  377 +{
  378 + "search_id": "search_abc123xyz789",
  379 + "event_type": "search_click",
  380 + "user_id": "user_12345",
  381 + "product_id": "prod_67890",
  382 + "position": 1,
  383 + "timestamp": "2025-01-15T10:30:00Z"
  384 +}
  385 +```
  386 +
  387 +---
  388 +
  389 +## 6. 实时目录更新 API
  390 +
  391 +**端点:** `POST /catalog/update`
  392 +
  393 +**功能:** 实时更新商品目录数据。
  394 +
  395 +**请求格式:**
  396 +```json
  397 +{
  398 + "updates": [
  399 + {
  400 + "product_id": "prod_67890",
  401 + "field": "stock_quantity",
  402 + "value": 23,
  403 + "action": "update"
  404 + },
  405 + {
  406 + "product_id": "prod_67891",
  407 + "action": "delete"
  408 + }
  409 + ]
  410 +}
  411 +```
  412 +
  413 +---
  414 +
  415 +## 7. 错误处理
  416 +
  417 +### 7.1 HTTP 状态码
  418 +
  419 +| 状态码 | 说明 |
  420 +|--------|------|
  421 +| `200` | 请求成功 |
  422 +| `400` | 请求参数错误 |
  423 +| `401` | 认证失败 - API Key 无效或缺失 |
  424 +| `403` | 权限不足 |
  425 +| `404` | 资源未找到 |
  426 +| `429` | 请求频率限制 |
  427 +| `500` | 服务器内部错误 |
  428 +
  429 +### 7.2 错误响应格式
  430 +
  431 +```json
  432 +{
  433 + "error": {
  434 + "code": "INVALID_API_KEY",
  435 + "message": "The provided API key is invalid or expired.",
  436 + "status": 401,
  437 + "request_id": "req_123456789"
  438 + }
  439 +}
  440 +```
  441 +
  442 +---
  443 +
  444 +## 8. 代码集成示例
  445 +
  446 +### 8.1 JavaScript/Frontend 集成
  447 +
  448 +```javascript
  449 +// 初始化 Prefixbox 客户端
  450 +const prefixbox = new PrefixboxClient({
  451 + apiKey: 'pb_live_abc123xyz...',
  452 + searchEngineId: 'your-engine-identifier',
  453 + language: 'en'
  454 +});
  455 +
  456 +// 自动完成
  457 +const suggestions = await prefixbox.autocomplete({
  458 + query: 'run',
  459 + limit: 8
  460 +});
  461 +
  462 +// 搜索
  463 +const searchResults = await prefixbox.search({
  464 + query: 'running shoes',
  465 + page: 1,
  466 + pageSize: 24,
  467 + filters: {
  468 + brand: ['Nike', 'Adidas'],
  469 + priceRange: { min: 50, max: 200 }
  470 + }
  471 +});
  472 +
  473 +// 追踪事件
  474 +prefixbox.trackEvent('search_click', {
  475 + searchId: searchResults.searchId,
  476 + productId: 'prod_67890',
  477 + position: 1
  478 +});
  479 +```
  480 +
  481 +### 8.2 cURL 示例
  482 +
  483 +```bash
  484 +# 自动完成
  485 +curl -X GET "https://api.prefixbox.com/autocomplete?q=apple&limit=5" \
  486 + -H "Authorization: Bearer pb_live_abc123xyz..." \
  487 + -H "X-Search-Engine-Id: your-engine-identifier"
  488 +
  489 +# 搜索
  490 +curl -X GET "https://api.prefixbox.com/search?q=laptop&page=1&page_size=24" \
  491 + -H "Authorization: Bearer pb_live_abc123xyz..." \
  492 + -H "X-Search-Engine-Id: your-engine-identifier"
  493 +```
  494 +
  495 +### 8.3 Python 集成示例
  496 +
  497 +```python
  498 +import requests
  499 +
  500 +class PrefixboxClient:
  501 + def __init__(self, api_key, search_engine_id):
  502 + self.base_url = "https://api.prefixbox.com"
  503 + self.headers = {
  504 + "Authorization": f"Bearer {api_key}",
  505 + "X-Search-Engine-Id": search_engine_id,
  506 + "Content-Type": "application/json"
  507 + }
  508 +
  509 + def autocomplete(self, query, limit=10):
  510 + endpoint = f"{self.base_url}/autocomplete"
  511 + params = {"q": query, "limit": limit}
  512 + response = requests.get(endpoint, headers=self.headers, params=params)
  513 + return response.json()
  514 +
  515 + def search(self, query, page=1, page_size=24, filters=None):
  516 + endpoint = f"{self.base_url}/search"
  517 + params = {
  518 + "q": query,
  519 + "page": page,
  520 + "page_size": page_size
  521 + }
  522 + if filters:
  523 + params["filters"] = filters
  524 +
  525 + response = requests.get(endpoint, headers=self.headers, params=params)
  526 + return response.json()
  527 +
  528 +# 使用示例
  529 +client = PrefixboxClient(
  530 + api_key="pb_live_abc123xyz...",
  531 + search_engine_id="your-engine-identifier"
  532 +)
  533 +
  534 +# 执行搜索
  535 +results = client.search(
  536 + query="wireless headphones",
  537 + filters={"brand": ["Sony", "Bose"], "price_range": {"min": 100, "max": 300}}
  538 +)
  539 +
  540 +print(f"找到 {results['total_results']} 个商品")
  541 +for product in results['products']:
  542 + print(f"- {product['name']}: ${product['price']}")
  543 +```
  544 +
  545 +---
  546 +
  547 +## 9. SDK 和库
  548 +
  549 +Prefixbox 提供以下官方 SDK:
  550 +
  551 +- **JavaScript SDK**: `npm install @prefixbox/search-js`
  552 +- **React SDK**: `npm install @prefixbox/react-search`
  553 +- **Python SDK**: `pip install prefixbox-search`
  554 +- **PHP SDK**: `composer require prefixbox/search`
  555 +
  556 +---
  557 +
  558 +## 10. 集成方式
  559 +
  560 +### 10.1 API 集成
  561 +
  562 +**适用场景:** 需要完全自定义 UI 和用户体验
  563 +
  564 +**客户职责:**
  565 +- 提供商品 Feed URL
  566 +- 实现与 Prefixbox API 的通信
  567 +- 实现搜索 UX 界面
  568 +- 实现用户行为追踪
  569 +- Bug 修复和发布
  570 +
  571 +**Prefixbox 职责:**
  572 +- 在 Admin Portal 配置搜索模块
  573 +- 定制功能需求
  574 +- 测试和报告 Bug
  575 +- 运行 A/B 测试
  576 +
  577 +### 10.2 前端 JavaScript 集成
  578 +
  579 +**适用场景:** 快速部署,最小化开发工作量
  580 +
  581 +**客户职责:**
  582 +- 提供带 header/footer 的空搜索结果页
  583 +- 提供商品 Feed URL
  584 +- 在网站中插入 Prefixbox JavaScript
  585 +
  586 +**Prefixbox 职责:**
  587 +- 配置搜索模块
  588 +- 定制外观和功能
  589 +- 测试、预览和修复 Bug
  590 +- 发布到生产环境
  591 +
  592 +---
  593 +
  594 +## 11. 商品 Feed 格式
  595 +
  596 +Prefixbox 要求提供兼容的商品 Feed:
  597 +
  598 +**必需字段:**
  599 +- `id` - 商品唯一标识符
  600 +- `name` - 商品名称
  601 +- `url` - 商品页面 URL
  602 +- `price` - 价格
  603 +- `currency` - 货币代码
  604 +- `availability` - 库存状态
  605 +
  606 +**推荐字段:**
  607 +- `description` - 商品描述
  608 +- `image` - 商品图片 URL
  609 +- `brand` - 品牌
  610 +- `category` - 分类
  611 +- `attributes` - 商品属性(颜色、尺寸等)
  612 +- `created_at` - 创建日期
  613 +- `popularity_score` - 热门度分数
  614 +
  615 +**支持格式:** JSON, XML, CSV
  616 +
  617 +---
  618 +
  619 +## 12. 限制和配额
  620 +
  621 +### 12.1 API 请求限制
  622 +
  623 +| 套餐 | 月请求配额 | 每分钟限制 |
  624 +|------|-----------|-----------|
  625 +| Starter | 20,000 | 100 |
  626 +| Growth | 200,000 | 500 |
  627 +| Grow+ | 600,000-900,000 | 1,000 |
  628 +| Enterprise | 自定义 | 自定义 |
  629 +
  630 +### 12.2 什么算作 API 请求?
  631 +
  632 +- **自动完成:** 每个字符输入 = 1 次请求
  633 +- **搜索:** 每次搜索 = 1 次请求
  634 +- **过滤/排序:** 每次操作 = 1 次请求
  635 +- **分页:** 每次翻页 = 1 次请求
  636 +
  637 +**示例:** 用户输入 "apple"(6 次自动完成请求)+ 执行搜索(1 次)+ 过滤(1 次)+ 翻页(1 次)= **共 9 次 API 请求**
  638 +
  639 +---
  640 +
  641 +## 13. 支持和资源
  642 +
  643 +### 13.1 官方资源
  644 +
  645 +- **API 文档门户:** https://api-docs.prefixbox.com
  646 +- **开发者中心:** https://developers.prefixbox.com
  647 +- **技术文档:** https://www.prefixbox.com/en-us/technical
  648 +- **集成指南:** https://www.prefixbox.com/en-us/technical/integration
  649 +
  650 +### 13.2 支持级别
  651 +
  652 +| 支持级别 | 可用套餐 | 响应时间 |
  653 +|---------|---------|---------|
  654 +| 标准支持 | Starter, Growth | 24-48 小时 |
  655 +| 高级支持 | Grow+ | 4 小时 |
  656 +| 专属支持 | Enterprise | 1 小时 |
  657 +
  658 +---
  659 +
  660 +## 14. 最佳实践
  661 +
  662 +### 14.1 性能优化
  663 +
  664 +1. **缓存自动完成结果** - 减少重复请求
  665 +2. **延迟搜索** - 用户停止输入 300ms 后再发送请求
  666 +3. **分页加载** - 使用无限滚动或分页
  667 +4. **图片优化** - 使用 CDN 和适当尺寸的图片
  668 +
  669 +### 14.2 提升搜索相关性
  670 +
  671 +1. **配置同义词** - 连接不同词汇(如 "sneakers" = "trainers")
  672 +2. **设置字段权重** - 提升重要字段的权重(如 name > description)
  673 +3. **使用 A/B 测试** - 测试不同配置对转化率的影响
  674 +4. **分析零结果搜索** - 识别缺失商品或查询问题
  675 +
  676 +### 14.3 用户体验
  677 +
  678 +1. **显示搜索建议** - 帮助用户快速找到相关查询
  679 +2. **保留搜索上下文** - 支持返回搜索结果
  680 +3. **移动端优化** - 确保触摸友好的界面
  681 +4. **加载状态** - 提供清晰的加载指示
  682 +
  683 +---
  684 +
  685 +## 15. 常见问题
  686 +
  687 +### Q: API 返回 429 错误怎么办?
  688 +A: 表示达到速率限制。实现指数退避重试策略,或升级到更高配额的套餐。
  689 +
  690 +### Q: 如何处理多语言搜索?
  691 +A: 在请求中使用 `lang` 参数,并确保商品 Feed 包含翻译内容。
  692 +
  693 +### Q: 可以自定义搜索算法吗?
  694 +A: 可以在 Prefixbox Admin Portal 中调整字段权重和同义词规则。
  695 +
  696 +### Q: 支持语音搜索吗?
  697 +A: 是的,Prefixbox AI Agent 支持自然语言查询和语音搜索集成。
  698 +
  699 +### Q: 如何集成到移动应用?
  700 +A: 通过 API 集成方式,使用 iOS/Android 原生开发或 React Native 调用 REST API。
docs/系统设计文档.md
@@ -699,45 +699,44 @@ Elasticsearch @@ -699,45 +699,44 @@ Elasticsearch
699 699
700 #### 8.3.2 Facets 配置数据流 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 - 检测 `"specifications"` 或 `"specifications.{name}"` 格式 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 #### 8.3.3 Range Filters 数据流 741 #### 8.3.3 Range Filters 数据流
743 742
docs/索引字段说明v2-参考表结构.md
  1 +
  2 +
  3 +类目表 product_category
  4 +id bigint(20) NO PRI auto_increment
  5 +parent_id bigint(20) NO
  6 +name varchar(255) NO
  7 +pic_url varchar(255) NO
  8 +sort int(11) YES 0
  9 +status tinyint(4) NO
  10 +creator varchar(64) YES
  11 +create_time datetime NO CURRENT_TIMESTAMP
  12 +updater varchar(64) YES
  13 +update_time datetime NO CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
  14 +deleted bit(1) NO b'0'
  15 +tenant_id bigint(20) NO 0
  16 +
  17 +
  18 +
1 spu表 shoplazza_product_spu 全部字段 19 spu表 shoplazza_product_spu 全部字段
2 "Field" "Type" "Null" "Key" "Default" "Extra" 20 "Field" "Type" "Null" "Key" "Default" "Extra"
3 "id" "bigint(20)" "NO" "PRI" "auto_increment" 21 "id" "bigint(20)" "NO" "PRI" "auto_increment"
docs/索引字段说明v2.md
@@ -284,11 +284,12 @@ @@ -284,11 +284,12 @@
284 | `sku_weights` | long | 所有 SKU 重量列表(数组) | 从所有 SKU 重量汇总 | 284 | `sku_weights` | long | 所有 SKU 重量列表(数组) | 从所有 SKU 重量汇总 |
285 | `sku_weight_units` | keyword | 所有 SKU 重量单位列表(数组) | 从所有 SKU 重量单位汇总 | 285 | `sku_weight_units` | keyword | 所有 SKU 重量单位列表(数组) | 从所有 SKU 重量单位汇总 |
286 286
287 -### 9. 库存字段 287 +### 9. 库存与销量字段
288 288
289 | 字段名 | ES类型 | 说明 | 数据来源 | 289 | 字段名 | ES类型 | 说明 | 数据来源 |
290 |--------|--------|------|----------| 290 |--------|--------|------|----------|
291 | `total_inventory` | long | 总库存(所有 SKU 库存之和) | 从所有 SKU 库存汇总 | 291 | `total_inventory` | long | 总库存(所有 SKU 库存之和) | 从所有 SKU 库存汇总 |
  292 +| `sales` | long | 销量(展示销量) | MySQL: `shoplazza_product_spu.fake_sales` |
292 293
293 ### 10. SKU 嵌套字段 294 ### 10. SKU 嵌套字段
294 295
@@ -363,6 +364,16 @@ @@ -363,6 +364,16 @@
363 - `vendor_zh.keyword`, `vendor_en.keyword` 364 - `vendor_zh.keyword`, `vendor_en.keyword`
364 - `specifications` (嵌套查询) 365 - `specifications` (嵌套查询)
365 - `min_price`, `max_price` (范围过滤) 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,11 +94,18 @@
94 <span class="arrow-down" data-field="create_time" data-order="asc" onclick="sortByField('create_time', 'asc')">▼</span> 94 <span class="arrow-down" data-field="create_time" data-order="asc" onclick="sortByField('create_time', 'asc')">▼</span>
95 </span> 95 </span>
96 </button> 96 </button>
97 - <button class="sort-btn" data-sort="min_price"> 97 + <button class="sort-btn" data-sort="price">
98 By Price 98 By Price
99 <span class="sort-arrows"> 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 </span> 109 </span>
103 </button> 110 </button>
104 111
@@ -106,8 +113,8 @@ @@ -106,8 +113,8 @@
106 <select id="resultSize" onchange="performSearch()"> 113 <select id="resultSize" onchange="performSearch()">
107 <option value="20">20 per page</option> 114 <option value="20">20 per page</option>
108 <option value="50" selected>50 per page</option> 115 <option value="50" selected>50 per page</option>
109 - <option value="100">50 per page</option>  
110 - <option value="200">50 per page</option> 116 + <option value="100">100 per page</option>
  117 + <option value="200">200 per page</option>
111 </select> 118 </select>
112 </div> 119 </div>
113 </div> 120 </div>
@@ -135,6 +142,6 @@ @@ -135,6 +142,6 @@
135 <p>SearchEngine © 2025 | API: <span id="apiUrl">Loading...</span></p> 142 <p>SearchEngine © 2025 | API: <span id="apiUrl">Loading...</span></p>
136 </footer> 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 </body> 146 </body>
140 </html> 147 </html>
frontend/static/js/app.js
@@ -86,10 +86,10 @@ async function performSearch(page = 1) { @@ -86,10 +86,10 @@ async function performSearch(page = 1) {
86 86
87 // Define facets (一级分类 + 三个属性分面) 87 // Define facets (一级分类 + 三个属性分面)
88 const facets = [ 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 // Show loading 95 // Show loading
indexer/spu_transformer.py
@@ -6,11 +6,15 @@ Transforms SPU and SKU data from MySQL into SPU-level ES documents with nested s @@ -6,11 +6,15 @@ Transforms SPU and SKU data from MySQL into SPU-level ES documents with nested s
6 6
7 import pandas as pd 7 import pandas as pd
8 import numpy as np 8 import numpy as np
  9 +import logging
9 from typing import Dict, Any, List, Optional 10 from typing import Dict, Any, List, Optional
10 from sqlalchemy import create_engine, text 11 from sqlalchemy import create_engine, text
11 from utils.db_connector import create_db_connection 12 from utils.db_connector import create_db_connection
12 from config import ConfigLoader 13 from config import ConfigLoader
13 14
  15 +# Configure logger
  16 +logger = logging.getLogger(__name__)
  17 +
14 18
15 class SPUTransformer: 19 class SPUTransformer:
16 """Transform SPU and SKU data into SPU-level ES documents.""" 20 """Transform SPU and SKU data into SPU-level ES documents."""
@@ -38,7 +42,48 @@ class SPUTransformer: @@ -38,7 +42,48 @@ class SPUTransformer:
38 except Exception as e: 42 except Exception as e:
39 print(f"Warning: Failed to load config, using default searchable_option_dimensions: {e}") 43 print(f"Warning: Failed to load config, using default searchable_option_dimensions: {e}")
40 self.searchable_option_dimensions = ['option1', 'option2', 'option3'] 44 self.searchable_option_dimensions = ['option1', 'option2', 'option3']
  45 +
  46 + # Load category ID to name mapping
  47 + self.category_id_to_name = self._load_category_mapping()
41 48
  49 + def _load_category_mapping(self) -> Dict[str, str]:
  50 + """
  51 + Load category ID to name mapping from database.
  52 +
  53 + Returns:
  54 + Dictionary mapping category_id to category_name
  55 + """
  56 + query = text("""
  57 + SELECT DISTINCT
  58 + category_id,
  59 + category
  60 + FROM shoplazza_product_spu
  61 + WHERE deleted = 0 AND category_id IS NOT NULL
  62 + """)
  63 +
  64 + mapping = {}
  65 + with self.db_engine.connect() as conn:
  66 + result = conn.execute(query)
  67 + for row in result:
  68 + category_id = str(int(row.category_id))
  69 + category_name = row.category
  70 +
  71 + if not category_name or not category_name.strip():
  72 + logger.warning(f"Category ID {category_id} has empty name, skipping")
  73 + continue
  74 +
  75 + mapping[category_id] = category_name
  76 +
  77 + logger.info(f"Loaded {len(mapping)} category ID to name mappings")
  78 +
  79 + # Log all category mappings for debugging
  80 + if mapping:
  81 + logger.debug("Category ID mappings:")
  82 + for cid, name in sorted(mapping.items()):
  83 + logger.debug(f" {cid} -> {name}")
  84 +
  85 + return mapping
  86 +
42 def load_spu_data(self) -> pd.DataFrame: 87 def load_spu_data(self) -> pd.DataFrame:
43 """ 88 """
44 Load SPU data from MySQL. 89 Load SPU data from MySQL.
@@ -53,6 +98,7 @@ class SPUTransformer: @@ -53,6 +98,7 @@ class SPUTransformer:
53 image_src, image_width, image_height, image_path, image_alt, 98 image_src, image_width, image_height, image_path, image_alt,
54 tags, note, category, category_id, category_google_id, 99 tags, note, category, category_id, category_google_id,
55 category_level, category_path, 100 category_level, category_path,
  101 + fake_sales, display_fake_sales,
56 tenant_id, creator, create_time, updater, update_time, deleted 102 tenant_id, creator, create_time, updater, update_time, deleted
57 FROM shoplazza_product_spu 103 FROM shoplazza_product_spu
58 WHERE tenant_id = :tenant_id AND deleted = 0 104 WHERE tenant_id = :tenant_id AND deleted = 0
@@ -61,8 +107,26 @@ class SPUTransformer: @@ -61,8 +107,26 @@ class SPUTransformer:
61 with self.db_engine.connect() as conn: 107 with self.db_engine.connect() as conn:
62 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id}) 108 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id})
63 109
64 - # Debug: Check if there's any data for this tenant_id  
65 - if len(df) == 0: 110 + logger.info(f"Loaded {len(df)} SPU records for tenant_id={self.tenant_id}")
  111 +
  112 + # Statistics
  113 + if len(df) > 0:
  114 + has_category_path = df['category_path'].notna().sum()
  115 + has_category = df['category'].notna().sum()
  116 + has_title = df['title'].notna().sum()
  117 +
  118 + logger.info(f"SPU data statistics:")
  119 + logger.info(f" - Has title: {has_title}/{len(df)} ({100*has_title/len(df):.1f}%)")
  120 + logger.info(f" - Has category_path: {has_category_path}/{len(df)} ({100*has_category_path/len(df):.1f}%)")
  121 + logger.info(f" - Has category: {has_category}/{len(df)} ({100*has_category/len(df):.1f}%)")
  122 +
  123 + # Warn if too many SPUs don't have category_path
  124 + if has_category_path < len(df) * 0.5:
  125 + logger.warning(f"Only {100*has_category_path/len(df):.1f}% of SPUs have category_path, data quality may be low")
  126 + else:
  127 + logger.warning(f"No SPU data found for tenant_id={self.tenant_id}")
  128 +
  129 + # Debug: Check if there's any data for this tenant_id
66 debug_query = text(""" 130 debug_query = text("""
67 SELECT 131 SELECT
68 COUNT(*) as total_count, 132 COUNT(*) as total_count,
@@ -77,7 +141,7 @@ class SPUTransformer: @@ -77,7 +141,7 @@ class SPUTransformer:
77 total = debug_df.iloc[0]['total_count'] 141 total = debug_df.iloc[0]['total_count']
78 active = debug_df.iloc[0]['active_count'] 142 active = debug_df.iloc[0]['active_count']
79 deleted = debug_df.iloc[0]['deleted_count'] 143 deleted = debug_df.iloc[0]['deleted_count']
80 - print(f"DEBUG: tenant_id={self.tenant_id}: total={total}, active={active}, deleted={deleted}") 144 + logger.debug(f"tenant_id={self.tenant_id}: total={total}, active={active}, deleted={deleted}")
81 145
82 # Check what tenant_ids exist in the table 146 # Check what tenant_ids exist in the table
83 tenant_check_query = text(""" 147 tenant_check_query = text("""
@@ -90,9 +154,9 @@ class SPUTransformer: @@ -90,9 +154,9 @@ class SPUTransformer:
90 with self.db_engine.connect() as conn: 154 with self.db_engine.connect() as conn:
91 tenant_df = pd.read_sql(tenant_check_query, conn) 155 tenant_df = pd.read_sql(tenant_check_query, conn)
92 if not tenant_df.empty: 156 if not tenant_df.empty:
93 - print(f"DEBUG: Available tenant_ids in shoplazza_product_spu:") 157 + logger.debug(f"Available tenant_ids in shoplazza_product_spu:")
94 for _, row in tenant_df.iterrows(): 158 for _, row in tenant_df.iterrows():
95 - print(f" tenant_id={row['tenant_id']}: total={row['count']}, active={row['active']}") 159 + logger.debug(f" tenant_id={row['tenant_id']}: total={row['count']}, active={row['active']}")
96 160
97 return df 161 return df
98 162
@@ -120,7 +184,26 @@ class SPUTransformer: @@ -120,7 +184,26 @@ class SPUTransformer:
120 with self.db_engine.connect() as conn: 184 with self.db_engine.connect() as conn:
121 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id}) 185 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id})
122 186
123 - print(f"DEBUG: Loaded {len(df)} SKU records for tenant_id={self.tenant_id}") 187 + logger.info(f"Loaded {len(df)} SKU records for tenant_id={self.tenant_id}")
  188 +
  189 + # Statistics
  190 + if len(df) > 0:
  191 + has_price = df['price'].notna().sum()
  192 + has_inventory = df['inventory_quantity'].notna().sum()
  193 + has_option1 = df['option1'].notna().sum()
  194 + has_option2 = df['option2'].notna().sum()
  195 + has_option3 = df['option3'].notna().sum()
  196 +
  197 + logger.info(f"SKU data statistics:")
  198 + logger.info(f" - Has price: {has_price}/{len(df)} ({100*has_price/len(df):.1f}%)")
  199 + logger.info(f" - Has inventory: {has_inventory}/{len(df)} ({100*has_inventory/len(df):.1f}%)")
  200 + logger.info(f" - Has option1: {has_option1}/{len(df)} ({100*has_option1/len(df):.1f}%)")
  201 + logger.info(f" - Has option2: {has_option2}/{len(df)} ({100*has_option2/len(df):.1f}%)")
  202 + logger.info(f" - Has option3: {has_option3}/{len(df)} ({100*has_option3/len(df):.1f}%)")
  203 +
  204 + # Warn about data quality issues
  205 + if has_price < len(df) * 0.95:
  206 + logger.warning(f"Only {100*has_price/len(df):.1f}% of SKUs have price")
124 207
125 return df 208 return df
126 209
@@ -144,7 +227,21 @@ class SPUTransformer: @@ -144,7 +227,21 @@ class SPUTransformer:
144 with self.db_engine.connect() as conn: 227 with self.db_engine.connect() as conn:
145 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id}) 228 df = pd.read_sql(query, conn, params={"tenant_id": self.tenant_id})
146 229
147 - print(f"DEBUG: Loaded {len(df)} option records for tenant_id={self.tenant_id}") 230 + logger.info(f"Loaded {len(df)} option records for tenant_id={self.tenant_id}")
  231 +
  232 + # Statistics
  233 + if len(df) > 0:
  234 + unique_spus_with_options = df['spu_id'].nunique()
  235 + has_name = df['name'].notna().sum()
  236 +
  237 + logger.info(f"Option data statistics:")
  238 + logger.info(f" - Unique SPUs with options: {unique_spus_with_options}")
  239 + logger.info(f" - Has name: {has_name}/{len(df)} ({100*has_name/len(df):.1f}%)")
  240 +
  241 + # Warn about missing option names
  242 + if has_name < len(df):
  243 + missing = len(df) - has_name
  244 + logger.warning(f"{missing} option records are missing names")
148 245
149 return df 246 return df
150 247
@@ -155,35 +252,61 @@ class SPUTransformer: @@ -155,35 +252,61 @@ class SPUTransformer:
155 Returns: 252 Returns:
156 List of SPU-level ES documents 253 List of SPU-level ES documents
157 """ 254 """
  255 + logger.info(f"Starting data transformation for tenant_id={self.tenant_id}")
  256 +
158 # Load data 257 # Load data
159 spu_df = self.load_spu_data() 258 spu_df = self.load_spu_data()
160 sku_df = self.load_sku_data() 259 sku_df = self.load_sku_data()
161 option_df = self.load_option_data() 260 option_df = self.load_option_data()
162 261
163 if spu_df.empty: 262 if spu_df.empty:
  263 + logger.warning("No SPU data to transform")
164 return [] 264 return []
165 265
166 # Group SKUs by SPU 266 # Group SKUs by SPU
167 sku_groups = sku_df.groupby('spu_id') 267 sku_groups = sku_df.groupby('spu_id')
  268 + logger.info(f"Grouped SKUs into {len(sku_groups)} SPU groups")
168 269
169 # Group options by SPU 270 # Group options by SPU
170 option_groups = option_df.groupby('spu_id') if not option_df.empty else None 271 option_groups = option_df.groupby('spu_id') if not option_df.empty else None
  272 + if option_groups:
  273 + logger.info(f"Grouped options into {len(option_groups)} SPU groups")
171 274
172 documents = [] 275 documents = []
173 - for _, spu_row in spu_df.iterrows(): 276 + skipped_count = 0
  277 + error_count = 0
  278 +
  279 + for idx, spu_row in spu_df.iterrows():
174 spu_id = spu_row['id'] 280 spu_id = spu_row['id']
175 281
176 - # Get SKUs for this SPU  
177 - skus = sku_groups.get_group(spu_id) if spu_id in sku_groups.groups else pd.DataFrame()  
178 -  
179 - # Get options for this SPU  
180 - options = option_groups.get_group(spu_id) if option_groups and spu_id in option_groups.groups else pd.DataFrame()  
181 -  
182 - # Transform to ES document  
183 - doc = self._transform_spu_to_doc(spu_row, skus, options)  
184 - if doc:  
185 - documents.append(doc)  
186 - 282 + try:
  283 + # Get SKUs for this SPU
  284 + skus = sku_groups.get_group(spu_id) if spu_id in sku_groups.groups else pd.DataFrame()
  285 +
  286 + # Get options for this SPU
  287 + options = option_groups.get_group(spu_id) if option_groups and spu_id in option_groups.groups else pd.DataFrame()
  288 +
  289 + # Warn if SPU has no SKUs
  290 + if skus.empty:
  291 + logger.warning(f"SPU {spu_id} (title: {spu_row.get('title', 'N/A')}) has no SKUs")
  292 +
  293 + # Transform to ES document
  294 + doc = self._transform_spu_to_doc(spu_row, skus, options)
  295 + if doc:
  296 + documents.append(doc)
  297 + else:
  298 + skipped_count += 1
  299 + logger.warning(f"SPU {spu_id} transformation returned None, skipped")
  300 + except Exception as e:
  301 + error_count += 1
  302 + logger.error(f"Error transforming SPU {spu_id}: {e}", exc_info=True)
  303 +
  304 + logger.info(f"Transformation complete:")
  305 + logger.info(f" - Total SPUs: {len(spu_df)}")
  306 + logger.info(f" - Successfully transformed: {len(documents)}")
  307 + logger.info(f" - Skipped: {skipped_count}")
  308 + logger.info(f" - Errors: {error_count}")
  309 +
187 return documents 310 return documents
188 311
189 def _transform_spu_to_doc( 312 def _transform_spu_to_doc(
@@ -209,7 +332,12 @@ class SPUTransformer: @@ -209,7 +332,12 @@ class SPUTransformer:
209 doc['tenant_id'] = str(self.tenant_id) 332 doc['tenant_id'] = str(self.tenant_id)
210 333
211 # SPU ID 334 # SPU ID
212 - doc['spu_id'] = str(spu_row['id']) 335 + spu_id = spu_row['id']
  336 + doc['spu_id'] = str(spu_id)
  337 +
  338 + # Validate required fields
  339 + if pd.isna(spu_row.get('title')) or not str(spu_row['title']).strip():
  340 + logger.error(f"SPU {spu_id} has no title, this may cause search issues")
213 341
214 # 文本相关性相关字段(中英文双语,暂时只填充中文) 342 # 文本相关性相关字段(中英文双语,暂时只填充中文)
215 if pd.notna(spu_row.get('title')): 343 if pd.notna(spu_row.get('title')):
@@ -237,17 +365,35 @@ class SPUTransformer: @@ -237,17 +365,35 @@ class SPUTransformer:
237 # Category相关字段 365 # Category相关字段
238 if pd.notna(spu_row.get('category_path')): 366 if pd.notna(spu_row.get('category_path')):
239 category_path = str(spu_row['category_path']) 367 category_path = str(spu_row['category_path'])
240 - doc['category_path_zh'] = category_path  
241 - doc['category_path_en'] = None # 暂时设为空  
242 368
243 - # 解析category_path获取多层级分类名称  
244 - path_parts = category_path.split('/')  
245 - if len(path_parts) > 0:  
246 - doc['category1_name'] = path_parts[0].strip()  
247 - if len(path_parts) > 1:  
248 - doc['category2_name'] = path_parts[1].strip()  
249 - if len(path_parts) > 2:  
250 - doc['category3_name'] = path_parts[2].strip() 369 + # 解析category_path - 这是逗号分隔的类目ID列表
  370 + category_ids = [cid.strip() for cid in category_path.split(',') if cid.strip()]
  371 +
  372 + # 将ID映射为名称
  373 + category_names = []
  374 + missing_category_ids = []
  375 + for cid in category_ids:
  376 + if cid in self.category_id_to_name:
  377 + category_names.append(self.category_id_to_name[cid])
  378 + else:
  379 + # 如果找不到映射,记录错误并使用ID作为备选
  380 + logger.error(f"Category ID {cid} not found in mapping for SPU {spu_row['id']} (title: {spu_row.get('title', 'N/A')}), category_path={category_path}")
  381 + missing_category_ids.append(cid)
  382 + category_names.append(cid) # 使用ID作为备选
  383 +
  384 + # 构建类目路径字符串(用于搜索)
  385 + if category_names:
  386 + category_path_str = '/'.join(category_names)
  387 + doc['category_path_zh'] = category_path_str
  388 + doc['category_path_en'] = None # 暂时设为空
  389 +
  390 + # 填充分层类目名称
  391 + if len(category_names) > 0:
  392 + doc['category1_name'] = category_names[0]
  393 + if len(category_names) > 1:
  394 + doc['category2_name'] = category_names[1]
  395 + if len(category_names) > 2:
  396 + doc['category3_name'] = category_names[2]
251 elif pd.notna(spu_row.get('category')): 397 elif pd.notna(spu_row.get('category')):
252 # 如果category_path为空,使用category字段作为category1_name的备选 398 # 如果category_path为空,使用category字段作为category1_name的备选
253 category = str(spu_row['category']) 399 category = str(spu_row['category'])
@@ -302,6 +448,15 @@ class SPUTransformer: @@ -302,6 +448,15 @@ class SPUTransformer:
302 image_src = f"//{image_src}" if image_src.startswith('//') else image_src 448 image_src = f"//{image_src}" if image_src.startswith('//') else image_src
303 doc['image_url'] = image_src 449 doc['image_url'] = image_src
304 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 +
305 # Process SKUs and build specifications 460 # Process SKUs and build specifications
306 skus_list = [] 461 skus_list = []
307 prices = [] 462 prices = []
search/es_query_builder.py
@@ -519,7 +519,7 @@ class ESQueryBuilder: @@ -519,7 +519,7 @@ class ESQueryBuilder:
519 519
520 Args: 520 Args:
521 es_query: Existing ES query 521 es_query: Existing ES query
522 - sort_by: Field name for sorting 522 + sort_by: Field name for sorting (支持 'price' 自动映射)
523 sort_order: Sort order: 'asc' or 'desc' 523 sort_order: Sort order: 'asc' or 'desc'
524 524
525 Returns: 525 Returns:
@@ -531,6 +531,13 @@ class ESQueryBuilder: @@ -531,6 +531,13 @@ class ESQueryBuilder:
531 if not sort_order: 531 if not sort_order:
532 sort_order = "desc" 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 if "sort" not in es_query: 541 if "sort" not in es_query:
535 es_query["sort"] = [] 542 es_query["sort"] = []
536 543
@@ -546,20 +553,18 @@ class ESQueryBuilder: @@ -546,20 +553,18 @@ class ESQueryBuilder:
546 553
547 def build_facets( 554 def build_facets(
548 self, 555 self,
549 - facet_configs: Optional[List[Union[str, 'FacetConfig']]] = None 556 + facet_configs: Optional[List['FacetConfig']] = None
550 ) -> Dict[str, Any]: 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 Args: 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 Returns: 569 Returns:
565 ES aggregations 字典 570 ES aggregations 字典
@@ -570,99 +575,76 @@ class ESQueryBuilder: @@ -570,99 +575,76 @@ class ESQueryBuilder:
570 aggs = {} 575 aggs = {}
571 576
572 for config in facet_configs: 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 aggs[agg_name] = { 634 aggs[agg_name] = {
634 "terms": { 635 "terms": {
635 "field": field, 636 "field": field,
636 - "size": 10, 637 + "size": size,
637 "order": {"_count": "desc"} 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 aggs[agg_name] = { 643 aggs[agg_name] = {
652 - "terms": { 644 + "range": {
653 "field": field, 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 return aggs 650 return aggs
search/searcher.py
@@ -17,7 +17,7 @@ from .rerank_engine import RerankEngine @@ -17,7 +17,7 @@ from .rerank_engine import RerankEngine
17 from config import SearchConfig 17 from config import SearchConfig
18 from config.utils import get_match_fields_for_index 18 from config.utils import get_match_fields_for_index
19 from context.request_context import RequestContext, RequestContextStage, create_request_context 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 from api.result_formatter import ResultFormatter 21 from api.result_formatter import ResultFormatter
22 22
23 logger = logging.getLogger(__name__) 23 logger = logging.getLogger(__name__)
@@ -123,7 +123,7 @@ class Searcher: @@ -123,7 +123,7 @@ class Searcher:
123 from_: int = 0, 123 from_: int = 0,
124 filters: Optional[Dict[str, Any]] = None, 124 filters: Optional[Dict[str, Any]] = None,
125 range_filters: Optional[Dict[str, Any]] = None, 125 range_filters: Optional[Dict[str, Any]] = None,
126 - facets: Optional[List[Any]] = None, 126 + facets: Optional[List[FacetConfig]] = None,
127 min_score: Optional[float] = None, 127 min_score: Optional[float] = None,
128 context: Optional[RequestContext] = None, 128 context: Optional[RequestContext] = None,
129 sort_by: Optional[str] = None, 129 sort_by: Optional[str] = None,
@@ -651,7 +651,7 @@ class Searcher: @@ -651,7 +651,7 @@ class Searcher:
651 # 构建 FacetResult 对象 651 # 构建 FacetResult 对象
652 facet_result = FacetResult( 652 facet_result = FacetResult(
653 field=field, 653 field=field,
654 - label=self._get_field_label(field), 654 + label=field,
655 type=facet_type, 655 type=facet_type,
656 values=facet_values 656 values=facet_values
657 ) 657 )
@@ -659,14 +659,3 @@ class Searcher: @@ -659,14 +659,3 @@ class Searcher:
659 standardized_facets.append(facet_result) 659 standardized_facets.append(facet_result)
660 660
661 return standardized_facets if standardized_facets else None 661 return standardized_facets if standardized_facets else None
662 -  
663 - def _get_field_label(self, field: str) -> str:  
664 - """获取字段的显示标签"""  
665 - # 字段标签映射(简化版,不再从配置读取)  
666 - field_labels = {  
667 - "category1_name": "一级分类",  
668 - "category2_name": "二级分类",  
669 - "category3_name": "三级分类",  
670 - "specifications": "规格"  
671 - }  
672 - return field_labels.get(field, field)