Commit 5bac964944e8cd54c86dbb9a8713476fba74c50c

Authored by tangwang
1 parent 7214c2e7

文本 embedding 与图片 embedding 已拆分为两个独立进程 / 端口

docs/CNCLIP_SERVICE说明文档.md
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 ## 1. 作用与边界 5 ## 1. 作用与边界
6 6
7 - `cnclip` 是独立 gRPC 服务(默认 `grpc://127.0.0.1:51000`)。 7 - `cnclip` 是独立 gRPC 服务(默认 `grpc://127.0.0.1:51000`)。
8 -- `embedding` 服务(6005)在 `USE_CLIP_AS_SERVICE=true` 时调用它完成 `/embed/image`。 8 +- 图片 embedding 服务(默认 `6008`)在 `USE_CLIP_AS_SERVICE=true` 时调用它完成 `/embed/image`。
9 - `cnclip` 不负责文本向量;文本向量由 TEI(8080)负责。 9 - `cnclip` 不负责文本向量;文本向量由 TEI(8080)负责。
10 10
11 ## 2. 代码与脚本入口 11 ## 2. 代码与脚本入口
@@ -90,7 +90,7 @@ CNCLIP_MODEL_NAME=CN-CLIP/ViT-H-14 ./scripts/service_ctl.sh start cnclip @@ -90,7 +90,7 @@ CNCLIP_MODEL_NAME=CN-CLIP/ViT-H-14 ./scripts/service_ctl.sh start cnclip
90 90
91 ```bash 91 ```bash
92 ./scripts/service_ctl.sh start cnclip 92 ./scripts/service_ctl.sh start cnclip
93 -# 或一次启动可选能力:./scripts/service_ctl.sh start embedding tei cnclip 93 +# 或一次启动可选能力:./scripts/service_ctl.sh start embedding embedding-image tei cnclip
94 ``` 94 ```
95 95
96 ### 6.2 设备选择优先级 96 ### 6.2 设备选择优先级
@@ -162,9 +162,9 @@ GPU 模式下应出现 `clip_server` 相关 `python` 进程及显存占用。 @@ -162,9 +162,9 @@ GPU 模式下应出现 `clip_server` 相关 `python` 进程及显存占用。
162 ### 7.5 与 embedding 服务联调 162 ### 7.5 与 embedding 服务联调
163 163
164 ```bash 164 ```bash
165 -./scripts/start_embedding_service.sh 165 +./scripts/start_embedding_image_service.sh
166 166
167 -curl -sS -X POST "http://127.0.0.1:6005/embed/image" \ 167 +curl -sS -X POST "http://127.0.0.1:6008/embed/image" \
168 -H "Content-Type: application/json" \ 168 -H "Content-Type: application/json" \
169 -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' 169 -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]'
170 ``` 170 ```
docs/DEVELOPER_GUIDE.md
@@ -92,7 +92,8 @@ MySQL (店匠 SPU/SKU) @@ -92,7 +92,8 @@ MySQL (店匠 SPU/SKU)
92 | backend | 6002 | 搜索 API(含 admin) | ✓ | 92 | backend | 6002 | 搜索 API(含 admin) | ✓ |
93 | indexer | 6004 | 索引 API(reindex/build-docs 等) | ✓ | 93 | indexer | 6004 | 索引 API(reindex/build-docs 等) | ✓ |
94 | frontend | 6003 | 调试 UI | ✓ | 94 | frontend | 6003 | 调试 UI | ✓ |
95 -| embedding | 6005 | 向量服务(文本/图片) | 可选 | 95 +| embedding | 6005 | 文本向量服务 | 可选 |
  96 +| embedding-image | 6008 | 图片向量服务 | 可选 |
96 | translator | 6006 | 翻译服务(`POST /translate` 支持单条或批量 list;批量失败用 `null` 占位) | 可选 | 97 | translator | 6006 | 翻译服务(`POST /translate` 支持单条或批量 list;批量失败用 `null` 占位) | 可选 |
97 | reranker | 6007 | 重排服务 | 可选 | 98 | reranker | 6007 | 重排服务 | 可选 |
98 99
@@ -156,8 +157,8 @@ docs/ # 文档(含本指南) @@ -156,8 +157,8 @@ docs/ # 文档(含本指南)
156 157
157 ### 4.6 embeddings 158 ### 4.6 embeddings
158 159
159 -- **职责**:提供向量服务(FastAPI):`POST /embed/text`、`POST /embed/image`;服务内按配置加载文本后端(如 Qwen3-Embedding-0.6B)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。  
160 -- **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;文本后端与参数统一从 `config/config.yaml -> services.embedding.backend(s)` 读取。 160 +- **职责**:提供向量服务(FastAPI):文本服务默认监听 `6005`,图片服务默认监听 `6008`;对外暴露 `POST /embed/text`、`POST /embed/image`、`GET /health`、`GET /ready`;服务内按配置加载文本后端(如 Qwen3-Embedding-0.6B)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。
  161 +- **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;文本后端与参数统一从 `config/config.yaml -> services.embedding.backend(s)` 读取;文本与图片流量应通过独立进程和独立 inflight limit 做隔离。
161 - **详见**:`embeddings/README.md`、本指南 §7.5–§7.6。 162 - **详见**:`embeddings/README.md`、本指南 §7.5–§7.6。
162 163
163 ### 4.7 reranker 164 ### 4.7 reranker
docs/QUICKSTART.md
@@ -57,7 +57,8 @@ source activate.sh @@ -57,7 +57,8 @@ source activate.sh
57 | backend | 6002 | ✓ | 搜索 API(`/search/*`)+ 管理接口(`/admin/*`) | 57 | backend | 6002 | ✓ | 搜索 API(`/search/*`)+ 管理接口(`/admin/*`) |
58 | indexer | 6004 | ✓ | 索引 API(`/indexer/*`) | 58 | indexer | 6004 | ✓ | 索引 API(`/indexer/*`) |
59 | frontend | 6003 | ✓ | 调试 UI | 59 | frontend | 6003 | ✓ | 调试 UI |
60 -| embedding | 6005 | - | 向量服务(`/embed/text`, `/embed/image`) | 60 +| embedding | 6005 | - | 文本向量服务(`/embed/text`) |
  61 +| embedding-image | 6008 | - | 图片向量服务(`/embed/image`) |
61 | translator | 6006 | - | 翻译服务(`/translate`) | 62 | translator | 6006 | - | 翻译服务(`/translate`) |
62 | reranker | 6007 | - | 重排服务(`/rerank`) | 63 | reranker | 6007 | - | 重排服务(`/rerank`) |
63 64
@@ -135,21 +136,22 @@ curl -X POST http://localhost:6004/indexer/build-docs \ @@ -135,21 +136,22 @@ curl -X POST http://localhost:6004/indexer/build-docs \
135 136
136 API 文档:`http://localhost:6004/docs` 137 API 文档:`http://localhost:6004/docs`
137 138
138 -#### Embedding 服务(6005 139 +#### Embedding 服务(文本 6005 / 图片 6008
139 140
140 ```bash 141 ```bash
141 # TEI(文本向量后端,默认) 142 # TEI(文本向量后端,默认)
142 # GPU(需 nvidia-container-toolkit) 143 # GPU(需 nvidia-container-toolkit)
143 TEI_DEVICE=cuda ./scripts/start_tei_service.sh 144 TEI_DEVICE=cuda ./scripts/start_tei_service.sh
144 145
145 -# Embedding API(会校验 TEI /health)  
146 -./scripts/start_embedding_service.sh 146 +# Embedding API(text 会校验 TEI /health)
  147 +./scripts/start_embedding_text_service.sh
  148 +./scripts/start_embedding_image_service.sh
