## 缓存与 Redis 使用说明

本仓库中 Redis 主要用于**性能型缓存**，当前落地的业务缓存包括：

- **文本向量缓存**（embedding 缓存）
- **翻译结果缓存**（Qwen-MT 等机器翻译）
- **商品内容理解缓存**（锚文本 / 语义属性 / 标签）

底层连接配置统一来自 `config/env_config.py` 的 `REDIS_CONFIG`：

- **Host/Port**：`REDIS_HOST` / `REDIS_PORT`（默认 `localhost:6479`）
- **Password**：`REDIS_PASSWORD`
- **Socket & 超时**：`REDIS_SOCKET_TIMEOUT` / `REDIS_SOCKET_CONNECT_TIMEOUT` / `REDIS_RETRY_ON_TIMEOUT`
- **通用缓存 TTL**：`REDIS_CACHE_EXPIRE_DAYS`（默认 `360*2` 天，代码注释为 “6 months”）

---

## 1. 缓存总览表

| 模块 / 场景 | Key 模板 | Value 内容示例 | 过期策略 | 备注 |
|------------|----------|----------------|----------|------|
| 向量缓存（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"]` 控制 |
| 翻译结果缓存（translator service） | `trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)}` | 机翻后的单条字符串 | TTL=`services.translation.cache.ttl_seconds` 秒；可配置滑动过期 | 见 `translation/service.py` + `config/config.yaml` |
| 商品内容理解缓存（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` |

下面按模块详细说明。

---

## 2. 向量缓存（embeddings/text_encoder.py / embeddings/image_encoder.py / embeddings/server.py）

- **代码位置**：
  - 调用侧：`embeddings/text_encoder.py`、`embeddings/image_encoder.py`
  - 服务侧：`embeddings/server.py`
  - 公共 Redis/BF16 编解码：`embeddings/redis_embedding_cache.py`、`embeddings/bf16.py`
- **用途**：缓存文本/图片向量结果，避免重复推理、重复下载图片、重复占用模型并发槽位。

### 2.0 当前缓存链路总览

- 现在是**双层缓存**：
  - **调用侧缓存**：`TextEmbeddingEncoder` / `CLIPImageEncoder` 在发 HTTP 请求前先查 Redis；
  - **服务侧缓存**：`/embed/text` / `/embed/image` 在进入后端推理前再查 Redis。
- 这次改动里，**BF16 存储格式本身没有变化**：
  - 写入：`float32 -> BF16 -> bytes -> Redis`
  - 读取：`Redis bytes -> BF16 -> float32(np.float32)`
- 这次新增的核心变化是：
  - **缓存 key 加入 normalize 维度**，避免 `normalize=true` 与 `normalize=false` 命中同一条缓存；
  - **服务端也启用同一套 Redis 向量缓存**，而不是只有 client 侧缓存；
  - **拆分 text / image 服务** 后，图片压力不会再拖慢文本服务；
  - **服务侧 full-cache-hit 可直接返回**，不会再进入模型限流槽位。

### 2.1 Key 设计

- 统一 helper：`embeddings/cache_keys.py`
- 文本主 key（TEI/BGE）：`build_text_cache_key(text, normalize=...)`
- 多模态图片（CN-CLIP `/embed/image`）：`build_image_cache_key(url, normalize=..., model_name=...)`，其中 `model_name` 来自 `services.embedding.image_backends.*.model_name`（与 `embeddings.config.CONFIG.MULTIMODAL_MODEL_NAME` 一致）
- 多模态文本（CN-CLIP `/embed/clip_text`）：`build_clip_text_cache_key(text, normalize=..., model_name=...)`
- 模板：

```text
TEI 文本: {EMBEDDING_CACHE_PREFIX}:embed:norm{0|1}:{text}
CN-CLIP 图片: {EMBEDDING_CACHE_PREFIX}:image:embed:{model_name}:txt:norm{0|1}:{url_or_path}
CN-CLIP 文本塔: {EMBEDDING_CACHE_PREFIX}:clip_text:embed:{model_name}:img:norm{0|1}:{text}
```

- 字段说明：
  - `EMBEDDING_CACHE_PREFIX`：来自 `REDIS_CONFIG["embedding_cache_prefix"]`，默认值为 `"embedding"`，可通过环境变量 `REDIS_EMBEDDING_CACHE_PREFIX` 覆盖；
  - `model_name`：如 `CN-CLIP/ViT-H-14`；切换模型时自动使用新 key 空间，避免混用旧维度向量；
  - `norm1` / `norm0`：分别表示 `normalize=true` / `normalize=false`；
  - `text` / `url_or_path`：经 `strip` 后，若 Unicode 长度 **≤** ``CACHE_KEY_RAW_BODY_MAX_CHARS``（默认 256，见 ``embeddings/cache_keys.py``）则原样写入键尾；**更长**则改为 ``h:sha256:<64 hex>``（对 UTF-8 字节做 SHA-256）。TEI 文本与多模态共用 ``_stable_body_for_cache_key``。

补充说明：

- TEI 文本 raw key 仍为 `embed:norm{0|1}:...`（尾部负载规则同上）。
- 多模态键名中 `txt` / `img` 段为项目内约定（与 `embeddings/cache_keys.py` 一致），用于区分图片 lane 与 clip 文本 lane。

### 2.2 Value 与类型

- 类型：**BF16 bytes**（bfloat16），每一维用 2 字节无符号整数表示，按**大端**序列化。
- 写入流程：FP32 向量 → BF16 → bytes → Redis
- 读取流程：Redis bytes → BF16 → FP32（`np.float32`）向量
- 典型示例：BGE-M3 1024 维向量在 Redis value 大小约为 \(1024*2=2048\) bytes（不含 Redis 元数据开销）。

### 2.3 过期策略

- 初始化：
  - `self.expire_time = timedelta(days=REDIS_CONFIG.get("cache_expire_days", 180))`
  - `.env` 中可通过 `REDIS_CACHE_EXPIRE_DAYS` 配置，默认 `360*2`（代码注释标注为 6 个月）。
- 写入：
  - `redis.setex(cache_key, self.expire_time, serialized_data)`
- 访问（滑动过期）：
  - 命中缓存后，会调用 `redis.expire(cache_key, self.expire_time)` 延长 TTL。

### 2.4 特殊处理

- 若缓存中的向量 **为空 / shape 异常 / 含 NaN/Inf**，会：
  - 直接丢弃该缓存（并尝试 `delete` key）；
  - 回退为重新调用向量服务。

### 2.5 服务拆分与缓存/限流关系

- 当前默认部署形态：
  - 文本 embedding 服务：`6005`
  - 图片 embedding 服务：`6008`
- 调用方配置：
  - `EMBEDDING_TEXT_SERVICE_URL`
  - `EMBEDDING_IMAGE_SERVICE_URL`
- 服务端配置：
  - `TEXT_MAX_INFLIGHT`
  - `IMAGE_MAX_INFLIGHT`
  - `EMBEDDING_SERVICE_KIND=all|text|image`
- 关键行为：
  - **full-cache-hit** 请求在服务端会直接返回，不占用 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT`；
  - **cache miss** 才会进入对应模型 lane；
  - 因此高频重复图片请求不会再因为严格的图片限流而大量阻塞。

