# 搜索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` | 由本服务查库并构建文档,仅测试/调试用 | | 索引健康检查 | 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 节;内容理解字段生成已迁移到独立项目,不再由本仓库维护。 | 需要灵活编排、或希望将 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) > 完整请求体参考 `tests/manual/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, "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", "enriched_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": "..." }, "enriched_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 内容理解字段生成能力(已迁出) `/indexer/enrich-content` 已迁移到独立项目,本仓库当前的 Indexer 服务(默认端口 `6004`)**不再暴露该接口**,也**不再在** `/indexer/build-docs`、`/indexer/build-docs-from-db`、`/indexer/reindex`、`/indexer/index` 的构建链路里内置生成 `qanchors`、`enriched_attributes`、`enriched_tags`、`enriched_taxonomy_attributes`。 当前建议的对接方式: 1. 调用本仓库的 `POST /indexer/build-docs` 或 `POST /indexer/build-docs-from-db` 生成基础 ES 文档。 2. 调用独立内容理解服务生成 `qanchors` / `enriched_*` 字段。 3. 由上游索引程序自行合并字段后写入 ES。 补充说明: - `search_products` mapping 仍保留上述字段,便于独立内容理解服务继续产出并写入。 - `suggestion` 等消费侧仍可读取 ES 中已有的 `qanchors` 字段;迁移的是“生成实现”,不是字段模型本身。 - 本文档不再维护独立内容理解服务的请求/响应细节,请以对应独立项目的文档为准。 ---