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 流式问答,参考文档先推、回答逐字渲染
  • ✅ 支持 加权 RRFScore Fusion 两种融合策略

git地址:https://github.com/makelongs/spring-ai-paradedb-hybrid.git

混合检索对比

img.png

img_1.png

img_2.png

向量检索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_fusionMin-Max 归一化原始分数后加权求和对分数质量有信心的场景,能体现"搜得多准"

权重调参指南

场景vector-weightbm25-weight
语义优先(知识库问答)0.80.2
关键词优先(合同/型号检索)0.20.8
均衡模式0.50.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个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP