RAG 向量数据库选型:先看查询模式
不要先问哪个库最火
去年我给一个企业内部知识库做 RAG 落地,团队第一句话就是「用 Pinecone 还是 Milvus?」。我没接这个话,而是花了两周把真实查询日志拉出来分析——结果发现 70% 的查询都带着租户隔离、文档类型、时间范围这类过滤条件,而且有近 30% 的查询需要精确匹配产品编号。
这个发现直接否决了当时团队最倾向的方案。纯向量相似度检索根本扛不住这种查询模式。
向量数据库选型不该从工具开始,而该从查询模式开始。文档量、更新频率、过滤条件组合方式、权限模型和延迟预算,这些变量共同决定了你该走哪条路。热门工具如果不匹配你的查询模式,上线第一天就会出问题。
查询模式决定技术路径
在讨论任何具体产品之前,需要先理解向量检索的底层机制。RAG 系统里的向量检索本质是近似最近邻(Approximate Nearest Neighbor, ANN)搜索——在高维空间中找到与查询向量最接近的 K 个向量,但不保证找到的是绝对最近的那几个。
这个「近似」不是偷工减料,而是一个工程权衡:精确最近邻搜索的时间复杂度是 O(N·d),其中 N 是向量数量、d 是维度。当 N 达到千万级,暴力搜索在延迟上就不可接受了。ANN 算法通过预建索引结构,把查询复杂度压缩到 O(log N) 甚至更低,代价是召回率从 100% 降到 95%-99%。
两种主流索引结构
当前生产环境中最常见的两种 ANN 索引是 HNSW 和 IVF,它们的底层数据结构完全不同,适用的场景也不同。
HNSW(Hierarchical Navigable Small World) 基于图结构。它把向量组织成多层图:上层稀疏,用于快速跳跃到大致区域;下层密集,用于精细定位。查询时从顶层开始逐层向下遍历,每层走几步就跳到下一层。这种结构的优势是查询路径短、召回率高(通常 95% 以上),劣势是整个图必须常驻内存,内存占用与数据量线性相关。
HNSW 的核心调优参数有三个:
m(节点连接度):每个节点最多连接多少邻居。越大召回率越高,但内存和构建时间也越大。常用值 16-64。ef_construction(构建期搜索宽度):建索引时每个节点探索多少候选。越大图质量越好,构建越慢。常用值 128-512。ef_search(运行时探测深度):查询时探索多少候选。越大召回率越高,延迟越大。常用值 64-256。
IVF(Inverted File Index) 基于倒排结构。它先用 K-Means 把所有向量聚类到 nlist 个桶中,查询时只搜索距离查询向量最近的 nprobe 个桶。这种结构的优势是内存可控(可以结合量化压缩),劣势是需要预先训练聚类中心,数据分布漂移后可能需要重建索引。
IVF 的关键参数是两个:
nlist:聚类桶数量。经验值通常是4·√N到16·√N,N 是向量总数。nprobe:查询时搜索的桶数。越大召回率越高,延迟越大。
过滤条件如何影响索引选择
上述两种索引在「无过滤」场景下表现都很好。问题出在加上过滤条件之后。
企业 RAG 几乎都需要过滤:按租户隔离数据、按文档类型缩小范围、按时间窗口截取。2026 年初的一篇 arXiv 论文(Filtered Approximate Nearest Neighbor Search in Vector Databases)系统分析了过滤对 ANN 检索的影响,结论是:
- Pre-filtering(先过滤再搜索):用位掩码筛出符合条件的向量,然后在子集上做 ANN。当过滤条件很严格(比如只命中 1% 的数据)时,图结构会变得稀疏,HNSW 的召回率会急剧下降。
- Post-filtering(先搜索再过滤):先做 ANN 找到 top-K,然后过滤掉不符合条件的。风险是过滤后结果可能远少于 K 个,甚至为空。
- Runtime filtering(运行时过滤):图遍历过程中每访问一个节点就检查条件。延迟会增加(随机内存访问),但能保持较好的召回率。
Milvus 在 2.x 版本中实现了自适应策略——当符合条件的向量比例低于某个阈值时,自动从图遍历切换到精确扫描。Qdrant 则通过 payload 索引实现「零性能损耗」的过滤:对常用过滤字段建立独立的倒排索引,在图遍历前预先剪枝。
这不是纯理论问题。我在实际项目中就踩过这个坑。
案例一:租户过滤导致数据泄露
场景
一个 SaaS 平台为多个企业客户提供知识库问答。每个客户的文档通过 tenant_id 隔离。初期用 pgvector,查询逻辑是先做向量检索 top-20,再在应用层用 tenant_id 过滤。
翻车
上线两周后,安全审计发现 A 客户的问答结果中偶尔出现了 B 客户的文档片段。原因很直接:向量检索返回的 top-20 里只有 2-3 条属于当前租户,其余被过滤掉了,但应用层没有检查结果数量是否足够,直接把过滤后的少量结果喂给了 LLM。
更严重的情况是:当某个租户的文档量很少时,向量检索的 top-20 可能全部来自其他租户,过滤后结果为空,系统返回「未找到相关信息」——但知识库中明明有答案。
修复
把过滤条件下沉到数据库层,在向量检索阶段就完成租户隔离。pgvector 从 0.5 开始支持 HNSW 索引与普通 WHERE 条件结合,但需要注意过滤选择性和索引策略的配合。
# ❌ 错误做法:应用层过滤,存在数据泄露风险
def rag_query_bad(query_embedding, tenant_id):
# 先做向量检索,返回全局 top-20
results = db.execute("""
SELECT content, tenant_id
FROM document_chunks
ORDER BY embedding <=> %s
LIMIT 20
""", (query_embedding,))
# 应用层过滤——结果可能不足,甚至为空
filtered = [r for r in results if r.tenant_id == tenant_id]
return build_context(filtered)# ✅ 正确做法:数据库层过滤,向量检索和权限检查同时生效
def rag_query_correct(query_embedding, tenant_id):
# 过滤条件在 SQL 层执行,向量索引在过滤后的子集上搜索
results = db.execute("""
SELECT content
FROM document_chunks
WHERE tenant_id = %s
ORDER BY embedding <=> %s
LIMIT 20
""", (tenant_id, query_embedding))
return build_context(results)这两个版本的差异看起来只是 WHERE 子句的位置不同,但在 pgvector 中这涉及索引策略的选择。当使用 HNSW 索引时,pgvector 会在索引遍历过程中并行评估过滤条件(hybrid filter),但前提是过滤字段上也需要有合适的索引支持。如果 tenant_id 上没有 B-tree 索引,pgvector 可能退化为全表扫描。
-- 必须同时为过滤字段和向量列建立索引
CREATE INDEX idx_tenant ON document_chunks (tenant_id);
CREATE INDEX idx_embedding ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);案例二:纯向量检索漏掉精确匹配
场景
一个制造业客户的知识库包含大量产品手册,每份手册有唯一编号(如「MH-2024-0371」)。工程师提问时经常直接引用编号,比如「帮我查 MH-2024-0371 的维护流程」。
翻车
使用纯向量检索后,这类查询的召回率很低。原因是 embedding 模型会把编号编码成语义向量,但不同编号的向量在语义空间中可能非常接近(它们描述的都是类似类型的设备手册)。查询「MH-2024-0371」时,检索返回的可能是「MH-2024-0369」和「MH-2024-0372」的文档——语义相似但编号不对。
工程师连续几次拿到错误文档后,整个团队对 RAG 系统失去信任。
修复
引入混合检索:同时使用 BM25 关键词检索和向量语义检索,然后通过 RRF(Reciprocal Rank Fusion)合并结果。BM25 擅长精确匹配编号、术语和专有名词,向量检索擅长语义理解,两者互补。
# ❌ 错误做法:纯向量检索,无法处理精确匹配需求
def search_vector_only(query, collection):
query_embedding = embed(query)
results = collection.search(
query_embedding,
limit=10,
# 只有语义相似度,编号「0371」和「0369」几乎无法区分
)
return results# ✅ 正确做法:混合检索 + RRF 融合
from rank_bm25 import BM25Okapi
def search_hybrid(query, collection, bm25_index):
# 向量检索:捕获语义意图
query_embedding = embed(query)
vector_results = collection.search(
query_embedding,
limit=20,
)
# BM25 检索:捕获精确关键词(编号、术语)
tokens = tokenize(query)
bm25_results = bm25_index.get_top_n(tokens, n=20)
# RRF 融合:k 值通常取 60
rrf_scores = {}
k = 60
for rank, doc in enumerate(vector_results):
rrf_scores[doc.id] = rrf_scores.get(doc.id, 0) + 1.0 / (k + rank + 1)
for rank, doc in enumerate(bm25_results):
rrf_scores[doc.id] = rrf_scores.get(doc.id, 0) + 1.0 / (k + rank + 1)
# 按 RRF 分数排序返回
sorted_ids = sorted(rrf_scores, key=rrf_scores.get, reverse=True)
return sorted_ids[:10]Weaviate 和 Qdrant 都原生支持混合检索。Weaviate 的 GraphQL 接口可以在一个查询中同时执行 BM25 和向量检索并自动融合。Qdrant 在 2024 年推出了 BM42 混合检索框架,将 BM25 和稀疏向量检索统一到同一个 API 中。Milvus 通过分别创建稠密向量和稀疏向量字段来实现混合检索,灵活度更高但需要更多手动编排。
向量数据库选型矩阵
回到选型的核心问题。下面从多个维度对比当前主流的五个方案。
部署模式与扩展性对比
| 维度 | pgvector | Milvus | Qdrant | Weaviate | Pinecone |
|---|---|---|---|---|---|
| 部署方式 | PostgreSQL 扩展 | 自托管 / K8s 原生 | 自托管 / 云托管 | 自托管 / 云托管 | 纯 SaaS |
| 开源协议 | PostgreSQL License | Apache 2.0 (CNCF) | Apache 2.0 | Apache 2.0 | 闭源 |
| 数据规模上限 | ~5000 万 | 十亿级 | 十亿级 | 十亿级 | 无明确上限 |
| 扩展方式 | 单机(读副本可分担) | 分布式分片 | 分片 + 副本 | 分片 + 副本 | 自动弹性伸缩 |
| 内存模型 | 依赖 PostgreSQL 共享内存 | 可配置(DiskANN 降内存) | 支持量化压缩 | 需要较大内存 | 平台管理 |
| 运维复杂度 | 低(已有 PG 基础设施) | 高(需要 K8s + etcd + MinIO) | 中(单二进制部署) | 中(JVM 调优) | 极低(全托管) |
检索能力对比
| 维度 | pgvector | Milvus | Qdrant | Weaviate | Pinecone |
|---|---|---|---|---|---|
| 索引类型 | HNSW / IVFFlat | HNSW / IVF / DiskANN / Annoy | HNSW(量化可选) | HNSW | HNSW |
| 距离度量 | L2 / 内积 / 余弦 | L2 / IP / 余弦 / 汉明 / 杰卡德 | 余弦 / 欧氏 / 曼哈顿 | 余弦 / 欧氏 / 点积 | 余弦 / 欧氏 / 点积 |
| 过滤策略 | SQL WHERE(与索引并行) | 前置 / 自适应切换 | Payload 索引 + 运行时过滤 | 原生过滤 + GraphQL | 元数据过滤 |
| 混合检索 | PG 全文检索 + 向量 | 稠密 + 稀疏向量 | BM42 框架 | 原生 BM25 + 向量 | 2024 年引入,持续完善中 |
| 多向量支持 | 不支持 | 支持(多字段) | 支持 | 有限支持 | 不支持 |
索引算法适用规模对比
| 数据规模 | 推荐索引 | 预期召回率 | 内存占用参考 | 典型延迟 |
|---|---|---|---|---|
| <100 万 | FLAT 或 HNSW | 100% / 97%+ | 低 / 中 | <5ms / <3ms |
| 100 万-1000 万 | HNSW 或 IVF_FLAT | 95-98% | 中-高 / 中 | <5ms / <10ms |
| 1000 万-1 亿 | IVF_SQ8 或 HNSW(量化) | 93-97% | 中 / 中 | <10ms / <8ms |
| >1 亿 | DiskANN 或 IVF_PQ | 92-96% | 低 / 低 | <15ms / <20ms |
运维成本对比
| 维度 | pgvector | Milvus | Qdrant | Weaviate | Pinecone |
|---|---|---|---|---|---|
| 初始部署 | 一条 CREATE EXTENSION | 需要 K8s 集群 | 单二进制 / Docker | Docker Compose | 无需部署 |
| 备份恢复 | pg_dump 标准流程 | 需配置 S3/MinIO | 快照 API | 快照 + 备份插件 | 平台自动 |
| 索引重建 | REINDEX | 需要触发重建任务 | 自动增量更新 | 自动增量更新 | 平台管理 |
| 监控告警 | PG 生态工具 | Prometheus + Grafana | Prometheus 端点 | Prometheus 端点 | 控制台仪表盘 |
| 月成本参考(中等规模) | 0(复用现有 PG) | 服务器费用 ¥3000-8000 | 服务器费用 ¥2000-5000 | 服务器费用 ¥3000-6000 | $100-1000+ |
案例三:索引参数没调,延迟翻了 10 倍
场景
一个客服知识库,文档量约 200 万条 chunk,使用 Milvus + HNSW 索引。开发环境测试时延迟在 5ms 以内,上线后 P99 延迟飙到 50ms 以上。
翻车
排查后发现,开发环境的测试数据是随机生成的向量,分布均匀,HNSW 的图结构质量很好。生产数据是从真实文档生成的 embedding,存在明显的热点区域(大量关于退换货政策的 chunk 语义高度相似,向量挤在一起)。这导致 HNSW 图在热点区域的遍历路径变长,查询延迟上升。
另一个问题是 ef_search 参数设为了默认值 64,在生产数据的分布下不够用——需要探索更多节点才能找到足够近的邻居。
修复
调整了 HNSW 参数,并在数据入库后执行了一次全量索引优化。同时引入了量化压缩,将内存占用降低了约 40%。
# ❌ 错误做法:使用默认参数,不做针对性调优
from pymilvus import Collection, Index
collection = Collection("customer_service_chunks")
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
# 默认参数:m=16, ef_construction=200
# 生产数据有热点分布,默认参数图质量不够
"params": {} # 没有设置任何参数
}
collection.create_index("embedding", index_params)# ✅ 正确做法:根据数据分布和延迟预算调参
from pymilvus import Collection, Index
collection = Collection("customer_service_chunks")
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 32, # 连接度从 16 提高到 32,图更密,热点区域遍历更快
"efConstruction": 400, # 构建时探索更充分,图质量更高
"efSearch": 128, # 查询时探索更多节点,召回率从 ~93% 提升到 ~97%
}
}
collection.create_index("embedding", index_params)
# 同时设置搜索参数
collection.load()
collection.search(
data=[query_embedding],
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"ef": 128}},
limit=10,
)参数调优不是一次性工作。我后来建立了一个简单的基准测试流程:每次 embedding 模型更换或数据量增长超过 20%,就跑一轮 recall@10 和 P99 延迟测试。如果 recall 低于 95% 或 P99 超过 20ms,就调整 ef_search;如果调整 ef_search 后延迟仍然不达标,就考虑换索引类型(比如从 HNSW 切到 IVF_SQ8)。
选型决策流程
把上面的分析综合起来,选型可以按这个流程走:
这个流程没有唯一正确答案,但它把关键决策点显式化了。每个菱形节点都是一个需要你根据实际业务判断的分叉口。
距离度量:不是余弦一定最好
选型时另一个容易忽略的决策是距离度量。很多人默认用余弦相似度,但这不一定是最优选择。
| 距离度量 | 公式特点 | 适用场景 | 注意事项 |
|---|---|---|---|
| 余弦相似度 | 只看方向,忽略长度 | 文本语义匹配(文档长度不一) | 如果 embedding 已归一化,余弦等价于内积 |
| L2(欧氏距离) | 同时考虑方向和长度 | 图像特征、归一化后的向量 | 对向量长度敏感,长文档和短文档可能不公平 |
| 内积(点积) | 方向 + 长度的综合 | 已归一化的 embedding | 计算速度最快,但要求向量必须归一化 |
| 汉明距离 | 二进制位差异 | 二值化向量(极端压缩场景) | 精度损失大,只适合对精度要求不高的场景 |
OpenAI 的 text-embedding-3-small/large 和 text-embedding-ada-002 输出的向量已经归一化,此时余弦相似度和内积的排序结果完全一致,但内积的计算速度更快。如果你的 embedding 模型没有归一化输出,用余弦会更安全。
一个常见错误是在索引声明和查询时使用了不同的距离度量。比如建索引时声明用 L2,查询时按余弦排序——这会导致索引失效,退化为全量扫描,延迟可能翻 10 倍以上。
# ❌ 错误做法:索引和查询的距离度量不一致
# 建索引时用 L2
collection.create_index("embedding", {
"index_type": "HNSW",
"metric_type": "L2", # 索引按 L2 构建
})
# 查询时按余弦排序——索引失效,退化为暴力搜索
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"ef": 128}}, # 度量不匹配!
limit=10,
)# ✅ 正确做法:索引和查询使用相同的距离度量
METRIC = "COSINE"
collection.create_index("embedding", {
"index_type": "HNSW",
"metric_type": METRIC,
"params": {"M": 32, "efConstruction": 400},
})
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param={"metric_type": METRIC, "params": {"ef": 128}}, # 一致
limit=10,
)上线前的选型检查清单
以下是我在实际项目中总结的检查清单,按阶段分组。每一项都来自踩过的坑。
阶段一:需求分析
- 拉取至少一个月的真实查询日志(如果还没有系统,用业务方提供的 50 个典型问题)
- 统计查询中携带过滤条件的比例,以及过滤字段的种类和组合方式
- 确认是否有精确匹配需求(编号、术语、专有名词),如果有,混合检索是必选项
- 确认数据规模(当前 + 未来 12 个月预估),明确是百万级、千万级还是亿级
- 确认数据更新频率:每天新增/删除/更新的文档量
- 确认权限模型:是否需要行级权限(按租户、部门、角色)
阶段二:技术验证
- 用真实数据(不是随机生成的向量)测试候选方案,随机数据测不出热点分布问题
- 测试带过滤条件的召回率,不只是无过滤的 recall@10——过滤后召回率下降 5-10% 是常见的
- 测试 P99 延迟而不是平均延迟,长尾延迟才是用户体验的瓶颈
- 验证距离度量一致性:索引声明、查询参数、embedding 模型输出三者必须匹配
- 如果有混合检索需求,测试 BM25 + 向量的 RRF 融合效果,不能只看单路召回
阶段三:运维准备
- 确认索引重建的操作流程和耗时——embedding 模型更换或数据分布变化时可能需要重建
- 确认备份恢复策略,向量数据库的备份通常比普通数据库大很多
- 设置监控告警:索引大小增长速率、查询延迟 P99、召回率(通过定期抽样验证)
- 计算完整成本:不只是数据库本身,还有 embedding API 调用、索引构建的计算资源、运维人力
几个容易踩的坑
不要迷信 benchmark 排行榜。 ann-benchmarks.com 上的排名是在特定数据集和特定过滤条件下测出来的。你的数据分布、查询模式和过滤条件和 benchmark 不一样,排名可能完全不同。2026 年初 YDB 团队专门发了一篇文章质疑公开 ANN benchmark 的可信度,核心观点是:很多数据库在 benchmark 上用了最优配置,但在生产环境中不会这么配。
不要忽略 embedding 模型对选型的影响。 不同的 embedding 模型输出的向量维度不同(768、1024、1536、3072),维度越高内存占用越大,对索引策略的要求也不同。如果你计划未来更换 embedding 模型,选型时就要考虑索引重建的成本。
不要把向量数据库当成银弹。 RAG 系统的质量瓶颈可能在 chunk 策略、embedding 模型质量或 reranker 效果上,而不是向量检索本身。我见过不少团队花大量时间优化向量检索参数,最后发现换一个 reranker 模型带来的提升更大。
pgvector 的适用边界比很多人想的要宽。 如果你的数据量在 500 万以下,过滤条件不复杂,而且已经在用 PostgreSQL,pgvector 大概率是成本最低、运维最简单的选择。不需要一上来就引入 Milvus 或 Pinecone。pgvector 0.7+ 的 HNSW 实现已经相当成熟,配合 pgvectorscale 的 StreamingDiskANN 可以扩展到更大规模。
参考资料
-
Johnson, J., Douze, M., & Jégou, H. (2019). "Billion-scale similarity search with GPUs." IEEE Transactions on Big Data. Faiss 论文,IVF 索引的经典实现。https://arxiv.org/abs/1702.08734
-
Malkov, Y. A., & Yashunin, D. A. (2018). "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs." IEEE Transactions on Pattern Analysis and Machine Intelligence. HNSW 算法的原始论文。https://arxiv.org/abs/1603.09320
-
Chen, Z. et al. (2026). "Filtered Approximate Nearest Neighbor Search in Vector Databases." arXiv preprint. 系统分析了过滤条件对 ANN 检索的影响,提出 MoReVec 基准。https://arxiv.org/html/2602.11443
-
Milvus 官方文档. "Vector Index." 覆盖 HNSW、IVF、DiskANN 等索引类型的参数说明和选型指南。https://milvus.io/docs/index.md
-
Qdrant 官方文档. "Hybrid Search with BM42." BM42 混合检索框架的技术说明。https://qdrant.tech/documentation/concepts/hybrid-search/
-
Zilliz 技术博客. "RAG 搭建中,如何选择最合适的向量索引?" HNSW / IVF / DiskANN 在不同数据规模下的选型建议。https://zilliz.com.cn/blog/How-to-choose-the-most-suitable-vector-index-in-RAG-construction
-
Weaviate 官方文档. "Hybrid Search." 原生 BM25 + 向量混合检索的实现说明。https://weaviate.io/developers/weaviate/search/hybrid
-
ann-benchmarks.com. 向量检索算法的标准评测框架,提供可复现的 benchmark 结果。https://ann-benchmarks.com/