Commit 0342d897a243f63462f74765362a2e008709d5ff

Authored by tangwang
1 parent 41f0b2e9

搜索API对接指南 拆分

docs/搜索API对接指南-00-总览与快速开始.md 0 → 100644
... ... @@ -0,0 +1,110 @@
  1 +# 搜索API对接指南-00-总览与快速开始
  2 +
  3 +本文档旨在为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。
  4 +拆分目录:
  5 +- `-01-搜索接口(POST /search/ 与响应)`
  6 +- `-02-搜索建议与即时搜索`
  7 +- `-03-获取文档(GET /search/{doc_id})`
  8 +- `-05-索引接口(Indexer)`
  9 +- `-06-管理接口(Admin)`
  10 +- `-07-微服务接口(Embedding/Reranker/Translation)`
  11 +- `-08-数据模型与字段速查`
  12 +- `-10-接口级压测脚本`
  13 +
  14 +## 快速开始
  15 +
  16 +### 1.1 基础信息
  17 +
  18 +- **Base URL**: `http://43.166.252.75:6002`
  19 +- **协议**: HTTP/HTTPS
  20 +- **数据格式**: JSON
  21 +- **字符编码**: UTF-8
  22 +- **请求方法**: POST(搜索接口)
  23 +
  24 +**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。
  25 +
  26 +**环境与凭证**:MySQL、Redis、Elasticsearch 等外部服务的 AI 生产地址与凭证见 [QUICKSTART.md §1.6](./QUICKSTART.md#16-外部服务与-env含生产凭证)。
  27 +
  28 +### 1.2 最简单的搜索请求
  29 +
  30 +```bash
  31 +curl -X POST "http://43.166.252.75:6002/search/" \
  32 + -H "Content-Type: application/json" \
  33 + -H "X-Tenant-ID: 162" \
  34 + -d '{"query": "芭比娃娃"}'
  35 +```
  36 +
  37 +### 1.3 带过滤与分页的搜索
  38 +
  39 +```bash
  40 +curl -X POST "http://43.166.252.75:6002/search/" \
  41 + -H "Content-Type: application/json" \
  42 + -H "X-Tenant-ID: 162" \
  43 + -d '{
  44 + "query": "芭比娃娃",
  45 + "size": 5,
  46 + "from": 10,
  47 + "range_filters": {
  48 + "min_price": {
  49 + "gte": 50,
  50 + "lte": 200
  51 + },
  52 + "create_time": {
  53 + "gte": "2020-01-01T00:00:00Z"
  54 + }
  55 + },
  56 + "sort_by": "price",
  57 + "sort_order": "asc"
  58 + }'
  59 +```
  60 +
  61 +### 1.4 开启分面的搜索
  62 +
  63 +```bash
  64 +curl -X POST "http://43.166.252.75:6002/search/" \
  65 + -H "Content-Type: application/json" \
  66 + -H "X-Tenant-ID: 162" \
  67 + -d '{
  68 + "query": "芭比娃娃",
  69 + "facets": [
  70 + {"field": "category1_name", "size": 10, "type": "terms"},
  71 + {"field": "specifications.color", "size": 10, "type": "terms"},
  72 + {"field": "specifications.size", "size": 10, "type": "terms"}
  73 + ],
  74 + "min_score": 0.2
  75 + }'
  76 +```
  77 +
  78 +---
  79 +
  80 +## 接口概览
  81 +
  82 +| 接口 | HTTP Method | Endpoint | 说明 |
  83 +|------|------|------|------|
  84 +| 搜索 | POST | `/search/` | 执行搜索查询 |
  85 +| 搜索建议 | GET | `/search/suggestions` | 搜索建议(自动补全/热词,多语言) |
  86 +| 即时搜索 | GET | `/search/instant` | 即时搜索预留接口(当前返回 `501 Not Implemented`) |
  87 +| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 |
  88 +| 全量索引 | POST | `/indexer/reindex` | 全量索引接口(导入数据,不删除索引,仅推荐自测使用) |
  89 +| 增量索引 | POST | `/indexer/index` | 增量索引接口(指定SPU ID列表进行索引,支持自动检测删除和显式删除,仅推荐自测使用) |
  90 +| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据(不写入ES) |
  91 +| 构建ES文档(正式对接) | POST | `/indexer/build-docs` | 基于上游提供的 MySQL 行数据构建 ES doc,不写入 ES,供 Java 等调用后自行写入 |
  92 +| 构建ES文档(测试用) | POST | `/indexer/build-docs-from-db` | 仅在测试/调试时使用,根据 `tenant_id + spu_ids` 内部查库并构建 ES doc |
  93 +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags,供微服务组合方式使用 |
  94 +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 |
  95 +| 健康检查 | GET | `/admin/health` | 服务健康检查 |
  96 +| 获取配置 | GET | `/admin/config` | 获取租户配置 |
  97 +| 索引统计 | GET | `/admin/stats` | 获取租户索引统计信息(需 tenant_id) |
  98 +
  99 +**微服务(独立端口或 Indexer 内,外部可直连)**:
  100 +
  101 +| 服务 | 端口 | 接口 | 说明 |
  102 +|------|------|------|------|
  103 +| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 |
  104 +| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 |
  105 +| 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) |
  106 +| 重排服务 | 6007 | `POST /rerank` | 检索结果重排 |
  107 +| 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 |
  108 +
  109 +---
  110 +
