Commit 701ae503e50ab97ebbcf87a5171c2e5a968cc4c1
1 parent
c10f90fe
docs
Showing
14 changed files
with
1046 additions
and
78 deletions
Show diff stats
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.""" | ... | ... |
| ... | ... | @@ -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 | +*本文档旨在让所有后续开发在预知框架全貌的前提下,在规范内迭代,减少分叉与冗余,提升可维护性。如有结构或规范变更,请同步更新本指南及相关专项文档。* | ... | ... |
| ... | ... | @@ -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
docs/QUICKSTART.md
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 前缀缓存)。 | ... | ... |
| ... | ... | @@ -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"] | ... | ... |
| ... | ... | @@ -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) | ... | ... |
| ... | ... | @@ -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("reranker.service") |
| 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() -> 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 | ... | ... |