---

## 3. 翻译结果缓存（translation/service.py）

- **代码位置**：`translation/service.py`
- **用途**：统一缓存所有 translation capability 的翻译结果。
- **配置入口**：
  - `config/config.yaml -> services.translation.cache`
  - `config/config.yaml -> services.translation.capabilities.*.use_cache`

### 3.1 Key 设计

- 内部构造函数：`TranslationCache.build_key(...)`
- 模板：

```text
trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)}
```

其中：

- `model`：capability 名称，如 `qwen-mt`、`llm`、`opus-mt-zh-en`
- `target_lang`：目标语言，如 `en` / `zh`
- `source_text[:4]`：原文前 4 个字符
- `sha256(source_text)`：对完整原文做 SHA-256

### 3.2 Value 与类型

- 类型：**UTF-8 字符串**，即翻译后的文本结果。
- 存取逻辑：
  - 读取：`redis.get(key)` 返回 `str` 或 `None`；
  - 写入：`redis.setex(key, expire_seconds, translation)`。

### 3.3 过期策略

- 配置来源：`config/config.yaml -> services.translation.cache`，经 `get_translation_cache_config()` 解析：

```yaml
services:
  translation:
    cache:
      ttl_seconds: 62208000        # 默认约 720 天
      sliding_expiration: true
    capabilities:
      qwen-mt:
        use_cache: true
      llm:
        use_cache: true
      deepl:
        use_cache: true
      nllb-200-distilled-600m:
        use_cache: true
      opus-mt-zh-en:
        use_cache: true
      opus-mt-en-zh:
        use_cache: true
```