147 149
148 curl -X POST http://localhost:6005/embed/text \ 150 curl -X POST http://localhost:6005/embed/text \
149 -H "Content-Type: application/json" \ 151 -H "Content-Type: application/json" \
150 -d '["衣服", "Bohemian Maxi Dress"]' 152 -d '["衣服", "Bohemian Maxi Dress"]'
151 153
152 -curl -X POST http://localhost:6005/embed/image \ 154 +curl -X POST http://localhost:6008/embed/image \
153 -H "Content-Type: application/json" \ 155 -H "Content-Type: application/json" \
154 -d '["https://example.com/img.jpg"]' 156 -d '["https://example.com/img.jpg"]'
155 ``` 157 ```
@@ -157,7 +159,7 @@ curl -X POST http://localhost:6005/embed/image \ @@ -157,7 +159,7 @@ curl -X POST http://localhost:6005/embed/image \
157 说明: 159 说明:
158 - TEI 默认镜像按 `TEI_VERSION` 组装:`cuda-<version>`(默认 `1.9`)。 160 - TEI 默认镜像按 `TEI_VERSION` 组装:`cuda-<version>`(默认 `1.9`)。
159 - `TEI_DEVICE=cuda` 时会严格校验 Docker GPU runtime;未配置会直接报错退出。 161 - `TEI_DEVICE=cuda` 时会严格校验 Docker GPU runtime;未配置会直接报错退出。
160 -- `/embed/image` 依赖 `cnclip`(`grpc://127.0.0.1:51000`),未启动时 embedding 服务会启动失败。 162 +- `/embed/image` 依赖 `cnclip`(`grpc://127.0.0.1:51000`),未启动时图片 embedding 服务会启动失败。
161 163
162 #### Translator 服务(6006) 164 #### Translator 服务(6006)
163 165
@@ -530,6 +532,7 @@ curl http://localhost:6002/health @@ -530,6 +532,7 @@ curl http://localhost:6002/health
530 curl http://localhost:6002/admin/health 532 curl http://localhost:6002/admin/health
531 curl http://localhost:6004/health 533 curl http://localhost:6004/health
532 curl http://localhost:6005/health 534 curl http://localhost:6005/health
  535 +curl http://localhost:6008/health
533 curl http://localhost:6006/health 536 curl http://localhost:6006/health
534 curl http://localhost:6007/health 537 curl http://localhost:6007/health
535 ``` 538 ```
docs/TEI_SERVICE说明文档.md
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 ## 1. 作用与边界 5 ## 1. 作用与边界
6 6
7 - TEI 提供文本向量 HTTP 服务(默认 `http://127.0.0.1:8080`)。 7 - TEI 提供文本向量 HTTP 服务(默认 `http://127.0.0.1:8080`)。
8 -- 本项目中 `embedding` 服务(6005)默认把文本向量请求转发到 TEI。 8 +- 本项目中文本 embedding 服务(默认 `6005`)把文本向量请求转发到 TEI。
9 - TEI 仅负责文本向量,不负责图片向量(图片向量由 `cnclip` 提供)。 9 - TEI 仅负责文本向量,不负责图片向量(图片向量由 `cnclip` 提供)。
10 10
11 ## 2. 代码与脚本入口 11 ## 2. 代码与脚本入口
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 - 启动脚本:`scripts/start_tei_service.sh` 13 - 启动脚本:`scripts/start_tei_service.sh`
14 - 停止脚本:`scripts/stop_tei_service.sh` 14 - 停止脚本:`scripts/stop_tei_service.sh`
15 - 统一编排:`scripts/service_ctl.sh` 15 - 统一编排:`scripts/service_ctl.sh`
16 -- embedding 服务启动脚本(会校验 TEI 健康):`scripts/start_embedding_service.sh` 16 +- 文本 embedding 服务启动脚本(会校验 TEI 健康):`scripts/start_embedding_text_service.sh`
17 17
18 ## 3. 前置条件 18 ## 3. 前置条件
19 19
@@ -117,7 +117,7 @@ curl -sS http://127.0.0.1:8080/embed \ @@ -117,7 +117,7 @@ curl -sS http://127.0.0.1:8080/embed \
117 ### 6.3 与 embedding 服务联调 117 ### 6.3 与 embedding 服务联调
118 118
119 ```bash 119 ```bash
120 -./scripts/start_embedding_service.sh 120 +./scripts/start_embedding_text_service.sh
121 121
122 curl -sS -X POST "http://127.0.0.1:6005/embed/text" \ 122 curl -sS -X POST "http://127.0.0.1:6005/embed/text" \
123 -H "Content-Type: application/json" \ 123 -H "Content-Type: application/json" \
@@ -152,7 +152,7 @@ curl -sS -X POST &quot;http://127.0.0.1:6005/embed/text&quot; \ @@ -152,7 +152,7 @@ curl -sS -X POST &quot;http://127.0.0.1:6005/embed/text&quot; \
152 启动全套(含 TEI): 152 启动全套(含 TEI):
153 153
154 ```bash 154 ```bash
155 -TEI_DEVICE=cuda ./scripts/service_ctl.sh start tei cnclip embedding translator reranker 155 +TEI_DEVICE=cuda ./scripts/service_ctl.sh start tei cnclip embedding embedding-image translator reranker
156 ``` 156 ```
157 157
158 仅启动 TEI: 158 仅启动 TEI:
docs/Usage-Guide.md
@@ -335,7 +335,8 @@ python -m http.server 6003 @@ -335,7 +335,8 @@ python -m http.server 6003
335 | Backend API | 6002 | http://localhost:6002 | 335 | Backend API | 6002 | http://localhost:6002 |
336 | Indexer API | 6004 | http://localhost:6004 | 336 | Indexer API | 6004 | http://localhost:6004 |
337 | Frontend Web | 6003 | http://localhost:6003 | 337 | Frontend Web | 6003 | http://localhost:6003 |
338 -| Embedding (optional) | 6005 | http://localhost:6005 | 338 +| Embedding Text (optional) | 6005 | http://localhost:6005 |
  339 +| Embedding Image (optional) | 6008 | http://localhost:6008 |
339 | TEI (optional) | 8080 | http://localhost:8080 | 340 | TEI (optional) | 8080 | http://localhost:8080 |
340 | Translation (optional) | 6006 | http://localhost:6006 | 341 | Translation (optional) | 6006 | http://localhost:6006 |
341 | Reranker (optional) | 6007 | http://localhost:6007 | 342 | Reranker (optional) | 6007 | http://localhost:6007 |
@@ -380,7 +381,8 @@ INDEXER_PORT=6004 @@ -380,7 +381,8 @@ INDEXER_PORT=6004
380 381
381 # Optional service ports 382 # Optional service ports
382 FRONTEND_PORT=6003 383 FRONTEND_PORT=6003
383 -EMBEDDING_PORT=6005 384 +EMBEDDING_TEXT_PORT=6005
  385 +EMBEDDING_IMAGE_PORT=6008
