把 Embedding 当普通字段逐条算距离,数据一上规模就退化为秒级全表扫描。这篇教程讲清 ANN 索引的三条技术路线(IVF 分桶、PQ 量化、HNSW 图导航)、HNSW 为什么快,以及主流向量库的选型与召回-延迟权衡。

它解决什么问题

RAG 在线检索要回答一个几何问题:在上百万条 768~3072 维的向量里,几十毫秒内找出离查询向量最近的 K 条。不做索引就是暴力扫描:100 万条 1024 维向量,单次查询约十亿次乘法,延迟轻松到秒级,对实时问答不可用。

ANN(Approximate Nearest Neighbor,近似最近邻)的思想是接受一个工程取舍:不保证找到数学上绝对的最近邻,而是用预先构建的索引结构跳过绝大多数不相关向量,只在一个小候选集里做精确比较——用 1%~5% 的召回损失换几个数量级的速度。向量数据库要解决的就是这个权衡的工程化:索引构建、内存占用、元数据过滤、增量更新、分布式扩展,全都得管。

核心原理

距离度量先定坐标系

相似度度量必须在建索引前定死,常见的三种:

  • 余弦距离:只看方向不看长度,契合「语义方向接近」的检索目标,文本 RAG 最常用。
  • 内积:向量归一化后与余弦排序等价,计算更直接。
  • L2 欧氏距离:衡量绝对距离,适合模型明确按 L2 训练的场景。

一个高频踩坑点:查询运算符必须与索引的 operator class 一致。索引用 cosine 构建、查询却写 L2,引擎可能直接弃用索引,退化为全表扫描。

ANN 的三条路线

所有 ANN 索引都在做同一件事:把「全库比较」变成「小范围比较」,区别在于怎么划范围。

IVF(倒排索引):先用 K-Means 把向量空间聚成 nlist 个桶,每桶配一个倒排列表。查询时先找离查询点最近的 nprobe 个桶,只在桶内暴力比较。直觉是「先定城区,再挨家挨户」——nprobe 越大召回越高、延迟越高。

PQ(乘积量化):解决的是内存而非速度。把高维向量切成若干段,每段用一个码本 ID 近似表示,存储从上千个 float 压成几十个字节,距离计算用查表代替乘法。代价是有损:量化误差直接吃掉召回率,常与 IVF 组合成 IVF-PQ,用于亿级规模。

HNSW(图索引):把向量组织成分层图,查询时在图上导航。它是当前召回-延迟综合表现最强的主流算法,下面展开讲。

深入机制

HNSW 为什么快:小世界图导航

HNSW 的直觉可以类比「多层高速公路网」:最上层是稀疏的跨省高速,中间层是城际快速路,最底层是街区小路。跨省出行你不会在胡同里穿行,而是先上高速大跨度跳跃,接近目的地后再逐级下到小路精确定位。

机制上对应三点:

  1. 层次化构建:每个节点按指数衰减的概率被分配到最高层,越往上节点越少,天然形成金字塔。
  2. 贪心导航:查询从顶层入口点开始,每层反复跳到离查询点更近的邻居,直到本层无法更近,再下降一层。
  3. 由粗到精:高层负责快速逼近目标区域,底层(第 0 层包含全部节点)负责精细找出最终近邻。

小世界图的关键性质是「任意两点间存在短路径」,配合分层结构,查询复杂度近似对数级——百万级数据只需几十到几百次距离计算,这就是它快的根本原因。

三个参数控制权衡

  • m:每个节点的最大连接数。图越密,导航越不容易陷入局部最优,召回越高;但内存和构建时间同步上升。
  • ef_construction:构建时为每个新节点搜索邻居的范围。越大索引质量越好,构建越慢——这是离线成本。
  • ef_search:查询时的候选队列大小。这是运行时旋钮,直接调节召回-延迟曲线,可以不重建索引在线调:
# pgvector 示例:会话级调整 ef_search,并用执行计划验证索引命中
cur.execute("SET hnsw.ef_search = 100")
cur.execute(
"SELECT id, content FROM chunks ORDER BY embedding <=> %s::vector LIMIT 10",
(query_vec,),
)
# 用 EXPLAIN ANALYZE 确认走的是 Index Scan 而不是 Seq Scan

实用调法:先固定 m 和 ef_construction 建好索引,再用标注集画 ef_search 的召回-延迟曲线,达到业务可接受召回就停——再往上只是用延迟换微小收益。

过滤条件与索引的博弈

