跳至主要内容
向量数据库选型的四个坐标轴

向量数据库选型的四个坐标轴

有次压测一个检索服务,纯向量查询 P99 是 20 毫秒,加上「只看本部门文档」这个过滤条件之后变成 300 毫秒,而且召回率掉了。代码没改,索引没改,只是多了一个 filter。查下来是向量库在过滤后的子集上放弃了图索引,退化成逐条扫描。那次之后我选向量库,第一件事不是比谁的纯 ANN 快,是测带过滤条件的 ANN。

选型讨论里最容易跑偏的地方,是把「向量数据库」当成一个独立的、必须上新组件的东西。实际上它只是第 4 篇离线链路末端的一个存储与索引实现,选它的依据应该来自语料规模、查询形态和运维边界。

本系列第 7/15 篇 · 上一篇:Embedding 选型:别只看榜单分数 · 下一篇:混合检索:稀疏、稠密与分数融合

先弄清 ANN 在牺牲什么

精确最近邻要把查询向量和库里每一个向量算一遍相似度,复杂度 O(N·d)。近似最近邻(ANN)用索引结构换掉大部分距离计算,代价是不保证找到真正的最近邻,只能用召回率衡量。所以任何 ANN 的讨论都绕不开一个三角:

TEXT5 行
1
2
3
4
5
     召回率(recall@K)
▲
/ \
/ \
内存/存储 ───── 延迟

三个量两两牵制,调参基本就是在这个三角形里挪位置。理解每种索引怎么取舍,比记住参数名有用。

Flat / 暴力扫描:无损,零索引构建成本,几万到几十万条向量时完全够用。归一化矩阵乘一次余弦相似度,几十毫秒的事。第 4 篇提到的小语料场景(几百个 chunk)连数据库都不用,启动时读进内存即可。

HNSW(分层可导航小世界图):把向量建成多层图,查询从顶层入口贪心走到底层。核心参数 M(每个节点的邻居数)、efConstruction(建图时的候选宽度)、efSearch(查询时的候选宽度)。M 越大图越密、内存越高、召回越好;efSearch 是在线可调的旋钮,调大就多走一些节点,召回升、延迟升。它的优点是查询快、召回高,缺点是内存占用大(图结构本身要常驻),构建慢,删除和更新相对麻烦。

IVF(倒排文件索引):先聚类成 nlist 个桶,查询时只扫描最近的 nprobe 个桶。参数少、内存省、构建快,但聚类质量决定上限,数据分布漂移后要重训聚类中心。nprobe 从 1 调到 10,召回和延迟几乎线性变化,是很直观的调节手段。

PQ(乘积量化):把高维向量切段、每段量化成码本里的编号,用紧凑编码近似距离。主要目的是压缩存储,让千万到亿级向量能放进内存,代价是距离失真、召回下降。常见组合是 IVF-PQ 做粗排,再用原始向量对候选做一次精确重算(refine)。

DiskANN / 基于磁盘的图索引:把图放在 SSD 上,只把少量枢纽留在内存,用 IO 换内存。适合超大规模且预算受限的场景,对磁盘随机读性能敏感。

索引 召回 延迟 内存 适合规模 主要风险
Flat 100% 随 N 线性 低 十万级以内 规模涨了会突然变慢
HNSW 高 很低 高 百万—千万 内存成本、更新删除
IVF / IVF-PQ 中—高 低 中—低 千万—亿 分布漂移、量化失真
DiskANN 类 中—高 中 低 亿级 依赖 SSD 性能

参数不要抄默认值,要实测。我固定一份评测集,扫一遍参数网格,画出「recall@20 — P99 延迟」曲线,选拐点:

PYTHON3 行
1
2
3
grid = [(ef, recall_at_20(ef), p99_latency(ef))
for ef in (16, 32, 64, 128, 256)]
# 取 recall 增益开始小于延迟增幅的那个点,而不是 recall 最高的点

一个很实际的经验:很多系统把 efSearch 设得过高,追求 99% 的 ANN 召回,但真正决定端到端效果的是最终送进模型的那 5 个片段。ANN 召回从 95% 提到 99% 带来的收益,往往远小于后面加一层重排序。召回深度和排序精度要一起分配预算,这点第 9 篇会展开。

过滤:最容易失守的一跳

第 5 篇说过 acl 必须下推到召回阶段。问题在于,过滤和 ANN 的组合有两种实现,表现差别很大:

pre-filter:先按条件筛出候选集合,再在子集里做 ANN。如果子集很小,有些实现会直接退化成暴力扫描;如果子集占比大,需要索引支持「跳过不满足条件的节点」,否则图遍历会被大量无效节点拖慢。

post-filter:先 ANN 取 top_k,再过滤。实现简单,但过滤后可能只剩两三条,上下文稀薄,而且 top_k 越大浪费越多。

