混合检索的极简主义:PostgreSQL + pgvector 生产级方案
一、什么是混合检索?
在知识问答系统中,单一的检索方式总有局限:
- 关键词检索(BM25):擅长匹配精确术语,但无法理解语义。用户问"怎么解决登录失败",它只认识"登录""失败"这几个字,不理解"认证""鉴权""Session过期"等表达方式。
- 语义检索(向量):理解含义,能泛化召回,但对低频术语、缩写词、代码片段等缺乏敏感性。
混合检索将两者结合:用 BM25 保证精确匹配,用向量保证语义泛化,再用 RRF(倒数排名融合)把两路结果合并,取长补短。这套组合已成为 RAG 系统的标准检索模式,在 Elasticsearch 8.x、Solr 等主流搜索引擎中均有官方实现。
二、一个被忽视的事实
很多架构师接到检索需求后的第一反应,是画出这样一张架构图:
业务库(MySQL)→ 发件箱模式 → Kafka → 消费组 → Elasticsearch(BM25)+ Milvus(向量)→ 融合服务 → 返回结果
这张图很漂亮,但它隐含了一个前提:你的数据量足够大,值得用分布式架构去拆解。
现实中有大量场景并不需要这么复杂的方案:
- 企业内部知识库:几百到几千份文档
- 个人或小团队的 RAG 应用:几千万字的中文资料
- 垂直领域的问答助手:几万到十几万条切块
这些场景的共同特点是:数据量不大,但对稳定性要求不低,对运维投入要求极低。
这时候,PostgreSQL + pgvector 提供了另一种选择:不用拆,一个库扛所有。
三、PostgreSQL 如何"一人三角"?
PostgreSQL 通过扩展机制,在同一个内核上叠加多种数据模型:
- 关系数据库:存文档元数据、切块映射、状态字段------这是它的原生能力
- 全文检索引擎 :通过
tsvector和 GIN 索引,构建倒排表,支持 BM25 风格的全文检索 - 向量检索引擎 :通过
pgvector扩展,支持 HNSW 或 IVFFlat 索引,做向量的近似最近邻搜索
这三个角色共享同一套基础设施:
统一内存池:热数据、倒排词典、HNSW 图结构全部在 Shared Buffers 中,查询无需跨网络,延迟极低。
ACID 事务:写入一条记录,要么三个索引同时更新,要么一起回滚。不存在"数据已入业务库但索引没更新"的中间状态,也不需要发件箱、消息队列、最终一致性这些分布式补偿机制。
单一进程:BM25 检索和向量检索在同一个数据库进程内完成,没有序列化开销,没有网络往返,没有连接池争用。
关键理解:这三个索引是独立的物理文件,互不干扰。查询优化器会根据统计信息选择走 GIN 索引、HNSW 索引,还是两者都走再做 Bitmap 合并。
四、索引选型:HNSW 还是 IVFFlat?
pgvector 提供两种向量索引,选型依据是数据规模:
IVFFlat:先对向量做聚类,查询时只搜索最近的几个聚类中心。构建快,内存小,但需要预先训练聚类中心,且数据分布变化后需要重训。适合百万级以下、数据相对稳定的场景。
HNSW:构建一张分层图结构,查询时从顶层向下贪婪搜索。无需训练,支持动态插入,召回率极高(0.95-0.99),查询速度是 IVFFlat 的数倍到数十倍。代价是构建稍慢,内存占用略高。
对于持续增长的场景,HNSW 是更合适的选择------新数据直接插入图结构,无需等待聚类中心重算。构建时间较长(对于十万级数据约 20 分钟),但这是一次性成本,远低于日常运维的复杂度。
两个核心参数直接影响性能与内存的平衡:
m(每个节点的双向链接数):值越大,索引质量越高、查询越快,但内存和构建时间也增加。一般取 12-48,默认 16。ef_construction(构建时的动态列表大小):值越大,索引质量越高,构建越慢。建议至少为2 * m。
查询时还有第三个参数 ef_search,可在运行时动态调整:值越大召回率越高,但查询越慢。适合在负载低时提高召回率,负载高时牺牲少量召回率换取速度。
五、混合检索的实现思路
5.1 为什么不依赖数据库内置的混合查询?
pgvector 目前不提供 BM25 + 向量的原生混合检索能力,这是设计决策而非缺陷------它将融合逻辑留给应用层,保持了索引内核的简洁性。
在应用层做融合有三个好处:一是融合算法(如 RRF)可以独立迭代升级;二是可以灵活调整两路检索的权重和各自的超参数;三是不受数据库版本限制,任何支持向量和全文检索的数据库都可复用这套逻辑。
5.2 查询流程
用户输入一个问题后,应用层并行发起两路检索:
- 全文检索:将问题转为 ts_query,走 GIN 索引,召回 Top K(通常取 30-50)
- 向量检索:将问题转为 embedding,走 HNSW 索引,召回 Top K(同样取 30-50)
两路返回的是各自排序的文档 ID 列表,不涉及具体分数(分数尺度不同,无法直接比较)。
5.3 RRF 融合算法
RRF(倒数排名融合)是目前业界最稳定的多路召回融合算法:
RRF_score(d) = Σ 1 / (k + rank_i(d))
其中 k 为平滑常数,通常取 60。RRF 不依赖分数归一化,天然支持多路召回的公平融合------无论 BM25 打分是 0.1 还是 10,无论向量距离是 0.01 还是 1.0,最终只靠排名决定权重。Elasticsearch 8.x 和 Solr 均将 RRF 作为标准融合方式内置。
应用层计算出每个文档的 RRF 分数后,按分数降序排列,返回 Top N 给大模型作为上下文。
六、数据写入与索引更新
6.1 写入流程
新文档进入系统后,经历以下步骤:
- 文档解析,提取纯文本
- 按策略切块(512 tokens,10% 重叠,优先在标点处切割)
- 插入 chunks 表,全文索引同步更新(GIN 索引在事务提交时即可见)
- 发送异步任务,调用 Embedding 模型生成向量
- 向量回填,HNSW 索引自动插入新节点
关键点:全文索引是同步更新的------数据插入提交后,用户立即就能搜到新文档。向量索引是异步更新的------延迟取决于 Embedding 计算速度(毫秒到秒级)。这种设计兼顾了实时性与吞吐量。
6.2 为什么不需要消息队列?
分布式架构使用 MQ 是因为"写入业务库"和"写入 ES/Milvus"是多个独立的网络调用,必须用消息队列保证最终一致性。
但在单库方案中,全文索引和向量索引只是同一个数据库的不同索引结构。一个 INSERT 提交后,所有索引同时更新------不需要发件箱,不需要 MQ,不需要补偿任务。ACID 事务天然解决了数据一致性问题。
6.3 定期维护
虽然日常写入无需人工干预,但有两个维护任务值得关注:
全文索引的 IDF 更新 :随着新文档加入,词项的文档频率会变化,旧 IDF 会漂移。解决方式是定期重建全文索引(支持 CONCURRENTLY 在线重建,不阻塞查询),建议每周低峰期执行一次。
向量索引的长期健康:HNSW 索引在大量插入后,图结构可能产生局部退化,导致召回率缓慢下降。建议每月或每季度重建一次 HNSW 索引(同样支持在线重建),恢复最优结构。
对于十万级数据量,重建全文索引只需几十秒,重建 HNSW 索引也只需几分钟。这些操作可以在低峰期自动执行,对用户完全透明。
七、这个方案的边界在哪里?
没有任何方案是万能的。PostgreSQL + pgvector 的适用边界由以下因素决定:
数据量上限:业界公认的 pgvector 舒适区在 100 万条向量以内。超过这个量级,查询延迟和内存占用会显著上升。
并发上限:单机 PostgreSQL 的混合检索 QPS 通常在 100-200 之间(取决于数据量和索引参数)。内部系统通常远低于这个值。
向量维度限制:pgvector 对 HNSW 索引的支持上限为 2000 维。主流的 bge-large(768维)、OpenAI text-embedding-3-small(1536维)都在安全范围内。
当数据量突破 100 万、或 QPS 超过 200、或需要多租户隔离等复杂权限控制时,就是考虑迁移到 ES + Milvus 分布式架构的信号。但在此之前,单库方案足够支撑 3-5 年的增长。
八、总结
PostgreSQL + pgvector 方案的核心理念是:
- 用 ACID 事务解决数据一致性问题,而不是靠发件箱 + MQ + 补偿任务
- 用共享内存池解决数据访问延迟问题,而不是靠分布式缓存
- 用单一技术栈降低运维复杂度,而不是靠多套系统的组合来换取理论上无限的水平扩展
这套方案不够"酷"------它没有 Kafka 的吞吐,没有 Milvus 的专用优化,没有微服务的灵活部署。但它足够稳、足够简单,适合绝大多数中小规模 RAG 场景。
好的架构不是技术越多越好,而是恰到好处地匹配场景。 对于百万级 Chunks 以内的知识问答系统,PostgreSQL + pgvector 就是那个"恰到好处"的选择。