Commit 701ae503e50ab97ebbcf87a5171c2e5a968cc4c1

Authored by tangwang
1 parent c10f90fe

docs

@@ -125,14 +125,17 @@ python scripts/recreate_and_import.py \ @@ -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 | 1. 环境与启动 | `docs/QUICKSTART.md` | 135 | 1. 环境与启动 | `docs/QUICKSTART.md` |
133 | 2. 搜索/索引 API | `docs/QUICKSTART.md` §3、`docs/搜索API速查表.md` | 136 | 2. 搜索/索引 API | `docs/QUICKSTART.md` §3、`docs/搜索API速查表.md` |
134 | 3. 运维与故障 | `docs/Usage-Guide.md` | 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 ### Runtimes & 命令示例 140 ### Runtimes & 命令示例
138 141
@@ -172,11 +175,13 @@ curl -X POST http://localhost:6002/search/ \ @@ -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 | `docs/Usage-Guide.md` | 运维:日志、多环境、故障排查 | 180 | `docs/Usage-Guide.md` | 运维:日志、多环境、故障排查 |
177 | `docs/搜索API速查表.md` | 搜索 API 参数速查 | 181 | `docs/搜索API速查表.md` | 搜索 API 参数速查 |
178 | `docs/搜索API对接指南.md` | 搜索 API 完整说明 | 182 | `docs/搜索API对接指南.md` | 搜索 API 完整说明 |
179 | `docs/PROVIDER_ARCHITECTURE.md` | 翻译/向量/重排 provider 扩展 | 183 | `docs/PROVIDER_ARCHITECTURE.md` | 翻译/向量/重排 provider 扩展 |
  184 +| `docs/MODULE_EXTENSION_SPEC.md` | 向量/重排后端可插拔规范 |