... ...
docs/搜索API对接指南-01-搜索接口.md 0 → 100644
... ... @@ -0,0 +1,903 @@
  1 +# 搜索API对接指南-01-搜索接口(POST /search/ 与响应)
  2 +
  3 +本篇以 `POST /search/` 为主线,包含:
  4 +- 请求参数:`3.2`、过滤器:`3.3`、分面:`3.4`、SKU筛选维度:`3.5`
  5 +- 响应格式:第 `4` 章(4.1~4.5)
  6 +- 常见场景示例:第 `8` 章(示例整体并入本篇,避免散落)
  7 +
  8 +## 搜索接口
  9 +
  10 +### 3.1 接口信息
  11 +
  12 +- **端点**: `POST /search/`
  13 +- **描述**: 执行文本搜索查询,支持多语言、过滤器和分面搜索
  14 +- **租户标识**:`tenant_id` 通过 HTTP 请求头 **`X-Tenant-ID`** 传递(推荐);也可通过 URL query 参数 **`tenant_id`** 传递。**不要放在请求体中。**
  15 +
  16 +**请求示例(推荐)**:
  17 +
  18 +```python
  19 +url = f"{base_url.rstrip('/')}/search/"
  20 +headers = {
  21 + "Content-Type": "application/json",
  22 + "X-Tenant-ID": "162", # 租户ID,必填
  23 +}
  24 +response = requests.post(url, headers=headers, json={"query": "芭比娃娃"})
  25 +```
  26 +
  27 +### 3.2 请求参数
  28 +
  29 +#### 完整请求体结构
  30 +
  31 +```json
  32 +{
  33 + "query": "string (required)",
  34 + "size": 10,
  35 + "from": 0,
  36 + "language": "zh",
  37 + "filters": {},
  38 + "range_filters": {},
  39 + "facets": [],
  40 + "sort_by": "string",
  41 + "sort_order": "desc",
  42 + "min_score": 0.0,
  43 + "sku_filter_dimension": ["string"],
  44 + "debug": false,
  45 + "enable_rerank": null,
  46 + "rerank_query_template": "{query}",
  47 + "rerank_doc_template": "{title}",
  48 + "user_id": "string",
  49 + "session_id": "string"
  50 +}
  51 +```
  52 +
  53 +#### 参数详细说明
  54 +
  55 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  56 +|------|------|------|--------|------|
  57 +| `query` | string | Y | - | 搜索查询字符串(统一文本检索策略) |
  58 +| `size` | integer | N | 10 | 返回结果数量(1-100) |
  59 +| `from` | integer | N | 0 | 分页偏移量(用于分页) |
  60 +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 |
  61 +| `filters` | object | N | null | 精确匹配过滤器(见[过滤器详解](#33-过滤器详解)) |
  62 +| `range_filters` | object | N | null | 数值范围过滤器(见[过滤器详解](#33-过滤器详解)) |
  63 +| `facets` | array | N | null | 分面配置(见[分面配置](#34-分面配置)) |
  64 +| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 |
  65 +| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) |
  66 +| `min_score` | float | N | null | 最小相关性分数阈值 |
  67 +| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) |
  68 +| `debug` | boolean | N | false | 是否返回调试信息 |
  69 +| `enable_rerank` | boolean/null | N | null | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。不传/传 null 使用服务端 `rerank.enabled`(默认开启)。开启后会先对 ES TopN(`rerank_window`)重排,再按分页截取;若 `from+size>1000`,则不重排,直接按分页从 ES 返回 |
  70 +| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 |
  71 +| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 |
  72 +| `user_id` | string | N | null | 用户ID(用于个性化,预留) |
  73 +| `session_id` | string | N | null | 会话ID(用于分析,预留) |
  74 +
  75 +### 3.3 过滤器详解
  76 +
  77 +#### 3.3.1 精确匹配过滤器 (filters)
  78 +
  79 +用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。
  80 +
  81 +**格式**:
  82 +
  83 +```json
  84 +{
  85 + "filters": {
  86 + "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  87 + "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  88 + "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  89 + "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  90 + "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  91 + "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR)
  92 + "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签
  93 + "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段
  94 + // specifications 嵌套过滤(特殊格式)
  95 + "specifications": {
  96 + "name": "color",
  97 + "value": "white"
  98 + }
  99 + }
  100 +}
  101 +```
  102 +
  103 +**支持的值类型**:
  104 +- 字符串:精确匹配
  105 +- 整数:精确匹配
  106 +- 布尔值:精确匹配
  107 +- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值)
  108 +- 对象:specifications 嵌套过滤(见下文)
  109 +
  110 +**`*_all` 语义(多值 AND)**:
  111 +- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。
  112 +- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。
  113 +- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。
  114 +
  115 +**Specifications 嵌套过滤**:
  116 +
  117 +`specifications` 是嵌套字段,支持按规格名称和值进行过滤。
  118 +
  119 +**单个规格过滤**:
  120 +
  121 +```json
  122 +{
  123 + "filters": {
  124 + "specifications": {
  125 + "name": "color",
  126 + "value": "white"
  127 + }
  128 + }
  129 +}
  130 +```
  131 +
  132 +查询规格名称为"color"且值为"white"的商品。
  133 +
  134 +**多个规格过滤(按维度分组)**:
  135 +
  136 +```json
  137 +{
  138 + "filters": {
  139 + "specifications": [
  140 + {"name": "color", "value": "white"},
  141 + {"name": "size", "value": "256GB"}
  142 + ]
  143 + }
  144 +}
  145 +```
  146 +
  147 +查询同时满足所有规格的商品(color=white **且** size=256GB)。
  148 +
  149 +**相同维度的多个值(OR 逻辑)**:
  150 +
  151 +```json
  152 +{
  153 + "filters": {
  154 + "specifications": [
  155 + {"name": "size", "value": "3"},
  156 + {"name": "size", "value": "4"},
  157 + {"name": "size", "value": "5"},
  158 + {"name": "color", "value": "green"}
  159 + ]
  160 + }
  161 +}
  162 +```
  163 +
  164 +查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。
  165 +
  166 +**过滤逻辑说明**:
  167 +- **不同维度**(不同的 `name`)之间是 **AND** 关系(求交集)
  168 +- **相同维度**(相同的 `name`)的多个值之间是 **OR** 关系(求并集)
  169 +
  170 +**常用过滤字段**(详见[常用字段列表](./搜索API对接指南-08-数据模型与字段速查.md#93-常用字段列表)):
  171 +- `category_name`: 类目名称
  172 +- `category1_name`, `category2_name`, `category3_name`: 多级类目
  173 +- `category_id`: 类目ID
  174 +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段)
  175 +- `tags`: 标签(keyword类型,支持数组)
  176 +- `option1_name`, `option2_name`, `option3_name`: 选项名称
  177 +- `specifications`: 规格过滤(嵌套字段,格式见上文)
  178 +- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。
  179 +
  180 +#### 3.3.2 范围过滤器 (range_filters)
  181 +
  182 +用于数值字段的范围过滤。
  183 +
  184 +**格式**:
  185 +
  186 +```json
  187 +{
  188 + "range_filters": {
  189 + "min_price": {
  190 + "gte": 50, // 大于等于
  191 + "lte": 200 // 小于等于
  192 + },
  193 + "max_price": {
  194 + "gt": 100 // 大于
  195 + },
  196 + "create_time": {
  197 + "gte": "2024-01-01T00:00:00Z" // 日期时间字符串
  198 + }
  199 + }
  200 +}
  201 +```
  202 +
  203 +**支持的操作符**:
  204 +- `gte`: 大于等于 (>=)
  205 +- `gt`: 大于 (>)
  206 +- `lte`: 小于等于 (<=)
  207 +- `lt`: 小于 (<)
  208 +
  209 +**注意**: 至少需要指定一个操作符。
  210 +
  211 +**常用范围字段**(详见[常用字段列表](./搜索API对接指南-08-数据模型与字段速查.md#93-常用字段列表)):
  212 +- `min_price`: 最低价格
  213 +- `max_price`: 最高价格
  214 +- `compare_at_price`: 原价
  215 +- `create_time`: 创建时间
  216 +- `update_time`: 更新时间
  217 +
  218 +### 3.4 分面配置
  219 +
  220 +用于生成分面统计(分组聚合),常用于构建筛选器UI。
  221 +
  222 +#### 3.4.1 配置格式
  223 +
  224 +```json
  225 +{
  226 + "facets": [
  227 + {
  228 + "field": "category1_name",
  229 + "size": 15,
  230 + "type": "terms",
  231 + "disjunctive": false
  232 + },
  233 + {
  234 + "field": "brand_name",
  235 + "size": 10,
  236 + "type": "terms",
  237 + "disjunctive": true
  238 + },
  239 + {
  240 + "field": "specifications.color",
  241 + "size": 20,
  242 + "type": "terms",
  243 + "disjunctive": true
  244 + },
  245 + {
  246 + "field": "min_price",
  247 + "type": "range",
  248 + "ranges": [
  249 + {"key": "0-50", "to": 50},
  250 + {"key": "50-100", "from": 50, "to": 100},
  251 + {"key": "100-200", "from": 100, "to": 200},
  252 + {"key": "200+", "from": 200}
  253 + ]
  254 + }
  255 + ]
  256 +}
  257 +```
  258 +
  259 +#### 3.4.2 Facet 字段说明
  260 +
  261 +| 字段 | 类型 | 必填 | 默认值 | 说明 |
  262 +|------|------|------|--------|------|
  263 +| `field` | string | 是 | - | 分面字段名 |
  264 +| `size` | int | 否 | 10 | 返回的分面值数量(1-100) |
  265 +| `type` | string | 否 | "terms" | 分面类型:`terms`(词条聚合)或 `range`(范围聚合) |
  266 +| `disjunctive` | bool | 否 | false | 是否支持多选(disjunctive faceting)。启用后,选中该分面的过滤器时,仍会显示其他可选项 |
  267 +| `ranges` | array | 否 | null | 范围配置(仅 `type="range"` 时需要) |
  268 +
  269 +#### 3.4.3 disjunctive字段说明
  270 +
  271 +**重要特性**: `disjunctive` 字段控制分面的行为模式。启用后,选中该分面的过滤器时,仍会显示其他可选项
  272 +
  273 +**标准模式 (disjunctive: false)**:
  274 +- **行为**: 选中某个分面值后,该分面只显示选中的值
  275 +- **适用场景**: 层级类目、互斥选择
  276 +- **示例**: 类目下钻(玩具 > 娃娃 > 芭比)
  277 +
  278 +**Multi-Select 模式 (disjunctive: true)** ⭐:
  279 +- **行为**: 选中某个分面值后,该分面仍显示所有可选项
  280 +- **适用场景**: 颜色、品牌、尺码等可切换属性
  281 +- **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项
  282 +
  283 +**推荐配置**:
  284 +
  285 +| 分面类型 | disjunctive | 原因 |
  286 +|---------|-------------|------|
  287 +| 颜色 | `true` | 用户需要切换颜色 |
  288 +| 品牌 | `true` | 用户需要比较品牌 |
  289 +| 尺码 | `true` | 用户需要查看其他尺码 |
  290 +| 类目 | `false` | 层级下钻 |
  291 +| 价格区间 | `false` | 互斥选择 |
  292 +
  293 +#### 3.4.4 规格分面说明
  294 +
  295 +`specifications` 是嵌套字段,支持两种分面模式:
  296 +
  297 +**模式1:所有规格名称的分面**:
  298 +
  299 +```json
  300 +{
  301 + "facets": [
  302 + {
  303 + "field": "specifications",
  304 + "size": 10,
  305 + "type": "terms"
  306 + }
  307 + ]
  308 +}
  309 +```
  310 +
  311 +返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。
  312 +
  313 +**模式2:指定规格名称的分面**:
  314 +
  315 +```json
  316 +{
  317 + "facets": [
  318 + {
  319 + "field": "specifications.color",
  320 + "size": 20,
  321 + "type": "terms",
  322 + "disjunctive": true
  323 + },
  324 + {
  325 + "field": "specifications.size",
  326 + "size": 15,
  327 + "type": "terms",
  328 + "disjunctive": true
  329 + }
  330 + ]
  331 +}
  332 +```
  333 +
  334 +只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。
  335 +
  336 +**返回格式示例**:
  337 +
  338 +```json
  339 +{
  340 + "facets": [
  341 + {
  342 + "field": "specifications.color",
  343 + "label": "color",
  344 + "type": "terms",
  345 + "values": [
  346 + {"value": "white", "count": 50, "selected": true}, // ✓ selected 字段由后端标记
  347 + {"value": "black", "count": 30, "selected": false},
  348 + {"value": "red", "count": 20, "selected": false}
  349 + ]
  350 + },
  351 + {
  352 + "field": "specifications.size",
  353 + "label": "size",
  354 + "type": "terms",
  355 + "values": [
  356 + {"value": "256GB", "count": 40, "selected": false},
  357 + {"value": "512GB", "count": 20, "selected": false}
  358 + ]
  359 + }
  360 + ]
  361 +}
  362 +```
  363 +
  364 +### 3.5 SKU筛选维度
  365 +
  366 +**功能说明**:
  367 +`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式(子 SKU)维度**,为字符串列表。
  368 +在店铺的 **主题装修配置** 中,商家可以为店铺设置一个或多个子款式筛选维度(例如 `color`、`size`),前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表,用户可以通过点击不同维度值(如不同颜色)来切换展示的子款式。
  369 +当指定 `sku_filter_dimension` 后,后端会根据店铺的这项配置,从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据:系统会按指定维度**组合**对 SKU 进行分组,每个维度组合只返回第一个 SKU(从简实现,选择该组合下的第一款),其余不在这些维度组合中的子 SKU 将不返回。
  370 +
  371 +**支持的维度值**:
  372 +1. **直接选项字段**: `option1`、`option2`、`option3`
  373 + - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组
  374 +
  375 +2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配
  376 + - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组
  377 +
  378 +**示例**:
  379 +
  380 +**按颜色筛选(假设 option1_name = "color")**:
  381 +
  382 +```json
  383 +{
  384 + "query": "芭比娃娃",
  385 + "sku_filter_dimension": ["color"]
  386 +}
  387 +```
  388 +
  389 +**按选项1筛选**:
  390 +
  391 +```json
  392 +{
  393 + "query": "芭比娃娃",
  394 + "sku_filter_dimension": ["option1"]
  395 +}
  396 +```
  397 +
  398 +**按颜色 + 尺寸组合筛选(假设 option1_name = "color", option2_name = "size")**:
  399 +
  400 +```json
  401 +{
  402 + "query": "芭比娃娃",
  403 + "sku_filter_dimension": ["color", "size"]
  404 +}
  405 +```
  406 +
  407 +## 响应格式说明
  408 +
  409 +### 4.1 标准响应结构
  410 +
  411 +```json
  412 +{
  413 + "results": [
  414 + {
  415 + "spu_id": "12345",
  416 + "title": "芭比时尚娃娃",
  417 + "brief": "高品质芭比娃娃",
  418 + "description": "详细描述...",
  419 + "vendor": "美泰",
  420 + "category": "玩具",
  421 + "category_path": "玩具/娃娃/时尚",
  422 + "category_name": "时尚",
  423 + "category_id": "cat_001",
  424 + "category_level": 3,
  425 + "category1_name": "玩具",
  426 + "category2_name": "娃娃",
  427 + "category3_name": "时尚",
  428 + "tags": ["娃娃", "玩具", "女孩"],
  429 + "price": 89.99,
  430 + "compare_at_price": 129.99,
  431 + "currency": "USD",
  432 + "image_url": "https://example.com/image.jpg",
  433 + "in_stock": true,
  434 + "sku_prices": [89.99, 99.99, 109.99],
  435 + "sku_weights": [100, 150, 200],
  436 + "sku_weight_units": ["g", "g", "g"],
  437 + "total_inventory": 500,
  438 + "option1_name": "color",
  439 + "option2_name": "size",
  440 + "option3_name": null,
  441 + "specifications": [
  442 + {"sku_id": "sku_001", "name": "color", "value": "pink"},
  443 + {"sku_id": "sku_001", "name": "size", "value": "standard"}
  444 + ],
  445 + "skus": [
  446 + {
  447 + "sku_id": "67890",
  448 + "price": 89.99,
  449 + "compare_at_price": 129.99,
  450 + "sku": "BARBIE-001",
  451 + "stock": 100,
  452 + "weight": 0.1,
  453 + "weight_unit": "kg",
  454 + "option1_value": "pink",
  455 + "option2_value": "standard",
  456 + "option3_value": null,
  457 + "image_src": "https://example.com/sku1.jpg"
  458 + }
  459 + ],
  460 + "relevance_score": 8.5
  461 + }
  462 + ],
  463 + "total": 118,
  464 + "max_score": 8.5,
  465 + "facets": [
  466 + {
  467 + "field": "category1_name",
  468 + "label": "category1_name",
  469 + "type": "terms",
  470 + "values": [
  471 + {
  472 + "value": "玩具",
  473 + "label": "玩具",
  474 + "count": 85,
  475 + "selected": false
  476 + }
  477 + ]
  478 + },
  479 + {
  480 + "field": "specifications.color",
  481 + "label": "color",
  482 + "type": "terms",
  483 + "values": [
  484 + {
  485 + "value": "pink",
  486 + "label": "pink",
  487 + "count": 30,
  488 + "selected": false
  489 + }
  490 + ]
  491 + }
  492 + ],
  493 + "query_info": {
  494 + "original_query": "芭比娃娃",
  495 + "query_normalized": "芭比娃娃",
  496 + "rewritten_query": "芭比娃娃",
  497 + "detected_language": "zh",
  498 + "translations": {
  499 + "en": "barbie doll"
  500 + },
  501 + "domain": "default"
  502 + },
  503 + "suggestions": [],
  504 + "related_searches": [],
  505 + "took_ms": 45,
  506 + "performance_info": null,
  507 + "debug_info": null
  508 +}
  509 +```
  510 +
  511 +### 4.2 响应字段说明
  512 +
  513 +| 字段 | 类型 | 说明 |
  514 +|------|------|------|
  515 +| `results` | array | 搜索结果列表(SpuResult对象数组) |
  516 +| `results[].spu_id` | string | SPU ID |
  517 +| `results[].title` | string | 商品标题 |
  518 +| `results[].price` | float | 价格(min_price) |
  519 +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) |
  520 +| `results[].relevance_score` | float | 相关性分数 |
  521 +| `total` | integer | 匹配的总文档数 |
  522 +| `max_score` | float | 最高相关性分数 |
  523 +| `facets` | array | 分面统计结果 |
  524 +| `query_info` | object | query处理信息 |
  525 +| `took_ms` | integer | 搜索耗时(毫秒) |
  526 +| `debug_info` | object/null | 调试信息,仅当请求传 `debug=true` 时返回 |
  527 +
  528 +#### 4.2.1 query_info 说明
  529 +
  530 +`query_info` 包含本次搜索的查询解析与处理结果:
  531 +
  532 +| 子字段 | 类型 | 说明 |
  533 +|--------|------|------|
  534 +| `original_query` | string | 用户原始查询 |
  535 +| `query_normalized` | string | 归一化后的查询(去空白、大小写等预处理,用于后续解析与改写) |
  536 +| `rewritten_query` | string | 重写后的查询(同义词/词典扩展等) |
  537 +| `detected_language` | string | 检测到的查询语言(如 `zh`、`en`) |
  538 +| `translations` | object | 翻译结果,键为语言代码,值为翻译文本 |
  539 +| `domain` | string | 查询域(如 `default`、`title`、`brand` 等) |
  540 +
  541 +#### 4.2.2 debug_info 说明
  542 +
  543 +`debug_info` 主要用于检索效果评估、融合打分分析与 bad case 排查。
  544 +
  545 +`debug_info.query_analysis` 常见字段:
  546 +
  547 +| 子字段 | 类型 | 说明 |
  548 +|--------|------|------|
  549 +| `original_query` | string | 原始查询 |
  550 +| `query_normalized` | string | 归一化后的查询 |
  551 +| `rewritten_query` | string | 重写后的查询 |
  552 +| `detected_language` | string | 检测到的语言 |
  553 +| `translations` | object | 翻译结果 |
  554 +| `query_text_by_lang` | object | 实际参与检索的多语言 query 文本 |
  555 +| `search_langs` | array[string] | 实际参与检索的语言列表 |
  556 +| `supplemental_search_langs` | array[string] | 因 mixed query 补入的附加语言列表 |
  557 +| `has_vector` | boolean | 是否生成了向量 |
  558 +
  559 +`debug_info.per_result[]` 常见字段:
  560 +
  561 +| 子字段 | 类型 | 说明 |
  562 +|--------|------|------|
  563 +| `spu_id` | string | 结果 SPU ID |
  564 +| `es_score` | float | ES 原始 `_score` |
  565 +| `rerank_score` | float | 重排分数 |
  566 +| `text_score` | float | 文本相关性大分(由 `base_query` / `base_query_trans_*` / `fallback_original_query_*` 聚合而来) |
  567 +| `text_source_score` | float | `base_query` 分数 |
  568 +| `text_translation_score` | float | `base_query_trans_*` 里的最大分数 |
  569 +| `text_fallback_score` | float | `fallback_original_query_*` 里的最大分数 |
  570 +| `text_primary_score` | float | 文本大分中的主证据部分 |
  571 +| `text_support_score` | float | 文本大分中的辅助证据部分 |
  572 +| `knn_score` | float | `knn_query` 分数 |
  573 +| `fused_score` | float | 最终融合分数 |
  574 +| `matched_queries` | object/array | ES named queries 命中详情 |
  575 +
  576 +### 4.3 SpuResult字段说明
  577 +
  578 +| 字段 | 类型 | 说明 |
  579 +|------|------|------|
  580 +| `spu_id` | string | SPU ID |
  581 +| `title` | string | 商品标题(根据language参数自动选择 `title.zh` 或 `title.en`) |
  582 +| `brief` | string | 商品短描述(根据language参数自动选择) |
  583 +| `description` | string | 商品详细描述(根据language参数自动选择) |
  584 +| `vendor` | string | 供应商/品牌(根据language参数自动选择) |
  585 +| `category` | string | 类目(兼容字段,等同于category_name) |
  586 +| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) |
  587 +| `category_name` | string | 类目名称(展示用,根据language参数自动选择) |
  588 +| `category_id` | string | 类目ID |
  589 +| `category_level` | integer | 类目层级(1/2/3) |
  590 +| `category1_name` | string | 一级类目名称 |
  591 +| `category2_name` | string | 二级类目名称 |
  592 +| `category3_name` | string | 三级类目名称 |
  593 +| `tags` | array[string] | 标签列表 |
  594 +| `price` | float | 价格(min_price) |
  595 +| `compare_at_price` | float | 原价 |
  596 +| `currency` | string | 货币单位(默认USD) |
  597 +| `image_url` | string | 主图URL |
  598 +| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) |
  599 +| `sku_prices` | array[float] | 所有SKU价格列表 |
  600 +| `sku_weights` | array[integer] | 所有SKU重量列表 |
  601 +| `sku_weight_units` | array[string] | 所有SKU重量单位列表 |
  602 +| `total_inventory` | integer | 总库存 |
  603 +| `sales` | integer | 销量(展示销量) |
  604 +| `option1_name` | string | 选项1名称(如"color") |
  605 +| `option2_name` | string | 选项2名称(如"size") |
  606 +| `option3_name` | string | 选项3名称 |
  607 +| `specifications` | array[object] | 规格列表(与ES specifications字段对应) |
  608 +| `skus` | array | SKU 列表 |
  609 +| `relevance_score` | float | 相关性分数(默认为 ES 原始分数;当开启 AI 搜索时为融合后的最终分数) |
  610 +
  611 +### 4.4 SkuResult字段说明
  612 +
  613 +| 字段 | 类型 | 说明 |
  614 +|------|------|------|
  615 +| `sku_id` | string | SKU ID |
  616 +| `price` | float | 价格 |
  617 +| `compare_at_price` | float | 原价 |
  618 +| `sku` | string | SKU编码(sku_code) |
  619 +| `stock` | integer | 库存数量 |
  620 +| `weight` | float | 重量 |
  621 +| `weight_unit` | string | 重量单位 |
  622 +| `option1_value` | string | 选项1取值(如color值) |
  623 +| `option2_value` | string | 选项2取值(如size值) |
  624 +| `option3_value` | string | 选项3取值 |
  625 +| `image_src` | string | SKU图片地址 |
  626 +
  627 +### 4.5 多语言字段说明
  628 +
  629 +- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段
  630 +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段
  631 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段
  632 +
  633 +---
  634 +
  635 +## 8. 常见场景示例
  636 +
  637 +以下示例仅展示**请求体**(body);实际调用时请加上请求头 `X-Tenant-ID: <租户ID>`(或 URL 参数 `tenant_id`),参见 [3.1 接口信息](#31-接口信息)。
  638 +
  639 +### 8.1 基础搜索与排序
  640 +
  641 +**按价格从低到高排序**:
  642 +
  643 +```json
  644 +{
  645 + "query": "玩具",
  646 + "size": 20,
  647 + "from": 0,
  648 + "sort_by": "price",
  649 + "sort_order": "asc"
  650 +}
  651 +```
  652 +
  653 +**按价格从高到低排序**:
  654 +
  655 +```json
  656 +{
  657 + "query": "玩具",
  658 + "size": 20,
  659 + "from": 0,
  660 + "sort_by": "price",
  661 + "sort_order": "desc"
  662 +}
  663 +```
  664 +
  665 +**按销量从高到低排序**:
  666 +
  667 +```json
  668 +{
  669 + "query": "玩具",
  670 + "size": 20,
  671 + "from": 0,
  672 + "sort_by": "sales",
  673 + "sort_order": "desc"
  674 +}
  675 +```
  676 +
  677 +**按默认(相关性)排序**:
  678 +
  679 +```json
  680 +{
  681 + "query": "玩具",
  682 + "size": 20,
  683 + "from": 0
  684 +}
  685 +```
  686 +
  687 +### 8.2 过滤搜索
  688 +
  689 +**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间
  690 +
  691 +```json
  692 +{
  693 + "query": "玩具",
  694 + "size": 20,
  695 + "language": "zh",
  696 + "filters": {
  697 + "category_name": "益智玩具"
  698 + },
  699 + "range_filters": {
  700 + "min_price": {
  701 + "gte": 50,
  702 + "lte": 200
  703 + }
  704 + }
  705 +}
  706 +```
  707 +
  708 +**需求**: 搜索"手机",筛选多个品牌,价格范围
  709 +
  710 +```json
  711 +{
  712 + "query": "手机",
  713 + "size": 20,
  714 + "language": "zh",
  715 + "filters": {
  716 + "vendor.zh.keyword": ["品牌A", "品牌B"]
  717 + },
  718 + "range_filters": {
  719 + "min_price": {
  720 + "gte": 50,
  721 + "lte": 200
  722 + }
  723 + }
  724 +}
  725 +```
  726 +
  727 +### 8.3 分面搜索
  728 +
  729 +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器
  730 +
  731 +```json
  732 +{
  733 + "query": "玩具",
  734 + "size": 20,
  735 + "language": "zh",
  736 + "facets": [
  737 + {"field": "category1_name", "size": 15, "type": "terms"},
  738 + {"field": "category2_name", "size": 10, "type": "terms"},
  739 + {"field": "specifications", "size": 10, "type": "terms"}
  740 + ]
  741 +}
  742 +```
  743 +
  744 +**需求**: 搜索"手机",获取价格区间和规格的分面统计
  745 +
  746 +```json
  747 +{
  748 + "query": "手机",
  749 + "size": 20,
  750 + "language": "zh",
  751 + "facets": [
  752 + {
  753 + "field": "min_price",
  754 + "type": "range",
  755 + "ranges": [
  756 + {"key": "0-50", "to": 50},
  757 + {"key": "50-100", "from": 50, "to": 100},
  758 + {"key": "100-200", "from": 100, "to": 200},
  759 + {"key": "200+", "from": 200}
  760 + ]
  761 + },
  762 + {
  763 + "field": "specifications",
  764 + "size": 10,
  765 + "type": "terms"
  766 + }
  767 + ]
  768 +}
  769 +```
  770 +
  771 +### 8.4 规格过滤与分面
  772 +
  773 +**需求**: 搜索"手机",筛选color为"white"的商品
  774 +
  775 +```json
  776 +{
  777 + "query": "手机",
  778 + "size": 20,
  779 + "language": "zh",
  780 + "filters": {
  781 + "specifications": {
  782 + "name": "color",
  783 + "value": "white"
  784 + }
  785 + }
  786 +}
  787 +```
  788 +
  789 +**需求**: 搜索"手机",筛选color为"white"且size为"256GB"的商品
  790 +
  791 +```json
  792 +{
  793 + "query": "手机",
  794 + "size": 20,
  795 + "language": "zh",
  796 + "filters": {
  797 + "specifications": [
  798 + {"name": "color", "value": "white"},
  799 + {"name": "size", "value": "256GB"}
  800 + ]
  801 + }
  802 +}
  803 +```
  804 +
  805 +**需求**: 搜索"手机",筛选size为"3"、"4"或"5",且color为"green"的商品
  806 +
  807 +```json
  808 +{
  809 + "query": "手机",
  810 + "size": 20,
  811 + "language": "zh",
  812 + "filters": {
  813 + "specifications": [
  814 + {"name": "size", "value": "3"},
  815 + {"name": "size", "value": "4"},
  816 + {"name": "size", "value": "5"},
  817 + {"name": "color", "value": "green"}
  818 + ]
  819 + }
  820 +}
  821 +```
  822 +
  823 +**需求**: 搜索"手机",获取所有规格的分面统计
  824 +
  825 +```json
  826 +{
  827 + "query": "手机",
  828 + "size": 20,
  829 + "language": "zh",
  830 + "facets": [
  831 + {"field": "specifications", "size": 10, "type": "terms"}
  832 + ]
  833 +}
  834 +```
  835 +
  836 +**需求**: 只获取"color"和"size"规格的分面统计
  837 +
  838 +```json
  839 +{
  840 + "query": "手机",
  841 + "size": 20,
  842 + "language": "zh",
  843 + "facets": [
  844 + {"field": "specifications.color", "size": 20, "type": "terms"},
  845 + {"field": "specifications.size", "size": 15, "type": "terms"}
  846 + ]
  847 +}
  848 +```
  849 +
  850 +**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计
  851 +
  852 +```json
  853 +{
  854 + "query": "手机",
  855 + "size": 20,
  856 + "language": "zh",
  857 + "filters": {
  858 + "category_name": "手机",
  859 + "specifications": {
  860 + "name": "color",
  861 + "value": "white"
  862 + }
  863 + },
  864 + "facets": [
  865 + {"field": "category1_name", "size": 15, "type": "terms"},
  866 + {"field": "category2_name", "size": 10, "type": "terms"},
  867 + {"field": "specifications.color", "size": 20, "type": "terms"},
  868 + {"field": "specifications.size", "size": 15, "type": "terms"}
  869 + ]
  870 +}
  871 +```
  872 +
  873 +### 8.5 SKU筛选
  874 +
  875 +**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU
  876 +
  877 +```json
  878 +{
  879 + "query": "芭比娃娃",
  880 + "size": 20,
  881 + "sku_filter_dimension": ["color"]
  882 +}
  883 +```
  884 +
  885 +**说明**:
  886 +- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组
  887 +- 每个SPU下,每种颜色只会返回第一个SKU
  888 +- 如果维度不匹配,返回所有SKU(不进行过滤)
  889 +
  890 +### 8.6 分页查询
  891 +
  892 +**需求**: 获取第2页结果(每页20条)
  893 +
  894 +```json
  895 +{
  896 + "query": "手机",
  897 + "size": 20,
  898 + "from": 20
  899 +}
  900 +```
  901 +
  902 +---
  903 +
... ...
docs/搜索API对接指南-02-搜索建议与即时搜索.md 0 → 100644
... ... @@ -0,0 +1,81 @@
  1 +# 搜索API对接指南-02-搜索建议与即时搜索
  2 +
  3 +本篇面向前端联想词/搜索框团队,独立阅读 `GET /search/suggestions` 与 `GET /search/instant`。
  4 +
  5 +## 搜索接口
  6 +
  7 +### 3.7 搜索建议接口
  8 +
  9 +- **端点**: `GET /search/suggestions`
  10 +- **描述**: 返回搜索建议(自动补全/热词),支持多语言。
  11 +
  12 +#### 查询参数
  13 +
  14 +| 参数 | 类型 | 必填 | 默认值 | 描述 |
  15 +|------|------|------|--------|------|
  16 +| `q` | string | Y | - | 查询字符串(至少 1 个字符) |
  17 +| `size` | integer | N | 10 | 返回建议数量(1-50) |
  18 +| `language` | string | N | `en` | 请求语言,如 `zh` / `en` / `ar` / `ru`,用于路由到对应语种 suggestion 索引 |
  19 +| `debug` | bool | N | `false` | 是否开启调试(目前主要用于排查 suggestion 排序与语言解析) |
  20 +
  21 +> **租户标识**:同 [-01-搜索接口](./搜索API对接指南-01-搜索接口.md#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。
  22 +
  23 +#### 响应示例
  24 +
  25 +```json
  26 +{
  27 + "query": "iph",
  28 + "language": "en",
  29 + "resolved_language": "en",
  30 + "suggestions": [
  31 + {
  32 + "text": "iphone 15",
  33 + "lang": "en",
  34 + "score": 12.37,
  35 + "rank_score": 5.1,
  36 + "sources": ["query_log", "qanchor"],
  37 + "lang_source": "log_field",
  38 + "lang_confidence": 1.0,
  39 + "lang_conflict": false
  40 + }
  41 + ],
  42 + "took_ms": 12
  43 +}
  44 +```
  45 +
  46 +#### 请求示例
  47 +
  48 +```bash
  49 +curl "http://localhost:6002/search/suggestions?q=芭&size=5&language=zh" \
  50 + -H "X-Tenant-ID: 162"
  51 +```
  52 +
  53 +### 3.8 即时搜索接口
  54 +
  55 +> ⚠️ 当前版本未开放该能力。接口会明确返回 `501 Not Implemented`,避免误用未完成实现。
  56 +
  57 +- **端点**: `GET /search/instant`
  58 +- **描述**: 即时搜索预留端点,后续会在独立实现完成后开放。
  59 +
  60 +#### 查询参数
  61 +
  62 +| 参数 | 类型 | 必填 | 默认值 | 描述 |
  63 +|------|------|------|--------|------|
  64 +| `q` | string | Y | - | 搜索查询(至少 2 个字符) |
  65 +| `size` | integer | N | 5 | 返回结果数量(1-20) |
  66 +
  67 +#### 请求示例
  68 +
  69 +```bash
  70 +curl "http://localhost:6002/search/instant?q=玩具&size=5"
  71 +```
  72 +
  73 +#### 当前响应
  74 +
  75 +```json
  76 +{
  77 + "error": "/search/instant is not implemented yet. Use POST /search/ for production traffic.",
  78 + "status_code": 501
  79 +}
  80 +```
  81 +
... ...
docs/搜索API对接指南-03-获取文档.md 0 → 100644
... ... @@ -0,0 +1,40 @@
  1 +# 搜索API对接指南-03-获取文档(GET /search/{doc_id})
  2 +
  3 +用于点击结果后的详情页回源,或排查某个文档在检索侧的字段情况。
  4 +
  5 +## 搜索接口
  6 +
  7 +### 3.9 获取单个文档
  8 +
  9 +- **端点**: `GET /search/{doc_id}`
  10 +- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。
  11 +- **租户标识**:同 [-01-搜索接口](./搜索API对接指南-01-搜索接口.md#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。
  12 +
  13 +#### 路径参数
  14 +
  15 +| 参数 | 类型 | 描述 |
  16 +|------|------|------|
  17 +| `doc_id` | string | 商品或文档 ID |
  18 +
  19 +#### 响应示例
  20 +
  21 +```json
  22 +{
  23 + "id": "12345",
  24 + "source": {
  25 + "title": {
  26 + "zh": "芭比时尚娃娃"
  27 + },
  28 + "min_price": 89.99,
  29 + "category1_name": "玩具"
  30 + }
  31 +}
  32 +```
  33 +
  34 +#### 请求示例
  35 +
  36 +```bash
  37 +curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162"
  38 +# 或使用 query 参数:curl "http://localhost:6002/search/12345?tenant_id=162"
  39 +```
  40 +
... ...
docs/搜索API对接指南-05-索引接口(Indexer).md 0 → 100644
... ... @@ -0,0 +1,767 @@
  1 +# 搜索API对接指南-05-索引接口(Indexer)
  2 +
  3 +本篇覆盖数据同步/索引构建相关的所有接口(原文第 5 章),用于 `external indexer` 和 `Indexer 服务` 的对接。
  4 +
  5 +## 索引接口
  6 +
  7 +本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口:
  8 +
  9 +| 接口 | 方法 | 路径 | 说明 |
  10 +|------|------|------|------|
  11 +| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) |
  12 +| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 |
  13 +| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES |
  14 +| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES |
  15 +| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 |
  16 +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags(供微服务组合方式使用) |
  17 +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 |
  18 +
  19 +#### 5.0 支撑外部 indexer 的三种方式
  20 +
  21 +本服务对**外部 indexer 程序**(如 Java 索引系统)提供三种对接方式,可按需选择:
  22 +
  23 +| 方式 | 说明 | 适用场景 |
  24 +|------|------|----------|
  25 +| **1)doc 填充接口** | 调用 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db`,由本服务基于 MySQL 行数据构建完整 ES 文档(含多语言、向量、规格等),**不写入 ES**,由调用方自行写入。 | 希望一站式拿到 ES-ready doc,由己方控制写 ES 的时机与索引名。 |
  26 +| **2)微服务组合** | 单独调用**翻译**、**向量化**、**内容理解字段生成**等接口,由 indexer 程序自己组装 doc 并写入 ES。翻译与向量化为独立微服务(见第 7 节);内容理解为 Indexer 服务内接口 `POST /indexer/enrich-content`。 | 需要灵活编排、或希望将 LLM/向量等耗时步骤与主链路解耦(如异步补齐 qanchors/tags)。 |
  27 +| **3)本服务直接写 ES** | 调用全量索引 `POST /indexer/reindex`、增量索引 `POST /indexer/index`(指定 SPU ID 列表),由本服务从 MySQL 拉数并直接写入 ES。 | 自建运维、联调或不需要由 Java 写 ES 的场景。 |
  28 +
  29 +- **方式 1** 与 **方式 2** 下,ES 的写入方均为外部 indexer(或 Java),职责清晰。
  30 +- **方式 3** 下,本服务同时负责读库、构建 doc 与写 ES。
  31 +
  32 +### 5.1 为租户创建索引
  33 +
  34 +为租户创建索引需要两个步骤:
  35 +
  36 +1. **创建索引结构**(可选,仅在需要更新 mapping 或在新环境首次创建时执行)
  37 + - 使用脚本创建 ES 索引结构(基于 `mappings/search_products.json`)
  38 + - 如果索引已存在,会提示用户确认(会删除现有数据)
  39 +
  40 +2. **导入数据**(必需)
  41 + - 使用全量索引接口 `/indexer/reindex` 导入数据
  42 +
  43 +**创建索引结构(支持多环境 namespace)**:
  44 +
  45 +```bash
  46 +# 以 UAT 环境为例:
  47 +# 1. 准备 UAT 环境的 .env(包含 UAT 的 ES_HOST/DB_HOST 等)
  48 +# 2. 设置环境前缀(也可以直接在 .env 中配置):
  49 +export RUNTIME_ENV=uat
  50 +export ES_INDEX_NAMESPACE=uat_
  51 +
  52 +# 3. 为 tenant_id=170 创建索引结构
  53 +./scripts/create_tenant_index.sh 170
  54 +```
  55 +
  56 +脚本会自动从项目根目录的 `.env` 文件加载 ES 配置,并根据 `ES_INDEX_NAMESPACE` 创建:
  57 +
  58 +- prod 环境(ES_INDEX_NAMESPACE 为空):`search_products_tenant_170`
  59 +- UAT 环境(ES_INDEX_NAMESPACE=uat_):`uat_search_products_tenant_170`
  60 +
  61 +**注意事项**:
  62 +- ⚠️ 如果索引已存在,脚本会提示确认,确认后会删除现有数据
  63 +- 创建索引后,**必须**调用 `/indexer/reindex` 导入数据
  64 +- 如果只是更新数据而不需要修改索引结构,直接使用 `/indexer/reindex` 即可
  65 +
  66 +---
  67 +
  68 +### 5.2 全量索引接口
  69 +
  70 +- **端点**: `POST /indexer/reindex`
  71 +- **描述**: 全量索引,将指定租户的所有SPU数据导入到ES索引(不会删除现有索引)。**推荐仅用于自测/运维场景**;生产环境下更推荐由 Java 等上游控制调度与写 ES。
  72 +
  73 +#### 请求参数
  74 +
  75 +```json
  76 +{
  77 + "tenant_id": "162",
  78 + "batch_size": 500
  79 +}
  80 +```
  81 +
  82 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  83 +|------|------|------|--------|------|
  84 +| `tenant_id` | string | Y | - | 租户ID |
  85 +| `batch_size` | integer | N | 500 | 批量导入大小 |
  86 +
  87 +#### 响应格式
  88 +
  89 +**成功响应(200 OK)**(示例,实际 `index_name` 会带上 tenant 和环境前缀):
  90 +
  91 +```json
  92 +{
  93 + "success": true,
  94 + "total": 1000,
  95 + "indexed": 1000,
  96 + "failed": 0,
  97 + "elapsed_time": 12.34,
  98 + "index_name": "search_products_tenant_162",
  99 + "tenant_id": "162"
  100 +}
  101 +```
  102 +
  103 +**错误响应**:
  104 +- `400 Bad Request`: 参数错误
  105 +- `503 Service Unavailable`: 服务未初始化
  106 +
  107 +#### 请求示例
  108 +
  109 +**全量索引(不会删除现有索引)**:
  110 +
  111 +```bash
  112 +curl -X POST "http://localhost:6004/indexer/reindex" \
  113 + -H "Content-Type: application/json" \
  114 + -d '{
  115 + "tenant_id": "162",
  116 + "batch_size": 500
  117 + }'
  118 +```
  119 +
  120 +**查看日志**:
  121 +
  122 +```bash
  123 +# 查看API日志(包含索引操作日志)
  124 +tail -f logs/api.log
  125 +
  126 +# 或者查看所有日志文件
  127 +tail -f logs/*.log
  128 +```
  129 +
  130 +> ⚠️ **重要提示**:如需 **创建索引结构**,请参考 [5.1 为租户创建索引](#51-为租户创建索引) 章节,使用 `./scripts/create_tenant_index.sh <tenant_id>`。创建后需要调用 `/indexer/reindex` 导入数据。
  131 +
  132 +**查看索引日志**:
  133 +
  134 +索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON 格式),包括:
  135 +- 请求开始和结束时间
  136 +- 租户ID、SPU ID、操作类型
  137 +- 每个SPU的处理状态
  138 +- ES批量写入结果
  139 +- 成功/失败统计和详细错误信息
  140 +
  141 +```bash
  142 +# 实时查看索引日志(包含全量和增量索引的所有操作)
  143 +tail -f logs/indexer.log
  144 +
  145 +# 使用 grep 查询(简单方式)
  146 +# 查看全量索引日志
  147 +grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100
  148 +
  149 +# 查看增量索引日志
  150 +grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100
  151 +
  152 +# 查看特定租户的索引日志
  153 +grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100
  154 +
  155 +# 使用 jq 查询(推荐,更精确的 JSON 查询)
  156 +# 安装 jq: sudo apt-get install jq 或 brew install jq
  157 +
  158 +# 查看全量索引日志
  159 +cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100
  160 +
  161 +# 查看增量索引日志
  162 +cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100
  163 +
  164 +# 查看特定租户的索引日志
  165 +cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100
  166 +
  167 +# 查看失败的索引操作
  168 +cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)'
  169 +
  170 +# 查看特定SPU的处理日志
  171 +cat logs/indexer.log | jq 'select(.spu_id == "123")'
  172 +
  173 +# 查看最近的索引请求统计
  174 +cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}'
  175 +```
  176 +
  177 +### 5.3 增量索引接口
  178 +
  179 +- **端点**: `POST /indexer/index`
  180 +- **描述**: 增量索引接口,根据指定的SPU ID列表进行索引,直接将数据写入ES。用于增量更新指定商品。**推荐仅作为内部/调试入口**;正式对接建议改用 `/indexer/build-docs`,由上游写 ES。
  181 +
  182 +**删除说明**:
  183 +- `spu_ids`中的SPU:如果数据库`deleted=1`,自动从ES删除,响应状态为`deleted`
  184 +- `delete_spu_ids`中的SPU:直接删除,响应状态为`deleted`、`not_found`或`failed`
  185 +
  186 +#### 请求参数
  187 +
  188 +```json
  189 +{
  190 + "tenant_id": "162",
  191 + "spu_ids": ["123", "456", "789"],
  192 + "delete_spu_ids": ["100", "101"]
  193 +}
  194 +```
  195 +
  196 +| 参数 | 类型 | 必填 | 说明 |
  197 +|------|------|------|------|
  198 +| `tenant_id` | string | Y | 租户ID |
  199 +| `spu_ids` | array[string] | N | SPU ID列表(1-100个),要索引的SPU。如果为空,则只执行删除操作 |
  200 +| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表(1-100个),可选。无论数据库状态如何,都会从ES中删除这些SPU |
  201 +
  202 +**注意**:
  203 +- `spu_ids` 和 `delete_spu_ids` 不能同时为空
  204 +- 每个列表最多支持100个SPU ID
  205 +- 如果SPU在`spu_ids`中且数据库`deleted=1`,会自动从ES删除(自动检测删除)
  206 +
  207 +#### 响应格式
  208 +
  209 +```json
  210 +{
  211 + "spu_ids": [
  212 + {
  213 + "spu_id": "123",
  214 + "status": "indexed"
  215 + },
  216 + {
  217 + "spu_id": "456",
  218 + "status": "deleted"
  219 + },
  220 + {
  221 + "spu_id": "789",
  222 + "status": "failed",
  223 + "msg": "SPU not found (unexpected)"
  224 + }
  225 + ],
  226 + "delete_spu_ids": [
  227 + {
  228 + "spu_id": "100",
  229 + "status": "deleted"
  230 + },
  231 + {
  232 + "spu_id": "101",
  233 + "status": "not_found"
  234 + },
  235 + {
  236 + "spu_id": "102",
  237 + "status": "failed",
  238 + "msg": "Failed to delete from ES: Connection timeout"
  239 + }
  240 + ],
  241 + "total": 6,
  242 + "success_count": 4,
  243 + "failed_count": 2,
  244 + "elapsed_time": 1.23,
  245 + "index_name": "search_products",
  246 + "tenant_id": "162"
  247 +}
  248 +```
  249 +
  250 +| 字段 | 类型 | 说明 |
  251 +|------|------|------|
  252 +| `spu_ids` | array | spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` |
  253 +| `spu_ids[].status` | string | 状态:`indexed`(已索引)、`deleted`(已删除,自动检测)、`failed`(失败) |
  254 +| `spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) |
  255 +| `delete_spu_ids` | array | delete_spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` |
  256 +| `delete_spu_ids[].status` | string | 状态:`deleted`(已删除)、`not_found`(ES中不存在)、`failed`(失败) |
  257 +| `delete_spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) |
  258 +| `total` | integer | 总处理数量(spu_ids数量 + delete_spu_ids数量) |
  259 +| `success_count` | integer | 成功数量(indexed + deleted + not_found) |
  260 +| `failed_count` | integer | 失败数量 |
  261 +| `elapsed_time` | float | 耗时(秒) |
  262 +| `index_name` | string | 索引名称 |
  263 +| `tenant_id` | string | 租户ID |
  264 +
  265 +**状态说明**:
  266 +- `spu_ids` 的状态:
  267 + - `indexed`: SPU已成功索引到ES
  268 + - `deleted`: SPU在数据库中被标记为deleted=1,已从ES删除(自动检测)
  269 + - `failed`: 处理失败,会包含`msg`字段说明失败原因
  270 +- `delete_spu_ids` 的状态:
  271 + - `deleted`: SPU已从ES成功删除
  272 + - `not_found`: SPU在ES中不存在(也算成功,可能已经被删除过)
  273 + - `failed`: 删除失败,会包含`msg`字段说明失败原因
  274 +
  275 +#### 请求示例
  276 +
  277 +**示例1:普通增量索引(自动检测删除)**:
  278 +
  279 +```bash
  280 +curl -X POST "http://localhost:6004/indexer/index" \
  281 + -H "Content-Type: application/json" \
  282 + -d '{
  283 + "tenant_id": "162",
  284 + "spu_ids": ["123", "456", "789"]
  285 + }'
  286 +```
  287 +
  288 +说明:如果SPU 456在数据库中`deleted=1`,会自动从ES删除,在响应中`spu_ids`列表里456的状态为`deleted`。
  289 +
  290 +**示例2:显式删除(批量删除)**:
  291 +
  292 +```bash
  293 +curl -X POST "http://localhost:6004/indexer/index" \
  294 + -H "Content-Type: application/json" \
  295 + -d '{
  296 + "tenant_id": "162",
  297 + "spu_ids": ["123", "456"],
  298 + "delete_spu_ids": ["100", "101", "102"]
  299 + }'
  300 +```
  301 +
  302 +说明:SPU 100、101、102会被显式删除,无论数据库状态如何。
  303 +
  304 +**示例3:仅删除(不索引)**:
  305 +
  306 +```bash
  307 +curl -X POST "http://localhost:6004/indexer/index" \
  308 + -H "Content-Type: application/json" \
  309 + -d '{
  310 + "tenant_id": "162",
  311 + "spu_ids": [],
  312 + "delete_spu_ids": ["100", "101"]
  313 + }'
  314 +```
  315 +
  316 +说明:只执行删除操作,不进行索引。
  317 +
  318 +**示例4:混合操作(索引+删除)**:
  319 +
  320 +```bash
  321 +curl -X POST "http://localhost:6004/indexer/index" \
  322 + -H "Content-Type: application/json" \
  323 + -d '{
  324 + "tenant_id": "162",
  325 + "spu_ids": ["123", "456", "789"],
  326 + "delete_spu_ids": ["100", "101"]
  327 + }'
  328 +```
  329 +
  330 +说明:同时执行索引和删除操作。
  331 +
  332 +#### 日志说明
  333 +
  334 +增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON格式),包括:
  335 +- 请求开始和结束时间
  336 +- 每个SPU的处理状态(获取、转换、索引、删除)
  337 +- ES批量写入结果
  338 +- 成功/失败统计
  339 +- 详细的错误信息
  340 +
  341 +日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。
  342 +
  343 +### 5.4 查询文档接口
  344 +
  345 +- **端点**: `POST /indexer/documents`
  346 +- **描述**: 查询文档接口,根据SPU ID列表获取ES文档数据(**不写入ES**)。用于查看、调试或验证SPU数据。
  347 +
  348 +#### 请求参数
  349 +
  350 +```json
  351 +{
  352 + "tenant_id": "162",
  353 + "spu_ids": ["123", "456", "789"]
  354 +}
  355 +```
  356 +
  357 +| 参数 | 类型 | 必填 | 说明 |
  358 +|------|------|------|------|
  359 +| `tenant_id` | string | Y | 租户ID |
  360 +| `spu_ids` | array[string] | Y | SPU ID列表(1-100个) |
  361 +
  362 +#### 响应格式
  363 +
  364 +```json
  365 +{
  366 + "success": [
  367 + {
  368 + "spu_id": "123",
  369 + "document": {
  370 + "tenant_id": "162",
  371 + "spu_id": "123",
  372 + "title": {
  373 + "zh": "商品标题"
  374 + },
  375 + ...
  376 + }
  377 + },
  378 + {
  379 + "spu_id": "456",
  380 + "document": {...}
  381 + }
  382 + ],
  383 + "failed": [
  384 + {
  385 + "spu_id": "789",
  386 + "error": "SPU not found or deleted"
  387 + }
  388 + ],
  389 + "total": 3,
  390 + "success_count": 2,
  391 + "failed_count": 1
  392 +}
  393 +```
  394 +
  395 +| 字段 | 类型 | 说明 |
  396 +|------|------|------|
  397 +| `success` | array | 成功获取的SPU列表,每个元素包含 `spu_id` 和 `document`(完整的ES文档数据) |
  398 +| `failed` | array | 失败的SPU列表,每个元素包含 `spu_id` 和 `error`(失败原因) |
  399 +| `total` | integer | 总SPU数量 |
  400 +| `success_count` | integer | 成功数量 |
  401 +| `failed_count` | integer | 失败数量 |
  402 +
  403 +#### 请求示例
  404 +
  405 +**单个SPU查询**:
  406 +
  407 +```bash
  408 +curl -X POST "http://localhost:6004/indexer/documents" \
  409 + -H "Content-Type: application/json" \
  410 + -d '{
  411 + "tenant_id": "162",
  412 + "spu_ids": ["123"]
  413 + }'
  414 +```
  415 +
  416 +**批量SPU查询**:
  417 +
  418 +```bash
  419 +curl -X POST "http://localhost:6004/indexer/documents" \
  420 + -H "Content-Type: application/json" \
  421 + -d '{
  422 + "tenant_id": "162",
  423 + "spu_ids": ["123", "456", "789"]
  424 + }'
  425 +```
  426 +
  427 +#### 与 `/indexer/index` 的区别
  428 +
  429 +| 接口 | 功能 | 是否写入ES | 返回内容 |
  430 +|------|------|-----------|----------|
  431 +| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 |
  432 +| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 |
  433 +
  434 +**使用场景**:
  435 +- `/indexer/documents`:用于查看、调试或验证SPU数据,不修改ES索引
  436 +- `/indexer/index`:用于实际的增量索引操作,将更新的SPU数据同步到ES
  437 +
  438 +### 5.5 索引健康检查接口
  439 +
  440 +- **端点**: `GET /indexer/health`
  441 +- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致)
  442 +
  443 +#### 响应格式
  444 +
  445 +```json
  446 +{
  447 + "status": "available",
  448 + "database": "connected",
  449 + "preloaded_data": {
  450 + "category_mappings": 150
  451 + }
  452 +}
  453 +```
  454 +
  455 +| 字段 | 类型 | 说明 |
  456 +|------|------|------|
  457 +| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) |
  458 +| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` |
  459 +| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 |
  460 +
  461 +#### 请求示例
  462 +
  463 +```bash
  464 +curl -X GET "http://localhost:6004/indexer/health"
  465 +```
  466 +
  467 +### 5.6 文档构建接口(正式对接推荐)
  468 +
  469 +#### 5.6.1 `POST /indexer/build-docs`
  470 +
  471 +- **描述**:
  472 + 基于调用方(通常是 Java 索引程序)提供的 **MySQL 行数据** 构建 ES 文档(doc),**不写入 ES**。
  473 + 由本服务负责“如何构建 doc”(多语言、翻译、向量、规格聚合等),由调用方负责“何时调度 + 如何写 ES”。
  474 +
  475 +#### 请求参数
  476 +
  477 +```json
  478 +{
  479 + "tenant_id": "170",
  480 + "items": [
  481 + {
  482 + "spu": { "id": 223167, "tenant_id": 170, "title": "..." },
  483 + "skus": [
  484 + { "id": 3988393, "spu_id": 223167, "price": 25.99, "compare_at_price": 25.99 }
  485 + ],
  486 + "options": []
  487 + }
  488 + ]
  489 +}
  490 +```
  491 +
  492 +| 参数 | 类型 | 必填 | 说明 |
  493 +|------|------|------|------|
  494 +| `tenant_id` | string | Y | 租户 ID |
  495 +| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** |
  496 +
  497 +> `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。
  498 +
  499 +#### 请求示例(完整 curl)
  500 +
  501 +> 完整请求体参考 `scripts/test_build_docs_api.py` 中的 `build_sample_request()`。
  502 +
  503 +```bash
  504 +# 单条 SPU 示例(含 spu、skus、options)
  505 +curl -X POST "http://localhost:6004/indexer/build-docs" \
  506 + -H "Content-Type: application/json" \
  507 + -d '{
  508 + "tenant_id": "162",
  509 + "items": [
  510 + {
  511 + "spu": {
  512 + "id": 10001,
  513 + "tenant_id": "162",
  514 + "title": "测试T恤 纯棉短袖",
  515 + "brief": "舒适纯棉,多色可选",
  516 + "description": "这是一款适合日常穿着的纯棉T恤,透气吸汗。",
  517 + "vendor": "测试品牌",
  518 + "category": "服装/上衣/T恤",
  519 + "category_id": 100,
  520 + "category_level": 2,
  521 + "category_path": "服装/上衣/T恤",
  522 + "fake_sales": 1280,
  523 + "image_src": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg",
  524 + "tags": "T恤,纯棉,短袖,夏季",
  525 + "create_time": "2024-01-01T00:00:00Z",
  526 + "update_time": "2024-01-01T00:00:00Z"
  527 + },
  528 + "skus": [
  529 + {
  530 + "id": 20001,
  531 + "spu_id": 10001,
  532 + "price": 99.0,
  533 + "compare_at_price": 129.0,
  534 + "sku": "SKU-TSHIRT-001",
  535 + "inventory_quantity": 50,
  536 + "option1": "黑色",
  537 + "option2": "M",
  538 + "option3": null
  539 + },
  540 + {
  541 + "id": 20002,
  542 + "spu_id": 10001,
  543 + "price": 99.0,
  544 + "compare_at_price": 129.0,
  545 + "sku": "SKU-TSHIRT-002",
  546 + "inventory_quantity": 30,
  547 + "option1": "白色",
  548 + "option2": "L",
  549 + "option3": null
  550 + }
  551 + ],
  552 + "options": [
  553 + {"id": 1, "position": 1, "name": "颜色"},
  554 + {"id": 2, "position": 2, "name": "尺码"}
  555 + ]
  556 + }
  557 + ]
  558 +}'
  559 +```
  560 +
  561 +生产环境替换 `localhost:6004` 为实际 Indexer 地址,如 `http://43.166.252.75:6004`。
  562 +
  563 +#### 响应示例(节选)
  564 +
  565 +```json
  566 +{
  567 + "tenant_id": "170",
  568 + "docs": [
  569 + {
  570 + "tenant_id": "170",
  571 + "spu_id": "223167",
  572 + "title": { "en": "...", "zh": "..." },
  573 + "tags": ["Floerns", "Clothing", "Shoes & Jewelry"],
  574 + "skus": [
  575 + {
  576 + "sku_id": "3988393",
  577 + "price": 25.99,
  578 + "compare_at_price": 25.99,
  579 + "stock": 100
  580 + }
  581 + ],
  582 + "min_price": 25.99,
  583 + "max_price": 25.99,
  584 + "compare_at_price": 25.99,
  585 + "total_inventory": 100,
  586 + "title_embedding": [/* 1024 维向量 */]
  587 + // 其余字段与 mappings/search_products.json 一致
  588 + }
  589 + ],
  590 + "total": 1,
  591 + "success_count": 1,
  592 + "failed_count": 0,
  593 + "failed": []
  594 +}
  595 +```
  596 +
  597 +| 字段 | 类型 | 说明 |
  598 +|------|------|------|
  599 +| `tenant_id` | string | 租户 ID |
  600 +| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 |
  601 +| `total` | integer | 请求的 items 总数 |
  602 +| `success_count` | integer | 成功构建数量 |
  603 +| `failed_count` | integer | 失败数量 |
  604 +| `failed` | array | 失败项列表,每项含 `spu_id`、`error` |
  605 +
  606 +#### 使用建议
  607 +
  608 +- **生产环境推荐流程**:
  609 + 1. Java 根据业务逻辑决定哪些 SPU 需要(全量/增量)处理;
  610 + 2. Java 从 MySQL 查询 SPU/SKU/Option 行,拼成 `items`;
  611 + 3. 调用 `/indexer/build-docs` 获取 ES-ready `docs`;
  612 + 4. Java 使用自己的 ES 客户端写入 `search_products_tenant_{tenant_id}`。
  613 +
  614 +### 5.7 文档构建接口(测试 / 自测)
  615 +
  616 +#### 5.7.1 `POST /indexer/build-docs-from-db`
  617 +
  618 +- **描述**:
  619 + 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。**
  620 +
  621 +#### 请求参数
  622 +
  623 +```json
  624 +{
  625 + "tenant_id": "170",
  626 + "spu_ids": ["223167", "223168"]
  627 +}
  628 +```
  629 +
  630 +| 参数 | 类型 | 必填 | 说明 |
  631 +|------|------|------|------|
  632 +| `tenant_id` | string | Y | 租户 ID |
  633 +| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** |
  634 +
  635 +#### 响应格式
  636 +
  637 +与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。
  638 +
  639 +#### 请求示例
  640 +
  641 +```bash
  642 +curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \
  643 + -H "Content-Type: application/json" \
  644 + -d '{"tenant_id": "170", "spu_ids": ["223167"]}'
  645 +```
  646 +
  647 +返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。
  648 +
  649 +### 5.8 内容理解字段生成接口
  650 +
  651 +- **端点**: `POST /indexer/enrich-content`
  652 +- **描述**: 根据商品内容信息批量生成 **qanchors**(锚文本)、**semantic_attributes**(语义属性)、**tags**(细分标签),供外部 indexer 在「微服务组合」方式下自行拼装 doc 时使用。请求以 `items[]` 传入商品内容字段(必填/可选见下表)。内部逻辑与 `indexer.product_enrich` 一致,支持多语言与 Redis 缓存;单次请求在线程池中执行,避免阻塞其他接口。
  653 +
  654 +#### 请求参数
  655 +
  656 +```json
  657 +{
  658 + "tenant_id": "170",
  659 + "items": [
  660 + {
  661 + "spu_id": "223167",
  662 + "title": "纯棉短袖T恤 夏季男装",
  663 + "brief": "夏季透气纯棉短袖,舒适亲肤",
  664 + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。",
  665 + "image_url": "https://example.com/images/223167.jpg"
  666 + },
  667 + {
  668 + "spu_id": "223168",
  669 + "title": "12PCS Dolls with Bottles",
  670 + "image_url": "https://example.com/images/223168.jpg"
  671 + }
  672 + ],
  673 + "languages": ["zh", "en"]
  674 +}
  675 +```
  676 +
  677 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  678 +|------|------|------|--------|------|
  679 +| `tenant_id` | string | Y | - | 租户 ID。目前仅用于记录日志,不产生实际作用|
  680 +| `items` | array | Y | - | 待分析列表;**单次最多 50 条** |
  681 +| `languages` | array[string] | N | `["zh", "en"]` | 目标语言,需在支持范围内:`zh`、`en`、`de`、`ru`、`fr` |
  682 +
  683 +`items[]` 字段说明:
  684 +
  685 +| 字段 | 类型 | 必填 | 说明 |
  686 +|------|------|------|------|
  687 +| `spu_id` | string | Y | SPU ID,用于回填结果;目前仅用于记录日志,不产生实际作用|
  688 +| `title` | string | Y | 商品标题 |
  689 +| `image_url` | string | N | 商品主图 URL;当前会参与内容缓存键,后续可用于图像/多模态内容理解 |
  690 +| `brief` | string | N | 商品简介/短描述;当前会参与内容缓存键 |
  691 +| `description` | string | N | 商品详情/长描述;当前会参与内容缓存键 |
  692 +
  693 +缓存说明:
  694 +
  695 +- 内容缓存键仅由 `target_lang + items[]` 中会影响内容理解结果的输入文本构成,目前包括:`title`、`brief`、`description`、`image_url` 的规范化内容 hash。
  696 +- `tenant_id`、`spu_id` 只用于请求归属与结果回填,不参与缓存键。
  697 +- 因此,输入内容不变时可跨请求直接命中缓存;任一输入字段变化时,会自然落到新的缓存 key。
  698 +
  699 +批量请求建议:
  700 +- **全量**:强烈建议 尽可能 **20 个 SPU/doc** 攒成一个批次后再请求一次。
  701 +- **增量**:可按时效要求设置时间窗口(例如 **5 分钟**),在窗口内尽可能攒到 **20 个**;达到 20 或窗口到期就发送一次请求。
  702 +- 允许超过20,服务内部会拆分成小批次逐个处理。也允许小于20,但是将造成费用和耗时的成本上升,特别是每次请求一个doc的情况。
  703 +
  704 +#### 响应格式
  705 +
  706 +```json
  707 +{
  708 + "tenant_id": "170",
  709 + "total": 2,
  710 + "results": [
  711 + {
  712 + "spu_id": "223167",
  713 + "qanchors": {
  714 + "zh": "短袖T恤,纯棉,男装,夏季",
  715 + "en": "cotton t-shirt, short sleeve, men, summer"
  716 + },
  717 + "semantic_attributes": [
  718 + { "lang": "zh", "name": "tags", "value": "纯棉" },
  719 + { "lang": "zh", "name": "usage_scene", "value": "日常" },
  720 + { "lang": "en", "name": "tags", "value": "cotton" }
  721 + ],
  722 + "tags": ["纯棉", "短袖", "男装", "cotton", "short sleeve"]
  723 + },
  724 + {
  725 + "spu_id": "223168",
  726 + "qanchors": { "en": "dolls, toys, 12pcs" },
  727 + "semantic_attributes": [],
  728 + "tags": ["dolls", "toys"]
  729 + }
  730 + ]
  731 +}
  732 +```
  733 +
  734 +| 字段 | 类型 | 说明 |
  735 +|------|------|------|
  736 +| `results` | array | 与请求 `items` 一一对应,每项含 `spu_id`、`qanchors`、`semantic_attributes`、`tags` |
  737 +| `results[].qanchors` | object | 按语言键的锚文本(逗号分隔短语),可写入 ES 文档的 `qanchors.{lang}` |
  738 +| `results[].semantic_attributes` | array | 语义属性列表,每项为 `{ "lang", "name", "value" }`,可写入 ES 的 `semantic_attributes` nested 字段 |
  739 +| `results[].tags` | array | 从语义属性中抽取的 `name=tags` 的 value 集合,可与业务原有 `tags` 合并后写入 ES 的 `tags` 字段 |
  740 +| `results[].error` | string | 若该条处理失败(如 LLM 异常),会在此字段返回错误信息 |
  741 +
  742 +**错误响应**:
  743 +- `400`: `items` 为空或超过 50 条
  744 +- `503`: 未配置 `DASHSCOPE_API_KEY`,内容理解服务不可用
  745 +
  746 +#### 请求示例
  747 +
  748 +```bash
  749 +curl -X POST "http://localhost:6004/indexer/enrich-content" \
  750 + -H "Content-Type: application/json" \
  751 + -d '{
  752 + "tenant_id": "170",
  753 + "items": [
  754 + {
  755 + "spu_id": "223167",
  756 + "title": "纯棉短袖T恤 夏季男装",
  757 + "brief": "夏季透气纯棉短袖,舒适亲肤",
  758 + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。",
  759 + "image_url": "https://example.com/images/223167.jpg"
  760 + }
  761 + ],
  762 + "languages": ["zh", "en"]
  763 + }'
  764 +```
  765 +
  766 +---
  767 +