384 TEI_PORT=8080 386 TEI_PORT=8080
385 CNCLIP_PORT=51000 387 CNCLIP_PORT=51000
386 TRANSLATION_PORT=6006 388 TRANSLATION_PORT=6006
docs/向量化模块和API说明文档.md
@@ -4,12 +4,36 @@ @@ -4,12 +4,36 @@
4 4
5 ## 服务接口 5 ## 服务接口
6 6
7 -- `POST /embed/text`:文本向量,入参 `["text1", "text2"]`,出参 `[[...], [...]]`  
8 -- `POST /embed/image`:图片向量,入参 `["url1", "url2"]`,出参 `[[...], [...]]` 7 +- 文本服务:`POST http://localhost:6005/embed/text`
  8 +- 图片服务:`POST http://localhost:6008/embed/image`
  9 +- 健康检查:`GET /health`
  10 +- 就绪检查:`GET /ready`
  11 +
  12 +## 当前架构
  13 +
  14 +- 文本 embedding 与图片 embedding 已拆分为两个独立进程 / 端口:
  15 + - text: `6005`
  16 + - image: `6008`
  17 +- 两侧有独立并发控制:
  18 + - `TEXT_MAX_INFLIGHT`
  19 + - `IMAGE_MAX_INFLIGHT`
  20 +- 两侧都接入 Redis 向量缓存,value 统一使用 BF16 bytes 存储。
  21 +
  22 +## 缓存
  23 +
  24 +- 当前是双层缓存:
  25 + - 调用侧 client 先查 Redis
  26 + - 服务侧收到请求后再查 Redis
  27 +- 当前主 key 规则:
  28 + - 文本:`embedding:embed:norm{0|1}:{text}`
  29 + - 图片:`embedding:image:embed:norm{0|1}:{url_or_path}`
  30 +- full-cache-hit 时,服务会直接返回,不占用模型 lane。
9 31
10 ## 配置 32 ## 配置
11 33
12 - Provider/URL:`config/config.yaml` 的 `services.embedding` 34 - Provider/URL:`config/config.yaml` 的 `services.embedding`
  35 +- 文本服务 URL:`services.embedding.providers.http.text_base_url`
  36 +- 图片服务 URL:`services.embedding.providers.http.image_base_url`
13 - 文本模型:`embeddings/config.py` 的 `TEXT_MODEL_ID`(默认 `Qwen/Qwen3-Embedding-0.6B`) 37 - 文本模型:`embeddings/config.py` 的 `TEXT_MODEL_ID`(默认 `Qwen/Qwen3-Embedding-0.6B`)
14 - 运行参数:`TEXT_DEVICE`、`TEXT_BATCH_SIZE`、`TEXT_NORMALIZE_EMBEDDINGS` 38 - 运行参数:`TEXT_DEVICE`、`TEXT_BATCH_SIZE`、`TEXT_NORMALIZE_EMBEDDINGS`
15 39
docs/工作总结-微服务性能优化与架构.md
@@ -16,12 +16,12 @@ @@ -16,12 +16,12 @@
16 | **vLLM** | 高吞吐推理框架,更适合生成式与重排混合场景 | 纯 embedding 场景通常不作为首选 | 16 | **vLLM** | 高吞吐推理框架,更适合生成式与重排混合场景 | 纯 embedding 场景通常不作为首选 |
17 | **TEI** | HuggingFace 官方 embedding 专用推理服务,Docker 部署 | **当前最优选型** | 17 | **TEI** | HuggingFace 官方 embedding 专用推理服务,Docker 部署 | **当前最优选型** |
18 18
19 -**当前方案**:以 **TEI** 为文本向量后端,模型为 `Qwen/Qwen3-Embedding-0.6B`embedding 服务(端口 **6005**)将 `POST /embed/text` 请求转发至 TEI(默认端口 **8080**)。 19 +**当前方案**:以 **TEI** 为文本向量后端,模型为 `Qwen/Qwen3-Embedding-0.6B`;文本 embedding 服务(端口 **6005**)将 `POST /embed/text` 请求转发至 TEI(默认端口 **8080**)。
20 20
21 **具体配置与脚本**: 21 **具体配置与脚本**:
22 - **配置**:`config/config.yaml` → `services.embedding.backend: "tei"`,`services.embedding.backends.tei.base_url: "http://127.0.0.1:8080"`、`model_id: "Qwen/Qwen3-Embedding-0.6B"`、`timeout_sec: 20`。 22 - **配置**:`config/config.yaml` → `services.embedding.backend: "tei"`,`services.embedding.backends.tei.base_url: "http://127.0.0.1:8080"`、`model_id: "Qwen/Qwen3-Embedding-0.6B"`、`timeout_sec: 20`。
23 - **启动**:`scripts/start_tei_service.sh`(Docker 容器);环境变量:`TEI_DEVICE=cuda`(默认)、`TEI_PORT=8080`、`TEI_MODEL_ID=Qwen/Qwen3-Embedding-0.6B`、`TEI_VERSION=1.9`、`TEI_MAX_BATCH_TOKENS=4096`、`TEI_MAX_CLIENT_BATCH_SIZE=24`、`TEI_DTYPE=float16`;T4 自动选镜像 `ghcr.io/huggingface/text-embeddings-inference:turing-1.9`。 23 - **启动**:`scripts/start_tei_service.sh`(Docker 容器);环境变量:`TEI_DEVICE=cuda`(默认)、`TEI_PORT=8080`、`TEI_MODEL_ID=Qwen/Qwen3-Embedding-0.6B`、`TEI_VERSION=1.9`、`TEI_MAX_BATCH_TOKENS=4096`、`TEI_MAX_CLIENT_BATCH_SIZE=24`、`TEI_DTYPE=float16`;T4 自动选镜像 `ghcr.io/huggingface/text-embeddings-inference:turing-1.9`。
24 -- **编排**:`./scripts/service_ctl.sh start tei` 或 `start embedding`(embedding 会校验 TEI `/health` 后再就绪)。 24 +- **编排**:`./scripts/service_ctl.sh start tei` 或 `start embedding`(text embedding 会校验 TEI `/health` 后再就绪)。
25 25
26 **工程化收益**: 26 **工程化收益**:
27 - **独立服务**:TEI 以 Docker 容器运行,与主程序 `.venv` 解耦;embedding 使用 `.venv-embedding`,便于独立扩缩容与升级。 27 - **独立服务**:TEI 以 Docker 容器运行,与主程序 `.venv` 解耦;embedding 使用 `.venv-embedding`,便于独立扩缩容与升级。
@@ -71,7 +71,7 @@ instruction: &quot;Given a shopping query, rank product titles by relevance&quot; @@ -71,7 +71,7 @@ instruction: &quot;Given a shopping query, rank product titles by relevance&quot;
71 71
72 **具体内容**: 72 **具体内容**:
73 - **端口**:clip-as-service 默认 **51000**(`CNCLIP_PORT`);文本走 TEI(8080),图片走 clip-as-service。 73 - **端口**:clip-as-service 默认 **51000**(`CNCLIP_PORT`);文本走 TEI(8080),图片走 clip-as-service。
74 -- **API**:embedding 服务(6005)统一暴露 `POST /embed/text` 与 `POST /embed/image`;图片请求由 `embeddings/server.py` 按配置调用实现 `ImageEncoderProtocol` 的后端(clip-as-service 或本地 CN-CLIP)。 74 +- **API**:文本 embedding 服务默认暴露 `POST /embed/text`(6005),图片 embedding 服务默认暴露 `POST /embed/image`(6008);图片请求由 `embeddings/server.py` 按配置调用实现 `ImageEncoderProtocol` 的后端(clip-as-service 或本地 CN-CLIP)。
75 - **环境与启停**:CN-CLIP 使用独立虚拟环境 `.venv-cnclip`;启动 `scripts/start_cnclip_service.sh`,或 `./scripts/service_ctl.sh start cnclip`;设备可通过 `CNCLIP_DEVICE=cuda`(默认)或 `cpu` 指定。 75 - **环境与启停**:CN-CLIP 使用独立虚拟环境 `.venv-cnclip`;启动 `scripts/start_cnclip_service.sh`,或 `./scripts/service_ctl.sh start cnclip`;设备可通过 `CNCLIP_DEVICE=cuda`(默认)或 `cpu` 指定。
76 - **配置**:图片后端在 `config/config.yaml` 的 `services.embedding` 下配置(若存在 image 相关 backend);clip-as-service 默认模型由 `embeddings/config.py` 的 `CLIP_AS_SERVICE_MODEL_NAME` 控制,flow 配置在 `third-party/clip-as-service/server/torch-flow-temp.yml`。 76 - **配置**:图片后端在 `config/config.yaml` 的 `services.embedding` 下配置(若存在 image 相关 backend);clip-as-service 默认模型由 `embeddings/config.py` 的 `CLIP_AS_SERVICE_MODEL_NAME` 控制,flow 配置在 `third-party/clip-as-service/server/torch-flow-temp.yml`。
77 77
@@ -127,8 +127,8 @@ instruction: &quot;Given a shopping query, rank product titles by relevance&quot; @@ -127,8 +127,8 @@ instruction: &quot;Given a shopping query, rank product titles by relevance&quot;
127 - **脚本**:`scripts/service_ctl.sh` 统一负责各服务的生命周期与监控;依赖 `scripts/lib/load_env.sh` 与项目根目录 `.env`。 127 - **脚本**:`scripts/service_ctl.sh` 统一负责各服务的生命周期与监控;依赖 `scripts/lib/load_env.sh` 与项目根目录 `.env`。
128 - **服务与端口**: 128 - **服务与端口**:
129 - 核心:backend **6002**、indexer **6004**、frontend **6003**。 129 - 核心:backend **6002**、indexer **6004**、frontend **6003**。
130 - - 可选:embedding **6005**、translator **6006**、reranker **6007**、tei **8080**、cnclip **51000**。  
131 - - 端口可由环境变量覆盖:`API_PORT`、`INDEXER_PORT`、`FRONTEND_PORT`、`EMBEDDING_PORT`、`TRANSLATION_PORT`、`RERANKER_PORT`、`TEI_PORT`、`CNCLIP_PORT`。 130 + - 可选:embedding(text) **6005**、embedding-image **6008**、translator **6006**、reranker **6007**、tei **8080**、cnclip **51000**。
  131 + - 端口可由环境变量覆盖:`API_PORT`、`INDEXER_PORT`、`FRONTEND_PORT`、`EMBEDDING_TEXT_PORT`、`EMBEDDING_IMAGE_PORT`、`TRANSLATION_PORT`、`RERANKER_PORT`、`TEI_PORT`、`CNCLIP_PORT`。