更隐蔽的是过滤条件高度选择性时召回质量下降:HNSW 的图是在全量数据上建的,入口到目标节点的路径可能大量经过被过滤掉的节点,图被剪得支离破碎,连通性变差,召回率明显掉。有些引擎提供「按分区建独立索引」或「过滤感知的图」来缓解。

所以我实测时一定覆盖这几组:无过滤、过滤后占全量 50%、占 10%、占 1%。只测无过滤的性能,等于没测。另外要确认过滤字段是不是被正确索引了——标量字段没建索引时,过滤本身就会变成全表扫描。

选型的四个坐标轴

一、规模与增长。十万级以内,Flat 加内存矩阵足够,别引入新组件;百万级,HNSW 是主流;千万以上要开始考虑量化、分片和磁盘索引。但比当前规模更重要的是增长速度:半年翻十倍的话,现在就该验证分片方案,而不是等打满内存再迁移。

二、查询形态。是否需要混合检索(同时跑 BM25 和向量)、是否需要多向量、是否需要标量过滤、是否需要分组去重(按 doc_id 去重取最优片段)、是否要支持多租户隔离。这几项常常比「纯向量 QPS」更能决定选型。

三、运维边界。团队里有没有人愿意维护一套分布式系统?能不能接受单独的备份、监控、升级、容量规划?如果业务数据本来就在 Postgres,pgvector 让向量和元数据、事务、权限待在同一个库里,运维成本可能低一个数量级。代价是超大规模下的性能和索引能力不如专用引擎。

四、一致性与更新。文档频繁增删时,索引的删除是真的删了还是标了软删除?重建期间能否继续服务?能不能做原子切换 active_version(第 4 篇的双索引灰度)?这几项在内容型系统里非常关键,却很少出现在选型对比表里。

常见的几类选择,我按自己的理解粗分:

方案 形态 适合 主要限制
内存矩阵 / FAISS 库 嵌入式 小语料、离线评测、实验 无持久化与并发管理,得自己包
pgvector Postgres 扩展 已有 PG、百万级以内、强过滤与事务 大规模 ANN 性能与内存
Elasticsearch / OpenSearch 搜索引擎加向量 已有 ES、稀疏+稠密混合检索 向量索引调优空间相对有限
Qdrant / Weaviate / Milvus 专用向量库 百万级以上、多租户、复杂过滤 多一套系统要运维
云托管检索服务 托管 不想运维、吞吐波动大 成本、数据合规、供应商锁定

表里没有「最好」,只有约束下的取舍。我做过最划算的一次选型是不上向量库:一个内部知识库只有几百篇文档,向量存 sqlite,启动时读进 numpy 矩阵,暴力算余弦,几十毫秒出结果,全量重建几分钟。省掉的是一整套部署、备份和版本兼容问题。规模翻到十万级之后再迁也不迟。

上线前必须实测的四组数据

不管选哪个,我会把这几组结果留档,作为以后调参的基线:

  1. 纯 ANN 的 recall@20 与 P99,用同一份标注评测集,别用合成向量;
  2. 带过滤条件在 50% / 10% / 1% 选择性下的召回与延迟;
  3. 写入与重建:全量构建耗时、增量写入吞吐、删除后的空间回收;
  4. 并发下的长尾:QPS 拉到预期峰值时 P99 而不是平均值,以及查询超时后的降级行为。

第 4 组最容易被忽略。向量检索超时不能让整个 RAG 请求失败,我的降级顺序是:降低 efSearch 重试一次 → 退回 BM25 单路结果 → 返回「暂时无法检索」而不是让模型凭空作答。降级路径要在压测里真的走到,否则线上第一次超时就是一次幻觉输出。

优劣与边界

专用向量库的价值集中在规模:高维索引、过滤感知的 ANN、水平扩展、多租户隔离、增量更新。代价是引入新的有状态组件,以及随之而来的备份、升级、容量和数据同步。嵌入式方案简单可控,但一旦语料或并发上一个量级,就得自己补上所有缺失的能力。

还有一个判断值得写下来:如果你的主要痛点是「精确字符串查不到」,换向量库解决不了。那是表示和检索通道的问题,属于下一篇的混合检索。同理,如果痛点是「召回的前十条里正确答案排在第七」,那是排序问题,属于第 9 篇。把存储层的问题和排序层的问题分开,能省掉很多无意义的迁移。

现在假设索引和向量库都选好了,纯向量通道仍然会在错误码、编号、版本号上失手。下一篇讲怎么补一路稀疏检索,以及两路结果到底该怎么合并。你的库里,带 acl 过滤的查询和不带过滤的查询,P99 差多少倍?

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#ANN #RAG #向量数据库

评论