... ...
docs/搜索API对接指南-06-管理接口(Admin).md 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +# 搜索API对接指南-06-管理接口(Admin)
  2 +
  3 +用于查看服务健康状态、获取租户配置与索引统计信息(原文第 6 章)。
  4 +
  5 +## 管理接口
  6 +
  7 +### 6.1 健康检查
  8 +
  9 +- **端点**: `GET /admin/health`
  10 +- **描述**: 检查服务与依赖(如 Elasticsearch)状态。
  11 +
  12 +```json
  13 +{
  14 + "status": "healthy",
  15 + "elasticsearch": "connected",
  16 + "tenant_id": "tenant1"
  17 +}
  18 +```
  19 +
  20 +### 6.2 获取配置
  21 +
  22 +- **端点**: `GET /admin/config`
  23 +- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。
  24 +
  25 +```json
  26 +{
  27 + "tenant_id": "tenant1",
  28 + "tenant_name": "Tenant1 Test Instance",
  29 + "es_index_name": "search_tenant1",
  30 + "num_fields": 20,
  31 + "num_indexes": 4,
  32 + "supported_languages": ["zh", "en", "ru"],
  33 + "spu_enabled": false
  34 +}
  35 +```
  36 +
  37 +### 6.3 索引统计
  38 +
  39 +- **端点**: `GET /admin/stats`
  40 +- **描述**: 获取指定租户索引文档数量与磁盘大小,方便监控。
  41 +- **租户标识**:通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递(必填)。
  42 +
  43 +```json
  44 +{
  45 + "tenant_id": "162",
  46 + "index_name": "search_products_tenant_162",
  47 + "document_count": 10000,
  48 + "size_mb": 523.45
  49 +}
  50 +```
  51 +
  52 +---
  53 +
