Commit 5bac964944e8cd54c86dbb9a8713476fba74c50c
1 parent
7214c2e7
文本 embedding 与图片 embedding 已拆分为两个独立进程 / 端口
Showing
16 changed files
with
229 additions
and
150 deletions
Show diff stats
docs/CNCLIP_SERVICE说明文档.md
| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | ## 1. 作用与边界 |
| 6 | 6 | |
| 7 | 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 | 9 | - `cnclip` 不负责文本向量;文本向量由 TEI(8080)负责。 |
| 10 | 10 | |
| 11 | 11 | ## 2. 代码与脚本入口 |
| ... | ... | @@ -90,7 +90,7 @@ CNCLIP_MODEL_NAME=CN-CLIP/ViT-H-14 ./scripts/service_ctl.sh start cnclip |
| 90 | 90 | |
| 91 | 91 | ```bash |
| 92 | 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 | 96 | ### 6.2 设备选择优先级 |
| ... | ... | @@ -162,9 +162,9 @@ GPU 模式下应出现 `clip_server` 相关 `python` 进程及显存占用。 |
| 162 | 162 | ### 7.5 与 embedding 服务联调 |
| 163 | 163 | |
| 164 | 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 | 168 | -H "Content-Type: application/json" \ |
| 169 | 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 | 92 | | backend | 6002 | 搜索 API(含 admin) | ✓ | |
| 93 | 93 | | indexer | 6004 | 索引 API(reindex/build-docs 等) | ✓ | |
| 94 | 94 | | frontend | 6003 | 调试 UI | ✓ | |
| 95 | -| embedding | 6005 | 向量服务(文本/图片) | 可选 | | |
| 95 | +| embedding | 6005 | 文本向量服务 | 可选 | | |
| 96 | +| embedding-image | 6008 | 图片向量服务 | 可选 | | |
| 96 | 97 | | translator | 6006 | 翻译服务(`POST /translate` 支持单条或批量 list;批量失败用 `null` 占位) | 可选 | |
| 97 | 98 | | reranker | 6007 | 重排服务 | 可选 | |
| 98 | 99 | |
| ... | ... | @@ -156,8 +157,8 @@ docs/ # 文档(含本指南) |
| 156 | 157 | |
| 157 | 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 | 162 | - **详见**:`embeddings/README.md`、本指南 §7.5–§7.6。 |
| 162 | 163 | |
| 163 | 164 | ### 4.7 reranker | ... | ... |
docs/QUICKSTART.md
| ... | ... | @@ -57,7 +57,8 @@ source activate.sh |
| 57 | 57 | | backend | 6002 | ✓ | 搜索 API(`/search/*`)+ 管理接口(`/admin/*`) | |
| 58 | 58 | | indexer | 6004 | ✓ | 索引 API(`/indexer/*`) | |
| 59 | 59 | | frontend | 6003 | ✓ | 调试 UI | |
| 60 | -| embedding | 6005 | - | 向量服务(`/embed/text`, `/embed/image`) | | |
| 60 | +| embedding | 6005 | - | 文本向量服务(`/embed/text`) | | |
| 61 | +| embedding-image | 6008 | - | 图片向量服务(`/embed/image`) | | |
| 61 | 62 | | translator | 6006 | - | 翻译服务(`/translate`) | |
| 62 | 63 | | reranker | 6007 | - | 重排服务(`/rerank`) | |
| 63 | 64 | |
| ... | ... | @@ -135,21 +136,22 @@ curl -X POST http://localhost:6004/indexer/build-docs \ |
| 135 | 136 | |
| 136 | 137 | API 文档:`http://localhost:6004/docs` |
| 137 | 138 | |
| 138 | -#### Embedding 服务(6005) | |
| 139 | +#### Embedding 服务(文本 6005 / 图片 6008) | |
| 139 | 140 | |
| 140 | 141 | ```bash |
| 141 | 142 | # TEI(文本向量后端,默认) |
| 142 | 143 | # GPU(需 nvidia-container-toolkit) |
| 143 | 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 | 150 | curl -X POST http://localhost:6005/embed/text \ |
| 149 | 151 | -H "Content-Type: application/json" \ |
| 150 | 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 | 155 | -H "Content-Type: application/json" \ |
| 154 | 156 | -d '["https://example.com/img.jpg"]' |
| 155 | 157 | ``` |
| ... | ... | @@ -157,7 +159,7 @@ curl -X POST http://localhost:6005/embed/image \ |
| 157 | 159 | 说明: |
| 158 | 160 | - TEI 默认镜像按 `TEI_VERSION` 组装:`cuda-<version>`(默认 `1.9`)。 |
| 159 | 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 | 164 | #### Translator 服务(6006) |
| 163 | 165 | |
| ... | ... | @@ -530,6 +532,7 @@ curl http://localhost:6002/health |
| 530 | 532 | curl http://localhost:6002/admin/health |
| 531 | 533 | curl http://localhost:6004/health |
| 532 | 534 | curl http://localhost:6005/health |
| 535 | +curl http://localhost:6008/health | |
| 533 | 536 | curl http://localhost:6006/health |
| 534 | 537 | curl http://localhost:6007/health |
| 535 | 538 | ``` | ... | ... |
docs/TEI_SERVICE说明文档.md
| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | ## 1. 作用与边界 |
| 6 | 6 | |
| 7 | 7 | - TEI 提供文本向量 HTTP 服务(默认 `http://127.0.0.1:8080`)。 |
| 8 | -- 本项目中 `embedding` 服务(6005)默认把文本向量请求转发到 TEI。 | |
| 8 | +- 本项目中文本 embedding 服务(默认 `6005`)把文本向量请求转发到 TEI。 | |
| 9 | 9 | - TEI 仅负责文本向量,不负责图片向量(图片向量由 `cnclip` 提供)。 |
| 10 | 10 | |
| 11 | 11 | ## 2. 代码与脚本入口 |
| ... | ... | @@ -13,7 +13,7 @@ |
| 13 | 13 | - 启动脚本:`scripts/start_tei_service.sh` |
| 14 | 14 | - 停止脚本:`scripts/stop_tei_service.sh` |
| 15 | 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 | 18 | ## 3. 前置条件 |
| 19 | 19 | |
| ... | ... | @@ -117,7 +117,7 @@ curl -sS http://127.0.0.1:8080/embed \ |
| 117 | 117 | ### 6.3 与 embedding 服务联调 |
| 118 | 118 | |
| 119 | 119 | ```bash |
| 120 | -./scripts/start_embedding_service.sh | |
| 120 | +./scripts/start_embedding_text_service.sh | |
| 121 | 121 | |
| 122 | 122 | curl -sS -X POST "http://127.0.0.1:6005/embed/text" \ |
| 123 | 123 | -H "Content-Type: application/json" \ |
| ... | ... | @@ -152,7 +152,7 @@ curl -sS -X POST "http://127.0.0.1:6005/embed/text" \ |
| 152 | 152 | 启动全套(含 TEI): |
| 153 | 153 | |
| 154 | 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 | 158 | 仅启动 TEI: | ... | ... |
docs/Usage-Guide.md
| ... | ... | @@ -335,7 +335,8 @@ python -m http.server 6003 |
| 335 | 335 | | Backend API | 6002 | http://localhost:6002 | |
| 336 | 336 | | Indexer API | 6004 | http://localhost:6004 | |
| 337 | 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 | 340 | | TEI (optional) | 8080 | http://localhost:8080 | |
| 340 | 341 | | Translation (optional) | 6006 | http://localhost:6006 | |
| 341 | 342 | | Reranker (optional) | 6007 | http://localhost:6007 | |
| ... | ... | @@ -380,7 +381,8 @@ INDEXER_PORT=6004 |
| 380 | 381 | |
| 381 | 382 | # Optional service ports |
| 382 | 383 | FRONTEND_PORT=6003 |
| 383 | -EMBEDDING_PORT=6005 | |
| 384 | +EMBEDDING_TEXT_PORT=6005 | |
| 385 | +EMBEDDING_IMAGE_PORT=6008 | |
| 384 | 386 | TEI_PORT=8080 |
| 385 | 387 | CNCLIP_PORT=51000 |
| 386 | 388 | TRANSLATION_PORT=6006 | ... | ... |
docs/向量化模块和API说明文档.md
| ... | ... | @@ -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 | 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 | 37 | - 文本模型:`embeddings/config.py` 的 `TEXT_MODEL_ID`(默认 `Qwen/Qwen3-Embedding-0.6B`) |
| 14 | 38 | - 运行参数:`TEXT_DEVICE`、`TEXT_BATCH_SIZE`、`TEXT_NORMALIZE_EMBEDDINGS` |
| 15 | 39 | ... | ... |
docs/工作总结-微服务性能优化与架构.md
| ... | ... | @@ -16,12 +16,12 @@ |
| 16 | 16 | | **vLLM** | 高吞吐推理框架,更适合生成式与重排混合场景 | 纯 embedding 场景通常不作为首选 | |
| 17 | 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 | 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 | 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 | 27 | - **独立服务**:TEI 以 Docker 容器运行,与主程序 `.venv` 解耦;embedding 使用 `.venv-embedding`,便于独立扩缩容与升级。 |
| ... | ... | @@ -71,7 +71,7 @@ instruction: "Given a shopping query, rank product titles by relevance" |
| 71 | 71 | |
| 72 | 72 | **具体内容**: |
| 73 | 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 | 75 | - **环境与启停**:CN-CLIP 使用独立虚拟环境 `.venv-cnclip`;启动 `scripts/start_cnclip_service.sh`,或 `./scripts/service_ctl.sh start cnclip`;设备可通过 `CNCLIP_DEVICE=cuda`(默认)或 `cpu` 指定。 |
| 76 | 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: "Given a shopping query, rank product titles by relevance" |
| 127 | 127 | - **脚本**:`scripts/service_ctl.sh` 统一负责各服务的生命周期与监控;依赖 `scripts/lib/load_env.sh` 与项目根目录 `.env`。 |
| 128 | 128 | - **服务与端口**: |
| 129 | 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 | 133 | - `./scripts/service_ctl.sh start [service...]` 或 `start all`(all = tei cnclip embedding translator reranker backend indexer frontend,按依赖顺序);`stop`、`restart` 同参数;`status` 默认列出所有服务。 |
| 134 | 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 "http://43.166.252.75:6002/search/" \ |
| 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 | 162 | | 翻译服务 | 6006 | `POST /translate` | 文本翻译(支持 qwen-mt / llm / deepl / 本地模型) | |
| 163 | 163 | | 重排服务 | 6007 | `POST /rerank` | 检索结果重排 | |
| 164 | 164 | | 内容理解(Indexer 内) | 6004 | `POST /indexer/enrich-content` | 根据商品标题生成 qanchors、tags 等,供 indexer 微服务组合方式使用 | |
| ... | ... | @@ -1691,7 +1691,8 @@ curl -X POST "http://localhost:6004/indexer/enrich-content" \ |
| 1691 | 1691 | |
| 1692 | 1692 | | 服务 | 默认端口 | Base URL | 说明 | |
| 1693 | 1693 | |------|----------|----------|------| |
| 1694 | -| 向量服务 | 6005 | `http://localhost:6005` | 文本/图片向量化,用于语义搜索与以图搜图 | | |
| 1694 | +| 向量服务(文本) | 6005 | `http://localhost:6005` | 文本向量化,用于 query/doc 语义检索 | | |
| 1695 | +| 向量服务(图片) | 6008 | `http://localhost:6008` | 图片向量化,用于以图搜图 | | |
| 1695 | 1696 | | 翻译服务 | 6006 | `http://localhost:6006` | 多语言翻译(云端与本地模型统一入口) | |
| 1696 | 1697 | | 重排服务 | 6007 | `http://localhost:6007` | 对检索结果进行二次排序 | |
| 1697 | 1698 | |
| ... | ... | @@ -1700,13 +1701,28 @@ curl -X POST "http://localhost:6004/indexer/enrich-content" \ |
| 1700 | 1701 | |
| 1701 | 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 | 1712 | - 文本向量后端默认走 TEI(`http://127.0.0.1:8080`) |
| 1707 | 1713 | - 图片向量依赖 `cnclip`(`grpc://127.0.0.1:51000`) |
| 1708 | 1714 | - TEI 默认使用 GPU(`TEI_DEVICE=cuda`);当配置为 GPU 且不可用时会启动失败(不会自动降级到 CPU) |
| 1709 | 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 | 1727 | #### 7.1.1 `POST /embed/text` — 文本向量化 |
| 1712 | 1728 | |
| ... | ... | @@ -1724,7 +1740,7 @@ curl -X POST "http://localhost:6004/indexer/enrich-content" \ |
| 1724 | 1740 | |
| 1725 | 1741 | **完整 curl 示例**: |
| 1726 | 1742 | ```bash |
| 1727 | -curl -X POST "http://localhost:6005/embed/text" \ | |
| 1743 | +curl -X POST "http://localhost:6005/embed/text?normalize=true" \ | |
| 1728 | 1744 | -H "Content-Type: application/json" \ |
| 1729 | 1745 | -d '["芭比娃娃 儿童玩具", "纯棉T恤 短袖"]' |
| 1730 | 1746 | ``` |
| ... | ... | @@ -1733,7 +1749,7 @@ curl -X POST "http://localhost:6005/embed/text" \ |
| 1733 | 1749 | |
| 1734 | 1750 | 将图片 URL 或路径转为向量,用于以图搜图。 |
| 1735 | 1751 | |
| 1736 | -前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,`/embed/image` 会返回 500。 | |
| 1752 | +前置条件:`cnclip` 服务已启动(默认端口 `51000`)。若未启动,图片 embedding 服务启动会失败或请求返回错误。 | |
| 1737 | 1753 | |
| 1738 | 1754 | **请求体**(JSON 数组): |
| 1739 | 1755 | ```json |
| ... | ... | @@ -1747,7 +1763,7 @@ curl -X POST "http://localhost:6005/embed/text" \ |
| 1747 | 1763 | |
| 1748 | 1764 | **完整 curl 示例**: |
| 1749 | 1765 | ```bash |
| 1750 | -curl -X POST "http://localhost:6005/embed/image" \ | |
| 1766 | +curl -X POST "http://localhost:6008/embed/image?normalize=true" \ | |
| 1751 | 1767 | -H "Content-Type: application/json" \ |
| 1752 | 1768 | -d '["https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg"]' |
| 1753 | 1769 | ``` |
| ... | ... | @@ -1756,9 +1772,32 @@ curl -X POST "http://localhost:6005/embed/image" \ |
| 1756 | 1772 | |
| 1757 | 1773 | ```bash |
| 1758 | 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 | 1803 | - 在线 query 向量化(低延迟,常见 `batch=1~4`) |
| ... | ... | @@ -1773,7 +1812,8 @@ curl "http://localhost:6005/health" |
| 1773 | 1812 | |
| 1774 | 1813 | 默认端口: |
| 1775 | 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 | 1818 | 当前主 TEI 启动默认值(已按 T4/短文本场景调优): |
| 1779 | 1819 | - `TEI_MAX_BATCH_TOKENS=4096` | ... | ... |
docs/缓存与Redis使用说明.md
| ... | ... | @@ -19,7 +19,7 @@ |
| 19 | 19 | |
| 20 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 74 | ### 2.2 Value 与类型 |
| 49 | 75 | |
| ... | ... | @@ -68,6 +94,23 @@ |
| 68 | 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 | 116 | ## 3. 翻译结果缓存(translation/service.py) | ... | ... |
embeddings/README.md
| ... | ... | @@ -16,7 +16,7 @@ |
| 16 | 16 | - **统一配置**:`config.py` |
| 17 | 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 | 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 | 39 | - `POST /embed/text` |
| 31 | 40 | - 入参:`["文本1", "文本2", ...]` |
| 32 | 41 | - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认) |
| ... | ... | @@ -37,6 +46,28 @@ |
| 37 | 46 | - 可选 query 参数:`normalize=true|false`(不传则使用服务端默认) |
| 38 | 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 | 71 | ### 图片向量:clip-as-service(推荐) |
| 41 | 72 | |
| 42 | 73 | 默认使用 `third-party/clip-as-service` 的 Jina CLIP 服务生成图片向量。 |
| ... | ... | @@ -63,7 +94,7 @@ |
| 63 | 94 | |
| 64 | 95 | ### 启动服务 |
| 65 | 96 | |
| 66 | -使用仓库脚本启动(默认端口 6005): | |
| 97 | +使用仓库脚本启动: | |
| 67 | 98 | |
| 68 | 99 | ```bash |
| 69 | 100 | # GPU(需 nvidia-container-toolkit) |
| ... | ... | @@ -72,16 +103,27 @@ TEI_DEVICE=cuda ./scripts/start_tei_service.sh |
| 72 | 103 | # CPU |
| 73 | 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 | 116 | 编辑 `embeddings/config.py`: |
| 81 | 117 | |
| 82 | -- `PORT`: 服务端口(默认 6005) | |
| 118 | +- `PORT`: combined 模式默认端口(默认 6005) | |
| 83 | 119 | - `TEXT_MODEL_ID`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE`, `TEXT_NORMALIZE_EMBEDDINGS` |
| 84 | 120 | - `IMAGE_NORMALIZE_EMBEDDINGS`(默认 true) |
| 85 | 121 | - `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`, `CLIP_AS_SERVICE_MODEL_NAME`:图片向量(clip-as-service) |
| 86 | 122 | - `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`:本地 CN-CLIP(当 `USE_CLIP_AS_SERVICE=false` 时) |
| 87 | 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 | 10 | from __future__ import annotations |
| 4 | 11 | |
| 5 | 12 | |
| 6 | 13 | def build_text_cache_key(text: str, *, normalize: bool) -> str: |
| 7 | 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 | 18 | def build_image_cache_key(url: str, *, normalize: bool) -> str: |
| 12 | 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 | 127 | cached = self.cache.get(cache_key) |
| 128 | 128 | if cached is not None: |
| 129 | 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 | 135 | for i in range(0, len(pending_urls), batch_size): |
| 136 | 136 | batch_urls = pending_urls[i : i + batch_size] | ... | ... |
embeddings/text_encoder.py
| ... | ... | @@ -164,12 +164,14 @@ class TextEmbeddingEncoder: |
| 164 | 164 | normalize_embeddings: bool, |
| 165 | 165 | ) -> Optional[np.ndarray]: |
| 166 | 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 | 169 | if embedding is not None: |
| 169 | 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 | 172 | normalize_embeddings, |
| 172 | 173 | query, |
| 174 | + cache_key, | |
| 173 | 175 | ) |
| 174 | 176 | return embedding |
| 175 | 177 | ... | ... |
scripts/trace_indexer_calls.sh
| ... | ... | @@ -14,7 +14,8 @@ echo "索引服务调用方排查" |
| 14 | 14 | echo "==========================================" |
| 15 | 15 | |
| 16 | 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 | 20 | echo "" |
| 20 | 21 | echo "1. 监听端口 6004 的进程(Indexer 服务)" |
| ... | ... | @@ -37,10 +38,10 @@ else |
| 37 | 38 | fi |
| 38 | 39 | |
| 39 | 40 | echo "" |
| 40 | -echo "3. 连接到 6005 的客户端(Indexer 会调用 Embedding 服务)" | |
| 41 | +echo "3. 连接到 Embedding 服务的客户端" | |
| 41 | 42 | echo "------------------------------------------" |
| 42 | 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 | 45 | fi |
| 45 | 46 | |
| 46 | 47 | echo "" |
| ... | ... | @@ -63,12 +64,13 @@ echo " 全量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/reindex |
| 63 | 64 | echo " 增量: curl -X POST http://localhost:${INDEXER_PORT}/indexer/index -d '{\"tenant_id\":\"170\",\"spu_ids\":[\"123\"]}'" |
| 64 | 65 | echo "" |
| 65 | 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 | 69 | echo " - Qwen API: dashscope.aliyuncs.com (翻译、LLM 分析)" |
| 68 | 70 | echo " - MySQL: 商品数据" |
| 69 | 71 | echo " - Elasticsearch: 写入索引" |
| 70 | 72 | echo "" |
| 71 | 73 | echo "6. 实时监控连接(按 Ctrl+C 停止)" |
| 72 | 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 | 76 | echo "" | ... | ... |
utils/__init__.py
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | from .db_connector import create_db_connection, get_connection_from_config, test_connection |
| 4 | 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 | 7 | __all__ = [ |
| 8 | 8 | 'create_db_connection', |
| ... | ... | @@ -10,6 +10,5 @@ __all__ = [ |
| 10 | 10 | 'test_connection', |
| 11 | 11 | 'ESClient', |
| 12 | 12 | 'get_es_client_from_env', |
| 13 | - 'EmbeddingCache', | |
| 14 | 13 | 'DictCache', |
| 15 | 14 | ] | ... | ... |
utils/cache.py
| 1 | -""" | |
| 2 | -Cache utility for storing embedding results. | |
| 3 | -""" | |
| 1 | +"""Small file-backed cache helpers.""" | |
| 4 | 2 | |
| 5 | 3 | import json |
| 6 | -import hashlib | |
| 7 | -import pickle | |
| 8 | 4 | from pathlib import Path |
| 9 | 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 | 8 | class DictCache: | ... | ... |