180 | `docs/环境配置说明.md` | 首次部署、新机器环境 | 185 | `docs/环境配置说明.md` | 首次部署、新机器环境 |
181 | `docs/系统设计文档.md` | 架构与模块细节 | 186 | `docs/系统设计文档.md` | 架构与模块细节 |
182 187
config/__init__.py
@@ -30,6 +30,7 @@ from .services_config import ( @@ -30,6 +30,7 @@ from .services_config import (
30 get_translation_config, 30 get_translation_config,
31 get_embedding_config, 31 get_embedding_config,
32 get_rerank_config, 32 get_rerank_config,
  33 + get_rerank_backend_config,
33 get_translation_base_url, 34 get_translation_base_url,
34 get_embedding_base_url, 35 get_embedding_base_url,
35 get_rerank_service_url, 36 get_rerank_service_url,
@@ -58,6 +59,7 @@ __all__ = [ @@ -58,6 +59,7 @@ __all__ = [
58 'get_translation_config', 59 'get_translation_config',
59 'get_embedding_config', 60 'get_embedding_config',
60 'get_rerank_config', 61 'get_rerank_config',
  62 + 'get_rerank_backend_config',
61 'get_translation_base_url', 63 'get_translation_base_url',
62 'get_embedding_base_url', 64 'get_embedding_base_url',
63 'get_rerank_service_url', 65 'get_rerank_service_url',
config/config.yaml
@@ -173,16 +173,31 @@ services: @@ -173,16 +173,31 @@ services:
173 model: "" 173 model: ""
174 note: "reserved for future vLLM embedding backend" 174 note: "reserved for future vLLM embedding backend"
175 rerank: 175 rerank:
176 - provider: "http" # http | vllm(reserved) 176 + provider: "http"
177 base_url: "http://127.0.0.1:6007" 177 base_url: "http://127.0.0.1:6007"
178 providers: 178 providers:
179 http: 179 http:
180 base_url: "http://127.0.0.1:6007" 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 # SPU配置(已启用,使用嵌套skus) 202 # SPU配置(已启用,使用嵌套skus)
188 spu_config: 203 spu_config:
config/services_config.py
@@ -112,6 +112,25 @@ def _resolve_rerank() -> ServiceConfig: @@ -112,6 +112,25 @@ def _resolve_rerank() -> ServiceConfig:
112 return ServiceConfig(provider=provider, providers=providers) 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 @lru_cache(maxsize=1) 134 @lru_cache(maxsize=1)
116 def get_translation_config() -> ServiceConfig: 135 def get_translation_config() -> ServiceConfig:
117 """Get translation service config.""" 136 """Get translation service config."""
docs/DEVELOPER_GUIDE.md 0 → 100644
@@ -0,0 +1,386 @@ @@ -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 @@ @@ -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,6 +2,8 @@
2 2
3 本文档说明如何统一管理翻译、向量化、重排等“能力提供者(provider)”。 3 本文档说明如何统一管理翻译、向量化、重排等“能力提供者(provider)”。
4 4
  5 +**扩展重排/向量化后端(如新增 Qwen3-Reranker、vLLM 等)**:请同时参阅 [模块扩展规范(MODULE_EXTENSION_SPEC.md)](./MODULE_EXTENSION_SPEC.md),其中定义了服务内后端协议、统一配置结构与可插拔实现方式。
  6 +
5 ## 1. 设计目标 7 ## 1. 设计目标
6 8
7 - **调用方稳定**:业务代码不关心具体供应商,只调用统一接口。 9 - **调用方稳定**:业务代码不关心具体供应商,只调用统一接口。
docs/QUICKSTART.md
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 2
3 新人入口文档:环境、服务、模块、请求示例一页搞定。 3 新人入口文档:环境、服务、模块、请求示例一页搞定。
4 4
  5 +**建议**:首次参与开发请先阅读 [DEVELOPER_GUIDE.md](./DEVELOPER_GUIDE.md) 建立项目全貌与规范,再使用本页做环境与请求速查。
  6 +
5 ## 1. 环境 7 ## 1. 环境
6 8
7 ```bash 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 # Install when you need: 3 # Install when you need:
4 # - `./scripts/start_embedding_service.sh` (local embeddings server) 4 # - `./scripts/start_embedding_service.sh` (local embeddings server)
5 # - local BGE-M3 / CN-CLIP inference 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 # Notes: 12 # Notes:
8 # - `torch` wheels can be very large; if you want CPU-only wheels, 13 # - `torch` wheels can be very large; if you want CPU-only wheels,
reranker/README.md
1 # Reranker 模块 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 ```bash 59 ```bash
40 uvicorn reranker.server:app --host 0.0.0.0 --port 6007 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 ## API 64 ## API
44 ### Health 65 ### Health
45 ``` 66 ```
46 GET /health 67 GET /health
47 ``` 68 ```
  69 +Response 含 `backend`(当前后端名)、`model`、`model_loaded`、`status`。
48 70
49 ### Rerank 71 ### Rerank
50 ``` 72 ```
@@ -86,6 +108,6 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info @@ -86,6 +108,6 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info
86 ``` 108 ```
87 109
88 ## Notes 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 @@ @@ -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 @@ @@ -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 @@ @@ -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 POST /rerank 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 import logging 11 import logging
@@ -22,7 +15,8 @@ from typing import Any, Dict, List, Optional @@ -22,7 +15,8 @@ from typing import Any, Dict, List, Optional
22 from fastapi import FastAPI, HTTPException 15 from fastapi import FastAPI, HTTPException
23 from pydantic import BaseModel, Field 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 from reranker.config import CONFIG 20 from reranker.config import CONFIG
27 21
28 logging.basicConfig( 22 logging.basicConfig(
@@ -33,7 +27,8 @@ logger = logging.getLogger(&quot;reranker.service&quot;) @@ -33,7 +27,8 @@ logger = logging.getLogger(&quot;reranker.service&quot;)
33 27
34 app = FastAPI(title="saas-search Reranker Service", version="1.0.0") 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 class RerankRequest(BaseModel): 34 class RerankRequest(BaseModel):
@@ -51,25 +46,17 @@ class RerankResponse(BaseModel): @@ -51,25 +46,17 @@ class RerankResponse(BaseModel):
51 46
52 @app.on_event("startup") 47 @app.on_event("startup")
53 def load_model() -> None: 48 def load_model() -> None:
54 - global _reranker 49 + global _reranker, _backend_name
55 logger.info("Starting reranker service on port %s", CONFIG.PORT) 50 logger.info("Starting reranker service on port %s", CONFIG.PORT)
56 try: 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 logger.info( 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 except Exception as exc: 61 except Exception as exc:
75 logger.error("Failed to initialize reranker: %s", exc, exc_info=True) 62 logger.error("Failed to initialize reranker: %s", exc, exc_info=True)
@@ -78,11 +65,16 @@ def load_model() -&gt; None: @@ -78,11 +65,16 @@ def load_model() -&gt; None:
78 65
79 @app.get("/health") 66 @app.get("/health")
80 def health() -> Dict[str, Any]: 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 return { 73 return {
82 "status": "ok" if _reranker is not None else "unavailable", 74 "status": "ok" if _reranker is not None else "unavailable",
83 "model_loaded": _reranker is not None, 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