... ...
docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md 0 → 100644
... ... @@ -0,0 +1,401 @@
  1 +# 搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation)
  2 +
  3 +本篇覆盖向量服务(Embedding)、重排服务(Reranker)、翻译服务(Translation)以及 Indexer 服务内的内容理解字段生成(原文第 7 章)。
  4 +
  5 +## 7. 微服务接口(向量、重排、翻译)
  6 +
  7 +以下三个微服务独立部署,**外部系统可直接调用**。它们被搜索后端(6002)和索引服务(6004)内部使用,也可供其他业务系统直接对接。
  8 +
  9 +| 服务 | 默认端口 | Base URL | 说明 |
  10 +|------|----------|----------|------|
  11 +| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 |
  12 +| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 |
  13 +| 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) |
  14 +| 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 |
  15 +
  16 +生产环境请将 `localhost` 替换为实际服务地址。
  17 +服务管理入口与完整启停规则见:`docs/Usage-Guide.md` -> `服务管理总览`。
  18 +
  19 +### 7.1 向量服务(Embedding)
  20 +
  21 +- **Base URL**:
  22 + - 文本:`http://localhost:6005`(可通过 `EMBEDDING_TEXT_SERVICE_URL` 覆盖)
  23 + - 图片:`http://localhost:6008`(可通过 `EMBEDDING_IMAGE_SERVICE_URL` 覆盖)
  24 +- **启动**:
  25 + - 文本:`./scripts/start_embedding_text_service.sh`
  26 + - 图片:`./scripts/start_embedding_image_service.sh`
  27 +- **依赖**:
  28 + - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`)
  29 + - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`)
  30 + - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU)
  31 + - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`)
  32 + - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力
  33 +
  34 +补充说明:
  35 +
  36 +- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。
  37 +- 服务端对 text / image 有**独立 admission control**:
  38 + - `TEXT_MAX_INFLIGHT`
  39 + - `IMAGE_MAX_INFLIGHT`
  40 +- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。
  41 +- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。
  42 +
  43 +#### 7.1.1 `POST /embed/text` — 文本向量化
  44 +
  45 +将文本列表转为 1024 维向量,用于语义搜索、文档索引等。
  46 +
  47 +**请求体**(JSON 数组):
  48 +
  49 +```json
  50 +["文本1", "文本2", "文本3"]
  51 +```
  52 +
  53 +**响应**(JSON 数组,与输入一一对应):
  54 +
  55 +```json
  56 +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...]
  57 +```
  58 +
  59 +**完整 curl 示例**:
  60 +
  61 +```bash
  62 +curl -X POST "http://localhost:6005/embed/text?normalize=true" \
  63 + -H "Content-Type: application/json" \
  64 + -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]'
  65 +```
  66 +
  67 +#### 7.1.2 `POST /embed/image` — 图片向量化
  68 +
  69 +将图片 URL 或路径转为向量,用于以图搜图。
  70 +
  71 +前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。
  72 +
  73 +**请求体**(JSON 数组):
  74 +
  75 +```json
  76 +["https://example.com/image1.jpg", "https://example.com/image2.jpg"]
  77 +```
  78 +
  79 +**响应**(JSON 数组,与输入一一对应):
  80 +
  81 +```json
  82 +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...]
  83 +```
  84 +
  85 +**完整 curl 示例**:
  86 +
  87 +```bash
  88 +curl -X POST "http://localhost:6008/embed/image?normalize=true" \
  89 + -H "Content-Type: application/json" \
  90 + -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]'
  91 +```
  92 +
  93 +#### 7.1.3 `GET /health` — 健康检查
  94 +
  95 +```bash
  96 +curl "http://localhost:6005/health"
  97 +curl "http://localhost:6008/health"
  98 +```
  99 +
  100 +返回中会包含:
  101 +
  102 +- `service_kind`:`text` / `image` / `all`
  103 +- `cache_enabled`:text/image Redis 缓存是否可用
  104 +- `limits`:当前 inflight limit、active、rejected_total 等
  105 +- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等
  106 +
  107 +#### 7.1.4 `GET /ready` — 就绪检查
  108 +
  109 +```bash
  110 +curl "http://localhost:6005/ready"
  111 +curl "http://localhost:6008/ready"
  112 +```
  113 +
  114 +#### 7.1.5 缓存与限流说明
  115 +
  116 +- 文本与图片都会先查 Redis 向量缓存。
  117 +- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。
  118 +- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。
  119 +- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。
  120 +- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。
  121 +
  122 +#### 7.1.6 TEI 统一调优建议(主服务)
  123 +
  124 +使用单套主服务即可同时兼顾:
  125 +- 在线 query 向量化(低延迟,常见 `batch=1~4`)
  126 +- 索引构建向量化(高吞吐,常见 `batch=15~20`)
  127 +
  128 +统一启动(主链路):
  129 +
  130 +```bash
  131 +./scripts/start_tei_service.sh
  132 +./scripts/service_ctl.sh restart embedding
  133 +```
  134 +
  135 +默认端口:
  136 +- TEI: `http://127.0.0.1:8080`
  137 +- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005`
  138 +- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008`
  139 +
  140 +当前主 TEI 启动默认值(已按 T4/短文本场景调优):
  141 +- `TEI_MAX_BATCH_TOKENS=4096`
  142 +- `TEI_MAX_CLIENT_BATCH_SIZE=24`
  143 +- `TEI_DTYPE=float16`
  144 +
  145 +### 7.2 重排服务(Reranker)
  146 +
  147 +- **Base URL**: `http://localhost:6007`(可通过 `RERANKER_SERVICE_URL` 覆盖)
  148 +- **启动**: `./scripts/start_reranker.sh`
  149 +
  150 +说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。
  151 +
  152 +补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确)。
  153 +
  154 +#### 7.2.1 `POST /rerank` — 结果重排
  155 +
  156 +根据 query 与 doc 的相关性对文档列表重新打分排序。
  157 +
  158 +**请求体**:
  159 +```json
  160 +{
  161 + "query": "玩具 芭比",
  162 + "docs": [
  163 + "12PCS 6 Types of Dolls with Bottles",
  164 + "纯棉T恤 短袖 夏季"
  165 + ],
  166 + "normalize": true
  167 +}
  168 +```
  169 +
  170 +| 参数 | 类型 | 必填 | 说明 |
  171 +|------|------|------|------|
  172 +| `query` | string | Y | 搜索查询 |
  173 +| `docs` | array[string] | Y | 待重排的文档列表(单次最多由服务端配置限制) |
  174 +| `normalize` | boolean | N | 是否对分数做 sigmoid 归一化,默认 true |
  175 +
  176 +**响应**:
  177 +```json
  178 +{
  179 + "scores": [0.92, 0.15],
  180 + "meta": {
  181 + "service_elapsed_ms": 45.2,
  182 + "input_docs": 2,
  183 + "unique_docs": 2
  184 + }
  185 +}
  186 +```
  187 +
  188 +**完整 curl 示例**:
  189 +```bash
  190 +curl -X POST "http://localhost:6007/rerank" \
  191 + -H "Content-Type: application/json" \
  192 + -d '{
  193 + "query": "玩具 芭比",
  194 + "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"],
  195 + "top_n":386,
  196 + "normalize": true
  197 + }'
  198 +```
  199 +
  200 +#### 7.2.2 `GET /health` — 健康检查
  201 +
  202 +```bash
  203 +curl "http://localhost:6007/health"
  204 +```
  205 +
  206 +### 7.3 翻译服务(Translation)
  207 +
  208 +- **Base URL**: `http://localhost:6006`(以 `config/config.yaml -> services.translation.service_url` 为准)
  209 +- **启动**: `./scripts/start_translator.sh`
  210 +
  211 +#### 7.3.1 `POST /translate` — 文本翻译
  212 +
  213 +支持 translator service 内所有已启用 capability,适用于商品名称、描述、query 等电商场景。当前可配置能力包括 `qwen-mt`、`llm`、`deepl` 以及本地模型 `nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh`。
  214 +
  215 +**请求体**(支持单条字符串或字符串列表):
  216 +```json
  217 +{
  218 + "text": "商品名称",
  219 + "target_lang": "en",
  220 + "source_lang": "zh",
  221 + "model": "qwen-mt",
  222 + "scene": "sku_name"
  223 +}
  224 +```
  225 +
  226 +也支持批量列表形式:
  227 +```json
  228 +{
  229 + "text": ["商品名称1", "商品名称2"],
  230 + "target_lang": "en",
  231 + "source_lang": "zh",
  232 + "model": "qwen-mt",
  233 + "scene": "sku_name"
  234 +}
  235 +```
  236 +
  237 +| 参数 | 类型 | 必填 | 说明 |
  238 +|------|------|------|------|
  239 +| `text` | string \| string[] | Y | 待翻译文本,既支持单条字符串,也支持字符串列表(批量翻译) |
  240 +| `target_lang` | string | Y | 目标语言:`zh`、`en`、`ru` 等 |
  241 +| `source_lang` | string | N | 源语言。云端模型可不传;`nllb-200-distilled-600m` 建议显式传入 |
  242 +| `model` | string | N | 已启用 capability 名称,如 `qwen-mt`、`llm`、`deepl`、`nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh` |
  243 +| `scene` | string | N | 翻译场景参数,与 `model` 配套使用;当前标准值为 `sku_name`、`ecommerce_search_query`、`general` |
  244 +
  245 +说明:
  246 +- 外部接口不接受 `prompt`;LLM prompt 由服务端按 `scene` 自动生成。
  247 +- 传入未定义的 `scene` 或未启用的 `model` 会返回 `400`。
  248 +
  249 +**SKU 名称场景选型建议**:
  250 +- 批量 SKU 名称翻译,优先考虑本地大吞吐方案时,可使用 `"model": "nllb-200-distilled-600m"`(该模型"scene":参数无效)。
  251 +- 如果目标是更高质量,且可以接受更慢速度与额外 LLM API 费用,可使用 `"model": "llm"` + `"scene": "sku_name"`。
  252 +- 如果是en-zh互译、期待更高的速度,可以考虑`opus-mt-zh-en` / `opus-mt-en-zh`。(质量未详细评测,一些文章说比blib-200-600m更好,但是我看了些case感觉要差不少)
  253 +
  254 +**实时翻译选型建议**:
  255 +- 在线 query 翻译如果只是 `en/zh` 互译,优先使用 `opus-mt-zh-en` 或 `opus-mt-en-zh`,它们是当前已测本地模型里延迟最低的一档。
  256 +- 如果涉及其他语言,或对质量要求高于本地轻量模型,优先考虑 `deepl`。
  257 +- `nllb-200-distilled-600m` 不建议作为在线 query 翻译默认方案;我们在 `Tesla T4` 上测到 `batch_size=1` 时,`zh -> en` p50 约 `292.54 ms`、p95 约 `624.12 ms`,`en -> zh` p50 约 `481.61 ms`、p95 约 `1171.71 ms`。
  258 +
  259 +**Batch Size / 调用方式建议**:
  260 +- 本接口支持 `text: string[]`;离线或批量索引翻译时,应尽量合并请求,让底层 backend 发挥批处理能力。
  261 +- `nllb-200-distilled-600m` 在当前 `Tesla T4` 压测中,推荐配置是 `batch_size=16`、`max_new_tokens=64`、`attn_implementation=sdpa`;继续升到 `batch_size=32` 虽可能提高吞吐,但 tail latency 会明显变差。
  262 +- 在线 query 场景可直接把“单条请求”理解为 `batch_size=1`;更关注 request latency,而不是离线吞吐。
  263 +- `opus-mt-zh-en` / `opus-mt-en-zh` 当前生产配置也是 `batch_size=16`,适合作为中英互译的低延迟本地默认值;若走在线单条调用,同样按 `batch_size=1` 理解即可。
  264 +- `llm` 按单条请求即可。
  265 +
  266 +**响应**:
  267 +```json
  268 +{
  269 + "text": "商品名称",
  270 + "target_lang": "en",
  271 + "source_lang": "zh",
  272 + "translated_text": "Product name",
  273 + "status": "success",
  274 + "model": "qwen-mt",
  275 + "scene": "sku_name"
  276 +}
  277 +```
  278 +
  279 +当请求为列表形式时,`text` 与 `translated_text` 均为等长数组:
  280 +```json
  281 +{
  282 + "text": ["商品名称1", "商品名称2"],
  283 + "target_lang": "en",
  284 + "source_lang": "zh",
  285 + "translated_text": ["Product name 1", "Product name 2"],
  286 + "status": "success",
  287 + "model": "qwen-mt",
  288 + "scene": "sku_name"
  289 +}
  290 +```
  291 +
  292 +> **失败语义(批量)**:当 `text` 为列表时,如果其中某条翻译失败,对应位置返回 `null`(即 `translated_text[i] = null`),并保持数组长度与顺序不变;接口整体仍返回 `status="success"`,用于避免“部分失败”导致整批请求失败。
  293 +
  294 +> **实现提示(可忽略)**:服务端会尽可能使用底层 backend 的批量能力(若支持),否则自动拆分逐条翻译;无论采用哪种方式,上述批量契约保持一致。
  295 +
  296 +**完整 curl 示例**:
  297 +
  298 +中文 → 英文:
  299 +```bash
  300 +curl -X POST "http://localhost:6006/translate" \
  301 + -H "Content-Type: application/json" \
  302 + -d '{
  303 + "text": "商品名称",
  304 + "target_lang": "en",
  305 + "source_lang": "zh"
  306 + }'
  307 +```
  308 +
  309 +俄文 → 英文:
  310 +```bash
  311 +curl -X POST "http://localhost:6006/translate" \
  312 + -H "Content-Type: application/json" \
  313 + -d '{
  314 + "text": "Название товара",
  315 + "target_lang": "en",
  316 + "source_lang": "ru"
  317 + }'
  318 +```
  319 +
  320 +使用 DeepL 模型:
  321 +```bash
  322 +curl -X POST "http://localhost:6006/translate" \
  323 + -H "Content-Type: application/json" \
  324 + -d '{
  325 + "text": "商品名称",
  326 + "target_lang": "en",
  327 + "source_lang": "zh",
  328 + "model": "deepl"
  329 + }'
  330 +```
  331 +
  332 +使用本地 OPUS 模型(中文 → 英文):
  333 +```bash
  334 +curl -X POST "http://localhost:6006/translate" \
  335 + -H "Content-Type: application/json" \
  336 + -d '{
  337 + "text": "蓝牙耳机",
  338 + "target_lang": "en",
  339 + "source_lang": "zh",
  340 + "model": "opus-mt-zh-en",
  341 + "scene": "sku_name"
  342 + }'
  343 +```
  344 +
  345 +使用本地 NLLB 做 SKU 名称批量翻译:
  346 +```bash
  347 +curl -X POST "http://localhost:6006/translate" \
  348 + -H "Content-Type: application/json" \
  349 + -d '{
  350 + "text": ["商品名称1", "商品名称2", "商品名称3"],
  351 + "target_lang": "en",
  352 + "source_lang": "zh",
  353 + "model": "nllb-200-distilled-600m",
  354 + "scene": "sku_name"
  355 + }'
  356 +```
  357 +
  358 +使用 LLM 做高质量 SKU 名称翻译:
  359 +```bash
  360 +curl -X POST "http://localhost:6006/translate" \
  361 + -H "Content-Type: application/json" \
  362 + -d '{
  363 + "text": "男士偏光飞行员太阳镜",
  364 + "target_lang": "en",
  365 + "source_lang": "zh",
  366 + "model": "llm",
  367 + "scene": "sku_name"
  368 + }'
  369 +```
  370 +
  371 +#### 7.3.2 `GET /health` — 健康检查
  372 +
  373 +```bash
  374 +curl "http://localhost:6006/health"
  375 +```
  376 +
  377 +典型响应:
  378 +```json
  379 +{
  380 + "status": "healthy",
  381 + "service": "translation",
  382 + "default_model": "llm",
  383 + "default_scene": "general",
  384 + "available_models": ["qwen-mt", "llm", "opus-mt-zh-en"],
  385 + "enabled_capabilities": ["qwen-mt", "llm", "opus-mt-zh-en"],
  386 + "loaded_models": ["llm"]
  387 +}
  388 +```
  389 +
  390 +### 7.4 内容理解字段生成(Indexer 服务内)
  391 +
  392 +内容理解字段生成接口部署在 **Indexer 服务**(默认端口 6004)内,与「翻译、向量化」等独立端口微服务并列,供采用**微服务组合**方式的 indexer 调用。
  393 +
  394 +- **Base URL**: Indexer 服务地址,如 `http://localhost:6004`
  395 +- **路径**: `POST /indexer/enrich-content`
  396 +- **说明**: 根据商品标题批量生成 `qanchors`、`semantic_attributes`、`tags`,用于拼装 ES 文档。内部使用大模型(需配置 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存;单次最多 50 条,建议批量调用以提升效率。
  397 +
  398 +请求/响应格式、示例及错误码见 [-05-索引接口(Indexer)](./搜索API对接指南-05-索引接口(Indexer).md#58-内容理解字段生成接口)。
  399 +
  400 +---
  401 +
... ...
docs/搜索API对接指南-08-数据模型与字段速查.md 0 → 100644
... ... @@ -0,0 +1,97 @@
  1 +# 搜索API对接指南-08-数据模型与字段速查
  2 +
  3 +本篇覆盖原文第 9 章:商品字段定义、字段类型速查、常用字段列表、支持的分析器。
  4 +
  5 +## 9. 数据模型
  6 +
  7 +### 9.1 商品字段定义
  8 +
  9 +| 字段名 | 类型 | 描述 |
  10 +|--------|------|------|
  11 +| `tenant_id` | keyword | 租户ID(多租户隔离) |
  12 +| `spu_id` | keyword | SPU ID |
  13 +| `title.<lang>` | object/text | 商品标题(多语言对象,如 `title.zh`, `title.en`) |
  14 +| `brief.<lang>` | object/text | 商品短描述(多语言对象,如 `brief.zh`, `brief.en`) |
  15 +| `description.<lang>` | object/text | 商品详细描述(多语言对象,如 `description.zh`, `description.en`) |
  16 +| `vendor.<lang>` | object/text | 供应商/品牌(多语言对象,且带 keyword 子字段,如 `vendor.zh.keyword`) |
  17 +| `category_path.<lang>` | object/text | 类目路径(多语言对象,用于搜索,如 `category_path.zh`) |
  18 +| `category_name_text.<lang>` | object/text | 类目名称(多语言对象,用于搜索,如 `category_name_text.zh`) |
  19 +| `category_id` | keyword | 类目ID |
  20 +| `category_name` | keyword | 类目名称(用于过滤) |
  21 +| `category_level` | integer | 类目层级 |
  22 +| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) |
  23 +| `tags` | keyword | 标签(数组) |
  24 +| `specifications` | nested | 规格(嵌套对象数组) |
  25 +| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 |
  26 +| `min_price`, `max_price` | float | 最低/最高价格 |
  27 +| `compare_at_price` | float | 原价 |
  28 +| `sku_prices` | float | SKU价格列表(数组) |
  29 +| `sku_weights` | long | SKU重量列表(数组) |
  30 +| `sku_weight_units` | keyword | SKU重量单位列表(数组) |
  31 +| `total_inventory` | long | 总库存 |
  32 +| `sales` | long | 销量(展示销量) |
  33 +| `skus` | nested | SKU详细信息(嵌套对象数组) |
  34 +| `create_time`, `update_time` | date | 创建/更新时间 |
  35 +| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) |
  36 +| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) |
  37 +
  38 +> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。
  39 +
  40 +### 9.2 字段类型速查
  41 +
  42 +| 类型 | ES Mapping | 用途 |
  43 +|------|------------|------|
  44 +| `text` | `text` | 全文检索(支持中英文分析器) |
  45 +| `keyword` | `keyword` | 精确匹配、聚合、排序 |
  46 +| `integer` | `integer` | 整数 |
  47 +| `long` | `long` | 长整数 |
  48 +| `float` | `float` | 浮点数 |
  49 +| `date` | `date` | 日期时间 |
  50 +| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) |
  51 +| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) |
  52 +
  53 +### 9.3 常用字段列表
  54 +
  55 +#### 过滤字段
  56 +
  57 +- `category_name`: 类目名称
  58 +- `category1_name`, `category2_name`, `category3_name`: 多级类目
  59 +- `category_id`: 类目ID
  60 +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段)
  61 +- `tags`: 标签(keyword类型)
  62 +- `option1_name`, `option2_name`, `option3_name`: 选项名称
  63 +- `specifications`: 规格过滤(嵌套字段,格式见[过滤器详解](./搜索API对接指南-01-搜索接口.md#33-过滤器详解))
  64 +
  65 +#### 范围字段
  66 +
  67 +- `min_price`: 最低价格
  68 +- `max_price`: 最高价格
  69 +- `compare_at_price`: 原价
  70 +- `create_time`: 创建时间
  71 +- `update_time`: 更新时间
  72 +
  73 +#### 排序字段
  74 +
  75 +- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price)
  76 +- `sales`: 销量
  77 +- `create_time`: 创建时间
  78 +- `update_time`: 更新时间
  79 +- `relevance_score`: 相关性分数(默认,不指定sort_by时使用)
  80 +
  81 +**注意**: 前端只需传 `price`,后端会自动处理:
  82 +- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高)
  83 +- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低)
  84 +
  85 +### 9.4 支持的分析器
  86 +
  87 +| 分析器 | 语言 | 描述 |
  88 +|--------|------|------|
  89 +| `index_ik` | 中文 | 中文索引分析器(用于中文字段) |
  90 +| `query_ik` | 中文 | 中文查询分析器(用于中文字段) |
  91 +| `hanlp_index` ⚠️ TODO(暂不支持) | 中文 | 中文索引分析器(用于中文字段) |
  92 +| `hanlp_standard` ⚠️ TODO(暂不支持) | 中文 | 中文查询分析器(用于中文字段) |
  93 +| `english` | 英文 | 标准英文分析器(用于英文字段) |
  94 +| `lowercase` | - | 小写标准化器(用于keyword子字段) |
  95 +
  96 +---
  97 +
