diff --git a/docs/搜索API对接指南-00-总览与快速开始.md b/docs/搜索API对接指南-00-总览与快速开始.md new file mode 100644 index 0000000..b573ca1 --- /dev/null +++ b/docs/搜索API对接指南-00-总览与快速开始.md @@ -0,0 +1,110 @@ +# 搜索API对接指南-00-总览与快速开始 + +本文档旨在为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 +拆分目录: +- `-01-搜索接口(POST /search/ 与响应)` +- `-02-搜索建议与即时搜索` +- `-03-获取文档(GET /search/{doc_id})` +- `-05-索引接口(Indexer)` +- `-06-管理接口(Admin)` +- `-07-微服务接口(Embedding/Reranker/Translation)` +- `-08-数据模型与字段速查` +- `-10-接口级压测脚本` + +## 快速开始 + +### 1.1 基础信息 + +- **Base URL**: `http://43.166.252.75:6002` +- **协议**: HTTP/HTTPS +- **数据格式**: JSON +- **字符编码**: UTF-8 +- **请求方法**: POST(搜索接口) + +**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。 + +**环境与凭证**:MySQL、Redis、Elasticsearch 等外部服务的 AI 生产地址与凭证见 [QUICKSTART.md §1.6](./QUICKSTART.md#16-外部服务与-env含生产凭证)。 + +### 1.2 最简单的搜索请求 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{"query": "芭比娃娃"}' +``` + +### 1.3 带过滤与分页的搜索 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{ + "query": "芭比娃娃", + "size": 5, + "from": 10, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + }, + "create_time": { + "gte": "2020-01-01T00:00:00Z" + } + }, + "sort_by": "price", + "sort_order": "asc" + }' +``` + +### 1.4 开启分面的搜索 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{ + "query": "芭比娃娃", + "facets": [ + {"field": "category1_name", "size": 10, "type": "terms"}, + {"field": "specifications.color", "size": 10, "type": "terms"}, + {"field": "specifications.size", "size": 10, "type": "terms"} + ], + "min_score": 0.2 + }' +``` + +--- + +## 接口概览 + +| 接口 | HTTP Method | Endpoint | 说明 | +|------|------|------|------| +| 搜索 | POST | `/search/` | 执行搜索查询 | +| 搜索建议 | GET | `/search/suggestions` | 搜索建议(自动补全/热词,多语言) | +| 即时搜索 | GET | `/search/instant` | 即时搜索预留接口(当前返回 `501 Not Implemented`) | +| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 | +| 全量索引 | POST | `/indexer/reindex` | 全量索引接口(导入数据,不删除索引,仅推荐自测使用) | +| 增量索引 | POST | `/indexer/index` | 增量索引接口(指定SPU ID列表进行索引,支持自动检测删除和显式删除,仅推荐自测使用) | +| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据(不写入ES) | +| 构建ES文档(正式对接) | POST | `/indexer/build-docs` | 基于上游提供的 MySQL 行数据构建 ES doc,不写入 ES,供 Java 等调用后自行写入 | +| 构建ES文档(测试用) | POST | `/indexer/build-docs-from-db` | 仅在测试/调试时使用,根据 `tenant_id + spu_ids` 内部查库并构建 ES doc | +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags,供微服务组合方式使用 | +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 | +| 健康检查 | GET | `/admin/health` | 服务健康检查 | +| 获取配置 | GET | `/admin/config` | 获取租户配置 | +| 索引统计 | GET | `/admin/stats` | 获取租户索引统计信息(需 tenant_id) | + +**微服务(独立端口或 Indexer 内,外部可直连)**: + +| 服务 | 端口 | 接口 | 说明 | +|------|------|------|------| +| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 | +| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 | +| 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) | +| 重排服务 | 6007 | `POST /rerank` | 检索结果重排 | +| 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 | + +--- + diff --git a/docs/搜索API对接指南-01-搜索接口.md b/docs/搜索API对接指南-01-搜索接口.md new file mode 100644 index 0000000..b3cded4 --- /dev/null +++ b/docs/搜索API对接指南-01-搜索接口.md @@ -0,0 +1,903 @@ +# 搜索API对接指南-01-搜索接口(POST /search/ 与响应) + +本篇以 `POST /search/` 为主线,包含: +- 请求参数:`3.2`、过滤器:`3.3`、分面:`3.4`、SKU筛选维度:`3.5` +- 响应格式:第 `4` 章(4.1~4.5) +- 常见场景示例:第 `8` 章(示例整体并入本篇,避免散落) + +## 搜索接口 + +### 3.1 接口信息 + +- **端点**: `POST /search/` +- **描述**: 执行文本搜索查询,支持多语言、过滤器和分面搜索 +- **租户标识**:`tenant_id` 通过 HTTP 请求头 **`X-Tenant-ID`** 传递(推荐);也可通过 URL query 参数 **`tenant_id`** 传递。**不要放在请求体中。** + +**请求示例(推荐)**: + +```python +url = f"{base_url.rstrip('/')}/search/" +headers = { + "Content-Type": "application/json", + "X-Tenant-ID": "162", # 租户ID,必填 +} +response = requests.post(url, headers=headers, json={"query": "芭比娃娃"}) +``` + +### 3.2 请求参数 + +#### 完整请求体结构 + +```json +{ + "query": "string (required)", + "size": 10, + "from": 0, + "language": "zh", + "filters": {}, + "range_filters": {}, + "facets": [], + "sort_by": "string", + "sort_order": "desc", + "min_score": 0.0, + "sku_filter_dimension": ["string"], + "debug": false, + "enable_rerank": null, + "rerank_query_template": "{query}", + "rerank_doc_template": "{title}", + "user_id": "string", + "session_id": "string" +} +``` + +#### 参数详细说明 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `query` | string | Y | - | 搜索查询字符串(统一文本检索策略) | +| `size` | integer | N | 10 | 返回结果数量(1-100) | +| `from` | integer | N | 0 | 分页偏移量(用于分页) | +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 | +| `filters` | object | N | null | 精确匹配过滤器(见[过滤器详解](#33-过滤器详解)) | +| `range_filters` | object | N | null | 数值范围过滤器(见[过滤器详解](#33-过滤器详解)) | +| `facets` | array | N | null | 分面配置(见[分面配置](#34-分面配置)) | +| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 | +| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) | +| `min_score` | float | N | null | 最小相关性分数阈值 | +| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) | +| `debug` | boolean | N | false | 是否返回调试信息 | +| `enable_rerank` | boolean/null | N | null | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。不传/传 null 使用服务端 `rerank.enabled`(默认开启)。开启后会先对 ES TopN(`rerank_window`)重排,再按分页截取;若 `from+size>1000`,则不重排,直接按分页从 ES 返回 | +| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 | +| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 | +| `user_id` | string | N | null | 用户ID(用于个性化,预留) | +| `session_id` | string | N | null | 会话ID(用于分析,预留) | + +### 3.3 过滤器详解 + +#### 3.3.1 精确匹配过滤器 (filters) + +用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。 + +**格式**: + +```json +{ + "filters": { + "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签 + "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段 + // specifications 嵌套过滤(特殊格式) + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +**支持的值类型**: +- 字符串:精确匹配 +- 整数:精确匹配 +- 布尔值:精确匹配 +- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值) +- 对象:specifications 嵌套过滤(见下文) + +**`*_all` 语义(多值 AND)**: +- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。 +- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。 +- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。 + +**Specifications 嵌套过滤**: + +`specifications` 是嵌套字段,支持按规格名称和值进行过滤。 + +**单个规格过滤**: + +```json +{ + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +查询规格名称为"color"且值为"white"的商品。 + +**多个规格过滤(按维度分组)**: + +```json +{ + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` + +查询同时满足所有规格的商品(color=white **且** size=256GB)。 + +**相同维度的多个值(OR 逻辑)**: + +```json +{ + "filters": { + "specifications": [ + {"name": "size", "value": "3"}, + {"name": "size", "value": "4"}, + {"name": "size", "value": "5"}, + {"name": "color", "value": "green"} + ] + } +} +``` + +查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。 + +**过滤逻辑说明**: +- **不同维度**(不同的 `name`)之间是 **AND** 关系(求交集) +- **相同维度**(相同的 `name`)的多个值之间是 **OR** 关系(求并集) + +**常用过滤字段**(详见[常用字段列表](./搜索API对接指南-08-数据模型与字段速查.md#93-常用字段列表)): +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型,支持数组) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 +- `specifications`: 规格过滤(嵌套字段,格式见上文) +- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。 + +#### 3.3.2 范围过滤器 (range_filters) + +用于数值字段的范围过滤。 + +**格式**: + +```json +{ + "range_filters": { + "min_price": { + "gte": 50, // 大于等于 + "lte": 200 // 小于等于 + }, + "max_price": { + "gt": 100 // 大于 + }, + "create_time": { + "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 + } + } +} +``` + +**支持的操作符**: +- `gte`: 大于等于 (>=) +- `gt`: 大于 (>) +- `lte`: 小于等于 (<=) +- `lt`: 小于 (<) + +**注意**: 至少需要指定一个操作符。 + +**常用范围字段**(详见[常用字段列表](./搜索API对接指南-08-数据模型与字段速查.md#93-常用字段列表)): +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +### 3.4 分面配置 + +用于生成分面统计(分组聚合),常用于构建筛选器UI。 + +#### 3.4.1 配置格式 + +```json +{ + "facets": [ + { + "field": "category1_name", + "size": 15, + "type": "terms", + "disjunctive": false + }, + { + "field": "brand_name", + "size": 10, + "type": "terms", + "disjunctive": true + }, + { + "field": "specifications.color", + "size": 20, + "type": "terms", + "disjunctive": true + }, + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ] +} +``` + +#### 3.4.2 Facet 字段说明 + +| 字段 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `field` | string | 是 | - | 分面字段名 | +| `size` | int | 否 | 10 | 返回的分面值数量(1-100) | +| `type` | string | 否 | "terms" | 分面类型:`terms`(词条聚合)或 `range`(范围聚合) | +| `disjunctive` | bool | 否 | false | 是否支持多选(disjunctive faceting)。启用后,选中该分面的过滤器时,仍会显示其他可选项 | +| `ranges` | array | 否 | null | 范围配置(仅 `type="range"` 时需要) | + +#### 3.4.3 disjunctive字段说明 + +**重要特性**: `disjunctive` 字段控制分面的行为模式。启用后,选中该分面的过滤器时,仍会显示其他可选项 + +**标准模式 (disjunctive: false)**: +- **行为**: 选中某个分面值后,该分面只显示选中的值 +- **适用场景**: 层级类目、互斥选择 +- **示例**: 类目下钻(玩具 > 娃娃 > 芭比) + +**Multi-Select 模式 (disjunctive: true)** ⭐: +- **行为**: 选中某个分面值后,该分面仍显示所有可选项 +- **适用场景**: 颜色、品牌、尺码等可切换属性 +- **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项 + +**推荐配置**: + +| 分面类型 | disjunctive | 原因 | +|---------|-------------|------| +| 颜色 | `true` | 用户需要切换颜色 | +| 品牌 | `true` | 用户需要比较品牌 | +| 尺码 | `true` | 用户需要查看其他尺码 | +| 类目 | `false` | 层级下钻 | +| 价格区间 | `false` | 互斥选择 | + +#### 3.4.4 规格分面说明 + +`specifications` 是嵌套字段,支持两种分面模式: + +**模式1:所有规格名称的分面**: + +```json +{ + "facets": [ + { + "field": "specifications", + "size": 10, + "type": "terms" + } + ] +} +``` + +返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 + +**模式2:指定规格名称的分面**: + +```json +{ + "facets": [ + { + "field": "specifications.color", + "size": 20, + "type": "terms", + "disjunctive": true + }, + { + "field": "specifications.size", + "size": 15, + "type": "terms", + "disjunctive": true + } + ] +} +``` + +只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。 + +**返回格式示例**: + +```json +{ + "facets": [ + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + {"value": "white", "count": 50, "selected": true}, // ✓ selected 字段由后端标记 + {"value": "black", "count": 30, "selected": false}, + {"value": "red", "count": 20, "selected": false} + ] + }, + { + "field": "specifications.size", + "label": "size", + "type": "terms", + "values": [ + {"value": "256GB", "count": 40, "selected": false}, + {"value": "512GB", "count": 20, "selected": false} + ] + } + ] +} +``` + +### 3.5 SKU筛选维度 + +**功能说明**: +`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式(子 SKU)维度**,为字符串列表。 +在店铺的 **主题装修配置** 中,商家可以为店铺设置一个或多个子款式筛选维度(例如 `color`、`size`),前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表,用户可以通过点击不同维度值(如不同颜色)来切换展示的子款式。 +当指定 `sku_filter_dimension` 后,后端会根据店铺的这项配置,从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据:系统会按指定维度**组合**对 SKU 进行分组,每个维度组合只返回第一个 SKU(从简实现,选择该组合下的第一款),其余不在这些维度组合中的子 SKU 将不返回。 + +**支持的维度值**: +1. **直接选项字段**: `option1`、`option2`、`option3` + - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组 + +2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配 + - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组 + +**示例**: + +**按颜色筛选(假设 option1_name = "color")**: + +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["color"] +} +``` + +**按选项1筛选**: + +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["option1"] +} +``` + +**按颜色 + 尺寸组合筛选(假设 option1_name = "color", option2_name = "size")**: + +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["color", "size"] +} +``` + +## 响应格式说明 + +### 4.1 标准响应结构 + +```json +{ + "results": [ + { + "spu_id": "12345", + "title": "芭比时尚娃娃", + "brief": "高品质芭比娃娃", + "description": "详细描述...", + "vendor": "美泰", + "category": "玩具", + "category_path": "玩具/娃娃/时尚", + "category_name": "时尚", + "category_id": "cat_001", + "category_level": 3, + "category1_name": "玩具", + "category2_name": "娃娃", + "category3_name": "时尚", + "tags": ["娃娃", "玩具", "女孩"], + "price": 89.99, + "compare_at_price": 129.99, + "currency": "USD", + "image_url": "https://example.com/image.jpg", + "in_stock": true, + "sku_prices": [89.99, 99.99, 109.99], + "sku_weights": [100, 150, 200], + "sku_weight_units": ["g", "g", "g"], + "total_inventory": 500, + "option1_name": "color", + "option2_name": "size", + "option3_name": null, + "specifications": [ + {"sku_id": "sku_001", "name": "color", "value": "pink"}, + {"sku_id": "sku_001", "name": "size", "value": "standard"} + ], + "skus": [ + { + "sku_id": "67890", + "price": 89.99, + "compare_at_price": 129.99, + "sku": "BARBIE-001", + "stock": 100, + "weight": 0.1, + "weight_unit": "kg", + "option1_value": "pink", + "option2_value": "standard", + "option3_value": null, + "image_src": "https://example.com/sku1.jpg" + } + ], + "relevance_score": 8.5 + } + ], + "total": 118, + "max_score": 8.5, + "facets": [ + { + "field": "category1_name", + "label": "category1_name", + "type": "terms", + "values": [ + { + "value": "玩具", + "label": "玩具", + "count": 85, + "selected": false + } + ] + }, + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + { + "value": "pink", + "label": "pink", + "count": 30, + "selected": false + } + ] + } + ], + "query_info": { + "original_query": "芭比娃娃", + "query_normalized": "芭比娃娃", + "rewritten_query": "芭比娃娃", + "detected_language": "zh", + "translations": { + "en": "barbie doll" + }, + "domain": "default" + }, + "suggestions": [], + "related_searches": [], + "took_ms": 45, + "performance_info": null, + "debug_info": null +} +``` + +### 4.2 响应字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `results` | array | 搜索结果列表(SpuResult对象数组) | +| `results[].spu_id` | string | SPU ID | +| `results[].title` | string | 商品标题 | +| `results[].price` | float | 价格(min_price) | +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) | +| `results[].relevance_score` | float | 相关性分数 | +| `total` | integer | 匹配的总文档数 | +| `max_score` | float | 最高相关性分数 | +| `facets` | array | 分面统计结果 | +| `query_info` | object | query处理信息 | +| `took_ms` | integer | 搜索耗时(毫秒) | +| `debug_info` | object/null | 调试信息,仅当请求传 `debug=true` 时返回 | + +#### 4.2.1 query_info 说明 + +`query_info` 包含本次搜索的查询解析与处理结果: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `original_query` | string | 用户原始查询 | +| `query_normalized` | string | 归一化后的查询(去空白、大小写等预处理,用于后续解析与改写) | +| `rewritten_query` | string | 重写后的查询(同义词/词典扩展等) | +| `detected_language` | string | 检测到的查询语言(如 `zh`、`en`) | +| `translations` | object | 翻译结果,键为语言代码,值为翻译文本 | +| `domain` | string | 查询域(如 `default`、`title`、`brand` 等) | + +#### 4.2.2 debug_info 说明 + +`debug_info` 主要用于检索效果评估、融合打分分析与 bad case 排查。 + +`debug_info.query_analysis` 常见字段: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `original_query` | string | 原始查询 | +| `query_normalized` | string | 归一化后的查询 | +| `rewritten_query` | string | 重写后的查询 | +| `detected_language` | string | 检测到的语言 | +| `translations` | object | 翻译结果 | +| `query_text_by_lang` | object | 实际参与检索的多语言 query 文本 | +| `search_langs` | array[string] | 实际参与检索的语言列表 | +| `supplemental_search_langs` | array[string] | 因 mixed query 补入的附加语言列表 | +| `has_vector` | boolean | 是否生成了向量 | + +`debug_info.per_result[]` 常见字段: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `spu_id` | string | 结果 SPU ID | +| `es_score` | float | ES 原始 `_score` | +| `rerank_score` | float | 重排分数 | +| `text_score` | float | 文本相关性大分(由 `base_query` / `base_query_trans_*` / `fallback_original_query_*` 聚合而来) | +| `text_source_score` | float | `base_query` 分数 | +| `text_translation_score` | float | `base_query_trans_*` 里的最大分数 | +| `text_fallback_score` | float | `fallback_original_query_*` 里的最大分数 | +| `text_primary_score` | float | 文本大分中的主证据部分 | +| `text_support_score` | float | 文本大分中的辅助证据部分 | +| `knn_score` | float | `knn_query` 分数 | +| `fused_score` | float | 最终融合分数 | +| `matched_queries` | object/array | ES named queries 命中详情 | + +### 4.3 SpuResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `spu_id` | string | SPU ID | +| `title` | string | 商品标题(根据language参数自动选择 `title.zh` 或 `title.en`) | +| `brief` | string | 商品短描述(根据language参数自动选择) | +| `description` | string | 商品详细描述(根据language参数自动选择) | +| `vendor` | string | 供应商/品牌(根据language参数自动选择) | +| `category` | string | 类目(兼容字段,等同于category_name) | +| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) | +| `category_name` | string | 类目名称(展示用,根据language参数自动选择) | +| `category_id` | string | 类目ID | +| `category_level` | integer | 类目层级(1/2/3) | +| `category1_name` | string | 一级类目名称 | +| `category2_name` | string | 二级类目名称 | +| `category3_name` | string | 三级类目名称 | +| `tags` | array[string] | 标签列表 | +| `price` | float | 价格(min_price) | +| `compare_at_price` | float | 原价 | +| `currency` | string | 货币单位(默认USD) | +| `image_url` | string | 主图URL | +| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | +| `sku_prices` | array[float] | 所有SKU价格列表 | +| `sku_weights` | array[integer] | 所有SKU重量列表 | +| `sku_weight_units` | array[string] | 所有SKU重量单位列表 | +| `total_inventory` | integer | 总库存 | +| `sales` | integer | 销量(展示销量) | +| `option1_name` | string | 选项1名称(如"color") | +| `option2_name` | string | 选项2名称(如"size") | +| `option3_name` | string | 选项3名称 | +| `specifications` | array[object] | 规格列表(与ES specifications字段对应) | +| `skus` | array | SKU 列表 | +| `relevance_score` | float | 相关性分数(默认为 ES 原始分数;当开启 AI 搜索时为融合后的最终分数) | + +### 4.4 SkuResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `sku_id` | string | SKU ID | +| `price` | float | 价格 | +| `compare_at_price` | float | 原价 | +| `sku` | string | SKU编码(sku_code) | +| `stock` | integer | 库存数量 | +| `weight` | float | 重量 | +| `weight_unit` | string | 重量单位 | +| `option1_value` | string | 选项1取值(如color值) | +| `option2_value` | string | 选项2取值(如size值) | +| `option3_value` | string | 选项3取值 | +| `image_src` | string | SKU图片地址 | + +### 4.5 多语言字段说明 + +- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段 +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 + +--- + +## 8. 常见场景示例 + +以下示例仅展示**请求体**(body);实际调用时请加上请求头 `X-Tenant-ID: <租户ID>`(或 URL 参数 `tenant_id`),参见 [3.1 接口信息](#31-接口信息)。 + +### 8.1 基础搜索与排序 + +**按价格从低到高排序**: + +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "price", + "sort_order": "asc" +} +``` + +**按价格从高到低排序**: + +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "price", + "sort_order": "desc" +} +``` + +**按销量从高到低排序**: + +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "sales", + "sort_order": "desc" +} +``` + +**按默认(相关性)排序**: + +```json +{ + "query": "玩具", + "size": 20, + "from": 0 +} +``` + +### 8.2 过滤搜索 + +**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 + +```json +{ + "query": "玩具", + "size": 20, + "language": "zh", + "filters": { + "category_name": "益智玩具" + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + } +} +``` + +**需求**: 搜索"手机",筛选多个品牌,价格范围 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "vendor.zh.keyword": ["品牌A", "品牌B"] + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + } +} +``` + +### 8.3 分面搜索 + +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器 + +```json +{ + "query": "玩具", + "size": 20, + "language": "zh", + "facets": [ + {"field": "category1_name", "size": 15, "type": "terms"}, + {"field": "category2_name", "size": 10, "type": "terms"}, + {"field": "specifications", "size": 10, "type": "terms"} + ] +} +``` + +**需求**: 搜索"手机",获取价格区间和规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + }, + { + "field": "specifications", + "size": 10, + "type": "terms" + } + ] +} +``` + +### 8.4 规格过滤与分面 + +**需求**: 搜索"手机",筛选color为"white"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +**需求**: 搜索"手机",筛选color为"white"且size为"256GB"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` + +**需求**: 搜索"手机",筛选size为"3"、"4"或"5",且color为"green"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": [ + {"name": "size", "value": "3"}, + {"name": "size", "value": "4"}, + {"name": "size", "value": "5"}, + {"name": "color", "value": "green"} + ] + } +} +``` + +**需求**: 搜索"手机",获取所有规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + {"field": "specifications", "size": 10, "type": "terms"} + ] +} +``` + +**需求**: 只获取"color"和"size"规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + {"field": "specifications.color", "size": 20, "type": "terms"}, + {"field": "specifications.size", "size": 15, "type": "terms"} + ] +} +``` + +**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "category_name": "手机", + "specifications": { + "name": "color", + "value": "white" + } + }, + "facets": [ + {"field": "category1_name", "size": 15, "type": "terms"}, + {"field": "category2_name", "size": 10, "type": "terms"}, + {"field": "specifications.color", "size": 20, "type": "terms"}, + {"field": "specifications.size", "size": 15, "type": "terms"} + ] +} +``` + +### 8.5 SKU筛选 + +**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU + +```json +{ + "query": "芭比娃娃", + "size": 20, + "sku_filter_dimension": ["color"] +} +``` + +**说明**: +- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组 +- 每个SPU下,每种颜色只会返回第一个SKU +- 如果维度不匹配,返回所有SKU(不进行过滤) + +### 8.6 分页查询 + +**需求**: 获取第2页结果(每页20条) + +```json +{ + "query": "手机", + "size": 20, + "from": 20 +} +``` + +--- + diff --git a/docs/搜索API对接指南-02-搜索建议与即时搜索.md b/docs/搜索API对接指南-02-搜索建议与即时搜索.md new file mode 100644 index 0000000..6dca4cf --- /dev/null +++ b/docs/搜索API对接指南-02-搜索建议与即时搜索.md @@ -0,0 +1,81 @@ +# 搜索API对接指南-02-搜索建议与即时搜索 + +本篇面向前端联想词/搜索框团队,独立阅读 `GET /search/suggestions` 与 `GET /search/instant`。 + +## 搜索接口 + +### 3.7 搜索建议接口 + +- **端点**: `GET /search/suggestions` +- **描述**: 返回搜索建议(自动补全/热词),支持多语言。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | Y | - | 查询字符串(至少 1 个字符) | +| `size` | integer | N | 10 | 返回建议数量(1-50) | +| `language` | string | N | `en` | 请求语言,如 `zh` / `en` / `ar` / `ru`,用于路由到对应语种 suggestion 索引 | +| `debug` | bool | N | `false` | 是否开启调试(目前主要用于排查 suggestion 排序与语言解析) | + +> **租户标识**:同 [-01-搜索接口](./搜索API对接指南-01-搜索接口.md#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 + +#### 响应示例 + +```json +{ + "query": "iph", + "language": "en", + "resolved_language": "en", + "suggestions": [ + { + "text": "iphone 15", + "lang": "en", + "score": 12.37, + "rank_score": 5.1, + "sources": ["query_log", "qanchor"], + "lang_source": "log_field", + "lang_confidence": 1.0, + "lang_conflict": false + } + ], + "took_ms": 12 +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/suggestions?q=芭&size=5&language=zh" \ + -H "X-Tenant-ID: 162" +``` + +### 3.8 即时搜索接口 + +> ⚠️ 当前版本未开放该能力。接口会明确返回 `501 Not Implemented`,避免误用未完成实现。 + +- **端点**: `GET /search/instant` +- **描述**: 即时搜索预留端点,后续会在独立实现完成后开放。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | Y | - | 搜索查询(至少 2 个字符) | +| `size` | integer | N | 5 | 返回结果数量(1-20) | + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/instant?q=玩具&size=5" +``` + +#### 当前响应 + +```json +{ + "error": "/search/instant is not implemented yet. Use POST /search/ for production traffic.", + "status_code": 501 +} +``` + diff --git a/docs/搜索API对接指南-03-获取文档.md b/docs/搜索API对接指南-03-获取文档.md new file mode 100644 index 0000000..1ff51a3 --- /dev/null +++ b/docs/搜索API对接指南-03-获取文档.md @@ -0,0 +1,40 @@ +# 搜索API对接指南-03-获取文档(GET /search/{doc_id}) + +用于点击结果后的详情页回源,或排查某个文档在检索侧的字段情况。 + +## 搜索接口 + +### 3.9 获取单个文档 + +- **端点**: `GET /search/{doc_id}` +- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。 +- **租户标识**:同 [-01-搜索接口](./搜索API对接指南-01-搜索接口.md#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 + +#### 路径参数 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `doc_id` | string | 商品或文档 ID | + +#### 响应示例 + +```json +{ + "id": "12345", + "source": { + "title": { + "zh": "芭比时尚娃娃" + }, + "min_price": 89.99, + "category1_name": "玩具" + } +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162" +# 或使用 query 参数:curl "http://localhost:6002/search/12345?tenant_id=162" +``` + diff --git a/docs/搜索API对接指南-05-索引接口(Indexer).md b/docs/搜索API对接指南-05-索引接口(Indexer).md new file mode 100644 index 0000000..6911913 --- /dev/null +++ b/docs/搜索API对接指南-05-索引接口(Indexer).md @@ -0,0 +1,767 @@ +# 搜索API对接指南-05-索引接口(Indexer) + +本篇覆盖数据同步/索引构建相关的所有接口(原文第 5 章),用于 `external indexer` 和 `Indexer 服务` 的对接。 + +## 索引接口 + +本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) | +| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 | +| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES | +| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES | +| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 | +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags(供微服务组合方式使用) | +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 | + +#### 5.0 支撑外部 indexer 的三种方式 + +本服务对**外部 indexer 程序**(如 Java 索引系统)提供三种对接方式,可按需选择: + +| 方式 | 说明 | 适用场景 | +|------|------|----------| +| **1)doc 填充接口** | 调用 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db`,由本服务基于 MySQL 行数据构建完整 ES 文档(含多语言、向量、规格等),**不写入 ES**,由调用方自行写入。 | 希望一站式拿到 ES-ready doc,由己方控制写 ES 的时机与索引名。 | +| **2)微服务组合** | 单独调用**翻译**、**向量化**、**内容理解字段生成**等接口,由 indexer 程序自己组装 doc 并写入 ES。翻译与向量化为独立微服务(见第 7 节);内容理解为 Indexer 服务内接口 `POST /indexer/enrich-content`。 | 需要灵活编排、或希望将 LLM/向量等耗时步骤与主链路解耦(如异步补齐 qanchors/tags)。 | +| **3)本服务直接写 ES** | 调用全量索引 `POST /indexer/reindex`、增量索引 `POST /indexer/index`(指定 SPU ID 列表),由本服务从 MySQL 拉数并直接写入 ES。 | 自建运维、联调或不需要由 Java 写 ES 的场景。 | + +- **方式 1** 与 **方式 2** 下,ES 的写入方均为外部 indexer(或 Java),职责清晰。 +- **方式 3** 下,本服务同时负责读库、构建 doc 与写 ES。 + +### 5.1 为租户创建索引 + +为租户创建索引需要两个步骤: + +1. **创建索引结构**(可选,仅在需要更新 mapping 或在新环境首次创建时执行) + - 使用脚本创建 ES 索引结构(基于 `mappings/search_products.json`) + - 如果索引已存在,会提示用户确认(会删除现有数据) + +2. **导入数据**(必需) + - 使用全量索引接口 `/indexer/reindex` 导入数据 + +**创建索引结构(支持多环境 namespace)**: + +```bash +# 以 UAT 环境为例: +# 1. 准备 UAT 环境的 .env(包含 UAT 的 ES_HOST/DB_HOST 等) +# 2. 设置环境前缀(也可以直接在 .env 中配置): +export RUNTIME_ENV=uat +export ES_INDEX_NAMESPACE=uat_ + +# 3. 为 tenant_id=170 创建索引结构 +./scripts/create_tenant_index.sh 170 +``` + +脚本会自动从项目根目录的 `.env` 文件加载 ES 配置,并根据 `ES_INDEX_NAMESPACE` 创建: + +- prod 环境(ES_INDEX_NAMESPACE 为空):`search_products_tenant_170` +- UAT 环境(ES_INDEX_NAMESPACE=uat_):`uat_search_products_tenant_170` + +**注意事项**: +- ⚠️ 如果索引已存在,脚本会提示确认,确认后会删除现有数据 +- 创建索引后,**必须**调用 `/indexer/reindex` 导入数据 +- 如果只是更新数据而不需要修改索引结构,直接使用 `/indexer/reindex` 即可 + +--- + +### 5.2 全量索引接口 + +- **端点**: `POST /indexer/reindex` +- **描述**: 全量索引,将指定租户的所有SPU数据导入到ES索引(不会删除现有索引)。**推荐仅用于自测/运维场景**;生产环境下更推荐由 Java 等上游控制调度与写 ES。 + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "batch_size": 500 +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `tenant_id` | string | Y | - | 租户ID | +| `batch_size` | integer | N | 500 | 批量导入大小 | + +#### 响应格式 + +**成功响应(200 OK)**(示例,实际 `index_name` 会带上 tenant 和环境前缀): + +```json +{ + "success": true, + "total": 1000, + "indexed": 1000, + "failed": 0, + "elapsed_time": 12.34, + "index_name": "search_products_tenant_162", + "tenant_id": "162" +} +``` + +**错误响应**: +- `400 Bad Request`: 参数错误 +- `503 Service Unavailable`: 服务未初始化 + +#### 请求示例 + +**全量索引(不会删除现有索引)**: + +```bash +curl -X POST "http://localhost:6004/indexer/reindex" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "batch_size": 500 + }' +``` + +**查看日志**: + +```bash +# 查看API日志(包含索引操作日志) +tail -f logs/api.log + +# 或者查看所有日志文件 +tail -f logs/*.log +``` + +> ⚠️ **重要提示**:如需 **创建索引结构**,请参考 [5.1 为租户创建索引](#51-为租户创建索引) 章节,使用 `./scripts/create_tenant_index.sh `。创建后需要调用 `/indexer/reindex` 导入数据。 + +**查看索引日志**: + +索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON 格式),包括: +- 请求开始和结束时间 +- 租户ID、SPU ID、操作类型 +- 每个SPU的处理状态 +- ES批量写入结果 +- 成功/失败统计和详细错误信息 + +```bash +# 实时查看索引日志(包含全量和增量索引的所有操作) +tail -f logs/indexer.log + +# 使用 grep 查询(简单方式) +# 查看全量索引日志 +grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100 + +# 查看增量索引日志 +grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100 + +# 查看特定租户的索引日志 +grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100 + +# 使用 jq 查询(推荐,更精确的 JSON 查询) +# 安装 jq: sudo apt-get install jq 或 brew install jq + +# 查看全量索引日志 +cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100 + +# 查看增量索引日志 +cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100 + +# 查看特定租户的索引日志 +cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100 + +# 查看失败的索引操作 +cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)' + +# 查看特定SPU的处理日志 +cat logs/indexer.log | jq 'select(.spu_id == "123")' + +# 查看最近的索引请求统计 +cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}' +``` + +### 5.3 增量索引接口 + +- **端点**: `POST /indexer/index` +- **描述**: 增量索引接口,根据指定的SPU ID列表进行索引,直接将数据写入ES。用于增量更新指定商品。**推荐仅作为内部/调试入口**;正式对接建议改用 `/indexer/build-docs`,由上游写 ES。 + +**删除说明**: +- `spu_ids`中的SPU:如果数据库`deleted=1`,自动从ES删除,响应状态为`deleted` +- `delete_spu_ids`中的SPU:直接删除,响应状态为`deleted`、`not_found`或`failed` + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"], + "delete_spu_ids": ["100", "101"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户ID | +| `spu_ids` | array[string] | N | SPU ID列表(1-100个),要索引的SPU。如果为空,则只执行删除操作 | +| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表(1-100个),可选。无论数据库状态如何,都会从ES中删除这些SPU | + +**注意**: +- `spu_ids` 和 `delete_spu_ids` 不能同时为空 +- 每个列表最多支持100个SPU ID +- 如果SPU在`spu_ids`中且数据库`deleted=1`,会自动从ES删除(自动检测删除) + +#### 响应格式 + +```json +{ + "spu_ids": [ + { + "spu_id": "123", + "status": "indexed" + }, + { + "spu_id": "456", + "status": "deleted" + }, + { + "spu_id": "789", + "status": "failed", + "msg": "SPU not found (unexpected)" + } + ], + "delete_spu_ids": [ + { + "spu_id": "100", + "status": "deleted" + }, + { + "spu_id": "101", + "status": "not_found" + }, + { + "spu_id": "102", + "status": "failed", + "msg": "Failed to delete from ES: Connection timeout" + } + ], + "total": 6, + "success_count": 4, + "failed_count": 2, + "elapsed_time": 1.23, + "index_name": "search_products", + "tenant_id": "162" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `spu_ids` | array | spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | +| `spu_ids[].status` | string | 状态:`indexed`(已索引)、`deleted`(已删除,自动检测)、`failed`(失败) | +| `spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | +| `delete_spu_ids` | array | delete_spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | +| `delete_spu_ids[].status` | string | 状态:`deleted`(已删除)、`not_found`(ES中不存在)、`failed`(失败) | +| `delete_spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | +| `total` | integer | 总处理数量(spu_ids数量 + delete_spu_ids数量) | +| `success_count` | integer | 成功数量(indexed + deleted + not_found) | +| `failed_count` | integer | 失败数量 | +| `elapsed_time` | float | 耗时(秒) | +| `index_name` | string | 索引名称 | +| `tenant_id` | string | 租户ID | + +**状态说明**: +- `spu_ids` 的状态: + - `indexed`: SPU已成功索引到ES + - `deleted`: SPU在数据库中被标记为deleted=1,已从ES删除(自动检测) + - `failed`: 处理失败,会包含`msg`字段说明失败原因 +- `delete_spu_ids` 的状态: + - `deleted`: SPU已从ES成功删除 + - `not_found`: SPU在ES中不存在(也算成功,可能已经被删除过) + - `failed`: 删除失败,会包含`msg`字段说明失败原因 + +#### 请求示例 + +**示例1:普通增量索引(自动检测删除)**: + +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] + }' +``` + +说明:如果SPU 456在数据库中`deleted=1`,会自动从ES删除,在响应中`spu_ids`列表里456的状态为`deleted`。 + +**示例2:显式删除(批量删除)**: + +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456"], + "delete_spu_ids": ["100", "101", "102"] + }' +``` + +说明:SPU 100、101、102会被显式删除,无论数据库状态如何。 + +**示例3:仅删除(不索引)**: + +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": [], + "delete_spu_ids": ["100", "101"] + }' +``` + +说明:只执行删除操作,不进行索引。 + +**示例4:混合操作(索引+删除)**: + +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"], + "delete_spu_ids": ["100", "101"] + }' +``` + +说明:同时执行索引和删除操作。 + +#### 日志说明 + +增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON格式),包括: +- 请求开始和结束时间 +- 每个SPU的处理状态(获取、转换、索引、删除) +- ES批量写入结果 +- 成功/失败统计 +- 详细的错误信息 + +日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。 + +### 5.4 查询文档接口 + +- **端点**: `POST /indexer/documents` +- **描述**: 查询文档接口,根据SPU ID列表获取ES文档数据(**不写入ES**)。用于查看、调试或验证SPU数据。 + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户ID | +| `spu_ids` | array[string] | Y | SPU ID列表(1-100个) | + +#### 响应格式 + +```json +{ + "success": [ + { + "spu_id": "123", + "document": { + "tenant_id": "162", + "spu_id": "123", + "title": { + "zh": "商品标题" + }, + ... + } + }, + { + "spu_id": "456", + "document": {...} + } + ], + "failed": [ + { + "spu_id": "789", + "error": "SPU not found or deleted" + } + ], + "total": 3, + "success_count": 2, + "failed_count": 1 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | array | 成功获取的SPU列表,每个元素包含 `spu_id` 和 `document`(完整的ES文档数据) | +| `failed` | array | 失败的SPU列表,每个元素包含 `spu_id` 和 `error`(失败原因) | +| `total` | integer | 总SPU数量 | +| `success_count` | integer | 成功数量 | +| `failed_count` | integer | 失败数量 | + +#### 请求示例 + +**单个SPU查询**: + +```bash +curl -X POST "http://localhost:6004/indexer/documents" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123"] + }' +``` + +**批量SPU查询**: + +```bash +curl -X POST "http://localhost:6004/indexer/documents" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] + }' +``` + +#### 与 `/indexer/index` 的区别 + +| 接口 | 功能 | 是否写入ES | 返回内容 | +|------|------|-----------|----------| +| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 | +| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 | + +**使用场景**: +- `/indexer/documents`:用于查看、调试或验证SPU数据,不修改ES索引 +- `/indexer/index`:用于实际的增量索引操作,将更新的SPU数据同步到ES + +### 5.5 索引健康检查接口 + +- **端点**: `GET /indexer/health` +- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致) + +#### 响应格式 + +```json +{ + "status": "available", + "database": "connected", + "preloaded_data": { + "category_mappings": 150 + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) | +| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` | +| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 | + +#### 请求示例 + +```bash +curl -X GET "http://localhost:6004/indexer/health" +``` + +### 5.6 文档构建接口(正式对接推荐) + +#### 5.6.1 `POST /indexer/build-docs` + +- **描述**: + 基于调用方(通常是 Java 索引程序)提供的 **MySQL 行数据** 构建 ES 文档(doc),**不写入 ES**。 + 由本服务负责“如何构建 doc”(多语言、翻译、向量、规格聚合等),由调用方负责“何时调度 + 如何写 ES”。 + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "items": [ + { + "spu": { "id": 223167, "tenant_id": 170, "title": "..." }, + "skus": [ + { "id": 3988393, "spu_id": 223167, "price": 25.99, "compare_at_price": 25.99 } + ], + "options": [] + } + ] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** | + +> `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。 + +#### 请求示例(完整 curl) + +> 完整请求体参考 `scripts/test_build_docs_api.py` 中的 `build_sample_request()`。 + +```bash +# 单条 SPU 示例(含 spu、skus、options) +curl -X POST "http://localhost:6004/indexer/build-docs" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "items": [ + { + "spu": { + "id": 10001, + "tenant_id": "162", + "title": "测试T恤 纯棉短袖", + "brief": "舒适纯棉,多色可选", + "description": "这是一款适合日常穿着的纯棉T恤,透气吸汗。", + "vendor": "测试品牌", + "category": "服装/上衣/T恤", + "category_id": 100, + "category_level": 2, + "category_path": "服装/上衣/T恤", + "fake_sales": 1280, + "image_src": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg", + "tags": "T恤,纯棉,短袖,夏季", + "create_time": "2024-01-01T00:00:00Z", + "update_time": "2024-01-01T00:00:00Z" + }, + "skus": [ + { + "id": 20001, + "spu_id": 10001, + "price": 99.0, + "compare_at_price": 129.0, + "sku": "SKU-TSHIRT-001", + "inventory_quantity": 50, + "option1": "黑色", + "option2": "M", + "option3": null + }, + { + "id": 20002, + "spu_id": 10001, + "price": 99.0, + "compare_at_price": 129.0, + "sku": "SKU-TSHIRT-002", + "inventory_quantity": 30, + "option1": "白色", + "option2": "L", + "option3": null + } + ], + "options": [ + {"id": 1, "position": 1, "name": "颜色"}, + {"id": 2, "position": 2, "name": "尺码"} + ] + } + ] +}' +``` + +生产环境替换 `localhost:6004` 为实际 Indexer 地址,如 `http://43.166.252.75:6004`。 + +#### 响应示例(节选) + +```json +{ + "tenant_id": "170", + "docs": [ + { + "tenant_id": "170", + "spu_id": "223167", + "title": { "en": "...", "zh": "..." }, + "tags": ["Floerns", "Clothing", "Shoes & Jewelry"], + "skus": [ + { + "sku_id": "3988393", + "price": 25.99, + "compare_at_price": 25.99, + "stock": 100 + } + ], + "min_price": 25.99, + "max_price": 25.99, + "compare_at_price": 25.99, + "total_inventory": 100, + "title_embedding": [/* 1024 维向量 */] + // 其余字段与 mappings/search_products.json 一致 + } + ], + "total": 1, + "success_count": 1, + "failed_count": 0, + "failed": [] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `tenant_id` | string | 租户 ID | +| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 | +| `total` | integer | 请求的 items 总数 | +| `success_count` | integer | 成功构建数量 | +| `failed_count` | integer | 失败数量 | +| `failed` | array | 失败项列表,每项含 `spu_id`、`error` | + +#### 使用建议 + +- **生产环境推荐流程**: + 1. Java 根据业务逻辑决定哪些 SPU 需要(全量/增量)处理; + 2. Java 从 MySQL 查询 SPU/SKU/Option 行,拼成 `items`; + 3. 调用 `/indexer/build-docs` 获取 ES-ready `docs`; + 4. Java 使用自己的 ES 客户端写入 `search_products_tenant_{tenant_id}`。 + +### 5.7 文档构建接口(测试 / 自测) + +#### 5.7.1 `POST /indexer/build-docs-from-db` + +- **描述**: + 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。** + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "spu_ids": ["223167", "223168"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** | + +#### 响应格式 + +与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。 + +#### 请求示例 + +```bash +curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \ + -H "Content-Type: application/json" \ + -d '{"tenant_id": "170", "spu_ids": ["223167"]}' +``` + +返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。 + +### 5.8 内容理解字段生成接口 + +- **端点**: `POST /indexer/enrich-content` +- **描述**: 根据商品内容信息批量生成 **qanchors**(锚文本)、**semantic_attributes**(语义属性)、**tags**(细分标签),供外部 indexer 在「微服务组合」方式下自行拼装 doc 时使用。请求以 `items[]` 传入商品内容字段(必填/可选见下表)。内部逻辑与 `indexer.product_enrich` 一致,支持多语言与 Redis 缓存;单次请求在线程池中执行,避免阻塞其他接口。 + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "items": [ + { + "spu_id": "223167", + "title": "纯棉短袖T恤 夏季男装", + "brief": "夏季透气纯棉短袖,舒适亲肤", + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", + "image_url": "https://example.com/images/223167.jpg" + }, + { + "spu_id": "223168", + "title": "12PCS Dolls with Bottles", + "image_url": "https://example.com/images/223168.jpg" + } + ], + "languages": ["zh", "en"] +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `tenant_id` | string | Y | - | 租户 ID。目前仅用于记录日志,不产生实际作用| +| `items` | array | Y | - | 待分析列表;**单次最多 50 条** | +| `languages` | array[string] | N | `["zh", "en"]` | 目标语言,需在支持范围内:`zh`、`en`、`de`、`ru`、`fr` | + +`items[]` 字段说明: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `spu_id` | string | Y | SPU ID,用于回填结果;目前仅用于记录日志,不产生实际作用| +| `title` | string | Y | 商品标题 | +| `image_url` | string | N | 商品主图 URL;当前会参与内容缓存键,后续可用于图像/多模态内容理解 | +| `brief` | string | N | 商品简介/短描述;当前会参与内容缓存键 | +| `description` | string | N | 商品详情/长描述;当前会参与内容缓存键 | + +缓存说明: + +- 内容缓存键仅由 `target_lang + items[]` 中会影响内容理解结果的输入文本构成,目前包括:`title`、`brief`、`description`、`image_url` 的规范化内容 hash。 +- `tenant_id`、`spu_id` 只用于请求归属与结果回填,不参与缓存键。 +- 因此,输入内容不变时可跨请求直接命中缓存;任一输入字段变化时,会自然落到新的缓存 key。 + +批量请求建议: +- **全量**:强烈建议 尽可能 **20 个 SPU/doc** 攒成一个批次后再请求一次。 +- **增量**:可按时效要求设置时间窗口(例如 **5 分钟**),在窗口内尽可能攒到 **20 个**;达到 20 或窗口到期就发送一次请求。 +- 允许超过20,服务内部会拆分成小批次逐个处理。也允许小于20,但是将造成费用和耗时的成本上升,特别是每次请求一个doc的情况。 + +#### 响应格式 + +```json +{ + "tenant_id": "170", + "total": 2, + "results": [ + { + "spu_id": "223167", + "qanchors": { + "zh": "短袖T恤,纯棉,男装,夏季", + "en": "cotton t-shirt, short sleeve, men, summer" + }, + "semantic_attributes": [ + { "lang": "zh", "name": "tags", "value": "纯棉" }, + { "lang": "zh", "name": "usage_scene", "value": "日常" }, + { "lang": "en", "name": "tags", "value": "cotton" } + ], + "tags": ["纯棉", "短袖", "男装", "cotton", "short sleeve"] + }, + { + "spu_id": "223168", + "qanchors": { "en": "dolls, toys, 12pcs" }, + "semantic_attributes": [], + "tags": ["dolls", "toys"] + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `results` | array | 与请求 `items` 一一对应,每项含 `spu_id`、`qanchors`、`semantic_attributes`、`tags` | +| `results[].qanchors` | object | 按语言键的锚文本(逗号分隔短语),可写入 ES 文档的 `qanchors.{lang}` | +| `results[].semantic_attributes` | array | 语义属性列表,每项为 `{ "lang", "name", "value" }`,可写入 ES 的 `semantic_attributes` nested 字段 | +| `results[].tags` | array | 从语义属性中抽取的 `name=tags` 的 value 集合,可与业务原有 `tags` 合并后写入 ES 的 `tags` 字段 | +| `results[].error` | string | 若该条处理失败(如 LLM 异常),会在此字段返回错误信息 | + +**错误响应**: +- `400`: `items` 为空或超过 50 条 +- `503`: 未配置 `DASHSCOPE_API_KEY`,内容理解服务不可用 + +#### 请求示例 + +```bash +curl -X POST "http://localhost:6004/indexer/enrich-content" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "170", + "items": [ + { + "spu_id": "223167", + "title": "纯棉短袖T恤 夏季男装", + "brief": "夏季透气纯棉短袖,舒适亲肤", + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", + "image_url": "https://example.com/images/223167.jpg" + } + ], + "languages": ["zh", "en"] + }' +``` + +--- + diff --git a/docs/搜索API对接指南-06-管理接口(Admin).md b/docs/搜索API对接指南-06-管理接口(Admin).md new file mode 100644 index 0000000..303b6f8 --- /dev/null +++ b/docs/搜索API对接指南-06-管理接口(Admin).md @@ -0,0 +1,53 @@ +# 搜索API对接指南-06-管理接口(Admin) + +用于查看服务健康状态、获取租户配置与索引统计信息(原文第 6 章)。 + +## 管理接口 + +### 6.1 健康检查 + +- **端点**: `GET /admin/health` +- **描述**: 检查服务与依赖(如 Elasticsearch)状态。 + +```json +{ + "status": "healthy", + "elasticsearch": "connected", + "tenant_id": "tenant1" +} +``` + +### 6.2 获取配置 + +- **端点**: `GET /admin/config` +- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。 + +```json +{ + "tenant_id": "tenant1", + "tenant_name": "Tenant1 Test Instance", + "es_index_name": "search_tenant1", + "num_fields": 20, + "num_indexes": 4, + "supported_languages": ["zh", "en", "ru"], + "spu_enabled": false +} +``` + +### 6.3 索引统计 + +- **端点**: `GET /admin/stats` +- **描述**: 获取指定租户索引文档数量与磁盘大小,方便监控。 +- **租户标识**:通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递(必填)。 + +```json +{ + "tenant_id": "162", + "index_name": "search_products_tenant_162", + "document_count": 10000, + "size_mb": 523.45 +} +``` + +--- + diff --git a/docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md b/docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md new file mode 100644 index 0000000..ad33930 --- /dev/null +++ b/docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @@ -0,0 +1,401 @@ +# 搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation) + +本篇覆盖向量服务(Embedding)、重排服务(Reranker)、翻译服务(Translation)以及 Indexer 服务内的内容理解字段生成(原文第 7 章)。 + +## 7. 微服务接口(向量、重排、翻译) + +以下三个微服务独立部署,**外部系统可直接调用**。它们被搜索后端(6002)和索引服务(6004)内部使用,也可供其他业务系统直接对接。 + +| 服务 | 默认端口 | Base URL | 说明 | +|------|----------|----------|------| +| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 | +| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 | +| 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) | +| 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 | + +生产环境请将 `localhost` 替换为实际服务地址。 +服务管理入口与完整启停规则见:`docs/Usage-Guide.md` -> `服务管理总览`。 + +### 7.1 向量服务(Embedding) + +- **Base URL**: + - 文本:`http://localhost:6005`(可通过 `EMBEDDING_TEXT_SERVICE_URL` 覆盖) + - 图片:`http://localhost:6008`(可通过 `EMBEDDING_IMAGE_SERVICE_URL` 覆盖) +- **启动**: + - 文本:`./scripts/start_embedding_text_service.sh` + - 图片:`./scripts/start_embedding_image_service.sh` +- **依赖**: + - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`) + - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`) + - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU) + - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`) + - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力 + +补充说明: + +- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。 +- 服务端对 text / image 有**独立 admission control**: + - `TEXT_MAX_INFLIGHT` + - `IMAGE_MAX_INFLIGHT` +- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。 +- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。 + +#### 7.1.1 `POST /embed/text` — 文本向量化 + +将文本列表转为 1024 维向量,用于语义搜索、文档索引等。 + +**请求体**(JSON 数组): + +```json +["文本1", "文本2", "文本3"] +``` + +**响应**(JSON 数组,与输入一一对应): + +```json +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] +``` + +**完整 curl 示例**: + +```bash +curl -X POST "http://localhost:6005/embed/text?normalize=true" \ + -H "Content-Type: application/json" \ + -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]' +``` + +#### 7.1.2 `POST /embed/image` — 图片向量化 + +将图片 URL 或路径转为向量,用于以图搜图。 + +前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。 + +**请求体**(JSON 数组): + +```json +["https://example.com/image1.jpg", "https://example.com/image2.jpg"] +``` + +**响应**(JSON 数组,与输入一一对应): + +```json +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] +``` + +**完整 curl 示例**: + +```bash +curl -X POST "http://localhost:6008/embed/image?normalize=true" \ + -H "Content-Type: application/json" \ + -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' +``` + +#### 7.1.3 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6005/health" +curl "http://localhost:6008/health" +``` + +返回中会包含: + +- `service_kind`:`text` / `image` / `all` +- `cache_enabled`:text/image Redis 缓存是否可用 +- `limits`:当前 inflight limit、active、rejected_total 等 +- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等 + +#### 7.1.4 `GET /ready` — 就绪检查 + +```bash +curl "http://localhost:6005/ready" +curl "http://localhost:6008/ready" +``` + +#### 7.1.5 缓存与限流说明 + +- 文本与图片都会先查 Redis 向量缓存。 +- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。 +- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。 +- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。 +- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。 + +#### 7.1.6 TEI 统一调优建议(主服务) + +使用单套主服务即可同时兼顾: +- 在线 query 向量化(低延迟,常见 `batch=1~4`) +- 索引构建向量化(高吞吐,常见 `batch=15~20`) + +统一启动(主链路): + +```bash +./scripts/start_tei_service.sh +./scripts/service_ctl.sh restart embedding +``` + +默认端口: +- TEI: `http://127.0.0.1:8080` +- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005` +- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008` + +当前主 TEI 启动默认值(已按 T4/短文本场景调优): +- `TEI_MAX_BATCH_TOKENS=4096` +- `TEI_MAX_CLIENT_BATCH_SIZE=24` +- `TEI_DTYPE=float16` + +### 7.2 重排服务(Reranker) + +- **Base URL**: `http://localhost:6007`(可通过 `RERANKER_SERVICE_URL` 覆盖) +- **启动**: `./scripts/start_reranker.sh` + +说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。 + +补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确)。 + +#### 7.2.1 `POST /rerank` — 结果重排 + +根据 query 与 doc 的相关性对文档列表重新打分排序。 + +**请求体**: +```json +{ + "query": "玩具 芭比", + "docs": [ + "12PCS 6 Types of Dolls with Bottles", + "纯棉T恤 短袖 夏季" + ], + "normalize": true +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `query` | string | Y | 搜索查询 | +| `docs` | array[string] | Y | 待重排的文档列表(单次最多由服务端配置限制) | +| `normalize` | boolean | N | 是否对分数做 sigmoid 归一化,默认 true | + +**响应**: +```json +{ + "scores": [0.92, 0.15], + "meta": { + "service_elapsed_ms": 45.2, + "input_docs": 2, + "unique_docs": 2 + } +} +``` + +**完整 curl 示例**: +```bash +curl -X POST "http://localhost:6007/rerank" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 芭比", + "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"], + "top_n":386, + "normalize": true + }' +``` + +#### 7.2.2 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6007/health" +``` + +### 7.3 翻译服务(Translation) + +- **Base URL**: `http://localhost:6006`(以 `config/config.yaml -> services.translation.service_url` 为准) +- **启动**: `./scripts/start_translator.sh` + +#### 7.3.1 `POST /translate` — 文本翻译 + +支持 translator service 内所有已启用 capability,适用于商品名称、描述、query 等电商场景。当前可配置能力包括 `qwen-mt`、`llm`、`deepl` 以及本地模型 `nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh`。 + +**请求体**(支持单条字符串或字符串列表): +```json +{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +也支持批量列表形式: +```json +{ + "text": ["商品名称1", "商品名称2"], + "target_lang": "en", + "source_lang": "zh", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `text` | string \| string[] | Y | 待翻译文本,既支持单条字符串,也支持字符串列表(批量翻译) | +| `target_lang` | string | Y | 目标语言:`zh`、`en`、`ru` 等 | +| `source_lang` | string | N | 源语言。云端模型可不传;`nllb-200-distilled-600m` 建议显式传入 | +| `model` | string | N | 已启用 capability 名称,如 `qwen-mt`、`llm`、`deepl`、`nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh` | +| `scene` | string | N | 翻译场景参数,与 `model` 配套使用;当前标准值为 `sku_name`、`ecommerce_search_query`、`general` | + +说明: +- 外部接口不接受 `prompt`;LLM prompt 由服务端按 `scene` 自动生成。 +- 传入未定义的 `scene` 或未启用的 `model` 会返回 `400`。 + +**SKU 名称场景选型建议**: +- 批量 SKU 名称翻译,优先考虑本地大吞吐方案时,可使用 `"model": "nllb-200-distilled-600m"`(该模型"scene":参数无效)。 +- 如果目标是更高质量,且可以接受更慢速度与额外 LLM API 费用,可使用 `"model": "llm"` + `"scene": "sku_name"`。 +- 如果是en-zh互译、期待更高的速度,可以考虑`opus-mt-zh-en` / `opus-mt-en-zh`。(质量未详细评测,一些文章说比blib-200-600m更好,但是我看了些case感觉要差不少) + +**实时翻译选型建议**: +- 在线 query 翻译如果只是 `en/zh` 互译,优先使用 `opus-mt-zh-en` 或 `opus-mt-en-zh`,它们是当前已测本地模型里延迟最低的一档。 +- 如果涉及其他语言,或对质量要求高于本地轻量模型,优先考虑 `deepl`。 +- `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`。 + +**Batch Size / 调用方式建议**: +- 本接口支持 `text: string[]`;离线或批量索引翻译时,应尽量合并请求,让底层 backend 发挥批处理能力。 +- `nllb-200-distilled-600m` 在当前 `Tesla T4` 压测中,推荐配置是 `batch_size=16`、`max_new_tokens=64`、`attn_implementation=sdpa`;继续升到 `batch_size=32` 虽可能提高吞吐,但 tail latency 会明显变差。 +- 在线 query 场景可直接把“单条请求”理解为 `batch_size=1`;更关注 request latency,而不是离线吞吐。 +- `opus-mt-zh-en` / `opus-mt-en-zh` 当前生产配置也是 `batch_size=16`,适合作为中英互译的低延迟本地默认值;若走在线单条调用,同样按 `batch_size=1` 理解即可。 +- `llm` 按单条请求即可。 + +**响应**: +```json +{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "translated_text": "Product name", + "status": "success", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +当请求为列表形式时,`text` 与 `translated_text` 均为等长数组: +```json +{ + "text": ["商品名称1", "商品名称2"], + "target_lang": "en", + "source_lang": "zh", + "translated_text": ["Product name 1", "Product name 2"], + "status": "success", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +> **失败语义(批量)**:当 `text` 为列表时,如果其中某条翻译失败,对应位置返回 `null`(即 `translated_text[i] = null`),并保持数组长度与顺序不变;接口整体仍返回 `status="success"`,用于避免“部分失败”导致整批请求失败。 + +> **实现提示(可忽略)**:服务端会尽可能使用底层 backend 的批量能力(若支持),否则自动拆分逐条翻译;无论采用哪种方式,上述批量契约保持一致。 + +**完整 curl 示例**: + +中文 → 英文: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh" + }' +``` + +俄文 → 英文: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Название товара", + "target_lang": "en", + "source_lang": "ru" + }' +``` + +使用 DeepL 模型: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "model": "deepl" + }' +``` + +使用本地 OPUS 模型(中文 → 英文): +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "蓝牙耳机", + "target_lang": "en", + "source_lang": "zh", + "model": "opus-mt-zh-en", + "scene": "sku_name" + }' +``` + +使用本地 NLLB 做 SKU 名称批量翻译: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": ["商品名称1", "商品名称2", "商品名称3"], + "target_lang": "en", + "source_lang": "zh", + "model": "nllb-200-distilled-600m", + "scene": "sku_name" + }' +``` + +使用 LLM 做高质量 SKU 名称翻译: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "男士偏光飞行员太阳镜", + "target_lang": "en", + "source_lang": "zh", + "model": "llm", + "scene": "sku_name" + }' +``` + +#### 7.3.2 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6006/health" +``` + +典型响应: +```json +{ + "status": "healthy", + "service": "translation", + "default_model": "llm", + "default_scene": "general", + "available_models": ["qwen-mt", "llm", "opus-mt-zh-en"], + "enabled_capabilities": ["qwen-mt", "llm", "opus-mt-zh-en"], + "loaded_models": ["llm"] +} +``` + +### 7.4 内容理解字段生成(Indexer 服务内) + +内容理解字段生成接口部署在 **Indexer 服务**(默认端口 6004)内,与「翻译、向量化」等独立端口微服务并列,供采用**微服务组合**方式的 indexer 调用。 + +- **Base URL**: Indexer 服务地址,如 `http://localhost:6004` +- **路径**: `POST /indexer/enrich-content` +- **说明**: 根据商品标题批量生成 `qanchors`、`semantic_attributes`、`tags`,用于拼装 ES 文档。内部使用大模型(需配置 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存;单次最多 50 条,建议批量调用以提升效率。 + +请求/响应格式、示例及错误码见 [-05-索引接口(Indexer)](./搜索API对接指南-05-索引接口(Indexer).md#58-内容理解字段生成接口)。 + +--- + diff --git a/docs/搜索API对接指南-08-数据模型与字段速查.md b/docs/搜索API对接指南-08-数据模型与字段速查.md new file mode 100644 index 0000000..9864355 --- /dev/null +++ b/docs/搜索API对接指南-08-数据模型与字段速查.md @@ -0,0 +1,97 @@ +# 搜索API对接指南-08-数据模型与字段速查 + +本篇覆盖原文第 9 章:商品字段定义、字段类型速查、常用字段列表、支持的分析器。 + +## 9. 数据模型 + +### 9.1 商品字段定义 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| `tenant_id` | keyword | 租户ID(多租户隔离) | +| `spu_id` | keyword | SPU ID | +| `title.` | object/text | 商品标题(多语言对象,如 `title.zh`, `title.en`) | +| `brief.` | object/text | 商品短描述(多语言对象,如 `brief.zh`, `brief.en`) | +| `description.` | object/text | 商品详细描述(多语言对象,如 `description.zh`, `description.en`) | +| `vendor.` | object/text | 供应商/品牌(多语言对象,且带 keyword 子字段,如 `vendor.zh.keyword`) | +| `category_path.` | object/text | 类目路径(多语言对象,用于搜索,如 `category_path.zh`) | +| `category_name_text.` | object/text | 类目名称(多语言对象,用于搜索,如 `category_name_text.zh`) | +| `category_id` | keyword | 类目ID | +| `category_name` | keyword | 类目名称(用于过滤) | +| `category_level` | integer | 类目层级 | +| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) | +| `tags` | keyword | 标签(数组) | +| `specifications` | nested | 规格(嵌套对象数组) | +| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 | +| `min_price`, `max_price` | float | 最低/最高价格 | +| `compare_at_price` | float | 原价 | +| `sku_prices` | float | SKU价格列表(数组) | +| `sku_weights` | long | SKU重量列表(数组) | +| `sku_weight_units` | keyword | SKU重量单位列表(数组) | +| `total_inventory` | long | 总库存 | +| `sales` | long | 销量(展示销量) | +| `skus` | nested | SKU详细信息(嵌套对象数组) | +| `create_time`, `update_time` | date | 创建/更新时间 | +| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | +| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) | + +> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。 + +### 9.2 字段类型速查 + +| 类型 | ES Mapping | 用途 | +|------|------------|------| +| `text` | `text` | 全文检索(支持中英文分析器) | +| `keyword` | `keyword` | 精确匹配、聚合、排序 | +| `integer` | `integer` | 整数 | +| `long` | `long` | 长整数 | +| `float` | `float` | 浮点数 | +| `date` | `date` | 日期时间 | +| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) | +| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) | + +### 9.3 常用字段列表 + +#### 过滤字段 + +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 +- `specifications`: 规格过滤(嵌套字段,格式见[过滤器详解](./搜索API对接指南-01-搜索接口.md#33-过滤器详解)) + +#### 范围字段 + +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +#### 排序字段 + +- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price) +- `sales`: 销量 +- `create_time`: 创建时间 +- `update_time`: 更新时间 +- `relevance_score`: 相关性分数(默认,不指定sort_by时使用) + +**注意**: 前端只需传 `price`,后端会自动处理: +- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高) +- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低) + +### 9.4 支持的分析器 + +| 分析器 | 语言 | 描述 | +|--------|------|------| +| `index_ik` | 中文 | 中文索引分析器(用于中文字段) | +| `query_ik` | 中文 | 中文查询分析器(用于中文字段) | +| `hanlp_index` ⚠️ TODO(暂不支持) | 中文 | 中文索引分析器(用于中文字段) | +| `hanlp_standard` ⚠️ TODO(暂不支持) | 中文 | 中文查询分析器(用于中文字段) | +| `english` | 英文 | 标准英文分析器(用于英文字段) | +| `lowercase` | - | 小写标准化器(用于keyword子字段) | + +--- + diff --git a/docs/搜索API对接指南-10-接口级压测脚本.md b/docs/搜索API对接指南-10-接口级压测脚本.md new file mode 100644 index 0000000..68f463c --- /dev/null +++ b/docs/搜索API对接指南-10-接口级压测脚本.md @@ -0,0 +1,61 @@ +# 搜索API对接指南-10-接口级压测脚本 + +原文第 10 章:压测脚本与用例。 + +## 10. 接口级压测脚本 + +仓库提供统一压测脚本:`scripts/perf_api_benchmark.py`,用于对以下接口做并发压测: + +- 后端搜索:`POST /search/` +- 搜索建议:`GET /search/suggestions` +- 向量服务:`POST /embed/text` +- 翻译服务:`POST /translate` +- 重排服务:`POST /rerank` + +说明:脚本对 `embed_text` 场景会校验返回向量内容有效性(必须是有限数值,不允许 `null/NaN/Inf`),不是只看 HTTP 200。 + +### 10.1 快速示例 + +```bash +# suggest 压测(tenant 162) +python scripts/perf_api_benchmark.py \ + --scenario backend_suggest \ + --tenant-id 162 \ + --duration 30 \ + --concurrency 50 + +# search 压测 +python scripts/perf_api_benchmark.py \ + --scenario backend_search \ + --tenant-id 162 \ + --duration 30 \ + --concurrency 20 + +# 全链路压测(search + suggest + embedding + translate + rerank) +python scripts/perf_api_benchmark.py \ + --scenario all \ + --tenant-id 162 \ + --duration 60 \ + --concurrency 30 \ + --output perf_reports/all.json +``` + +### 10.2 自定义用例 + +可通过 `--cases-file` 覆盖默认请求模板。示例文件: + +```bash +scripts/perf_cases.json.example +``` + +执行示例: + +```bash +python scripts/perf_api_benchmark.py \ + --scenario all \ + --tenant-id 162 \ + --cases-file scripts/perf_cases.json.example \ + --duration 60 \ + --concurrency 40 +``` + diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md deleted file mode 100644 index 7383e15..0000000 --- a/docs/搜索API对接指南.md +++ /dev/null @@ -1,2495 +0,0 @@ -# 搜索API接口对接指南 - -本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 - -## 目录 - -1. [快速开始](#快速开始) - - 1.1 [基础信息](#11-基础信息) - - 1.2 [最简单的搜索请求](#12-最简单的搜索请求) - - 1.3 [带过滤与分页的搜索](#13-带过滤与分页的搜索) - - 1.4 [开启分面的搜索](#14-开启分面的搜索) - -2. [接口概览](#接口概览) - -3. [搜索接口](#搜索接口) - - 3.1 [接口信息](#31-接口信息) - - 3.2 [请求参数](#32-请求参数) - - 3.3 [过滤器详解](#33-过滤器详解) - - 3.4 [分面配置](#34-分面配置) - - 3.5 [SKU筛选维度](#35-sku筛选维度) - - 3.6 [搜索建议接口](#37-搜索建议接口) - - 3.7 [即时搜索接口](#38-即时搜索接口) - - 3.8 [获取单个文档](#39-获取单个文档) - -4. [响应格式说明](#响应格式说明) - - 4.1 [标准响应结构](#41-标准响应结构) - - 4.2 [响应字段说明](#42-响应字段说明) - - 4.2.1 [query_info 说明](#421-query_info-说明) - - 4.3 [SpuResult字段说明](#43-spuresult字段说明) - - 4.4 [SkuResult字段说明](#44-skuresult字段说明) - - 4.5 [多语言字段说明](#45-多语言字段说明) - -5. [索引接口](#索引接口) - - 5.0 [支撑外部 indexer 的三种方式](#50-支撑外部-indexer-的三种方式) - - 5.1 [为租户创建索引](#51-为租户创建索引) - - 5.2 [全量索引接口](#52-全量索引接口) - - 5.3 [增量索引接口](#53-增量索引接口) - - 5.4 [查询文档接口](#54-查询文档接口) - - 5.5 [索引健康检查接口](#55-索引健康检查接口) - - 5.6 [文档构建接口(正式对接)](#56-文档构建接口正式对接推荐) - - 5.7 [文档构建接口(测试/自测)](#57-文档构建接口测试--自测) - - 5.8 [内容理解字段生成接口](#58-内容理解字段生成接口) - -6. [管理接口](#管理接口) - - 6.1 [健康检查](#61-健康检查) - - 6.2 [获取配置](#62-获取配置) - - 6.3 [索引统计](#63-索引统计) - -7. [微服务接口(向量、重排、翻译、内容理解)](#7-微服务接口向量重排翻译) - - 7.1 [向量服务(Embedding)](#71-向量服务embedding) - - 7.2 [重排服务(Reranker)](#72-重排服务reranker) - - 7.3 [翻译服务(Translation)](#73-翻译服务translation) - - 7.4 [内容理解字段生成(Indexer 服务内)](#74-内容理解字段生成indexer-服务内) - -8. [常见场景示例](#8-常见场景示例) - - 8.1 [基础搜索与排序](#81-基础搜索与排序) - - 8.2 [过滤搜索](#82-过滤搜索) - - 8.3 [分面搜索](#83-分面搜索) - - 8.4 [规格过滤与分面](#84-规格过滤与分面) - - 8.5 [SKU筛选](#85-sku筛选) - - 8.6 [分页查询](#87-分页查询) - -9. [数据模型](#9-数据模型) - - 9.1 [商品字段定义](#91-商品字段定义) - - 9.2 [字段类型速查](#92-字段类型速查) - - 9.3 [常用字段列表](#93-常用字段列表) - - 9.4 [支持的分析器](#94-支持的分析器) - ---- - -## 快速开始 - -### 1.1 基础信息 - -- **Base URL**: `http://43.166.252.75:6002` -- **协议**: HTTP/HTTPS -- **数据格式**: JSON -- **字符编码**: UTF-8 -- **请求方法**: POST(搜索接口) - -**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。 - -**环境与凭证**:MySQL、Redis、Elasticsearch 等外部服务的 AI 生产地址与凭证见 [QUICKSTART.md §1.6](./QUICKSTART.md#16-外部服务与-env含生产凭证)。 - -### 1.2 最简单的搜索请求 - -```bash -curl -X POST "http://43.166.252.75:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{"query": "芭比娃娃"}' -``` - -### 1.3 带过滤与分页的搜索 - -```bash -curl -X POST "http://43.166.252.75:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "芭比娃娃", - "size": 5, - "from": 10, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - }, - "create_time": { - "gte": "2020-01-01T00:00:00Z" - } - }, - "sort_by": "price", - "sort_order": "asc" - }' -``` - -### 1.4 开启分面的搜索 - -```bash -curl -X POST "http://43.166.252.75:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "芭比娃娃", - "facets": [ - {"field": "category1_name", "size": 10, "type": "terms"}, - {"field": "specifications.color", "size": 10, "type": "terms"}, - {"field": "specifications.size", "size": 10, "type": "terms"} - ], - "min_score": 0.2 - }' -``` - ---- - -## 接口概览 - -| 接口 | HTTP Method | Endpoint | 说明 | -|------|------|------|------| -| 搜索 | POST | `/search/` | 执行搜索查询 | -| 搜索建议 | GET | `/search/suggestions` | 搜索建议(自动补全/热词,多语言) | -| 即时搜索 | GET | `/search/instant` | 即时搜索预留接口(当前返回 `501 Not Implemented`) | -| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 | -| 全量索引 | POST | `/indexer/reindex` | 全量索引接口(导入数据,不删除索引,仅推荐自测使用) | -| 增量索引 | POST | `/indexer/index` | 增量索引接口(指定SPU ID列表进行索引,支持自动检测删除和显式删除,仅推荐自测使用) | -| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据(不写入ES) | -| 构建ES文档(正式对接) | POST | `/indexer/build-docs` | 基于上游提供的 MySQL 行数据构建 ES doc,不写入 ES,供 Java 等调用后自行写入 | -| 构建ES文档(测试用) | POST | `/indexer/build-docs-from-db` | 仅在测试/调试时使用,根据 `tenant_id + spu_ids` 内部查库并构建 ES doc | -| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags,供微服务组合方式使用 | -| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 | -| 健康检查 | GET | `/admin/health` | 服务健康检查 | -| 获取配置 | GET | `/admin/config` | 获取租户配置 | -| 索引统计 | GET | `/admin/stats` | 获取租户索引统计信息(需 tenant_id) | - -**微服务(独立端口或 Indexer 内,外部可直连)**: - -| 服务 | 端口 | 接口 | 说明 | -|------|------|------|------| -| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 | -| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 | -| 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) | -| 重排服务 | 6007 | `POST /rerank` | 检索结果重排 | -| 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 | - ---- - -## 搜索接口 - -### 3.1 接口信息 - -- **端点**: `POST /search/` -- **描述**: 执行文本搜索查询,支持多语言、过滤器和分面搜索 -- **租户标识**:`tenant_id` 通过 HTTP 请求头 **`X-Tenant-ID`** 传递(推荐);也可通过 URL query 参数 **`tenant_id`** 传递。**不要放在请求体中。** - -**请求示例(推荐)**: -```python -url = f"{base_url.rstrip('/')}/search/" -headers = { - "Content-Type": "application/json", - "X-Tenant-ID": "162", # 租户ID,必填 -} -response = requests.post(url, headers=headers, json={"query": "芭比娃娃"}) -``` - -### 3.2 请求参数 - -#### 完整请求体结构 - -```json -{ - "query": "string (required)", - "size": 10, - "from": 0, - "language": "zh", - "filters": {}, - "range_filters": {}, - "facets": [], - "sort_by": "string", - "sort_order": "desc", - "min_score": 0.0, - "sku_filter_dimension": ["string"], - "debug": false, - "enable_rerank": null, - "rerank_query_template": "{query}", - "rerank_doc_template": "{title}", - "user_id": "string", - "session_id": "string" -} -``` - -#### 参数详细说明 - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `query` | string | Y | - | 搜索查询字符串(统一文本检索策略) | -| `size` | integer | N | 10 | 返回结果数量(1-100) | -| `from` | integer | N | 0 | 分页偏移量(用于分页) | -| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 | -| `filters` | object | N | null | 精确匹配过滤器(见[过滤器详解](#33-过滤器详解)) | -| `range_filters` | object | N | null | 数值范围过滤器(见[过滤器详解](#33-过滤器详解)) | -| `facets` | array | N | null | 分面配置(见[分面配置](#34-分面配置)) | -| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 | -| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) | -| `min_score` | float | N | null | 最小相关性分数阈值 | -| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) | -| `debug` | boolean | N | false | 是否返回调试信息 | -| `enable_rerank` | boolean/null | N | null | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。不传/传 null 使用服务端 `rerank.enabled`(默认开启)。开启后会先对 ES TopN(`rerank_window`)重排,再按分页截取;若 `from+size>1000`,则不重排,直接按分页从 ES 返回 | -| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 | -| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 | -| `user_id` | string | N | null | 用户ID(用于个性化,预留) | -| `session_id` | string | N | null | 会话ID(用于分析,预留) | - -### 3.3 过滤器详解 - -#### 3.3.1 精确匹配过滤器 (filters) - -用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。 - -**格式**: -```json -{ - "filters": { - "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) - "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签 - "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段 - // specifications 嵌套过滤(特殊格式) - "specifications": { - "name": "color", - "value": "white" - } - } -} -``` - -**支持的值类型**: -- 字符串:精确匹配 -- 整数:精确匹配 -- 布尔值:精确匹配 -- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值) -- 对象:specifications 嵌套过滤(见下文) - -**`*_all` 语义(多值 AND)**: -- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。 -- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。 -- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。 - -**Specifications 嵌套过滤**: - -`specifications` 是嵌套字段,支持按规格名称和值进行过滤。 - -**单个规格过滤**: -```json -{ - "filters": { - "specifications": { - "name": "color", - "value": "white" - } - } -} -``` -查询规格名称为"color"且值为"white"的商品。 - -**多个规格过滤(按维度分组)**: -```json -{ - "filters": { - "specifications": [ - {"name": "color", "value": "white"}, - {"name": "size", "value": "256GB"} - ] - } -} -``` -查询同时满足所有规格的商品(color=white **且** size=256GB)。 - -**相同维度的多个值(OR 逻辑)**: -```json -{ - "filters": { - "specifications": [ - {"name": "size", "value": "3"}, - {"name": "size", "value": "4"}, - {"name": "size", "value": "5"}, - {"name": "color", "value": "green"} - ] - } -} -``` -查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。 - -**过滤逻辑说明**: -- **不同维度**(不同的 `name`)之间是 **AND** 关系(求交集) -- **相同维度**(相同的 `name`)的多个值之间是 **OR** 关系(求并集) - -**常用过滤字段**(详见[常用字段列表](#83-常用字段列表)): -- `category_name`: 类目名称 -- `category1_name`, `category2_name`, `category3_name`: 多级类目 -- `category_id`: 类目ID -- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) -- `tags`: 标签(keyword类型,支持数组) -- `option1_name`, `option2_name`, `option3_name`: 选项名称 -- `specifications`: 规格过滤(嵌套字段,格式见上文) -- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。 - -#### 3.3.2 范围过滤器 (range_filters) - -用于数值字段的范围过滤。 - -**格式**: -```json -{ - "range_filters": { - "min_price": { - "gte": 50, // 大于等于 - "lte": 200 // 小于等于 - }, - "max_price": { - "gt": 100 // 大于 - }, - "create_time": { - "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 - } - } -} -``` - -**支持的操作符**: -- `gte`: 大于等于 (>=) -- `gt`: 大于 (>) -- `lte`: 小于等于 (<=) -- `lt`: 小于 (<) - -**注意**: 至少需要指定一个操作符。 - -**常用范围字段**(详见[常用字段列表](#83-常用字段列表)): -- `min_price`: 最低价格 -- `max_price`: 最高价格 -- `compare_at_price`: 原价 -- `create_time`: 创建时间 -- `update_time`: 更新时间 - -### 3.4 分面配置 - -用于生成分面统计(分组聚合),常用于构建筛选器UI。 - -#### 3.4.1 配置格式 - -```json -{ - "facets": [ - { - "field": "category1_name", - "size": 15, - "type": "terms", - "disjunctive": false - }, - { - "field": "brand_name", - "size": 10, - "type": "terms", - "disjunctive": true - }, - { - "field": "specifications.color", - "size": 20, - "type": "terms", - "disjunctive": true - }, - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] -} -``` - -#### 3.4.2 Facet 字段说明 - -| 字段 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `field` | string | 是 | - | 分面字段名 | -| `size` | int | 否 | 10 | 返回的分面值数量(1-100) | -| `type` | string | 否 | "terms" | 分面类型:`terms`(词条聚合)或 `range`(范围聚合) | -| `disjunctive` | bool | 否 | false | 是否支持多选(disjunctive faceting)。启用后,选中该分面的过滤器时,仍会显示其他可选项 | -| `ranges` | array | 否 | null | 范围配置(仅 `type="range"` 时需要) | - -#### 3.4.3 disjunctive字段说明 - -**重要特性**: `disjunctive` 字段控制分面的行为模式。启用后,选中该分面的过滤器时,仍会显示其他可选项 - -**标准模式 (disjunctive: false)**: -- **行为**: 选中某个分面值后,该分面只显示选中的值 -- **适用场景**: 层级类目、互斥选择 -- **示例**: 类目下钻(玩具 > 娃娃 > 芭比) - -**Multi-Select 模式 (disjunctive: true)** ⭐: -- **行为**: 选中某个分面值后,该分面仍显示所有可选项 -- **适用场景**: 颜色、品牌、尺码等可切换属性 -- **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项 - -**推荐配置**: - -| 分面类型 | disjunctive | 原因 | -|---------|-------------|------| -| 颜色 | `true` | 用户需要切换颜色 | -| 品牌 | `true` | 用户需要比较品牌 | -| 尺码 | `true` | 用户需要查看其他尺码 | -| 类目 | `false` | 层级下钻 | -| 价格区间 | `false` | 互斥选择 | - -#### 3.4.4 规格分面说明 - -`specifications` 是嵌套字段,支持两种分面模式: - -**模式1:所有规格名称的分面**: -```json -{ - "facets": [ - { - "field": "specifications", - "size": 10, - "type": "terms" - } - ] -} -``` -返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 - -**模式2:指定规格名称的分面**: -```json -{ - "facets": [ - { - "field": "specifications.color", - "size": 20, - "type": "terms", - "disjunctive": true - }, - { - "field": "specifications.size", - "size": 15, - "type": "terms", - "disjunctive": true - } - ] -} -``` -只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。 - -**返回格式示例**: -```json -{ - "facets": [ - { - "field": "specifications.color", - "label": "color", - "type": "terms", - "values": [ - {"value": "white", "count": 50, "selected": true}, // ✓ selected 字段由后端标记 - {"value": "black", "count": 30, "selected": false}, - {"value": "red", "count": 20, "selected": false} - ] - }, - { - "field": "specifications.size", - "label": "size", - "type": "terms", - "values": [ - {"value": "256GB", "count": 40, "selected": false}, - {"value": "512GB", "count": 20, "selected": false} - ] - } - ] -} -``` - -### 3.5 SKU筛选维度 - -**功能说明**: -`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式(子 SKU)维度**,为字符串列表。 -在店铺的 **主题装修配置** 中,商家可以为店铺设置一个或多个子款式筛选维度(例如 `color`、`size`),前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表,用户可以通过点击不同维度值(如不同颜色)来切换展示的子款式。 -当指定 `sku_filter_dimension` 后,后端会根据店铺的这项配置,从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据:系统会按指定维度**组合**对 SKU 进行分组,每个维度组合只返回第一个 SKU(从简实现,选择该组合下的第一款),其余不在这些维度组合中的子 SKU 将不返回。 - -**支持的维度值**: -1. **直接选项字段**: `option1`、`option2`、`option3` - - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组 - -2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配 - - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组 - -**示例**: - -**按颜色筛选(假设 option1_name = "color")**: -```json -{ - "query": "芭比娃娃", - "sku_filter_dimension": ["color"] -} -``` - -**按选项1筛选**: -```json -{ - "query": "芭比娃娃", - "sku_filter_dimension": ["option1"] -} -``` - -**按颜色 + 尺寸组合筛选(假设 option1_name = "color", option2_name = "size")**: -```json -{ - "query": "芭比娃娃", - "sku_filter_dimension": ["color", "size"] -} -``` - -### 3.7 搜索建议接口 - -- **端点**: `GET /search/suggestions` -- **描述**: 返回搜索建议(自动补全/热词),支持多语言。 - -#### 查询参数 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `q` | string | Y | - | 查询字符串(至少 1 个字符) | -| `size` | integer | N | 10 | 返回建议数量(1-50) | -| `language` | string | N | `en` | 请求语言,如 `zh` / `en` / `ar` / `ru`,用于路由到对应语种 suggestion 索引 | -| `debug` | bool | N | `false` | 是否开启调试(目前主要用于排查 suggestion 排序与语言解析) | - -> **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 - -#### 响应示例 - -```json -{ - "query": "iph", - "language": "en", - "resolved_language": "en", - "suggestions": [ - { - "text": "iphone 15", - "lang": "en", - "score": 12.37, - "rank_score": 5.1, - "sources": ["query_log", "qanchor"], - "lang_source": "log_field", - "lang_confidence": 1.0, - "lang_conflict": false - } - ], - "took_ms": 12 -} -``` - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/suggestions?q=芭&size=5&language=zh" \ - -H "X-Tenant-ID: 162" -``` - -### 3.8 即时搜索接口 - -> ⚠️ 当前版本未开放该能力。接口会明确返回 `501 Not Implemented`,避免误用未完成实现。 - -- **端点**: `GET /search/instant` -- **描述**: 即时搜索预留端点,后续会在独立实现完成后开放。 - -#### 查询参数 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `q` | string | Y | - | 搜索查询(至少 2 个字符) | -| `size` | integer | N | 5 | 返回结果数量(1-20) | - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/instant?q=玩具&size=5" -``` - -#### 当前响应 - -```json -{ - "error": "/search/instant is not implemented yet. Use POST /search/ for production traffic.", - "status_code": 501 -} -``` - -### 3.9 获取单个文档 - -- **端点**: `GET /search/{doc_id}` -- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。 -- **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 - -#### 路径参数 - -| 参数 | 类型 | 描述 | -|------|------|------| -| `doc_id` | string | 商品或文档 ID | - -#### 响应示例 - -```json -{ - "id": "12345", - "source": { - "title": { - "zh": "芭比时尚娃娃" - }, - "min_price": 89.99, - "category1_name": "玩具" - } -} -``` - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162" -# 或使用 query 参数:curl "http://localhost:6002/search/12345?tenant_id=162" -``` - ---- - -## 响应格式说明 - -### 4.1 标准响应结构 - -```json -{ - "results": [ - { - "spu_id": "12345", - "title": "芭比时尚娃娃", - "brief": "高品质芭比娃娃", - "description": "详细描述...", - "vendor": "美泰", - "category": "玩具", - "category_path": "玩具/娃娃/时尚", - "category_name": "时尚", - "category_id": "cat_001", - "category_level": 3, - "category1_name": "玩具", - "category2_name": "娃娃", - "category3_name": "时尚", - "tags": ["娃娃", "玩具", "女孩"], - "price": 89.99, - "compare_at_price": 129.99, - "currency": "USD", - "image_url": "https://example.com/image.jpg", - "in_stock": true, - "sku_prices": [89.99, 99.99, 109.99], - "sku_weights": [100, 150, 200], - "sku_weight_units": ["g", "g", "g"], - "total_inventory": 500, - "option1_name": "color", - "option2_name": "size", - "option3_name": null, - "specifications": [ - {"sku_id": "sku_001", "name": "color", "value": "pink"}, - {"sku_id": "sku_001", "name": "size", "value": "standard"} - ], - "skus": [ - { - "sku_id": "67890", - "price": 89.99, - "compare_at_price": 129.99, - "sku": "BARBIE-001", - "stock": 100, - "weight": 0.1, - "weight_unit": "kg", - "option1_value": "pink", - "option2_value": "standard", - "option3_value": null, - "image_src": "https://example.com/sku1.jpg" - } - ], - "relevance_score": 8.5 - } - ], - "total": 118, - "max_score": 8.5, - "facets": [ - { - "field": "category1_name", - "label": "category1_name", - "type": "terms", - "values": [ - { - "value": "玩具", - "label": "玩具", - "count": 85, - "selected": false - } - ] - }, - { - "field": "specifications.color", - "label": "color", - "type": "terms", - "values": [ - { - "value": "pink", - "label": "pink", - "count": 30, - "selected": false - } - ] - } - ], - "query_info": { - "original_query": "芭比娃娃", - "query_normalized": "芭比娃娃", - "rewritten_query": "芭比娃娃", - "detected_language": "zh", - "translations": { - "en": "barbie doll" - }, - "domain": "default" - }, - "suggestions": [], - "related_searches": [], - "took_ms": 45, - "performance_info": null, - "debug_info": null -} -``` - -### 4.2 响应字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `results` | array | 搜索结果列表(SpuResult对象数组) | -| `results[].spu_id` | string | SPU ID | -| `results[].title` | string | 商品标题 | -| `results[].price` | float | 价格(min_price) | -| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) | -| `results[].relevance_score` | float | 相关性分数 | -| `total` | integer | 匹配的总文档数 | -| `max_score` | float | 最高相关性分数 | -| `facets` | array | 分面统计结果 | -| `query_info` | object | query处理信息 | -| `took_ms` | integer | 搜索耗时(毫秒) | -| `debug_info` | object/null | 调试信息,仅当请求传 `debug=true` 时返回 | - -#### 4.2.1 query_info 说明 - -`query_info` 包含本次搜索的查询解析与处理结果: - -| 子字段 | 类型 | 说明 | -|--------|------|------| -| `original_query` | string | 用户原始查询 | -| `query_normalized` | string | 归一化后的查询(去空白、大小写等预处理,用于后续解析与改写) | -| `rewritten_query` | string | 重写后的查询(同义词/词典扩展等) | -| `detected_language` | string | 检测到的查询语言(如 `zh`、`en`) | -| `translations` | object | 翻译结果,键为语言代码,值为翻译文本 | -| `domain` | string | 查询域(如 `default`、`title`、`brand` 等) | - -#### 4.2.2 debug_info 说明 - -`debug_info` 主要用于检索效果评估、融合打分分析与 bad case 排查。 - -`debug_info.query_analysis` 常见字段: - -| 子字段 | 类型 | 说明 | -|--------|------|------| -| `original_query` | string | 原始查询 | -| `query_normalized` | string | 归一化后的查询 | -| `rewritten_query` | string | 重写后的查询 | -| `detected_language` | string | 检测到的语言 | -| `translations` | object | 翻译结果 | -| `query_text_by_lang` | object | 实际参与检索的多语言 query 文本 | -| `search_langs` | array[string] | 实际参与检索的语言列表 | -| `supplemental_search_langs` | array[string] | 因 mixed query 补入的附加语言列表 | -| `has_vector` | boolean | 是否生成了向量 | - -`debug_info.per_result[]` 常见字段: - -| 子字段 | 类型 | 说明 | -|--------|------|------| -| `spu_id` | string | 结果 SPU ID | -| `es_score` | float | ES 原始 `_score` | -| `rerank_score` | float | 重排分数 | -| `text_score` | float | 文本相关性大分(由 `base_query` / `base_query_trans_*` / `fallback_original_query_*` 聚合而来) | -| `text_source_score` | float | `base_query` 分数 | -| `text_translation_score` | float | `base_query_trans_*` 里的最大分数 | -| `text_fallback_score` | float | `fallback_original_query_*` 里的最大分数 | -| `text_primary_score` | float | 文本大分中的主证据部分 | -| `text_support_score` | float | 文本大分中的辅助证据部分 | -| `knn_score` | float | `knn_query` 分数 | -| `fused_score` | float | 最终融合分数 | -| `matched_queries` | object/array | ES named queries 命中详情 | - -### 4.3 SpuResult字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `spu_id` | string | SPU ID | -| `title` | string | 商品标题(根据language参数自动选择 `title.zh` 或 `title.en`) | -| `brief` | string | 商品短描述(根据language参数自动选择) | -| `description` | string | 商品详细描述(根据language参数自动选择) | -| `vendor` | string | 供应商/品牌(根据language参数自动选择) | -| `category` | string | 类目(兼容字段,等同于category_name) | -| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) | -| `category_name` | string | 类目名称(展示用,根据language参数自动选择) | -| `category_id` | string | 类目ID | -| `category_level` | integer | 类目层级(1/2/3) | -| `category1_name` | string | 一级类目名称 | -| `category2_name` | string | 二级类目名称 | -| `category3_name` | string | 三级类目名称 | -| `tags` | array[string] | 标签列表 | -| `price` | float | 价格(min_price) | -| `compare_at_price` | float | 原价 | -| `currency` | string | 货币单位(默认USD) | -| `image_url` | string | 主图URL | -| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | -| `sku_prices` | array[float] | 所有SKU价格列表 | -| `sku_weights` | array[integer] | 所有SKU重量列表 | -| `sku_weight_units` | array[string] | 所有SKU重量单位列表 | -| `total_inventory` | integer | 总库存 | -| `sales` | integer | 销量(展示销量) | -| `option1_name` | string | 选项1名称(如"color") | -| `option2_name` | string | 选项2名称(如"size") | -| `option3_name` | string | 选项3名称 | -| `specifications` | array[object] | 规格列表(与ES specifications字段对应) | -| `skus` | array | SKU 列表 | -| `relevance_score` | float | 相关性分数(默认为 ES 原始分数;当开启 AI 搜索时为融合后的最终分数) | - -### 4.4 SkuResult字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `sku_id` | string | SKU ID | -| `price` | float | 价格 | -| `compare_at_price` | float | 原价 | -| `sku` | string | SKU编码(sku_code) | -| `stock` | integer | 库存数量 | -| `weight` | float | 重量 | -| `weight_unit` | string | 重量单位 | -| `option1_value` | string | 选项1取值(如color值) | -| `option2_value` | string | 选项2取值(如size值) | -| `option3_value` | string | 选项3取值 | -| `image_src` | string | SKU图片地址 | - -### 4.5 多语言字段说明 - -- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段 -- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 -- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 - ---- - -## 索引接口 - -本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口: - -| 接口 | 方法 | 路径 | 说明 | -|------|------|------|------| -| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) | -| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 | -| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES | -| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES | -| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 | -| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags(供微服务组合方式使用) | -| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 | - -#### 5.0 支撑外部 indexer 的三种方式 - -本服务对**外部 indexer 程序**(如 Java 索引系统)提供三种对接方式,可按需选择: - -| 方式 | 说明 | 适用场景 | -|------|------|----------| -| **1)doc 填充接口** | 调用 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db`,由本服务基于 MySQL 行数据构建完整 ES 文档(含多语言、向量、规格等),**不写入 ES**,由调用方自行写入。 | 希望一站式拿到 ES-ready doc,由己方控制写 ES 的时机与索引名。 | -| **2)微服务组合** | 单独调用**翻译**、**向量化**、**内容理解字段生成**等接口,由 indexer 程序自己组装 doc 并写入 ES。翻译与向量化为独立微服务(见第 7 节);内容理解为 Indexer 服务内接口 `POST /indexer/enrich-content`。 | 需要灵活编排、或希望将 LLM/向量等耗时步骤与主链路解耦(如异步补齐 qanchors/tags)。 | -| **3)本服务直接写 ES** | 调用全量索引 `POST /indexer/reindex`、增量索引 `POST /indexer/index`(指定 SPU ID 列表),由本服务从 MySQL 拉数并直接写入 ES。 | 自建运维、联调或不需要由 Java 写 ES 的场景。 | - -- **方式 1** 与 **方式 2** 下,ES 的写入方均为外部 indexer(或 Java),职责清晰。 -- **方式 3** 下,本服务同时负责读库、构建 doc 与写 ES。 - -### 5.1 为租户创建索引 - -为租户创建索引需要两个步骤: - -1. **创建索引结构**(可选,仅在需要更新 mapping 或在新环境首次创建时执行) - - 使用脚本创建 ES 索引结构(基于 `mappings/search_products.json`) - - 如果索引已存在,会提示用户确认(会删除现有数据) - -2. **导入数据**(必需) - - 使用全量索引接口 `/indexer/reindex` 导入数据 - -**创建索引结构(支持多环境 namespace)**: - -```bash -# 以 UAT 环境为例: -# 1. 准备 UAT 环境的 .env(包含 UAT 的 ES_HOST/DB_HOST 等) -# 2. 设置环境前缀(也可以直接在 .env 中配置): -export RUNTIME_ENV=uat -export ES_INDEX_NAMESPACE=uat_ - -# 3. 为 tenant_id=170 创建索引结构 -./scripts/create_tenant_index.sh 170 -``` - -脚本会自动从项目根目录的 `.env` 文件加载 ES 配置,并根据 `ES_INDEX_NAMESPACE` 创建: - -- prod 环境(ES_INDEX_NAMESPACE 为空):`search_products_tenant_170` -- UAT 环境(ES_INDEX_NAMESPACE=uat_):`uat_search_products_tenant_170` - -**注意事项**: -- ⚠️ 如果索引已存在,脚本会提示确认,确认后会删除现有数据 -- 创建索引后,**必须**调用 `/indexer/reindex` 导入数据 -- 如果只是更新数据而不需要修改索引结构,直接使用 `/indexer/reindex` 即可 - ---- - -### 5.2 全量索引接口 - -- **端点**: `POST /indexer/reindex` -- **描述**: 全量索引,将指定租户的所有SPU数据导入到ES索引(不会删除现有索引)。**推荐仅用于自测/运维场景**;生产环境下更推荐由 Java 等上游控制调度与写 ES。 - -#### 请求参数 - -```json -{ - "tenant_id": "162", - "batch_size": 500 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `tenant_id` | string | Y | - | 租户ID | -| `batch_size` | integer | N | 500 | 批量导入大小 | - -#### 响应格式 - -**成功响应(200 OK)**(示例,实际 `index_name` 会带上 tenant 和环境前缀): -```json -{ - "success": true, - "total": 1000, - "indexed": 1000, - "failed": 0, - "elapsed_time": 12.34, - "index_name": "search_products_tenant_162", - "tenant_id": "162" -} -``` - -**错误响应**: -- `400 Bad Request`: 参数错误 -- `503 Service Unavailable`: 服务未初始化 - -#### 请求示例 - -**全量索引(不会删除现有索引)**: -```bash -curl -X POST "http://localhost:6004/indexer/reindex" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "batch_size": 500 - }' -``` - -**查看日志**: -```bash -# 查看API日志(包含索引操作日志) -tail -f logs/api.log - -# 或者查看所有日志文件 -tail -f logs/*.log -``` - -> ⚠️ **重要提示**:如需 **创建索引结构**,请参考 [5.1 为租户创建索引](#51-为租户创建索引) 章节,使用 `./scripts/create_tenant_index.sh `。创建后需要调用 `/indexer/reindex` 导入数据。 - -**查看索引日志**: - -索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON 格式),包括: -- 请求开始和结束时间 -- 租户ID、SPU ID、操作类型 -- 每个SPU的处理状态 -- ES批量写入结果 -- 成功/失败统计和详细错误信息 - -```bash -# 实时查看索引日志(包含全量和增量索引的所有操作) -tail -f logs/indexer.log - -# 使用 grep 查询(简单方式) -# 查看全量索引日志 -grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100 - -# 查看增量索引日志 -grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100 - -# 查看特定租户的索引日志 -grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100 - -# 使用 jq 查询(推荐,更精确的 JSON 查询) -# 安装 jq: sudo apt-get install jq 或 brew install jq - -# 查看全量索引日志 -cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100 - -# 查看增量索引日志 -cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100 - -# 查看特定租户的索引日志 -cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100 - -# 查看失败的索引操作 -cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)' - -# 查看特定SPU的处理日志 -cat logs/indexer.log | jq 'select(.spu_id == "123")' - -# 查看最近的索引请求统计 -cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}' -``` - -### 5.3 增量索引接口 - -- **端点**: `POST /indexer/index` -- **描述**: 增量索引接口,根据指定的SPU ID列表进行索引,直接将数据写入ES。用于增量更新指定商品。**推荐仅作为内部/调试入口**;正式对接建议改用 `/indexer/build-docs`,由上游写 ES。 - -**删除说明**: -- `spu_ids`中的SPU:如果数据库`deleted=1`,自动从ES删除,响应状态为`deleted` -- `delete_spu_ids`中的SPU:直接删除,响应状态为`deleted`、`not_found`或`failed` - -#### 请求参数 - -```json -{ - "tenant_id": "162", - "spu_ids": ["123", "456", "789"], - "delete_spu_ids": ["100", "101"] -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `tenant_id` | string | Y | 租户ID | -| `spu_ids` | array[string] | N | SPU ID列表(1-100个),要索引的SPU。如果为空,则只执行删除操作 | -| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表(1-100个),可选。无论数据库状态如何,都会从ES中删除这些SPU | - -**注意**: -- `spu_ids` 和 `delete_spu_ids` 不能同时为空 -- 每个列表最多支持100个SPU ID -- 如果SPU在`spu_ids`中且数据库`deleted=1`,会自动从ES删除(自动检测删除) - -#### 响应格式 - -```json -{ - "spu_ids": [ - { - "spu_id": "123", - "status": "indexed" - }, - { - "spu_id": "456", - "status": "deleted" - }, - { - "spu_id": "789", - "status": "failed", - "msg": "SPU not found (unexpected)" - } - ], - "delete_spu_ids": [ - { - "spu_id": "100", - "status": "deleted" - }, - { - "spu_id": "101", - "status": "not_found" - }, - { - "spu_id": "102", - "status": "failed", - "msg": "Failed to delete from ES: Connection timeout" - } - ], - "total": 6, - "success_count": 4, - "failed_count": 2, - "elapsed_time": 1.23, - "index_name": "search_products", - "tenant_id": "162" -} -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| `spu_ids` | array | spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | -| `spu_ids[].status` | string | 状态:`indexed`(已索引)、`deleted`(已删除,自动检测)、`failed`(失败) | -| `spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | -| `delete_spu_ids` | array | delete_spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | -| `delete_spu_ids[].status` | string | 状态:`deleted`(已删除)、`not_found`(ES中不存在)、`failed`(失败) | -| `delete_spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | -| `total` | integer | 总处理数量(spu_ids数量 + delete_spu_ids数量) | -| `success_count` | integer | 成功数量(indexed + deleted + not_found) | -| `failed_count` | integer | 失败数量 | -| `elapsed_time` | float | 耗时(秒) | -| `index_name` | string | 索引名称 | -| `tenant_id` | string | 租户ID | - -**状态说明**: -- `spu_ids` 的状态: - - `indexed`: SPU已成功索引到ES - - `deleted`: SPU在数据库中被标记为deleted=1,已从ES删除(自动检测) - - `failed`: 处理失败,会包含`msg`字段说明失败原因 -- `delete_spu_ids` 的状态: - - `deleted`: SPU已从ES成功删除 - - `not_found`: SPU在ES中不存在(也算成功,可能已经被删除过) - - `failed`: 删除失败,会包含`msg`字段说明失败原因 - -#### 请求示例 - -**示例1:普通增量索引(自动检测删除)**: -```bash -curl -X POST "http://localhost:6004/indexer/index" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": ["123", "456", "789"] - }' -``` -说明:如果SPU 456在数据库中`deleted=1`,会自动从ES删除,在响应中`spu_ids`列表里456的状态为`deleted`。 - -**示例2:显式删除(批量删除)**: -```bash -curl -X POST "http://localhost:6004/indexer/index" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": ["123", "456"], - "delete_spu_ids": ["100", "101", "102"] - }' -``` -说明:SPU 100、101、102会被显式删除,无论数据库状态如何。 - -**示例3:仅删除(不索引)**: -```bash -curl -X POST "http://localhost:6004/indexer/index" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": [], - "delete_spu_ids": ["100", "101"] - }' -``` -说明:只执行删除操作,不进行索引。 - -**示例4:混合操作(索引+删除)**: -```bash -curl -X POST "http://localhost:6004/indexer/index" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": ["123", "456", "789"], - "delete_spu_ids": ["100", "101"] - }' -``` -说明:同时执行索引和删除操作。 - -#### 日志说明 - -增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON格式),包括: -- 请求开始和结束时间 -- 每个SPU的处理状态(获取、转换、索引、删除) -- ES批量写入结果 -- 成功/失败统计 -- 详细的错误信息 - -日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。 - -### 5.4 查询文档接口 - -- **端点**: `POST /indexer/documents` -- **描述**: 查询文档接口,根据SPU ID列表获取ES文档数据(**不写入ES**)。用于查看、调试或验证SPU数据。 - -#### 请求参数 - -```json -{ - "tenant_id": "162", - "spu_ids": ["123", "456", "789"] -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `tenant_id` | string | Y | 租户ID | -| `spu_ids` | array[string] | Y | SPU ID列表(1-100个) | - -#### 响应格式 - -```json -{ - "success": [ - { - "spu_id": "123", - "document": { - "tenant_id": "162", - "spu_id": "123", - "title": { - "zh": "商品标题" - }, - ... - } - }, - { - "spu_id": "456", - "document": {...} - } - ], - "failed": [ - { - "spu_id": "789", - "error": "SPU not found or deleted" - } - ], - "total": 3, - "success_count": 2, - "failed_count": 1 -} -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | array | 成功获取的SPU列表,每个元素包含 `spu_id` 和 `document`(完整的ES文档数据) | -| `failed` | array | 失败的SPU列表,每个元素包含 `spu_id` 和 `error`(失败原因) | -| `total` | integer | 总SPU数量 | -| `success_count` | integer | 成功数量 | -| `failed_count` | integer | 失败数量 | - -#### 请求示例 - -**单个SPU查询**: -```bash -curl -X POST "http://localhost:6004/indexer/documents" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": ["123"] - }' -``` - -**批量SPU查询**: -```bash -curl -X POST "http://localhost:6004/indexer/documents" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "spu_ids": ["123", "456", "789"] - }' -``` - -#### 与 `/indexer/index` 的区别 - -| 接口 | 功能 | 是否写入ES | 返回内容 | -|------|------|-----------|----------| -| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 | -| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 | - -**使用场景**: -- `/indexer/documents`:用于查看、调试或验证SPU数据,不修改ES索引 -- `/indexer/index`:用于实际的增量索引操作,将更新的SPU数据同步到ES - -### 5.5 索引健康检查接口 - -- **端点**: `GET /indexer/health` -- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致) - -#### 响应格式 - -```json -{ - "status": "available", - "database": "connected", - "preloaded_data": { - "category_mappings": 150 - } -} -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) | -| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` | -| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 | - -#### 请求示例 - -```bash -curl -X GET "http://localhost:6004/indexer/health" -``` - -### 5.6 文档构建接口(正式对接推荐) - -#### 5.6.1 `POST /indexer/build-docs` - -- **描述**: - 基于调用方(通常是 Java 索引程序)提供的 **MySQL 行数据** 构建 ES 文档(doc),**不写入 ES**。 - 由本服务负责“如何构建 doc”(多语言、翻译、向量、规格聚合等),由调用方负责“何时调度 + 如何写 ES”。 - -#### 请求参数 - -```json -{ - "tenant_id": "170", - "items": [ - { - "spu": { "id": 223167, "tenant_id": 170, "title": "..." }, - "skus": [ - { "id": 3988393, "spu_id": 223167, "price": 25.99, "compare_at_price": 25.99 } - ], - "options": [] - } - ] -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `tenant_id` | string | Y | 租户 ID | -| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** | - -> `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。 - -#### 请求示例(完整 curl) - -> 完整请求体参考 `scripts/test_build_docs_api.py` 中的 `build_sample_request()`。 - -```bash -# 单条 SPU 示例(含 spu、skus、options) -curl -X POST "http://localhost:6004/indexer/build-docs" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "162", - "items": [ - { - "spu": { - "id": 10001, - "tenant_id": "162", - "title": "测试T恤 纯棉短袖", - "brief": "舒适纯棉,多色可选", - "description": "这是一款适合日常穿着的纯棉T恤,透气吸汗。", - "vendor": "测试品牌", - "category": "服装/上衣/T恤", - "category_id": 100, - "category_level": 2, - "category_path": "服装/上衣/T恤", - "fake_sales": 1280, - "image_src": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg", - "tags": "T恤,纯棉,短袖,夏季", - "create_time": "2024-01-01T00:00:00Z", - "update_time": "2024-01-01T00:00:00Z" - }, - "skus": [ - { - "id": 20001, - "spu_id": 10001, - "price": 99.0, - "compare_at_price": 129.0, - "sku": "SKU-TSHIRT-001", - "inventory_quantity": 50, - "option1": "黑色", - "option2": "M", - "option3": null - }, - { - "id": 20002, - "spu_id": 10001, - "price": 99.0, - "compare_at_price": 129.0, - "sku": "SKU-TSHIRT-002", - "inventory_quantity": 30, - "option1": "白色", - "option2": "L", - "option3": null - } - ], - "options": [ - {"id": 1, "position": 1, "name": "颜色"}, - {"id": 2, "position": 2, "name": "尺码"} - ] - } - ] -}' -``` - -生产环境替换 `localhost:6004` 为实际 Indexer 地址,如 `http://43.166.252.75:6004`。 - -#### 响应示例(节选) - -```json -{ - "tenant_id": "170", - "docs": [ - { - "tenant_id": "170", - "spu_id": "223167", - "title": { "en": "...", "zh": "..." }, - "tags": ["Floerns", "Clothing", "Shoes & Jewelry"], - "skus": [ - { - "sku_id": "3988393", - "price": 25.99, - "compare_at_price": 25.99, - "stock": 100 - } - ], - "min_price": 25.99, - "max_price": 25.99, - "compare_at_price": 25.99, - "total_inventory": 100, - "title_embedding": [/* 1024 维向量 */] - // 其余字段与 mappings/search_products.json 一致 - } - ], - "total": 1, - "success_count": 1, - "failed_count": 0, - "failed": [] -} -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| `tenant_id` | string | 租户 ID | -| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 | -| `total` | integer | 请求的 items 总数 | -| `success_count` | integer | 成功构建数量 | -| `failed_count` | integer | 失败数量 | -| `failed` | array | 失败项列表,每项含 `spu_id`、`error` | - -#### 使用建议 - -- **生产环境推荐流程**: - 1. Java 根据业务逻辑决定哪些 SPU 需要(全量/增量)处理; - 2. Java 从 MySQL 查询 SPU/SKU/Option 行,拼成 `items`; - 3. 调用 `/indexer/build-docs` 获取 ES-ready `docs`; - 4. Java 使用自己的 ES 客户端写入 `search_products_tenant_{tenant_id}`。 - -### 5.7 文档构建接口(测试 / 自测) - -#### 5.7.1 `POST /indexer/build-docs-from-db` - -- **描述**: - 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。** - -#### 请求参数 - -```json -{ - "tenant_id": "170", - "spu_ids": ["223167", "223168"] -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `tenant_id` | string | Y | 租户 ID | -| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** | - -#### 响应格式 - -与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。 - -#### 请求示例 - -```bash -curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \ - -H "Content-Type: application/json" \ - -d '{"tenant_id": "170", "spu_ids": ["223167"]}' -``` - -返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。 - -### 5.8 内容理解字段生成接口 - -- **端点**: `POST /indexer/enrich-content` -- **描述**: 根据商品内容信息批量生成 **qanchors**(锚文本)、**semantic_attributes**(语义属性)、**tags**(细分标签),供外部 indexer 在「微服务组合」方式下自行拼装 doc 时使用。请求以 `items[]` 传入商品内容字段(必填/可选见下表)。内部逻辑与 `indexer.product_enrich` 一致,支持多语言与 Redis 缓存;单次请求在线程池中执行,避免阻塞其他接口。 - -#### 请求参数 - -```json -{ - "tenant_id": "170", - "items": [ - { - "spu_id": "223167", - "title": "纯棉短袖T恤 夏季男装", - "brief": "夏季透气纯棉短袖,舒适亲肤", - "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", - "image_url": "https://example.com/images/223167.jpg" - }, - { - "spu_id": "223168", - "title": "12PCS Dolls with Bottles", - "image_url": "https://example.com/images/223168.jpg" - } - ], - "languages": ["zh", "en"] -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `tenant_id` | string | Y | - | 租户 ID。目前仅用于记录日志,不产生实际作用| -| `items` | array | Y | - | 待分析列表;**单次最多 50 条** | -| `languages` | array[string] | N | `["zh", "en"]` | 目标语言,需在支持范围内:`zh`、`en`、`de`、`ru`、`fr` | - -`items[]` 字段说明: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `spu_id` | string | Y | SPU ID,用于回填结果;目前仅用于记录日志,不产生实际作用| -| `title` | string | Y | 商品标题 | -| `image_url` | string | N | 商品主图 URL;当前会参与内容缓存键,后续可用于图像/多模态内容理解 | -| `brief` | string | N | 商品简介/短描述;当前会参与内容缓存键 | -| `description` | string | N | 商品详情/长描述;当前会参与内容缓存键 | - -缓存说明: - -- 内容缓存键仅由 `target_lang + items[]` 中会影响内容理解结果的输入文本构成,目前包括:`title`、`brief`、`description`、`image_url` 的规范化内容 hash。 -- `tenant_id`、`spu_id` 只用于请求归属与结果回填,不参与缓存键。 -- 因此,输入内容不变时可跨请求直接命中缓存;任一输入字段变化时,会自然落到新的缓存 key。 - -批量请求建议: -- **全量**:强烈建议 尽可能 **20 个 SPU/doc** 攒成一个批次后再请求一次。 -- **增量**:可按时效要求设置时间窗口(例如 **5 分钟**),在窗口内尽可能攒到 **20 个**;达到 20 或窗口到期就发送一次请求。 -- 允许超过20,服务内部会拆分成小批次逐个处理。也允许小于20,但是将造成费用和耗时的成本上升,特别是每次请求一个doc的情况。 - -#### 响应格式 - -```json -{ - "tenant_id": "170", - "total": 2, - "results": [ - { - "spu_id": "223167", - "qanchors": { - "zh": "短袖T恤,纯棉,男装,夏季", - "en": "cotton t-shirt, short sleeve, men, summer" - }, - "semantic_attributes": [ - { "lang": "zh", "name": "tags", "value": "纯棉" }, - { "lang": "zh", "name": "usage_scene", "value": "日常" }, - { "lang": "en", "name": "tags", "value": "cotton" } - ], - "tags": ["纯棉", "短袖", "男装", "cotton", "short sleeve"] - }, - { - "spu_id": "223168", - "qanchors": { "en": "dolls, toys, 12pcs" }, - "semantic_attributes": [], - "tags": ["dolls", "toys"] - } - ] -} -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| `results` | array | 与请求 `items` 一一对应,每项含 `spu_id`、`qanchors`、`semantic_attributes`、`tags` | -| `results[].qanchors` | object | 按语言键的锚文本(逗号分隔短语),可写入 ES 文档的 `qanchors.{lang}` | -| `results[].semantic_attributes` | array | 语义属性列表,每项为 `{ "lang", "name", "value" }`,可写入 ES 的 `semantic_attributes` nested 字段 | -| `results[].tags` | array | 从语义属性中抽取的 `name=tags` 的 value 集合,可与业务原有 `tags` 合并后写入 ES 的 `tags` 字段 | -| `results[].error` | string | 若该条处理失败(如 LLM 异常),会在此字段返回错误信息 | - -**错误响应**: -- `400`: `items` 为空或超过 50 条 -- `503`: 未配置 `DASHSCOPE_API_KEY`,内容理解服务不可用 - -#### 请求示例 - -```bash -curl -X POST "http://localhost:6004/indexer/enrich-content" \ - -H "Content-Type: application/json" \ - -d '{ - "tenant_id": "170", - "items": [ - { - "spu_id": "223167", - "title": "纯棉短袖T恤 夏季男装", - "brief": "夏季透气纯棉短袖,舒适亲肤", - "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", - "image_url": "https://example.com/images/223167.jpg" - } - ], - "languages": ["zh", "en"] - }' -``` - ---- - -## 管理接口 - -### 6.1 健康检查 - -- **端点**: `GET /admin/health` -- **描述**: 检查服务与依赖(如 Elasticsearch)状态。 - -```json -{ - "status": "healthy", - "elasticsearch": "connected", - "tenant_id": "tenant1" -} -``` - -### 6.2 获取配置 - -- **端点**: `GET /admin/config` -- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。 - -```json -{ - "tenant_id": "tenant1", - "tenant_name": "Tenant1 Test Instance", - "es_index_name": "search_tenant1", - "num_fields": 20, - "num_indexes": 4, - "supported_languages": ["zh", "en", "ru"], - "spu_enabled": false -} -``` - -### 6.3 索引统计 - -- **端点**: `GET /admin/stats` -- **描述**: 获取指定租户索引文档数量与磁盘大小,方便监控。 -- **租户标识**:通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递(必填)。 - -```json -{ - "tenant_id": "162", - "index_name": "search_products_tenant_162", - "document_count": 10000, - "size_mb": 523.45 -} -``` - ---- - -## 7. 微服务接口(向量、重排、翻译) - -以下三个微服务独立部署,**外部系统可直接调用**。它们被搜索后端(6002)和索引服务(6004)内部使用,也可供其他业务系统直接对接。 - -| 服务 | 默认端口 | Base URL | 说明 | -|------|----------|----------|------| -| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 | -| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 | -| 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) | -| 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 | - -生产环境请将 `localhost` 替换为实际服务地址。 -服务管理入口与完整启停规则见:`docs/Usage-Guide.md` -> `服务管理总览`。 - -### 7.1 向量服务(Embedding) - -- **Base URL**: - - 文本:`http://localhost:6005`(可通过 `EMBEDDING_TEXT_SERVICE_URL` 覆盖) - - 图片:`http://localhost:6008`(可通过 `EMBEDDING_IMAGE_SERVICE_URL` 覆盖) -- **启动**: - - 文本:`./scripts/start_embedding_text_service.sh` - - 图片:`./scripts/start_embedding_image_service.sh` -- **依赖**: - - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`) - - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`) - - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU) - - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`) - - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力 - -补充说明: - -- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。 -- 服务端对 text / image 有**独立 admission control**: - - `TEXT_MAX_INFLIGHT` - - `IMAGE_MAX_INFLIGHT` -- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。 -- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。 - -#### 7.1.1 `POST /embed/text` — 文本向量化 - -将文本列表转为 1024 维向量,用于语义搜索、文档索引等。 - -**请求体**(JSON 数组): -```json -["文本1", "文本2", "文本3"] -``` - -**响应**(JSON 数组,与输入一一对应): -```json -[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] -``` - -**完整 curl 示例**: -```bash -curl -X POST "http://localhost:6005/embed/text?normalize=true" \ - -H "Content-Type: application/json" \ - -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]' -``` - -#### 7.1.2 `POST /embed/image` — 图片向量化 - -将图片 URL 或路径转为向量,用于以图搜图。 - -前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。 - -**请求体**(JSON 数组): -```json -["https://example.com/image1.jpg", "https://example.com/image2.jpg"] -``` - -**响应**(JSON 数组,与输入一一对应): -```json -[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] -``` - -**完整 curl 示例**: -```bash -curl -X POST "http://localhost:6008/embed/image?normalize=true" \ - -H "Content-Type: application/json" \ - -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' -``` - -#### 7.1.3 `GET /health` — 健康检查 - -```bash -curl "http://localhost:6005/health" -curl "http://localhost:6008/health" -``` - -返回中会包含: - -- `service_kind`:`text` / `image` / `all` -- `cache_enabled`:text/image Redis 缓存是否可用 -- `limits`:当前 inflight limit、active、rejected_total 等 -- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等 - -#### 7.1.4 `GET /ready` — 就绪检查 - -```bash -curl "http://localhost:6005/ready" -curl "http://localhost:6008/ready" -``` - -#### 7.1.5 缓存与限流说明 - -- 文本与图片都会先查 Redis 向量缓存。 -- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。 -- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。 -- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。 -- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。 - -#### 7.1.6 TEI 统一调优建议(主服务) - -使用单套主服务即可同时兼顾: -- 在线 query 向量化(低延迟,常见 `batch=1~4`) -- 索引构建向量化(高吞吐,常见 `batch=15~20`) - -统一启动(主链路): - -```bash -./scripts/start_tei_service.sh -./scripts/service_ctl.sh restart embedding -``` - -默认端口: -- TEI: `http://127.0.0.1:8080` -- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005` -- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008` - -当前主 TEI 启动默认值(已按 T4/短文本场景调优): -- `TEI_MAX_BATCH_TOKENS=4096` -- `TEI_MAX_CLIENT_BATCH_SIZE=24` -- `TEI_DTYPE=float16` - -### 7.2 重排服务(Reranker) - -- **Base URL**: `http://localhost:6007`(可通过 `RERANKER_SERVICE_URL` 覆盖) -- **启动**: `./scripts/start_reranker.sh` - -说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。 -补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确)。 - -#### 7.2.1 `POST /rerank` — 结果重排 - -根据 query 与 doc 的相关性对文档列表重新打分排序。 - -**请求体**: -```json -{ - "query": "玩具 芭比", - "docs": [ - "12PCS 6 Types of Dolls with Bottles", - "纯棉T恤 短袖 夏季" - ], - "normalize": true -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `query` | string | Y | 搜索查询 | -| `docs` | array[string] | Y | 待重排的文档列表(单次最多由服务端配置限制) | -| `normalize` | boolean | N | 是否对分数做 sigmoid 归一化,默认 true | - -**响应**: -```json -{ - "scores": [0.92, 0.15], - "meta": { - "service_elapsed_ms": 45.2, - "input_docs": 2, - "unique_docs": 2 - } -} -``` - -**完整 curl 示例**: -```bash -curl -X POST "http://localhost:6007/rerank" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 芭比", - "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"], - "top_n":386, - "normalize": true - }' - -``` - -#### 7.2.2 `GET /health` — 健康检查 - -```bash -curl "http://localhost:6007/health" -``` - -### 7.3 翻译服务(Translation) - -- **Base URL**: `http://localhost:6006`(以 `config/config.yaml -> services.translation.service_url` 为准) -- **启动**: `./scripts/start_translator.sh` - -#### 7.3.1 `POST /translate` — 文本翻译 - -支持 translator service 内所有已启用 capability,适用于商品名称、描述、query 等电商场景。当前可配置能力包括 `qwen-mt`、`llm`、`deepl` 以及本地模型 `nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh`。 - -**请求体**(支持单条字符串或字符串列表): -```json -{ - "text": "商品名称", - "target_lang": "en", - "source_lang": "zh", - "model": "qwen-mt", - "scene": "sku_name" -} -``` - -也支持批量列表形式: -```json -{ - "text": ["商品名称1", "商品名称2"], - "target_lang": "en", - "source_lang": "zh", - "model": "qwen-mt", - "scene": "sku_name" -} -``` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `text` | string \| string[] | Y | 待翻译文本,既支持单条字符串,也支持字符串列表(批量翻译) | -| `target_lang` | string | Y | 目标语言:`zh`、`en`、`ru` 等 | -| `source_lang` | string | N | 源语言。云端模型可不传;`nllb-200-distilled-600m` 建议显式传入 | -| `model` | string | N | 已启用 capability 名称,如 `qwen-mt`、`llm`、`deepl`、`nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh` | -| `scene` | string | N | 翻译场景参数,与 `model` 配套使用;当前标准值为 `sku_name`、`ecommerce_search_query`、`general` | - -说明: -- 外部接口不接受 `prompt`;LLM prompt 由服务端按 `scene` 自动生成。 -- 传入未定义的 `scene` 或未启用的 `model` 会返回 `400`。 - -**SKU 名称场景选型建议**: - -- 批量 SKU 名称翻译,优先考虑本地大吞吐方案时,可使用 `"model": "nllb-200-distilled-600m"`(该模型"scene":参数无效)。 -- 如果目标是更高质量,且可以接受更慢速度与额外 LLM API 费用,可使用 `"model": "llm"` + `"scene": "sku_name"`。 -- 如果是en-zh互译、期待更高的速度,可以考虑`opus-mt-zh-en` / `opus-mt-en-zh`。(质量未详细评测,一些文章说比blib-200-600m更好,但是我看了些case感觉要差不少) - -**实时翻译选型建议**: - -- 在线 query 翻译如果只是 `en/zh` 互译,优先使用 `opus-mt-zh-en` 或 `opus-mt-en-zh`,它们是当前已测本地模型里延迟最低的一档。 -- 如果涉及其他语言,或对质量要求高于本地轻量模型,优先考虑 `deepl`。 -- `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`。 - -**Batch Size / 调用方式建议**: - -- 本接口支持 `text: string[]`;离线或批量索引翻译时,应尽量合并请求,让底层 backend 发挥批处理能力。 -- `nllb-200-distilled-600m` 在当前 `Tesla T4` 压测中,推荐配置是 `batch_size=16`、`max_new_tokens=64`、`attn_implementation=sdpa`;继续升到 `batch_size=32` 虽可能提高吞吐,但 tail latency 会明显变差。 -- 在线 query 场景可直接把“单条请求”理解为 `batch_size=1`;更关注 request latency,而不是离线吞吐。 -- `opus-mt-zh-en` / `opus-mt-en-zh` 当前生产配置也是 `batch_size=16`,适合作为中英互译的低延迟本地默认值;若走在线单条调用,同样按 `batch_size=1` 理解即可。 -- `llm` 按单条请求即可。 - -**响应**: -```json -{ - "text": "商品名称", - "target_lang": "en", - "source_lang": "zh", - "translated_text": "Product name", - "status": "success", - "model": "qwen-mt", - "scene": "sku_name" -} -``` - -当请求为列表形式时,`text` 与 `translated_text` 均为等长数组: -```json -{ - "text": ["商品名称1", "商品名称2"], - "target_lang": "en", - "source_lang": "zh", - "translated_text": ["Product name 1", "Product name 2"], - "status": "success", - "model": "qwen-mt", - "scene": "sku_name" -} -``` - -> **失败语义(批量)**:当 `text` 为列表时,如果其中某条翻译失败,对应位置返回 `null`(即 `translated_text[i] = null`),并保持数组长度与顺序不变;接口整体仍返回 `status="success"`,用于避免“部分失败”导致整批请求失败。 - -> **实现提示(可忽略)**:服务端会尽可能使用底层 backend 的批量能力(若支持),否则自动拆分逐条翻译;无论采用哪种方式,上述批量契约保持一致。 - -**完整 curl 示例**: - -中文 → 英文: -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": "商品名称", - "target_lang": "en", - "source_lang": "zh" - }' -``` - -俄文 → 英文: -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": "Название товара", - "target_lang": "en", - "source_lang": "ru" - }' -``` - -使用 DeepL 模型: -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": "商品名称", - "target_lang": "en", - "source_lang": "zh", - "model": "deepl" - }' -``` - -使用本地 OPUS 模型(中文 → 英文): -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": "蓝牙耳机", - "target_lang": "en", - "source_lang": "zh", - "model": "opus-mt-zh-en", - "scene": "sku_name" - }' -``` - -使用本地 NLLB 做 SKU 名称批量翻译: -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": ["商品名称1", "商品名称2", "商品名称3"], - "target_lang": "en", - "source_lang": "zh", - "model": "nllb-200-distilled-600m", - "scene": "sku_name" - }' -``` - -使用 LLM 做高质量 SKU 名称翻译: -```bash -curl -X POST "http://localhost:6006/translate" \ - -H "Content-Type: application/json" \ - -d '{ - "text": "男士偏光飞行员太阳镜", - "target_lang": "en", - "source_lang": "zh", - "model": "llm", - "scene": "sku_name" - }' -``` - -#### 7.3.2 `GET /health` — 健康检查 - -```bash -curl "http://localhost:6006/health" -``` - -典型响应: -```json -{ - "status": "healthy", - "service": "translation", - "default_model": "llm", - "default_scene": "general", - "available_models": ["qwen-mt", "llm", "opus-mt-zh-en"], - "enabled_capabilities": ["qwen-mt", "llm", "opus-mt-zh-en"], - "loaded_models": ["llm"] -} -``` - -### 7.4 内容理解字段生成(Indexer 服务内) - -内容理解字段生成接口部署在 **Indexer 服务**(默认端口 6004)内,与「翻译、向量化」等独立端口微服务并列,供采用**微服务组合**方式的 indexer 调用。 - -- **Base URL**: Indexer 服务地址,如 `http://localhost:6004` -- **路径**: `POST /indexer/enrich-content` -- **说明**: 根据商品标题批量生成 `qanchors`、`semantic_attributes`、`tags`,用于拼装 ES 文档。内部使用大模型(需配置 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存;单次最多 50 条,建议批量调用以提升效率。 - -请求/响应格式、示例及错误码见 [5.8 内容理解字段生成接口](#58-内容理解字段生成接口)。 - ---- - -## 8. 常见场景示例 - -以下示例仅展示**请求体**(body);实际调用时请加上请求头 `X-Tenant-ID: <租户ID>`(或 URL 参数 `tenant_id`),参见 [3.1 接口信息](#31-接口信息)。 - -### 8.1 基础搜索与排序 - -**按价格从低到高排序**: -```json -{ - "query": "玩具", - "size": 20, - "from": 0, - "sort_by": "price", - "sort_order": "asc" -} -``` - -**按价格从高到低排序**: -```json -{ - "query": "玩具", - "size": 20, - "from": 0, - "sort_by": "price", - "sort_order": "desc" -} -``` - -**按销量从高到低排序**: -```json -{ - "query": "玩具", - "size": 20, - "from": 0, - "sort_by": "sales", - "sort_order": "desc" -} -``` - -**按默认(相关性)排序**: -```json -{ - "query": "玩具", - "size": 20, - "from": 0 -} -``` - -### 8.2 过滤搜索 - -**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 - -```json -{ - "query": "玩具", - "size": 20, - "language": "zh", - "filters": { - "category_name": "益智玩具" - }, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - } - } -} -``` - -**需求**: 搜索"手机",筛选多个品牌,价格范围 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "filters": { - "vendor.zh.keyword": ["品牌A", "品牌B"] - }, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - } - } -} -``` - -### 8.3 分面搜索 - -**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器 - -```json -{ - "query": "玩具", - "size": 20, - "language": "zh", - "facets": [ - {"field": "category1_name", "size": 15, "type": "terms"}, - {"field": "category2_name", "size": 10, "type": "terms"}, - {"field": "specifications", "size": 10, "type": "terms"} - ] -} -``` - -**需求**: 搜索"手机",获取价格区间和规格的分面统计 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "facets": [ - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - }, - { - "field": "specifications", - "size": 10, - "type": "terms" - } - ] -} -``` - -### 8.4 规格过滤与分面 - -**需求**: 搜索"手机",筛选color为"white"的商品 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "filters": { - "specifications": { - "name": "color", - "value": "white" - } - } -} -``` - -**需求**: 搜索"手机",筛选color为"white"且size为"256GB"的商品 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "filters": { - "specifications": [ - {"name": "color", "value": "white"}, - {"name": "size", "value": "256GB"} - ] - } -} -``` - -**需求**: 搜索"手机",筛选size为"3"、"4"或"5",且color为"green"的商品 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "filters": { - "specifications": [ - {"name": "size", "value": "3"}, - {"name": "size", "value": "4"}, - {"name": "size", "value": "5"}, - {"name": "color", "value": "green"} - ] - } -} -``` - -**需求**: 搜索"手机",获取所有规格的分面统计 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "facets": [ - {"field": "specifications", "size": 10, "type": "terms"} - ] -} -``` - -**需求**: 只获取"color"和"size"规格的分面统计 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "facets": [ - {"field": "specifications.color", "size": 20, "type": "terms"}, - {"field": "specifications.size", "size": 15, "type": "terms"} - ] -} -``` - -**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 - -```json -{ - "query": "手机", - "size": 20, - "language": "zh", - "filters": { - "category_name": "手机", - "specifications": { - "name": "color", - "value": "white" - } - }, - "facets": [ - {"field": "category1_name", "size": 15, "type": "terms"}, - {"field": "category2_name", "size": 10, "type": "terms"}, - {"field": "specifications.color", "size": 20, "type": "terms"}, - {"field": "specifications.size", "size": 15, "type": "terms"} - ] -} -``` - -### 8.5 SKU筛选 - -**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU - -```json -{ - "query": "芭比娃娃", - "size": 20, - "sku_filter_dimension": ["color"] -} -``` - -**说明**: -- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组 -- 每个SPU下,每种颜色只会返回第一个SKU -- 如果维度不匹配,返回所有SKU(不进行过滤) - -### 8.7 分页查询 - -**需求**: 获取第2页结果(每页20条) - -```json -{ - "query": "手机", - "size": 20, - "from": 20 -} -``` - ---- - -## 9. 数据模型 - -### 9.1 商品字段定义 - -| 字段名 | 类型 | 描述 | -|--------|------|------| -| `tenant_id` | keyword | 租户ID(多租户隔离) | -| `spu_id` | keyword | SPU ID | -| `title.` | object/text | 商品标题(多语言对象,如 `title.zh`, `title.en`) | -| `brief.` | object/text | 商品短描述(多语言对象,如 `brief.zh`, `brief.en`) | -| `description.` | object/text | 商品详细描述(多语言对象,如 `description.zh`, `description.en`) | -| `vendor.` | object/text | 供应商/品牌(多语言对象,且带 keyword 子字段,如 `vendor.zh.keyword`) | -| `category_path.` | object/text | 类目路径(多语言对象,用于搜索,如 `category_path.zh`) | -| `category_name_text.` | object/text | 类目名称(多语言对象,用于搜索,如 `category_name_text.zh`) | -| `category_id` | keyword | 类目ID | -| `category_name` | keyword | 类目名称(用于过滤) | -| `category_level` | integer | 类目层级 | -| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) | -| `tags` | keyword | 标签(数组) | -| `specifications` | nested | 规格(嵌套对象数组) | -| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 | -| `min_price`, `max_price` | float | 最低/最高价格 | -| `compare_at_price` | float | 原价 | -| `sku_prices` | float | SKU价格列表(数组) | -| `sku_weights` | long | SKU重量列表(数组) | -| `sku_weight_units` | keyword | SKU重量单位列表(数组) | -| `total_inventory` | long | 总库存 | -| `sales` | long | 销量(展示销量) | -| `skus` | nested | SKU详细信息(嵌套对象数组) | -| `create_time`, `update_time` | date | 创建/更新时间 | -| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | -| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) | - -> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。 - -### 9.2 字段类型速查 - -| 类型 | ES Mapping | 用途 | -|------|------------|------| -| `text` | `text` | 全文检索(支持中英文分析器) | -| `keyword` | `keyword` | 精确匹配、聚合、排序 | -| `integer` | `integer` | 整数 | -| `long` | `long` | 长整数 | -| `float` | `float` | 浮点数 | -| `date` | `date` | 日期时间 | -| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) | -| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) | - -### 9.3 常用字段列表 - -#### 过滤字段 - -- `category_name`: 类目名称 -- `category1_name`, `category2_name`, `category3_name`: 多级类目 -- `category_id`: 类目ID -- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) -- `tags`: 标签(keyword类型) -- `option1_name`, `option2_name`, `option3_name`: 选项名称 -- `specifications`: 规格过滤(嵌套字段,格式见[过滤器详解](#33-过滤器详解)) - -#### 范围字段 - -- `min_price`: 最低价格 -- `max_price`: 最高价格 -- `compare_at_price`: 原价 -- `create_time`: 创建时间 -- `update_time`: 更新时间 - -#### 排序字段 - -- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price) -- `sales`: 销量 -- `create_time`: 创建时间 -- `update_time`: 更新时间 -- `relevance_score`: 相关性分数(默认,不指定sort_by时使用) - -**注意**: 前端只需传 `price`,后端会自动处理: -- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高) -- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低) - -### 9.4 支持的分析器 - -| 分析器 | 语言 | 描述 | -|--------|------|------| -| `index_ik` | 中文 | 中文索引分析器(用于中文字段) | -| `query_ik` | 中文 | 中文查询分析器(用于中文字段) | -| `hanlp_index` ⚠️ TODO(暂不支持) | 中文 | 中文索引分析器(用于中文字段) | -| `hanlp_standard` ⚠️ TODO(暂不支持) | 中文 | 中文查询分析器(用于中文字段) | -| `english` | 英文 | 标准英文分析器(用于英文字段) | -| `lowercase` | - | 小写标准化器(用于keyword子字段) | - ---- - -## 10. 接口级压测脚本 - -仓库提供统一压测脚本:`scripts/perf_api_benchmark.py`,用于对以下接口做并发压测: - -- 后端搜索:`POST /search/` -- 搜索建议:`GET /search/suggestions` -- 向量服务:`POST /embed/text` -- 翻译服务:`POST /translate` -- 重排服务:`POST /rerank` - -说明:脚本对 `embed_text` 场景会校验返回向量内容有效性(必须是有限数值,不允许 `null/NaN/Inf`),不是只看 HTTP 200。 - -### 10.1 快速示例 - -```bash -# suggest 压测(tenant 162) -python scripts/perf_api_benchmark.py \ - --scenario backend_suggest \ - --tenant-id 162 \ - --duration 30 \ - --concurrency 50 - -# search 压测 -python scripts/perf_api_benchmark.py \ - --scenario backend_search \ - --tenant-id 162 \ - --duration 30 \ - --concurrency 20 - -# 全链路压测(search + suggest + embedding + translate + rerank) -python scripts/perf_api_benchmark.py \ - --scenario all \ - --tenant-id 162 \ - --duration 60 \ - --concurrency 30 \ - --output perf_reports/all.json -``` - -### 10.2 自定义用例 - -可通过 `--cases-file` 覆盖默认请求模板。示例文件: - -```bash -scripts/perf_cases.json.example -``` - -执行示例: - -```bash -python scripts/perf_api_benchmark.py \ - --scenario all \ - --tenant-id 162 \ - --cases-file scripts/perf_cases.json.example \ - --duration 60 \ - --concurrency 40 -``` diff --git a/docs/搜索API对接指南—拆分前版本存档.md b/docs/搜索API对接指南—拆分前版本存档.md new file mode 100644 index 0000000..7383e15 --- /dev/null +++ b/docs/搜索API对接指南—拆分前版本存档.md @@ -0,0 +1,2495 @@ +# 搜索API接口对接指南 + +本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 + +## 目录 + +1. [快速开始](#快速开始) + - 1.1 [基础信息](#11-基础信息) + - 1.2 [最简单的搜索请求](#12-最简单的搜索请求) + - 1.3 [带过滤与分页的搜索](#13-带过滤与分页的搜索) + - 1.4 [开启分面的搜索](#14-开启分面的搜索) + +2. [接口概览](#接口概览) + +3. [搜索接口](#搜索接口) + - 3.1 [接口信息](#31-接口信息) + - 3.2 [请求参数](#32-请求参数) + - 3.3 [过滤器详解](#33-过滤器详解) + - 3.4 [分面配置](#34-分面配置) + - 3.5 [SKU筛选维度](#35-sku筛选维度) + - 3.6 [搜索建议接口](#37-搜索建议接口) + - 3.7 [即时搜索接口](#38-即时搜索接口) + - 3.8 [获取单个文档](#39-获取单个文档) + +4. [响应格式说明](#响应格式说明) + - 4.1 [标准响应结构](#41-标准响应结构) + - 4.2 [响应字段说明](#42-响应字段说明) + - 4.2.1 [query_info 说明](#421-query_info-说明) + - 4.3 [SpuResult字段说明](#43-spuresult字段说明) + - 4.4 [SkuResult字段说明](#44-skuresult字段说明) + - 4.5 [多语言字段说明](#45-多语言字段说明) + +5. [索引接口](#索引接口) + - 5.0 [支撑外部 indexer 的三种方式](#50-支撑外部-indexer-的三种方式) + - 5.1 [为租户创建索引](#51-为租户创建索引) + - 5.2 [全量索引接口](#52-全量索引接口) + - 5.3 [增量索引接口](#53-增量索引接口) + - 5.4 [查询文档接口](#54-查询文档接口) + - 5.5 [索引健康检查接口](#55-索引健康检查接口) + - 5.6 [文档构建接口(正式对接)](#56-文档构建接口正式对接推荐) + - 5.7 [文档构建接口(测试/自测)](#57-文档构建接口测试--自测) + - 5.8 [内容理解字段生成接口](#58-内容理解字段生成接口) + +6. [管理接口](#管理接口) + - 6.1 [健康检查](#61-健康检查) + - 6.2 [获取配置](#62-获取配置) + - 6.3 [索引统计](#63-索引统计) + +7. [微服务接口(向量、重排、翻译、内容理解)](#7-微服务接口向量重排翻译) + - 7.1 [向量服务(Embedding)](#71-向量服务embedding) + - 7.2 [重排服务(Reranker)](#72-重排服务reranker) + - 7.3 [翻译服务(Translation)](#73-翻译服务translation) + - 7.4 [内容理解字段生成(Indexer 服务内)](#74-内容理解字段生成indexer-服务内) + +8. [常见场景示例](#8-常见场景示例) + - 8.1 [基础搜索与排序](#81-基础搜索与排序) + - 8.2 [过滤搜索](#82-过滤搜索) + - 8.3 [分面搜索](#83-分面搜索) + - 8.4 [规格过滤与分面](#84-规格过滤与分面) + - 8.5 [SKU筛选](#85-sku筛选) + - 8.6 [分页查询](#87-分页查询) + +9. [数据模型](#9-数据模型) + - 9.1 [商品字段定义](#91-商品字段定义) + - 9.2 [字段类型速查](#92-字段类型速查) + - 9.3 [常用字段列表](#93-常用字段列表) + - 9.4 [支持的分析器](#94-支持的分析器) + +--- + +## 快速开始 + +### 1.1 基础信息 + +- **Base URL**: `http://43.166.252.75:6002` +- **协议**: HTTP/HTTPS +- **数据格式**: JSON +- **字符编码**: UTF-8 +- **请求方法**: POST(搜索接口) + +**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。 + +**环境与凭证**:MySQL、Redis、Elasticsearch 等外部服务的 AI 生产地址与凭证见 [QUICKSTART.md §1.6](./QUICKSTART.md#16-外部服务与-env含生产凭证)。 + +### 1.2 最简单的搜索请求 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{"query": "芭比娃娃"}' +``` + +### 1.3 带过滤与分页的搜索 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{ + "query": "芭比娃娃", + "size": 5, + "from": 10, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + }, + "create_time": { + "gte": "2020-01-01T00:00:00Z" + } + }, + "sort_by": "price", + "sort_order": "asc" + }' +``` + +### 1.4 开启分面的搜索 + +```bash +curl -X POST "http://43.166.252.75:6002/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 162" \ + -d '{ + "query": "芭比娃娃", + "facets": [ + {"field": "category1_name", "size": 10, "type": "terms"}, + {"field": "specifications.color", "size": 10, "type": "terms"}, + {"field": "specifications.size", "size": 10, "type": "terms"} + ], + "min_score": 0.2 + }' +``` + +--- + +## 接口概览 + +| 接口 | HTTP Method | Endpoint | 说明 | +|------|------|------|------| +| 搜索 | POST | `/search/` | 执行搜索查询 | +| 搜索建议 | GET | `/search/suggestions` | 搜索建议(自动补全/热词,多语言) | +| 即时搜索 | GET | `/search/instant` | 即时搜索预留接口(当前返回 `501 Not Implemented`) | +| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 | +| 全量索引 | POST | `/indexer/reindex` | 全量索引接口(导入数据,不删除索引,仅推荐自测使用) | +| 增量索引 | POST | `/indexer/index` | 增量索引接口(指定SPU ID列表进行索引,支持自动检测删除和显式删除,仅推荐自测使用) | +| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据(不写入ES) | +| 构建ES文档(正式对接) | POST | `/indexer/build-docs` | 基于上游提供的 MySQL 行数据构建 ES doc,不写入 ES,供 Java 等调用后自行写入 | +| 构建ES文档(测试用) | POST | `/indexer/build-docs-from-db` | 仅在测试/调试时使用,根据 `tenant_id + spu_ids` 内部查库并构建 ES doc | +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags,供微服务组合方式使用 | +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 | +| 健康检查 | GET | `/admin/health` | 服务健康检查 | +| 获取配置 | GET | `/admin/config` | 获取租户配置 | +| 索引统计 | GET | `/admin/stats` | 获取租户索引统计信息(需 tenant_id) | + +**微服务(独立端口或 Indexer 内,外部可直连)**: + +| 服务 | 端口 | 接口 | 说明 | +|------|------|------|------| +| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 | +| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 | +| 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) | +| 重排服务 | 6007 | `POST /rerank` | 检索结果重排 | +| 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 | + +--- + +## 搜索接口 + +### 3.1 接口信息 + +- **端点**: `POST /search/` +- **描述**: 执行文本搜索查询,支持多语言、过滤器和分面搜索 +- **租户标识**:`tenant_id` 通过 HTTP 请求头 **`X-Tenant-ID`** 传递(推荐);也可通过 URL query 参数 **`tenant_id`** 传递。**不要放在请求体中。** + +**请求示例(推荐)**: +```python +url = f"{base_url.rstrip('/')}/search/" +headers = { + "Content-Type": "application/json", + "X-Tenant-ID": "162", # 租户ID,必填 +} +response = requests.post(url, headers=headers, json={"query": "芭比娃娃"}) +``` + +### 3.2 请求参数 + +#### 完整请求体结构 + +```json +{ + "query": "string (required)", + "size": 10, + "from": 0, + "language": "zh", + "filters": {}, + "range_filters": {}, + "facets": [], + "sort_by": "string", + "sort_order": "desc", + "min_score": 0.0, + "sku_filter_dimension": ["string"], + "debug": false, + "enable_rerank": null, + "rerank_query_template": "{query}", + "rerank_doc_template": "{title}", + "user_id": "string", + "session_id": "string" +} +``` + +#### 参数详细说明 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `query` | string | Y | - | 搜索查询字符串(统一文本检索策略) | +| `size` | integer | N | 10 | 返回结果数量(1-100) | +| `from` | integer | N | 0 | 分页偏移量(用于分页) | +| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 | +| `filters` | object | N | null | 精确匹配过滤器(见[过滤器详解](#33-过滤器详解)) | +| `range_filters` | object | N | null | 数值范围过滤器(见[过滤器详解](#33-过滤器详解)) | +| `facets` | array | N | null | 分面配置(见[分面配置](#34-分面配置)) | +| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 | +| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) | +| `min_score` | float | N | null | 最小相关性分数阈值 | +| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) | +| `debug` | boolean | N | false | 是否返回调试信息 | +| `enable_rerank` | boolean/null | N | null | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。不传/传 null 使用服务端 `rerank.enabled`(默认开启)。开启后会先对 ES TopN(`rerank_window`)重排,再按分页截取;若 `from+size>1000`,则不重排,直接按分页从 ES 返回 | +| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 | +| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 | +| `user_id` | string | N | null | 用户ID(用于个性化,预留) | +| `session_id` | string | N | null | 会话ID(用于分析,预留) | + +### 3.3 过滤器详解 + +#### 3.3.1 精确匹配过滤器 (filters) + +用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。 + +**格式**: +```json +{ + "filters": { + "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) + "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签 + "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段 + // specifications 嵌套过滤(特殊格式) + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +**支持的值类型**: +- 字符串:精确匹配 +- 整数:精确匹配 +- 布尔值:精确匹配 +- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值) +- 对象:specifications 嵌套过滤(见下文) + +**`*_all` 语义(多值 AND)**: +- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。 +- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。 +- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。 + +**Specifications 嵌套过滤**: + +`specifications` 是嵌套字段,支持按规格名称和值进行过滤。 + +**单个规格过滤**: +```json +{ + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` +查询规格名称为"color"且值为"white"的商品。 + +**多个规格过滤(按维度分组)**: +```json +{ + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` +查询同时满足所有规格的商品(color=white **且** size=256GB)。 + +**相同维度的多个值(OR 逻辑)**: +```json +{ + "filters": { + "specifications": [ + {"name": "size", "value": "3"}, + {"name": "size", "value": "4"}, + {"name": "size", "value": "5"}, + {"name": "color", "value": "green"} + ] + } +} +``` +查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。 + +**过滤逻辑说明**: +- **不同维度**(不同的 `name`)之间是 **AND** 关系(求交集) +- **相同维度**(相同的 `name`)的多个值之间是 **OR** 关系(求并集) + +**常用过滤字段**(详见[常用字段列表](#83-常用字段列表)): +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型,支持数组) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 +- `specifications`: 规格过滤(嵌套字段,格式见上文) +- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。 + +#### 3.3.2 范围过滤器 (range_filters) + +用于数值字段的范围过滤。 + +**格式**: +```json +{ + "range_filters": { + "min_price": { + "gte": 50, // 大于等于 + "lte": 200 // 小于等于 + }, + "max_price": { + "gt": 100 // 大于 + }, + "create_time": { + "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 + } + } +} +``` + +**支持的操作符**: +- `gte`: 大于等于 (>=) +- `gt`: 大于 (>) +- `lte`: 小于等于 (<=) +- `lt`: 小于 (<) + +**注意**: 至少需要指定一个操作符。 + +**常用范围字段**(详见[常用字段列表](#83-常用字段列表)): +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +### 3.4 分面配置 + +用于生成分面统计(分组聚合),常用于构建筛选器UI。 + +#### 3.4.1 配置格式 + +```json +{ + "facets": [ + { + "field": "category1_name", + "size": 15, + "type": "terms", + "disjunctive": false + }, + { + "field": "brand_name", + "size": 10, + "type": "terms", + "disjunctive": true + }, + { + "field": "specifications.color", + "size": 20, + "type": "terms", + "disjunctive": true + }, + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ] +} +``` + +#### 3.4.2 Facet 字段说明 + +| 字段 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `field` | string | 是 | - | 分面字段名 | +| `size` | int | 否 | 10 | 返回的分面值数量(1-100) | +| `type` | string | 否 | "terms" | 分面类型:`terms`(词条聚合)或 `range`(范围聚合) | +| `disjunctive` | bool | 否 | false | 是否支持多选(disjunctive faceting)。启用后,选中该分面的过滤器时,仍会显示其他可选项 | +| `ranges` | array | 否 | null | 范围配置(仅 `type="range"` 时需要) | + +#### 3.4.3 disjunctive字段说明 + +**重要特性**: `disjunctive` 字段控制分面的行为模式。启用后,选中该分面的过滤器时,仍会显示其他可选项 + +**标准模式 (disjunctive: false)**: +- **行为**: 选中某个分面值后,该分面只显示选中的值 +- **适用场景**: 层级类目、互斥选择 +- **示例**: 类目下钻(玩具 > 娃娃 > 芭比) + +**Multi-Select 模式 (disjunctive: true)** ⭐: +- **行为**: 选中某个分面值后,该分面仍显示所有可选项 +- **适用场景**: 颜色、品牌、尺码等可切换属性 +- **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项 + +**推荐配置**: + +| 分面类型 | disjunctive | 原因 | +|---------|-------------|------| +| 颜色 | `true` | 用户需要切换颜色 | +| 品牌 | `true` | 用户需要比较品牌 | +| 尺码 | `true` | 用户需要查看其他尺码 | +| 类目 | `false` | 层级下钻 | +| 价格区间 | `false` | 互斥选择 | + +#### 3.4.4 规格分面说明 + +`specifications` 是嵌套字段,支持两种分面模式: + +**模式1:所有规格名称的分面**: +```json +{ + "facets": [ + { + "field": "specifications", + "size": 10, + "type": "terms" + } + ] +} +``` +返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。 + +**模式2:指定规格名称的分面**: +```json +{ + "facets": [ + { + "field": "specifications.color", + "size": 20, + "type": "terms", + "disjunctive": true + }, + { + "field": "specifications.size", + "size": 15, + "type": "terms", + "disjunctive": true + } + ] +} +``` +只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。 + +**返回格式示例**: +```json +{ + "facets": [ + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + {"value": "white", "count": 50, "selected": true}, // ✓ selected 字段由后端标记 + {"value": "black", "count": 30, "selected": false}, + {"value": "red", "count": 20, "selected": false} + ] + }, + { + "field": "specifications.size", + "label": "size", + "type": "terms", + "values": [ + {"value": "256GB", "count": 40, "selected": false}, + {"value": "512GB", "count": 20, "selected": false} + ] + } + ] +} +``` + +### 3.5 SKU筛选维度 + +**功能说明**: +`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式(子 SKU)维度**,为字符串列表。 +在店铺的 **主题装修配置** 中,商家可以为店铺设置一个或多个子款式筛选维度(例如 `color`、`size`),前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表,用户可以通过点击不同维度值(如不同颜色)来切换展示的子款式。 +当指定 `sku_filter_dimension` 后,后端会根据店铺的这项配置,从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据:系统会按指定维度**组合**对 SKU 进行分组,每个维度组合只返回第一个 SKU(从简实现,选择该组合下的第一款),其余不在这些维度组合中的子 SKU 将不返回。 + +**支持的维度值**: +1. **直接选项字段**: `option1`、`option2`、`option3` + - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组 + +2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配 + - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组 + +**示例**: + +**按颜色筛选(假设 option1_name = "color")**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["color"] +} +``` + +**按选项1筛选**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["option1"] +} +``` + +**按颜色 + 尺寸组合筛选(假设 option1_name = "color", option2_name = "size")**: +```json +{ + "query": "芭比娃娃", + "sku_filter_dimension": ["color", "size"] +} +``` + +### 3.7 搜索建议接口 + +- **端点**: `GET /search/suggestions` +- **描述**: 返回搜索建议(自动补全/热词),支持多语言。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | Y | - | 查询字符串(至少 1 个字符) | +| `size` | integer | N | 10 | 返回建议数量(1-50) | +| `language` | string | N | `en` | 请求语言,如 `zh` / `en` / `ar` / `ru`,用于路由到对应语种 suggestion 索引 | +| `debug` | bool | N | `false` | 是否开启调试(目前主要用于排查 suggestion 排序与语言解析) | + +> **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 + +#### 响应示例 + +```json +{ + "query": "iph", + "language": "en", + "resolved_language": "en", + "suggestions": [ + { + "text": "iphone 15", + "lang": "en", + "score": 12.37, + "rank_score": 5.1, + "sources": ["query_log", "qanchor"], + "lang_source": "log_field", + "lang_confidence": 1.0, + "lang_conflict": false + } + ], + "took_ms": 12 +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/suggestions?q=芭&size=5&language=zh" \ + -H "X-Tenant-ID: 162" +``` + +### 3.8 即时搜索接口 + +> ⚠️ 当前版本未开放该能力。接口会明确返回 `501 Not Implemented`,避免误用未完成实现。 + +- **端点**: `GET /search/instant` +- **描述**: 即时搜索预留端点,后续会在独立实现完成后开放。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | Y | - | 搜索查询(至少 2 个字符) | +| `size` | integer | N | 5 | 返回结果数量(1-20) | + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/instant?q=玩具&size=5" +``` + +#### 当前响应 + +```json +{ + "error": "/search/instant is not implemented yet. Use POST /search/ for production traffic.", + "status_code": 501 +} +``` + +### 3.9 获取单个文档 + +- **端点**: `GET /search/{doc_id}` +- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。 +- **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。 + +#### 路径参数 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `doc_id` | string | 商品或文档 ID | + +#### 响应示例 + +```json +{ + "id": "12345", + "source": { + "title": { + "zh": "芭比时尚娃娃" + }, + "min_price": 89.99, + "category1_name": "玩具" + } +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162" +# 或使用 query 参数:curl "http://localhost:6002/search/12345?tenant_id=162" +``` + +--- + +## 响应格式说明 + +### 4.1 标准响应结构 + +```json +{ + "results": [ + { + "spu_id": "12345", + "title": "芭比时尚娃娃", + "brief": "高品质芭比娃娃", + "description": "详细描述...", + "vendor": "美泰", + "category": "玩具", + "category_path": "玩具/娃娃/时尚", + "category_name": "时尚", + "category_id": "cat_001", + "category_level": 3, + "category1_name": "玩具", + "category2_name": "娃娃", + "category3_name": "时尚", + "tags": ["娃娃", "玩具", "女孩"], + "price": 89.99, + "compare_at_price": 129.99, + "currency": "USD", + "image_url": "https://example.com/image.jpg", + "in_stock": true, + "sku_prices": [89.99, 99.99, 109.99], + "sku_weights": [100, 150, 200], + "sku_weight_units": ["g", "g", "g"], + "total_inventory": 500, + "option1_name": "color", + "option2_name": "size", + "option3_name": null, + "specifications": [ + {"sku_id": "sku_001", "name": "color", "value": "pink"}, + {"sku_id": "sku_001", "name": "size", "value": "standard"} + ], + "skus": [ + { + "sku_id": "67890", + "price": 89.99, + "compare_at_price": 129.99, + "sku": "BARBIE-001", + "stock": 100, + "weight": 0.1, + "weight_unit": "kg", + "option1_value": "pink", + "option2_value": "standard", + "option3_value": null, + "image_src": "https://example.com/sku1.jpg" + } + ], + "relevance_score": 8.5 + } + ], + "total": 118, + "max_score": 8.5, + "facets": [ + { + "field": "category1_name", + "label": "category1_name", + "type": "terms", + "values": [ + { + "value": "玩具", + "label": "玩具", + "count": 85, + "selected": false + } + ] + }, + { + "field": "specifications.color", + "label": "color", + "type": "terms", + "values": [ + { + "value": "pink", + "label": "pink", + "count": 30, + "selected": false + } + ] + } + ], + "query_info": { + "original_query": "芭比娃娃", + "query_normalized": "芭比娃娃", + "rewritten_query": "芭比娃娃", + "detected_language": "zh", + "translations": { + "en": "barbie doll" + }, + "domain": "default" + }, + "suggestions": [], + "related_searches": [], + "took_ms": 45, + "performance_info": null, + "debug_info": null +} +``` + +### 4.2 响应字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `results` | array | 搜索结果列表(SpuResult对象数组) | +| `results[].spu_id` | string | SPU ID | +| `results[].title` | string | 商品标题 | +| `results[].price` | float | 价格(min_price) | +| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) | +| `results[].relevance_score` | float | 相关性分数 | +| `total` | integer | 匹配的总文档数 | +| `max_score` | float | 最高相关性分数 | +| `facets` | array | 分面统计结果 | +| `query_info` | object | query处理信息 | +| `took_ms` | integer | 搜索耗时(毫秒) | +| `debug_info` | object/null | 调试信息,仅当请求传 `debug=true` 时返回 | + +#### 4.2.1 query_info 说明 + +`query_info` 包含本次搜索的查询解析与处理结果: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `original_query` | string | 用户原始查询 | +| `query_normalized` | string | 归一化后的查询(去空白、大小写等预处理,用于后续解析与改写) | +| `rewritten_query` | string | 重写后的查询(同义词/词典扩展等) | +| `detected_language` | string | 检测到的查询语言(如 `zh`、`en`) | +| `translations` | object | 翻译结果,键为语言代码,值为翻译文本 | +| `domain` | string | 查询域(如 `default`、`title`、`brand` 等) | + +#### 4.2.2 debug_info 说明 + +`debug_info` 主要用于检索效果评估、融合打分分析与 bad case 排查。 + +`debug_info.query_analysis` 常见字段: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `original_query` | string | 原始查询 | +| `query_normalized` | string | 归一化后的查询 | +| `rewritten_query` | string | 重写后的查询 | +| `detected_language` | string | 检测到的语言 | +| `translations` | object | 翻译结果 | +| `query_text_by_lang` | object | 实际参与检索的多语言 query 文本 | +| `search_langs` | array[string] | 实际参与检索的语言列表 | +| `supplemental_search_langs` | array[string] | 因 mixed query 补入的附加语言列表 | +| `has_vector` | boolean | 是否生成了向量 | + +`debug_info.per_result[]` 常见字段: + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `spu_id` | string | 结果 SPU ID | +| `es_score` | float | ES 原始 `_score` | +| `rerank_score` | float | 重排分数 | +| `text_score` | float | 文本相关性大分(由 `base_query` / `base_query_trans_*` / `fallback_original_query_*` 聚合而来) | +| `text_source_score` | float | `base_query` 分数 | +| `text_translation_score` | float | `base_query_trans_*` 里的最大分数 | +| `text_fallback_score` | float | `fallback_original_query_*` 里的最大分数 | +| `text_primary_score` | float | 文本大分中的主证据部分 | +| `text_support_score` | float | 文本大分中的辅助证据部分 | +| `knn_score` | float | `knn_query` 分数 | +| `fused_score` | float | 最终融合分数 | +| `matched_queries` | object/array | ES named queries 命中详情 | + +### 4.3 SpuResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `spu_id` | string | SPU ID | +| `title` | string | 商品标题(根据language参数自动选择 `title.zh` 或 `title.en`) | +| `brief` | string | 商品短描述(根据language参数自动选择) | +| `description` | string | 商品详细描述(根据language参数自动选择) | +| `vendor` | string | 供应商/品牌(根据language参数自动选择) | +| `category` | string | 类目(兼容字段,等同于category_name) | +| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) | +| `category_name` | string | 类目名称(展示用,根据language参数自动选择) | +| `category_id` | string | 类目ID | +| `category_level` | integer | 类目层级(1/2/3) | +| `category1_name` | string | 一级类目名称 | +| `category2_name` | string | 二级类目名称 | +| `category3_name` | string | 三级类目名称 | +| `tags` | array[string] | 标签列表 | +| `price` | float | 价格(min_price) | +| `compare_at_price` | float | 原价 | +| `currency` | string | 货币单位(默认USD) | +| `image_url` | string | 主图URL | +| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) | +| `sku_prices` | array[float] | 所有SKU价格列表 | +| `sku_weights` | array[integer] | 所有SKU重量列表 | +| `sku_weight_units` | array[string] | 所有SKU重量单位列表 | +| `total_inventory` | integer | 总库存 | +| `sales` | integer | 销量(展示销量) | +| `option1_name` | string | 选项1名称(如"color") | +| `option2_name` | string | 选项2名称(如"size") | +| `option3_name` | string | 选项3名称 | +| `specifications` | array[object] | 规格列表(与ES specifications字段对应) | +| `skus` | array | SKU 列表 | +| `relevance_score` | float | 相关性分数(默认为 ES 原始分数;当开启 AI 搜索时为融合后的最终分数) | + +### 4.4 SkuResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `sku_id` | string | SKU ID | +| `price` | float | 价格 | +| `compare_at_price` | float | 原价 | +| `sku` | string | SKU编码(sku_code) | +| `stock` | integer | 库存数量 | +| `weight` | float | 重量 | +| `weight_unit` | string | 重量单位 | +| `option1_value` | string | 选项1取值(如color值) | +| `option2_value` | string | 选项2取值(如size值) | +| `option3_value` | string | 选项3取值 | +| `image_src` | string | SKU图片地址 | + +### 4.5 多语言字段说明 + +- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段 +- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段 +- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段 + +--- + +## 索引接口 + +本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) | +| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 | +| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES | +| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES | +| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 | +| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags(供微服务组合方式使用) | +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 | + +#### 5.0 支撑外部 indexer 的三种方式 + +本服务对**外部 indexer 程序**(如 Java 索引系统)提供三种对接方式,可按需选择: + +| 方式 | 说明 | 适用场景 | +|------|------|----------| +| **1)doc 填充接口** | 调用 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db`,由本服务基于 MySQL 行数据构建完整 ES 文档(含多语言、向量、规格等),**不写入 ES**,由调用方自行写入。 | 希望一站式拿到 ES-ready doc,由己方控制写 ES 的时机与索引名。 | +| **2)微服务组合** | 单独调用**翻译**、**向量化**、**内容理解字段生成**等接口,由 indexer 程序自己组装 doc 并写入 ES。翻译与向量化为独立微服务(见第 7 节);内容理解为 Indexer 服务内接口 `POST /indexer/enrich-content`。 | 需要灵活编排、或希望将 LLM/向量等耗时步骤与主链路解耦(如异步补齐 qanchors/tags)。 | +| **3)本服务直接写 ES** | 调用全量索引 `POST /indexer/reindex`、增量索引 `POST /indexer/index`(指定 SPU ID 列表),由本服务从 MySQL 拉数并直接写入 ES。 | 自建运维、联调或不需要由 Java 写 ES 的场景。 | + +- **方式 1** 与 **方式 2** 下,ES 的写入方均为外部 indexer(或 Java),职责清晰。 +- **方式 3** 下,本服务同时负责读库、构建 doc 与写 ES。 + +### 5.1 为租户创建索引 + +为租户创建索引需要两个步骤: + +1. **创建索引结构**(可选,仅在需要更新 mapping 或在新环境首次创建时执行) + - 使用脚本创建 ES 索引结构(基于 `mappings/search_products.json`) + - 如果索引已存在,会提示用户确认(会删除现有数据) + +2. **导入数据**(必需) + - 使用全量索引接口 `/indexer/reindex` 导入数据 + +**创建索引结构(支持多环境 namespace)**: + +```bash +# 以 UAT 环境为例: +# 1. 准备 UAT 环境的 .env(包含 UAT 的 ES_HOST/DB_HOST 等) +# 2. 设置环境前缀(也可以直接在 .env 中配置): +export RUNTIME_ENV=uat +export ES_INDEX_NAMESPACE=uat_ + +# 3. 为 tenant_id=170 创建索引结构 +./scripts/create_tenant_index.sh 170 +``` + +脚本会自动从项目根目录的 `.env` 文件加载 ES 配置,并根据 `ES_INDEX_NAMESPACE` 创建: + +- prod 环境(ES_INDEX_NAMESPACE 为空):`search_products_tenant_170` +- UAT 环境(ES_INDEX_NAMESPACE=uat_):`uat_search_products_tenant_170` + +**注意事项**: +- ⚠️ 如果索引已存在,脚本会提示确认,确认后会删除现有数据 +- 创建索引后,**必须**调用 `/indexer/reindex` 导入数据 +- 如果只是更新数据而不需要修改索引结构,直接使用 `/indexer/reindex` 即可 + +--- + +### 5.2 全量索引接口 + +- **端点**: `POST /indexer/reindex` +- **描述**: 全量索引,将指定租户的所有SPU数据导入到ES索引(不会删除现有索引)。**推荐仅用于自测/运维场景**;生产环境下更推荐由 Java 等上游控制调度与写 ES。 + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "batch_size": 500 +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `tenant_id` | string | Y | - | 租户ID | +| `batch_size` | integer | N | 500 | 批量导入大小 | + +#### 响应格式 + +**成功响应(200 OK)**(示例,实际 `index_name` 会带上 tenant 和环境前缀): +```json +{ + "success": true, + "total": 1000, + "indexed": 1000, + "failed": 0, + "elapsed_time": 12.34, + "index_name": "search_products_tenant_162", + "tenant_id": "162" +} +``` + +**错误响应**: +- `400 Bad Request`: 参数错误 +- `503 Service Unavailable`: 服务未初始化 + +#### 请求示例 + +**全量索引(不会删除现有索引)**: +```bash +curl -X POST "http://localhost:6004/indexer/reindex" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "batch_size": 500 + }' +``` + +**查看日志**: +```bash +# 查看API日志(包含索引操作日志) +tail -f logs/api.log + +# 或者查看所有日志文件 +tail -f logs/*.log +``` + +> ⚠️ **重要提示**:如需 **创建索引结构**,请参考 [5.1 为租户创建索引](#51-为租户创建索引) 章节,使用 `./scripts/create_tenant_index.sh `。创建后需要调用 `/indexer/reindex` 导入数据。 + +**查看索引日志**: + +索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON 格式),包括: +- 请求开始和结束时间 +- 租户ID、SPU ID、操作类型 +- 每个SPU的处理状态 +- ES批量写入结果 +- 成功/失败统计和详细错误信息 + +```bash +# 实时查看索引日志(包含全量和增量索引的所有操作) +tail -f logs/indexer.log + +# 使用 grep 查询(简单方式) +# 查看全量索引日志 +grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100 + +# 查看增量索引日志 +grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100 + +# 查看特定租户的索引日志 +grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100 + +# 使用 jq 查询(推荐,更精确的 JSON 查询) +# 安装 jq: sudo apt-get install jq 或 brew install jq + +# 查看全量索引日志 +cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100 + +# 查看增量索引日志 +cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100 + +# 查看特定租户的索引日志 +cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100 + +# 查看失败的索引操作 +cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)' + +# 查看特定SPU的处理日志 +cat logs/indexer.log | jq 'select(.spu_id == "123")' + +# 查看最近的索引请求统计 +cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}' +``` + +### 5.3 增量索引接口 + +- **端点**: `POST /indexer/index` +- **描述**: 增量索引接口,根据指定的SPU ID列表进行索引,直接将数据写入ES。用于增量更新指定商品。**推荐仅作为内部/调试入口**;正式对接建议改用 `/indexer/build-docs`,由上游写 ES。 + +**删除说明**: +- `spu_ids`中的SPU:如果数据库`deleted=1`,自动从ES删除,响应状态为`deleted` +- `delete_spu_ids`中的SPU:直接删除,响应状态为`deleted`、`not_found`或`failed` + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"], + "delete_spu_ids": ["100", "101"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户ID | +| `spu_ids` | array[string] | N | SPU ID列表(1-100个),要索引的SPU。如果为空,则只执行删除操作 | +| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表(1-100个),可选。无论数据库状态如何,都会从ES中删除这些SPU | + +**注意**: +- `spu_ids` 和 `delete_spu_ids` 不能同时为空 +- 每个列表最多支持100个SPU ID +- 如果SPU在`spu_ids`中且数据库`deleted=1`,会自动从ES删除(自动检测删除) + +#### 响应格式 + +```json +{ + "spu_ids": [ + { + "spu_id": "123", + "status": "indexed" + }, + { + "spu_id": "456", + "status": "deleted" + }, + { + "spu_id": "789", + "status": "failed", + "msg": "SPU not found (unexpected)" + } + ], + "delete_spu_ids": [ + { + "spu_id": "100", + "status": "deleted" + }, + { + "spu_id": "101", + "status": "not_found" + }, + { + "spu_id": "102", + "status": "failed", + "msg": "Failed to delete from ES: Connection timeout" + } + ], + "total": 6, + "success_count": 4, + "failed_count": 2, + "elapsed_time": 1.23, + "index_name": "search_products", + "tenant_id": "162" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `spu_ids` | array | spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | +| `spu_ids[].status` | string | 状态:`indexed`(已索引)、`deleted`(已删除,自动检测)、`failed`(失败) | +| `spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | +| `delete_spu_ids` | array | delete_spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` | +| `delete_spu_ids[].status` | string | 状态:`deleted`(已删除)、`not_found`(ES中不存在)、`failed`(失败) | +| `delete_spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) | +| `total` | integer | 总处理数量(spu_ids数量 + delete_spu_ids数量) | +| `success_count` | integer | 成功数量(indexed + deleted + not_found) | +| `failed_count` | integer | 失败数量 | +| `elapsed_time` | float | 耗时(秒) | +| `index_name` | string | 索引名称 | +| `tenant_id` | string | 租户ID | + +**状态说明**: +- `spu_ids` 的状态: + - `indexed`: SPU已成功索引到ES + - `deleted`: SPU在数据库中被标记为deleted=1,已从ES删除(自动检测) + - `failed`: 处理失败,会包含`msg`字段说明失败原因 +- `delete_spu_ids` 的状态: + - `deleted`: SPU已从ES成功删除 + - `not_found`: SPU在ES中不存在(也算成功,可能已经被删除过) + - `failed`: 删除失败,会包含`msg`字段说明失败原因 + +#### 请求示例 + +**示例1:普通增量索引(自动检测删除)**: +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] + }' +``` +说明:如果SPU 456在数据库中`deleted=1`,会自动从ES删除,在响应中`spu_ids`列表里456的状态为`deleted`。 + +**示例2:显式删除(批量删除)**: +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456"], + "delete_spu_ids": ["100", "101", "102"] + }' +``` +说明:SPU 100、101、102会被显式删除,无论数据库状态如何。 + +**示例3:仅删除(不索引)**: +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": [], + "delete_spu_ids": ["100", "101"] + }' +``` +说明:只执行删除操作,不进行索引。 + +**示例4:混合操作(索引+删除)**: +```bash +curl -X POST "http://localhost:6004/indexer/index" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"], + "delete_spu_ids": ["100", "101"] + }' +``` +说明:同时执行索引和删除操作。 + +#### 日志说明 + +增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON格式),包括: +- 请求开始和结束时间 +- 每个SPU的处理状态(获取、转换、索引、删除) +- ES批量写入结果 +- 成功/失败统计 +- 详细的错误信息 + +日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。 + +### 5.4 查询文档接口 + +- **端点**: `POST /indexer/documents` +- **描述**: 查询文档接口,根据SPU ID列表获取ES文档数据(**不写入ES**)。用于查看、调试或验证SPU数据。 + +#### 请求参数 + +```json +{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户ID | +| `spu_ids` | array[string] | Y | SPU ID列表(1-100个) | + +#### 响应格式 + +```json +{ + "success": [ + { + "spu_id": "123", + "document": { + "tenant_id": "162", + "spu_id": "123", + "title": { + "zh": "商品标题" + }, + ... + } + }, + { + "spu_id": "456", + "document": {...} + } + ], + "failed": [ + { + "spu_id": "789", + "error": "SPU not found or deleted" + } + ], + "total": 3, + "success_count": 2, + "failed_count": 1 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | array | 成功获取的SPU列表,每个元素包含 `spu_id` 和 `document`(完整的ES文档数据) | +| `failed` | array | 失败的SPU列表,每个元素包含 `spu_id` 和 `error`(失败原因) | +| `total` | integer | 总SPU数量 | +| `success_count` | integer | 成功数量 | +| `failed_count` | integer | 失败数量 | + +#### 请求示例 + +**单个SPU查询**: +```bash +curl -X POST "http://localhost:6004/indexer/documents" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123"] + }' +``` + +**批量SPU查询**: +```bash +curl -X POST "http://localhost:6004/indexer/documents" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "spu_ids": ["123", "456", "789"] + }' +``` + +#### 与 `/indexer/index` 的区别 + +| 接口 | 功能 | 是否写入ES | 返回内容 | +|------|------|-----------|----------| +| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 | +| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 | + +**使用场景**: +- `/indexer/documents`:用于查看、调试或验证SPU数据,不修改ES索引 +- `/indexer/index`:用于实际的增量索引操作,将更新的SPU数据同步到ES + +### 5.5 索引健康检查接口 + +- **端点**: `GET /indexer/health` +- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致) + +#### 响应格式 + +```json +{ + "status": "available", + "database": "connected", + "preloaded_data": { + "category_mappings": 150 + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) | +| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` | +| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 | + +#### 请求示例 + +```bash +curl -X GET "http://localhost:6004/indexer/health" +``` + +### 5.6 文档构建接口(正式对接推荐) + +#### 5.6.1 `POST /indexer/build-docs` + +- **描述**: + 基于调用方(通常是 Java 索引程序)提供的 **MySQL 行数据** 构建 ES 文档(doc),**不写入 ES**。 + 由本服务负责“如何构建 doc”(多语言、翻译、向量、规格聚合等),由调用方负责“何时调度 + 如何写 ES”。 + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "items": [ + { + "spu": { "id": 223167, "tenant_id": 170, "title": "..." }, + "skus": [ + { "id": 3988393, "spu_id": 223167, "price": 25.99, "compare_at_price": 25.99 } + ], + "options": [] + } + ] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** | + +> `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。 + +#### 请求示例(完整 curl) + +> 完整请求体参考 `scripts/test_build_docs_api.py` 中的 `build_sample_request()`。 + +```bash +# 单条 SPU 示例(含 spu、skus、options) +curl -X POST "http://localhost:6004/indexer/build-docs" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "162", + "items": [ + { + "spu": { + "id": 10001, + "tenant_id": "162", + "title": "测试T恤 纯棉短袖", + "brief": "舒适纯棉,多色可选", + "description": "这是一款适合日常穿着的纯棉T恤,透气吸汗。", + "vendor": "测试品牌", + "category": "服装/上衣/T恤", + "category_id": 100, + "category_level": 2, + "category_path": "服装/上衣/T恤", + "fake_sales": 1280, + "image_src": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg", + "tags": "T恤,纯棉,短袖,夏季", + "create_time": "2024-01-01T00:00:00Z", + "update_time": "2024-01-01T00:00:00Z" + }, + "skus": [ + { + "id": 20001, + "spu_id": 10001, + "price": 99.0, + "compare_at_price": 129.0, + "sku": "SKU-TSHIRT-001", + "inventory_quantity": 50, + "option1": "黑色", + "option2": "M", + "option3": null + }, + { + "id": 20002, + "spu_id": 10001, + "price": 99.0, + "compare_at_price": 129.0, + "sku": "SKU-TSHIRT-002", + "inventory_quantity": 30, + "option1": "白色", + "option2": "L", + "option3": null + } + ], + "options": [ + {"id": 1, "position": 1, "name": "颜色"}, + {"id": 2, "position": 2, "name": "尺码"} + ] + } + ] +}' +``` + +生产环境替换 `localhost:6004` 为实际 Indexer 地址,如 `http://43.166.252.75:6004`。 + +#### 响应示例(节选) + +```json +{ + "tenant_id": "170", + "docs": [ + { + "tenant_id": "170", + "spu_id": "223167", + "title": { "en": "...", "zh": "..." }, + "tags": ["Floerns", "Clothing", "Shoes & Jewelry"], + "skus": [ + { + "sku_id": "3988393", + "price": 25.99, + "compare_at_price": 25.99, + "stock": 100 + } + ], + "min_price": 25.99, + "max_price": 25.99, + "compare_at_price": 25.99, + "total_inventory": 100, + "title_embedding": [/* 1024 维向量 */] + // 其余字段与 mappings/search_products.json 一致 + } + ], + "total": 1, + "success_count": 1, + "failed_count": 0, + "failed": [] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `tenant_id` | string | 租户 ID | +| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 | +| `total` | integer | 请求的 items 总数 | +| `success_count` | integer | 成功构建数量 | +| `failed_count` | integer | 失败数量 | +| `failed` | array | 失败项列表,每项含 `spu_id`、`error` | + +#### 使用建议 + +- **生产环境推荐流程**: + 1. Java 根据业务逻辑决定哪些 SPU 需要(全量/增量)处理; + 2. Java 从 MySQL 查询 SPU/SKU/Option 行,拼成 `items`; + 3. 调用 `/indexer/build-docs` 获取 ES-ready `docs`; + 4. Java 使用自己的 ES 客户端写入 `search_products_tenant_{tenant_id}`。 + +### 5.7 文档构建接口(测试 / 自测) + +#### 5.7.1 `POST /indexer/build-docs-from-db` + +- **描述**: + 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。** + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "spu_ids": ["223167", "223168"] +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** | + +#### 响应格式 + +与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。 + +#### 请求示例 + +```bash +curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \ + -H "Content-Type: application/json" \ + -d '{"tenant_id": "170", "spu_ids": ["223167"]}' +``` + +返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。 + +### 5.8 内容理解字段生成接口 + +- **端点**: `POST /indexer/enrich-content` +- **描述**: 根据商品内容信息批量生成 **qanchors**(锚文本)、**semantic_attributes**(语义属性)、**tags**(细分标签),供外部 indexer 在「微服务组合」方式下自行拼装 doc 时使用。请求以 `items[]` 传入商品内容字段(必填/可选见下表)。内部逻辑与 `indexer.product_enrich` 一致,支持多语言与 Redis 缓存;单次请求在线程池中执行,避免阻塞其他接口。 + +#### 请求参数 + +```json +{ + "tenant_id": "170", + "items": [ + { + "spu_id": "223167", + "title": "纯棉短袖T恤 夏季男装", + "brief": "夏季透气纯棉短袖,舒适亲肤", + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", + "image_url": "https://example.com/images/223167.jpg" + }, + { + "spu_id": "223168", + "title": "12PCS Dolls with Bottles", + "image_url": "https://example.com/images/223168.jpg" + } + ], + "languages": ["zh", "en"] +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `tenant_id` | string | Y | - | 租户 ID。目前仅用于记录日志,不产生实际作用| +| `items` | array | Y | - | 待分析列表;**单次最多 50 条** | +| `languages` | array[string] | N | `["zh", "en"]` | 目标语言,需在支持范围内:`zh`、`en`、`de`、`ru`、`fr` | + +`items[]` 字段说明: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `spu_id` | string | Y | SPU ID,用于回填结果;目前仅用于记录日志,不产生实际作用| +| `title` | string | Y | 商品标题 | +| `image_url` | string | N | 商品主图 URL;当前会参与内容缓存键,后续可用于图像/多模态内容理解 | +| `brief` | string | N | 商品简介/短描述;当前会参与内容缓存键 | +| `description` | string | N | 商品详情/长描述;当前会参与内容缓存键 | + +缓存说明: + +- 内容缓存键仅由 `target_lang + items[]` 中会影响内容理解结果的输入文本构成,目前包括:`title`、`brief`、`description`、`image_url` 的规范化内容 hash。 +- `tenant_id`、`spu_id` 只用于请求归属与结果回填,不参与缓存键。 +- 因此,输入内容不变时可跨请求直接命中缓存;任一输入字段变化时,会自然落到新的缓存 key。 + +批量请求建议: +- **全量**:强烈建议 尽可能 **20 个 SPU/doc** 攒成一个批次后再请求一次。 +- **增量**:可按时效要求设置时间窗口(例如 **5 分钟**),在窗口内尽可能攒到 **20 个**;达到 20 或窗口到期就发送一次请求。 +- 允许超过20,服务内部会拆分成小批次逐个处理。也允许小于20,但是将造成费用和耗时的成本上升,特别是每次请求一个doc的情况。 + +#### 响应格式 + +```json +{ + "tenant_id": "170", + "total": 2, + "results": [ + { + "spu_id": "223167", + "qanchors": { + "zh": "短袖T恤,纯棉,男装,夏季", + "en": "cotton t-shirt, short sleeve, men, summer" + }, + "semantic_attributes": [ + { "lang": "zh", "name": "tags", "value": "纯棉" }, + { "lang": "zh", "name": "usage_scene", "value": "日常" }, + { "lang": "en", "name": "tags", "value": "cotton" } + ], + "tags": ["纯棉", "短袖", "男装", "cotton", "short sleeve"] + }, + { + "spu_id": "223168", + "qanchors": { "en": "dolls, toys, 12pcs" }, + "semantic_attributes": [], + "tags": ["dolls", "toys"] + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `results` | array | 与请求 `items` 一一对应,每项含 `spu_id`、`qanchors`、`semantic_attributes`、`tags` | +| `results[].qanchors` | object | 按语言键的锚文本(逗号分隔短语),可写入 ES 文档的 `qanchors.{lang}` | +| `results[].semantic_attributes` | array | 语义属性列表,每项为 `{ "lang", "name", "value" }`,可写入 ES 的 `semantic_attributes` nested 字段 | +| `results[].tags` | array | 从语义属性中抽取的 `name=tags` 的 value 集合,可与业务原有 `tags` 合并后写入 ES 的 `tags` 字段 | +| `results[].error` | string | 若该条处理失败(如 LLM 异常),会在此字段返回错误信息 | + +**错误响应**: +- `400`: `items` 为空或超过 50 条 +- `503`: 未配置 `DASHSCOPE_API_KEY`,内容理解服务不可用 + +#### 请求示例 + +```bash +curl -X POST "http://localhost:6004/indexer/enrich-content" \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "170", + "items": [ + { + "spu_id": "223167", + "title": "纯棉短袖T恤 夏季男装", + "brief": "夏季透气纯棉短袖,舒适亲肤", + "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。", + "image_url": "https://example.com/images/223167.jpg" + } + ], + "languages": ["zh", "en"] + }' +``` + +--- + +## 管理接口 + +### 6.1 健康检查 + +- **端点**: `GET /admin/health` +- **描述**: 检查服务与依赖(如 Elasticsearch)状态。 + +```json +{ + "status": "healthy", + "elasticsearch": "connected", + "tenant_id": "tenant1" +} +``` + +### 6.2 获取配置 + +- **端点**: `GET /admin/config` +- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。 + +```json +{ + "tenant_id": "tenant1", + "tenant_name": "Tenant1 Test Instance", + "es_index_name": "search_tenant1", + "num_fields": 20, + "num_indexes": 4, + "supported_languages": ["zh", "en", "ru"], + "spu_enabled": false +} +``` + +### 6.3 索引统计 + +- **端点**: `GET /admin/stats` +- **描述**: 获取指定租户索引文档数量与磁盘大小,方便监控。 +- **租户标识**:通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递(必填)。 + +```json +{ + "tenant_id": "162", + "index_name": "search_products_tenant_162", + "document_count": 10000, + "size_mb": 523.45 +} +``` + +--- + +## 7. 微服务接口(向量、重排、翻译) + +以下三个微服务独立部署,**外部系统可直接调用**。它们被搜索后端(6002)和索引服务(6004)内部使用,也可供其他业务系统直接对接。 + +| 服务 | 默认端口 | Base URL | 说明 | +|------|----------|----------|------| +| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 | +| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 | +| 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) | +| 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 | + +生产环境请将 `localhost` 替换为实际服务地址。 +服务管理入口与完整启停规则见:`docs/Usage-Guide.md` -> `服务管理总览`。 + +### 7.1 向量服务(Embedding) + +- **Base URL**: + - 文本:`http://localhost:6005`(可通过 `EMBEDDING_TEXT_SERVICE_URL` 覆盖) + - 图片:`http://localhost:6008`(可通过 `EMBEDDING_IMAGE_SERVICE_URL` 覆盖) +- **启动**: + - 文本:`./scripts/start_embedding_text_service.sh` + - 图片:`./scripts/start_embedding_image_service.sh` +- **依赖**: + - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`) + - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`) + - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU) + - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`) + - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力 + +补充说明: + +- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。 +- 服务端对 text / image 有**独立 admission control**: + - `TEXT_MAX_INFLIGHT` + - `IMAGE_MAX_INFLIGHT` +- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。 +- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。 + +#### 7.1.1 `POST /embed/text` — 文本向量化 + +将文本列表转为 1024 维向量,用于语义搜索、文档索引等。 + +**请求体**(JSON 数组): +```json +["文本1", "文本2", "文本3"] +``` + +**响应**(JSON 数组,与输入一一对应): +```json +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] +``` + +**完整 curl 示例**: +```bash +curl -X POST "http://localhost:6005/embed/text?normalize=true" \ + -H "Content-Type: application/json" \ + -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]' +``` + +#### 7.1.2 `POST /embed/image` — 图片向量化 + +将图片 URL 或路径转为向量,用于以图搜图。 + +前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。 + +**请求体**(JSON 数组): +```json +["https://example.com/image1.jpg", "https://example.com/image2.jpg"] +``` + +**响应**(JSON 数组,与输入一一对应): +```json +[[0.01, -0.02, ...], [0.03, 0.01, ...], ...] +``` + +**完整 curl 示例**: +```bash +curl -X POST "http://localhost:6008/embed/image?normalize=true" \ + -H "Content-Type: application/json" \ + -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' +``` + +#### 7.1.3 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6005/health" +curl "http://localhost:6008/health" +``` + +返回中会包含: + +- `service_kind`:`text` / `image` / `all` +- `cache_enabled`:text/image Redis 缓存是否可用 +- `limits`:当前 inflight limit、active、rejected_total 等 +- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等 + +#### 7.1.4 `GET /ready` — 就绪检查 + +```bash +curl "http://localhost:6005/ready" +curl "http://localhost:6008/ready" +``` + +#### 7.1.5 缓存与限流说明 + +- 文本与图片都会先查 Redis 向量缓存。 +- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。 +- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。 +- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。 +- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。 + +#### 7.1.6 TEI 统一调优建议(主服务) + +使用单套主服务即可同时兼顾: +- 在线 query 向量化(低延迟,常见 `batch=1~4`) +- 索引构建向量化(高吞吐,常见 `batch=15~20`) + +统一启动(主链路): + +```bash +./scripts/start_tei_service.sh +./scripts/service_ctl.sh restart embedding +``` + +默认端口: +- TEI: `http://127.0.0.1:8080` +- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005` +- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008` + +当前主 TEI 启动默认值(已按 T4/短文本场景调优): +- `TEI_MAX_BATCH_TOKENS=4096` +- `TEI_MAX_CLIENT_BATCH_SIZE=24` +- `TEI_DTYPE=float16` + +### 7.2 重排服务(Reranker) + +- **Base URL**: `http://localhost:6007`(可通过 `RERANKER_SERVICE_URL` 覆盖) +- **启动**: `./scripts/start_reranker.sh` + +说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。 +补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确)。 + +#### 7.2.1 `POST /rerank` — 结果重排 + +根据 query 与 doc 的相关性对文档列表重新打分排序。 + +**请求体**: +```json +{ + "query": "玩具 芭比", + "docs": [ + "12PCS 6 Types of Dolls with Bottles", + "纯棉T恤 短袖 夏季" + ], + "normalize": true +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `query` | string | Y | 搜索查询 | +| `docs` | array[string] | Y | 待重排的文档列表(单次最多由服务端配置限制) | +| `normalize` | boolean | N | 是否对分数做 sigmoid 归一化,默认 true | + +**响应**: +```json +{ + "scores": [0.92, 0.15], + "meta": { + "service_elapsed_ms": 45.2, + "input_docs": 2, + "unique_docs": 2 + } +} +``` + +**完整 curl 示例**: +```bash +curl -X POST "http://localhost:6007/rerank" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 芭比", + "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"], + "top_n":386, + "normalize": true + }' + +``` + +#### 7.2.2 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6007/health" +``` + +### 7.3 翻译服务(Translation) + +- **Base URL**: `http://localhost:6006`(以 `config/config.yaml -> services.translation.service_url` 为准) +- **启动**: `./scripts/start_translator.sh` + +#### 7.3.1 `POST /translate` — 文本翻译 + +支持 translator service 内所有已启用 capability,适用于商品名称、描述、query 等电商场景。当前可配置能力包括 `qwen-mt`、`llm`、`deepl` 以及本地模型 `nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh`。 + +**请求体**(支持单条字符串或字符串列表): +```json +{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +也支持批量列表形式: +```json +{ + "text": ["商品名称1", "商品名称2"], + "target_lang": "en", + "source_lang": "zh", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `text` | string \| string[] | Y | 待翻译文本,既支持单条字符串,也支持字符串列表(批量翻译) | +| `target_lang` | string | Y | 目标语言:`zh`、`en`、`ru` 等 | +| `source_lang` | string | N | 源语言。云端模型可不传;`nllb-200-distilled-600m` 建议显式传入 | +| `model` | string | N | 已启用 capability 名称,如 `qwen-mt`、`llm`、`deepl`、`nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh` | +| `scene` | string | N | 翻译场景参数,与 `model` 配套使用;当前标准值为 `sku_name`、`ecommerce_search_query`、`general` | + +说明: +- 外部接口不接受 `prompt`;LLM prompt 由服务端按 `scene` 自动生成。 +- 传入未定义的 `scene` 或未启用的 `model` 会返回 `400`。 + +**SKU 名称场景选型建议**: + +- 批量 SKU 名称翻译,优先考虑本地大吞吐方案时,可使用 `"model": "nllb-200-distilled-600m"`(该模型"scene":参数无效)。 +- 如果目标是更高质量,且可以接受更慢速度与额外 LLM API 费用,可使用 `"model": "llm"` + `"scene": "sku_name"`。 +- 如果是en-zh互译、期待更高的速度,可以考虑`opus-mt-zh-en` / `opus-mt-en-zh`。(质量未详细评测,一些文章说比blib-200-600m更好,但是我看了些case感觉要差不少) + +**实时翻译选型建议**: + +- 在线 query 翻译如果只是 `en/zh` 互译,优先使用 `opus-mt-zh-en` 或 `opus-mt-en-zh`,它们是当前已测本地模型里延迟最低的一档。 +- 如果涉及其他语言,或对质量要求高于本地轻量模型,优先考虑 `deepl`。 +- `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`。 + +**Batch Size / 调用方式建议**: + +- 本接口支持 `text: string[]`;离线或批量索引翻译时,应尽量合并请求,让底层 backend 发挥批处理能力。 +- `nllb-200-distilled-600m` 在当前 `Tesla T4` 压测中,推荐配置是 `batch_size=16`、`max_new_tokens=64`、`attn_implementation=sdpa`;继续升到 `batch_size=32` 虽可能提高吞吐,但 tail latency 会明显变差。 +- 在线 query 场景可直接把“单条请求”理解为 `batch_size=1`;更关注 request latency,而不是离线吞吐。 +- `opus-mt-zh-en` / `opus-mt-en-zh` 当前生产配置也是 `batch_size=16`,适合作为中英互译的低延迟本地默认值;若走在线单条调用,同样按 `batch_size=1` 理解即可。 +- `llm` 按单条请求即可。 + +**响应**: +```json +{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "translated_text": "Product name", + "status": "success", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +当请求为列表形式时,`text` 与 `translated_text` 均为等长数组: +```json +{ + "text": ["商品名称1", "商品名称2"], + "target_lang": "en", + "source_lang": "zh", + "translated_text": ["Product name 1", "Product name 2"], + "status": "success", + "model": "qwen-mt", + "scene": "sku_name" +} +``` + +> **失败语义(批量)**:当 `text` 为列表时,如果其中某条翻译失败,对应位置返回 `null`(即 `translated_text[i] = null`),并保持数组长度与顺序不变;接口整体仍返回 `status="success"`,用于避免“部分失败”导致整批请求失败。 + +> **实现提示(可忽略)**:服务端会尽可能使用底层 backend 的批量能力(若支持),否则自动拆分逐条翻译;无论采用哪种方式,上述批量契约保持一致。 + +**完整 curl 示例**: + +中文 → 英文: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh" + }' +``` + +俄文 → 英文: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Название товара", + "target_lang": "en", + "source_lang": "ru" + }' +``` + +使用 DeepL 模型: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh", + "model": "deepl" + }' +``` + +使用本地 OPUS 模型(中文 → 英文): +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "蓝牙耳机", + "target_lang": "en", + "source_lang": "zh", + "model": "opus-mt-zh-en", + "scene": "sku_name" + }' +``` + +使用本地 NLLB 做 SKU 名称批量翻译: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": ["商品名称1", "商品名称2", "商品名称3"], + "target_lang": "en", + "source_lang": "zh", + "model": "nllb-200-distilled-600m", + "scene": "sku_name" + }' +``` + +使用 LLM 做高质量 SKU 名称翻译: +```bash +curl -X POST "http://localhost:6006/translate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "男士偏光飞行员太阳镜", + "target_lang": "en", + "source_lang": "zh", + "model": "llm", + "scene": "sku_name" + }' +``` + +#### 7.3.2 `GET /health` — 健康检查 + +```bash +curl "http://localhost:6006/health" +``` + +典型响应: +```json +{ + "status": "healthy", + "service": "translation", + "default_model": "llm", + "default_scene": "general", + "available_models": ["qwen-mt", "llm", "opus-mt-zh-en"], + "enabled_capabilities": ["qwen-mt", "llm", "opus-mt-zh-en"], + "loaded_models": ["llm"] +} +``` + +### 7.4 内容理解字段生成(Indexer 服务内) + +内容理解字段生成接口部署在 **Indexer 服务**(默认端口 6004)内,与「翻译、向量化」等独立端口微服务并列,供采用**微服务组合**方式的 indexer 调用。 + +- **Base URL**: Indexer 服务地址,如 `http://localhost:6004` +- **路径**: `POST /indexer/enrich-content` +- **说明**: 根据商品标题批量生成 `qanchors`、`semantic_attributes`、`tags`,用于拼装 ES 文档。内部使用大模型(需配置 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存;单次最多 50 条,建议批量调用以提升效率。 + +请求/响应格式、示例及错误码见 [5.8 内容理解字段生成接口](#58-内容理解字段生成接口)。 + +--- + +## 8. 常见场景示例 + +以下示例仅展示**请求体**(body);实际调用时请加上请求头 `X-Tenant-ID: <租户ID>`(或 URL 参数 `tenant_id`),参见 [3.1 接口信息](#31-接口信息)。 + +### 8.1 基础搜索与排序 + +**按价格从低到高排序**: +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "price", + "sort_order": "asc" +} +``` + +**按价格从高到低排序**: +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "price", + "sort_order": "desc" +} +``` + +**按销量从高到低排序**: +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "sales", + "sort_order": "desc" +} +``` + +**按默认(相关性)排序**: +```json +{ + "query": "玩具", + "size": 20, + "from": 0 +} +``` + +### 8.2 过滤搜索 + +**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 + +```json +{ + "query": "玩具", + "size": 20, + "language": "zh", + "filters": { + "category_name": "益智玩具" + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + } +} +``` + +**需求**: 搜索"手机",筛选多个品牌,价格范围 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "vendor.zh.keyword": ["品牌A", "品牌B"] + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + } +} +``` + +### 8.3 分面搜索 + +**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器 + +```json +{ + "query": "玩具", + "size": 20, + "language": "zh", + "facets": [ + {"field": "category1_name", "size": 15, "type": "terms"}, + {"field": "category2_name", "size": 10, "type": "terms"}, + {"field": "specifications", "size": 10, "type": "terms"} + ] +} +``` + +**需求**: 搜索"手机",获取价格区间和规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + }, + { + "field": "specifications", + "size": 10, + "type": "terms" + } + ] +} +``` + +### 8.4 规格过滤与分面 + +**需求**: 搜索"手机",筛选color为"white"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": { + "name": "color", + "value": "white" + } + } +} +``` + +**需求**: 搜索"手机",筛选color为"white"且size为"256GB"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": [ + {"name": "color", "value": "white"}, + {"name": "size", "value": "256GB"} + ] + } +} +``` + +**需求**: 搜索"手机",筛选size为"3"、"4"或"5",且color为"green"的商品 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "specifications": [ + {"name": "size", "value": "3"}, + {"name": "size", "value": "4"}, + {"name": "size", "value": "5"}, + {"name": "color", "value": "green"} + ] + } +} +``` + +**需求**: 搜索"手机",获取所有规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + {"field": "specifications", "size": 10, "type": "terms"} + ] +} +``` + +**需求**: 只获取"color"和"size"规格的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "facets": [ + {"field": "specifications.color", "size": 20, "type": "terms"}, + {"field": "specifications.size", "size": 15, "type": "terms"} + ] +} +``` + +**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计 + +```json +{ + "query": "手机", + "size": 20, + "language": "zh", + "filters": { + "category_name": "手机", + "specifications": { + "name": "color", + "value": "white" + } + }, + "facets": [ + {"field": "category1_name", "size": 15, "type": "terms"}, + {"field": "category2_name", "size": 10, "type": "terms"}, + {"field": "specifications.color", "size": 20, "type": "terms"}, + {"field": "specifications.size", "size": 15, "type": "terms"} + ] +} +``` + +### 8.5 SKU筛选 + +**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU + +```json +{ + "query": "芭比娃娃", + "size": 20, + "sku_filter_dimension": ["color"] +} +``` + +**说明**: +- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组 +- 每个SPU下,每种颜色只会返回第一个SKU +- 如果维度不匹配,返回所有SKU(不进行过滤) + +### 8.7 分页查询 + +**需求**: 获取第2页结果(每页20条) + +```json +{ + "query": "手机", + "size": 20, + "from": 20 +} +``` + +--- + +## 9. 数据模型 + +### 9.1 商品字段定义 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| `tenant_id` | keyword | 租户ID(多租户隔离) | +| `spu_id` | keyword | SPU ID | +| `title.` | object/text | 商品标题(多语言对象,如 `title.zh`, `title.en`) | +| `brief.` | object/text | 商品短描述(多语言对象,如 `brief.zh`, `brief.en`) | +| `description.` | object/text | 商品详细描述(多语言对象,如 `description.zh`, `description.en`) | +| `vendor.` | object/text | 供应商/品牌(多语言对象,且带 keyword 子字段,如 `vendor.zh.keyword`) | +| `category_path.` | object/text | 类目路径(多语言对象,用于搜索,如 `category_path.zh`) | +| `category_name_text.` | object/text | 类目名称(多语言对象,用于搜索,如 `category_name_text.zh`) | +| `category_id` | keyword | 类目ID | +| `category_name` | keyword | 类目名称(用于过滤) | +| `category_level` | integer | 类目层级 | +| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) | +| `tags` | keyword | 标签(数组) | +| `specifications` | nested | 规格(嵌套对象数组) | +| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 | +| `min_price`, `max_price` | float | 最低/最高价格 | +| `compare_at_price` | float | 原价 | +| `sku_prices` | float | SKU价格列表(数组) | +| `sku_weights` | long | SKU重量列表(数组) | +| `sku_weight_units` | keyword | SKU重量单位列表(数组) | +| `total_inventory` | long | 总库存 | +| `sales` | long | 销量(展示销量) | +| `skus` | nested | SKU详细信息(嵌套对象数组) | +| `create_time`, `update_time` | date | 创建/更新时间 | +| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) | +| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) | + +> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。 + +### 9.2 字段类型速查 + +| 类型 | ES Mapping | 用途 | +|------|------------|------| +| `text` | `text` | 全文检索(支持中英文分析器) | +| `keyword` | `keyword` | 精确匹配、聚合、排序 | +| `integer` | `integer` | 整数 | +| `long` | `long` | 长整数 | +| `float` | `float` | 浮点数 | +| `date` | `date` | 日期时间 | +| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) | +| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) | + +### 9.3 常用字段列表 + +#### 过滤字段 + +- `category_name`: 类目名称 +- `category1_name`, `category2_name`, `category3_name`: 多级类目 +- `category_id`: 类目ID +- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段) +- `tags`: 标签(keyword类型) +- `option1_name`, `option2_name`, `option3_name`: 选项名称 +- `specifications`: 规格过滤(嵌套字段,格式见[过滤器详解](#33-过滤器详解)) + +#### 范围字段 + +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +#### 排序字段 + +- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price) +- `sales`: 销量 +- `create_time`: 创建时间 +- `update_time`: 更新时间 +- `relevance_score`: 相关性分数(默认,不指定sort_by时使用) + +**注意**: 前端只需传 `price`,后端会自动处理: +- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高) +- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低) + +### 9.4 支持的分析器 + +| 分析器 | 语言 | 描述 | +|--------|------|------| +| `index_ik` | 中文 | 中文索引分析器(用于中文字段) | +| `query_ik` | 中文 | 中文查询分析器(用于中文字段) | +| `hanlp_index` ⚠️ TODO(暂不支持) | 中文 | 中文索引分析器(用于中文字段) | +| `hanlp_standard` ⚠️ TODO(暂不支持) | 中文 | 中文查询分析器(用于中文字段) | +| `english` | 英文 | 标准英文分析器(用于英文字段) | +| `lowercase` | - | 小写标准化器(用于keyword子字段) | + +--- + +## 10. 接口级压测脚本 + +仓库提供统一压测脚本:`scripts/perf_api_benchmark.py`,用于对以下接口做并发压测: + +- 后端搜索:`POST /search/` +- 搜索建议:`GET /search/suggestions` +- 向量服务:`POST /embed/text` +- 翻译服务:`POST /translate` +- 重排服务:`POST /rerank` + +说明:脚本对 `embed_text` 场景会校验返回向量内容有效性(必须是有限数值,不允许 `null/NaN/Inf`),不是只看 HTTP 200。 + +### 10.1 快速示例 + +```bash +# suggest 压测(tenant 162) +python scripts/perf_api_benchmark.py \ + --scenario backend_suggest \ + --tenant-id 162 \ + --duration 30 \ + --concurrency 50 + +# search 压测 +python scripts/perf_api_benchmark.py \ + --scenario backend_search \ + --tenant-id 162 \ + --duration 30 \ + --concurrency 20 + +# 全链路压测(search + suggest + embedding + translate + rerank) +python scripts/perf_api_benchmark.py \ + --scenario all \ + --tenant-id 162 \ + --duration 60 \ + --concurrency 30 \ + --output perf_reports/all.json +``` + +### 10.2 自定义用例 + +可通过 `--cases-file` 覆盖默认请求模板。示例文件: + +```bash +scripts/perf_cases.json.example +``` + +执行示例: + +```bash +python scripts/perf_api_benchmark.py \ + --scenario all \ + --tenant-id 162 \ + --cases-file scripts/perf_cases.json.example \ + --duration 60 \ + --concurrency 40 +``` -- libgit2 0.21.2