Commit 16204531164db13f666377e654fd3311e38b5234

Authored by tangwang
1 parent 0342d897

docs

1 1
2 2
3 3
  4 +先阅读图片和文本embedding相关的代码:
  5 +@embeddings/README.md @embeddings/server.py @docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @embeddings/image_encoder.py @embeddings/text_encoder.py
  6 +目前有TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入限制,超限返回过载状态码。
  7 +
  8 +embedding服务(包括图片和文本的embedding),要支持 priority 查询参数,priority > 0:不计入上述 inflight、不会因准入被拒绝;
  9 +priority == 0(默认,适合做索引之类的离线任务):仍走原有 TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入;超限返回过载状态码。
  10 +priority > 0(或者==1)(适合在线请求):不会因准入被拒绝,但是仍然需要占用inflight,这样保证在线请求不被限制,并且在线请求很多的时候可以拒绝掉离线的请求。
  11 +
  12 +除了限制规则的修改,更进一步的,看有没有什么简单的机制,可以保证这种请求是优先处理的(priority=1的相比=0的更优先被处理)。
  13 +关于技术方案,有Worker + 双队列、PriorityMutex等等。
  14 +需要优先级的能力,但是只需要两档,并不需要多级的优先级。不过如果支持多级优先级的方案更成熟、稳定,那么仍然可以考虑多优先级的方案。
  15 +在支持两档优先级的情况下,简单、成熟稳定、不带来复杂度、性能、稳定性方面的副作用,是最重要的。请先了解代码、需求,深度思考解决方案
  16 +
  17 +
  18 +
  19 +
  20 +配置体系的重构。
  21 +
  22 +Referring to @docs/config-system-review-and-redesign.md , most of the modifications have been completed. Could you conduct a review to check what else needs improvement in the configuration documentation system? Are there any outstanding issues?
  23 +
  24 +一、仍然存在大量通过环境变量获取配置的地方
  25 +_SERVICE_KIND = (os.getenv("EMBEDDING_SERVICE_KIND", "all") or "all").strip().lower()
  26 +if _SERVICE_KIND not in {"all", "text", "image"}:
  27 + raise RuntimeError(
  28 + f"Invalid EMBEDDING_SERVICE_KIND={_SERVICE_KIND!r}; expected all, text, or image"
  29 + )
  30 +_TEXT_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_TEXT_MODEL", "true").lower() in ("1", "true", "yes")
  31 +_IMAGE_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_IMAGE_MODEL", "true").lower() in ("1", "true", "yes")
  32 +open_text_model = _TEXT_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "text"}
  33 +open_image_model = _IMAGE_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "image"}
  34 +
  35 +_text_encode_lock = threading.Lock()
  36 +_image_encode_lock = threading.Lock()
  37 +
  38 +_TEXT_MICROBATCH_WINDOW_SEC = max(
  39 + 0.0, float(os.getenv("TEXT_MICROBATCH_WINDOW_MS", "4")) / 1000.0
  40 +)
  41 +_TEXT_REQUEST_TIMEOUT_SEC = max(
  42 + 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30"))
  43 +)
  44 +_TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32")))
  45 +_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1")))
  46 +_OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503"))
  47 +_LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3")))
  48 +_LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120")))
  49 +_LOG_IMAGE_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_IMAGE_PREVIEW_CHARS", "180")))
  50 +_VECTOR_PREVIEW_DIMS = max(1, int(os.getenv("EMBEDDING_VECTOR_PREVIEW_DIMS", "6")))
  51 +_CACHE_PREFIX = str(REDIS_CONFIG.get("embedding_cache_prefix", "embedding")).strip() or "embedding"
  52 +
  53 +
  54 +看起来似乎并没有完全遵循这些原则?
  55 +4. 重新设计的设计原则
  56 +重新设计应遵循以下规则。
  57 +
  58 +4.1 单一逻辑配置系统
  59 +可以有多个文件,但不能有多个职责重叠的加载器。
  60 +必须有一个加载器管道,能够生成一个类型化的 AppConfig 对象。
  61 +
  62 +4.2 配置文件负责声明,解析代码负责解释,环境变量负责运行时注入
  63 +职责应明确如下:
  64 +配置文件
  65 +声明非敏感的目标行为和可部署的非敏感设置
  66 +解析逻辑
  67 +加载、合并、验证、规范化并暴露类型化的配置
  68 +绝不发明隐藏的业务行为
  69 +环境变量
  70 +承载密钥和少量运行时/进程相关的值
  71 +不随意地重新定义业务行为
  72 +
  73 +4.3 整个系统采用单一的优先级规则
  74 +除非明确豁免,否则每个配置类别都应遵循相同的合并模型。
  75 +
  76 +4.4 业务行为不得有静默的隐式后备
  77 +在启动时,如果必需的配置缺失或无效,应快速失败。
  78 +不要静默地回退到诸如硬编码语言列表之类的遗留行为。
  79 +
  80 +4.5 有效配置必须可观测
  81 +每个服务都应能够展示:
  82 +配置版本或哈希值
  83 +加载的源文件
  84 +环境名称
  85 +经过清理的有效配置
  86 +
  87 +5. 推荐的目标设计
  88 +
  89 +5.1 边界模型
  90 +使用三个清晰的层级。
  91 +层级 1:代码仓库管理的静态配置
  92 +目的:
  93 +搜索行为
  94 +租户行为
  95 +提供商/后端注册表
  96 +非敏感的服务拓扑默认值
  97 +功能开关
  98 +示例:
  99 +字段权重
  100 +查询策略
  101 +重排序融合参数
  102 +租户语言方案
  103 +翻译能力注册表
  104 +嵌入后端选择默认值
  105 +
  106 +层级 2:特定环境的层叠配置
  107 +目的:
  108 +按环境区分的非敏感差异
  109 +按环境区分的服务端点
  110 +按环境区分的资源大小默认值
  111 +开发/测试/生产环境的运维差异
  112 +示例:
  113 +本地嵌入 URL 与生产环境嵌入 URL
  114 +开发环境重排序后端与生产环境重排序后端
  115 +本地开发环境中较低的并发度
  116 +
  117 +层级 3:环境变量
  118 +目的:
  119 +密钥
  120 +绑定主机/端口
  121 +外部基础设施凭证
  122 +容器编排器的最后一步注入
  123 +示例:
  124 +ES_HOST, ES_USERNAME, ES_PASSWORD
  125 +DB_HOST, DB_USERNAME, DB_PASSWORD
  126 +REDIS_HOST, REDIS_PASSWORD
  127 +DASHSCOPE_API_KEY, DEEPL_AUTH_KEY
  128 +API_HOST, API_PORT, INDEXER_PORT, TRANSLATION_PORT
  129 +规则:
  130 +环境变量不应成为选择业务行为(如翻译模型、嵌入后端或租户语言策略)的常规途径
  131 +如果允许对非敏感字段进行环境变量覆盖,则必须将其明确列出并记录为运维覆盖手段,而非隐藏的约定
  132 +
  133 +5.2 统一的优先级
  134 +推荐的优先级顺序:
  135 +代码中的模式(schema)默认值
  136 +config/base.yaml
  137 +config/environments/<env>.yaml
  138 +来自 config/tenants/ 的租户层叠配置
  139 +针对明确允许的运行时键的环境变量
  140 +仅适用于当前进程的命令行标志
  141 +重要规则:
  142 +只能有一个模块实现此合并逻辑
  143 +任何业务模块不得直接调用 os.getenv() 来获取配置
  144 +
  145 +
  146 +
  147 +二、cursor(composer2)帮我审查的
  148 +
  149 +## 配置文档系统审查
  150 +
  151 +这是对**配置文档系统**的简要审查,对照 [`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 和当前代码库进行。
  152 +
  153 +### 实现已匹配的部分
  154 +
  155 +代码库已朝着重新设计的目标迈进了很大一步:
  156 +
  157 +- **单一类型化根节点**:`AppConfig` 位于 [`config/schema.py`](config/schema.py),通过 [`config/loader.py`](config/loader.py) 和 [`get_app_config()`](config/loader.py) 加载。
  158 +- **精简的 `services_config`**:[`config/services_config.py`](config/services_config.py) 委托给 `get_app_config()`(解决了服务层的发现 A)。
  159 +- **租户目录**:[`config/tenant_config_loader.py`](config/tenant_config_loader.py) 使用 `get_app_config().tenants`。
  160 +- **重写资产路径**:`config/dictionaries/query_rewrite.dict` 已存在(解决了发现 E 中的文件名不匹配问题)。
  161 +- **可观测性**:[`GET /admin/config`](api/routes/admin.py) 返回**经过脱敏处理的有效配置树**;[`GET /admin/config/meta`](api/routes/admin.py) 暴露环境信息、`config_hash`、`loaded_files`、`deprecated_keys`(涵盖了 §5.10 的意图;文档中提议的是 `/admin/config/effective`,但实际实现位于 `/admin/config`)。
  162 +
  163 +因此,**"单一加载器 + 有效配置可见性"** 的故事在代码中已基本实现;文档尚未完全跟上。
  164 +
  165 +---
  166 +
  167 +## 文档问题(影响最大)
  168 +
  169 +### 1. 管理 API 文档中关于 `/admin/config` 的描述错误
  170 +
  171 +[`docs/搜索API对接指南.md`](docs/搜索API对接指南.md)(管理部分附近)和 [`docs/搜索API对接指南-06-管理接口(Admin).md`](docs/搜索API对接指南-06-管理接口(Admin).md) 仍将 `/admin/config` 描述为**按租户**的 JSON(包含 `tenant_id`、`es_index_name`、`supported_languages` 等字段)。实际实现返回的是 **`AppConfig.sanitized_dict()`**(完整的应用配置,敏感信息已脱敏),而不是租户摘要字段。
  172 +
  173 +**这些指南中还缺少:** `GET /admin/config/meta`。
  174 +
  175 +**健康检查:** 拆分指南中的示例包含了 [`HealthResponse`](api/models.py) 中不存在的字段(只有 `status` 和 `elasticsearch`)。
  176 +
  177 +对于任何仅根据文档进行 API 集成的人来说,这是最明显的"未解决问题"。
  178 +
  179 +### 2. 面向开发者的指南仍将 `services_config` 作为"配置解析器"的核心
  180 +
  181 +[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §5.2 仍说搜索配置由 **`ConfigLoader`** 加载,服务由 **`config/services_config`** "解析"。§6.2 仍将 **`config/services_config.py`** 列为主要的"解析入口"。[`docs/QUICKSTART.md`](docs/QUICKSTART.md) §3.1 仍说"配置解析:`config/services_config.py`"。
  182 +
  183 +文档中准确的说法应该是:**规范入口是 `config/loader.py` + `get_app_config()`**;[`config/config_loader.py`](config/config_loader.py) 中的 `ConfigLoader` 包装了统一加载器;`services_config` 是现有调用点的**兼容性外观**。
  184 +
  185 +### 3. 重新设计文档本身不是"活的"状态文档
  186 +
  187 +[`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 读起来仍是**纯粹的问题陈述 + 目标**,没有简短的**"已实现 vs 剩余"**部分。这很容易让人假设什么都没做,或者重复工作。添加一个小附录(或一页 `config/README.md` —— 见下文)可以解决这个问题。
  188 +
  189 +### 4. 缺少 `config/README.md`(§5.3 中推荐)
  190 +
  191 +仍然没有专门的 **`config/README.md`** 来描述:加载器入口点、高级优先级、字典存放位置、指向 `/admin/config` + `/admin/config/meta` 的链接,以及重新设计文档的链接。这是重新设计中明确的交付物,可以锚定"文档系统"。
  192 +
  193 +### 5. 轻微的文档整洁问题
  194 +
  195 +- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) §1.9 环境变量项目后的行有一个多余字符:`---·`(可能是打字错误)。
  196 +- [`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §10 **文档索引**没有列出 `config-system-review-and-redesign.md` 或未来的 `config/README.md`。
  197 +
  198 +---
  199 +
  200 +## 重新设计目标与当前代码之间的差距(文档不应声称"已完成")
  201 +
  202 +这些影响文档的诚实度:
  203 +
  204 +| 主题 | 状态 |
  205 +|--------|--------|
  206 +| **`config dump` CLI**(§5.10) | `main.py` 中不存在;运维人员依赖 HTTP 或临时脚本。 |
  207 +| **隐藏的 `["en", "zh"]` 回退**(阶段 3 / 发现 D) | 仍在 [`indexer/document_transformer.py`](indexer/document_transformer.py)、[`suggestion/builder.py`](suggestion/builder.py) 等中使用。 |
  208 +| **加载器外的 `os.getenv`**(规则 1–2) | 仍在例如 [`embeddings/server.py`](embeddings/server.py)、[`reranker/server.py`](reranker/server.py)、[`api/app.py`](api/app.py) 中使用 —— 文档声称"仅加载器"将是夸大其词。 |
  209 +| **拆分 `base.yaml` / `environments/` / `tenants/*.yaml`**(阶段 5) | 未采用;仍是单一的 [`config/config.yaml`](config/config.yaml)。 |
  210 +| **遗留租户标志**(阶段 6 / 发现 H) | [`indexer/README.md`](indexer/README.md) 仍描述上游 MySQL 的 `translate_to_en` / `translate_to_zh`(这可能作为**上游模式**文档保留;需与 Python `tenant_config` 模型区分开来)。 |
  211 +
  212 +---
  213 +
  214 +## 推荐的后续步骤(仅文档,按优先级排序)
  215 +
  216 +1. **修复管理 API 文档**(合并指南 + `-06-` 拆分):`/admin/config` 的响应格式,添加 `/admin/config/meta`,使健康检查示例与 [`HealthResponse`](api/models.py) 一致。
  217 +2. **更新 DEVELOPER_GUIDE §5–§6 和 QUICKSTART §1.9 / §3.1**,将 **`get_app_config()` / `loader.py`** 描述为主要入口,将 **`services_config`** 描述为适配器。
  218 +3. **添加 `config/README.md`**(简短的操作 + 开发者入口)。
  219 +4. **在 `config-system-review-and-redesign.md` 中添加**带日期的**实现状态表**(已交付 vs 推迟的内容),使审查文档不与现实矛盾。
  220 +5. **DEVELOPER_GUIDE §9 检查清单**:将"配置来自 `services_config`"替换为允许 `get_app_config()` 或精简适配器的语言,与 §6 保持一致。
  221 +
  222 +如果需要,我可以在后续处理中为项目 1–3 和重新设计文档中的简短状态块应用补丁。
  223 +
  224 +
  225 +
  226 +
  227 +
  228 +
  229 +
  230 +
  231 +
  232 +
  233 +
  234 +
  235 +
4 nllb-200-distilled-600M性能优化 236 nllb-200-distilled-600M性能优化
5 已完成(2026-03) 237 已完成(2026-03)
6 - CTranslate2 迁移 + float16 转换 238 - CTranslate2 迁移 + float16 转换
docs/搜索API速查表.md renamed to docs/搜索API对接指南-速查表.md
docs/搜索API对接指南—拆分前版本存档.md deleted
@@ -1,2495 +0,0 @@ @@ -1,2495 +0,0 @@
1 -# 搜索API接口对接指南  
2 -  
3 -本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。  
4 -  
5 -## 目录  
6 -  
7 -1. [快速开始](#快速开始)  
8 - - 1.1 [基础信息](#11-基础信息)  
9 - - 1.2 [最简单的搜索请求](#12-最简单的搜索请求)  
10 - - 1.3 [带过滤与分页的搜索](#13-带过滤与分页的搜索)  
11 - - 1.4 [开启分面的搜索](#14-开启分面的搜索)  
12 -  
13 -2. [接口概览](#接口概览)  
14 -  
15 -3. [搜索接口](#搜索接口)  
16 - - 3.1 [接口信息](#31-接口信息)  
17 - - 3.2 [请求参数](#32-请求参数)  
18 - - 3.3 [过滤器详解](#33-过滤器详解)  
19 - - 3.4 [分面配置](#34-分面配置)  
20 - - 3.5 [SKU筛选维度](#35-sku筛选维度)  
21 - - 3.6 [搜索建议接口](#37-搜索建议接口)  
22 - - 3.7 [即时搜索接口](#38-即时搜索接口)  
23 - - 3.8 [获取单个文档](#39-获取单个文档)  
24 -  
25 -4. [响应格式说明](#响应格式说明)  
26 - - 4.1 [标准响应结构](#41-标准响应结构)  
27 - - 4.2 [响应字段说明](#42-响应字段说明)  
28 - - 4.2.1 [query_info 说明](#421-query_info-说明)  
29 - - 4.3 [SpuResult字段说明](#43-spuresult字段说明)  
30 - - 4.4 [SkuResult字段说明](#44-skuresult字段说明)  
31 - - 4.5 [多语言字段说明](#45-多语言字段说明)  
32 -  
33 -5. [索引接口](#索引接口)  
34 - - 5.0 [支撑外部 indexer 的三种方式](#50-支撑外部-indexer-的三种方式)  
35 - - 5.1 [为租户创建索引](#51-为租户创建索引)  
36 - - 5.2 [全量索引接口](#52-全量索引接口)  
37 - - 5.3 [增量索引接口](#53-增量索引接口)  
38 - - 5.4 [查询文档接口](#54-查询文档接口)  
39 - - 5.5 [索引健康检查接口](#55-索引健康检查接口)  
40 - - 5.6 [文档构建接口(正式对接)](#56-文档构建接口正式对接推荐)  
41 - - 5.7 [文档构建接口(测试/自测)](#57-文档构建接口测试--自测)  
42 - - 5.8 [内容理解字段生成接口](#58-内容理解字段生成接口)  
43 -  
44 -6. [管理接口](#管理接口)  
45 - - 6.1 [健康检查](#61-健康检查)  
46 - - 6.2 [获取配置](#62-获取配置)  
47 - - 6.3 [索引统计](#63-索引统计)  
48 -  
49 -7. [微服务接口(向量、重排、翻译、内容理解)](#7-微服务接口向量重排翻译)  
50 - - 7.1 [向量服务(Embedding)](#71-向量服务embedding)  
51 - - 7.2 [重排服务(Reranker)](#72-重排服务reranker)  
52 - - 7.3 [翻译服务(Translation)](#73-翻译服务translation)  
53 - - 7.4 [内容理解字段生成(Indexer 服务内)](#74-内容理解字段生成indexer-服务内)  
54 -  
55 -8. [常见场景示例](#8-常见场景示例)  
56 - - 8.1 [基础搜索与排序](#81-基础搜索与排序)  
57 - - 8.2 [过滤搜索](#82-过滤搜索)  
58 - - 8.3 [分面搜索](#83-分面搜索)  
59 - - 8.4 [规格过滤与分面](#84-规格过滤与分面)  
60 - - 8.5 [SKU筛选](#85-sku筛选)  
61 - - 8.6 [分页查询](#87-分页查询)  
62 -  
63 -9. [数据模型](#9-数据模型)  
64 - - 9.1 [商品字段定义](#91-商品字段定义)  
65 - - 9.2 [字段类型速查](#92-字段类型速查)  
66 - - 9.3 [常用字段列表](#93-常用字段列表)  
67 - - 9.4 [支持的分析器](#94-支持的分析器)  
68 -  
69 ----  
70 -  
71 -## 快速开始  
72 -  
73 -### 1.1 基础信息  
74 -  
75 -- **Base URL**: `http://43.166.252.75:6002`  
76 -- **协议**: HTTP/HTTPS  
77 -- **数据格式**: JSON  
78 -- **字符编码**: UTF-8  
79 -- **请求方法**: POST(搜索接口)  
80 -  
81 -**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递,不在请求体中。  
82 -  
83 -**环境与凭证**:MySQL、Redis、Elasticsearch 等外部服务的 AI 生产地址与凭证见 [QUICKSTART.md §1.6](./QUICKSTART.md#16-外部服务与-env含生产凭证)。  
84 -  
85 -### 1.2 最简单的搜索请求  
86 -  
87 -```bash  
88 -curl -X POST "http://43.166.252.75:6002/search/" \  
89 - -H "Content-Type: application/json" \  
90 - -H "X-Tenant-ID: 162" \  
91 - -d '{"query": "芭比娃娃"}'  
92 -```  
93 -  
94 -### 1.3 带过滤与分页的搜索  
95 -  
96 -```bash  
97 -curl -X POST "http://43.166.252.75:6002/search/" \  
98 - -H "Content-Type: application/json" \  
99 - -H "X-Tenant-ID: 162" \  
100 - -d '{  
101 - "query": "芭比娃娃",  
102 - "size": 5,  
103 - "from": 10,  
104 - "range_filters": {  
105 - "min_price": {  
106 - "gte": 50,  
107 - "lte": 200  
108 - },  
109 - "create_time": {  
110 - "gte": "2020-01-01T00:00:00Z"  
111 - }  
112 - },  
113 - "sort_by": "price",  
114 - "sort_order": "asc"  
115 - }'  
116 -```  
117 -  
118 -### 1.4 开启分面的搜索  
119 -  
120 -```bash  
121 -curl -X POST "http://43.166.252.75:6002/search/" \  
122 - -H "Content-Type: application/json" \  
123 - -H "X-Tenant-ID: 162" \  
124 - -d '{  
125 - "query": "芭比娃娃",  
126 - "facets": [  
127 - {"field": "category1_name", "size": 10, "type": "terms"},  
128 - {"field": "specifications.color", "size": 10, "type": "terms"},  
129 - {"field": "specifications.size", "size": 10, "type": "terms"}  
130 - ],  
131 - "min_score": 0.2  
132 - }'  
133 -```  
134 -  
135 ----  
136 -  
137 -## 接口概览  
138 -  
139 -| 接口 | HTTP Method | Endpoint | 说明 |  
140 -|------|------|------|------|  
141 -| 搜索 | POST | `/search/` | 执行搜索查询 |  
142 -| 搜索建议 | GET | `/search/suggestions` | 搜索建议(自动补全/热词,多语言) |  
143 -| 即时搜索 | GET | `/search/instant` | 即时搜索预留接口(当前返回 `501 Not Implemented`) |  
144 -| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 |  
145 -| 全量索引 | POST | `/indexer/reindex` | 全量索引接口(导入数据,不删除索引,仅推荐自测使用) |  
146 -| 增量索引 | POST | `/indexer/index` | 增量索引接口(指定SPU ID列表进行索引,支持自动检测删除和显式删除,仅推荐自测使用) |  
147 -| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据(不写入ES) |  
148 -| 构建ES文档(正式对接) | POST | `/indexer/build-docs` | 基于上游提供的 MySQL 行数据构建 ES doc,不写入 ES,供 Java 等调用后自行写入 |  
149 -| 构建ES文档(测试用) | POST | `/indexer/build-docs-from-db` | 仅在测试/调试时使用,根据 `tenant_id + spu_ids` 内部查库并构建 ES doc |  
150 -| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags,供微服务组合方式使用 |  
151 -| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 |  
152 -| 健康检查 | GET | `/admin/health` | 服务健康检查 |  
153 -| 获取配置 | GET | `/admin/config` | 获取租户配置 |  
154 -| 索引统计 | GET | `/admin/stats` | 获取租户索引统计信息(需 tenant_id) |  
155 -  
156 -**微服务(独立端口或 Indexer 内,外部可直连)**:  
157 -  
158 -| 服务 | 端口 | 接口 | 说明 |  
159 -|------|------|------|------|  
160 -| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 |  
161 -| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 |  
162 -| 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) |  
163 -| 重排服务 | 6007 | `POST /rerank` | 检索结果重排 |  
164 -| 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 |  
165 -  
166 ----  
167 -  
168 -## 搜索接口  
169 -  
170 -### 3.1 接口信息  
171 -  
172 -- **端点**: `POST /search/`  
173 -- **描述**: 执行文本搜索查询,支持多语言、过滤器和分面搜索  
174 -- **租户标识**:`tenant_id` 通过 HTTP 请求头 **`X-Tenant-ID`** 传递(推荐);也可通过 URL query 参数 **`tenant_id`** 传递。**不要放在请求体中。**  
175 -  
176 -**请求示例(推荐)**:  
177 -```python  
178 -url = f"{base_url.rstrip('/')}/search/"  
179 -headers = {  
180 - "Content-Type": "application/json",  
181 - "X-Tenant-ID": "162", # 租户ID,必填  
182 -}  
183 -response = requests.post(url, headers=headers, json={"query": "芭比娃娃"})  
184 -```  
185 -  
186 -### 3.2 请求参数  
187 -  
188 -#### 完整请求体结构  
189 -  
190 -```json  
191 -{  
192 - "query": "string (required)",  
193 - "size": 10,  
194 - "from": 0,  
195 - "language": "zh",  
196 - "filters": {},  
197 - "range_filters": {},  
198 - "facets": [],  
199 - "sort_by": "string",  
200 - "sort_order": "desc",  
201 - "min_score": 0.0,  
202 - "sku_filter_dimension": ["string"],  
203 - "debug": false,  
204 - "enable_rerank": null,  
205 - "rerank_query_template": "{query}",  
206 - "rerank_doc_template": "{title}",  
207 - "user_id": "string",  
208 - "session_id": "string"  
209 -}  
210 -```  
211 -  
212 -#### 参数详细说明  
213 -  
214 -| 参数 | 类型 | 必填 | 默认值 | 说明 |  
215 -|------|------|------|--------|------|  
216 -| `query` | string | Y | - | 搜索查询字符串(统一文本检索策略) |  
217 -| `size` | integer | N | 10 | 返回结果数量(1-100) |  
218 -| `from` | integer | N | 0 | 分页偏移量(用于分页) |  
219 -| `language` | string | N | "zh" | 返回语言:`zh`(中文)或 `en`(英文)。后端会根据此参数选择对应的中英文字段返回 |  
220 -| `filters` | object | N | null | 精确匹配过滤器(见[过滤器详解](#33-过滤器详解)) |  
221 -| `range_filters` | object | N | null | 数值范围过滤器(见[过滤器详解](#33-过滤器详解)) |  
222 -| `facets` | array | N | null | 分面配置(见[分面配置](#34-分面配置)) |  
223 -| `sort_by` | string | N | null | 排序字段名。支持:`price`(价格)、`sales`(销量)、`create_time`(创建时间)、`update_time`(更新时间)。默认按相关性排序 |  
224 -| `sort_order` | string | N | "desc" | 排序方向:`asc`(升序)或 `desc`(降序)。注意:`price`+`asc`=价格从低到高,`price`+`desc`=价格从高到低(后端自动映射为min_price或max_price) |  
225 -| `min_score` | float | N | null | 最小相关性分数阈值 |  
226 -| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) |  
227 -| `debug` | boolean | N | false | 是否返回调试信息 |  
228 -| `enable_rerank` | boolean/null | N | null | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。不传/传 null 使用服务端 `rerank.enabled`(默认开启)。开启后会先对 ES TopN(`rerank_window`)重排,再按分页截取;若 `from+size>1000`,则不重排,直接按分页从 ES 返回 |  
229 -| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 |  
230 -| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 |  
231 -| `user_id` | string | N | null | 用户ID(用于个性化,预留) |  
232 -| `session_id` | string | N | null | 会话ID(用于分析,预留) |  
233 -  
234 -### 3.3 过滤器详解  
235 -  
236 -#### 3.3.1 精确匹配过滤器 (filters)  
237 -  
238 -用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。  
239 -  
240 -**格式**:  
241 -```json  
242 -{  
243 - "filters": {  
244 - "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
245 - "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
246 - "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
247 - "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
248 - "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
249 - "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR)  
250 - "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签  
251 - "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段  
252 - // specifications 嵌套过滤(特殊格式)  
253 - "specifications": {  
254 - "name": "color",  
255 - "value": "white"  
256 - }  
257 - }  
258 -}  
259 -```  
260 -  
261 -**支持的值类型**:  
262 -- 字符串:精确匹配  
263 -- 整数:精确匹配  
264 -- 布尔值:精确匹配  
265 -- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值)  
266 -- 对象:specifications 嵌套过滤(见下文)  
267 -  
268 -**`*_all` 语义(多值 AND)**:  
269 -- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。  
270 -- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。  
271 -- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。  
272 -  
273 -**Specifications 嵌套过滤**:  
274 -  
275 -`specifications` 是嵌套字段,支持按规格名称和值进行过滤。  
276 -  
277 -**单个规格过滤**:  
278 -```json  
279 -{  
280 - "filters": {  
281 - "specifications": {  
282 - "name": "color",  
283 - "value": "white"  
284 - }  
285 - }  
286 -}  
287 -```  
288 -查询规格名称为"color"且值为"white"的商品。  
289 -  
290 -**多个规格过滤(按维度分组)**:  
291 -```json  
292 -{  
293 - "filters": {  
294 - "specifications": [  
295 - {"name": "color", "value": "white"},  
296 - {"name": "size", "value": "256GB"}  
297 - ]  
298 - }  
299 -}  
300 -```  
301 -查询同时满足所有规格的商品(color=white **且** size=256GB)。  
302 -  
303 -**相同维度的多个值(OR 逻辑)**:  
304 -```json  
305 -{  
306 - "filters": {  
307 - "specifications": [  
308 - {"name": "size", "value": "3"},  
309 - {"name": "size", "value": "4"},  
310 - {"name": "size", "value": "5"},  
311 - {"name": "color", "value": "green"}  
312 - ]  
313 - }  
314 -}  
315 -```  
316 -查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。  
317 -  
318 -**过滤逻辑说明**:  
319 -- **不同维度**(不同的 `name`)之间是 **AND** 关系(求交集)  
320 -- **相同维度**(相同的 `name`)的多个值之间是 **OR** 关系(求并集)  
321 -  
322 -**常用过滤字段**(详见[常用字段列表](#83-常用字段列表)):  
323 -- `category_name`: 类目名称  
324 -- `category1_name`, `category2_name`, `category3_name`: 多级类目  
325 -- `category_id`: 类目ID  
326 -- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段)  
327 -- `tags`: 标签(keyword类型,支持数组)  
328 -- `option1_name`, `option2_name`, `option3_name`: 选项名称  
329 -- `specifications`: 规格过滤(嵌套字段,格式见上文)  
330 -- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。  
331 -  
332 -#### 3.3.2 范围过滤器 (range_filters)  
333 -  
334 -用于数值字段的范围过滤。  
335 -  
336 -**格式**:  
337 -```json  
338 -{  
339 - "range_filters": {  
340 - "min_price": {  
341 - "gte": 50, // 大于等于  
342 - "lte": 200 // 小于等于  
343 - },  
344 - "max_price": {  
345 - "gt": 100 // 大于  
346 - },  
347 - "create_time": {  
348 - "gte": "2024-01-01T00:00:00Z" // 日期时间字符串  
349 - }  
350 - }  
351 -}  
352 -```  
353 -  
354 -**支持的操作符**:  
355 -- `gte`: 大于等于 (>=)  
356 -- `gt`: 大于 (>)  
357 -- `lte`: 小于等于 (<=)  
358 -- `lt`: 小于 (<)  
359 -  
360 -**注意**: 至少需要指定一个操作符。  
361 -  
362 -**常用范围字段**(详见[常用字段列表](#83-常用字段列表)):  
363 -- `min_price`: 最低价格  
364 -- `max_price`: 最高价格  
365 -- `compare_at_price`: 原价  
366 -- `create_time`: 创建时间  
367 -- `update_time`: 更新时间  
368 -  
369 -### 3.4 分面配置  
370 -  
371 -用于生成分面统计(分组聚合),常用于构建筛选器UI。  
372 -  
373 -#### 3.4.1 配置格式  
374 -  
375 -```json  
376 -{  
377 - "facets": [  
378 - {  
379 - "field": "category1_name",  
380 - "size": 15,  
381 - "type": "terms",  
382 - "disjunctive": false  
383 - },  
384 - {  
385 - "field": "brand_name",  
386 - "size": 10,  
387 - "type": "terms",  
388 - "disjunctive": true  
389 - },  
390 - {  
391 - "field": "specifications.color",  
392 - "size": 20,  
393 - "type": "terms",  
394 - "disjunctive": true  
395 - },  
396 - {  
397 - "field": "min_price",  
398 - "type": "range",  
399 - "ranges": [  
400 - {"key": "0-50", "to": 50},  
401 - {"key": "50-100", "from": 50, "to": 100},  
402 - {"key": "100-200", "from": 100, "to": 200},  
403 - {"key": "200+", "from": 200}  
404 - ]  
405 - }  
406 - ]  
407 -}  
408 -```  
409 -  
410 -#### 3.4.2 Facet 字段说明  
411 -  
412 -| 字段 | 类型 | 必填 | 默认值 | 说明 |  
413 -|------|------|------|--------|------|  
414 -| `field` | string | 是 | - | 分面字段名 |  
415 -| `size` | int | 否 | 10 | 返回的分面值数量(1-100) |  
416 -| `type` | string | 否 | "terms" | 分面类型:`terms`(词条聚合)或 `range`(范围聚合) |  
417 -| `disjunctive` | bool | 否 | false | 是否支持多选(disjunctive faceting)。启用后,选中该分面的过滤器时,仍会显示其他可选项 |  
418 -| `ranges` | array | 否 | null | 范围配置(仅 `type="range"` 时需要) |  
419 -  
420 -#### 3.4.3 disjunctive字段说明  
421 -  
422 -**重要特性**: `disjunctive` 字段控制分面的行为模式。启用后,选中该分面的过滤器时,仍会显示其他可选项  
423 -  
424 -**标准模式 (disjunctive: false)**:  
425 -- **行为**: 选中某个分面值后,该分面只显示选中的值  
426 -- **适用场景**: 层级类目、互斥选择  
427 -- **示例**: 类目下钻(玩具 > 娃娃 > 芭比)  
428 -  
429 -**Multi-Select 模式 (disjunctive: true)** ⭐:  
430 -- **行为**: 选中某个分面值后,该分面仍显示所有可选项  
431 -- **适用场景**: 颜色、品牌、尺码等可切换属性  
432 -- **示例**: 选择了"红色"后,仍能看到"蓝色"、"绿色"等选项  
433 -  
434 -**推荐配置**:  
435 -  
436 -| 分面类型 | disjunctive | 原因 |  
437 -|---------|-------------|------|  
438 -| 颜色 | `true` | 用户需要切换颜色 |  
439 -| 品牌 | `true` | 用户需要比较品牌 |  
440 -| 尺码 | `true` | 用户需要查看其他尺码 |  
441 -| 类目 | `false` | 层级下钻 |  
442 -| 价格区间 | `false` | 互斥选择 |  
443 -  
444 -#### 3.4.4 规格分面说明  
445 -  
446 -`specifications` 是嵌套字段,支持两种分面模式:  
447 -  
448 -**模式1:所有规格名称的分面**:  
449 -```json  
450 -{  
451 - "facets": [  
452 - {  
453 - "field": "specifications",  
454 - "size": 10,  
455 - "type": "terms"  
456 - }  
457 - ]  
458 -}  
459 -```  
460 -返回所有规格名称(name)及其对应的值(value)列表。每个 name 会生成一个独立的分面结果。  
461 -  
462 -**模式2:指定规格名称的分面**:  
463 -```json  
464 -{  
465 - "facets": [  
466 - {  
467 - "field": "specifications.color",  
468 - "size": 20,  
469 - "type": "terms",  
470 - "disjunctive": true  
471 - },  
472 - {  
473 - "field": "specifications.size",  
474 - "size": 15,  
475 - "type": "terms",  
476 - "disjunctive": true  
477 - }  
478 - ]  
479 -}  
480 -```  
481 -只返回指定规格名称的值列表。格式:`specifications.{name}`,其中 `{name}` 是规格名称(如"color"、"size"、"material")。  
482 -  
483 -**返回格式示例**:  
484 -```json  
485 -{  
486 - "facets": [  
487 - {  
488 - "field": "specifications.color",  
489 - "label": "color",  
490 - "type": "terms",  
491 - "values": [  
492 - {"value": "white", "count": 50, "selected": true}, // ✓ selected 字段由后端标记  
493 - {"value": "black", "count": 30, "selected": false},  
494 - {"value": "red", "count": 20, "selected": false}  
495 - ]  
496 - },  
497 - {  
498 - "field": "specifications.size",  
499 - "label": "size",  
500 - "type": "terms",  
501 - "values": [  
502 - {"value": "256GB", "count": 40, "selected": false},  
503 - {"value": "512GB", "count": 20, "selected": false}  
504 - ]  
505 - }  
506 - ]  
507 -}  
508 -```  
509 -  
510 -### 3.5 SKU筛选维度  
511 -  
512 -**功能说明**:  
513 -`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式(子 SKU)维度**,为字符串列表。  
514 -在店铺的 **主题装修配置** 中,商家可以为店铺设置一个或多个子款式筛选维度(例如 `color`、`size`),前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表,用户可以通过点击不同维度值(如不同颜色)来切换展示的子款式。  
515 -当指定 `sku_filter_dimension` 后,后端会根据店铺的这项配置,从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据:系统会按指定维度**组合**对 SKU 进行分组,每个维度组合只返回第一个 SKU(从简实现,选择该组合下的第一款),其余不在这些维度组合中的子 SKU 将不返回。  
516 -  
517 -**支持的维度值**:  
518 -1. **直接选项字段**: `option1`、`option2`、`option3`  
519 - - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组  
520 -  
521 -2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配  
522 - - 例如:如果 `option1_name` 为 `"color"`,则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组  
523 -  
524 -**示例**:  
525 -  
526 -**按颜色筛选(假设 option1_name = "color")**:  
527 -```json  
528 -{  
529 - "query": "芭比娃娃",  
530 - "sku_filter_dimension": ["color"]  
531 -}  
532 -```  
533 -  
534 -**按选项1筛选**:  
535 -```json  
536 -{  
537 - "query": "芭比娃娃",  
538 - "sku_filter_dimension": ["option1"]  
539 -}  
540 -```  
541 -  
542 -**按颜色 + 尺寸组合筛选(假设 option1_name = "color", option2_name = "size")**:  
543 -```json  
544 -{  
545 - "query": "芭比娃娃",  
546 - "sku_filter_dimension": ["color", "size"]  
547 -}  
548 -```  
549 -  
550 -### 3.7 搜索建议接口  
551 -  
552 -- **端点**: `GET /search/suggestions`  
553 -- **描述**: 返回搜索建议(自动补全/热词),支持多语言。  
554 -  
555 -#### 查询参数  
556 -  
557 -| 参数 | 类型 | 必填 | 默认值 | 描述 |  
558 -|------|------|------|--------|------|  
559 -| `q` | string | Y | - | 查询字符串(至少 1 个字符) |  
560 -| `size` | integer | N | 10 | 返回建议数量(1-50) |  
561 -| `language` | string | N | `en` | 请求语言,如 `zh` / `en` / `ar` / `ru`,用于路由到对应语种 suggestion 索引 |  
562 -| `debug` | bool | N | `false` | 是否开启调试(目前主要用于排查 suggestion 排序与语言解析) |  
563 -  
564 -> **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。  
565 -  
566 -#### 响应示例  
567 -  
568 -```json  
569 -{  
570 - "query": "iph",  
571 - "language": "en",  
572 - "resolved_language": "en",  
573 - "suggestions": [  
574 - {  
575 - "text": "iphone 15",  
576 - "lang": "en",  
577 - "score": 12.37,  
578 - "rank_score": 5.1,  
579 - "sources": ["query_log", "qanchor"],  
580 - "lang_source": "log_field",  
581 - "lang_confidence": 1.0,  
582 - "lang_conflict": false  
583 - }  
584 - ],  
585 - "took_ms": 12  
586 -}  
587 -```  
588 -  
589 -#### 请求示例  
590 -  
591 -```bash  
592 -curl "http://localhost:6002/search/suggestions?q=芭&size=5&language=zh" \  
593 - -H "X-Tenant-ID: 162"  
594 -```  
595 -  
596 -### 3.8 即时搜索接口  
597 -  
598 -> ⚠️ 当前版本未开放该能力。接口会明确返回 `501 Not Implemented`,避免误用未完成实现。  
599 -  
600 -- **端点**: `GET /search/instant`  
601 -- **描述**: 即时搜索预留端点,后续会在独立实现完成后开放。  
602 -  
603 -#### 查询参数  
604 -  
605 -| 参数 | 类型 | 必填 | 默认值 | 描述 |  
606 -|------|------|------|--------|------|  
607 -| `q` | string | Y | - | 搜索查询(至少 2 个字符) |  
608 -| `size` | integer | N | 5 | 返回结果数量(1-20) |  
609 -  
610 -#### 请求示例  
611 -  
612 -```bash  
613 -curl "http://localhost:6002/search/instant?q=玩具&size=5"  
614 -```  
615 -  
616 -#### 当前响应  
617 -  
618 -```json  
619 -{  
620 - "error": "/search/instant is not implemented yet. Use POST /search/ for production traffic.",  
621 - "status_code": 501  
622 -}  
623 -```  
624 -  
625 -### 3.9 获取单个文档  
626 -  
627 -- **端点**: `GET /search/{doc_id}`  
628 -- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。  
629 -- **租户标识**:同 [3.1](#31-接口信息),通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递。  
630 -  
631 -#### 路径参数  
632 -  
633 -| 参数 | 类型 | 描述 |  
634 -|------|------|------|  
635 -| `doc_id` | string | 商品或文档 ID |  
636 -  
637 -#### 响应示例  
638 -  
639 -```json  
640 -{  
641 - "id": "12345",  
642 - "source": {  
643 - "title": {  
644 - "zh": "芭比时尚娃娃"  
645 - },  
646 - "min_price": 89.99,  
647 - "category1_name": "玩具"  
648 - }  
649 -}  
650 -```  
651 -  
652 -#### 请求示例  
653 -  
654 -```bash  
655 -curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162"  
656 -# 或使用 query 参数:curl "http://localhost:6002/search/12345?tenant_id=162"  
657 -```  
658 -  
659 ----  
660 -  
661 -## 响应格式说明  
662 -  
663 -### 4.1 标准响应结构  
664 -  
665 -```json  
666 -{  
667 - "results": [  
668 - {  
669 - "spu_id": "12345",  
670 - "title": "芭比时尚娃娃",  
671 - "brief": "高品质芭比娃娃",  
672 - "description": "详细描述...",  
673 - "vendor": "美泰",  
674 - "category": "玩具",  
675 - "category_path": "玩具/娃娃/时尚",  
676 - "category_name": "时尚",  
677 - "category_id": "cat_001",  
678 - "category_level": 3,  
679 - "category1_name": "玩具",  
680 - "category2_name": "娃娃",  
681 - "category3_name": "时尚",  
682 - "tags": ["娃娃", "玩具", "女孩"],  
683 - "price": 89.99,  
684 - "compare_at_price": 129.99,  
685 - "currency": "USD",  
686 - "image_url": "https://example.com/image.jpg",  
687 - "in_stock": true,  
688 - "sku_prices": [89.99, 99.99, 109.99],  
689 - "sku_weights": [100, 150, 200],  
690 - "sku_weight_units": ["g", "g", "g"],  
691 - "total_inventory": 500,  
692 - "option1_name": "color",  
693 - "option2_name": "size",  
694 - "option3_name": null,  
695 - "specifications": [  
696 - {"sku_id": "sku_001", "name": "color", "value": "pink"},  
697 - {"sku_id": "sku_001", "name": "size", "value": "standard"}  
698 - ],  
699 - "skus": [  
700 - {  
701 - "sku_id": "67890",  
702 - "price": 89.99,  
703 - "compare_at_price": 129.99,  
704 - "sku": "BARBIE-001",  
705 - "stock": 100,  
706 - "weight": 0.1,  
707 - "weight_unit": "kg",  
708 - "option1_value": "pink",  
709 - "option2_value": "standard",  
710 - "option3_value": null,  
711 - "image_src": "https://example.com/sku1.jpg"  
712 - }  
713 - ],  
714 - "relevance_score": 8.5  
715 - }  
716 - ],  
717 - "total": 118,  
718 - "max_score": 8.5,  
719 - "facets": [  
720 - {  
721 - "field": "category1_name",  
722 - "label": "category1_name",  
723 - "type": "terms",  
724 - "values": [  
725 - {  
726 - "value": "玩具",  
727 - "label": "玩具",  
728 - "count": 85,  
729 - "selected": false  
730 - }  
731 - ]  
732 - },  
733 - {  
734 - "field": "specifications.color",  
735 - "label": "color",  
736 - "type": "terms",  
737 - "values": [  
738 - {  
739 - "value": "pink",  
740 - "label": "pink",  
741 - "count": 30,  
742 - "selected": false  
743 - }  
744 - ]  
745 - }  
746 - ],  
747 - "query_info": {  
748 - "original_query": "芭比娃娃",  
749 - "query_normalized": "芭比娃娃",  
750 - "rewritten_query": "芭比娃娃",  
751 - "detected_language": "zh",  
752 - "translations": {  
753 - "en": "barbie doll"  
754 - },  
755 - "domain": "default"  
756 - },  
757 - "suggestions": [],  
758 - "related_searches": [],  
759 - "took_ms": 45,  
760 - "performance_info": null,  
761 - "debug_info": null  
762 -}  
763 -```  
764 -  
765 -### 4.2 响应字段说明  
766 -  
767 -| 字段 | 类型 | 说明 |  
768 -|------|------|------|  
769 -| `results` | array | 搜索结果列表(SpuResult对象数组) |  
770 -| `results[].spu_id` | string | SPU ID |  
771 -| `results[].title` | string | 商品标题 |  
772 -| `results[].price` | float | 价格(min_price) |  
773 -| `results[].skus` | array | SKU列表(如果指定了`sku_filter_dimension`,则按维度过滤后的SKU) |  
774 -| `results[].relevance_score` | float | 相关性分数 |  
775 -| `total` | integer | 匹配的总文档数 |  
776 -| `max_score` | float | 最高相关性分数 |  
777 -| `facets` | array | 分面统计结果 |  
778 -| `query_info` | object | query处理信息 |  
779 -| `took_ms` | integer | 搜索耗时(毫秒) |  
780 -| `debug_info` | object/null | 调试信息,仅当请求传 `debug=true` 时返回 |  
781 -  
782 -#### 4.2.1 query_info 说明  
783 -  
784 -`query_info` 包含本次搜索的查询解析与处理结果:  
785 -  
786 -| 子字段 | 类型 | 说明 |  
787 -|--------|------|------|  
788 -| `original_query` | string | 用户原始查询 |  
789 -| `query_normalized` | string | 归一化后的查询(去空白、大小写等预处理,用于后续解析与改写) |  
790 -| `rewritten_query` | string | 重写后的查询(同义词/词典扩展等) |  
791 -| `detected_language` | string | 检测到的查询语言(如 `zh`、`en`) |  
792 -| `translations` | object | 翻译结果,键为语言代码,值为翻译文本 |  
793 -| `domain` | string | 查询域(如 `default`、`title`、`brand` 等) |  
794 -  
795 -#### 4.2.2 debug_info 说明  
796 -  
797 -`debug_info` 主要用于检索效果评估、融合打分分析与 bad case 排查。  
798 -  
799 -`debug_info.query_analysis` 常见字段:  
800 -  
801 -| 子字段 | 类型 | 说明 |  
802 -|--------|------|------|  
803 -| `original_query` | string | 原始查询 |  
804 -| `query_normalized` | string | 归一化后的查询 |  
805 -| `rewritten_query` | string | 重写后的查询 |  
806 -| `detected_language` | string | 检测到的语言 |  
807 -| `translations` | object | 翻译结果 |  
808 -| `query_text_by_lang` | object | 实际参与检索的多语言 query 文本 |  
809 -| `search_langs` | array[string] | 实际参与检索的语言列表 |  
810 -| `supplemental_search_langs` | array[string] | 因 mixed query 补入的附加语言列表 |  
811 -| `has_vector` | boolean | 是否生成了向量 |  
812 -  
813 -`debug_info.per_result[]` 常见字段:  
814 -  
815 -| 子字段 | 类型 | 说明 |  
816 -|--------|------|------|  
817 -| `spu_id` | string | 结果 SPU ID |  
818 -| `es_score` | float | ES 原始 `_score` |  
819 -| `rerank_score` | float | 重排分数 |  
820 -| `text_score` | float | 文本相关性大分(由 `base_query` / `base_query_trans_*` / `fallback_original_query_*` 聚合而来) |  
821 -| `text_source_score` | float | `base_query` 分数 |  
822 -| `text_translation_score` | float | `base_query_trans_*` 里的最大分数 |  
823 -| `text_fallback_score` | float | `fallback_original_query_*` 里的最大分数 |  
824 -| `text_primary_score` | float | 文本大分中的主证据部分 |  
825 -| `text_support_score` | float | 文本大分中的辅助证据部分 |  
826 -| `knn_score` | float | `knn_query` 分数 |  
827 -| `fused_score` | float | 最终融合分数 |  
828 -| `matched_queries` | object/array | ES named queries 命中详情 |  
829 -  
830 -### 4.3 SpuResult字段说明  
831 -  
832 -| 字段 | 类型 | 说明 |  
833 -|------|------|------|  
834 -| `spu_id` | string | SPU ID |  
835 -| `title` | string | 商品标题(根据language参数自动选择 `title.zh` 或 `title.en`) |  
836 -| `brief` | string | 商品短描述(根据language参数自动选择) |  
837 -| `description` | string | 商品详细描述(根据language参数自动选择) |  
838 -| `vendor` | string | 供应商/品牌(根据language参数自动选择) |  
839 -| `category` | string | 类目(兼容字段,等同于category_name) |  
840 -| `category_path` | string | 类目路径(多级,用于面包屑,根据language参数自动选择) |  
841 -| `category_name` | string | 类目名称(展示用,根据language参数自动选择) |  
842 -| `category_id` | string | 类目ID |  
843 -| `category_level` | integer | 类目层级(1/2/3) |  
844 -| `category1_name` | string | 一级类目名称 |  
845 -| `category2_name` | string | 二级类目名称 |  
846 -| `category3_name` | string | 三级类目名称 |  
847 -| `tags` | array[string] | 标签列表 |  
848 -| `price` | float | 价格(min_price) |  
849 -| `compare_at_price` | float | 原价 |  
850 -| `currency` | string | 货币单位(默认USD) |  
851 -| `image_url` | string | 主图URL |  
852 -| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) |  
853 -| `sku_prices` | array[float] | 所有SKU价格列表 |  
854 -| `sku_weights` | array[integer] | 所有SKU重量列表 |  
855 -| `sku_weight_units` | array[string] | 所有SKU重量单位列表 |  
856 -| `total_inventory` | integer | 总库存 |  
857 -| `sales` | integer | 销量(展示销量) |  
858 -| `option1_name` | string | 选项1名称(如"color") |  
859 -| `option2_name` | string | 选项2名称(如"size") |  
860 -| `option3_name` | string | 选项3名称 |  
861 -| `specifications` | array[object] | 规格列表(与ES specifications字段对应) |  
862 -| `skus` | array | SKU 列表 |  
863 -| `relevance_score` | float | 相关性分数(默认为 ES 原始分数;当开启 AI 搜索时为融合后的最终分数) |  
864 -  
865 -### 4.4 SkuResult字段说明  
866 -  
867 -| 字段 | 类型 | 说明 |  
868 -|------|------|------|  
869 -| `sku_id` | string | SKU ID |  
870 -| `price` | float | 价格 |  
871 -| `compare_at_price` | float | 原价 |  
872 -| `sku` | string | SKU编码(sku_code) |  
873 -| `stock` | integer | 库存数量 |  
874 -| `weight` | float | 重量 |  
875 -| `weight_unit` | string | 重量单位 |  
876 -| `option1_value` | string | 选项1取值(如color值) |  
877 -| `option2_value` | string | 选项2取值(如size值) |  
878 -| `option3_value` | string | 选项3取值 |  
879 -| `image_src` | string | SKU图片地址 |  
880 -  
881 -### 4.5 多语言字段说明  
882 -  
883 -- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段  
884 -- `language="zh"`: 优先返回 `*_zh` 字段,如果为空则回退到 `*_en` 字段  
885 -- `language="en"`: 优先返回 `*_en` 字段,如果为空则回退到 `*_zh` 字段  
886 -  
887 ----  
888 -  
889 -## 索引接口  
890 -  
891 -本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口:  
892 -  
893 -| 接口 | 方法 | 路径 | 说明 |  
894 -|------|------|------|------|  
895 -| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) |  
896 -| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 |  
897 -| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES |  
898 -| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES |  
899 -| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 |  
900 -| 内容理解字段生成 | POST | `/indexer/enrich-content` | 根据商品标题批量生成 qanchors、semantic_attributes、tags(供微服务组合方式使用) |  
901 -| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 |  
902 -  
903 -#### 5.0 支撑外部 indexer 的三种方式  
904 -  
905 -本服务对**外部 indexer 程序**(如 Java 索引系统)提供三种对接方式,可按需选择:  
906 -  
907 -| 方式 | 说明 | 适用场景 |  
908 -|------|------|----------|  
909 -| **1)doc 填充接口** | 调用 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db`,由本服务基于 MySQL 行数据构建完整 ES 文档(含多语言、向量、规格等),**不写入 ES**,由调用方自行写入。 | 希望一站式拿到 ES-ready doc,由己方控制写 ES 的时机与索引名。 |  
910 -| **2)微服务组合** | 单独调用**翻译**、**向量化**、**内容理解字段生成**等接口,由 indexer 程序自己组装 doc 并写入 ES。翻译与向量化为独立微服务(见第 7 节);内容理解为 Indexer 服务内接口 `POST /indexer/enrich-content`。 | 需要灵活编排、或希望将 LLM/向量等耗时步骤与主链路解耦(如异步补齐 qanchors/tags)。 |  
911 -| **3)本服务直接写 ES** | 调用全量索引 `POST /indexer/reindex`、增量索引 `POST /indexer/index`(指定 SPU ID 列表),由本服务从 MySQL 拉数并直接写入 ES。 | 自建运维、联调或不需要由 Java 写 ES 的场景。 |  
912 -  
913 -- **方式 1** 与 **方式 2** 下,ES 的写入方均为外部 indexer(或 Java),职责清晰。  
914 -- **方式 3** 下,本服务同时负责读库、构建 doc 与写 ES。  
915 -  
916 -### 5.1 为租户创建索引  
917 -  
918 -为租户创建索引需要两个步骤:  
919 -  
920 -1. **创建索引结构**(可选,仅在需要更新 mapping 或在新环境首次创建时执行)  
921 - - 使用脚本创建 ES 索引结构(基于 `mappings/search_products.json`)  
922 - - 如果索引已存在,会提示用户确认(会删除现有数据)  
923 -  
924 -2. **导入数据**(必需)  
925 - - 使用全量索引接口 `/indexer/reindex` 导入数据  
926 -  
927 -**创建索引结构(支持多环境 namespace)**:  
928 -  
929 -```bash  
930 -# 以 UAT 环境为例:  
931 -# 1. 准备 UAT 环境的 .env(包含 UAT 的 ES_HOST/DB_HOST 等)  
932 -# 2. 设置环境前缀(也可以直接在 .env 中配置):  
933 -export RUNTIME_ENV=uat  
934 -export ES_INDEX_NAMESPACE=uat_  
935 -  
936 -# 3. 为 tenant_id=170 创建索引结构  
937 -./scripts/create_tenant_index.sh 170  
938 -```  
939 -  
940 -脚本会自动从项目根目录的 `.env` 文件加载 ES 配置,并根据 `ES_INDEX_NAMESPACE` 创建:  
941 -  
942 -- prod 环境(ES_INDEX_NAMESPACE 为空):`search_products_tenant_170`  
943 -- UAT 环境(ES_INDEX_NAMESPACE=uat_):`uat_search_products_tenant_170`  
944 -  
945 -**注意事项**:  
946 -- ⚠️ 如果索引已存在,脚本会提示确认,确认后会删除现有数据  
947 -- 创建索引后,**必须**调用 `/indexer/reindex` 导入数据  
948 -- 如果只是更新数据而不需要修改索引结构,直接使用 `/indexer/reindex` 即可  
949 -  
950 ----  
951 -  
952 -### 5.2 全量索引接口  
953 -  
954 -- **端点**: `POST /indexer/reindex`  
955 -- **描述**: 全量索引,将指定租户的所有SPU数据导入到ES索引(不会删除现有索引)。**推荐仅用于自测/运维场景**;生产环境下更推荐由 Java 等上游控制调度与写 ES。  
956 -  
957 -#### 请求参数  
958 -  
959 -```json  
960 -{  
961 - "tenant_id": "162",  
962 - "batch_size": 500  
963 -}  
964 -```  
965 -  
966 -| 参数 | 类型 | 必填 | 默认值 | 说明 |  
967 -|------|------|------|--------|------|  
968 -| `tenant_id` | string | Y | - | 租户ID |  
969 -| `batch_size` | integer | N | 500 | 批量导入大小 |  
970 -  
971 -#### 响应格式  
972 -  
973 -**成功响应(200 OK)**(示例,实际 `index_name` 会带上 tenant 和环境前缀):  
974 -```json  
975 -{  
976 - "success": true,  
977 - "total": 1000,  
978 - "indexed": 1000,  
979 - "failed": 0,  
980 - "elapsed_time": 12.34,  
981 - "index_name": "search_products_tenant_162",  
982 - "tenant_id": "162"  
983 -}  
984 -```  
985 -  
986 -**错误响应**:  
987 -- `400 Bad Request`: 参数错误  
988 -- `503 Service Unavailable`: 服务未初始化  
989 -  
990 -#### 请求示例  
991 -  
992 -**全量索引(不会删除现有索引)**:  
993 -```bash  
994 -curl -X POST "http://localhost:6004/indexer/reindex" \  
995 - -H "Content-Type: application/json" \  
996 - -d '{  
997 - "tenant_id": "162",  
998 - "batch_size": 500  
999 - }'  
1000 -```  
1001 -  
1002 -**查看日志**:  
1003 -```bash  
1004 -# 查看API日志(包含索引操作日志)  
1005 -tail -f logs/api.log  
1006 -  
1007 -# 或者查看所有日志文件  
1008 -tail -f logs/*.log  
1009 -```  
1010 -  
1011 -> ⚠️ **重要提示**:如需 **创建索引结构**,请参考 [5.1 为租户创建索引](#51-为租户创建索引) 章节,使用 `./scripts/create_tenant_index.sh <tenant_id>`。创建后需要调用 `/indexer/reindex` 导入数据。  
1012 -  
1013 -**查看索引日志**:  
1014 -  
1015 -索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON 格式),包括:  
1016 -- 请求开始和结束时间  
1017 -- 租户ID、SPU ID、操作类型  
1018 -- 每个SPU的处理状态  
1019 -- ES批量写入结果  
1020 -- 成功/失败统计和详细错误信息  
1021 -  
1022 -```bash  
1023 -# 实时查看索引日志(包含全量和增量索引的所有操作)  
1024 -tail -f logs/indexer.log  
1025 -  
1026 -# 使用 grep 查询(简单方式)  
1027 -# 查看全量索引日志  
1028 -grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100  
1029 -  
1030 -# 查看增量索引日志  
1031 -grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100  
1032 -  
1033 -# 查看特定租户的索引日志  
1034 -grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100  
1035 -  
1036 -# 使用 jq 查询(推荐,更精确的 JSON 查询)  
1037 -# 安装 jq: sudo apt-get install jq 或 brew install jq  
1038 -  
1039 -# 查看全量索引日志  
1040 -cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100  
1041 -  
1042 -# 查看增量索引日志  
1043 -cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100  
1044 -  
1045 -# 查看特定租户的索引日志  
1046 -cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100  
1047 -  
1048 -# 查看失败的索引操作  
1049 -cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)'  
1050 -  
1051 -# 查看特定SPU的处理日志  
1052 -cat logs/indexer.log | jq 'select(.spu_id == "123")'  
1053 -  
1054 -# 查看最近的索引请求统计  
1055 -cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}'  
1056 -```  
1057 -  
1058 -### 5.3 增量索引接口  
1059 -  
1060 -- **端点**: `POST /indexer/index`  
1061 -- **描述**: 增量索引接口,根据指定的SPU ID列表进行索引,直接将数据写入ES。用于增量更新指定商品。**推荐仅作为内部/调试入口**;正式对接建议改用 `/indexer/build-docs`,由上游写 ES。  
1062 -  
1063 -**删除说明**:  
1064 -- `spu_ids`中的SPU:如果数据库`deleted=1`,自动从ES删除,响应状态为`deleted`  
1065 -- `delete_spu_ids`中的SPU:直接删除,响应状态为`deleted`、`not_found`或`failed`  
1066 -  
1067 -#### 请求参数  
1068 -  
1069 -```json  
1070 -{  
1071 - "tenant_id": "162",  
1072 - "spu_ids": ["123", "456", "789"],  
1073 - "delete_spu_ids": ["100", "101"]  
1074 -}  
1075 -```  
1076 -  
1077 -| 参数 | 类型 | 必填 | 说明 |  
1078 -|------|------|------|------|  
1079 -| `tenant_id` | string | Y | 租户ID |  
1080 -| `spu_ids` | array[string] | N | SPU ID列表(1-100个),要索引的SPU。如果为空,则只执行删除操作 |  
1081 -| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表(1-100个),可选。无论数据库状态如何,都会从ES中删除这些SPU |  
1082 -  
1083 -**注意**:  
1084 -- `spu_ids` 和 `delete_spu_ids` 不能同时为空  
1085 -- 每个列表最多支持100个SPU ID  
1086 -- 如果SPU在`spu_ids`中且数据库`deleted=1`,会自动从ES删除(自动检测删除)  
1087 -  
1088 -#### 响应格式  
1089 -  
1090 -```json  
1091 -{  
1092 - "spu_ids": [  
1093 - {  
1094 - "spu_id": "123",  
1095 - "status": "indexed"  
1096 - },  
1097 - {  
1098 - "spu_id": "456",  
1099 - "status": "deleted"  
1100 - },  
1101 - {  
1102 - "spu_id": "789",  
1103 - "status": "failed",  
1104 - "msg": "SPU not found (unexpected)"  
1105 - }  
1106 - ],  
1107 - "delete_spu_ids": [  
1108 - {  
1109 - "spu_id": "100",  
1110 - "status": "deleted"  
1111 - },  
1112 - {  
1113 - "spu_id": "101",  
1114 - "status": "not_found"  
1115 - },  
1116 - {  
1117 - "spu_id": "102",  
1118 - "status": "failed",  
1119 - "msg": "Failed to delete from ES: Connection timeout"  
1120 - }  
1121 - ],  
1122 - "total": 6,  
1123 - "success_count": 4,  
1124 - "failed_count": 2,  
1125 - "elapsed_time": 1.23,  
1126 - "index_name": "search_products",  
1127 - "tenant_id": "162"  
1128 -}  
1129 -```  
1130 -  
1131 -| 字段 | 类型 | 说明 |  
1132 -|------|------|------|  
1133 -| `spu_ids` | array | spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` |  
1134 -| `spu_ids[].status` | string | 状态:`indexed`(已索引)、`deleted`(已删除,自动检测)、`failed`(失败) |  
1135 -| `spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) |  
1136 -| `delete_spu_ids` | array | delete_spu_ids对应的响应列表,每个元素包含 `spu_id` 和 `status` |  
1137 -| `delete_spu_ids[].status` | string | 状态:`deleted`(已删除)、`not_found`(ES中不存在)、`failed`(失败) |  
1138 -| `delete_spu_ids[].msg` | string | 当status为`failed`时,包含失败原因(可选) |  
1139 -| `total` | integer | 总处理数量(spu_ids数量 + delete_spu_ids数量) |  
1140 -| `success_count` | integer | 成功数量(indexed + deleted + not_found) |  
1141 -| `failed_count` | integer | 失败数量 |  
1142 -| `elapsed_time` | float | 耗时(秒) |  
1143 -| `index_name` | string | 索引名称 |  
1144 -| `tenant_id` | string | 租户ID |  
1145 -  
1146 -**状态说明**:  
1147 -- `spu_ids` 的状态:  
1148 - - `indexed`: SPU已成功索引到ES  
1149 - - `deleted`: SPU在数据库中被标记为deleted=1,已从ES删除(自动检测)  
1150 - - `failed`: 处理失败,会包含`msg`字段说明失败原因  
1151 -- `delete_spu_ids` 的状态:  
1152 - - `deleted`: SPU已从ES成功删除  
1153 - - `not_found`: SPU在ES中不存在(也算成功,可能已经被删除过)  
1154 - - `failed`: 删除失败,会包含`msg`字段说明失败原因  
1155 -  
1156 -#### 请求示例  
1157 -  
1158 -**示例1:普通增量索引(自动检测删除)**:  
1159 -```bash  
1160 -curl -X POST "http://localhost:6004/indexer/index" \  
1161 - -H "Content-Type: application/json" \  
1162 - -d '{  
1163 - "tenant_id": "162",  
1164 - "spu_ids": ["123", "456", "789"]  
1165 - }'  
1166 -```  
1167 -说明:如果SPU 456在数据库中`deleted=1`,会自动从ES删除,在响应中`spu_ids`列表里456的状态为`deleted`。  
1168 -  
1169 -**示例2:显式删除(批量删除)**:  
1170 -```bash  
1171 -curl -X POST "http://localhost:6004/indexer/index" \  
1172 - -H "Content-Type: application/json" \  
1173 - -d '{  
1174 - "tenant_id": "162",  
1175 - "spu_ids": ["123", "456"],  
1176 - "delete_spu_ids": ["100", "101", "102"]  
1177 - }'  
1178 -```  
1179 -说明:SPU 100、101、102会被显式删除,无论数据库状态如何。  
1180 -  
1181 -**示例3:仅删除(不索引)**:  
1182 -```bash  
1183 -curl -X POST "http://localhost:6004/indexer/index" \  
1184 - -H "Content-Type: application/json" \  
1185 - -d '{  
1186 - "tenant_id": "162",  
1187 - "spu_ids": [],  
1188 - "delete_spu_ids": ["100", "101"]  
1189 - }'  
1190 -```  
1191 -说明:只执行删除操作,不进行索引。  
1192 -  
1193 -**示例4:混合操作(索引+删除)**:  
1194 -```bash  
1195 -curl -X POST "http://localhost:6004/indexer/index" \  
1196 - -H "Content-Type: application/json" \  
1197 - -d '{  
1198 - "tenant_id": "162",  
1199 - "spu_ids": ["123", "456", "789"],  
1200 - "delete_spu_ids": ["100", "101"]  
1201 - }'  
1202 -```  
1203 -说明:同时执行索引和删除操作。  
1204 -  
1205 -#### 日志说明  
1206 -  
1207 -增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中(JSON格式),包括:  
1208 -- 请求开始和结束时间  
1209 -- 每个SPU的处理状态(获取、转换、索引、删除)  
1210 -- ES批量写入结果  
1211 -- 成功/失败统计  
1212 -- 详细的错误信息  
1213 -  
1214 -日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。  
1215 -  
1216 -### 5.4 查询文档接口  
1217 -  
1218 -- **端点**: `POST /indexer/documents`  
1219 -- **描述**: 查询文档接口,根据SPU ID列表获取ES文档数据(**不写入ES**)。用于查看、调试或验证SPU数据。  
1220 -  
1221 -#### 请求参数  
1222 -  
1223 -```json  
1224 -{  
1225 - "tenant_id": "162",  
1226 - "spu_ids": ["123", "456", "789"]  
1227 -}  
1228 -```  
1229 -  
1230 -| 参数 | 类型 | 必填 | 说明 |  
1231 -|------|------|------|------|  
1232 -| `tenant_id` | string | Y | 租户ID |  
1233 -| `spu_ids` | array[string] | Y | SPU ID列表(1-100个) |  
1234 -  
1235 -#### 响应格式  
1236 -  
1237 -```json  
1238 -{  
1239 - "success": [  
1240 - {  
1241 - "spu_id": "123",  
1242 - "document": {  
1243 - "tenant_id": "162",  
1244 - "spu_id": "123",  
1245 - "title": {  
1246 - "zh": "商品标题"  
1247 - },  
1248 - ...  
1249 - }  
1250 - },  
1251 - {  
1252 - "spu_id": "456",  
1253 - "document": {...}  
1254 - }  
1255 - ],  
1256 - "failed": [  
1257 - {  
1258 - "spu_id": "789",  
1259 - "error": "SPU not found or deleted"  
1260 - }  
1261 - ],  
1262 - "total": 3,  
1263 - "success_count": 2,  
1264 - "failed_count": 1  
1265 -}  
1266 -```  
1267 -  
1268 -| 字段 | 类型 | 说明 |  
1269 -|------|------|------|  
1270 -| `success` | array | 成功获取的SPU列表,每个元素包含 `spu_id` 和 `document`(完整的ES文档数据) |  
1271 -| `failed` | array | 失败的SPU列表,每个元素包含 `spu_id` 和 `error`(失败原因) |  
1272 -| `total` | integer | 总SPU数量 |  
1273 -| `success_count` | integer | 成功数量 |  
1274 -| `failed_count` | integer | 失败数量 |  
1275 -  
1276 -#### 请求示例  
1277 -  
1278 -**单个SPU查询**:  
1279 -```bash  
1280 -curl -X POST "http://localhost:6004/indexer/documents" \  
1281 - -H "Content-Type: application/json" \  
1282 - -d '{  
1283 - "tenant_id": "162",  
1284 - "spu_ids": ["123"]  
1285 - }'  
1286 -```  
1287 -  
1288 -**批量SPU查询**:  
1289 -```bash  
1290 -curl -X POST "http://localhost:6004/indexer/documents" \  
1291 - -H "Content-Type: application/json" \  
1292 - -d '{  
1293 - "tenant_id": "162",  
1294 - "spu_ids": ["123", "456", "789"]  
1295 - }'  
1296 -```  
1297 -  
1298 -#### 与 `/indexer/index` 的区别  
1299 -  
1300 -| 接口 | 功能 | 是否写入ES | 返回内容 |  
1301 -|------|------|-----------|----------|  
1302 -| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 |  
1303 -| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 |  
1304 -  
1305 -**使用场景**:  
1306 -- `/indexer/documents`:用于查看、调试或验证SPU数据,不修改ES索引  
1307 -- `/indexer/index`:用于实际的增量索引操作,将更新的SPU数据同步到ES  
1308 -  
1309 -### 5.5 索引健康检查接口  
1310 -  
1311 -- **端点**: `GET /indexer/health`  
1312 -- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致)  
1313 -  
1314 -#### 响应格式  
1315 -  
1316 -```json  
1317 -{  
1318 - "status": "available",  
1319 - "database": "connected",  
1320 - "preloaded_data": {  
1321 - "category_mappings": 150  
1322 - }  
1323 -}  
1324 -```  
1325 -  
1326 -| 字段 | 类型 | 说明 |  
1327 -|------|------|------|  
1328 -| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) |  
1329 -| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` |  
1330 -| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 |  
1331 -  
1332 -#### 请求示例  
1333 -  
1334 -```bash  
1335 -curl -X GET "http://localhost:6004/indexer/health"  
1336 -```  
1337 -  
1338 -### 5.6 文档构建接口(正式对接推荐)  
1339 -  
1340 -#### 5.6.1 `POST /indexer/build-docs`  
1341 -  
1342 -- **描述**:  
1343 - 基于调用方(通常是 Java 索引程序)提供的 **MySQL 行数据** 构建 ES 文档(doc),**不写入 ES**。  
1344 - 由本服务负责“如何构建 doc”(多语言、翻译、向量、规格聚合等),由调用方负责“何时调度 + 如何写 ES”。  
1345 -  
1346 -#### 请求参数  
1347 -  
1348 -```json  
1349 -{  
1350 - "tenant_id": "170",  
1351 - "items": [  
1352 - {  
1353 - "spu": { "id": 223167, "tenant_id": 170, "title": "..." },  
1354 - "skus": [  
1355 - { "id": 3988393, "spu_id": 223167, "price": 25.99, "compare_at_price": 25.99 }  
1356 - ],  
1357 - "options": []  
1358 - }  
1359 - ]  
1360 -}  
1361 -```  
1362 -  
1363 -| 参数 | 类型 | 必填 | 说明 |  
1364 -|------|------|------|------|  
1365 -| `tenant_id` | string | Y | 租户 ID |  
1366 -| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** |  
1367 -  
1368 -> `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。  
1369 -  
1370 -#### 请求示例(完整 curl)  
1371 -  
1372 -> 完整请求体参考 `scripts/test_build_docs_api.py` 中的 `build_sample_request()`。  
1373 -  
1374 -```bash  
1375 -# 单条 SPU 示例(含 spu、skus、options)  
1376 -curl -X POST "http://localhost:6004/indexer/build-docs" \  
1377 - -H "Content-Type: application/json" \  
1378 - -d '{  
1379 - "tenant_id": "162",  
1380 - "items": [  
1381 - {  
1382 - "spu": {  
1383 - "id": 10001,  
1384 - "tenant_id": "162",  
1385 - "title": "测试T恤 纯棉短袖",  
1386 - "brief": "舒适纯棉,多色可选",  
1387 - "description": "这是一款适合日常穿着的纯棉T恤,透气吸汗。",  
1388 - "vendor": "测试品牌",  
1389 - "category": "服装/上衣/T恤",  
1390 - "category_id": 100,  
1391 - "category_level": 2,  
1392 - "category_path": "服装/上衣/T恤",  
1393 - "fake_sales": 1280,  
1394 - "image_src": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg",  
1395 - "tags": "T恤,纯棉,短袖,夏季",  
1396 - "create_time": "2024-01-01T00:00:00Z",  
1397 - "update_time": "2024-01-01T00:00:00Z"  
1398 - },  
1399 - "skus": [  
1400 - {  
1401 - "id": 20001,  
1402 - "spu_id": 10001,  
1403 - "price": 99.0,  
1404 - "compare_at_price": 129.0,  
1405 - "sku": "SKU-TSHIRT-001",  
1406 - "inventory_quantity": 50,  
1407 - "option1": "黑色",  
1408 - "option2": "M",  
1409 - "option3": null  
1410 - },  
1411 - {  
1412 - "id": 20002,  
1413 - "spu_id": 10001,  
1414 - "price": 99.0,  
1415 - "compare_at_price": 129.0,  
1416 - "sku": "SKU-TSHIRT-002",  
1417 - "inventory_quantity": 30,  
1418 - "option1": "白色",  
1419 - "option2": "L",  
1420 - "option3": null  
1421 - }  
1422 - ],  
1423 - "options": [  
1424 - {"id": 1, "position": 1, "name": "颜色"},  
1425 - {"id": 2, "position": 2, "name": "尺码"}  
1426 - ]  
1427 - }  
1428 - ]  
1429 -}'  
1430 -```  
1431 -  
1432 -生产环境替换 `localhost:6004` 为实际 Indexer 地址,如 `http://43.166.252.75:6004`。  
1433 -  
1434 -#### 响应示例(节选)  
1435 -  
1436 -```json  
1437 -{  
1438 - "tenant_id": "170",  
1439 - "docs": [  
1440 - {  
1441 - "tenant_id": "170",  
1442 - "spu_id": "223167",  
1443 - "title": { "en": "...", "zh": "..." },  
1444 - "tags": ["Floerns", "Clothing", "Shoes & Jewelry"],  
1445 - "skus": [  
1446 - {  
1447 - "sku_id": "3988393",  
1448 - "price": 25.99,  
1449 - "compare_at_price": 25.99,  
1450 - "stock": 100  
1451 - }  
1452 - ],  
1453 - "min_price": 25.99,  
1454 - "max_price": 25.99,  
1455 - "compare_at_price": 25.99,  
1456 - "total_inventory": 100,  
1457 - "title_embedding": [/* 1024 维向量 */]  
1458 - // 其余字段与 mappings/search_products.json 一致  
1459 - }  
1460 - ],  
1461 - "total": 1,  
1462 - "success_count": 1,  
1463 - "failed_count": 0,  
1464 - "failed": []  
1465 -}  
1466 -```  
1467 -  
1468 -| 字段 | 类型 | 说明 |  
1469 -|------|------|------|  
1470 -| `tenant_id` | string | 租户 ID |  
1471 -| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 |  
1472 -| `total` | integer | 请求的 items 总数 |  
1473 -| `success_count` | integer | 成功构建数量 |  
1474 -| `failed_count` | integer | 失败数量 |  
1475 -| `failed` | array | 失败项列表,每项含 `spu_id`、`error` |  
1476 -  
1477 -#### 使用建议  
1478 -  
1479 -- **生产环境推荐流程**:  
1480 - 1. Java 根据业务逻辑决定哪些 SPU 需要(全量/增量)处理;  
1481 - 2. Java 从 MySQL 查询 SPU/SKU/Option 行,拼成 `items`;  
1482 - 3. 调用 `/indexer/build-docs` 获取 ES-ready `docs`;  
1483 - 4. Java 使用自己的 ES 客户端写入 `search_products_tenant_{tenant_id}`。  
1484 -  
1485 -### 5.7 文档构建接口(测试 / 自测)  
1486 -  
1487 -#### 5.7.1 `POST /indexer/build-docs-from-db`  
1488 -  
1489 -- **描述**:  
1490 - 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。**  
1491 -  
1492 -#### 请求参数  
1493 -  
1494 -```json  
1495 -{  
1496 - "tenant_id": "170",  
1497 - "spu_ids": ["223167", "223168"]  
1498 -}  
1499 -```  
1500 -  
1501 -| 参数 | 类型 | 必填 | 说明 |  
1502 -|------|------|------|------|  
1503 -| `tenant_id` | string | Y | 租户 ID |  
1504 -| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** |  
1505 -  
1506 -#### 响应格式  
1507 -  
1508 -与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。  
1509 -  
1510 -#### 请求示例  
1511 -  
1512 -```bash  
1513 -curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \  
1514 - -H "Content-Type: application/json" \  
1515 - -d '{"tenant_id": "170", "spu_ids": ["223167"]}'  
1516 -```  
1517 -  
1518 -返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。  
1519 -  
1520 -### 5.8 内容理解字段生成接口  
1521 -  
1522 -- **端点**: `POST /indexer/enrich-content`  
1523 -- **描述**: 根据商品内容信息批量生成 **qanchors**(锚文本)、**semantic_attributes**(语义属性)、**tags**(细分标签),供外部 indexer 在「微服务组合」方式下自行拼装 doc 时使用。请求以 `items[]` 传入商品内容字段(必填/可选见下表)。内部逻辑与 `indexer.product_enrich` 一致,支持多语言与 Redis 缓存;单次请求在线程池中执行,避免阻塞其他接口。  
1524 -  
1525 -#### 请求参数  
1526 -  
1527 -```json  
1528 -{  
1529 - "tenant_id": "170",  
1530 - "items": [  
1531 - {  
1532 - "spu_id": "223167",  
1533 - "title": "纯棉短袖T恤 夏季男装",  
1534 - "brief": "夏季透气纯棉短袖,舒适亲肤",  
1535 - "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。",  
1536 - "image_url": "https://example.com/images/223167.jpg"  
1537 - },  
1538 - {  
1539 - "spu_id": "223168",  
1540 - "title": "12PCS Dolls with Bottles",  
1541 - "image_url": "https://example.com/images/223168.jpg"  
1542 - }  
1543 - ],  
1544 - "languages": ["zh", "en"]  
1545 -}  
1546 -```  
1547 -  
1548 -| 参数 | 类型 | 必填 | 默认值 | 说明 |  
1549 -|------|------|------|--------|------|  
1550 -| `tenant_id` | string | Y | - | 租户 ID。目前仅用于记录日志,不产生实际作用|  
1551 -| `items` | array | Y | - | 待分析列表;**单次最多 50 条** |  
1552 -| `languages` | array[string] | N | `["zh", "en"]` | 目标语言,需在支持范围内:`zh`、`en`、`de`、`ru`、`fr` |  
1553 -  
1554 -`items[]` 字段说明:  
1555 -  
1556 -| 字段 | 类型 | 必填 | 说明 |  
1557 -|------|------|------|------|  
1558 -| `spu_id` | string | Y | SPU ID,用于回填结果;目前仅用于记录日志,不产生实际作用|  
1559 -| `title` | string | Y | 商品标题 |  
1560 -| `image_url` | string | N | 商品主图 URL;当前会参与内容缓存键,后续可用于图像/多模态内容理解 |  
1561 -| `brief` | string | N | 商品简介/短描述;当前会参与内容缓存键 |  
1562 -| `description` | string | N | 商品详情/长描述;当前会参与内容缓存键 |  
1563 -  
1564 -缓存说明:  
1565 -  
1566 -- 内容缓存键仅由 `target_lang + items[]` 中会影响内容理解结果的输入文本构成,目前包括:`title`、`brief`、`description`、`image_url` 的规范化内容 hash。  
1567 -- `tenant_id`、`spu_id` 只用于请求归属与结果回填,不参与缓存键。  
1568 -- 因此,输入内容不变时可跨请求直接命中缓存;任一输入字段变化时,会自然落到新的缓存 key。  
1569 -  
1570 -批量请求建议:  
1571 -- **全量**:强烈建议 尽可能 **20 个 SPU/doc** 攒成一个批次后再请求一次。  
1572 -- **增量**:可按时效要求设置时间窗口(例如 **5 分钟**),在窗口内尽可能攒到 **20 个**;达到 20 或窗口到期就发送一次请求。  
1573 -- 允许超过20,服务内部会拆分成小批次逐个处理。也允许小于20,但是将造成费用和耗时的成本上升,特别是每次请求一个doc的情况。  
1574 -  
1575 -#### 响应格式  
1576 -  
1577 -```json  
1578 -{  
1579 - "tenant_id": "170",  
1580 - "total": 2,  
1581 - "results": [  
1582 - {  
1583 - "spu_id": "223167",  
1584 - "qanchors": {  
1585 - "zh": "短袖T恤,纯棉,男装,夏季",  
1586 - "en": "cotton t-shirt, short sleeve, men, summer"  
1587 - },  
1588 - "semantic_attributes": [  
1589 - { "lang": "zh", "name": "tags", "value": "纯棉" },  
1590 - { "lang": "zh", "name": "usage_scene", "value": "日常" },  
1591 - { "lang": "en", "name": "tags", "value": "cotton" }  
1592 - ],  
1593 - "tags": ["纯棉", "短袖", "男装", "cotton", "short sleeve"]  
1594 - },  
1595 - {  
1596 - "spu_id": "223168",  
1597 - "qanchors": { "en": "dolls, toys, 12pcs" },  
1598 - "semantic_attributes": [],  
1599 - "tags": ["dolls", "toys"]  
1600 - }  
1601 - ]  
1602 -}  
1603 -```  
1604 -  
1605 -| 字段 | 类型 | 说明 |  
1606 -|------|------|------|  
1607 -| `results` | array | 与请求 `items` 一一对应,每项含 `spu_id`、`qanchors`、`semantic_attributes`、`tags` |  
1608 -| `results[].qanchors` | object | 按语言键的锚文本(逗号分隔短语),可写入 ES 文档的 `qanchors.{lang}` |  
1609 -| `results[].semantic_attributes` | array | 语义属性列表,每项为 `{ "lang", "name", "value" }`,可写入 ES 的 `semantic_attributes` nested 字段 |  
1610 -| `results[].tags` | array | 从语义属性中抽取的 `name=tags` 的 value 集合,可与业务原有 `tags` 合并后写入 ES 的 `tags` 字段 |  
1611 -| `results[].error` | string | 若该条处理失败(如 LLM 异常),会在此字段返回错误信息 |  
1612 -  
1613 -**错误响应**:  
1614 -- `400`: `items` 为空或超过 50 条  
1615 -- `503`: 未配置 `DASHSCOPE_API_KEY`,内容理解服务不可用  
1616 -  
1617 -#### 请求示例  
1618 -  
1619 -```bash  
1620 -curl -X POST "http://localhost:6004/indexer/enrich-content" \  
1621 - -H "Content-Type: application/json" \  
1622 - -d '{  
1623 - "tenant_id": "170",  
1624 - "items": [  
1625 - {  
1626 - "spu_id": "223167",  
1627 - "title": "纯棉短袖T恤 夏季男装",  
1628 - "brief": "夏季透气纯棉短袖,舒适亲肤",  
1629 - "description": "100%棉,圆领版型,适合日常通勤与休闲穿搭。",  
1630 - "image_url": "https://example.com/images/223167.jpg"  
1631 - }  
1632 - ],  
1633 - "languages": ["zh", "en"]  
1634 - }'  
1635 -```  
1636 -  
1637 ----  
1638 -  
1639 -## 管理接口  
1640 -  
1641 -### 6.1 健康检查  
1642 -  
1643 -- **端点**: `GET /admin/health`  
1644 -- **描述**: 检查服务与依赖(如 Elasticsearch)状态。  
1645 -  
1646 -```json  
1647 -{  
1648 - "status": "healthy",  
1649 - "elasticsearch": "connected",  
1650 - "tenant_id": "tenant1"  
1651 -}  
1652 -```  
1653 -  
1654 -### 6.2 获取配置  
1655 -  
1656 -- **端点**: `GET /admin/config`  
1657 -- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。  
1658 -  
1659 -```json  
1660 -{  
1661 - "tenant_id": "tenant1",  
1662 - "tenant_name": "Tenant1 Test Instance",  
1663 - "es_index_name": "search_tenant1",  
1664 - "num_fields": 20,  
1665 - "num_indexes": 4,  
1666 - "supported_languages": ["zh", "en", "ru"],  
1667 - "spu_enabled": false  
1668 -}  
1669 -```  
1670 -  
1671 -### 6.3 索引统计  
1672 -  
1673 -- **端点**: `GET /admin/stats`  
1674 -- **描述**: 获取指定租户索引文档数量与磁盘大小,方便监控。  
1675 -- **租户标识**:通过请求头 `X-Tenant-ID` 或 query 参数 `tenant_id` 传递(必填)。  
1676 -  
1677 -```json  
1678 -{  
1679 - "tenant_id": "162",  
1680 - "index_name": "search_products_tenant_162",  
1681 - "document_count": 10000,  
1682 - "size_mb": 523.45  
1683 -}  
1684 -```  
1685 -  
1686 ----  
1687 -  
1688 -## 7. 微服务接口(向量、重排、翻译)  
1689 -  
1690 -以下三个微服务独立部署,**外部系统可直接调用**。它们被搜索后端(6002)和索引服务(6004)内部使用,也可供其他业务系统直接对接。  
1691 -  
1692 -| 服务 | 默认端口 | Base URL | 说明 |  
1693 -|------|----------|----------|------|  
1694 -| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 |  
1695 -| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 |  
1696 -| 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) |  
1697 -| 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 |  
1698 -  
1699 -生产环境请将 `localhost` 替换为实际服务地址。  
1700 -服务管理入口与完整启停规则见:`docs/Usage-Guide.md` -> `服务管理总览`。  
1701 -  
1702 -### 7.1 向量服务(Embedding)  
1703 -  
1704 -- **Base URL**:  
1705 - - 文本:`http://localhost:6005`(可通过 `EMBEDDING_TEXT_SERVICE_URL` 覆盖)  
1706 - - 图片:`http://localhost:6008`(可通过 `EMBEDDING_IMAGE_SERVICE_URL` 覆盖)  
1707 -- **启动**:  
1708 - - 文本:`./scripts/start_embedding_text_service.sh`  
1709 - - 图片:`./scripts/start_embedding_image_service.sh`  
1710 -- **依赖**:  
1711 - - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`)  
1712 - - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`)  
1713 - - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU)  
1714 - - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`)  
1715 - - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力  
1716 -  
1717 -补充说明:  
1718 -  
1719 -- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。  
1720 -- 服务端对 text / image 有**独立 admission control**:  
1721 - - `TEXT_MAX_INFLIGHT`  
1722 - - `IMAGE_MAX_INFLIGHT`  
1723 -- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。  
1724 -- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。  
1725 -  
1726 -#### 7.1.1 `POST /embed/text` — 文本向量化  
1727 -  
1728 -将文本列表转为 1024 维向量,用于语义搜索、文档索引等。  
1729 -  
1730 -**请求体**(JSON 数组):  
1731 -```json  
1732 -["文本1", "文本2", "文本3"]  
1733 -```  
1734 -  
1735 -**响应**(JSON 数组,与输入一一对应):  
1736 -```json  
1737 -[[0.01, -0.02, ...], [0.03, 0.01, ...], ...]  
1738 -```  
1739 -  
1740 -**完整 curl 示例**:  
1741 -```bash  
1742 -curl -X POST "http://localhost:6005/embed/text?normalize=true" \  
1743 - -H "Content-Type: application/json" \  
1744 - -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]'  
1745 -```  
1746 -  
1747 -#### 7.1.2 `POST /embed/image` — 图片向量化  
1748 -  
1749 -将图片 URL 或路径转为向量,用于以图搜图。  
1750 -  
1751 -前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。  
1752 -  
1753 -**请求体**(JSON 数组):  
1754 -```json  
1755 -["https://example.com/image1.jpg", "https://example.com/image2.jpg"]  
1756 -```  
1757 -  
1758 -**响应**(JSON 数组,与输入一一对应):  
1759 -```json  
1760 -[[0.01, -0.02, ...], [0.03, 0.01, ...], ...]  
1761 -```  
1762 -  
1763 -**完整 curl 示例**:  
1764 -```bash  
1765 -curl -X POST "http://localhost:6008/embed/image?normalize=true" \  
1766 - -H "Content-Type: application/json" \  
1767 - -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]'  
1768 -```  
1769 -  
1770 -#### 7.1.3 `GET /health` — 健康检查  
1771 -  
1772 -```bash  
1773 -curl "http://localhost:6005/health"  
1774 -curl "http://localhost:6008/health"  
1775 -```  
1776 -  
1777 -返回中会包含:  
1778 -  
1779 -- `service_kind`:`text` / `image` / `all`  
1780 -- `cache_enabled`:text/image Redis 缓存是否可用  
1781 -- `limits`:当前 inflight limit、active、rejected_total 等  
1782 -- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等  
1783 -  
1784 -#### 7.1.4 `GET /ready` — 就绪检查  
1785 -  
1786 -```bash  
1787 -curl "http://localhost:6005/ready"  
1788 -curl "http://localhost:6008/ready"  
1789 -```  
1790 -  
1791 -#### 7.1.5 缓存与限流说明  
1792 -  
1793 -- 文本与图片都会先查 Redis 向量缓存。  
1794 -- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。  
1795 -- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。  
1796 -- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。  
1797 -- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。  
1798 -  
1799 -#### 7.1.6 TEI 统一调优建议(主服务)  
1800 -  
1801 -使用单套主服务即可同时兼顾:  
1802 -- 在线 query 向量化(低延迟,常见 `batch=1~4`)  
1803 -- 索引构建向量化(高吞吐,常见 `batch=15~20`)  
1804 -  
1805 -统一启动(主链路):  
1806 -  
1807 -```bash  
1808 -./scripts/start_tei_service.sh  
1809 -./scripts/service_ctl.sh restart embedding  
1810 -```  
1811 -  
1812 -默认端口:  
1813 -- TEI: `http://127.0.0.1:8080`  
1814 -- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005`  
1815 -- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008`  
1816 -  
1817 -当前主 TEI 启动默认值(已按 T4/短文本场景调优):  
1818 -- `TEI_MAX_BATCH_TOKENS=4096`  
1819 -- `TEI_MAX_CLIENT_BATCH_SIZE=24`  
1820 -- `TEI_DTYPE=float16`  
1821 -  
1822 -### 7.2 重排服务(Reranker)  
1823 -  
1824 -- **Base URL**: `http://localhost:6007`(可通过 `RERANKER_SERVICE_URL` 覆盖)  
1825 -- **启动**: `./scripts/start_reranker.sh`  
1826 -  
1827 -说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。  
1828 -补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确)。  
1829 -  
1830 -#### 7.2.1 `POST /rerank` — 结果重排  
1831 -  
1832 -根据 query 与 doc 的相关性对文档列表重新打分排序。  
1833 -  
1834 -**请求体**:  
1835 -```json  
1836 -{  
1837 - "query": "玩具 芭比",  
1838 - "docs": [  
1839 - "12PCS 6 Types of Dolls with Bottles",  
1840 - "纯棉T恤 短袖 夏季"  
1841 - ],  
1842 - "normalize": true  
1843 -}  
1844 -```  
1845 -  
1846 -| 参数 | 类型 | 必填 | 说明 |  
1847 -|------|------|------|------|  
1848 -| `query` | string | Y | 搜索查询 |  
1849 -| `docs` | array[string] | Y | 待重排的文档列表(单次最多由服务端配置限制) |  
1850 -| `normalize` | boolean | N | 是否对分数做 sigmoid 归一化,默认 true |  
1851 -  
1852 -**响应**:  
1853 -```json  
1854 -{  
1855 - "scores": [0.92, 0.15],  
1856 - "meta": {  
1857 - "service_elapsed_ms": 45.2,  
1858 - "input_docs": 2,  
1859 - "unique_docs": 2  
1860 - }  
1861 -}  
1862 -```  
1863 -  
1864 -**完整 curl 示例**:  
1865 -```bash  
1866 -curl -X POST "http://localhost:6007/rerank" \  
1867 - -H "Content-Type: application/json" \  
1868 - -d '{  
1869 - "query": "玩具 芭比",  
1870 - "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"],  
1871 - "top_n":386,  
1872 - "normalize": true  
1873 - }'  
1874 -  
1875 -```  
1876 -  
1877 -#### 7.2.2 `GET /health` — 健康检查  
1878 -  
1879 -```bash  
1880 -curl "http://localhost:6007/health"  
1881 -```  
1882 -  
1883 -### 7.3 翻译服务(Translation)  
1884 -  
1885 -- **Base URL**: `http://localhost:6006`(以 `config/config.yaml -> services.translation.service_url` 为准)  
1886 -- **启动**: `./scripts/start_translator.sh`  
1887 -  
1888 -#### 7.3.1 `POST /translate` — 文本翻译  
1889 -  
1890 -支持 translator service 内所有已启用 capability,适用于商品名称、描述、query 等电商场景。当前可配置能力包括 `qwen-mt`、`llm`、`deepl` 以及本地模型 `nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh`。  
1891 -  
1892 -**请求体**(支持单条字符串或字符串列表):  
1893 -```json  
1894 -{  
1895 - "text": "商品名称",  
1896 - "target_lang": "en",  
1897 - "source_lang": "zh",  
1898 - "model": "qwen-mt",  
1899 - "scene": "sku_name"  
1900 -}  
1901 -```  
1902 -  
1903 -也支持批量列表形式:  
1904 -```json  
1905 -{  
1906 - "text": ["商品名称1", "商品名称2"],  
1907 - "target_lang": "en",  
1908 - "source_lang": "zh",  
1909 - "model": "qwen-mt",  
1910 - "scene": "sku_name"  
1911 -}  
1912 -```  
1913 -  
1914 -| 参数 | 类型 | 必填 | 说明 |  
1915 -|------|------|------|------|  
1916 -| `text` | string \| string[] | Y | 待翻译文本,既支持单条字符串,也支持字符串列表(批量翻译) |  
1917 -| `target_lang` | string | Y | 目标语言:`zh`、`en`、`ru` 等 |  
1918 -| `source_lang` | string | N | 源语言。云端模型可不传;`nllb-200-distilled-600m` 建议显式传入 |  
1919 -| `model` | string | N | 已启用 capability 名称,如 `qwen-mt`、`llm`、`deepl`、`nllb-200-distilled-600m`、`opus-mt-zh-en`、`opus-mt-en-zh` |  
1920 -| `scene` | string | N | 翻译场景参数,与 `model` 配套使用;当前标准值为 `sku_name`、`ecommerce_search_query`、`general` |  
1921 -  
1922 -说明:  
1923 -- 外部接口不接受 `prompt`;LLM prompt 由服务端按 `scene` 自动生成。  
1924 -- 传入未定义的 `scene` 或未启用的 `model` 会返回 `400`。  
1925 -  
1926 -**SKU 名称场景选型建议**:  
1927 -  
1928 -- 批量 SKU 名称翻译,优先考虑本地大吞吐方案时,可使用 `"model": "nllb-200-distilled-600m"`(该模型"scene":参数无效)。  
1929 -- 如果目标是更高质量,且可以接受更慢速度与额外 LLM API 费用,可使用 `"model": "llm"` + `"scene": "sku_name"`。  
1930 -- 如果是en-zh互译、期待更高的速度,可以考虑`opus-mt-zh-en` / `opus-mt-en-zh`。(质量未详细评测,一些文章说比blib-200-600m更好,但是我看了些case感觉要差不少)  
1931 -  
1932 -**实时翻译选型建议**:  
1933 -  
1934 -- 在线 query 翻译如果只是 `en/zh` 互译,优先使用 `opus-mt-zh-en` 或 `opus-mt-en-zh`,它们是当前已测本地模型里延迟最低的一档。  
1935 -- 如果涉及其他语言,或对质量要求高于本地轻量模型,优先考虑 `deepl`。  
1936 -- `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`。  
1937 -  
1938 -**Batch Size / 调用方式建议**:  
1939 -  
1940 -- 本接口支持 `text: string[]`;离线或批量索引翻译时,应尽量合并请求,让底层 backend 发挥批处理能力。  
1941 -- `nllb-200-distilled-600m` 在当前 `Tesla T4` 压测中,推荐配置是 `batch_size=16`、`max_new_tokens=64`、`attn_implementation=sdpa`;继续升到 `batch_size=32` 虽可能提高吞吐,但 tail latency 会明显变差。  
1942 -- 在线 query 场景可直接把“单条请求”理解为 `batch_size=1`;更关注 request latency,而不是离线吞吐。  
1943 -- `opus-mt-zh-en` / `opus-mt-en-zh` 当前生产配置也是 `batch_size=16`,适合作为中英互译的低延迟本地默认值;若走在线单条调用,同样按 `batch_size=1` 理解即可。  
1944 -- `llm` 按单条请求即可。  
1945 -  
1946 -**响应**:  
1947 -```json  
1948 -{  
1949 - "text": "商品名称",  
1950 - "target_lang": "en",  
1951 - "source_lang": "zh",  
1952 - "translated_text": "Product name",  
1953 - "status": "success",  
1954 - "model": "qwen-mt",  
1955 - "scene": "sku_name"  
1956 -}  
1957 -```  
1958 -  
1959 -当请求为列表形式时,`text` 与 `translated_text` 均为等长数组:  
1960 -```json  
1961 -{  
1962 - "text": ["商品名称1", "商品名称2"],  
1963 - "target_lang": "en",  
1964 - "source_lang": "zh",  
1965 - "translated_text": ["Product name 1", "Product name 2"],  
1966 - "status": "success",  
1967 - "model": "qwen-mt",  
1968 - "scene": "sku_name"  
1969 -}  
1970 -```  
1971 -  
1972 -> **失败语义(批量)**:当 `text` 为列表时,如果其中某条翻译失败,对应位置返回 `null`(即 `translated_text[i] = null`),并保持数组长度与顺序不变;接口整体仍返回 `status="success"`,用于避免“部分失败”导致整批请求失败。  
1973 -  
1974 -> **实现提示(可忽略)**:服务端会尽可能使用底层 backend 的批量能力(若支持),否则自动拆分逐条翻译;无论采用哪种方式,上述批量契约保持一致。  
1975 -  
1976 -**完整 curl 示例**:  
1977 -  
1978 -中文 → 英文:  
1979 -```bash  
1980 -curl -X POST "http://localhost:6006/translate" \  
1981 - -H "Content-Type: application/json" \  
1982 - -d '{  
1983 - "text": "商品名称",  
1984 - "target_lang": "en",  
1985 - "source_lang": "zh"  
1986 - }'  
1987 -```  
1988 -  
1989 -俄文 → 英文:  
1990 -```bash  
1991 -curl -X POST "http://localhost:6006/translate" \  
1992 - -H "Content-Type: application/json" \  
1993 - -d '{  
1994 - "text": "Название товара",  
1995 - "target_lang": "en",  
1996 - "source_lang": "ru"  
1997 - }'  
1998 -```  
1999 -  
2000 -使用 DeepL 模型:  
2001 -```bash  
2002 -curl -X POST "http://localhost:6006/translate" \  
2003 - -H "Content-Type: application/json" \  
2004 - -d '{  
2005 - "text": "商品名称",  
2006 - "target_lang": "en",  
2007 - "source_lang": "zh",  
2008 - "model": "deepl"  
2009 - }'  
2010 -```  
2011 -  
2012 -使用本地 OPUS 模型(中文 → 英文):  
2013 -```bash  
2014 -curl -X POST "http://localhost:6006/translate" \  
2015 - -H "Content-Type: application/json" \  
2016 - -d '{  
2017 - "text": "蓝牙耳机",  
2018 - "target_lang": "en",  
2019 - "source_lang": "zh",  
2020 - "model": "opus-mt-zh-en",  
2021 - "scene": "sku_name"  
2022 - }'  
2023 -```  
2024 -  
2025 -使用本地 NLLB 做 SKU 名称批量翻译:  
2026 -```bash  
2027 -curl -X POST "http://localhost:6006/translate" \  
2028 - -H "Content-Type: application/json" \  
2029 - -d '{  
2030 - "text": ["商品名称1", "商品名称2", "商品名称3"],  
2031 - "target_lang": "en",  
2032 - "source_lang": "zh",  
2033 - "model": "nllb-200-distilled-600m",  
2034 - "scene": "sku_name"  
2035 - }'  
2036 -```  
2037 -  
2038 -使用 LLM 做高质量 SKU 名称翻译:  
2039 -```bash  
2040 -curl -X POST "http://localhost:6006/translate" \  
2041 - -H "Content-Type: application/json" \  
2042 - -d '{  
2043 - "text": "男士偏光飞行员太阳镜",  
2044 - "target_lang": "en",  
2045 - "source_lang": "zh",  
2046 - "model": "llm",  
2047 - "scene": "sku_name"  
2048 - }'  
2049 -```  
2050 -  
2051 -#### 7.3.2 `GET /health` — 健康检查  
2052 -  
2053 -```bash  
2054 -curl "http://localhost:6006/health"  
2055 -```  
2056 -  
2057 -典型响应:  
2058 -```json  
2059 -{  
2060 - "status": "healthy",  
2061 - "service": "translation",  
2062 - "default_model": "llm",  
2063 - "default_scene": "general",  
2064 - "available_models": ["qwen-mt", "llm", "opus-mt-zh-en"],  
2065 - "enabled_capabilities": ["qwen-mt", "llm", "opus-mt-zh-en"],  
2066 - "loaded_models": ["llm"]  
2067 -}  
2068 -```  
2069 -  
2070 -### 7.4 内容理解字段生成(Indexer 服务内)  
2071 -  
2072 -内容理解字段生成接口部署在 **Indexer 服务**(默认端口 6004)内,与「翻译、向量化」等独立端口微服务并列,供采用**微服务组合**方式的 indexer 调用。  
2073 -  
2074 -- **Base URL**: Indexer 服务地址,如 `http://localhost:6004`  
2075 -- **路径**: `POST /indexer/enrich-content`  
2076 -- **说明**: 根据商品标题批量生成 `qanchors`、`semantic_attributes`、`tags`,用于拼装 ES 文档。内部使用大模型(需配置 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存;单次最多 50 条,建议批量调用以提升效率。  
2077 -  
2078 -请求/响应格式、示例及错误码见 [5.8 内容理解字段生成接口](#58-内容理解字段生成接口)。  
2079 -  
2080 ----  
2081 -  
2082 -## 8. 常见场景示例  
2083 -  
2084 -以下示例仅展示**请求体**(body);实际调用时请加上请求头 `X-Tenant-ID: <租户ID>`(或 URL 参数 `tenant_id`),参见 [3.1 接口信息](#31-接口信息)。  
2085 -  
2086 -### 8.1 基础搜索与排序  
2087 -  
2088 -**按价格从低到高排序**:  
2089 -```json  
2090 -{  
2091 - "query": "玩具",  
2092 - "size": 20,  
2093 - "from": 0,  
2094 - "sort_by": "price",  
2095 - "sort_order": "asc"  
2096 -}  
2097 -```  
2098 -  
2099 -**按价格从高到低排序**:  
2100 -```json  
2101 -{  
2102 - "query": "玩具",  
2103 - "size": 20,  
2104 - "from": 0,  
2105 - "sort_by": "price",  
2106 - "sort_order": "desc"  
2107 -}  
2108 -```  
2109 -  
2110 -**按销量从高到低排序**:  
2111 -```json  
2112 -{  
2113 - "query": "玩具",  
2114 - "size": 20,  
2115 - "from": 0,  
2116 - "sort_by": "sales",  
2117 - "sort_order": "desc"  
2118 -}  
2119 -```  
2120 -  
2121 -**按默认(相关性)排序**:  
2122 -```json  
2123 -{  
2124 - "query": "玩具",  
2125 - "size": 20,  
2126 - "from": 0  
2127 -}  
2128 -```  
2129 -  
2130 -### 8.2 过滤搜索  
2131 -  
2132 -**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间  
2133 -  
2134 -```json  
2135 -{  
2136 - "query": "玩具",  
2137 - "size": 20,  
2138 - "language": "zh",  
2139 - "filters": {  
2140 - "category_name": "益智玩具"  
2141 - },  
2142 - "range_filters": {  
2143 - "min_price": {  
2144 - "gte": 50,  
2145 - "lte": 200  
2146 - }  
2147 - }  
2148 -}  
2149 -```  
2150 -  
2151 -**需求**: 搜索"手机",筛选多个品牌,价格范围  
2152 -  
2153 -```json  
2154 -{  
2155 - "query": "手机",  
2156 - "size": 20,  
2157 - "language": "zh",  
2158 - "filters": {  
2159 - "vendor.zh.keyword": ["品牌A", "品牌B"]  
2160 - },  
2161 - "range_filters": {  
2162 - "min_price": {  
2163 - "gte": 50,  
2164 - "lte": 200  
2165 - }  
2166 - }  
2167 -}  
2168 -```  
2169 -  
2170 -### 8.3 分面搜索  
2171 -  
2172 -**需求**: 搜索"玩具",获取类目和规格的分面统计,用于构建筛选器  
2173 -  
2174 -```json  
2175 -{  
2176 - "query": "玩具",  
2177 - "size": 20,  
2178 - "language": "zh",  
2179 - "facets": [  
2180 - {"field": "category1_name", "size": 15, "type": "terms"},  
2181 - {"field": "category2_name", "size": 10, "type": "terms"},  
2182 - {"field": "specifications", "size": 10, "type": "terms"}  
2183 - ]  
2184 -}  
2185 -```  
2186 -  
2187 -**需求**: 搜索"手机",获取价格区间和规格的分面统计  
2188 -  
2189 -```json  
2190 -{  
2191 - "query": "手机",  
2192 - "size": 20,  
2193 - "language": "zh",  
2194 - "facets": [  
2195 - {  
2196 - "field": "min_price",  
2197 - "type": "range",  
2198 - "ranges": [  
2199 - {"key": "0-50", "to": 50},  
2200 - {"key": "50-100", "from": 50, "to": 100},  
2201 - {"key": "100-200", "from": 100, "to": 200},  
2202 - {"key": "200+", "from": 200}  
2203 - ]  
2204 - },  
2205 - {  
2206 - "field": "specifications",  
2207 - "size": 10,  
2208 - "type": "terms"  
2209 - }  
2210 - ]  
2211 -}  
2212 -```  
2213 -  
2214 -### 8.4 规格过滤与分面  
2215 -  
2216 -**需求**: 搜索"手机",筛选color为"white"的商品  
2217 -  
2218 -```json  
2219 -{  
2220 - "query": "手机",  
2221 - "size": 20,  
2222 - "language": "zh",  
2223 - "filters": {  
2224 - "specifications": {  
2225 - "name": "color",  
2226 - "value": "white"  
2227 - }  
2228 - }  
2229 -}  
2230 -```  
2231 -  
2232 -**需求**: 搜索"手机",筛选color为"white"且size为"256GB"的商品  
2233 -  
2234 -```json  
2235 -{  
2236 - "query": "手机",  
2237 - "size": 20,  
2238 - "language": "zh",  
2239 - "filters": {  
2240 - "specifications": [  
2241 - {"name": "color", "value": "white"},  
2242 - {"name": "size", "value": "256GB"}  
2243 - ]  
2244 - }  
2245 -}  
2246 -```  
2247 -  
2248 -**需求**: 搜索"手机",筛选size为"3"、"4"或"5",且color为"green"的商品  
2249 -  
2250 -```json  
2251 -{  
2252 - "query": "手机",  
2253 - "size": 20,  
2254 - "language": "zh",  
2255 - "filters": {  
2256 - "specifications": [  
2257 - {"name": "size", "value": "3"},  
2258 - {"name": "size", "value": "4"},  
2259 - {"name": "size", "value": "5"},  
2260 - {"name": "color", "value": "green"}  
2261 - ]  
2262 - }  
2263 -}  
2264 -```  
2265 -  
2266 -**需求**: 搜索"手机",获取所有规格的分面统计  
2267 -  
2268 -```json  
2269 -{  
2270 - "query": "手机",  
2271 - "size": 20,  
2272 - "language": "zh",  
2273 - "facets": [  
2274 - {"field": "specifications", "size": 10, "type": "terms"}  
2275 - ]  
2276 -}  
2277 -```  
2278 -  
2279 -**需求**: 只获取"color"和"size"规格的分面统计  
2280 -  
2281 -```json  
2282 -{  
2283 - "query": "手机",  
2284 - "size": 20,  
2285 - "language": "zh",  
2286 - "facets": [  
2287 - {"field": "specifications.color", "size": 20, "type": "terms"},  
2288 - {"field": "specifications.size", "size": 15, "type": "terms"}  
2289 - ]  
2290 -}  
2291 -```  
2292 -  
2293 -**需求**: 搜索"手机",筛选类目和规格,并获取对应的分面统计  
2294 -  
2295 -```json  
2296 -{  
2297 - "query": "手机",  
2298 - "size": 20,  
2299 - "language": "zh",  
2300 - "filters": {  
2301 - "category_name": "手机",  
2302 - "specifications": {  
2303 - "name": "color",  
2304 - "value": "white"  
2305 - }  
2306 - },  
2307 - "facets": [  
2308 - {"field": "category1_name", "size": 15, "type": "terms"},  
2309 - {"field": "category2_name", "size": 10, "type": "terms"},  
2310 - {"field": "specifications.color", "size": 20, "type": "terms"},  
2311 - {"field": "specifications.size", "size": 15, "type": "terms"}  
2312 - ]  
2313 -}  
2314 -```  
2315 -  
2316 -### 8.5 SKU筛选  
2317 -  
2318 -**需求**: 搜索"芭比娃娃",每个SPU下按颜色筛选,每种颜色只显示一个SKU  
2319 -  
2320 -```json  
2321 -{  
2322 - "query": "芭比娃娃",  
2323 - "size": 20,  
2324 - "sku_filter_dimension": ["color"]  
2325 -}  
2326 -```  
2327 -  
2328 -**说明**:  
2329 -- 如果 `option1_name` 为 `"color"`,则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组  
2330 -- 每个SPU下,每种颜色只会返回第一个SKU  
2331 -- 如果维度不匹配,返回所有SKU(不进行过滤)  
2332 -  
2333 -### 8.7 分页查询  
2334 -  
2335 -**需求**: 获取第2页结果(每页20条)  
2336 -  
2337 -```json  
2338 -{  
2339 - "query": "手机",  
2340 - "size": 20,  
2341 - "from": 20  
2342 -}  
2343 -```  
2344 -  
2345 ----  
2346 -  
2347 -## 9. 数据模型  
2348 -  
2349 -### 9.1 商品字段定义  
2350 -  
2351 -| 字段名 | 类型 | 描述 |  
2352 -|--------|------|------|  
2353 -| `tenant_id` | keyword | 租户ID(多租户隔离) |  
2354 -| `spu_id` | keyword | SPU ID |  
2355 -| `title.<lang>` | object/text | 商品标题(多语言对象,如 `title.zh`, `title.en`) |  
2356 -| `brief.<lang>` | object/text | 商品短描述(多语言对象,如 `brief.zh`, `brief.en`) |  
2357 -| `description.<lang>` | object/text | 商品详细描述(多语言对象,如 `description.zh`, `description.en`) |  
2358 -| `vendor.<lang>` | object/text | 供应商/品牌(多语言对象,且带 keyword 子字段,如 `vendor.zh.keyword`) |  
2359 -| `category_path.<lang>` | object/text | 类目路径(多语言对象,用于搜索,如 `category_path.zh`) |  
2360 -| `category_name_text.<lang>` | object/text | 类目名称(多语言对象,用于搜索,如 `category_name_text.zh`) |  
2361 -| `category_id` | keyword | 类目ID |  
2362 -| `category_name` | keyword | 类目名称(用于过滤) |  
2363 -| `category_level` | integer | 类目层级 |  
2364 -| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称(用于过滤和分面) |  
2365 -| `tags` | keyword | 标签(数组) |  
2366 -| `specifications` | nested | 规格(嵌套对象数组) |  
2367 -| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 |  
2368 -| `min_price`, `max_price` | float | 最低/最高价格 |  
2369 -| `compare_at_price` | float | 原价 |  
2370 -| `sku_prices` | float | SKU价格列表(数组) |  
2371 -| `sku_weights` | long | SKU重量列表(数组) |  
2372 -| `sku_weight_units` | keyword | SKU重量单位列表(数组) |  
2373 -| `total_inventory` | long | 总库存 |  
2374 -| `sales` | long | 销量(展示销量) |  
2375 -| `skus` | nested | SKU详细信息(嵌套对象数组) |  
2376 -| `create_time`, `update_time` | date | 创建/更新时间 |  
2377 -| `title_embedding` | dense_vector | 标题向量(1024维,仅用于搜索) |  
2378 -| `image_embedding` | nested | 图片向量(嵌套,仅用于搜索) |  
2379 -  
2380 -> 所有租户共享统一的索引结构。文本字段支持中英文双语,后端根据 `language` 参数自动选择对应字段返回。  
2381 -  
2382 -### 9.2 字段类型速查  
2383 -  
2384 -| 类型 | ES Mapping | 用途 |  
2385 -|------|------------|------|  
2386 -| `text` | `text` | 全文检索(支持中英文分析器) |  
2387 -| `keyword` | `keyword` | 精确匹配、聚合、排序 |  
2388 -| `integer` | `integer` | 整数 |  
2389 -| `long` | `long` | 长整数 |  
2390 -| `float` | `float` | 浮点数 |  
2391 -| `date` | `date` | 日期时间 |  
2392 -| `nested` | `nested` | 嵌套对象(specifications, skus, image_embedding) |  
2393 -| `dense_vector` | `dense_vector` | 向量字段(title_embedding,仅用于搜索) |  
2394 -  
2395 -### 9.3 常用字段列表  
2396 -  
2397 -#### 过滤字段  
2398 -  
2399 -- `category_name`: 类目名称  
2400 -- `category1_name`, `category2_name`, `category3_name`: 多级类目  
2401 -- `category_id`: 类目ID  
2402 -- `vendor.zh.keyword`, `vendor.en.keyword`: 供应商/品牌(使用keyword子字段)  
2403 -- `tags`: 标签(keyword类型)  
2404 -- `option1_name`, `option2_name`, `option3_name`: 选项名称  
2405 -- `specifications`: 规格过滤(嵌套字段,格式见[过滤器详解](#33-过滤器详解))  
2406 -  
2407 -#### 范围字段  
2408 -  
2409 -- `min_price`: 最低价格  
2410 -- `max_price`: 最高价格  
2411 -- `compare_at_price`: 原价  
2412 -- `create_time`: 创建时间  
2413 -- `update_time`: 更新时间  
2414 -  
2415 -#### 排序字段  
2416 -  
2417 -- `price`: 价格(后端自动根据sort_order映射:asc→min_price,desc→max_price)  
2418 -- `sales`: 销量  
2419 -- `create_time`: 创建时间  
2420 -- `update_time`: 更新时间  
2421 -- `relevance_score`: 相关性分数(默认,不指定sort_by时使用)  
2422 -  
2423 -**注意**: 前端只需传 `price`,后端会自动处理:  
2424 -- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序(价格从低到高)  
2425 -- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序(价格从高到低)  
2426 -  
2427 -### 9.4 支持的分析器  
2428 -  
2429 -| 分析器 | 语言 | 描述 |  
2430 -|--------|------|------|  
2431 -| `index_ik` | 中文 | 中文索引分析器(用于中文字段) |  
2432 -| `query_ik` | 中文 | 中文查询分析器(用于中文字段) |  
2433 -| `hanlp_index` ⚠️ TODO(暂不支持) | 中文 | 中文索引分析器(用于中文字段) |  
2434 -| `hanlp_standard` ⚠️ TODO(暂不支持) | 中文 | 中文查询分析器(用于中文字段) |  
2435 -| `english` | 英文 | 标准英文分析器(用于英文字段) |  
2436 -| `lowercase` | - | 小写标准化器(用于keyword子字段) |  
2437 -  
2438 ----  
2439 -  
2440 -## 10. 接口级压测脚本  
2441 -  
2442 -仓库提供统一压测脚本:`scripts/perf_api_benchmark.py`,用于对以下接口做并发压测:  
2443 -  
2444 -- 后端搜索:`POST /search/`  
2445 -- 搜索建议:`GET /search/suggestions`  
2446 -- 向量服务:`POST /embed/text`  
2447 -- 翻译服务:`POST /translate`  
2448 -- 重排服务:`POST /rerank`  
2449 -  
2450 -说明:脚本对 `embed_text` 场景会校验返回向量内容有效性(必须是有限数值,不允许 `null/NaN/Inf`),不是只看 HTTP 200。  
2451 -  
2452 -### 10.1 快速示例  
2453 -  
2454 -```bash  
2455 -# suggest 压测(tenant 162)  
2456 -python scripts/perf_api_benchmark.py \  
2457 - --scenario backend_suggest \  
2458 - --tenant-id 162 \  
2459 - --duration 30 \  
2460 - --concurrency 50  
2461 -  
2462 -# search 压测  
2463 -python scripts/perf_api_benchmark.py \  
2464 - --scenario backend_search \  
2465 - --tenant-id 162 \  
2466 - --duration 30 \  
2467 - --concurrency 20  
2468 -  
2469 -# 全链路压测(search + suggest + embedding + translate + rerank)  
2470 -python scripts/perf_api_benchmark.py \  
2471 - --scenario all \  
2472 - --tenant-id 162 \  
2473 - --duration 60 \  
2474 - --concurrency 30 \  
2475 - --output perf_reports/all.json  
2476 -```  
2477 -  
2478 -### 10.2 自定义用例  
2479 -  
2480 -可通过 `--cases-file` 覆盖默认请求模板。示例文件:  
2481 -  
2482 -```bash  
2483 -scripts/perf_cases.json.example  
2484 -```  
2485 -  
2486 -执行示例:  
2487 -  
2488 -```bash  
2489 -python scripts/perf_api_benchmark.py \  
2490 - --scenario all \  
2491 - --tenant-id 162 \  
2492 - --cases-file scripts/perf_cases.json.example \  
2493 - --duration 60 \  
2494 - --concurrency 40  
2495 -```  
embeddings/server.py
@@ -129,7 +129,7 @@ _TEXT_REQUEST_TIMEOUT_SEC = max( @@ -129,7 +129,7 @@ _TEXT_REQUEST_TIMEOUT_SEC = max(
129 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30")) 129 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30"))
130 ) 130 )
131 _TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32"))) 131 _TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32")))
132 -_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1"))) 132 +_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "20")))
133 _OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503")) 133 _OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503"))
134 _LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3"))) 134 _LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3")))
135 _LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120"))) 135 _LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120")))