Commit 701ae503e50ab97ebbcf87a5171c2e5a968cc4c1

Authored by tangwang
1 parent c10f90fe

docs

README.md
... ... @@ -125,14 +125,17 @@ python scripts/recreate_and_import.py \
125 125  
126 126 ## 新人入口
127 127  
128   -**→ `docs/QUICKSTART.md`**:环境、服务、模块、请求示例一页搞定。
  128 +**→ 开发者必读**:[docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) — 项目全貌、设计原则、扩展规范与迭代检查清单,保证后续开发在统一框架内进行。
  129 +
  130 +**→ 快速上手**:[docs/QUICKSTART.md](docs/QUICKSTART.md) — 环境、服务、模块、请求示例一页搞定。
129 131  
130 132 | 步骤 | 文档 |
131 133 |------|------|
  134 +| 0. 框架与规范(推荐首读) | `docs/DEVELOPER_GUIDE.md` |
132 135 | 1. 环境与启动 | `docs/QUICKSTART.md` |
133 136 | 2. 搜索/索引 API | `docs/QUICKSTART.md` §3、`docs/搜索API速查表.md` |
134 137 | 3. 运维与故障 | `docs/Usage-Guide.md` |
135   -| 4. 架构与扩展 | `docs/PROVIDER_ARCHITECTURE.md`、`docs/系统设计文档.md` |
  138 +| 4. 架构与扩展 | `docs/PROVIDER_ARCHITECTURE.md`、`docs/MODULE_EXTENSION_SPEC.md`、`docs/系统设计文档.md` |
136 139  
137 140 ### Runtimes & 命令示例
138 141  
... ... @@ -172,11 +175,13 @@ curl -X POST http://localhost:6002/search/ \
172 175  
173 176 | 文档 | 用途 |
174 177 |------|------|
175   -| `docs/QUICKSTART.md` | **新人入口**:环境、服务、模块、请求 |
  178 +| `docs/DEVELOPER_GUIDE.md` | **开发者开放指南**:全貌、原则、规范、检查清单 |
  179 +| `docs/QUICKSTART.md` | 新人上手:环境、服务、模块、请求 |
176 180 | `docs/Usage-Guide.md` | 运维:日志、多环境、故障排查 |
177 181 | `docs/搜索API速查表.md` | 搜索 API 参数速查 |
178 182 | `docs/搜索API对接指南.md` | 搜索 API 完整说明 |
179 183 | `docs/PROVIDER_ARCHITECTURE.md` | 翻译/向量/重排 provider 扩展 |
  184 +| `docs/MODULE_EXTENSION_SPEC.md` | 向量/重排后端可插拔规范 |
