向量相似度、召回率与向量索引:从 HNSW 到 IVFFlat
向量检索的核心问题是:
给定一个查询向量,如何从大量向量中快速找出最相似的 topK 个结果。
在小数据量下,可以直接暴力计算所有向量距离。但当数据达到百万、千万甚至更大规模时,就需要向量索引,例如 HNSW、IVFFlat、DiskANN 等。
本文整理向量相似度、召回率、pgvector、Milvus、HNSW、IVFFlat 的基本原理和工程选择。
1. 常见向量相似度计算方法
1.1 余弦相似度 Cosine Similarity
余弦相似度衡量两个向量方向是否接近:
|
|
越接近 1 越相似。
适合:
- 文本 embedding
- 语义搜索
- RAG 检索
- 向量长度不重要,只关心方向的场景
在大模型 embedding 检索里,余弦相似度非常常见。
1.2 点积 Dot Product / Inner Product
|
|
如果向量已经归一化,点积和余弦相似度等价。
适合:
- 已归一化 embedding
- 推荐系统
- 最大内积搜索 MIPS
注意:未归一化时,点积会受向量模长影响。
1.3 欧氏距离 L2 Distance
|
|
距离越小越相似。
适合:
- 图像特征
- 聚类
- 几何空间意义较强的向量
1.4 其他方法
| 方法 | 适合场景 |
|---|---|
| L1 / 曼哈顿距离 | 稀疏特征、逐维差异累加 |
| Pearson 相关系数 | 用户评分、趋势相似 |
| Jaccard 相似度 | 标签集合、关键词集合 |
| Hamming 距离 | 二进制向量、哈希向量 |
| Mahalanobis 距离 | 异常检测、统计建模 |
实际做文本 embedding 检索时,常见组合是:
|
|
或:
|
|
2. 召回率是什么意思?
召回率 Recall 表示:
所有真正相关的结果里,被系统找回来了多少。
公式:
|
|
例子:
数据库里真实相关文档有 100 篇,系统找回了 80 篇:
|
|
3. 召回错误算不算召回率?
不算。
召回率只关心:
应该找回的,有没有找回来。
错误召回,也就是“不相关但被返回”的结果,不会提高召回率。
它影响的是另一个指标:Precision,精确率。
|
|
例如返回 10 条结果:
|
|
那么:
|
|
如果真实相关结果总共有 20 条,而返回结果里只有 8 条相关:
|
|
在向量数据库里,常见的 topK recall 通常是:
近似索引返回的 topK,和暴力精确搜索 topK 的重合比例。
4. 为什么需要向量索引?
暴力搜索需要计算查询向量和所有向量的距离。
|
|
复杂度接近:
|
|
当 N 很大时,查询会变慢。
向量索引的目标是:
不扫描全部向量,而是快速定位最可能相似的一小部分候选。
代价是:
大多数向量索引是近似最近邻搜索 ANN,速度更快,但可能损失一点召回率。
5. HNSW 原理简介
HNSW 全称是 Hierarchical Navigable Small World,是一种基于图的近似最近邻搜索算法。
一句话:
HNSW 把向量组织成多层近邻图,查询时先在高层快速导航,再到底层精细搜索。
5.1 HNSW 的多层图结构
graph TD
subgraph Layer_2["Layer 2: 稀疏高速层"]
A2((A)) --- K2((K))
end
subgraph Layer_1["Layer 1: 中间导航层"]
A1((A)) --- B1((B)) --- K1((K)) --- P1((P))
end
subgraph Layer_0["Layer 0: 全量近邻图"]
A0((A)) --- B0((B)) --- C0((C)) --- D0((D)) --- K0((K)) --- P0((P)) --- Q0((Q))
end
A2 --> A1
K2 --> K1
A1 --> A0
B1 --> B0
K1 --> K0
P1 --> P0
高层节点少,边更长,用于快速接近目标区域。
底层节点多,连接更密,用于精细查找最近邻。
5.2 HNSW 查询流程
flowchart TD
Q["查询向量 q"] --> E["从最高层入口点开始"]
E --> G["当前层贪心搜索,找到更近节点"]
G --> D{"是否到达 Layer 0?"}
D -- 否 --> L["下降一层"]
L --> G
D -- 是 --> S["在底层扩大候选搜索"]
S --> T["返回 topK 近邻"]
简化流程:
|
|
5.3 HNSW 为什么快?
因为它不是全量扫描,而是在图上跳转。
暴力搜索:
|
|
HNSW:
|
|
工程上通常能取得很好的速度和召回平衡。
5.4 HNSW 为什么是近似的?
因为它只搜索图上的一部分节点。
如果真实最近邻不在搜索路径附近,就可能漏掉。
可以通过增大搜索参数提高召回率。
在 pgvector 里:
|
|
ef_search 越大:
- 搜索候选越多
- 召回率越高
- 查询越慢
6. IVFFlat 原理简介
IVFFlat 可以拆成两部分:
|
|
一句话:
IVFFlat 先用聚类把向量分桶,查询时只搜索最接近查询向量的几个桶。
6.1 建索引:聚类分桶
IVFFlat 通常使用类似 k-means 的方法,把向量聚成多个簇。
每个簇有一个中心点 centroid。
flowchart TD V["全部向量"] --> K["k-means 聚类"] K --> C1["簇中心 1"] K --> C2["簇中心 2"] K --> C3["簇中心 3"] C1 --> L1["list_1: v1, v8, v19..."] C2 --> L2["list_2: v2, v5, v11..."] C3 --> L3["list_3: v3, v7, v13..."]
这个结构类似:
|
|
所以叫倒排文件。
6.2 查询:只搜索部分桶
查询向量 q 来了以后:
|
|
flowchart TD Q["查询向量 q"] --> C["计算 q 到所有中心点距离"] C --> P["选择最近的 probes 个桶"] P --> S["扫描这些桶中的原始向量"] S --> R["精确计算距离并返回 topK"]
6.3 nlist 和 probes
IVFFlat 有两个关键参数:
|
|
nlist 越大:
- 桶更多
- 每个桶更小
- 单桶扫描更快
- 但聚类成本更高
- 可能需要更大的 probes 保证召回
probes 越大:
- 搜索桶更多
- 召回率更高
- 查询更慢
如果:
|
|
就接近全量扫描,召回高,但加速意义下降。
7. HNSW 和 IVFFlat 对比
| 对比项 | HNSW | IVFFlat |
|---|---|---|
| 底层结构 | 多层近邻图 | 聚类倒排桶 |
| 是否需要训练 | 不需要 | 需要聚类 |
| 能否空表建索引 | 可以 | 不理想 |
| 查询方式 | 图上跳转搜索 | 找最近桶,再扫桶 |
| 召回调参 | ef_search |
probes |
| 构建速度 | 较慢 | 较快 |
| 内存占用 | 较高 | 较低 |
| 查询性能/召回平衡 | 通常更好 | 通常一般 |
| 数据分布敏感度 | 较低 | 较高 |
简单选择:
|
|
8. pgvector 的 HNSW 是内存数据结构吗?
不是纯内存数据结构。
pgvector 的 HNSW 更准确地说是:
|
|
也就是说,它是 PostgreSQL 的磁盘持久化索引。
数据库重启后,索引不会丢。
8.1 建索引时的内存
pgvector HNSW 构建时,如果图能放进 maintenance_work_mem,构建会更快。
|
|
如果构建图超过 maintenance_work_mem,pgvector 会继续构建,但速度会明显变慢。
这不表示索引不能大于内存,而是构建过程会变慢。
8.2 查询时的内存
建完索引后,索引文件存储在 PostgreSQL 数据目录中。
查询时,Postgres 会按需把索引页加载到:
|
|
如果热点索引页在内存中,查询快。
如果索引远大于内存,并且查询频繁读磁盘页,延迟会上升。
9. pgvector 的 HNSW 有压缩吗?
默认没有。
如果这样建索引:
|
|
其中 embedding 是普通 vector(1536),那么索引不是自动压缩成 PQ、SQ8 或二进制。
pgvector 的默认 vector 是 float32 语义。
9.1 使用 halfvec 做半精度索引
pgvector 支持 halfvec,即 16-bit float,空间大约是普通 vector 的一半。
|
|
查询也要匹配表达式:
|
|
适合:
|
|
9.2 使用 binary_quantize 做二值量化
pgvector 也支持把向量二值化,再用 HNSW 做 Hamming 距离搜索。
|
|
常见用法是:
|
|
示例:
|
|
这种方式更省空间,但召回和排序精度会下降,通常需要 rerank。
10. Milvus 一般用哪种算法?
Milvus 支持多种向量索引,不是只有一种算法。
常见包括:
|
|
一般选择:
| 场景 | 推荐 |
|---|---|
| 不想调参 | AUTOINDEX |
| 高召回、内存充足 | HNSW |
| 内存有限、可接受中等召回 | IVF_FLAT |
| 需要进一步压缩 | IVF_PQ |
| 数据大到内存放不下 | DISKANN |
| 小数据、要精确结果 | FLAT |
| 稀疏向量 / BM25 / SPLADE | SPARSE_INVERTED_INDEX |
| 有 GPU | GPU_CAGRA / GPU_IVF_FLAT / GPU_IVF_PQ |
对于普通文本 embedding / RAG 检索:
|
|
11. pgvector 和 Milvus 的差异
pgvector 是 PostgreSQL 扩展。
优势:
- SQL 原生
- 事务一致性
- 权限、备份、复制、生态成熟
- 可以和关系数据直接 JOIN / WHERE
- 运维简单,适合已有 Postgres 系统
不足:
- 极大规模纯向量检索,不一定比专用向量数据库更快
- 索引和查询受 PostgreSQL buffer/cache 机制影响
- 高并发、大规模 ANN 调优空间相对有限
Milvus 是专门的向量数据库。
优势:
- 面向大规模向量检索设计
- 支持更多索引类型
- 支持分布式扩展
- 支持 GPU、DiskANN、量化等更丰富能力
不足:
- 系统复杂度更高
- 需要额外服务和运维
- 与关系数据结合通常不如 PostgreSQL 直接
简单理解:
|
|
12. 工程选型建议
12.1 如果使用 pgvector
普通文本 embedding 检索可以先用:
|
|
查询:
|
|
如果想提高召回:
|
|
如果想降低索引体积:
|
|
12.2 如果使用 Milvus
不想调参:
|
|
高召回、内存足:
|
|
超大规模、内存放不下:
|
|
内存敏感:
|
|
12.3 过滤后候选很少时,可以不建向量索引
在 pgvector 中,如果查询会先通过普通字段过滤,并且过滤后剩下的候选行数不多,可以不建 HNSW / IVFFlat 向量索引。
例如:
|
|
如果 tenant_id + category 过滤后只剩几百或几千行,直接对这些候选行计算向量距离通常就够了。
这种场景应优先给过滤字段建普通索引:
|
|
然后用 EXPLAIN ANALYZE 验证实际耗时:
|
|
简单判断:
| 过滤后候选行数 | 建议 |
|---|---|
| 几十到几千 | 通常不用向量索引 |
| 几万 | 看延迟和并发 |
| 十万以上 | 考虑 HNSW / IVFFlat |
| 过滤条件很弱 | 考虑向量索引 |
需要注意的是,pgvector 的 ANN 索引常见执行方式是先用向量索引找近邻,再应用 WHERE 过滤。
如果过滤条件很强,例如每个租户只占全表很小比例,向量索引可能先召回大量其他租户的数据,再被过滤掉,导致效果不理想。
这种情况下,常见方案是:
- 先用 B-tree 索引过滤,再对小候选集精确排序
- 对固定高频条件建立 partial vector index
- 按租户、业务域或时间做分区
- 必要时增大
hnsw.ef_search,用更多候选换召回
例如 partial vector index:
|
|
一句话:
如果普通字段已经能把候选集缩到很小,B-tree 过滤 + 精确向量排序,往往比 ANN 索引更简单、更准确,也可能更快。
13. 总结
向量检索可以分成三层理解:
|
|
HNSW 的核心是:
多层近邻图,先粗定位,再局部搜索。
IVFFlat 的核心是:
聚类分桶,只搜索最近的几个桶。
pgvector 的 HNSW:
是 PostgreSQL 的磁盘持久化索引,不是纯内存结构;默认不压缩,但可以用 halfvec 或 binary_quantize 做降精度索引。
Milvus:
支持多种索引,新项目可先用 AUTOINDEX,手动调优常见选择是 HNSW,大规模数据可考虑 DiskANN 或 IVF_PQ。
最终选型要看:
|
|
没有绝对最好的索引,只有适合当前业务约束的索引。
参考资料
- pgvector GitHub: https://github.com/pgvector/pgvector
- Milvus Index Selection: https://milvus.io/docs/zh/index_selection.md
- Milvus Index Explained: https://blog.milvus.io/docs/index-explained.md