# Embeddings 模块

**请求示例**见 `docs/QUICKSTART.md` §3.3。
**专项文档**：
- `../docs/TEI_SERVICE说明文档.md`
- `../docs/CNCLIP_SERVICE说明文档.md`

**请求日志串联（reqid / uid）**：统一实现在仓库根目录的 `request_log_context.py`（勿放到 `utils/` 下，以免 `.venv-embedding` 因 `utils/__init__.py` 拉取数据库依赖）。Uvicorn 日志配置见 `config/uvicorn_embedding_logging.json`。

---

这个目录是一个完整的“向量化模块”，包含：

- **HTTP 客户端**：`text_encoder.py` / `image_encoder.py`（供搜索/索引模块调用）
- **本地模型实现**：`text_embedding_sentence_transformers.py` / `clip_model.py`
- **clip-as-service 客户端**：`clip_as_service_encoder.py`（图片向量，推荐）
- **向量化服务（FastAPI）**：`server.py`
- **统一配置**：`config.py`
- **接口契约**：`protocols.ImageEncoderProtocol`（`encode_image_urls` + `encode_clip_texts`；本地 CN-CLIP 与 clip-as-service 均实现）

说明：历史上的云端 embedding 试验实现（DashScope）已从主仓库移除。当前默认部署为**文本服务 6005** 与**图片服务 6008** 两条独立链路；`all` 模式仅作为单进程调试入口。

### 文本向量后端（默认）

- 6005 文本向量服务默认后端：`TEI`（Text Embeddings Inference）
- 默认模型：`Qwen/Qwen3-Embedding-0.6B`
- 后端配置来源：`config/config.yaml -> services.embedding.backend/backends`
- 环境变量覆盖：`EMBEDDING_BACKEND`、`TEI_BASE_URL`、`TEI_TIMEOUT_SEC`

### 服务接口

- 文本服务（默认 `6005`）
  - `POST /embed/text`
  - 请求体：`["文本1", "文本2", ...]`
  - 可选 query 参数：`normalize=true|false`、`priority=0|1`
  - 返回：`[[...], [...], ...]`
  - 健康接口：`GET /health`、`GET /ready`
- 图片服务（默认 `6008`）
  - `POST /embed/image`：图片 URL 或本地路径
  - `POST /embed/clip_text`：自然语言（CN-CLIP 文本塔，与 `/embed/image` 同向量空间；勿传 `http://` 图 URL）
  - 请求体：`["...", ...]` 字符串数组
  - 可选 query 参数：`normalize=true|false`、`priority=0|1`
  - 返回：`[[...], [...], ...]`
  - 健康接口：`GET /health`、`GET /ready`

### Redis 向量缓存

- Value 格式没有变化，仍然是 **BF16 bytes**：
  - 写入：`float32 -> BF16 -> bytes`
  - 读取：`bytes -> BF16 -> float32`
- 现在是**双层缓存**：
  - client 侧：`text_encoder.py` / `image_encoder.py`
  - service 侧：`server.py`
- 当前主 key 格式（`model_name` 见 `CONFIG.MULTIMODAL_MODEL_NAME`，与 `services.embedding.image_backends` 一致）：
  - 文本（TEI）：`embedding:embed:norm{0|1}:{text_or_sha256_digest}`
  - CN-CLIP 图片：`embedding:image:embed:{model_name}:txt:norm{0|1}:{url_or_sha256_digest}`
  - CN-CLIP 文本塔：`embedding:clip_text:embed:{model_name}:img:norm{0|1}:{text_or_sha256_digest}`
- 尾部负载：长度 ≤ `CACHE_KEY_RAW_BODY_MAX_CHARS`（默认 256，见 `embeddings/cache_keys.py`）用原文；更长用 `h:sha256:<hex>`（TEI 与多模态共用同一辅助函数）。
- 切换多模态模型会自然换 key 空间；旧键需自行清理或等待过期。

### 压力隔离与拒绝策略

- 文本与图片各自有独立 admission control：
  - `TEXT_MAX_INFLIGHT`
  - `IMAGE_MAX_INFLIGHT`
- 图片服务可以配置得比文本更严格。
- 请求若是 full-cache-hit，会在服务端直接返回，不占用模型并发槽位。
- 超过处理能力时直接拒绝，比无限排队更稳定。
- 文本服务支持 `priority`：
  - `priority=0`（默认，适合离线索引）仍受 `TEXT_MAX_INFLIGHT` 限制，超限直接返回 overload。
  - `priority>0`（建议在线 query 用 `1`）不会因 admission control 被拒绝，但仍会计入 inflight。
  - 文本服务内部使用双队列调度，处理时会优先消费高优先级请求，避免在线请求长期排在离线批量任务后面。
- 图片服务同样支持 `priority`（语义与文本一致，按 `IMAGE_MAX_INFLIGHT` 计数；不做队列插队，仅 admission 规则不同）。

### 图片向量：clip-as-service（推荐）

默认使用 `third-party/clip-as-service` 的 Jina CLIP 服务生成图片向量。

1. **安装 embedding 专用环境**（首次使用）：
   ```bash
   ./scripts/setup_embedding_venv.sh
   ```
   如需使用本地 `local_st` 文本后端，再执行：
   ```bash
   INSTALL_LOCAL_ST=1 ./scripts/setup_embedding_venv.sh
   ```

2. **启动 CN-CLIP 服务**（独立 gRPC 服务，默认端口 51000，详见 `../docs/CNCLIP_SERVICE说明文档.md`）：
   ```bash
   ./scripts/start_cnclip_service.sh
   ```

3. **配置**（`embeddings/config.py` 或环境变量）：
   - `USE_CLIP_AS_SERVICE=true`（默认）
   - `CLIP_AS_SERVICE_SERVER=grpc://127.0.0.1:51000`
   - `CLIP_AS_SERVICE_MODEL_NAME=CN-CLIP/ViT-H-14`（与 `config/config.yaml` 中 `services.embedding.image_backends` 一致；换 ViT-L 时为 768 维，须同步改 ES 映射）
   - `scripts/start_cnclip_service.sh` 默认会读取同一个 `CLIP_AS_SERVICE_MODEL_NAME`，也可用 `CNCLIP_MODEL_NAME` 或 `--model-name` 临时覆盖

### 性能与压测（沿用仓库脚本）

- 接口级压测（与 `perf_reports/2026-03-12/matrix_report/` 等方法一致）：`benchmarks/perf_api_benchmark.py`
  - 示例：`python benchmarks/perf_api_benchmark.py --scenario embed_text --duration 30 --concurrency 20`
  - 文本/图片向量可带 `priority`（与线上 admission 语义一致）：`--embed-text-priority 1`、`--embed-image-priority 1`
  - 自定义请求模板：`--cases-file benchmarks/perf_cases.json.example`
- 历史矩阵结果与说明见 `perf_reports/2026-03-12/matrix_report/summary.md`。

### 启动服务

使用仓库脚本启动：

```bash
# GPU（需 nvidia-container-toolkit）
TEI_DEVICE=cuda ./scripts/start_tei_service.sh

# CPU
TEI_DEVICE=cpu ./scripts/start_tei_service.sh

./scripts/start_embedding_text_service.sh
./scripts/start_embedding_image_service.sh
```

### 修改配置

编辑 `embeddings/config.py`：

- `PORT`: `all` 模式单进程端口（默认 6005）
- `TEXT_MODEL_ID`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE`, `TEXT_NORMALIZE_EMBEDDINGS`
- `IMAGE_NORMALIZE_EMBEDDINGS`（默认 true）
- `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`, `CLIP_AS_SERVICE_MODEL_NAME`：图片向量（clip-as-service）
- `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`：本地 CN-CLIP（当 `USE_CLIP_AS_SERVICE=false` 时）
- TEI 相关：`TEI_DEVICE`、`TEI_VERSION`、`TEI_MAX_BATCH_TOKENS`、`TEI_MAX_CLIENT_BATCH_SIZE`、`TEI_HEALTH_TIMEOUT_SEC`
- 分流/限流相关：
  - `EMBEDDING_SERVICE_KIND=all|text|image`
  - `EMBEDDING_TEXT_PORT`
  - `EMBEDDING_IMAGE_PORT`
  - `TEXT_MAX_INFLIGHT`
  - `IMAGE_MAX_INFLIGHT`
