From 701ae503e50ab97ebbcf87a5171c2e5a968cc4c1 Mon Sep 17 00:00:00 2001 From: tangwang Date: Sun, 8 Mar 2026 14:30:07 +0800 Subject: [PATCH] docs --- README.md | 11 ++++++++--- config/__init__.py | 2 ++ config/config.yaml | 27 +++++++++++++++++++++------ config/services_config.py | 19 +++++++++++++++++++ docs/DEVELOPER_GUIDE.md | 386 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/MODULE_EXTENSION_SPEC.md | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/PROVIDER_ARCHITECTURE.md | 2 ++ docs/QUICKSTART.md | 2 ++ requirements_ml.txt | 7 ++++++- reranker/README.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------ reranker/backends/__init__.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ reranker/backends/bge.py | 33 +++++++++++++++++++++++++++++++++ reranker/backends/qwen3_vllm.py | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ reranker/server.py | 56 ++++++++++++++++++++++++-------------------------------- 14 files changed, 1046 insertions(+), 78 deletions(-) create mode 100644 docs/DEVELOPER_GUIDE.md create mode 100644 docs/MODULE_EXTENSION_SPEC.md create mode 100644 reranker/backends/__init__.py create mode 100644 reranker/backends/bge.py create mode 100644 reranker/backends/qwen3_vllm.py diff --git a/README.md b/README.md index 00fa986..b9b4aeb 100644 --- a/README.md +++ b/README.md @@ -125,14 +125,17 @@ python scripts/recreate_and_import.py \ ## 新人入口 -**→ `docs/QUICKSTART.md`**:环境、服务、模块、请求示例一页搞定。 +**→ 开发者必读**:[docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) — 项目全貌、设计原则、扩展规范与迭代检查清单,保证后续开发在统一框架内进行。 + +**→ 快速上手**:[docs/QUICKSTART.md](docs/QUICKSTART.md) — 环境、服务、模块、请求示例一页搞定。 | 步骤 | 文档 | |------|------| +| 0. 框架与规范(推荐首读) | `docs/DEVELOPER_GUIDE.md` | | 1. 环境与启动 | `docs/QUICKSTART.md` | | 2. 搜索/索引 API | `docs/QUICKSTART.md` §3、`docs/搜索API速查表.md` | | 3. 运维与故障 | `docs/Usage-Guide.md` | -| 4. 架构与扩展 | `docs/PROVIDER_ARCHITECTURE.md`、`docs/系统设计文档.md` | +| 4. 架构与扩展 | `docs/PROVIDER_ARCHITECTURE.md`、`docs/MODULE_EXTENSION_SPEC.md`、`docs/系统设计文档.md` | ### Runtimes & 命令示例 @@ -172,11 +175,13 @@ curl -X POST http://localhost:6002/search/ \ | 文档 | 用途 | |------|------| -| `docs/QUICKSTART.md` | **新人入口**:环境、服务、模块、请求 | +| `docs/DEVELOPER_GUIDE.md` | **开发者开放指南**:全貌、原则、规范、检查清单 | +| `docs/QUICKSTART.md` | 新人上手:环境、服务、模块、请求 | | `docs/Usage-Guide.md` | 运维:日志、多环境、故障排查 | | `docs/搜索API速查表.md` | 搜索 API 参数速查 | | `docs/搜索API对接指南.md` | 搜索 API 完整说明 | | `docs/PROVIDER_ARCHITECTURE.md` | 翻译/向量/重排 provider 扩展 | +| `docs/MODULE_EXTENSION_SPEC.md` | 向量/重排后端可插拔规范 | | `docs/环境配置说明.md` | 首次部署、新机器环境 | | `docs/系统设计文档.md` | 架构与模块细节 | diff --git a/config/__init__.py b/config/__init__.py index 3fcff16..ed7fc4e 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -30,6 +30,7 @@ from .services_config import ( get_translation_config, get_embedding_config, get_rerank_config, + get_rerank_backend_config, get_translation_base_url, get_embedding_base_url, get_rerank_service_url, @@ -58,6 +59,7 @@ __all__ = [ 'get_translation_config', 'get_embedding_config', 'get_rerank_config', + 'get_rerank_backend_config', 'get_translation_base_url', 'get_embedding_base_url', 'get_rerank_service_url', diff --git a/config/config.yaml b/config/config.yaml index 63857df..3182c84 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -173,16 +173,31 @@ services: model: "" note: "reserved for future vLLM embedding backend" rerank: - provider: "http" # http | vllm(reserved) + provider: "http" base_url: "http://127.0.0.1:6007" providers: http: base_url: "http://127.0.0.1:6007" - vllm: - enabled: false - base_url: "" - model: "" - note: "reserved for future vLLM reranker backend" + service_url: "http://127.0.0.1:6007/rerank" + # 服务内后端(reranker 进程启动时读取) + backend: "bge" # bge | qwen3_vllm + backends: + bge: + model_name: "BAAI/bge-reranker-v2-m3" + device: null + use_fp16: true + batch_size: 64 + max_length: 512 + cache_dir: "./model_cache" + enable_warmup: true + qwen3_vllm: + model_name: "Qwen/Qwen3-Reranker-0.6B" + engine: "vllm" + max_model_len: 8192 + tensor_parallel_size: 1 + gpu_memory_utilization: 0.8 + enable_prefix_caching: true + instruction: "Given a web search query, retrieve relevant passages that answer the query" # SPU配置(已启用,使用嵌套skus) spu_config: diff --git a/config/services_config.py b/config/services_config.py index e4b095f..9c9501b 100644 --- a/config/services_config.py +++ b/config/services_config.py @@ -112,6 +112,25 @@ def _resolve_rerank() -> ServiceConfig: return ServiceConfig(provider=provider, providers=providers) +def get_rerank_backend_config() -> tuple[str, dict]: + """ + Resolve reranker backend name and config for the reranker service process. + Returns (backend_name, backend_cfg). + Env RERANK_BACKEND overrides config. + """ + raw = _load_services_raw() + cfg = raw.get("rerank", {}) if isinstance(raw.get("rerank"), dict) else {} + backends = cfg.get("backends", {}) if isinstance(cfg.get("backends"), dict) else {} + name = ( + os.getenv("RERANK_BACKEND") + or cfg.get("backend") + or "bge" + ) + name = str(name).strip().lower() + backend_cfg = backends.get(name, {}) if isinstance(backends.get(name), dict) else {} + return name, backend_cfg + + @lru_cache(maxsize=1) def get_translation_config() -> ServiceConfig: """Get translation service config.""" diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..41e47c9 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,386 @@ +# 开发者开放指南 + +本文档面向**后续参与开发的工程师**,用于快速建立项目全貌、理解设计原则与规范,保证所有迭代在统一框架内进行,减少设计分叉与冗余代码,产出可持续继承的高质量代码。 + +**阅读建议**:首次请按顺序通读第一至第五节;扩展能力或新增模块时重点阅读第六、七节;日常开发与 Code Review 可依赖第八、九节。 + +--- + +## 目录 + +1. [文档说明与阅读路径](#1-文档说明与阅读路径) +2. [项目定位与核心价值](#2-项目定位与核心价值) +3. [总体架构](#3-总体架构) +4. [核心模块与职责](#4-核心模块与职责) +5. [设计原则与约束](#5-设计原则与约束) +6. [配置体系](#6-配置体系) +7. [扩展规范(能力与后端)](#7-扩展规范能力与后端) +8. [代码规范与质量](#8-代码规范与质量) +9. [迭代检查清单](#9-迭代检查清单) +10. [文档与资源索引](#10-文档与资源索引) + +--- + +## 1. 文档说明与阅读路径 + +### 1.1 本指南的角色 + +- **唯一入口**:新人应首先阅读本指南,建立“框架全貌 + 规范”的认知。 +- **规范聚合**:设计原则、配置约定、扩展方式、代码质量要求均在此汇总,并指向更细的专项文档。 +- **迭代约束**:所有新功能、新模块、重构都应在符合本指南的前提下进行,Code Review 时可对照第九节检查清单。 + +### 1.2 推荐阅读路径 + +| 阶段 | 阅读内容 | 目的 | +|------|----------|------| +| 入职/接手 | 本指南 §1–§5 | 建立全貌:项目是什么、架构怎样、模块边界 | +| 开发前 | 本指南 §5–§7 + 相关专项文档 | 理解原则与配置、扩展方式,避免造轮子与分叉 | +| 开发中 | 本指南 §8 + QUICKSTART / API 文档 | 编码风格、测试要求、接口约定 | +| 提测/合入前 | 本指南 §9 | 自检是否满足框架与规范 | + +### 1.3 与本指南配套的专项文档 + +以下文档由本指南引用,按需深入: + +- [QUICKSTART.md](./QUICKSTART.md) — 环境、服务、模块、请求示例 +- [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) — 翻译/向量/重排 Provider 架构与新增 Provider +- [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) — 向量化/重排后端可插拔设计、协议与配置 +- [系统设计文档.md](./系统设计文档.md) — 索引结构、数据流、通用化设计 +- [基础配置指南.md](./基础配置指南.md) — 索引与查询配置说明 +- [搜索API对接指南.md](./搜索API对接指南.md) — 搜索/索引/管理接口完整说明 +- [环境配置说明.md](./环境配置说明.md) — 首次部署、新机器环境 +- [Usage-Guide.md](./Usage-Guide.md) — 运维、日志、多环境、故障排查 + +--- + +## 2. 项目定位与核心价值 + +### 2.1 项目是什么 + +- **产品形态**:面向跨境独立站(如店匠 Shoplazza)的**多租户可配置搜索 SaaS**,提供搜索后端与索引富化能力。 +- **核心交付**: + - **搜索服务**:文本搜索、图片搜索、建议(suggestions)、过滤、分面、排序、可选重排。 + - **索引服务**:将 MySQL 中的店匠标准表(SPU/SKU)富化为符合 ES mapping 的文档(多语言、翻译、向量、规格聚合等),支持全量/增量及“仅构建 doc、由上游写 ES”的对接方式。 + - **支撑服务**:向量服务(embedding)、翻译服务(translator)、重排服务(reranker),可独立部署、通过配置切换。 + +### 2.2 核心价值与边界 + +- **多租户**:单套代码与索引结构,通过 `tenant_id` 隔离数据;租户级配置(如主语言、索引语言)由配置与 tenant_config 支持。 +- **可配置**:字段权重、搜索域、排序表达式、查询改写、功能开关等由配置驱动,避免硬编码业务逻辑。 +- **可扩展**:翻译/向量/重排采用 Provider + 后端可插拔设计,新增实现时遵循协议与配置规范,不破坏现有调用方。 +- **不负责**:商品主数据同步、店铺配置写库、全量/增量调度策略由上游(如 Java 索引程序)负责;本仓库专注“如何查、如何建 doc”。 + +--- + +## 3. 总体架构 + +### 3.1 数据流(简化) + +``` +MySQL (店匠 SPU/SKU) + → Indexer(富化:多语言、翻译、向量、规格聚合) + → Elasticsearch(按租户索引:search_products_tenant_) + → 搜索 API(QueryParser → Searcher,可选翻译/向量/重排) + → 前端 / 上游业务 +``` + +- **索引侧**:Java 或脚本决定“对哪些 SPU 做索引”;Python indexer 负责“单条/批量 SPU → ES 文档”的完整逻辑,或通过 `/indexer/build-docs` 仅返回 doc、由调用方写 ES。 +- **搜索侧**:请求经 QueryParser(解析、改写、翻译、向量化)→ Searcher(ES 查询、可选重排)→ 结果格式化 → 返回。 + +### 3.2 服务拓扑与端口 + +| 服务 | 端口 | 说明 | 默认随 run.sh 启动 | +|------|------|------|--------------------| +| backend | 6002 | 搜索 API(含 admin) | ✓ | +| indexer | 6004 | 索引 API(reindex/build-docs 等) | ✓ | +| frontend | 6003 | 调试 UI | ✓ | +| embedding | 6005 | 向量服务(文本/图片) | 可选 | +| translator | 6006 | 翻译服务 | 可选 | +| reranker | 6007 | 重排服务 | 可选 | + +- 启动:`./run.sh` 仅启动 backend / indexer / frontend;需全功能时通过环境变量或脚本另行启动 embedding / translator / reranker。 +- 停止:统一使用 `./scripts/stop.sh`(会停止上述所有端口上的进程)。 +- 详见 [QUICKSTART.md](./QUICKSTART.md) 与 [Usage-Guide.md](./Usage-Guide.md)。 + +### 3.3 仓库目录结构(与架构对应) + +``` +api/ # FastAPI 应用:搜索路由、管理路由、索引路由(indexer_app) +config/ # 配置加载与解析:config.yaml、services、env +indexer/ # MySQL → ES 管道:mapping、transformer、bulk、增量、build-docs +query/ # 查询解析:规范化、改写、翻译、embedding 调用、布尔解析 +search/ # 搜索执行:多语言查询构建、Searcher、重排客户端、分数融合 +embeddings/ # 向量化:服务端(server)、文本/图像后端、协议与配置 +reranker/ # 重排:服务端(server)、后端(backends)、配置 +providers/ # 能力提供者:翻译/向量/重排的客户端抽象与工厂 +suggestion/ # 建议:索引构建、建议检索 +utils/ # 共享工具:ES 客户端、DB 连接等 +mappings/ # ES 索引 mapping 定义(如 search_products.json) +scripts/ # 脚本:环境、服务启停、数据、运维 +frontend/ # 调试用前端静态资源 +tests/ # 单元与集成测试 +docs/ # 文档(含本指南) +``` + +- **约定**:业务逻辑按能力放入对应顶层包;新增“能力”时优先考虑是否属于现有某包或 providers,避免随意新建顶层包导致分叉。 + +--- + +## 4. 核心模块与职责 + +### 4.1 api + +- **职责**:对外 HTTP 入口;挂载搜索、管理、索引等路由;中间件(限流、CORS、安全头等);不承载具体搜索/索引算法。 +- **入口**:`api/app.py`(搜索 + 管理)、`api/indexer_app.py`(索引),均由 `main.py` 的 `serve` / `serve-indexer` 启动。 +- **原则**:路由层只做参数校验、租户解析、调用 search/query/indexer 等模块,不写复杂业务逻辑;配置与能力访问通过 config 与 providers 统一获取。 + +### 4.2 config + +- **职责**:加载与解析 `config/config.yaml`(搜索行为、字段权重、分面、function_score、rerank 融合参数等);提供 `ConfigLoader` 与 `SearchConfig` 等数据结构;**服务级**配置(翻译/向量/重排的 provider、URL、后端)由 `config/services_config.py` 从 `config.yaml` 的 `services` 块及环境变量解析。 +- **原则**:索引结构由 `mappings/search_products.json` 定义;搜索行为与能力配置以 config 为主、环境变量覆盖,不在业务代码中散落硬编码。 + +### 4.3 indexer + +- **职责**:将 MySQL 行或上游传入的 SPU/SKU/options 转为符合 `mappings/search_products.json` 的 ES 文档;含多语言组织、翻译调用、向量生成、规格/SKU 聚合、类目路径等;支持全量/增量写入 ES,以及仅返回 doc(build-docs)供上游写 ES。 +- **对接**:调用方通过 `providers` 获取翻译、向量等能力;索引名通过 `indexer/mapping_generator.get_tenant_index_name(tenant_id)` 与 `ES_INDEX_NAMESPACE` 一致。 +- **详见**:`indexer/README.md`、[系统设计文档.md](./系统设计文档.md)。 + +### 4.4 query + +- **职责**:查询解析与预处理:规范化、语言检测、改写(词典)、翻译、文本向量化、布尔表达式解析;输出可供 Searcher 使用的结构化查询信息。 +- **原则**:翻译/向量通过 `providers` 获取,不直接依赖具体服务 URL 或实现;支持按配置关闭翻译/向量(如短查询、typing 场景)。 + +### 4.5 search + +- **职责**:构建多语言 ES 查询、执行检索、可选重排、分数融合、结果格式化;分面、过滤、排序、SKU 维度筛选等。 +- **原则**:重排通过 `search/rerank_client.py` 调用 `create_rerank_provider()`,不关心重排服务内是 BGE 还是 Qwen3;与 ES 的交互封装在 Searcher 内,便于 mock 与测试。 + +### 4.6 embeddings + +- **职责**:提供向量服务(FastAPI):`POST /embed/text`、`POST /embed/image`;服务内按配置加载文本后端(如 BGE)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。 +- **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;配置优先从 `config` 或 `embeddings/config.py` 读取,与 `services.embedding` 的 URL 分离。 +- **详见**:`embeddings/README.md`、[MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。 + +### 4.7 reranker + +- **职责**:提供重排服务(FastAPI):`POST /rerank`(query + docs → scores);服务内按配置加载一个重排后端(如 BGE 或 Qwen3-vLLM),实现 `score_with_meta(query, docs, normalize)` 协议。 +- **原则**:对外 HTTP 契约固定;新增后端只在 `reranker/backends` 中实现协议并注册,不修改调用方。 +- **详见**:[MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。 + +### 4.8 providers + +- **职责**:统一“能力”的调用方式:翻译、向量、重排均通过工厂函数(如 `create_translation_provider()`、`create_rerank_provider()`、`create_embedding_provider()`)获取实现,配置来自 `config/services_config`(即 `config.yaml` 的 `services` + 环境变量)。 +- **原则**:业务代码只依赖 Provider 接口,不依赖具体 URL 或后端类型;新增调用方式(如新 Provider 类型)在对应 `providers/.py` 中实现并在工厂中注册。 +- **详见**:[PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md)。 + +### 4.9 suggestion + +- **职责**:建议索引的构建与检索:从 ES 商品索引与 MySQL 日志等构建 suggestion 索引;搜索 API 的 `/search/suggestions` 使用本模块。 +- **原则**:索引命名与租户、环境命名空间一致;构建入口可通过 `main.py build-suggestions` 或脚本封装调用。 + +### 4.10 utils / mappings + +- **utils**:ES 客户端、DB 连接等通用工具;避免在业务包内重复实现。 +- **mappings**:ES 索引 mapping 的 JSON 定义;所有租户共享同一结构,仅索引名按租户与环境区分。 + +--- + +## 5. 设计原则与约束 + +### 5.1 多租户 + +- 数据隔离仅通过 `tenant_id` 实现;索引可为单索引多租户或 per-tenant 索引(如 `search_products_tenant_`),由索引名与查询时 filter 统一保证。 +- 租户级配置(主语言、索引语言等)从 `tenant_config` 或等价配置读取,不在代码中写死租户 ID 或店铺逻辑。 + +### 5.2 配置驱动 + +- 搜索行为(字段权重、搜索域、排序、function_score、重排融合参数等)来自 `config/config.yaml`,由 `ConfigLoader` 加载。 +- 能力访问(翻译/向量/重排的 provider、URL、后端类型)来自 `config.yaml` 的 `services` 块及环境变量,由 `config/services_config` 解析。 +- 新增开关或参数时,优先在现有 config 结构下扩展,避免新增散落配置文件。 + +### 5.3 单一配置源与优先级 + +- 同一类配置只在一个地方定义默认值;覆盖顺序约定为:**环境变量 > config 文件**。 +- 服务 URL、后端类型等均在 `services.` 下配置;环境变量用于部署态覆盖(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`)。 + +### 5.4 调用方与实现解耦(Provider + Backend) + +- **调用方**:通过 Provider(如 `HttpRerankProvider`)访问能力,不依赖具体 URL 或服务内实现。 +- **服务内**:通过“后端”实现具体推理(如 BGE 与 Qwen3-vLLM);后端实现协议、在配置与工厂中注册即可插拔。 +- 新增“一种调用方式”在 providers 中扩展;新增“一种推理实现”在对应服务的 backends 中扩展,并遵循 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。 + +### 5.5 协议契约 + +- 同类型后端实现同一协议(如重排的 `score_with_meta`、图片的 `ImageEncoderProtocol`);调用方只依赖协议,不依赖具体类名或实现细节。 +- 新增后端时必须满足现有协议(输入输出、顺序、长度、meta 字段等),避免调用方为兼容新后端而改代码。 + +### 5.6 索引与查询结构统一 + +- 索引结构以 `mappings/search_products.json` 为唯一来源;indexer 产出的 doc 必须与该 mapping 一致。 +- 查询侧使用的字段名、多语言后缀(.zh/.en)、嵌套路径等与 mapping 保持一致;新增字段时同步更新 mapping 与查询/分面/过滤逻辑。 + +### 5.7 错误与降级 + +- 外部能力(翻译、向量、重排)调用失败时,应有明确降级策略(如跳过向量、仅用 BM25、重排失败时保留 ES 顺序),并打日志便于排查;不因单一能力不可用导致整请求失败。 + +--- + +## 6. 配置体系 + +### 6.1 主配置文件 + +- **config/config.yaml**:搜索行为(field_boosts、indexes、query_config、ranking、function_score、rerank 融合参数)、SPU 配置、**services**(翻译/向量/重排的 provider 与 backends)、tenant_config 等。 +- **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。 + +### 6.2 services 块结构(能力统一约定) + +```yaml +services: + : + provider: "http" # 调用方使用方式:http | direct | ... + base_url: "http://..." + providers: + http: { base_url: "...", ... } + direct: { ... } + backend: "bge" # 服务内后端(可选) + backends: + bge: { model_name: "...", ... } + qwen3_vllm: { ... } +``` + +- **provider**:调用方如何访问(如 HTTP)。 +- **backend / backends**:当能力由本仓库内服务提供时,该服务加载哪个后端及参数。 +- 解析入口:`config/services_config.py` 的 `get_*_config()` 及 `get_*_base_url()` / `get_rerank_service_url()` 等。 + +### 6.3 环境变量(常用) + +- 能力 URL:`TRANSLATION_SERVICE_URL`、`EMBEDDING_SERVICE_URL`、`RERANKER_SERVICE_URL` +- 能力选择:`TRANSLATION_PROVIDER`、`EMBEDDING_PROVIDER`、`RERANK_PROVIDER`、`RERANK_BACKEND` +- 环境与索引:`ES_HOST`、`ES_INDEX_NAMESPACE`、`RUNTIME_ENV`、DB 与 Redis 等 + +详见 [环境配置说明.md](./环境配置说明.md)、[Usage-Guide.md](./Usage-Guide.md)。 + +--- + +## 7. 扩展规范(能力与后端) + +### 7.1 何时看扩展规范 + +- 新增或替换**翻译/向量/重排**的调用方式(如新的 HTTP 客户端、gRPC):见 [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md)。 +- 新增或替换**向量/重排**的推理实现(如新模型、vLLM):见 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md)。 + +### 7.2 新增 Provider(调用方式) + +1. 在 `providers/.py` 中实现新类(与现有 Provider 同接口)。 +2. 在 `create_*_provider()` 中按 `config.provider` 或环境变量增加分支。 +3. 在 `config/config.yaml` 的 `services..providers` 下补充参数。 +4. 不修改业务调用方(search/query/indexer 仍通过工厂获取实例)。 + +### 7.3 新增 Backend(推理实现) + +1. **实现协议**:在对应目录(如 `reranker/backends/`、`embeddings/`)实现满足协议接口的类。 +2. **配置**:在 `config/config.yaml` 的 `services..backends` 下增加新后端名及参数;支持环境变量覆盖(如 `RERANK_BACKEND`)。 +3. **注册**:在 backends 的工厂(如 `get_rerank_backend(name, config)`)中增加分支并返回实例。 +4. **服务启动**:服务(如 `reranker/server.py`)启动时读取 backend 配置并调用工厂,不写死后端类型。 +5. **文档与依赖**:在 README 或 docs 中说明新后端的依赖、资源要求;可选依赖放入 `requirements_ml.txt` 或 extra。 + +详见 [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) 的“新增后端清单”。 + +### 7.4 禁止做法 + +- 在业务代码中硬编码服务 URL 或后端类型。 +- 新增能力时复制一套独立配置体系或新顶层包,而不纳入 `services` 与 providers/backends。 +- 新增后端时破坏现有协议(如修改返回长度、顺序或 meta 约定)。 + +--- + +## 8. 代码规范与质量 + +### 8.1 风格与结构 + +- **Python**:遵循 PEP 8;类型注解推荐在公共接口与配置数据结构上使用;模块级文档字符串简要说明职责。 +- **包结构**:业务逻辑按能力归属对应顶层包;共享工具放 `utils`;不随意新增与现有包平行的“杂项”包。 +- **命名**:模块与类名清晰表意;配置键与 `config.yaml` / 环境变量命名保持一致。 + +### 8.2 测试 + +- **位置**:`tests/`,可按 `unit/`、`integration/` 或按模块划分子目录;公共 fixture 在 `conftest.py`。 +- **标记**:使用 `@pytest.mark.unit`、`@pytest.mark.integration`、`@pytest.mark.api` 等区分用例类型,便于按需运行。 +- **依赖**:单元测试通过 mock(如 `mock_es_client`、`sample_search_config`)不依赖真实 ES/DB;集成测试需在说明中注明依赖服务。 +- **运行**:`python -m pytest tests/`;仅单元:`python -m pytest tests/unit/` 或 `-m unit`。 +- **原则**:新增逻辑应有对应测试;修改协议或配置契约时更新相关测试与 fixture。 + +### 8.3 配置与环境 + +- 测试用配置优先从 fixture 或临时 config 构造,避免依赖仓库外部的 `.env` 或真实 DB/ES;必要时使用 `clear_services_cache()` 等清理缓存。 +- 不在代码中提交敏感信息;敏感项通过 `.env` 或环境变量注入,并在文档中说明。 + +### 8.4 日志与可观测性 + +- 关键路径(请求入口、外部调用、失败降级)打日志;日志级别合理(如 debug 用于详细参数,info 用于流程,warning 用于降级)。 +- 对外接口的耗时、错误码、租户等可考虑结构化日志或后续接入监控,便于运维与排查。 + +--- + +## 9. 迭代检查清单 + +在提交代码或发起 Code Review 前,建议自检以下项,确保迭代符合框架与规范。 + +### 9.1 架构与模块 + +- [ ] 新逻辑放在合适的现有包中,未随意新建与现有能力平行的顶层包。 +- [ ] 未在业务代码中硬编码服务 URL、后端类型或租户 ID。 +- [ ] 调用外部能力(翻译/向量/重排)时通过 providers 工厂获取实例,配置来自 `services_config`。 + +### 9.2 配置与扩展 + +- [ ] 新增配置项放在 `config.yaml` 或 `services.` 下,并有环境变量覆盖方式(如需要)。 +- [ ] 新增 Provider 或 Backend 时已阅读 [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) / [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md),并按要求实现协议、注册与配置。 +- [ ] 新增后端满足现有协议(输入输出、顺序、长度、meta),未破坏调用方。 + +### 9.3 索引与查询 + +- [ ] 索引结构变更已同步到 `mappings/search_products.json`;indexer 产出与 mapping 一致。 +- [ ] 查询/分面/过滤使用的字段名与 mapping 一致;多语言字段使用 `.zh`/`.en` 等约定。 + +### 9.4 测试与质量 + +- [ ] 新增或修改逻辑有对应测试;修改接口或协议时已更新相关测试与 fixture。 +- [ ] 单元测试不依赖真实 ES/DB;集成测试在文档或注释中说明依赖。 +- [ ] 无敏感信息提交;敏感配置通过环境变量或 .env 说明。 + +### 9.5 文档与可维护性 + +- [ ] 新增模块或重要行为在 README 或 docs 中有简要说明;复杂逻辑有注释或文档引用。 +- [ ] 本指南与相关专项文档在需要时已更新(如新增服务、端口、配置项、扩展步骤)。 + +--- + +## 10. 文档与资源索引 + +### 10.1 按用途查找 + +| 用途 | 文档 | +|------|------| +| 新人上手、环境与请求示例 | [QUICKSTART.md](./QUICKSTART.md) | +| 框架全貌与规范(本文) | 本指南 | +| 翻译/向量/重排 Provider 扩展 | [PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) | +| 向量/重排后端可插拔与协议 | [MODULE_EXTENSION_SPEC.md](./MODULE_EXTENSION_SPEC.md) | +| 索引结构、数据流、通用化设计 | [系统设计文档.md](./系统设计文档.md) | +| 索引与查询配置说明 | [基础配置指南.md](./基础配置指南.md) | +| 搜索/索引 API 完整说明 | [搜索API对接指南.md](./搜索API对接指南.md) | +| 搜索 API 参数速查 | [搜索API速查表.md](./搜索API速查表.md) | +| 首次部署、新机器环境 | [环境配置说明.md](./环境配置说明.md) | +| 运维、日志、多环境、故障 | [Usage-Guide.md](./Usage-Guide.md) | +| 索引模块职责与 Java 对接 | [indexer/README.md](../indexer/README.md) | +| 向量模块与 clip-as-service | [embeddings/README.md](../embeddings/README.md) | + +### 10.2 仓库内入口 + +- **README.md**:项目简介、快速命令、文档索引。 +- **CLAUDE.md**:面向 AI 助手的项目说明与命令汇总,与本指南互补。 +- **本指南(docs/DEVELOPER_GUIDE.md)**:面向人的全貌与规范入口。 + +--- + +*本文档旨在让所有后续开发在预知框架全貌的前提下,在规范内迭代,减少分叉与冗余,提升可维护性。如有结构或规范变更,请同步更新本指南及相关专项文档。* diff --git a/docs/MODULE_EXTENSION_SPEC.md b/docs/MODULE_EXTENSION_SPEC.md new file mode 100644 index 0000000..f2d23ff --- /dev/null +++ b/docs/MODULE_EXTENSION_SPEC.md @@ -0,0 +1,223 @@ +# 模块扩展规范(向量化 / 重排 可插拔设计) + +本文档定义**向量化(embedding)**与**重排(rerank)**模块的扩展规范,保证新增模型/推理引擎时框架统一、配置统一、可插拔。新增 Qwen3-Reranker-0.6B(vLLM)等模块时需遵循本规范。 + +**相关文档**: +- 调用方(Provider 选择、HTTP 客户端):[PROVIDER_ARCHITECTURE.md](./PROVIDER_ARCHITECTURE.md) +- 向量化使用说明:[embeddings/README.md](../embeddings/README.md)、[向量化模块和API说明文档.md](./向量化模块和API说明文档.md) + +--- + +## 1. 设计原则 + +| 原则 | 说明 | +|------|------| +| **接口契约** | 所有同类型后端实现同一协议(Protocol),调用方只依赖协议不依赖具体实现。 | +| **单一配置源** | 能力类型、后端类型、后端参数均来自 `config/config.yaml` 的 `services` 块,环境变量可覆盖。 | +| **服务与后端分离** | **调用方**通过 Provider(如 `HttpRerankProvider`)访问**服务**;**服务内部**通过后端实现(如 BGE、Qwen3-vLLM)完成推理。新增“提供者”时区分:是新增一种**调用方式**(新 Provider)还是新增一种**推理实现**(新 Backend)。 | +| **可插拔后端** | 重排/向量化服务在启动时根据配置加载一个后端;新增后端 = 实现协议 + 在配置与工厂中注册,不改服务入口代码。 | + +--- + +## 2. 配置体系(统一结构) + +### 2.1 配置来源与优先级 + +- **主配置**:`config/config.yaml` 下的 `services.` +- **覆盖**:环境变量(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`)> config 文件 +- **解析**:`config/services_config.py` 提供 `get_*_config()`,各模块从该处读取,避免散落多处。 + +### 2.2 能力块通用结构 + +每种能力(translation / embedding / rerank)在 `services` 下结构一致: + +```yaml +services: + : + provider: "http" # 调用方使用的提供者:http | direct | vllm 等 + base_url: "http://..." # 对外服务 URL(provider=http 时) + providers: + http: { base_url: "...", ... } + direct: { ... } + vllm: { ... } + # 以下为「服务内部后端」配置(仅当本能力由本仓库启动的服务承载时使用) + backend: "bge" # 可选:服务内加载的后端类型 + backends: + bge: { model_name: "...", batch_size: 64, ... } + qwen3_vllm: { model_name: "Qwen/Qwen3-Reranker-0.6B", ... } +``` + +- **provider**:调用方(搜索 API、索引等)如何访问该能力(如 HTTP 调 `base_url`)。 +- **backend / backends**:当该能力由本仓库内的服务进程提供时,该进程内应加载哪个后端及参数(如 reranker 服务内用 BGE 还是 Qwen3-vLLM)。 + +--- + +## 3. 重排(Rerank)模块规范 + +### 3.1 调用链 + +- **调用方**:`search/rerank_client.py` → `create_rerank_provider()` → `HttpRerankProvider.rerank(query, docs, timeout_sec)` +- **协议**:HTTP `POST /rerank`,请求体 `{ "query": str, "docs": [str] }`,响应体 `{ "scores": [float], "meta": dict }`,scores 与 docs 一一对应。 +- **服务实现**:`reranker/server.py`(FastAPI)在启动时加载一个**重排后端**,对 `/rerank` 的请求用该后端计算分数。 + +因此: +- **新增一种“调用方式”**(如 gRPC):在 `providers/rerank.py` 增加新 Provider 类,并在 `create_rerank_provider()` 中按 `provider` 选择。 +- **新增一种“推理实现”**(如 Qwen3-vLLM):在 reranker 服务内实现**重排后端协议**并注册,服务通过配置选择后端。 + +### 3.2 重排后端协议(服务内) + +所有在 `reranker` 服务内加载的后端必须实现以下接口(与当前 `BGEReranker` 一致): + +```python +# 行为契约(不强制继承,实现以下方法即可) +class RerankBackendProtocol(Protocol): + def score_with_meta( + self, + query: str, + docs: List[str], + normalize: bool = True, + ) -> Tuple[List[float], Dict[str, Any]]: + """ + 输入: + - query: 搜索查询字符串 + - docs: 文档列表,与返回的 scores 一一对应 + - normalize: 是否对分数做归一化(如 sigmoid) + 输出: + - scores: 与 docs 等长的分数列表,顺序一致 + - meta: 至少含 input_docs, usable_docs, unique_docs, elapsed_ms 等,供日志与调试 + """ + ... +``` + +- **顺序**:返回的 `scores[i]` 必须对应 `docs[i]`。 +- **空/无效**:对无法打分的 doc 可填 0.0,并在 meta 中说明。 +- **去重**:后端可对 docs 去重再推理以省算力,但返回的 scores 必须按原始 docs 顺序与长度还原。 + +### 3.3 重排服务配置项(建议) + +在 `config/config.yaml` 的 `services.rerank` 下建议结构(与现有 `rerank` 顶层配置区分:顶层为搜索侧融合参数,此处为服务/后端配置): + +```yaml +services: + rerank: + provider: "http" + base_url: "http://127.0.0.1:6007" + providers: + http: + base_url: "http://127.0.0.1:6007" + service_url: "http://127.0.0.1:6007/rerank" + # 服务内后端(reranker 进程启动时读取) + backend: "bge" # bge | qwen3_vllm + backends: + bge: + model_name: "BAAI/bge-reranker-v2-m3" + device: null + use_fp16: true + batch_size: 64 + max_length: 512 + cache_dir: "./model_cache" + enable_warmup: true + qwen3_vllm: + model_name: "Qwen/Qwen3-Reranker-0.6B" + engine: "vllm" + max_model_len: 8192 + tensor_parallel_size: 1 + gpu_memory_utilization: 0.8 + instruction: "Given a web search query, retrieve relevant passages that answer the query" +``` + +- 环境变量示例:`RERANK_BACKEND=qwen3_vllm`、`RERANKER_SERVICE_URL=http://127.0.0.1:6007`。 + +### 3.4 重排后端目录与注册 + +- **推荐目录**:`reranker/backends/` + - `reranker/backends/__init__.py`:导出 `get_rerank_backend(name, config) -> 实现 RerankBackendProtocol 的实例` + - `reranker/backends/bge.py`:现有 BGE 逻辑迁移或封装为 `BGERerankerBackend` + - `reranker/backends/qwen3_vllm.py`:新增 Qwen3-Reranker-0.6B + vLLM 实现 +- **服务启动**:`reranker/server.py` 在 `startup` 中读取 `services.rerank.backend` 与 `services.rerank.backends.`,调用 `get_rerank_backend(backend, cfg)` 得到实例,再对外提供同一 `/rerank` API。 + +### 3.5 重排 HTTP API 契约(不变) + +无论后端是 BGE 还是 Qwen3-vLLM,对外接口保持一致,便于调用方与运维统一: + +- **POST /rerank** + - Request: `{ "query": string, "docs": [string], "normalize": optional bool }` + - Response: `{ "scores": [float], "meta": object }` +- **GET /health** + - Response: `{ "status": "ok"|"unavailable", "model_loaded": bool, "model": string, "backend": string }` + +--- + +## 4. 向量化(Embedding)模块规范 + +### 4.1 调用链 + +- **调用方**:通过 `providers.create_embedding_provider()` 得到 HTTP 客户端,请求 `POST /embed/text`、`POST /embed/image`。 +- **服务实现**:`embeddings/server.py` 在启动时按配置加载**文本后端**与**图片后端**,二者可独立选择。 + +### 4.2 向量化后端协议(服务内) + +- **文本**:与当前 `BgeTextModel` 一致,需支持 `encode_batch(texts, batch_size, device) -> List[ndarray]`,元素与 `texts` 一一对应,失败可为 None。 +- **图片**:已定义 `embeddings/protocols.ImageEncoderProtocol`: + - `encode_image_urls(urls: List[str], batch_size: Optional[int]) -> List[Optional[np.ndarray]]` + - 与 `urls` 等长,失败位置为 None。 + +新增文本/图片后端时实现对应协议即可;服务通过配置选择后端(如 `USE_CLIP_AS_SERVICE` 选 clip-as-service 或本地 CN-CLIP)。 + +### 4.3 向量化配置(现有与扩展) + +- **Provider/URL**:`config/config.yaml` → `services.embedding`,环境变量 `EMBEDDING_SERVICE_URL`。 +- **服务内**:`embeddings/config.py` 中已有 `TEXT_*`、`IMAGE_*`、`USE_CLIP_AS_SERVICE`、`CLIP_AS_SERVICE_SERVER`;若未来支持多种文本/图像后端,建议在 `services.embedding.backend` / `services.embedding.backends` 中统一,与重排结构对齐。 + +--- + +## 5. 新增后端清单(以 Qwen3-Reranker-0.6B + vLLM 为例) + +按本规范新增「重排后端」Qwen3-Reranker-0.6B(vLLM 推理)时,建议步骤: + +1. **实现协议** + - 在 `reranker/backends/qwen3_vllm.py` 中实现类(如 `Qwen3VLLMReranker`),提供 `score_with_meta(query, docs, normalize) -> (scores, meta)`。 + - 推理逻辑参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B) 的 vLLM 用法(format_instruction、process_inputs、compute_logits、yes/no token 等),输出与 `docs` 等长且顺序一致的 scores。 + +2. **配置** + - 在 `config/config.yaml` 的 `services.rerank.backends` 下增加 `qwen3_vllm` 块(model_name、engine、max_model_len、tensor_parallel_size、gpu_memory_utilization、instruction 等)。 + - 在 `config/services_config.py` 或 reranker 专用 config 中增加对 `backend` / `backends` 的读取;环境变量支持 `RERANK_BACKEND=qwen3_vllm`。 + +3. **注册** + - 在 `reranker/backends/__init__.py` 的 `get_rerank_backend(name, config)` 中增加 `"qwen3_vllm"` 分支,实例化 `Qwen3VLLMReranker` 并传入 config。 + +4. **服务启动** + - 若尚未重构:可暂时在 `reranker/server.py` 中根据 `RERANK_BACKEND` 或 config 选择加载 `BGEReranker` 或 `Qwen3VLLMReranker`。 + - 若已引入 `get_rerank_backend()`:`reranker/server.py` 启动时统一调用 `get_rerank_backend(backend_name, backend_cfg)` 得到实例。 + +5. **调用方** + - 无需修改:`providers/rerank.py` 仍为 HTTP,`search/rerank_client.py` 仍调用同一 `/rerank` 接口;仅部署时启动使用 Qwen3-vLLM 后端的 reranker 服务即可。 + +6. **文档与依赖** + - 在 `reranker/README.md` 或 `docs/` 中说明 Qwen3-vLLM 的依赖(vllm>=0.8.5、transformers 等)、显存建议、与 BGE 的对比。 + - 若 vLLM 为可选依赖,在 `requirements_ml.txt` 或可选 extra 中声明。 + +--- + +## 6. 小结表 + +| 层次 | 配置键 | 重排 | 向量化(文本/图) | +|------|--------|------|-------------------| +| 调用方 | `services..provider` | http | http | +| 调用方 | `services..providers.http.base_url` | 6007 | 6005 | +| 服务内 | `services..backend` | bge / qwen3_vllm | (当前在 embeddings/config.py) | +| 服务内 | `services..backends.` | 模型名、batch、vLLM 参数等 | 模型名、device 等 | +| 协议 | 重排 | `score_with_meta(query, docs, normalize)` | — | +| 协议 | 向量化 | — | 文本: encode_batch;图: ImageEncoderProtocol | + +遵循上述规范后,新增 Qwen3-Reranker-0.6B 或其它重排/向量化后端时,只需实现协议、在配置与工厂中注册,即可与现有 BGE/CLIP 等并列切换,保持框架统一与可插拔。 + +--- + +## 7. 与现有配置文件的兼容说明 + +- **reranker**:当前 `reranker/config.py` 中 `RerankerConfig`(PORT、MODEL_NAME、BATCH_SIZE 等)仅被 BGE 服务使用。扩展多后端时,建议: + - 保留该文件作为**默认/兜底**(仅当未配置 `services.rerank.backend` 时使用),或 + - 将 BGE 的默认值迁移到 `config.yaml` 的 `services.rerank.backends.bge`,`reranker/config.py` 只读环境变量与 YAML,不再硬编码模型名。 +- **embeddings**:`embeddings/config.py` 的 `EmbeddingConfig` 已包含文本/图片及 clip-as-service 开关,与 `services.embedding` 的 URL 分离(URL 由 `services_config` 管)。后续若增加多种文本/图像后端,可同样在 `services.embedding.backends` 中增加条目,与重排对齐。 +- **环境变量**:所有能力均支持通过环境变量覆盖(如 `RERANKER_SERVICE_URL`、`RERANK_BACKEND`、`EMBEDDING_SERVICE_URL`),便于部署与多环境。 diff --git a/docs/PROVIDER_ARCHITECTURE.md b/docs/PROVIDER_ARCHITECTURE.md index 95a1a2e..7752ed3 100644 --- a/docs/PROVIDER_ARCHITECTURE.md +++ b/docs/PROVIDER_ARCHITECTURE.md @@ -2,6 +2,8 @@ 本文档说明如何统一管理翻译、向量化、重排等“能力提供者(provider)”。 +**扩展重排/向量化后端(如新增 Qwen3-Reranker、vLLM 等)**:请同时参阅 [模块扩展规范(MODULE_EXTENSION_SPEC.md)](./MODULE_EXTENSION_SPEC.md),其中定义了服务内后端协议、统一配置结构与可插拔实现方式。 + ## 1. 设计目标 - **调用方稳定**:业务代码不关心具体供应商,只调用统一接口。 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ebe7030..1e4e594 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -2,6 +2,8 @@ 新人入口文档:环境、服务、模块、请求示例一页搞定。 +**建议**:首次参与开发请先阅读 [DEVELOPER_GUIDE.md](./DEVELOPER_GUIDE.md) 建立项目全貌与规范,再使用本页做环境与请求速查。 + ## 1. 环境 ```bash diff --git a/requirements_ml.txt b/requirements_ml.txt index e6e55d3..cadc9fc 100644 --- a/requirements_ml.txt +++ b/requirements_ml.txt @@ -1,8 +1,13 @@ -# Optional heavy dependencies for local embedding/image encoding. +# Optional heavy dependencies for local embedding/image encoding and reranker backends. # # Install when you need: # - `./scripts/start_embedding_service.sh` (local embeddings server) # - local BGE-M3 / CN-CLIP inference +# - reranker with BGE backend (modelscope) +# +# For Qwen3-Reranker-0.6B (vLLM backend), also install: +# pip install vllm>=0.8.5 +# (transformers already listed below) # # Notes: # - `torch` wheels can be very large; if you want CPU-only wheels, diff --git a/reranker/README.md b/reranker/README.md index 7de460b..f88f4b7 100644 --- a/reranker/README.md +++ b/reranker/README.md @@ -1,50 +1,72 @@ # Reranker 模块 -**请求示例**见 `docs/QUICKSTART.md` §3.5。 +**请求示例**见 `docs/QUICKSTART.md` §3.5。扩展规范见 `docs/MODULE_EXTENSION_SPEC.md`。 --- -A minimal, production-ready reranker service based on **BAAI/bge-reranker-v2-m3**. - -Features -- FP16 on GPU -- Length-based sorting to reduce padding waste -- Deduplication to avoid redundant inference -- Scores returned in original input order -- Simple FastAPI service - -## Files -- `reranker/bge_reranker.py`: core model loading + scoring logic -- `reranker/server.py`: FastAPI service with `/health` and `/rerank` -- `reranker/config.py`: simple configuration - -## Requirements -Install Python deps (already in project requirements): -- `torch` -- `modelscope` -- `fastapi` -- `uvicorn` - -## Configuration -Edit `reranker/config.py`: -- `MODEL_NAME`: default `BAAI/bge-reranker-v2-m3` -- `DEVICE`: `None` (auto), `cuda`, or `cpu` -- `USE_FP16`: enable fp16 on GPU -- `BATCH_SIZE`: default 64 -- `MAX_LENGTH`: default 512 -- `PORT`: default 6007 -- `MAX_DOCS`: request limit (default 1000) - -## Run the Service +Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwen3-vLLM)。调用方通过 HTTP 访问,不关心具体后端。 + +**特性** +- 多后端:`bge`(BAAI/bge-reranker-v2-m3)、`qwen3_vllm`(Qwen3-Reranker-0.6B + vLLM) +- 统一配置:`config/config.yaml` → `services.rerank.backend` / `services.rerank.backends.` +- 文档去重、分数与输入顺序一致、FP16/GPU 支持(视后端) + +## 目录与入口 +- `reranker/server.py`:FastAPI 服务,启动时按配置加载一个后端 +- `reranker/backends/`:后端实现与工厂 + - `backends/__init__.py`:`get_rerank_backend(name, config)` + - `backends/bge.py`:BGE 后端 + - `backends/qwen3_vllm.py`:Qwen3-Reranker-0.6B + vLLM 后端 +- `reranker/bge_reranker.py`:BGE 核心推理(被 bge 后端封装) +- `reranker/config.py`:服务端口、MAX_DOCS、NORMALIZE 等(后端参数在 config.yaml) + +## 依赖 +- 通用:`torch`、`modelscope`、`fastapi`、`uvicorn`(见项目 `requirements.txt` / `requirements_ml.txt`) +- **Qwen3-vLLM 后端**:`vllm>=0.8.5`、`transformers`(可选,仅当使用 `backend: qwen3_vllm` 时安装) + ```bash + pip install vllm>=0.8.5 transformers + ``` + +## 配置 +- **后端选择**:`config/config.yaml` 中 `services.rerank.backend`(`bge` | `qwen3_vllm`),或环境变量 `RERANK_BACKEND`。 +- **后端参数**:`services.rerank.backends.bge` / `services.rerank.backends.qwen3_vllm`,例如: + +```yaml +services: + rerank: + backend: "bge" # 或 qwen3_vllm + backends: + bge: + model_name: "BAAI/bge-reranker-v2-m3" + device: null + use_fp16: true + batch_size: 64 + max_length: 512 + cache_dir: "./model_cache" + enable_warmup: true + qwen3_vllm: + model_name: "Qwen/Qwen3-Reranker-0.6B" + max_model_len: 8192 + tensor_parallel_size: 1 + gpu_memory_utilization: 0.8 + enable_prefix_caching: true + instruction: "Given a web search query, retrieve relevant passages that answer the query" +``` + +- 服务端口、请求限制等仍在 `reranker/config.py`(或环境变量 `RERANKER_PORT`、`RERANKER_HOST`)。 + +## 运行 ```bash uvicorn reranker.server:app --host 0.0.0.0 --port 6007 ``` +使用 Qwen3-vLLM 时需先安装 vLLM 与 transformers,并将 `services.rerank.backend` 设为 `qwen3_vllm` 或设置 `RERANK_BACKEND=qwen3_vllm`。 ## API ### Health ``` GET /health ``` +Response 含 `backend`(当前后端名)、`model`、`model_loaded`、`status`。 ### Rerank ``` @@ -86,6 +108,6 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info ``` ## Notes -- No caching is used by design. -- Inputs are deduplicated by exact string match. -- Empty or null docs are skipped and scored as 0. +- 无请求级缓存;输入按字符串去重后推理,再按原始顺序回填分数。 +- 空或 null 的 doc 跳过并计为 0。 +- **Qwen3-vLLM**:参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B),需 GPU 与较多显存;与 BGE 相比适合长文本、高吞吐场景(vLLM 前缀缓存)。 diff --git a/reranker/backends/__init__.py b/reranker/backends/__init__.py new file mode 100644 index 0000000..164fba2 --- /dev/null +++ b/reranker/backends/__init__.py @@ -0,0 +1,49 @@ +""" +Rerank backends - pluggable implementations of the rerank protocol. + +Each backend implements score_with_meta(query, docs, normalize) -> (scores, meta). +Service loads one backend via get_rerank_backend(name, config) from config. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Protocol, Tuple + + +class RerankBackendProtocol(Protocol): + """Protocol for reranker backends (service-internal).""" + + def score_with_meta( + self, + query: str, + docs: List[str], + normalize: bool = True, + ) -> Tuple[List[float], Dict[str, Any]]: + """ + Input: + query: search query string + docs: list of documents, scores must align 1:1 with docs + normalize: whether to normalize scores (e.g. sigmoid) + Output: + scores: list same length as docs, same order + meta: at least input_docs, usable_docs, unique_docs, elapsed_ms + """ + ... + + +def get_rerank_backend(name: str, config: Dict[str, Any]) -> RerankBackendProtocol: + """ + Factory: return a reranker backend instance for the given name and config. + Config is the corresponding block from services.rerank.backends.. + """ + name = (name or "bge").strip().lower() + if name == "bge": + from reranker.backends.bge import BGERerankerBackend + return BGERerankerBackend(config) + if name == "qwen3_vllm": + from reranker.backends.qwen3_vllm import Qwen3VLLMRerankerBackend + return Qwen3VLLMRerankerBackend(config) + raise ValueError(f"Unknown rerank backend: {name!r}. Supported: bge, qwen3_vllm") + + +__all__ = ["RerankBackendProtocol", "get_rerank_backend"] diff --git a/reranker/backends/bge.py b/reranker/backends/bge.py new file mode 100644 index 0000000..f56a68e --- /dev/null +++ b/reranker/backends/bge.py @@ -0,0 +1,33 @@ +""" +BGE reranker backend - wraps BGEReranker for the unified backend protocol. +""" + +from __future__ import annotations + +from typing import Any, Dict, List + +from reranker.bge_reranker import BGEReranker + + +class BGERerankerBackend: + """BGE reranker backend; config from services.rerank.backends.bge.""" + + def __init__(self, config: Dict[str, Any]) -> None: + self._config = config or {} + self._impl = BGEReranker( + model_name=str(self._config.get("model_name") or "BAAI/bge-reranker-v2-m3"), + device=self._config.get("device"), + batch_size=int(self._config.get("batch_size", 64)), + use_fp16=bool(self._config.get("use_fp16", True)), + max_length=int(self._config.get("max_length", 512)), + cache_dir=str(self._config.get("cache_dir") or "./model_cache"), + enable_warmup=bool(self._config.get("enable_warmup", True)), + ) + + def score_with_meta( + self, + query: str, + docs: List[str], + normalize: bool = True, + ) -> tuple[list[float], Dict[str, Any]]: + return self._impl.score_with_meta(query, docs, normalize=normalize) diff --git a/reranker/backends/qwen3_vllm.py b/reranker/backends/qwen3_vllm.py new file mode 100644 index 0000000..c40e75d --- /dev/null +++ b/reranker/backends/qwen3_vllm.py @@ -0,0 +1,213 @@ +""" +Qwen3-Reranker-0.6B backend using vLLM. + +Reference: https://huggingface.co/Qwen/Qwen3-Reranker-0.6B +Requires: vllm>=0.8.5, transformers; GPU recommended. +""" + +from __future__ import annotations + +import logging +import math +import time +from typing import Any, Dict, List, Optional, Tuple + +logger = logging.getLogger("reranker.backends.qwen3_vllm") + +try: + import torch + from transformers import AutoTokenizer + from vllm import LLM, SamplingParams + from vllm.inputs import TokensPrompt +except ImportError as e: + raise ImportError( + "Qwen3-vLLM reranker backend requires vllm>=0.8.5 and transformers. " + "Install with: pip install vllm transformers" + ) from e + + +def _format_instruction(instruction: str, query: str, doc: str) -> List[Dict[str, str]]: + """Build chat messages for one (query, doc) pair.""" + return [ + { + "role": "system", + "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\".", + }, + { + "role": "user", + "content": f": {instruction}\n\n: {query}\n\n: {doc}", + }, + ] + + +class Qwen3VLLMRerankerBackend: + """ + Qwen3-Reranker-0.6B with vLLM inference. + Config from services.rerank.backends.qwen3_vllm. + """ + + def __init__(self, config: Dict[str, Any]) -> None: + self._config = config or {} + model_name = str(self._config.get("model_name") or "Qwen/Qwen3-Reranker-0.6B") + max_model_len = int(self._config.get("max_model_len", 8192)) + tensor_parallel_size = int(self._config.get("tensor_parallel_size", 1)) + gpu_memory_utilization = float(self._config.get("gpu_memory_utilization", 0.8)) + enable_prefix_caching = bool(self._config.get("enable_prefix_caching", True)) + self._instruction = str( + self._config.get("instruction") + or "Given a web search query, retrieve relevant passages that answer the query" + ) + + logger.info( + "[Qwen3_VLLM] Loading model %s (max_model_len=%s, tp=%s, prefix_caching=%s)", + model_name, + max_model_len, + tensor_parallel_size, + enable_prefix_caching, + ) + + self._llm = LLM( + model=model_name, + tensor_parallel_size=tensor_parallel_size, + max_model_len=max_model_len, + gpu_memory_utilization=gpu_memory_utilization, + enable_prefix_caching=enable_prefix_caching, + ) + self._tokenizer = AutoTokenizer.from_pretrained(model_name) + self._tokenizer.padding_side = "left" + self._tokenizer.pad_token = self._tokenizer.eos_token + + # Suffix for generation prompt (assistant answer) + self._suffix = "<|im_end|>\n<|im_start|>assistant\n\n\n\n\n" + self._suffix_tokens = self._tokenizer.encode( + self._suffix, add_special_tokens=False + ) + self._max_prompt_len = max_model_len - len(self._suffix_tokens) + + self._true_token = self._tokenizer("yes", add_special_tokens=False).input_ids[0] + self._false_token = self._tokenizer("no", add_special_tokens=False).input_ids[0] + self._sampling_params = SamplingParams( + temperature=0, + max_tokens=1, + logprobs=20, + allowed_token_ids=[self._true_token, self._false_token], + ) + + self._model_name = model_name + logger.info("[Qwen3_VLLM] Model ready | model=%s", model_name) + + def _process_inputs( + self, + pairs: List[Tuple[str, str]], + ) -> List[TokensPrompt]: + """Build tokenized prompts for vLLM from (query, doc) pairs.""" + prompts = [] + for q, d in pairs: + messages = _format_instruction(self._instruction, q, d) + # One conversation per call (apply_chat_template expects single conversation) + token_ids = self._tokenizer.apply_chat_template( + messages, + tokenize=True, + add_generation_prompt=False, + enable_thinking=False, + ) + if isinstance(token_ids, list) and token_ids and isinstance(token_ids[0], list): + token_ids = token_ids[0] + ids = token_ids[: self._max_prompt_len] + self._suffix_tokens + prompts.append(TokensPrompt(prompt_token_ids=ids)) + return prompts + + def _compute_scores( + self, + prompts: List[TokensPrompt], + ) -> List[float]: + """Run vLLM generate and compute yes/no probability per prompt.""" + if not prompts: + return [] + outputs = self._llm.generate(prompts, self._sampling_params, use_tqdm=False) + scores = [] + for i in range(len(outputs)): + out = outputs[i] + if not out.outputs: + scores.append(0.0) + continue + logprobs = out.outputs[0].logprobs + if not logprobs: + scores.append(0.0) + continue + last = logprobs[-1] + true_logp = last.get(self._true_token) + false_logp = last.get(self._false_token) + true_p = math.exp(true_logp.logprob) if true_logp else 1e-10 + false_p = math.exp(false_logp.logprob) if false_logp else 1e-10 + score = true_p / (true_p + false_p) + scores.append(float(score)) + return scores + + def score_with_meta( + self, + query: str, + docs: List[str], + normalize: bool = True, + ) -> Tuple[List[float], Dict[str, Any]]: + start_ts = time.time() + total_docs = len(docs) if docs else 0 + output_scores: List[float] = [0.0] * total_docs + + query = "" if query is None else str(query).strip() + indexed: List[Tuple[int, str]] = [] + for i, doc in enumerate(docs or []): + if doc is None: + continue + text = str(doc).strip() + if not text: + continue + indexed.append((i, text)) + + if not query or not indexed: + elapsed_ms = (time.time() - start_ts) * 1000.0 + return output_scores, { + "input_docs": total_docs, + "usable_docs": len(indexed), + "unique_docs": 0, + "dedup_ratio": 0.0, + "elapsed_ms": round(elapsed_ms, 3), + "model": self._model_name, + "backend": "qwen3_vllm", + "normalize": normalize, + } + + # Deduplicate by text, keep mapping to original indices + unique_texts: List[str] = [] + position_to_unique: List[int] = [] + prev: Optional[str] = None + for _idx, text in indexed: + if text != prev: + unique_texts.append(text) + prev = text + position_to_unique.append(len(unique_texts) - 1) + + pairs = [(query, t) for t in unique_texts] + prompts = self._process_inputs(pairs) + unique_scores = self._compute_scores(prompts) + + for (orig_idx, _), unique_idx in zip(indexed, position_to_unique): + # Score is already P(yes) in [0,1] from yes/(yes+no) + output_scores[orig_idx] = float(unique_scores[unique_idx]) + + elapsed_ms = (time.time() - start_ts) * 1000.0 + dedup_ratio = 0.0 + if indexed: + dedup_ratio = 1.0 - (len(unique_texts) / float(len(indexed))) + + meta = { + "input_docs": total_docs, + "usable_docs": len(indexed), + "unique_docs": len(unique_texts), + "dedup_ratio": round(dedup_ratio, 4), + "elapsed_ms": round(elapsed_ms, 3), + "model": self._model_name, + "backend": "qwen3_vllm", + "normalize": normalize, + } + return output_scores, meta diff --git a/reranker/server.py b/reranker/server.py index 3f0ea54..39f2baa 100644 --- a/reranker/server.py +++ b/reranker/server.py @@ -1,18 +1,11 @@ """ -FastAPI service for BGE reranking. +Reranker service - unified /rerank API backed by pluggable backends (BGE, Qwen3-vLLM). POST /rerank -Request: -{ - "query": "...", - "docs": ["doc1", "doc2", ...] -} - -Response: -{ - "scores": [0.98, 0.12, ...], - "meta": {...} -} +Request: { "query": "...", "docs": ["doc1", "doc2", ...], "normalize": optional bool } +Response: { "scores": [float], "meta": {...} } + +Backend selected via config: services.rerank.backend (bge | qwen3_vllm), env RERANK_BACKEND. """ import logging @@ -22,7 +15,8 @@ from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field -from reranker.bge_reranker import BGEReranker +from config.services_config import get_rerank_backend_config +from reranker.backends import RerankBackendProtocol, get_rerank_backend from reranker.config import CONFIG logging.basicConfig( @@ -33,7 +27,8 @@ logger = logging.getLogger("reranker.service") app = FastAPI(title="saas-search Reranker Service", version="1.0.0") -_reranker: Optional[BGEReranker] = None +_reranker: Optional[RerankBackendProtocol] = None +_backend_name: str = "" class RerankRequest(BaseModel): @@ -51,25 +46,17 @@ class RerankResponse(BaseModel): @app.on_event("startup") def load_model() -> None: - global _reranker + global _reranker, _backend_name logger.info("Starting reranker service on port %s", CONFIG.PORT) try: - _reranker = BGEReranker( - model_name=CONFIG.MODEL_NAME, - device=CONFIG.DEVICE, - batch_size=CONFIG.BATCH_SIZE, - use_fp16=CONFIG.USE_FP16, - max_length=CONFIG.MAX_LENGTH, - cache_dir=CONFIG.CACHE_DIR, - enable_warmup=CONFIG.ENABLE_WARMUP, - ) + backend_name, backend_cfg = get_rerank_backend_config() + _backend_name = backend_name + _reranker = get_rerank_backend(backend_name, backend_cfg) + model_info = getattr(_reranker, "_model_name", None) or backend_cfg.get("model_name", backend_name) logger.info( - "Reranker ready | model=%s device=%s fp16=%s batch=%s max_len=%s", - CONFIG.MODEL_NAME, - _reranker.device, - _reranker.use_fp16, - _reranker.batch_size, - _reranker.max_length, + "Reranker ready | backend=%s model=%s", + _backend_name, + model_info, ) except Exception as exc: logger.error("Failed to initialize reranker: %s", exc, exc_info=True) @@ -78,11 +65,16 @@ def load_model() -> None: @app.get("/health") def health() -> Dict[str, Any]: + model_info = "" + if _reranker is not None: + model_info = getattr(_reranker, "_model_name", None) or getattr( + _reranker, "_config", {} + ).get("model_name", _backend_name) return { "status": "ok" if _reranker is not None else "unavailable", "model_loaded": _reranker is not None, - "model": CONFIG.MODEL_NAME, - "device": CONFIG.DEVICE, + "model": model_info, + "backend": _backend_name, } -- libgit2 0.21.2