132 - **命令**: 132 - **命令**:
133 - `./scripts/service_ctl.sh start [service...]` 或 `start all`(all = tei cnclip embedding translator reranker backend indexer frontend,按依赖顺序);`stop`、`restart` 同参数;`status` 默认列出所有服务。 133 - `./scripts/service_ctl.sh start [service...]` 或 `start all`(all = tei cnclip embedding translator reranker backend indexer frontend,按依赖顺序);`stop`、`restart` 同参数;`status` 默认列出所有服务。
134 - 启动时:backend/indexer/frontend/embedding/translator/reranker 会写 pid 到 `logs/<service>.pid`,并执行 `wait_for_health`(GET `http://127.0.0.1:<port>/health`);reranker 健康重试 90 次,其余 30 次;TEI 校验 Docker 容器存在且 `/health` 成功;cnclip 无 HTTP 健康则仅校验进程/端口。 134 - 启动时:backend/indexer/frontend/embedding/translator/reranker 会写 pid 到 `logs/<service>.pid`,并执行 `wait_for_health`(GET `http://127.0.0.1:<port>/health`);reranker 健康重试 90 次,其余 30 次;TEI 校验 Docker 容器存在且 `/health` 成功;cnclip 无 HTTP 健康则仅校验进程/端口。
docs/搜索API对接指南.md
@@ -157,8 +157,8 @@ curl -X POST &quot;http://43.166.252.75:6002/search/&quot; \ @@ -157,8 +157,8 @@ curl -X POST &quot;http://43.166.252.75:6002/search/&quot; \
157 157
158 | 服务 | 端口 | 接口 | 说明 | 158 | 服务 | 端口 | 接口 | 说明 |
159 |------|------|------|------| 159 |------|------|------|------|
160 -| 向量服务 | 6005 | `POST /embed/text` | 文本向量化 |  
161 -| 向量服务 | 6005 | `POST /embed/image` | 图片向量化 | 160 +| 向量服务(文本) | 6005 | `POST /embed/text` | 文本向量化 |
  161 +| 向量服务(图片) | 6008 | `POST /embed/image` | 图片向量化 |
162 | 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) | 162 | 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) |
163 | 重排服务 | 6007 | `POST /rerank` | 检索结果重排 | 163 | 重排服务 | 6007 | `POST /rerank` | 检索结果重排 |
164 | 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 | 164 | 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 |
@@ -1691,7 +1691,8 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \ @@ -1691,7 +1691,8 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \
1691 1691
1692 | 服务 | 默认端口 | Base URL | 说明 | 1692 | 服务 | 默认端口 | Base URL | 说明 |
1693 |------|----------|----------|------| 1693 |------|----------|----------|------|
1694 -| 向量服务 | 6005 | `http://localhost:6005` | 文本/图片向量化,用于语义搜索与以图搜图 | 1694 +| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 |
  1695 +| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 |