- 运行时行为：
  - translator service 启动时初始化共享 Redis cache；
  - **读缓存**：
    - 命中后，若 `sliding_expiration=True`，会调用 `redis.expire(key, expire_seconds)`；
  - **写缓存**：
    - 使用 `redis.setex(key, expire_seconds, translation)`。

### 3.4 关联模块

- `api/translator_app.py` 通过 `TranslationService` 统一复用同一套缓存逻辑；
- 所有翻译后端都通过 `TranslationService` 接入缓存。

---

## 4. 商品内容理解缓存（indexer/product_enrich.py）

- **代码位置**：`indexer/product_enrich.py`
- **用途**：在生成商品锚文本（qanchors）、语义属性、标签等内容理解结果时复用缓存，避免对同一标题重复调用大模型。

### 4.1 Key 设计

- 配置项：
  - `ANCHOR_CACHE_PREFIX = REDIS_CONFIG.get("anchor_cache_prefix", "product_anchors")`
  - `ANCHOR_CACHE_EXPIRE_DAYS = int(REDIS_CONFIG.get("anchor_cache_expire_days", 30))`
- Key 构造函数：`_make_analysis_cache_key(product, target_lang, analysis_kind)`
- 模板：

```text
{ANCHOR_CACHE_PREFIX}:{analysis_kind}:{prompt_contract_hash}:{target_lang}:{prompt_input_prefix}{md5(prompt_input)}
```

- 字段说明：
  - `ANCHOR_CACHE_PREFIX`：默认 `"product_anchors"`，可通过 `.env` 中的 `REDIS_ANCHOR_CACHE_PREFIX`（若存在）间接配置到 `REDIS_CONFIG`；
  - `analysis_kind`：分析族，目前至少包括 `content` 与 `taxonomy`，两者缓存隔离；
  - `prompt_contract_hash`：基于 system prompt、shared instruction、localized headers、result fields、user instruction template、schema cache version 等生成的短 hash；只要提示词或输出契约变化，缓存会自动失效；
  - `target_lang`：内容理解输出语言，例如 `zh`；
  - `prompt_input_prefix + md5(prompt_input)`：对真正送入 prompt 的商品文本做前缀 + MD5；当前 prompt 输入来自 `title`、`brief`、`description` 的规范化拼接结果。

设计原则：

- 只让**实际影响 LLM 输出**的输入参与 key；
- 不让 `tenant_id`、`spu_id` 这类“结果归属信息”污染缓存；
- prompt 或 schema 变更时，不依赖人工清理 Redis，也能自然切换到新 key。

### 4.2 Value 与类型

- 类型：`json.dumps(dict, ensure_ascii=False)`。
- 典型结构（简化）：

```json
{
  "id": "123",
  "lang": "zh",
  "title_input": "原始标题",
  "title": "归一化后的商品标题",
  "category_path": "...",
  "tags": "...",
  "target_audience": "...",
  "usage_scene": "...",
  "anchor_text": "..., ..."
}
```

- 读取时通过 `json.loads(raw)` 还原为 `Dict[str, Any]`。
- `content` 与 `taxonomy` 的 value 结构会随各自 schema 不同而不同，但都会先通过统一的 normalize 逻辑再写缓存。

### 4.3 过期策略

- TTL：`ttl = ANCHOR_CACHE_EXPIRE_DAYS * 24 * 3600` 秒（默认 30 天）；
- 写入：`redis.setex(key, ttl, json.dumps(result, ensure_ascii=False))`；
- 读取：仅做 `redis.get(key)`，**不做滑动过期**。

### 4.4 调用流程中的位置

- 单条调用（索引阶段常见）时，`analyze_products()` 会先尝试命中缓存：
  - 若命中，直接返回缓存结果；
  - 若 miss，调用 LLM，解析结果后再写入缓存。

---

## 5. Redis 运维脚本工具

`scripts/redis/` 下提供三个脚本，用于查看缓存数量、内存占用与健康状态。连接配置均来自 `config/env_config.py` 的 `REDIS_CONFIG`，运行前需在项目根目录执行（或保证 `PYTHONPATH` 含项目根），以便加载配置。

### 5.1 redis_cache_health_check.py（缓存健康巡检）

**功能**：按**业务缓存类型**（embedding / translation / anchors）做健康巡检，不扫全库。

- 对每类缓存：SCAN 匹配对应 key 前缀，统计**匹配 key 数量**（受 `--max-scan` 上限约束）；
- **TTL 分布**：对采样 key 统计 `no-expire-or-expired` / `0-1h` / `1h-1d` / `1d-30d` / `>30d`；
- **近期活跃 key**：从采样中选出 `OBJECT IDLETIME <= 600s` 的 key，用于判断是否有新写入；
- **样本 key 与 value 预览**：对 embedding 显示 ndarray 信息，对 translation 显示译文片段，对 anchors 显示 JSON 摘要。

**适用场景**：日常查看三类缓存是否在增长、TTL 是否合理、是否有近期写入；与「缓存总览表」中的 key 设计一一对应。

**用法示例**：

```bash
# 默认：检查 embedding / translation / anchors 三类
python scripts/redis/redis_cache_health_check.py

# 只检查某一类或两类
python scripts/redis/redis_cache_health_check.py --type embedding
python scripts/redis/redis_cache_health_check.py --type translation anchors

# 按自定义 pattern 检查（不按业务类型）
python scripts/redis/redis_cache_health_check.py --pattern "mycache:*"

# 调整采样与扫描规模
python scripts/redis/redis_cache_health_check.py --sample-size 100 --max-scan 50000 --db 0
```

**常用参数**：

| 参数 | 说明 | 默认 |
|------|------|------|
| `--type` | 缓存类型：`embedding` / `translation` / `anchors`，可多选 | 三类都检查 |
| `--pattern` | 自定义 key pattern（如 `mycache:*`），指定后忽略 `--type` | - |
| `--db` | Redis 数据库编号 | 0 |
| `--sample-size` | 每类采样的 key 数量 | 50 |
| `--max-scan` | 每类最多 SCAN 的 key 数量上限 | 20000 |

---

### 5.2 redis_cache_prefix_stats.py（按前缀统计条数与内存）

**功能**：**全局视角**，扫描当前 DB 下所有 key，按 key 的**前缀**（第一个冒号前）分类，统计每类 key 的**条数**与**内存占用量**（含占比），并输出每类示例 key 与 Redis 总内存信息。

- 使用 `SCAN` 扫全库，按前缀聚合；
- 内存优先用 Redis `MEMORY USAGE`，不可用时用 key+value 长度估算；key 过多时按 `--sample-size` 采样后按均值推算总内存；
- 输出：前缀、条数、内存及计算方式、占比、简要说明（如「翻译缓存」「向量化缓存」）；末尾附 Redis 总内存与 maxmemory（若配置）。

**适用场景**：了解「哪一类前缀占了多少条、多少内存」，做容量规划或清理决策；支持多 DB（`--all-db`）或指定前缀（`--prefix`）缩小范围。

**用法示例**：

```bash
# 默认 DB 0，全库按前缀统计
python scripts/redis/redis_cache_prefix_stats.py

# 统计所有有数据的 DB
python scripts/redis/redis_cache_prefix_stats.py --all-db

# 指定 DB
python scripts/redis/redis_cache_prefix_stats.py --db 1

# 只统计指定前缀（可多个）
python scripts/redis/redis_cache_prefix_stats.py --prefix trans embedding product_anchors

# 全 DB + 指定前缀
python scripts/redis/redis_cache_prefix_stats.py --all-db --prefix trans embedding
```

**常用参数**：

| 参数 | 说明 | 默认 |
|------|------|------|
| `--prefix` | 只统计这些前缀下的 key（如 `trans` `embedding`） | 全库按前缀统计 |
| `--db` | 数据库编号（0–15） | 0 |
| `--all-db` | 对所有有数据的 DB 分别执行 | 否 |
| `--sample-size` | 单前缀 key 过多时，用于内存采样的数量 | 100 |
| `--real` | 对单前缀 key 数 ≤10000 时计算全部 key 真实内存（较慢） | 否 |

---

### 5.3 redis_memory_heavy_keys.py（大 key / 内存占用排查）

**功能**：找出当前 DB 中**占用内存最多的 key**，并分析「按 key 估算的总内存」与「Redis 实际使用内存」的差异原因。

- 全库 `SCAN` 获取 key 列表，对 key 采样（默认最多 1000）调用 `MEMORY USAGE`（或估算）得到单 key 内存；
- 按内存排序，输出**占用最高的 N 个 key**（`--top`，默认 50）；
- 输出：前缀分布、采样统计、估算总内存 vs 实际内存、差异说明（碎片、内部结构等）；并检测超大 value（>1MB）、key 类型分布。

**适用场景**：内存异常升高时定位大 key；理解为何「按 key 加总」与 `used_memory` 不一致。

**用法示例**：

```bash
# 显示占用内存最多的 50 个 key（默认）
python scripts/redis/redis_memory_heavy_keys.py

# 显示前 100 个
python scripts/redis/redis_memory_heavy_keys.py --top 100
```

**常用参数**：

| 参数 | 说明 | 默认 |
|------|------|------|
| `--top` | 显示内存占用最高的 N 个 key | 50 |

---

### 5.4 三个脚本的选用建议

| 需求 | 推荐脚本 |
|------|----------|
| 看三类业务缓存（embedding/translation/anchors）的数量、TTL、近期写入、样本 value | `redis_cache_health_check.py` |
| 看全库或某前缀的 key 条数与内存占比 | `redis_cache_prefix_stats.py` |
| 找占用内存最多的大 key、分析内存差异 | `redis_memory_heavy_keys.py` |

---

## 6. 其他 Redis 相关代码（测试与替身）

除上述运维脚本外，以下代码使用 Redis 或其替身，但不定义新的业务缓存 key 协议：

- **单元测试**：`tests/test_embedding_pipeline.py` 中的 `_FakeRedis` 用于 mock embedding 缓存；`tests/test_translator_failure_semantics.py` 中的 `_RecordingRedis` 用于验证翻译失败时不写缓存。

---

## 7. 添加新缓存时的建议

新增 Redis 缓存时，建议遵循以下约定：

- **配置集中**：
  - key 前缀、TTL 优先放在 `config/env_config.py`（通用）或 `config/config.yaml -> services.<capability>.cache`；
  - 避免在业务代码中硬编码 TTL。
- **命名规范**：
  - 统一使用 `prefix:维度1:维度2:...` 的扁平 key 结构；
  - 对长文本/value 使用 `md5`/`sha256` 做哈希，避免过长 key。
- **文档同步**：
  - 新增缓存后，应在本文件中补充一行总览表 + 详细小节；
  - 若缓存与外部系统/历史实现兼容（如 Java 侧翻译缓存），需在说明中显式标注。