带 WHERE 过滤的向量查询有个经典陷阱:ANN 索引通常先按距离取候选再应用过滤,过滤条件严格时 Top-K 会被过滤得所剩无几——比如要 10 条「category=Java」的相似文档,候选里只有 3 条满足条件,就只能返回 3 条。应对手段包括调大 ef_search 让更多候选进入过滤、按高频过滤条件建部分索引、或使用支持迭代扫描的版本(过滤后结果不足时继续扫索引)。生产上必须用执行计划确认过滤后仍命中索引。

工程实践

  • 先压测再选型:用业务数据、真实 Top-K、真实过滤条件测 p99 延迟和召回率,公开 benchmark 只作初筛参考。
  • 监控两个指标:p99 查询延迟和召回率抽样(定期用暴力扫描结果对比 ANN 结果)。索引膨胀、大量删除更新之后,这两个指标会先劣化。
  • 索引重建要蓝绿:重建期间旧索引继续服务,新索引就绪后切换,避免查询中断。
  • 冷热分层:热数据放 SSD 与内存,冷数据沉到对象存储按需加载。
  • 混合检索预留通道:向量库若不支持 BM25,需要外挂搜索引擎做关键词召回,选型时把这条算进架构成本。

对比与权衡

索引算法对比

算法 原理 优势 劣势 适用场景
Flat 暴力 全量计算距离 100% 精确 数据一大就不可用 小规模、召回基准
IVF 聚类分桶 内存友好、构建快 需训练、召回略低 大规模、内存敏感
PQ 向量压缩编码 存储开销极低 精度损失较大 亿级海量数据
HNSW 分层图导航 低延迟、高召回 内存大、构建慢 中大规模在线检索

向量库选型对比

方案 定位 优势 代价 适合场景
pgvector PostgreSQL 扩展 技术栈统一、事务一致、SQL 过滤 与业务查询争资源、单机天花板 中小规模、已有 PG
Elasticsearch 搜索引擎 BM25 + 向量混合原生支持、生态成熟 资源消耗大、调优复杂 关键词检索为主的场景
Qdrant 专用向量库 Rust 实现内存效率高、过滤能力强 多一套运维组件 中大规模、重过滤
Milvus 专用向量库 分布式、索引类型全、亿级规模验证 架构重、运维门槛高 大规模、高 QPS

选型顺序:已有 PostgreSQL 且规模在百万级以内,先用 pgvector;业务强依赖关键词检索用 ES;规模或 QPS 涨到单机撑不住,再迁 Milvus/Qdrant;不想自运维就看托管服务——但索引参数、召回评测和成本控制仍然逃不掉。

面试视角

为什么 RAG 常用余弦相似度? 文本语义关心方向而非长度;向量归一化后内积与余弦排序等价,可用内积省计算。关键是查询、文档、索引三者的度量必须一致,否则索引可能不生效。

HNSW 为什么快? 分层小世界图:高层稀疏边做大跨度跳跃、快速逼近目标区域,底层密集边做精细搜索,配合贪心导航把查询复杂度压到近似对数级,百万数据只需几百次距离计算。

HNSW 的 ef_search 怎么调? 它是运行时参数,控制查询候选队列大小:调大召回升、延迟升。做法是固定构建参数,用标注评测集画召回-延迟曲线取拐点;无需重建索引,可在线实验。

IVF 和 HNSW 怎么选? 内存充足、追求低延迟高召回选 HNSW;内存紧张、规模超大、可接受召回损失选 IVF 或 IVF-PQ。IVF 要调 nlist/nprobe,数据分布明显变化时可能需要重训聚类中心。

向量从 100 万涨到 1 亿,架构怎么调? 单机 HNSW 的内存先撑不住:考虑量化(PQ)压缩内存、分片分布式(Milvus)、冷热分层;同时重建评测基线——规模变了,索引参数和召回目标都要重新校准。

小结

  1. ANN 的本质是用少量召回损失换数量级速度提升;没有索引的高维检索就是全表扫描。
  2. 三条路线各管一件事:IVF 管划分范围,PQ 管压缩内存,HNSW 管查询速度。
  3. HNSW 快的根源是分层小世界图导航;m、ef_construction、ef_search 分别控制图密度、构建质量和查询期权衡。
  4. 过滤查询要盯执行计划,先 ANN 后过滤可能让 Top-K 结果不足。
  5. 选型跟着现有技术栈和规模走:pgvector 起步,ES 做混合检索,Milvus/Qdrant 扛规模——压测数据比品牌重要。