1695 | 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) | 1696 | 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) |
1696 | 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 | 1697 | 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 |
1697 1698
@@ -1700,13 +1701,28 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \ @@ -1700,13 +1701,28 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \
1700 1701
1701 ### 7.1 向量服务(Embedding) 1702 ### 7.1 向量服务(Embedding)
1702 1703
1703 -- **Base URL**: `http://localhost:6005`(可通过 `EMBEDDING_SERVICE_URL` 覆盖)  
1704 -- **启动**: `./scripts/start_embedding_service.sh` 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 + - 兼容入口:`./scripts/start_embedding_service.sh text|image|all`
1705 - **依赖**: 1711 - **依赖**:
1706 - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`) 1712 - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`)
1707 - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`) 1713 - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`)
1708 - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU) 1714 - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU)
1709 - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`) 1715 - cnclip 默认使用 `cuda`;若配置为 `cuda` 但 GPU 不可用会启动失败(不会自动降级到 `cpu`)
  1716 + - 当前单机部署建议保持单实例,通过**文本/图片拆分 + 独立限流**隔离压力
  1717 +
  1718 +补充说明:
  1719 +
  1720 +- 文本和图片现在已经拆成**不同进程 / 不同端口**,避免图片下载与编码波动影响文本向量化。
  1721 +- 服务端对 text / image 有**独立 admission control**:
  1722 + - `TEXT_MAX_INFLIGHT`
  1723 + - `IMAGE_MAX_INFLIGHT`
  1724 +- 当超过处理能力时,服务会直接返回过载错误,而不是无限排队。
  1725 +- `GET /health` 会返回各自的 `limits`、`stats`、`cache_enabled` 等状态;`GET /ready` 用于就绪探针。
1710 1726
1711 #### 7.1.1 `POST /embed/text` — 文本向量化 1727 #### 7.1.1 `POST /embed/text` — 文本向量化
1712 1728
@@ -1724,7 +1740,7 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \ @@ -1724,7 +1740,7 @@ curl -X POST &quot;http://localhost:6004/indexer/enrich-content&quot; \
1724 1740
1725 **完整 curl 示例**: 1741 **完整 curl 示例**:
1726 ```bash 1742 ```bash
1727 -curl -X POST "http://localhost:6005/embed/text" \ 1743 +curl -X POST "http://localhost:6005/embed/text?normalize=true" \
1728 -H "Content-Type: application/json" \ 1744 -H "Content-Type: application/json" \
1729 -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]' 1745 -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]'
1730 ``` 1746 ```
@@ -1733,7 +1749,7 @@ curl -X POST &quot;http://localhost:6005/embed/text&quot; \ @@ -1733,7 +1749,7 @@ curl -X POST &quot;http://localhost:6005/embed/text&quot; \
1733 1749
1734 将图片 URL 或路径转为向量,用于以图搜图。 1750 将图片 URL 或路径转为向量,用于以图搜图。
1735 1751
1736 -前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,`/embed/image` 会返回 500 1752 +前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误
1737 1753
1738 **请求体**(JSON 数组): 1754 **请求体**(JSON 数组):
1739 ```json 1755 ```json
@@ -1747,7 +1763,7 @@ curl -X POST &quot;http://localhost:6005/embed/text&quot; \ @@ -1747,7 +1763,7 @@ curl -X POST &quot;http://localhost:6005/embed/text&quot; \
1747 1763
1748 **完整 curl 示例**: 1764 **完整 curl 示例**:
1749 ```bash 1765 ```bash
1750 -curl -X POST "http://localhost:6005/embed/image" \ 1766 +curl -X POST "http://localhost:6008/embed/image?normalize=true" \
1751 -H "Content-Type: application/json" \ 1767 -H "Content-Type: application/json" \
1752 -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' 1768 -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]'
1753 ``` 1769 ```
@@ -1756,9 +1772,32 @@ curl -X POST &quot;http://localhost:6005/embed/image&quot; \ @@ -1756,9 +1772,32 @@ curl -X POST &quot;http://localhost:6005/embed/image&quot; \
1756 1772
1757 ```bash 1773 ```bash
1758 curl "http://localhost:6005/health" 1774 curl "http://localhost:6005/health"
  1775 +curl "http://localhost:6008/health"
  1776 +```
  1777 +
  1778 +返回中会包含:
  1779 +
  1780 +- `service_kind`:`text` / `image` / `all`
  1781 +- `cache_enabled`:text/image Redis 缓存是否可用
  1782 +- `limits`:当前 inflight limit、active、rejected_total 等
  1783 +- `stats`:request_total、cache_hits、cache_misses、avg_latency_ms 等
  1784 +
  1785 +#### 7.1.4 `GET /ready` — 就绪检查
  1786 +
  1787 +```bash
  1788 +curl "http://localhost:6005/ready"
  1789 +curl "http://localhost:6008/ready"
1759 ``` 1790 ```
1760 1791
1761 -#### 7.1.4 TEI 统一调优建议(主服务) 1792 +#### 7.1.5 缓存与限流说明
  1793 +
  1794 +- 文本与图片都会先查 Redis 向量缓存。
  1795 +- Redis 中 value 仍是 **BF16 bytes**,读取后恢复成 `float32` 返回。
  1796 +- cache key 已区分 `normalize=true/false`,避免不同归一化策略命中同一条缓存。
  1797 +- 当服务端发现请求是 **full-cache-hit** 时,会直接返回,不占用模型并发槽位。
  1798 +- 当服务端发现超过 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT` 时,会直接拒绝,而不是无限排队。
  1799 +
  1800 +#### 7.1.6 TEI 统一调优建议(主服务)
1762 1801
1763 使用单套主服务即可同时兼顾: 1802 使用单套主服务即可同时兼顾:
1764 - 在线 query 向量化(低延迟,常见 `batch=1~4`) 1803 - 在线 query 向量化(低延迟,常见 `batch=1~4`)
@@ -1773,7 +1812,8 @@ curl &quot;http://localhost:6005/health&quot; @@ -1773,7 +1812,8 @@ curl &quot;http://localhost:6005/health&quot;
1773 1812
1774 默认端口: 1813 默认端口:
1775 - TEI: `http://127.0.0.1:8080` 1814 - TEI: `http://127.0.0.1:8080`
1776 -- 向量服务(`/embed/text`): `http://127.0.0.1:6005` 1815 +- 文本向量服务(`/embed/text`): `http://127.0.0.1:6005`
  1816 +- 图片向量服务(`/embed/image`): `http://127.0.0.1:6008`
1777 1817
1778 当前主 TEI 启动默认值(已按 T4/短文本场景调优): 1818 当前主 TEI 启动默认值(已按 T4/短文本场景调优):
1779 - `TEI_MAX_BATCH_TOKENS=4096` 1819 - `TEI_MAX_BATCH_TOKENS=4096`
docs/缓存与Redis使用说明.md
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 19
20 | 模块 / 场景 | Key 模板 | Value 内容示例 | 过期策略 | 备注 | 20 | 模块 / 场景 | Key 模板 | Value 内容示例 | 过期策略 | 备注 |
21 |------------|----------|----------------|----------|------| 21 |------------|----------|----------------|----------|------|
22 -| 向量缓存(text/image embedding) | `{EMBEDDING_CACHE_PREFIX}:{query_or_url}` / `{EMBEDDING_CACHE_PREFIX}:image:{url_or_path}` | **BF16 bytes**(每维 2 字节大端存储),读取后恢复为 `np.float32` | TTL=`REDIS_CONFIG["cache_expire_days"]` 天;访问时滑动过期 | 见 `embeddings/text_encoder.py`(文本)与 `embeddings/image_encoder.py`(图片);前缀由 `REDIS_CONFIG["embedding_cache_prefix"]` 控制 | 22 +| 向量缓存(text/image embedding) | 文本:`{EMBEDDING_CACHE_PREFIX}:embed:norm{0|1}:{text}`;图片:`{EMBEDDING_CACHE_PREFIX}:image:embed:norm{0|1}:{url_or_path}` | **BF16 bytes**(每维 2 字节大端存储),读取后恢复为 `np.float32` | TTL=`REDIS_CONFIG["cache_expire_days"]` 天;访问时滑动过期 | 见 `embeddings/text_encoder.py`、`embeddings/image_encoder.py`、`embeddings/server.py`;前缀由 `REDIS_CONFIG["embedding_cache_prefix"]` 控制 |
23 | 翻译结果缓存(translator service) | `trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)}` | 机翻后的单条字符串 | TTL=`services.translation.cache.ttl_seconds` 秒;可配置滑动过期 | 见 `translation/service.py` + `config/config.yaml` | 23 | 翻译结果缓存(translator service) | `trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)}` | 机翻后的单条字符串 | TTL=`services.translation.cache.ttl_seconds` 秒;可配置滑动过期 | 见 `translation/service.py` + `config/config.yaml` |
24 | 商品内容理解缓存(anchors / 语义属性 / tags) | `{ANCHOR_CACHE_PREFIX}:{tenant_or_global}:{target_lang}:{md5(title)}` | `json.dumps(dict)`,包含 id/title/category/tags/anchor_text 等 | TTL=`ANCHOR_CACHE_EXPIRE_DAYS` 天 | 见 `indexer/product_enrich.py` | 24 | 商品内容理解缓存(anchors / 语义属性 / tags) | `{ANCHOR_CACHE_PREFIX}:{tenant_or_global}:{target_lang}:{md5(title)}` | `json.dumps(dict)`,包含 id/title/category/tags/anchor_text 等 | TTL=`ANCHOR_CACHE_EXPIRE_DAYS` 天 | 见 `indexer/product_enrich.py` |
25 25
@@ -27,23 +27,49 @@ @@ -27,23 +27,49 @@
27 27
28 --- 28 ---
29 29
30 -## 2. 文本向量缓存(embeddings/text_encoder.py) 30 +## 2. 向量缓存(embeddings/text_encoder.py / embeddings/image_encoder.py / embeddings/server.py)
31 31
32 -- **代码位置**:`embeddings/text_encoder.py` 中 `TextEmbeddingEncoder`  
33 -- **用途**:缓存调用向量服务(6005)的文本向量结果,避免重复计算。 32 +- **代码位置**:
  33 + - 调用侧:`embeddings/text_encoder.py`、`embeddings/image_encoder.py`
  34 + - 服务侧:`embeddings/server.py`
  35 + - 公共 Redis/BF16 编解码:`embeddings/redis_embedding_cache.py`、`embeddings/bf16.py`
  36 +- **用途**:缓存文本/图片向量结果,避免重复推理、重复下载图片、重复占用模型并发槽位。
  37 +
  38 +### 2.0 当前缓存链路总览
  39 +
  40 +- 现在是**双层缓存**:
  41 + - **调用侧缓存**:`TextEmbeddingEncoder` / `CLIPImageEncoder` 在发 HTTP 请求前先查 Redis;
  42 + - **服务侧缓存**:`/embed/text` / `/embed/image` 在进入后端推理前再查 Redis。
  43 +- 这次改动里,**BF16 存储格式本身没有变化**:
  44 + - 写入:`float32 -> BF16 -> bytes -> Redis`
  45 + - 读取:`Redis bytes -> BF16 -> float32(np.float32)`
  46 +- 这次新增的核心变化是:
  47 + - **缓存 key 加入 normalize 维度**,避免 `normalize=true` 与 `normalize=false` 命中同一条缓存;
  48 + - **服务端也启用同一套 Redis 向量缓存**,而不是只有 client 侧缓存;
  49 + - **拆分 text / image 服务** 后,图片压力不会再拖慢文本服务;
  50 + - **服务侧 full-cache-hit 可直接返回**,不会再进入模型限流槽位。