180 185 | `docs/环境配置说明.md` | 首次部署、新机器环境 |
181 186 | `docs/系统设计文档.md` | 架构与模块细节 |
182 187  
... ...
config/__init__.py
... ... @@ -30,6 +30,7 @@ from .services_config import (
30 30 get_translation_config,
31 31 get_embedding_config,
32 32 get_rerank_config,
  33 + get_rerank_backend_config,
33 34 get_translation_base_url,
34 35 get_embedding_base_url,
35 36 get_rerank_service_url,
... ... @@ -58,6 +59,7 @@ __all__ = [
58 59 'get_translation_config',
59 60 'get_embedding_config',
60 61 'get_rerank_config',
  62 + 'get_rerank_backend_config',
61 63 'get_translation_base_url',
62 64 'get_embedding_base_url',
63 65 'get_rerank_service_url',
... ...
config/config.yaml
... ... @@ -173,16 +173,31 @@ services:
173 173 model: ""
174 174 note: "reserved for future vLLM embedding backend"
175 175 rerank:
176   - provider: "http" # http | vllm(reserved)
  176 + provider: "http"
177 177 base_url: "http://127.0.0.1:6007"
178 178 providers:
179 179 http:
180 180 base_url: "http://127.0.0.1:6007"
181   - vllm:
182   - enabled: false
183   - base_url: ""
184   - model: ""
185   - note: "reserved for future vLLM reranker backend"
  181 + service_url: "http://127.0.0.1:6007/rerank"
  182 + # 服务内后端(reranker 进程启动时读取)
  183 + backend: "bge" # bge | qwen3_vllm
  184 + backends:
  185 + bge:
  186 + model_name: "BAAI/bge-reranker-v2-m3"
  187 + device: null
  188 + use_fp16: true
  189 + batch_size: 64
  190 + max_length: 512
  191 + cache_dir: "./model_cache"
  192 + enable_warmup: true
  193 + qwen3_vllm:
  194 + model_name: "Qwen/Qwen3-Reranker-0.6B"
  195 + engine: "vllm"
  196 + max_model_len: 8192
  197 + tensor_parallel_size: 1
  198 + gpu_memory_utilization: 0.8
  199 + enable_prefix_caching: true
  200 + instruction: "Given a web search query, retrieve relevant passages that answer the query"
186 201  
187 202 # SPU配置(已启用,使用嵌套skus)
188 203 spu_config:
... ...
config/services_config.py
... ... @@ -112,6 +112,25 @@ def _resolve_rerank() -> ServiceConfig:
112 112 return ServiceConfig(provider=provider, providers=providers)
113 113  
114 114  
  115 +def get_rerank_backend_config() -> tuple[str, dict]:
  116 + """
  117 + Resolve reranker backend name and config for the reranker service process.
  118 + Returns (backend_name, backend_cfg).
  119 + Env RERANK_BACKEND overrides config.
  120 + """
  121 + raw = _load_services_raw()
  122 + cfg = raw.get("rerank", {}) if isinstance(raw.get("rerank"), dict) else {}
  123 + backends = cfg.get("backends", {}) if isinstance(cfg.get("backends"), dict) else {}
  124 + name = (
  125 + os.getenv("RERANK_BACKEND")
  126 + or cfg.get("backend")
  127 + or "bge"
  128 + )
  129 + name = str(name).strip().lower()
  130 + backend_cfg = backends.get(name, {}) if isinstance(backends.get(name), dict) else {}
  131 + return name, backend_cfg
  132 +
  133 +
115 134 @lru_cache(maxsize=1)
116 135 def get_translation_config() -> ServiceConfig:
117 136 """Get translation service config."""
... ...
docs/DEVELOPER_GUIDE.md 0 → 100644
... ... @@ -0,0 +1,386 @@
  1 +# 开发者开放指南
  2 +
  3 +本文档面向**后续参与开发的工程师**,用于快速建立项目全貌、理解设计原则与规范,保证所有迭代在统一框架内进行,减少设计分叉与冗余代码,产出可持续继承的高质量代码。
  4 +
  5 +**阅读建议**:首次请按顺序通读第一至第五节;扩展能力或新增模块时重点阅读第六、七节;日常开发与 Code Review 可依赖第八、九节。
  6 +
  7 +---
  8 +
  9 +## 目录
  10 +
  11 +1. [文档说明与阅读路径](#1-文档说明与阅读路径)
  12 +2. [项目定位与核心价值](#2-项目定位与核心价值)
  13 +3. [总体架构](#3-总体架构)
  14 +4. [核心模块与职责](#4-核心模块与职责)
  15 +5. [设计原则与约束](#5-设计原则与约束)
  16 +6. [配置体系](#6-配置体系)
  17 +7. [扩展规范(能力与后端)](#7-扩展规范能力与后端)
  18 +8. [代码规范与质量](#8-代码规范与质量)
  19 +9. [迭代检查清单](#9-迭代检查清单)
  20 +10. [文档与资源索引](#10-文档与资源索引)
  21 +
  22 +---
  23 +
  24 +## 1. 文档说明与阅读路径
  25 +
  26 +### 1.1 本指南的角色
  27 +
  28 +- **唯一入口**:新人应首先阅读本指南,建立“框架全貌 + 规范”的认知。
  29 +- **规范聚合**:设计原则、配置约定、扩展方式、代码质量要求均在此汇总,并指向更细的专项文档。
  30 +- **迭代约束**:所有新功能、新模块、重构都应在符合本指南的前提下进行,Code Review 时可对照第九节检查清单。
  31 +
  32 +### 1.2 推荐阅读路径
  33 +
  34 +| 阶段 | 阅读内容 | 目的 |
  35 +|------|----------|------|
  36 +| 入职/接手 | 本指南 §1–§5 | 建立全貌:项目是什么、架构怎样、模块边界 |
  37 +| 开发前 | 本指南 §5–§7 + 相关专项文档 | 理解原则与配置、扩展方式,避免造轮子与分叉 |
  38 +| 开发中 | 本指南 §8 + QUICKSTART / API 文档 | 编码风格、测试要求、接口约定 |
  39 +| 提测/合入前 | 本指南 §9 | 自检是否满足框架与规范 |
  40 +
  41 +### 1.3 与本指南配套的专项文档
  42 +
  43 +以下文档由本指南引用,按需深入:
  44 +
  45 +- [QUICKSTART.md](./QUICKSTART.md) — 环境、服务、模块、请求示例
  46 +- [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) — 翻译/向量/重排 Provider 架构与新增 Provider
  47 +- [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) — 向量化/重排后端可插拔设计、协议与配置
  48 +- [系统设计文档.md](./系统设计文档.md) — 索引结构、数据流、通用化设计
  49 +- [基础配置指南.md](./基础配置指南.md) — 索引与查询配置说明
  50 +- [搜索API对接指南.md](./搜索API对接指南.md) — 搜索/索引/管理接口完整说明
  51 +- [环境配置说明.md](./环境配置说明.md) — 首次部署、新机器环境
  52 +- [Usage-Guide.md](./Usage-Guide.md) — 运维、日志、多环境、故障排查
  53 +
  54 +---
  55 +
  56 +## 2. 项目定位与核心价值
  57 +
  58 +### 2.1 项目是什么
  59 +
  60 +- **产品形态**:面向跨境独立站(如店匠 Shoplazza)的**多租户可配置搜索 SaaS**,提供搜索后端与索引富化能力。
  61 +- **核心交付**:
  62 + - **搜索服务**:文本搜索、图片搜索、建议(suggestions)、过滤、分面、排序、可选重排。
  63 + - **索引服务**:将 MySQL 中的店匠标准表(SPU/SKU)富化为符合 ES mapping 的文档(多语言、翻译、向量、规格聚合等),支持全量/增量及“仅构建 doc、由上游写 ES”的对接方式。
  64 + - **支撑服务**:向量服务(embedding)、翻译服务(translator)、重排服务(reranker),可独立部署、通过配置切换。
  65 +
  66 +### 2.2 核心价值与边界
  67 +
  68 +- **多租户**:单套代码与索引结构,通过 `tenant_id` 隔离数据;租户级配置(如主语言、索引语言)由配置与 tenant_config 支持。
  69 +- **可配置**:字段权重、搜索域、排序表达式、查询改写、功能开关等由配置驱动,避免硬编码业务逻辑。
  70 +- **可扩展**:翻译/向量/重排采用 Provider + 后端可插拔设计,新增实现时遵循协议与配置规范,不破坏现有调用方。
  71 +- **不负责**:商品主数据同步、店铺配置写库、全量/增量调度策略由上游(如 Java 索引程序)负责;本仓库专注“如何查、如何建 doc”。
  72 +
  73 +---
  74 +
  75 +## 3. 总体架构
  76 +
  77 +### 3.1 数据流(简化)
  78 +
  79 +```
  80 +MySQL (店匠 SPU/SKU)
  81 + → Indexer(富化:多语言、翻译、向量、规格聚合)
  82 + → Elasticsearch(按租户索引:search_products_tenant_<id>)
  83 + → 搜索 API(QueryParser → Searcher,可选翻译/向量/重排)
  84 + → 前端 / 上游业务
  85 +```
  86 +
  87 +- **索引侧**:Java 或脚本决定“对哪些 SPU 做索引”;Python indexer 负责“单条/批量 SPU → ES 文档”的完整逻辑,或通过 `/indexer/build-docs` 仅返回 doc、由调用方写 ES。
  88 +- **搜索侧**:请求经 QueryParser(解析、改写、翻译、向量化)→ Searcher(ES 查询、可选重排)→ 结果格式化 → 返回。
  89 +
  90 +### 3.2 服务拓扑与端口
  91 +
  92 +| 服务 | 端口 | 说明 | 默认随 run.sh 启动 |
  93 +|------|------|------|--------------------|
  94 +| backend | 6002 | 搜索 API(含 admin) | ✓ |
  95 +| indexer | 6004 | 索引 API(reindex/build-docs 等) | ✓ |
  96 +| frontend | 6003 | 调试 UI | ✓ |
  97 +| embedding | 6005 | 向量服务(文本/图片) | 可选 |
  98 +| translator | 6006 | 翻译服务 | 可选 |
  99 +| reranker | 6007 | 重排服务 | 可选 |
  100 +
  101 +- 启动:`./run.sh` 仅启动 backend / indexer / frontend;需全功能时通过环境变量或脚本另行启动 embedding / translator / reranker。
  102 +- 停止:统一使用 `./scripts/stop.sh`(会停止上述所有端口上的进程)。
  103 +- 详见 [QUICKSTART.md](./QUICKSTART.md) 与 [Usage-Guide.md](./Usage-Guide.md)。
  104 +
  105 +### 3.3 仓库目录结构(与架构对应)
  106 +
  107 +```
  108 +api/ # FastAPI 应用:搜索路由、管理路由、索引路由(indexer_app)
  109 +config/ # 配置加载与解析:config.yaml、services、env
  110 +indexer/ # MySQL → ES 管道:mapping、transformer、bulk、增量、build-docs
  111 +query/ # 查询解析:规范化、改写、翻译、embedding 调用、布尔解析
  112 +search/ # 搜索执行:多语言查询构建、Searcher、重排客户端、分数融合
  113 +embeddings/ # 向量化:服务端(server)、文本/图像后端、协议与配置
  114 +reranker/ # 重排:服务端(server)、后端(backends)、配置
  115 +providers/ # 能力提供者:翻译/向量/重排的客户端抽象与工厂
  116 +suggestion/ # 建议:索引构建、建议检索
  117 +utils/ # 共享工具:ES 客户端、DB 连接等
  118 +mappings/ # ES 索引 mapping 定义(如 search_products.json)
  119 +scripts/ # 脚本:环境、服务启停、数据、运维
  120 +frontend/ # 调试用前端静态资源
  121 +tests/ # 单元与集成测试
  122 +docs/ # 文档(含本指南)
  123 +```
  124 +
  125 +- **约定**:业务逻辑按能力放入对应顶层包;新增“能力”时优先考虑是否属于现有某包或 providers,避免随意新建顶层包导致分叉。
  126 +
  127 +---
  128 +
  129 +## 4. 核心模块与职责
  130 +
  131 +### 4.1 api
  132 +
  133 +- **职责**:对外 HTTP 入口;挂载搜索、管理、索引等路由;中间件(限流、CORS、安全头等);不承载具体搜索/索引算法。
  134 +- **入口**:`api/app.py`(搜索 + 管理)、`api/indexer_app.py`(索引),均由 `main.py` 的 `serve` / `serve-indexer` 启动。
  135 +- **原则**:路由层只做参数校验、租户解析、调用 search/query/indexer 等模块,不写复杂业务逻辑;配置与能力访问通过 config 与 providers 统一获取。
  136 +
  137 +### 4.2 config
  138 +
  139 +- **职责**:加载与解析 `config/config.yaml`(搜索行为、字段权重、分面、function_score、rerank 融合参数等);提供 `ConfigLoader` 与 `SearchConfig` 等数据结构;**服务级**配置(翻译/向量/重排的 provider、URL、后端)由 `config/services_config.py` 从 `config.yaml` 的 `services` 块及环境变量解析。
  140 +- **原则**:索引结构由 `mappings/search_products.json` 定义;搜索行为与能力配置以 config 为主、环境变量覆盖,不在业务代码中散落硬编码。
  141 +
  142 +### 4.3 indexer
  143 +
  144 +- **职责**:将 MySQL 行或上游传入的 SPU/SKU/options 转为符合 `mappings/search_products.json` 的 ES 文档;含多语言组织、翻译调用、向量生成、规格/SKU 聚合、类目路径等;支持全量/增量写入 ES,以及仅返回 doc(build-docs)供上游写 ES。
  145 +- **对接**:调用方通过 `providers` 获取翻译、向量等能力;索引名通过 `indexer/mapping_generator.get_tenant_index_name(tenant_id)` 与 `ES_INDEX_NAMESPACE` 一致。
  146 +- **详见**:`indexer/README.md`、[系统设计文档.md](./系统设计文档.md)。
  147 +
  148 +### 4.4 query
  149 +
  150 +- **职责**:查询解析与预处理:规范化、语言检测、改写(词典)、翻译、文本向量化、布尔表达式解析;输出可供 Searcher 使用的结构化查询信息。
  151 +- **原则**:翻译/向量通过 `providers` 获取,不直接依赖具体服务 URL 或实现;支持按配置关闭翻译/向量(如短查询、typing 场景)。
  152 +
  153 +### 4.5 search
  154 +
  155 +- **职责**:构建多语言 ES 查询、执行检索、可选重排、分数融合、结果格式化;分面、过滤、排序、SKU 维度筛选等。
  156 +- **原则**:重排通过 `search/rerank_client.py` 调用 `create_rerank_provider()`,不关心重排服务内是 BGE 还是 Qwen3;与 ES 的交互封装在 Searcher 内,便于 mock 与测试。
  157 +
  158 +### 4.6 embeddings
  159 +
  160 +- **职责**:提供向量服务(FastAPI):`POST /embed/text`、`POST /embed/image`;服务内按配置加载文本后端(如 BGE)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。
  161 +- **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;配置优先从 `config` 或 `embeddings/config.py` 读取,与 `services.embedding` 的 URL 分离。
  162 +- **详见**:`embeddings/README.md`、[MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。
  163 +
  164 +### 4.7 reranker
  165 +
  166 +- **职责**:提供重排服务(FastAPI):`POST /rerank`(query + docs → scores);服务内按配置加载一个重排后端(如 BGE 或 Qwen3-vLLM),实现 `score_with_meta(query, docs, normalize)` 协议。
  167 +- **原则**:对外 HTTP 契约固定;新增后端只在 `reranker/backends` 中实现协议并注册,不修改调用方。
  168 +- **详见**:[MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。
  169 +
  170 +### 4.8 providers
  171 +
  172 +- **职责**:统一“能力”的调用方式:翻译、向量、重排均通过工厂函数(如 `create_translation_provider()`、`create_rerank_provider()`、`create_embedding_provider()`)获取实现,配置来自 `config/services_config`(即 `config.yaml` 的 `services` + 环境变量)。
  173 +- **原则**:业务代码只依赖 Provider 接口,不依赖具体 URL 或后端类型;新增调用方式(如新 Provider 类型)在对应 `providers/<capability>.py` 中实现并在工厂中注册。
  174 +- **详见**:[PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md)。
  175 +
  176 +### 4.9 suggestion
  177 +
  178 +- **职责**:建议索引的构建与检索:从 ES 商品索引与 MySQL 日志等构建 suggestion 索引;搜索 API 的 `/search/suggestions` 使用本模块。
  179 +- **原则**:索引命名与租户、环境命名空间一致;构建入口可通过 `main.py build-suggestions` 或脚本封装调用。
  180 +
  181 +### 4.10 utils / mappings
  182 +
  183 +- **utils**:ES 客户端、DB 连接等通用工具;避免在业务包内重复实现。
  184 +- **mappings**:ES 索引 mapping 的 JSON 定义;所有租户共享同一结构,仅索引名按租户与环境区分。
  185 +
  186 +---
  187 +
  188 +## 5. 设计原则与约束
  189 +
  190 +### 5.1 多租户
  191 +
  192 +- 数据隔离仅通过 `tenant_id` 实现;索引可为单索引多租户或 per-tenant 索引(如 `search_products_tenant_<id>`),由索引名与查询时 filter 统一保证。
  193 +- 租户级配置(主语言、索引语言等)从 `tenant_config` 或等价配置读取,不在代码中写死租户 ID 或店铺逻辑。
  194 +
  195 +### 5.2 配置驱动
  196 +
  197 +- 搜索行为(字段权重、搜索域、排序、function_score、重排融合参数等)来自 `config/config.yaml`,由 `ConfigLoader` 加载。
  198 +- 能力访问(翻译/向量/重排的 provider、URL、后端类型)来自 `config.yaml` 的 `services` 块及环境变量,由 `config/services_config` 解析。
  199 +- 新增开关或参数时,优先在现有 config 结构下扩展,避免新增散落配置文件。
  200 +
  201 +### 5.3 单一配置源与优先级
  202 +
  203 +- 同一类配置只在一个地方定义默认值;覆盖顺序约定为:**环境变量 > config 文件**。
  204 +- 服务 URL、后端类型等均在 `services.<capability>` 下配置;环境变量用于部署态覆盖(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`)。
  205 +
  206 +### 5.4 调用方与实现解耦(Provider + Backend)
  207 +
  208 +- **调用方**:通过 Provider(如 `HttpRerankProvider`)访问能力,不依赖具体 URL 或服务内实现。
  209 +- **服务内**:通过“后端”实现具体推理(如 BGE 与 Qwen3-vLLM);后端实现协议、在配置与工厂中注册即可插拔。
  210 +- 新增“一种调用方式”在 providers 中扩展;新增“一种推理实现”在对应服务的 backends 中扩展,并遵循 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。
  211 +
  212 +### 5.5 协议契约
  213 +
  214 +- 同类型后端实现同一协议(如重排的 `score_with_meta`、图片的 `ImageEncoderProtocol`);调用方只依赖协议,不依赖具体类名或实现细节。
  215 +- 新增后端时必须满足现有协议(输入输出、顺序、长度、meta 字段等),避免调用方为兼容新后端而改代码。
  216 +
  217 +### 5.6 索引与查询结构统一
  218 +
  219 +- 索引结构以 `mappings/search_products.json` 为唯一来源;indexer 产出的 doc 必须与该 mapping 一致。
  220 +- 查询侧使用的字段名、多语言后缀(.zh/.en)、嵌套路径等与 mapping 保持一致;新增字段时同步更新 mapping 与查询/分面/过滤逻辑。
  221 +
  222 +### 5.7 错误与降级
  223 +
  224 +- 外部能力(翻译、向量、重排)调用失败时,应有明确降级策略(如跳过向量、仅用 BM25、重排失败时保留 ES 顺序),并打日志便于排查;不因单一能力不可用导致整请求失败。
  225 +
  226 +---
  227 +
  228 +## 6. 配置体系
  229 +
  230 +### 6.1 主配置文件
  231 +
  232 +- **config/config.yaml**:搜索行为(field_boosts、indexes、query_config、ranking、function_score、rerank 融合参数)、SPU 配置、**services**(翻译/向量/重排的 provider 与 backends)、tenant_config 等。
  233 +- **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。
  234 +
  235 +### 6.2 services 块结构(能力统一约定)
  236 +
  237 +```yaml
  238 +services:
  239 + <capability>:
  240 + provider: "http" # 调用方使用方式:http | direct | ...
  241 + base_url: "http://..."
  242 + providers:
  243 + http: { base_url: "...", ... }
  244 + direct: { ... }
  245 + backend: "bge" # 服务内后端(可选)
  246 + backends:
  247 + bge: { model_name: "...", ... }
  248 + qwen3_vllm: { ... }
  249 +```
  250 +
  251 +- **provider**:调用方如何访问(如 HTTP)。
  252 +- **backend / backends**:当能力由本仓库内服务提供时,该服务加载哪个后端及参数。
  253 +- 解析入口:`config/services_config.py` 的 `get_*_config()` 及 `get_*_base_url()` / `get_rerank_service_url()` 等。
  254 +
  255 +### 6.3 环境变量(常用)
  256 +
  257 +- 能力 URL:`TRANSLATION_SERVICE_URL`、`EMBEDDING_SERVICE_URL`、`RERANKER_SERVICE_URL`
  258 +- 能力选择:`TRANSLATION_PROVIDER`、`EMBEDDING_PROVIDER`、`RERANK_PROVIDER`、`RERANK_BACKEND`
  259 +- 环境与索引:`ES_HOST`、`ES_INDEX_NAMESPACE`、`RUNTIME_ENV`、DB 与 Redis 等
  260 +
  261 +详见 [环境配置说明.md](./环境配置说明.md)、[Usage-Guide.md](./Usage-Guide.md)。
  262 +
  263 +---
  264 +
  265 +## 7. 扩展规范(能力与后端)
  266 +
  267 +### 7.1 何时看扩展规范
  268 +
  269 +- 新增或替换**翻译/向量/重排**的调用方式(如新的 HTTP 客户端、gRPC):见 [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md)。
  270 +- 新增或替换**向量/重排**的推理实现(如新模型、vLLM):见 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。
  271 +
  272 +### 7.2 新增 Provider(调用方式)
  273 +
  274 +1. 在 `providers/<capability>.py` 中实现新类(与现有 Provider 同接口)。
  275 +2. 在 `create_*_provider()` 中按 `config.provider` 或环境变量增加分支。
  276 +3. 在 `config/config.yaml` 的 `services.<capability>.providers` 下补充参数。
  277 +4. 不修改业务调用方(search/query/indexer 仍通过工厂获取实例)。
  278 +
  279 +### 7.3 新增 Backend(推理实现)
  280 +
  281 +1. **实现协议**:在对应目录(如 `reranker/backends/`、`embeddings/`)实现满足协议接口的类。
  282 +2. **配置**:在 `config/config.yaml` 的 `services.<capability>.backends` 下增加新后端名及参数;支持环境变量覆盖(如 `RERANK_BACKEND`)。
  283 +3. **注册**:在 backends 的工厂(如 `get_rerank_backend(name, config)`)中增加分支并返回实例。
  284 +4. **服务启动**:服务(如 `reranker/server.py`)启动时读取 backend 配置并调用工厂,不写死后端类型。
  285 +5. **文档与依赖**:在 README 或 docs 中说明新后端的依赖、资源要求;可选依赖放入 `requirements_ml.txt` 或 extra。
  286 +
  287 +详见 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) 的“新增后端清单”。
  288 +
  289 +### 7.4 禁止做法
  290 +
  291 +- 在业务代码中硬编码服务 URL 或后端类型。
  292 +- 新增能力时复制一套独立配置体系或新顶层包,而不纳入 `services` 与 providers/backends。
  293 +- 新增后端时破坏现有协议(如修改返回长度、顺序或 meta 约定)。
  294 +
  295 +---
  296 +
  297 +## 8. 代码规范与质量
  298 +
  299 +### 8.1 风格与结构
  300 +
  301 +- **Python**:遵循 PEP 8;类型注解推荐在公共接口与配置数据结构上使用;模块级文档字符串简要说明职责。
  302 +- **包结构**:业务逻辑按能力归属对应顶层包;共享工具放 `utils`;不随意新增与现有包平行的“杂项”包。
  303 +- **命名**:模块与类名清晰表意;配置键与 `config.yaml` / 环境变量命名保持一致。
  304 +
  305 +### 8.2 测试
  306 +
  307 +- **位置**:`tests/`,可按 `unit/`、`integration/` 或按模块划分子目录;公共 fixture 在 `conftest.py`。
  308 +- **标记**:使用 `@pytest.mark.unit`、`@pytest.mark.integration`、`@pytest.mark.api` 等区分用例类型,便于按需运行。
  309 +- **依赖**:单元测试通过 mock(如 `mock_es_client`、`sample_search_config`)不依赖真实 ES/DB;集成测试需在说明中注明依赖服务。
  310 +- **运行**:`python -m pytest tests/`;仅单元:`python -m pytest tests/unit/` 或 `-m unit`。
  311 +- **原则**:新增逻辑应有对应测试;修改协议或配置契约时更新相关测试与 fixture。
  312 +
  313 +### 8.3 配置与环境
  314 +
  315 +- 测试用配置优先从 fixture 或临时 config 构造,避免依赖仓库外部的 `.env` 或真实 DB/ES;必要时使用 `clear_services_cache()` 等清理缓存。
  316 +- 不在代码中提交敏感信息;敏感项通过 `.env` 或环境变量注入,并在文档中说明。
  317 +
  318 +### 8.4 日志与可观测性
  319 +
  320 +- 关键路径(请求入口、外部调用、失败降级)打日志;日志级别合理(如 debug 用于详细参数,info 用于流程,warning 用于降级)。
  321 +- 对外接口的耗时、错误码、租户等可考虑结构化日志或后续接入监控,便于运维与排查。
  322 +
  323 +---
  324 +
  325 +## 9. 迭代检查清单
  326 +
  327 +在提交代码或发起 Code Review 前,建议自检以下项,确保迭代符合框架与规范。
  328 +
  329 +### 9.1 架构与模块
  330 +
  331 +- [ ] 新逻辑放在合适的现有包中,未随意新建与现有能力平行的顶层包。
  332 +- [ ] 未在业务代码中硬编码服务 URL、后端类型或租户 ID。
  333 +- [ ] 调用外部能力(翻译/向量/重排)时通过 providers 工厂获取实例,配置来自 `services_config`。
  334 +
  335 +### 9.2 配置与扩展
  336 +
  337 +- [ ] 新增配置项放在 `config.yaml` 或 `services.<capability>` 下,并有环境变量覆盖方式(如需要)。
  338 +- [ ] 新增 Provider 或 Backend 时已阅读 [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) / [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md),并按要求实现协议、注册与配置。
  339 +- [ ] 新增后端满足现有协议(输入输出、顺序、长度、meta),未破坏调用方。
  340 +
  341 +### 9.3 索引与查询
  342 +
  343 +- [ ] 索引结构变更已同步到 `mappings/search_products.json`;indexer 产出与 mapping 一致。
  344 +- [ ] 查询/分面/过滤使用的字段名与 mapping 一致;多语言字段使用 `.zh`/`.en` 等约定。
  345 +
  346 +### 9.4 测试与质量
  347 +
  348 +- [ ] 新增或修改逻辑有对应测试;修改接口或协议时已更新相关测试与 fixture。
  349 +- [ ] 单元测试不依赖真实 ES/DB;集成测试在文档或注释中说明依赖。
  350 +- [ ] 无敏感信息提交;敏感配置通过环境变量或 .env 说明。
  351 +
  352 +### 9.5 文档与可维护性
  353 +
  354 +- [ ] 新增模块或重要行为在 README 或 docs 中有简要说明;复杂逻辑有注释或文档引用。
  355 +- [ ] 本指南与相关专项文档在需要时已更新(如新增服务、端口、配置项、扩展步骤)。
  356 +
  357 +---
  358 +
  359 +## 10. 文档与资源索引
  360 +
  361 +### 10.1 按用途查找
  362 +
  363 +| 用途 | 文档 |
  364 +|------|------|
  365 +| 新人上手、环境与请求示例 | [QUICKSTART.md](./QUICKSTART.md) |
  366 +| 框架全貌与规范(本文) | 本指南 |
  367 +| 翻译/向量/重排 Provider 扩展 | [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) |
  368 +| 向量/重排后端可插拔与协议 | [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) |
  369 +| 索引结构、数据流、通用化设计 | [系统设计文档.md](./系统设计文档.md) |
  370 +| 索引与查询配置说明 | [基础配置指南.md](./基础配置指南.md) |
  371 +| 搜索/索引 API 完整说明 | [搜索API对接指南.md](./搜索API对接指南.md) |
  372 +| 搜索 API 参数速查 | [搜索API速查表.md](./搜索API速查表.md) |
  373 +| 首次部署、新机器环境 | [环境配置说明.md](./环境配置说明.md) |
  374 +| 运维、日志、多环境、故障 | [Usage-Guide.md](./Usage-Guide.md) |
  375 +| 索引模块职责与 Java 对接 | [indexer/README.md](../indexer/README.md) |
  376 +| 向量模块与 clip-as-service | [embeddings/README.md](../embeddings/README.md) |
  377 +
  378 +### 10.2 仓库内入口
  379 +
  380 +- **README.md**:项目简介、快速命令、文档索引。
  381 +- **CLAUDE.md**:面向 AI 助手的项目说明与命令汇总,与本指南互补。
  382 +- **本指南(docs/DEVELOPER_GUIDE.md)**:面向人的全貌与规范入口。
  383 +
  384 +---
  385 +
  386 +*本文档旨在让所有后续开发在预知框架全貌的前提下,在规范内迭代,减少分叉与冗余,提升可维护性。如有结构或规范变更,请同步更新本指南及相关专项文档。*
... ...
docs/MODULE_EXTENSION_SPEC.md 0 → 100644
... ... @@ -0,0 +1,223 @@
  1 +# 模块扩展规范(向量化 / 重排 可插拔设计)
  2 +
  3 +本文档定义**向量化(embedding)**与**重排(rerank)**模块的扩展规范,保证新增模型/推理引擎时框架统一、配置统一、可插拔。新增 Qwen3-Reranker-0.6B(vLLM)等模块时需遵循本规范。
  4 +
  5 +**相关文档**:
  6 +- 调用方(Provider 选择、HTTP 客户端):[PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md)
  7 +- 向量化使用说明:[embeddings/README.md](../embeddings/README.md)、[向量化模块和API说明文档.md](./向量化模块和API说明文档.md)
  8 +
  9 +---
  10 +
  11 +## 1. 设计原则
  12 +
  13 +| 原则 | 说明 |
  14 +|------|------|
  15 +| **接口契约** | 所有同类型后端实现同一协议(Protocol),调用方只依赖协议不依赖具体实现。 |
  16 +| **单一配置源** | 能力类型、后端类型、后端参数均来自 `config/config.yaml` 的 `services` 块,环境变量可覆盖。 |
  17 +| **服务与后端分离** | **调用方**通过 Provider(如 `HttpRerankProvider`)访问**服务**;**服务内部**通过后端实现(如 BGE、Qwen3-vLLM)完成推理。新增“提供者”时区分:是新增一种**调用方式**(新 Provider)还是新增一种**推理实现**(新 Backend)。 |
  18 +| **可插拔后端** | 重排/向量化服务在启动时根据配置加载一个后端;新增后端 = 实现协议 + 在配置与工厂中注册,不改服务入口代码。 |
  19 +
  20 +---
  21 +
  22 +## 2. 配置体系(统一结构)
  23 +
  24 +### 2.1 配置来源与优先级
  25 +
  26 +- **主配置**:`config/config.yaml` 下的 `services.<capability>`
  27 +- **覆盖**:环境变量(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`)> config 文件
  28 +- **解析**:`config/services_config.py` 提供 `get_*_config()`,各模块从该处读取,避免散落多处。
  29 +
  30 +### 2.2 能力块通用结构
  31 +
  32 +每种能力(translation / embedding / rerank)在 `services` 下结构一致:
  33 +
  34 +```yaml
  35 +services:
  36 + <capability>:
  37 + provider: "http" # 调用方使用的提供者:http | direct | vllm 等
  38 + base_url: "http://..." # 对外服务 URL(provider=http 时)
  39 + providers:
  40 + http: { base_url: "...", ... }
  41 + direct: { ... }
  42 + vllm: { ... }
  43 + # 以下为「服务内部后端」配置(仅当本能力由本仓库启动的服务承载时使用)
  44 + backend: "bge" # 可选:服务内加载的后端类型
  45 + backends:
  46 + bge: { model_name: "...", batch_size: 64, ... }
  47 + qwen3_vllm: { model_name: "Qwen/Qwen3-Reranker-0.6B", ... }
  48 +```
  49 +
  50 +- **provider**:调用方(搜索 API、索引等)如何访问该能力(如 HTTP 调 `base_url`)。
  51 +- **backend / backends**:当该能力由本仓库内的服务进程提供时,该进程内应加载哪个后端及参数(如 reranker 服务内用 BGE 还是 Qwen3-vLLM)。
  52 +
  53 +---
  54 +
  55 +## 3. 重排(Rerank)模块规范
  56 +
  57 +### 3.1 调用链
  58 +
  59 +- **调用方**:`search/rerank_client.py` → `create_rerank_provider()` → `HttpRerankProvider.rerank(query, docs, timeout_sec)`
  60 +- **协议**:HTTP `POST <base>/rerank`,请求体 `{ "query": str, "docs": [str] }`,响应体 `{ "scores": [float], "meta": dict }`,scores 与 docs 一一对应。
  61 +- **服务实现**:`reranker/server.py`(FastAPI)在启动时加载一个**重排后端**,对 `/rerank` 的请求用该后端计算分数。
  62 +
  63 +因此:
  64 +- **新增一种“调用方式”**(如 gRPC):在 `providers/rerank.py` 增加新 Provider 类,并在 `create_rerank_provider()` 中按 `provider` 选择。
  65 +- **新增一种“推理实现”**(如 Qwen3-vLLM):在 reranker 服务内实现**重排后端协议**并注册,服务通过配置选择后端。
  66 +
  67 +### 3.2 重排后端协议(服务内)
  68 +
  69 +所有在 `reranker` 服务内加载的后端必须实现以下接口(与当前 `BGEReranker` 一致):
  70 +
  71 +```python
  72 +# 行为契约(不强制继承,实现以下方法即可)
  73 +class RerankBackendProtocol(Protocol):
  74 + def score_with_meta(
  75 + self,
  76 + query: str,
  77 + docs: List[str],
  78 + normalize: bool = True,
  79 + ) -> Tuple[List[float], Dict[str, Any]]:
  80 + """
  81 + 输入:
  82 + - query: 搜索查询字符串
  83 + - docs: 文档列表,与返回的 scores 一一对应
  84 + - normalize: 是否对分数做归一化(如 sigmoid)
  85 + 输出:
  86 + - scores: 与 docs 等长的分数列表,顺序一致
  87 + - meta: 至少含 input_docs, usable_docs, unique_docs, elapsed_ms 等,供日志与调试
  88 + """
  89 + ...
  90 +```
  91 +
  92 +- **顺序**:返回的 `scores[i]` 必须对应 `docs[i]`。
  93 +- **空/无效**:对无法打分的 doc 可填 0.0,并在 meta 中说明。
  94 +- **去重**:后端可对 docs 去重再推理以省算力,但返回的 scores 必须按原始 docs 顺序与长度还原。
  95 +
  96 +### 3.3 重排服务配置项(建议)
  97 +
  98 +在 `config/config.yaml` 的 `services.rerank` 下建议结构(与现有 `rerank` 顶层配置区分:顶层为搜索侧融合参数,此处为服务/后端配置):
  99 +
  100 +```yaml
  101 +services:
  102 + rerank:
  103 + provider: "http"
  104 + base_url: "http://127.0.0.1:6007"
  105 + providers:
  106 + http:
  107 + base_url: "http://127.0.0.1:6007"
  108 + service_url: "http://127.0.0.1:6007/rerank"
  109 + # 服务内后端(reranker 进程启动时读取)
  110 + backend: "bge" # bge | qwen3_vllm
  111 + backends:
  112 + bge:
  113 + model_name: "BAAI/bge-reranker-v2-m3"
  114 + device: null
  115 + use_fp16: true
  116 + batch_size: 64
  117 + max_length: 512
  118 + cache_dir: "./model_cache"
  119 + enable_warmup: true
  120 + qwen3_vllm:
  121 + model_name: "Qwen/Qwen3-Reranker-0.6B"
  122 + engine: "vllm"
  123 + max_model_len: 8192
  124 + tensor_parallel_size: 1
  125 + gpu_memory_utilization: 0.8
  126 + instruction: "Given a web search query, retrieve relevant passages that answer the query"
  127 +```
  128 +
  129 +- 环境变量示例:`RERANK_BACKEND=qwen3_vllm`、`RERANKER_SERVICE_URL=http://127.0.0.1:6007`。
  130 +
  131 +### 3.4 重排后端目录与注册
  132 +
  133 +- **推荐目录**:`reranker/backends/`
  134 + - `reranker/backends/__init__.py`:导出 `get_rerank_backend(name, config) -> 实现 RerankBackendProtocol 的实例`
  135 + - `reranker/backends/bge.py`:现有 BGE 逻辑迁移或封装为 `BGERerankerBackend`
  136 + - `reranker/backends/qwen3_vllm.py`:新增 Qwen3-Reranker-0.6B + vLLM 实现
  137 +- **服务启动**:`reranker/server.py` 在 `startup` 中读取 `services.rerank.backend` 与 `services.rerank.backends.<name>`,调用 `get_rerank_backend(backend, cfg)` 得到实例,再对外提供同一 `/rerank` API。
  138 +
  139 +### 3.5 重排 HTTP API 契约(不变)
  140 +
  141 +无论后端是 BGE 还是 Qwen3-vLLM,对外接口保持一致,便于调用方与运维统一:
  142 +
  143 +- **POST /rerank**
  144 + - Request: `{ "query": string, "docs": [string], "normalize": optional bool }`
  145 + - Response: `{ "scores": [float], "meta": object }`
  146 +- **GET /health**
  147 + - Response: `{ "status": "ok"|"unavailable", "model_loaded": bool, "model": string, "backend": string }`
  148 +
  149 +---
  150 +
  151 +## 4. 向量化(Embedding)模块规范
  152 +
  153 +### 4.1 调用链
  154 +
  155 +- **调用方**:通过 `providers.create_embedding_provider()` 得到 HTTP 客户端,请求 `POST /embed/text`、`POST /embed/image`。
  156 +- **服务实现**:`embeddings/server.py` 在启动时按配置加载**文本后端**与**图片后端**,二者可独立选择。
  157 +
  158 +### 4.2 向量化后端协议(服务内)
  159 +
  160 +- **文本**:与当前 `BgeTextModel` 一致,需支持 `encode_batch(texts, batch_size, device) -> List[ndarray]`,元素与 `texts` 一一对应,失败可为 None。
  161 +- **图片**:已定义 `embeddings/protocols.ImageEncoderProtocol`:
  162 + - `encode_image_urls(urls: List[str], batch_size: Optional[int]) -> List[Optional[np.ndarray]]`
  163 + - 与 `urls` 等长,失败位置为 None。
  164 +
  165 +新增文本/图片后端时实现对应协议即可;服务通过配置选择后端(如 `USE_CLIP_AS_SERVICE` 选 clip-as-service 或本地 CN-CLIP)。
  166 +
  167 +### 4.3 向量化配置(现有与扩展)
  168 +
  169 +- **Provider/URL**:`config/config.yaml` → `services.embedding`,环境变量 `EMBEDDING_SERVICE_URL`。
  170 +- **服务内**:`embeddings/config.py` 中已有 `TEXT_*`、`IMAGE_*`、`USE_CLIP_AS_SERVICE`、`CLIP_AS_SERVICE_SERVER`;若未来支持多种文本/图像后端,建议在 `services.embedding.backend` / `services.embedding.backends` 中统一,与重排结构对齐。
  171 +
  172 +---
  173 +
  174 +## 5. 新增后端清单(以 Qwen3-Reranker-0.6B + vLLM 为例)
  175 +
  176 +按本规范新增「重排后端」Qwen3-Reranker-0.6B(vLLM 推理)时,建议步骤:
  177 +
  178 +1. **实现协议**
  179 + - 在 `reranker/backends/qwen3_vllm.py` 中实现类(如 `Qwen3VLLMReranker`),提供 `score_with_meta(query, docs, normalize) -> (scores, meta)`。
  180 + - 推理逻辑参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B) 的 vLLM 用法(format_instruction、process_inputs、compute_logits、yes/no token 等),输出与 `docs` 等长且顺序一致的 scores。
  181 +
  182 +2. **配置**
  183 + - 在 `config/config.yaml` 的 `services.rerank.backends` 下增加 `qwen3_vllm` 块(model_name、engine、max_model_len、tensor_parallel_size、gpu_memory_utilization、instruction 等)。
  184 + - 在 `config/services_config.py` 或 reranker 专用 config 中增加对 `backend` / `backends` 的读取;环境变量支持 `RERANK_BACKEND=qwen3_vllm`。
  185 +
  186 +3. **注册**
  187 + - 在 `reranker/backends/__init__.py` 的 `get_rerank_backend(name, config)` 中增加 `"qwen3_vllm"` 分支,实例化 `Qwen3VLLMReranker` 并传入 config。
  188 +
  189 +4. **服务启动**
  190 + - 若尚未重构:可暂时在 `reranker/server.py` 中根据 `RERANK_BACKEND` 或 config 选择加载 `BGEReranker` 或 `Qwen3VLLMReranker`。
  191 + - 若已引入 `get_rerank_backend()`:`reranker/server.py` 启动时统一调用 `get_rerank_backend(backend_name, backend_cfg)` 得到实例。
  192 +
  193 +5. **调用方**
  194 + - 无需修改:`providers/rerank.py` 仍为 HTTP,`search/rerank_client.py` 仍调用同一 `/rerank` 接口;仅部署时启动使用 Qwen3-vLLM 后端的 reranker 服务即可。
  195 +
  196 +6. **文档与依赖**
  197 + - 在 `reranker/README.md` 或 `docs/` 中说明 Qwen3-vLLM 的依赖(vllm>=0.8.5、transformers 等)、显存建议、与 BGE 的对比。
  198 + - 若 vLLM 为可选依赖,在 `requirements_ml.txt` 或可选 extra 中声明。
  199 +
  200 +---
  201 +
  202 +## 6. 小结表
  203 +
  204 +| 层次 | 配置键 | 重排 | 向量化(文本/图) |
  205 +|------|--------|------|-------------------|
  206 +| 调用方 | `services.<capability>.provider` | http | http |
  207 +| 调用方 | `services.<capability>.providers.http.base_url` | 6007 | 6005 |
  208 +| 服务内 | `services.<capability>.backend` | bge / qwen3_vllm | (当前在 embeddings/config.py) |
  209 +| 服务内 | `services.<capability>.backends.<name>` | 模型名、batch、vLLM 参数等 | 模型名、device 等 |
  210 +| 协议 | 重排 | `score_with_meta(query, docs, normalize)` | — |
  211 +| 协议 | 向量化 | — | 文本: encode_batch;图: ImageEncoderProtocol |
  212 +
  213 +遵循上述规范后,新增 Qwen3-Reranker-0.6B 或其它重排/向量化后端时,只需实现协议、在配置与工厂中注册,即可与现有 BGE/CLIP 等并列切换,保持框架统一与可插拔。
  214 +
  215 +---
  216 +
  217 +## 7. 与现有配置文件的兼容说明
  218 +
  219 +- **reranker**:当前 `reranker/config.py` 中 `RerankerConfig`(PORT、MODEL_NAME、BATCH_SIZE 等)仅被 BGE 服务使用。扩展多后端时,建议:
  220 + - 保留该文件作为**默认/兜底**(仅当未配置 `services.rerank.backend` 时使用),或
  221 + - 将 BGE 的默认值迁移到 `config.yaml` 的 `services.rerank.backends.bge`,`reranker/config.py` 只读环境变量与 YAML,不再硬编码模型名。
  222 +- **embeddings**:`embeddings/config.py` 的 `EmbeddingConfig` 已包含文本/图片及 clip-as-service 开关,与 `services.embedding` 的 URL 分离(URL 由 `services_config` 管)。后续若增加多种文本/图像后端,可同样在 `services.embedding.backends` 中增加条目,与重排对齐。
  223 +- **环境变量**:所有能力均支持通过环境变量覆盖(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`、`EMBEDDING_SERVICE_URL`),便于部署与多环境。
... ...
docs/PROVIDER_ARCHITECTURE.md
... ... @@ -2,6 +2,8 @@
2 2  
3 3 本文档说明如何统一管理翻译、向量化、重排等“能力提供者(provider)”。
4 4  
  5 +**扩展重排/向量化后端(如新增 Qwen3-Reranker、vLLM 等)**:请同时参阅 [模块扩展规范(MODULE_EXTENSION_SPEC.md)](./MODULE_EXTENSION_SPEC.md),其中定义了服务内后端协议、统一配置结构与可插拔实现方式。
  6 +
5 7 ## 1. 设计目标
6 8  
7 9 - **调用方稳定**:业务代码不关心具体供应商,只调用统一接口。
... ...
docs/QUICKSTART.md
... ... @@ -2,6 +2,8 @@
2 2  
3 3 新人入口文档:环境、服务、模块、请求示例一页搞定。
4 4  
  5 +**建议**:首次参与开发请先阅读 [DEVELOPER_GUIDE.md](./DEVELOPER_GUIDE.md) 建立项目全貌与规范,再使用本页做环境与请求速查。
  6 +
5 7 ## 1. 环境
6 8  
7 9 ```bash
... ...
requirements_ml.txt
1   -# Optional heavy dependencies for local embedding/image encoding.
  1 +# Optional heavy dependencies for local embedding/image encoding and reranker backends.
2 2 #
3 3 # Install when you need:
4 4 # - `./scripts/start_embedding_service.sh` (local embeddings server)
5 5 # - local BGE-M3 / CN-CLIP inference
  6 +# - reranker with BGE backend (modelscope)
  7 +#
  8 +# For Qwen3-Reranker-0.6B (vLLM backend), also install:
  9 +# pip install vllm>=0.8.5
  10 +# (transformers already listed below)
6 11 #
7 12 # Notes:
8 13 # - `torch` wheels can be very large; if you want CPU-only wheels,
... ...
reranker/README.md
1 1 # Reranker 模块
2 2  
3   -**请求示例**见 `docs/QUICKSTART.md` §3.5。
  3 +**请求示例**见 `docs/QUICKSTART.md` §3.5。扩展规范见 `docs/MODULE_EXTENSION_SPEC.md`。
4 4  
5 5 ---
6 6  
7   -A minimal, production-ready reranker service based on **BAAI/bge-reranker-v2-m3**.
8   -
9   -Features
10   -- FP16 on GPU
11   -- Length-based sorting to reduce padding waste
12   -- Deduplication to avoid redundant inference
13   -- Scores returned in original input order
14   -- Simple FastAPI service
15   -
16   -## Files
17   -- `reranker/bge_reranker.py`: core model loading + scoring logic
18   -- `reranker/server.py`: FastAPI service with `/health` and `/rerank`
19   -- `reranker/config.py`: simple configuration
20   -
21   -## Requirements
22   -Install Python deps (already in project requirements):
23   -- `torch`
24   -- `modelscope`
25   -- `fastapi`
26   -- `uvicorn`
27   -
28   -## Configuration
29   -Edit `reranker/config.py`:
30   -- `MODEL_NAME`: default `BAAI/bge-reranker-v2-m3`
31   -- `DEVICE`: `None` (auto), `cuda`, or `cpu`
32   -- `USE_FP16`: enable fp16 on GPU
33   -- `BATCH_SIZE`: default 64
34   -- `MAX_LENGTH`: default 512
35   -- `PORT`: default 6007
36   -- `MAX_DOCS`: request limit (default 1000)
37   -
38   -## Run the Service
  7 +Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwen3-vLLM)。调用方通过 HTTP 访问,不关心具体后端。
  8 +
  9 +**特性**
  10 +- 多后端:`bge`(BAAI/bge-reranker-v2-m3)、`qwen3_vllm`(Qwen3-Reranker-0.6B + vLLM)
  11 +- 统一配置:`config/config.yaml` → `services.rerank.backend` / `services.rerank.backends.<name>`
  12 +- 文档去重、分数与输入顺序一致、FP16/GPU 支持(视后端)
  13 +
  14 +## 目录与入口
  15 +- `reranker/server.py`:FastAPI 服务,启动时按配置加载一个后端
  16 +- `reranker/backends/`:后端实现与工厂
  17 + - `backends/__init__.py`:`get_rerank_backend(name, config)`
  18 + - `backends/bge.py`:BGE 后端
  19 + - `backends/qwen3_vllm.py`:Qwen3-Reranker-0.6B + vLLM 后端
  20 +- `reranker/bge_reranker.py`:BGE 核心推理(被 bge 后端封装)
  21 +- `reranker/config.py`:服务端口、MAX_DOCS、NORMALIZE 等(后端参数在 config.yaml)
  22 +
  23 +## 依赖
  24 +- 通用:`torch`、`modelscope`、`fastapi`、`uvicorn`(见项目 `requirements.txt` / `requirements_ml.txt`)
  25 +- **Qwen3-vLLM 后端**:`vllm>=0.8.5`、`transformers`(可选,仅当使用 `backend: qwen3_vllm` 时安装)
  26 + ```bash
  27 + pip install vllm>=0.8.5 transformers
  28 + ```
  29 +
  30 +## 配置
  31 +- **后端选择**:`config/config.yaml` 中 `services.rerank.backend`(`bge` | `qwen3_vllm`),或环境变量 `RERANK_BACKEND`。
  32 +- **后端参数**:`services.rerank.backends.bge` / `services.rerank.backends.qwen3_vllm`,例如:
  33 +
  34 +```yaml
  35 +services:
  36 + rerank:
  37 + backend: "bge" # 或 qwen3_vllm
  38 + backends:
  39 + bge:
  40 + model_name: "BAAI/bge-reranker-v2-m3"
  41 + device: null
  42 + use_fp16: true
  43 + batch_size: 64
  44 + max_length: 512
  45 + cache_dir: "./model_cache"
  46 + enable_warmup: true
  47 + qwen3_vllm:
  48 + model_name: "Qwen/Qwen3-Reranker-0.6B"
  49 + max_model_len: 8192
  50 + tensor_parallel_size: 1
  51 + gpu_memory_utilization: 0.8
  52 + enable_prefix_caching: true
  53 + instruction: "Given a web search query, retrieve relevant passages that answer the query"
  54 +```
  55 +
  56 +- 服务端口、请求限制等仍在 `reranker/config.py`(或环境变量 `RERANKER_PORT`、`RERANKER_HOST`)。
  57 +
  58 +## 运行
39 59 ```bash
40 60 uvicorn reranker.server:app --host 0.0.0.0 --port 6007
41 61 ```
  62 +使用 Qwen3-vLLM 时需先安装 vLLM 与 transformers,并将 `services.rerank.backend` 设为 `qwen3_vllm` 或设置 `RERANK_BACKEND=qwen3_vllm`。
42 63  
43 64 ## API
44 65 ### Health
45 66 ```
46 67 GET /health
47 68 ```
  69 +Response 含 `backend`(当前后端名)、`model`、`model_loaded`、`status`。
48 70  
49 71 ### Rerank
50 72 ```
... ... @@ -86,6 +108,6 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info
86 108 ```
87 109  
88 110 ## Notes
89   -- No caching is used by design.
90   -- Inputs are deduplicated by exact string match.
91   -- Empty or null docs are skipped and scored as 0.
  111 +- 无请求级缓存;输入按字符串去重后推理,再按原始顺序回填分数。
  112 +- 空或 null 的 doc 跳过并计为 0。
  113 +- **Qwen3-vLLM**:参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B),需 GPU 与较多显存;与 BGE 相比适合长文本、高吞吐场景(vLLM 前缀缓存)。
... ...
reranker/backends/__init__.py 0 → 100644
... ... @@ -0,0 +1,49 @@
  1 +"""
  2 +Rerank backends - pluggable implementations of the rerank protocol.
  3 +
  4 +Each backend implements score_with_meta(query, docs, normalize) -> (scores, meta).
  5 +Service loads one backend via get_rerank_backend(name, config) from config.
  6 +"""
  7 +
  8 +from __future__ import annotations
  9 +
  10 +from typing import Any, Dict, List, Protocol, Tuple
  11 +
  12 +
  13 +class RerankBackendProtocol(Protocol):
  14 + """Protocol for reranker backends (service-internal)."""
  15 +
  16 + def score_with_meta(
  17 + self,
  18 + query: str,
  19 + docs: List[str],
  20 + normalize: bool = True,
  21 + ) -> Tuple[List[float], Dict[str, Any]]:
  22 + """
  23 + Input:
  24 + query: search query string
  25 + docs: list of documents, scores must align 1:1 with docs
  26 + normalize: whether to normalize scores (e.g. sigmoid)
  27 + Output:
  28 + scores: list same length as docs, same order
  29 + meta: at least input_docs, usable_docs, unique_docs, elapsed_ms
  30 + """
  31 + ...
  32 +
  33 +
  34 +def get_rerank_backend(name: str, config: Dict[str, Any]) -> RerankBackendProtocol:
  35 + """
  36 + Factory: return a reranker backend instance for the given name and config.
  37 + Config is the corresponding block from services.rerank.backends.<name>.
  38 + """
  39 + name = (name or "bge").strip().lower()
  40 + if name == "bge":
  41 + from reranker.backends.bge import BGERerankerBackend
  42 + return BGERerankerBackend(config)
  43 + if name == "qwen3_vllm":
  44 + from reranker.backends.qwen3_vllm import Qwen3VLLMRerankerBackend
  45 + return Qwen3VLLMRerankerBackend(config)
  46 + raise ValueError(f"Unknown rerank backend: {name!r}. Supported: bge, qwen3_vllm")
  47 +
  48 +
  49 +__all__ = ["RerankBackendProtocol", "get_rerank_backend"]
... ...
reranker/backends/bge.py 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +"""
  2 +BGE reranker backend - wraps BGEReranker for the unified backend protocol.
  3 +"""
  4 +
  5 +from __future__ import annotations
  6 +
  7 +from typing import Any, Dict, List
  8 +
  9 +from reranker.bge_reranker import BGEReranker
  10 +
  11 +
  12 +class BGERerankerBackend:
  13 + """BGE reranker backend; config from services.rerank.backends.bge."""
  14 +
  15 + def __init__(self, config: Dict[str, Any]) -> None:
  16 + self._config = config or {}
  17 + self._impl = BGEReranker(
  18 + model_name=str(self._config.get("model_name") or "BAAI/bge-reranker-v2-m3"),
  19 + device=self._config.get("device"),
  20 + batch_size=int(self._config.get("batch_size", 64)),
  21 + use_fp16=bool(self._config.get("use_fp16", True)),
  22 + max_length=int(self._config.get("max_length", 512)),
  23 + cache_dir=str(self._config.get("cache_dir") or "./model_cache"),
  24 + enable_warmup=bool(self._config.get("enable_warmup", True)),
  25 + )
  26 +
  27 + def score_with_meta(
  28 + self,
  29 + query: str,
  30 + docs: List[str],
  31 + normalize: bool = True,
  32 + ) -> tuple[list[float], Dict[str, Any]]:
  33 + return self._impl.score_with_meta(query, docs, normalize=normalize)
... ...
reranker/backends/qwen3_vllm.py 0 → 100644
... ... @@ -0,0 +1,213 @@
  1 +"""
  2 +Qwen3-Reranker-0.6B backend using vLLM.
  3 +
  4 +Reference: https://huggingface.co/Qwen/Qwen3-Reranker-0.6B
  5 +Requires: vllm>=0.8.5, transformers; GPU recommended.
  6 +"""
  7 +
  8 +from __future__ import annotations
  9 +
  10 +import logging
  11 +import math
  12 +import time
  13 +from typing import Any, Dict, List, Optional, Tuple
  14 +
  15 +logger = logging.getLogger("reranker.backends.qwen3_vllm")
  16 +
  17 +try:
  18 + import torch
  19 + from transformers import AutoTokenizer
  20 + from vllm import LLM, SamplingParams
  21 + from vllm.inputs import TokensPrompt
  22 +except ImportError as e:
  23 + raise ImportError(
  24 + "Qwen3-vLLM reranker backend requires vllm>=0.8.5 and transformers. "
  25 + "Install with: pip install vllm transformers"
  26 + ) from e
  27 +
  28 +
  29 +def _format_instruction(instruction: str, query: str, doc: str) -> List[Dict[str, str]]:
  30 + """Build chat messages for one (query, doc) pair."""
  31 + return [
  32 + {
  33 + "role": "system",
  34 + "content": "Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\".",
  35 + },
  36 + {
  37 + "role": "user",
  38 + "content": f"<Instruct>: {instruction}\n\n<Query>: {query}\n\n<Document>: {doc}",
  39 + },
  40 + ]
  41 +
  42 +
  43 +class Qwen3VLLMRerankerBackend:
  44 + """
  45 + Qwen3-Reranker-0.6B with vLLM inference.
  46 + Config from services.rerank.backends.qwen3_vllm.
  47 + """
  48 +
  49 + def __init__(self, config: Dict[str, Any]) -> None:
  50 + self._config = config or {}
  51 + model_name = str(self._config.get("model_name") or "Qwen/Qwen3-Reranker-0.6B")
  52 + max_model_len = int(self._config.get("max_model_len", 8192))
  53 + tensor_parallel_size = int(self._config.get("tensor_parallel_size", 1))
  54 + gpu_memory_utilization = float(self._config.get("gpu_memory_utilization", 0.8))
  55 + enable_prefix_caching = bool(self._config.get("enable_prefix_caching", True))
  56 + self._instruction = str(
  57 + self._config.get("instruction")
  58 + or "Given a web search query, retrieve relevant passages that answer the query"
  59 + )
  60 +
  61 + logger.info(
  62 + "[Qwen3_VLLM] Loading model %s (max_model_len=%s, tp=%s, prefix_caching=%s)",
  63 + model_name,
  64 + max_model_len,
  65 + tensor_parallel_size,
  66 + enable_prefix_caching,
  67 + )
  68 +
  69 + self._llm = LLM(
  70 + model=model_name,
  71 + tensor_parallel_size=tensor_parallel_size,
  72 + max_model_len=max_model_len,
  73 + gpu_memory_utilization=gpu_memory_utilization,
  74 + enable_prefix_caching=enable_prefix_caching,
  75 + )
  76 + self._tokenizer = AutoTokenizer.from_pretrained(model_name)
  77 + self._tokenizer.padding_side = "left"
  78 + self._tokenizer.pad_token = self._tokenizer.eos_token
  79 +
  80 + # Suffix for generation prompt (assistant answer)
  81 + self._suffix = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
  82 + self._suffix_tokens = self._tokenizer.encode(
  83 + self._suffix, add_special_tokens=False
  84 + )
  85 + self._max_prompt_len = max_model_len - len(self._suffix_tokens)
  86 +
  87 + self._true_token = self._tokenizer("yes", add_special_tokens=False).input_ids[0]
  88 + self._false_token = self._tokenizer("no", add_special_tokens=False).input_ids[0]
  89 + self._sampling_params = SamplingParams(
  90 + temperature=0,
  91 + max_tokens=1,
  92 + logprobs=20,
  93 + allowed_token_ids=[self._true_token, self._false_token],
  94 + )
  95 +
  96 + self._model_name = model_name
  97 + logger.info("[Qwen3_VLLM] Model ready | model=%s", model_name)
  98 +
  99 + def _process_inputs(
  100 + self,
  101 + pairs: List[Tuple[str, str]],
  102 + ) -> List[TokensPrompt]:
  103 + """Build tokenized prompts for vLLM from (query, doc) pairs."""
  104 + prompts = []
  105 + for q, d in pairs:
  106 + messages = _format_instruction(self._instruction, q, d)
  107 + # One conversation per call (apply_chat_template expects single conversation)
  108 + token_ids = self._tokenizer.apply_chat_template(
  109 + messages,
  110 + tokenize=True,
  111 + add_generation_prompt=False,
  112 + enable_thinking=False,
  113 + )
  114 + if isinstance(token_ids, list) and token_ids and isinstance(token_ids[0], list):
  115 + token_ids = token_ids[0]
  116 + ids = token_ids[: self._max_prompt_len] + self._suffix_tokens
  117 + prompts.append(TokensPrompt(prompt_token_ids=ids))
  118 + return prompts
  119 +
  120 + def _compute_scores(
  121 + self,
  122 + prompts: List[TokensPrompt],
  123 + ) -> List[float]:
  124 + """Run vLLM generate and compute yes/no probability per prompt."""
  125 + if not prompts:
  126 + return []
  127 + outputs = self._llm.generate(prompts, self._sampling_params, use_tqdm=False)
  128 + scores = []
  129 + for i in range(len(outputs)):
  130 + out = outputs[i]
  131 + if not out.outputs:
  132 + scores.append(0.0)
  133 + continue
  134 + logprobs = out.outputs[0].logprobs
  135 + if not logprobs:
  136 + scores.append(0.0)
  137 + continue
  138 + last = logprobs[-1]
  139 + true_logp = last.get(self._true_token)
  140 + false_logp = last.get(self._false_token)
  141 + true_p = math.exp(true_logp.logprob) if true_logp else 1e-10
  142 + false_p = math.exp(false_logp.logprob) if false_logp else 1e-10
  143 + score = true_p / (true_p + false_p)
  144 + scores.append(float(score))
  145 + return scores
  146 +
  147 + def score_with_meta(
  148 + self,
  149 + query: str,
  150 + docs: List[str],
  151 + normalize: bool = True,
  152 + ) -> Tuple[List[float], Dict[str, Any]]:
  153 + start_ts = time.time()
  154 + total_docs = len(docs) if docs else 0
  155 + output_scores: List[float] = [0.0] * total_docs
  156 +
  157 + query = "" if query is None else str(query).strip()
  158 + indexed: List[Tuple[int, str]] = []
  159 + for i, doc in enumerate(docs or []):
  160 + if doc is None:
  161 + continue
  162 + text = str(doc).strip()
  163 + if not text:
  164 + continue
  165 + indexed.append((i, text))
  166 +
  167 + if not query or not indexed:
  168 + elapsed_ms = (time.time() - start_ts) * 1000.0
  169 + return output_scores, {
  170 + "input_docs": total_docs,
  171 + "usable_docs": len(indexed),
  172 + "unique_docs": 0,
  173 + "dedup_ratio": 0.0,
  174 + "elapsed_ms": round(elapsed_ms, 3),
  175 + "model": self._model_name,
  176 + "backend": "qwen3_vllm",
  177 + "normalize": normalize,
  178 + }
  179 +
  180 + # Deduplicate by text, keep mapping to original indices
  181 + unique_texts: List[str] = []
  182 + position_to_unique: List[int] = []
  183 + prev: Optional[str] = None
  184 + for _idx, text in indexed:
  185 + if text != prev:
  186 + unique_texts.append(text)
  187 + prev = text
  188 + position_to_unique.append(len(unique_texts) - 1)
  189 +
  190 + pairs = [(query, t) for t in unique_texts]
  191 + prompts = self._process_inputs(pairs)
  192 + unique_scores = self._compute_scores(prompts)
  193 +
  194 + for (orig_idx, _), unique_idx in zip(indexed, position_to_unique):
  195 + # Score is already P(yes) in [0,1] from yes/(yes+no)
  196 + output_scores[orig_idx] = float(unique_scores[unique_idx])
  197 +
  198 + elapsed_ms = (time.time() - start_ts) * 1000.0
  199 + dedup_ratio = 0.0
  200 + if indexed:
  201 + dedup_ratio = 1.0 - (len(unique_texts) / float(len(indexed)))
  202 +
  203 + meta = {
  204 + "input_docs": total_docs,
  205 + "usable_docs": len(indexed),
  206 + "unique_docs": len(unique_texts),
  207 + "dedup_ratio": round(dedup_ratio, 4),
  208 + "elapsed_ms": round(elapsed_ms, 3),
  209 + "model": self._model_name,
  210 + "backend": "qwen3_vllm",
  211 + "normalize": normalize,
  212 + }
  213 + return output_scores, meta
... ...
reranker/server.py
1 1 """
2   -FastAPI service for BGE reranking.
  2 +Reranker service - unified /rerank API backed by pluggable backends (BGE, Qwen3-vLLM).
3 3  
4 4 POST /rerank
5   -Request:
6   -{
7   - "query": "...",
8   - "docs": ["doc1", "doc2", ...]
9   -}
10   -
11   -Response:
12   -{
13   - "scores": [0.98, 0.12, ...],
14   - "meta": {...}
15   -}
  5 +Request: { "query": "...", "docs": ["doc1", "doc2", ...], "normalize": optional bool }
  6 +Response: { "scores": [float], "meta": {...} }
  7 +
  8 +Backend selected via config: services.rerank.backend (bge | qwen3_vllm), env RERANK_BACKEND.
16 9 """
17 10  
18 11 import logging
... ... @@ -22,7 +15,8 @@ from typing import Any, Dict, List, Optional
22 15 from fastapi import FastAPI, HTTPException
23 16 from pydantic import BaseModel, Field
24 17  
25   -from reranker.bge_reranker import BGEReranker
  18 +from config.services_config import get_rerank_backend_config
  19 +from reranker.backends import RerankBackendProtocol, get_rerank_backend
26 20 from reranker.config import CONFIG
27 21  
28 22 logging.basicConfig(
... ... @@ -33,7 +27,8 @@ logger = logging.getLogger(&quot;reranker.service&quot;)
33 27  
34 28 app = FastAPI(title="saas-search Reranker Service", version="1.0.0")
35 29  
36   -_reranker: Optional[BGEReranker] = None
  30 +_reranker: Optional[RerankBackendProtocol] = None
  31 +_backend_name: str = ""
37 32  
38 33  
39 34 class RerankRequest(BaseModel):
... ... @@ -51,25 +46,17 @@ class RerankResponse(BaseModel):
51 46  
52 47 @app.on_event("startup")
53 48 def load_model() -> None:
54   - global _reranker
  49 + global _reranker, _backend_name
55 50 logger.info("Starting reranker service on port %s", CONFIG.PORT)
56 51 try:
57   - _reranker = BGEReranker(
58   - model_name=CONFIG.MODEL_NAME,
59   - device=CONFIG.DEVICE,
60   - batch_size=CONFIG.BATCH_SIZE,
61   - use_fp16=CONFIG.USE_FP16,
62   - max_length=CONFIG.MAX_LENGTH,
63   - cache_dir=CONFIG.CACHE_DIR,
64   - enable_warmup=CONFIG.ENABLE_WARMUP,
65   - )
  52 + backend_name, backend_cfg = get_rerank_backend_config()
  53 + _backend_name = backend_name
  54 + _reranker = get_rerank_backend(backend_name, backend_cfg)
  55 + model_info = getattr(_reranker, "_model_name", None) or backend_cfg.get("model_name", backend_name)
66 56 logger.info(
67   - "Reranker ready | model=%s device=%s fp16=%s batch=%s max_len=%s",
68   - CONFIG.MODEL_NAME,
69   - _reranker.device,
70   - _reranker.use_fp16,
71   - _reranker.batch_size,
72   - _reranker.max_length,
  57 + "Reranker ready | backend=%s model=%s",
  58 + _backend_name,
  59 + model_info,
73 60 )
74 61 except Exception as exc:
75 62 logger.error("Failed to initialize reranker: %s", exc, exc_info=True)
... ... @@ -78,11 +65,16 @@ def load_model() -&gt; None:
78 65  
79 66 @app.get("/health")
80 67 def health() -> Dict[str, Any]:
  68 + model_info = ""
  69 + if _reranker is not None:
  70 + model_info = getattr(_reranker, "_model_name", None) or getattr(
  71 + _reranker, "_config", {}
  72 + ).get("model_name", _backend_name)
81 73 return {
82 74 "status": "ok" if _reranker is not None else "unavailable",
83 75 "model_loaded": _reranker is not None,
84   - "model": CONFIG.MODEL_NAME,
85   - "device": CONFIG.DEVICE,
  76 + "model": model_info,
  77 + "backend": _backend_name,
86 78 }
87 79  
88 80  
... ...