Spring AI + ParadeDB 实现BM25、向量混合检索
🔍 Spring AI × ParadeDB 混合检索 RAG
一个数据库搞定向量 + 全文检索,加权 RRF 融合,可视化知识库检索对比,RAG问答对比
向量检索 · BM25 全文检索 · 加权 RRF 融合
做 RAG 时,向量检索对语义理解强,但遇到专有名词、型号、ID 就抓瞎;BM25 对关键词精准,但无法理解同义词和上下文。业界方案往往是 Elasticsearch + 向量数据库双写,运维成本高得离谱。
ParadeDB 把 BM25 和 pgvector 塞进了同一个 PostgreSQL,一张表、一个连接、一次查询,就能做混合检索。本项目基于 Spring AI 1.0 + ParadeDB,提供:
- ✅ 同一数据库内完成 向量检索 + BM25 全文检索 + 加权 RRF 融合
- ✅ 三种检索方式可视化耗时对比,直观感受各方案优劣
- ✅ Advisor 链编排,预检索复用、耗时统计、日志追踪一键接入
- ✅ SSE 流式问答,参考文档先推、回答逐字渲染
- ✅ 支持 加权 RRF 与 Score Fusion 两种融合策略
git地址:https://github.com/makelongs/spring-ai-paradedb-hybrid.git
混合检索对比