34 51
35 ### 2.1 Key 设计 52 ### 2.1 Key 设计
36 53
37 -- 函数:`_get_cache_key(query: str, normalize_embeddings: bool) -> str` 54 +- 统一 helper:`embeddings/cache_keys.py`
  55 +- 文本主 key:`build_text_cache_key(text, normalize=...)`
  56 +- 图片主 key:`build_image_cache_key(url, normalize=...)`
38 - 模板: 57 - 模板:
39 58
40 ```text 59 ```text
41 -{EMBEDDING_CACHE_PREFIX}:{query} 60 +文本: {EMBEDDING_CACHE_PREFIX}:embed:norm{0|1}:{text}
  61 +图片: {EMBEDDING_CACHE_PREFIX}:image:embed:norm{0|1}:{url_or_path}
42 ``` 62 ```
43 63
44 - 字段说明: 64 - 字段说明:
45 - `EMBEDDING_CACHE_PREFIX`:来自 `REDIS_CONFIG["embedding_cache_prefix"]`,默认值为 `"embedding"`,可通过环境变量 `REDIS_EMBEDDING_CACHE_PREFIX` 覆盖; 65 - `EMBEDDING_CACHE_PREFIX`:来自 `REDIS_CONFIG["embedding_cache_prefix"]`,默认值为 `"embedding"`,可通过环境变量 `REDIS_EMBEDDING_CACHE_PREFIX` 覆盖;
46 - - `query`:原始文本(未做哈希),注意长度特别长的 query 会直接出现在 key 中。 66 + - `norm1` / `norm0`:分别表示 `normalize=true` / `normalize=false`;
  67 + - `text` / `url_or_path`:当前仍直接使用规范化后的原始输入,不做哈希。
  68 +
  69 +补充说明:
  70 +
  71 +- 本次把 raw key 格式统一成 `embed:norm{0|1}:...`,比以 `norm:` 开头更清晰,也更接近历史命名习惯。
  72 +- 当前实现**不再兼容历史 key 协议**,只保留这一套主 key 规则,以降低运行时复杂度和歧义。