... ...
docs/搜索API对接指南-10-接口级压测脚本.md 0 → 100644
... ... @@ -0,0 +1,61 @@
  1 +# 搜索API对接指南-10-接口级压测脚本
  2 +
  3 +原文第 10 章:压测脚本与用例。
  4 +
  5 +## 10. 接口级压测脚本
  6 +
  7 +仓库提供统一压测脚本:`scripts/perf_api_benchmark.py`,用于对以下接口做并发压测:
  8 +
  9 +- 后端搜索:`POST /search/`
  10 +- 搜索建议:`GET /search/suggestions`
  11 +- 向量服务:`POST /embed/text`
  12 +- 翻译服务:`POST /translate`
  13 +- 重排服务:`POST /rerank`
  14 +
  15 +说明:脚本对 `embed_text` 场景会校验返回向量内容有效性(必须是有限数值,不允许 `null/NaN/Inf`),不是只看 HTTP 200。
  16 +
  17 +### 10.1 快速示例
  18 +
  19 +```bash
  20 +# suggest 压测(tenant 162)
  21 +python scripts/perf_api_benchmark.py \
  22 + --scenario backend_suggest \
  23 + --tenant-id 162 \
  24 + --duration 30 \
  25 + --concurrency 50
  26 +
  27 +# search 压测
  28 +python scripts/perf_api_benchmark.py \
  29 + --scenario backend_search \
  30 + --tenant-id 162 \
  31 + --duration 30 \
  32 + --concurrency 20
  33 +
  34 +# 全链路压测(search + suggest + embedding + translate + rerank)
  35 +python scripts/perf_api_benchmark.py \
  36 + --scenario all \
  37 + --tenant-id 162 \
  38 + --duration 60 \
  39 + --concurrency 30 \
  40 + --output perf_reports/all.json
  41 +```
  42 +
  43 +### 10.2 自定义用例
  44 +
  45 +可通过 `--cases-file` 覆盖默认请求模板。示例文件:
  46 +
  47 +```bash
  48 +scripts/perf_cases.json.example
  49 +```
  50 +
  51 +执行示例:
  52 +
  53 +```bash
  54 +python scripts/perf_api_benchmark.py \
  55 + --scenario all \
  56 + --tenant-id 162 \
  57 + --cases-file scripts/perf_cases.json.example \
  58 + --duration 60 \
  59 + --concurrency 40
  60 +```
  61 +
... ...
docs/搜索API对接指南.md renamed to docs/搜索API对接指南—拆分前版本存档.md