| 向量检索 | BM25 检索 | 混合检索 (RRF 融合) |
|---|---|---|
| 297 ms · 10 条 | 19 ms · 10 条 | 287 ms · 5 条 |
| 语义相似度高 | 关键词命中强 | 去重融合,互补召回 |
混合检索在保持向量语义能力的同时,用 BM25 补齐关键词短板,融合后 Top-5 质量显著优于单路。
▼text复制代码┌─────────────────────────────────────────────────────────────┐ │ 前端 (Material Design) │ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ │ │ 检索对比页面 │ │ AI 问答页面 │ │ 参考文档展开/收起 │ │ │ └──────┬───────┘ └──────┬───────┘ └─────────┬──────────┘ │ └─────────┼────────────────┼────────────────────┼─────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Spring Boot 3.4 + JDK 21 │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ ChatService (ChatClient 编排) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ │ │ Timing │ │ MyLogger │ │ QA Advisor │ │ │ │ │ │ Advisor │→ │ Advisor │→ │ (HYBRID/VECTOR/ │ │ │ │ │ │ (order=-200)│ │ (order=-150)│ │ BM25/NONE) │ │ │ │ │ └─────────────┘ └─────────────┘ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ┌───────────────┴───────┐ │ │ │ │ ▼ ▼ │ │ │ │ ┌─────────────────┐ ┌─────────────┐│ │ │ │ │ 向量检索 (pgvector)│ │ BM25检索 ││ │ │ │ │ HNSW + 余弦相似度 │ │ (ParadeDB) ││ │ │ │ └────────┬────────┘ └──────┬──────┘│ │ │ │ │ │ │ │ │ │ └────────┬─────────┘ │ │ │ │ ▼ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ │ HybridSearchService │ │ │ │ │ │ 加权 RRF / Score Fusion 融合 │ │ │ │ │ │ vector-weight: 0.7 │ │ │ │ │ │ bm25-weight: 0.3 │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ParadeDB (PostgreSQL) │ │ ┌────────────────────────┐ ┌─────────────────────────────┐ │ │ │ vector_store 表 │ │ idx_vector_store_embedding │ │ │ │ - id (UUID PK) │ │ USING hnsw (embedding │ │ │ │ - content (TEXT) │ │ vector_cosine_ops) │ │ │ │ - metadata (JSONB) │ │ │ │ │ │ - embedding (vector) │ │ idx_vector_store_bm25 │ │ │ └────────────────────────┘ │ USING bm25 (content) │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
🚀 快速开始
1. 环境准备
- JDK 21
- PostgreSQL 14+ 已安装 ParadeDB 扩展(
pg_search+pgvector) - OpenAI 兼容 API Key
2. 一键启动 ParadeDB
▼bash复制代码docker-compose up -d
进入容器手动创建表:
▼sql复制代码DROP TABLE IF EXISTS public.vector_store; CREATE TABLE "public"."vector_store" ( "id" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, "content" text COLLATE "pg_catalog"."default", "metadata" jsonb, "embedding" vector(1024) ); CREATE INDEX idx_vector_store_embedding ON public.vector_store USING hnsw (embedding vector_cosine_ops); CREATE INDEX idx_vector_store_bm25 ON public.vector_store USING bm25 (id, content, metadata) WITH (key_field='id', text_fields='{"content": {"tokenizer": {"type": "jieba"}}}', json_fields = '{"metadata": {"tokenizer": {"type": "jieba"},"fast": true}}'); ALTER TABLE "public"."vector_store" ADD CONSTRAINT "vector_store_pkey" PRIMARY KEY ("id");
3. 运行项目
配置供应商、文本模型、embedding模型(embedding模型维度与表维度保持一致,最大2000)
启动时 DocumentInitRunner 会自动检查 vector_store 表:
- 已有数据 → 跳过
- 表为空 → 加载
classpath:document/*.md并向量化入库
5. 访问
| 入口 | 地址 |
|---|---|
| 🎨 可视化对比页面 | http://localhost:6783/api/index.html |
| 📚 API 文档 (Knife4j) | http://localhost:6783/api/doc.html |
⚙️ 配置说明
▼yaml复制代码spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.siliconflow.cn} chat.options.model: Qwen/Qwen2.5-72B-Instruct embedding.options.model: BAAI/bge-large-zh-v1.5 vectorstore.pgvector: index-type: HNSW dimensions: 1024 distance-type: COSINE_DISTANCE schema-initialization: true hybrid-search: vector-top-k: 10 bm25-top-k: 10 final-top-k: 5 similarity-threshold: 0.5 vector-weight: 0.7 # 向量权重 bm25-weight: 0.3 # BM25 权重 rrf-k: 60 fusion-mode: weighted_rrf # weighted_rrf | score_fusion
融合策略对比
| 模式 | 原理 | 适用场景 |
|---|---|---|
weighted_rrf | 基于排名倒数融合,每路乘权重系数 | 推荐通用。不依赖原始分数稳定性,对异常值鲁棒 |
score_fusion | Min-Max 归一化原始分数后加权求和 | 对分数质量有信心的场景,能体现"搜得多准" |
权重调参指南
| 场景 | vector-weight | bm25-weight |
|---|---|---|
| 语义优先(知识库问答) | 0.8 | 0.2 |
| 关键词优先(合同/型号检索) | 0.2 | 0.8 |
| 均衡模式 | 0.5 | 0.5 |
🔧 API 接口
问答(SSE 流式)
▼http复制代码GET /api/chat/stream?question=住院报销待遇&mode=HYBRID
SSE 事件流:
▼text复制代码event: documents data: [{"id":"...","content":"...","score":0.85}, ...] event: content data: 根据相关政策,参保人员在定点医疗机构住院... event: content data: 发生的符合规定的医疗费用...
检索对比
▼http复制代码GET /api/search/compare?q=住院报销待遇
返回三种检索方式的耗时、命中数、完整结果列表,供前端可视化渲染。
📂 项目结构
▼text复制代码src/main/java/com/example/ ├── ai/ │ ├── advisor/ # Advisor 链:计时、日志、检索增强 │ ├── config/ # 检索参数、线程池、文档分割器 │ ├── init/ # 启动自动向量化 │ ├── search/ # 检索服务 + 混合融合 + 对比 │ ├── service/ # ChatClient 封装 │ └── util/ # Score 提取工具 ├── controller/ # ChatController (SSE) + SearchController ├── domain/dto/ # 传输对象 └── mapper/ # MyBatis-Plus BM25 查询
如果这个项目对你有帮助,请点个 ⭐ Star 支持一下!
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论