47 73
48 ### 2.2 Value 与类型 74 ### 2.2 Value 与类型
49 75
@@ -68,6 +94,23 @@ @@ -68,6 +94,23 @@
68 - 直接丢弃该缓存(并尝试 `delete` key); 94 - 直接丢弃该缓存(并尝试 `delete` key);
69 - 回退为重新调用向量服务。 95 - 回退为重新调用向量服务。
70 96
  97 +### 2.5 服务拆分与缓存/限流关系
  98 +
  99 +- 当前默认部署形态:
  100 + - 文本 embedding 服务:`6005`
  101 + - 图片 embedding 服务:`6008`
  102 +- 调用方配置:
  103 + - `EMBEDDING_TEXT_SERVICE_URL`
  104 + - `EMBEDDING_IMAGE_SERVICE_URL`
  105 +- 服务端配置:
  106 + - `TEXT_MAX_INFLIGHT`
  107 + - `IMAGE_MAX_INFLIGHT`
  108 + - `EMBEDDING_SERVICE_KIND=all|text|image`
  109 +- 关键行为:
  110 + - **full-cache-hit** 请求在服务端会直接返回,不占用 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT`;
  111 + - **cache miss** 才会进入对应模型 lane;
  112 + - 因此高频重复图片请求不会再因为严格的图片限流而大量阻塞。
  113 +
71 --- 114 ---
72 115
73 ## 3. 翻译结果缓存(translation/service.py) 116 ## 3. 翻译结果缓存(translation/service.py)
embeddings/README.md
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 - **统一配置**:`config.py` 16 - **统一配置**:`config.py`
17 - **接口契约**:`protocols.ImageEncoderProtocol`(图片编码统一为 `encode_image_urls(urls, batch_size, normalize_embeddings)`,本地 CN-CLIP 与 clip-as-service 均实现该接口) 17 - **接口契约**:`protocols.ImageEncoderProtocol`(图片编码统一为 `encode_image_urls(urls, batch_size, normalize_embeddings)`,本地 CN-CLIP 与 clip-as-service 均实现该接口)
18 18
19 -说明:历史上的云端 embedding 试验实现(DashScope)已从主仓库移除,当前仅维护 6005 这条统一向量服务链路 19 +说明:历史上的云端 embedding 试验实现(DashScope)已从主仓库移除。当前默认部署为**文本服务 6005** 与**图片服务 6008** 两条链路,也兼容 `all` 模式单进程启动
20 20
21 ### 文本向量后端(默认) 21 ### 文本向量后端(默认)
22 22
@@ -27,6 +27,15 @@ @@ -27,6 +27,15 @@
27 27
28 ### 服务接口 28 ### 服务接口
29 29
  30 +- 文本服务(默认 `6005`)
  31 + - `POST /embed/text`
  32 + - `GET /health`
  33 + - `GET /ready`
  34 +- 图片服务(默认 `6008`)
  35 + - `POST /embed/image`
  36 + - `GET /health`
  37 + - `GET /ready`
  38 +
30 - `POST /embed/text` 39 - `POST /embed/text`
31 - 入参:`["文本1", "文本2", ...]` 40 - 入参:`["文本1", "文本2", ...]`
32 - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认) 41 - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认)
@@ -37,6 +46,28 @@ @@ -37,6 +46,28 @@
37 - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认) 46 - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认)
38 - 出参:`[[...], [...], ...]`(与输入按 index 对齐,失败直接报错) 47 - 出参:`[[...], [...], ...]`(与输入按 index 对齐,失败直接报错)
39 48
  49 +### Redis 向量缓存
  50 +
  51 +- Value 格式没有变化,仍然是 **BF16 bytes**:
  52 + - 写入:`float32 -> BF16 -> bytes`
  53 + - 读取:`bytes -> BF16 -> float32`
  54 +- 现在是**双层缓存**:
  55 + - client 侧:`text_encoder.py` / `image_encoder.py`
  56 + - service 侧:`server.py`
  57 +- 当前主 key 格式:
  58 + - 文本:`embedding:embed:norm{0|1}:{text}`
  59 + - 图片:`embedding:image:embed:norm{0|1}:{url_or_path}`
  60 +- 当前实现不再兼容历史 key 规则,只保留这一套格式,减少代码路径和缓存歧义。
  61 +
  62 +### 压力隔离与拒绝策略
  63 +
  64 +- 文本与图片各自有独立 admission control:
  65 + - `TEXT_MAX_INFLIGHT`
  66 + - `IMAGE_MAX_INFLIGHT`
  67 +- 图片服务可以配置得比文本更严格。
  68 +- 请求若是 full-cache-hit,会在服务端直接返回,不占用模型并发槽位。
  69 +- 超过处理能力时直接拒绝,比无限排队更稳定。
  70 +
40 ### 图片向量:clip-as-service(推荐) 71 ### 图片向量:clip-as-service(推荐)
41 72
42 默认使用 `third-party/clip-as-service` 的 Jina CLIP 服务生成图片向量。 73 默认使用 `third-party/clip-as-service` 的 Jina CLIP 服务生成图片向量。
@@ -63,7 +94,7 @@ @@ -63,7 +94,7 @@
63 94
64 ### 启动服务 95 ### 启动服务
65 96
66 -使用仓库脚本启动(默认端口 6005) 97 +使用仓库脚本启动
67 98
68 ```bash 99 ```bash
69 # GPU(需 nvidia-container-toolkit) 100 # GPU(需 nvidia-container-toolkit)
@@ -72,16 +103,27 @@ TEI_DEVICE=cuda ./scripts/start_tei_service.sh @@ -72,16 +103,27 @@ TEI_DEVICE=cuda ./scripts/start_tei_service.sh
72 # CPU 103 # CPU
73 TEI_DEVICE=cpu ./scripts/start_tei_service.sh 104 TEI_DEVICE=cpu ./scripts/start_tei_service.sh
74 105
75 -./scripts/start_embedding_service.sh 106 +./scripts/start_embedding_text_service.sh
  107 +./scripts/start_embedding_image_service.sh
  108 +
  109 +# 或兼容入口
  110 +./scripts/start_embedding_service.sh text
  111 +./scripts/start_embedding_service.sh image
76 ``` 112 ```
77 113
78 ### 修改配置 114 ### 修改配置
79 115
80 编辑 `embeddings/config.py`: 116 编辑 `embeddings/config.py`:
81 117
82 -- `PORT`: 服务端口(默认 6005) 118 +- `PORT`: combined 模式默认端口(默认 6005)
83 - `TEXT_MODEL_ID`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE`, `TEXT_NORMALIZE_EMBEDDINGS` 119 - `TEXT_MODEL_ID`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE`, `TEXT_NORMALIZE_EMBEDDINGS`
84 - `IMAGE_NORMALIZE_EMBEDDINGS`(默认 true) 120 - `IMAGE_NORMALIZE_EMBEDDINGS`(默认 true)
85 - `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`, `CLIP_AS_SERVICE_MODEL_NAME`:图片向量(clip-as-service) 121 - `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`, `CLIP_AS_SERVICE_MODEL_NAME`:图片向量(clip-as-service)
86 - `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`:本地 CN-CLIP(当 `USE_CLIP_AS_SERVICE=false` 时) 122 - `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`:本地 CN-CLIP(当 `USE_CLIP_AS_SERVICE=false` 时)
87 - TEI 相关:`TEI_DEVICE`、`TEI_VERSION`、`TEI_MAX_BATCH_TOKENS`、`TEI_MAX_CLIENT_BATCH_SIZE`、`TEI_HEALTH_TIMEOUT_SEC` 123 - TEI 相关:`TEI_DEVICE`、`TEI_VERSION`、`TEI_MAX_BATCH_TOKENS`、`TEI_MAX_CLIENT_BATCH_SIZE`、`TEI_HEALTH_TIMEOUT_SEC`
  124 +- 分流/限流相关:
  125 + - `EMBEDDING_SERVICE_KIND=all|text|image`
  126 + - `EMBEDDING_TEXT_PORT`
  127 + - `EMBEDDING_IMAGE_PORT`
  128 + - `TEXT_MAX_INFLIGHT`
  129 + - `IMAGE_MAX_INFLIGHT`
embeddings/cache_keys.py
1 -"""Shared cache key helpers for embedding inputs.""" 1 +"""Shared cache key helpers for embedding inputs.
  2 +
  3 +Current canonical raw-key format:
  4 +- text: ``embed:norm1:<text>`` / ``embed:norm0:<text>``
  5 +- image: ``embed:norm1:<url>`` / ``embed:norm0:<url>``
  6 +
  7 +`RedisEmbeddingCache` adds the configured key prefix and optional namespace on top.
  8 +"""
2 9
3 from __future__ import annotations 10 from __future__ import annotations
4 11
5 12
6 def build_text_cache_key(text: str, *, normalize: bool) -> str: 13 def build_text_cache_key(text: str, *, normalize: bool) -> str:
7 normalized_text = str(text or "").strip() 14 normalized_text = str(text or "").strip()
8 - return f"norm:{1 if normalize else 0}:text:{normalized_text}" 15 + return f"embed:norm{1 if normalize else 0}:{normalized_text}"
9 16
10 17
11 def build_image_cache_key(url: str, *, normalize: bool) -> str: 18 def build_image_cache_key(url: str, *, normalize: bool) -> str:
12 normalized_url = str(url or "").strip() 19 normalized_url = str(url or "").strip()
13 - return f"norm:{1 if normalize else 0}:image:{normalized_url}" 20 + return f"embed:norm{1 if normalize else 0}:{normalized_url}"
embeddings/image_encoder.py
@@ -127,10 +127,10 @@ class CLIPImageEncoder: @@ -127,10 +127,10 @@ class CLIPImageEncoder:
127 cached = self.cache.get(cache_key) 127 cached = self.cache.get(cache_key)
128 if cached is not None: 128 if cached is not None:
129 results.append(cached) 129 results.append(cached)
130 - else:  
131 - results.append(np.array([], dtype=np.float32)) # placeholder  
132 - pending_positions.append(pos)  
133 - pending_urls.append(url) 130 + continue
  131 + results.append(np.array([], dtype=np.float32)) # placeholder
  132 + pending_positions.append(pos)
  133 + pending_urls.append(url)
134 134
135 for i in range(0, len(pending_urls), batch_size): 135 for i in range(0, len(pending_urls), batch_size):
136 batch_urls = pending_urls[i : i + batch_size] 136 batch_urls = pending_urls[i : i + batch_size]
embeddings/text_encoder.py
@@ -164,12 +164,14 @@ class TextEmbeddingEncoder: @@ -164,12 +164,14 @@ class TextEmbeddingEncoder:
164 normalize_embeddings: bool, 164 normalize_embeddings: bool,
165 ) -> Optional[np.ndarray]: 165 ) -> Optional[np.ndarray]:
166 """Get embedding from cache if exists (with sliding expiration).""" 166 """Get embedding from cache if exists (with sliding expiration)."""
167 - embedding = self.cache.get(build_text_cache_key(query, normalize=normalize_embeddings)) 167 + cache_key = build_text_cache_key(query, normalize=normalize_embeddings)
  168 + embedding = self.cache.get(cache_key)
168 if embedding is not None: 169 if embedding is not None:
169 logger.debug( 170 logger.debug(
170 - "Cache hit for text embedding | normalize=%s query=%s", 171 + "Cache hit for text embedding | normalize=%s query=%s key=%s",
171 normalize_embeddings, 172 normalize_embeddings,
172 query, 173 query,
  174 + cache_key,
173 ) 175 )
174 return embedding 176 return embedding
175 177
scripts/trace_indexer_calls.sh
@@ -14,7 +14,8 @@ echo &quot;索引服务调用方排查&quot; @@ -14,7 +14,8 @@ echo &quot;索引服务调用方排查&quot;
14 echo "==========================================" 14 echo "=========================================="
15 15
16 INDEXER_PORT="${INDEXER_PORT:-6004}" 16 INDEXER_PORT="${INDEXER_PORT:-6004}"
17 -EMBEDDING_PORT="${EMBEDDING_PORT:-6005}" 17 +EMBEDDING_TEXT_PORT="${EMBEDDING_TEXT_PORT:-${EMBEDDING_PORT:-6005}}"
  18 +EMBEDDING_IMAGE_PORT="${EMBEDDING_IMAGE_PORT:-6008}"
18 19
19 echo "" 20 echo ""
20 echo "1. 监听端口 6004 的进程(Indexer 服务)" 21 echo "1. 监听端口 6004 的进程(Indexer 服务)"
@@ -37,10 +38,10 @@ else @@ -37,10 +38,10 @@ else
37 fi 38 fi
38 39
39 echo "" 40 echo ""
40 -echo "3. 连接到 6005 的客户端(Indexer 会调用 Embedding 服务)" 41 +echo "3. 连接到 Embedding 服务的客户端"
41 echo "------------------------------------------" 42 echo "------------------------------------------"
42 if command -v ss >/dev/null 2>&1; then 43 if command -v ss >/dev/null 2>&1; then
43 - ss -tnp 2>/dev/null | grep ":${EMBEDDING_PORT}" || echo " (当前无活跃连接)" 44 + ss -tnp 2>/dev/null | grep -E ":${EMBEDDING_TEXT_PORT}|:${EMBEDDING_IMAGE_PORT}" || echo " (当前无活跃连接)"
44 fi 45 fi
45 46
46 echo "" 47 echo ""
@@ -63,12 +64,13 @@ echo &quot; 全量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/reindex @@ -63,12 +64,13 @@ echo &quot; 全量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/reindex
63 echo " 增量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/index -d '{\"tenant_id\":\"170\",\"spu_ids\":[\"123\"]}'" 64 echo " 增量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/index -d '{\"tenant_id\":\"170\",\"spu_ids\":[\"123\"]}'"
64 echo "" 65 echo ""
65 echo " - Indexer 内部会调用:" 66 echo " - Indexer 内部会调用:"
66 -echo " - Embedding 服务 (${EMBEDDING_PORT}): POST /embed/text" 67 +echo " - Text Embedding 服务 (${EMBEDDING_TEXT_PORT}): POST /embed/text"
  68 +echo " - Image Embedding 服务 (${EMBEDDING_IMAGE_PORT}): POST /embed/image"
67 echo " - Qwen API: dashscope.aliyuncs.com (翻译、LLM 分析)" 69 echo " - Qwen API: dashscope.aliyuncs.com (翻译、LLM 分析)"
68 echo " - MySQL: 商品数据" 70 echo " - MySQL: 商品数据"
69 echo " - Elasticsearch: 写入索引" 71 echo " - Elasticsearch: 写入索引"
70 echo "" 72 echo ""
71 echo "6. 实时监控连接(按 Ctrl+C 停止)" 73 echo "6. 实时监控连接(按 Ctrl+C 停止)"
72 echo "------------------------------------------" 74 echo "------------------------------------------"
73 -echo " 运行: watch -n 2 'ss -tnp | grep -E \":${INDEXER_PORT}|:${EMBEDDING_PORT}\"'" 75 +echo " 运行: watch -n 2 'ss -tnp | grep -E \":${INDEXER_PORT}|:${EMBEDDING_TEXT_PORT}|:${EMBEDDING_IMAGE_PORT}\"'"
74 echo "" 76 echo ""
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 2
3 from .db_connector import create_db_connection, get_connection_from_config, test_connection 3 from .db_connector import create_db_connection, get_connection_from_config, test_connection
4 from .es_client import ESClient, get_es_client_from_env 4 from .es_client import ESClient, get_es_client_from_env
5 -from .cache import EmbeddingCache, DictCache 5 +from .cache import DictCache
6 6
7 __all__ = [ 7 __all__ = [
8 'create_db_connection', 8 'create_db_connection',
@@ -10,6 +10,5 @@ __all__ = [ @@ -10,6 +10,5 @@ __all__ = [
10 'test_connection', 10 'test_connection',
11 'ESClient', 11 'ESClient',
12 'get_es_client_from_env', 12 'get_es_client_from_env',
13 - 'EmbeddingCache',  
14 'DictCache', 13 'DictCache',
15 ] 14 ]
1 -"""  
2 -Cache utility for storing embedding results.  
3 -""" 1 +"""Small file-backed cache helpers."""
4 2
5 import json 3 import json
6 -import hashlib  
7 -import pickle  
8 from pathlib import Path 4 from pathlib import Path
9 from typing import Any, Optional 5 from typing import Any, Optional
10 -import numpy as np  
11 -  
12 -  
13 -class EmbeddingCache:  
14 - """  
15 - Simple file-based cache for embeddings.  
16 -  
17 - Uses MD5 hash of input text/URL as cache key.  
18 - """  
19 -  
20 - def __init__(self, cache_dir: str = ".cache/embeddings"):  
21 - self.cache_dir = Path(cache_dir)  
22 - self.cache_dir.mkdir(parents=True, exist_ok=True)  
23 -  
24 - def _get_cache_key(self, input_str: str) -> str:  
25 - """Generate cache key from input string."""  
26 - return hashlib.md5(input_str.encode('utf-8')).hexdigest()  
27 -  
28 - def get(self, input_str: str) -> Optional[np.ndarray]:  
29 - """  
30 - Get cached embedding.  
31 -  
32 - Args:  
33 - input_str: Input text or URL  
34 -  
35 - Returns:  
36 - Cached embedding or None if not found  
37 - """  
38 - cache_key = self._get_cache_key(input_str)  
39 - cache_file = self.cache_dir / f"{cache_key}.npy"  
40 -  
41 - if cache_file.exists():  
42 - try:  
43 - return np.load(cache_file)  
44 - except Exception as e:  
45 - print(f"Failed to load cache for {input_str}: {e}")  
46 - return None  
47 - return None  
48 -  
49 - def set(self, input_str: str, embedding: np.ndarray) -> bool:  
50 - """  
51 - Store embedding in cache.  
52 -  
53 - Args:  
54 - input_str: Input text or URL  
55 - embedding: Embedding vector  
56 -  
57 - Returns:  
58 - True if successful  
59 - """  
60 - cache_key = self._get_cache_key(input_str)  
61 - cache_file = self.cache_dir / f"{cache_key}.npy"  
62 -  
63 - try:  
64 - np.save(cache_file, embedding)  
65 - return True  
66 - except Exception as e:  
67 - print(f"Failed to cache embedding for {input_str}: {e}")  
68 - return False  
69 -  
70 - def exists(self, input_str: str) -> bool:  
71 - """Check if embedding is cached."""  
72 - cache_key = self._get_cache_key(input_str)  
73 - cache_file = self.cache_dir / f"{cache_key}.npy"  
74 - return cache_file.exists()  
75 -  
76 - def clear(self) -> int:  
77 - """  
78 - Clear all cached embeddings.  
79 -  
80 - Returns:  
81 - Number of files deleted  
82 - """  
83 - count = 0  
84 - for cache_file in self.cache_dir.glob("*.npy"):  
85 - cache_file.unlink()  
86 - count += 1  
87 - return count  
88 -  
89 - def size(self) -> int:  
90 - """Get number of cached embeddings."""  
91 - return len(list(self.cache_dir.glob("*.npy")))  
92 6
93 7
94 class DictCache: 8 class DictCache: