向量相似度、召回率与向量索引:从 HNSW 到 IVFFlat

向量检索的核心问题是:

给定一个查询向量,如何从大量向量中快速找出最相似的 topK 个结果。

在小数据量下,可以直接暴力计算所有向量距离。但当数据达到百万、千万甚至更大规模时,就需要向量索引,例如 HNSW、IVFFlat、DiskANN 等。

本文整理向量相似度、召回率、pgvector、Milvus、HNSW、IVFFlat 的基本原理和工程选择。


1. 常见向量相似度计算方法

1.1 余弦相似度 Cosine Similarity

余弦相似度衡量两个向量方向是否接近:

1
cos(a, b) = (a · b) / (||a|| ||b||)

越接近 1 越相似。

适合:

  • 文本 embedding
  • 语义搜索
  • RAG 检索
  • 向量长度不重要,只关心方向的场景

在大模型 embedding 检索里,余弦相似度非常常见。


1.2 点积 Dot Product / Inner Product

1
sim(a, b) = a · b

如果向量已经归一化,点积和余弦相似度等价。

适合:

  • 已归一化 embedding
  • 推荐系统
  • 最大内积搜索 MIPS

注意:未归一化时,点积会受向量模长影响。


1.3 欧氏距离 L2 Distance

1
dist(a, b) = sqrt(sum((ai - bi)^2))

距离越小越相似。

适合:

  • 图像特征
  • 聚类
  • 几何空间意义较强的向量

1.4 其他方法

方法 适合场景
L1 / 曼哈顿距离 稀疏特征、逐维差异累加
Pearson 相关系数 用户评分、趋势相似
Jaccard 相似度 标签集合、关键词集合
Hamming 距离 二进制向量、哈希向量
Mahalanobis 距离 异常检测、统计建模

实际做文本 embedding 检索时,常见组合是:

1
向量归一化 + dot product

或:

1
cosine similarity

2. 召回率是什么意思?

召回率 Recall 表示:

所有真正相关的结果里,被系统找回来了多少。

公式:

1
Recall = 找回的相关结果数 / 全部真实相关结果数

例子:

数据库里真实相关文档有 100 篇,系统找回了 80 篇:

1
Recall = 80 / 100 = 80%

3. 召回错误算不算召回率?

不算。

召回率只关心:

应该找回的,有没有找回来。

错误召回,也就是“不相关但被返回”的结果,不会提高召回率。

它影响的是另一个指标:Precision,精确率。

1
Precision = 返回结果中真正相关的数量 / 返回结果总数量

例如返回 10 条结果:

1
2
8 条相关
2 条不相关

那么:

1
Precision = 8 / 10 = 80%

如果真实相关结果总共有 20 条,而返回结果里只有 8 条相关:

1
2
Recall = 8 / 20 = 40%
Precision = 8 / 10 = 80%

在向量数据库里,常见的 topK recall 通常是:

近似索引返回的 topK,和暴力精确搜索 topK 的重合比例。


4. 为什么需要向量索引?

暴力搜索需要计算查询向量和所有向量的距离。

1
2
3
4
5
query vs v1
query vs v2
query vs v3
...
query vs vN

复杂度接近:

1
O(N)

当 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 近邻"]

简化流程:

1
2
3
4
5
1. 从最高层入口点开始
2. 在当前层寻找离 query 更近的节点
3. 找不到更近节点后下降一层
4. 到第 0 层后扩大搜索候选
5. 返回 topK

5.3 HNSW 为什么快?

因为它不是全量扫描,而是在图上跳转。

暴力搜索:

1
扫描所有 N 个向量

HNSW:

1
2
高层粗定位
底层局部搜索

工程上通常能取得很好的速度和召回平衡。


5.4 HNSW 为什么是近似的?

因为它只搜索图上的一部分节点。

如果真实最近邻不在搜索路径附近,就可能漏掉。

可以通过增大搜索参数提高召回率。

在 pgvector 里:

1
SET hnsw.ef_search = 100;

ef_search 越大:

  • 搜索候选越多
  • 召回率越高
  • 查询越慢

6. IVFFlat 原理简介

IVFFlat 可以拆成两部分:

1
2
IVF = Inverted File,倒排文件 / 倒排桶
Flat = 桶内使用原始向量精确计算距离

一句话:

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..."]

这个结构类似:

1
簇中心 -> 向量列表

所以叫倒排文件。


6.2 查询:只搜索部分桶

查询向量 q 来了以后:

1
2
3
4
1. 计算 q 和所有簇中心的距离
2. 选择最近的 probes 个桶
3. 只扫描这些桶里的原始向量
4. 返回 topK
  flowchart TD
  Q["查询向量 q"] --> C["计算 q 到所有中心点距离"]
  C --> P["选择最近的 probes 个桶"]
  P --> S["扫描这些桶中的原始向量"]
  S --> R["精确计算距离并返回 topK"]

6.3 nlist 和 probes

IVFFlat 有两个关键参数:

1
2
nlist  = 建索引时分多少个桶
probes = 查询时搜索多少个桶

nlist 越大:

  • 桶更多
  • 每个桶更小
  • 单桶扫描更快
  • 但聚类成本更高
  • 可能需要更大的 probes 保证召回

probes 越大:

  • 搜索桶更多
  • 召回率更高
  • 查询更慢

如果:

1
probes = nlist

就接近全量扫描,召回高,但加速意义下降。


7. HNSW 和 IVFFlat 对比

对比项 HNSW IVFFlat
底层结构 多层近邻图 聚类倒排桶
是否需要训练 不需要 需要聚类
能否空表建索引 可以 不理想
查询方式 图上跳转搜索 找最近桶,再扫桶
召回调参 ef_search probes
构建速度 较慢 较快
内存占用 较高 较低
查询性能/召回平衡 通常更好 通常一般
数据分布敏感度 较低 较高

简单选择:

1
2
高召回、低延迟:HNSW
内存敏感、数据稳定:IVFFlat

8. pgvector 的 HNSW 是内存数据结构吗?

不是纯内存数据结构。

pgvector 的 HNSW 更准确地说是:

1
2
3
4
算法结构:HNSW 多层图
存储形式:PostgreSQL index relation,持久化落盘
查询执行:通过 shared_buffers / OS page cache 读取索引页
构建过程:尽量在 maintenance_work_mem 中构建图

也就是说,它是 PostgreSQL 的磁盘持久化索引。

数据库重启后,索引不会丢。


8.1 建索引时的内存

pgvector HNSW 构建时,如果图能放进 maintenance_work_mem,构建会更快。

1
2
3
4
SET maintenance_work_mem = '8GB';

CREATE INDEX ON items
USING hnsw (embedding vector_cosine_ops);

如果构建图超过 maintenance_work_mem,pgvector 会继续构建,但速度会明显变慢。

这不表示索引不能大于内存,而是构建过程会变慢。


8.2 查询时的内存

建完索引后,索引文件存储在 PostgreSQL 数据目录中。

查询时,Postgres 会按需把索引页加载到:

1
2
shared_buffers
OS page cache

如果热点索引页在内存中,查询快。

如果索引远大于内存,并且查询频繁读磁盘页,延迟会上升。


9. pgvector 的 HNSW 有压缩吗?

默认没有。

如果这样建索引:

1
2
CREATE INDEX ON items
USING hnsw (embedding vector_cosine_ops);

其中 embedding 是普通 vector(1536),那么索引不是自动压缩成 PQ、SQ8 或二进制。

pgvector 的默认 vector 是 float32 语义。


9.1 使用 halfvec 做半精度索引

pgvector 支持 halfvec,即 16-bit float,空间大约是普通 vector 的一半。

1
2
CREATE INDEX ON items
USING hnsw ((embedding::halfvec(1536)) halfvec_cosine_ops);

查询也要匹配表达式:

1
2
3
4
SELECT *
FROM items
ORDER BY embedding::halfvec(1536) <=> $1::halfvec(1536)
LIMIT 10;

适合:

1
2
3
希望减少索引大小
可以接受轻微精度损失
仍然想用 HNSW

9.2 使用 binary_quantize 做二值量化

pgvector 也支持把向量二值化,再用 HNSW 做 Hamming 距离搜索。

1
2
CREATE INDEX ON items
USING hnsw ((binary_quantize(embedding)::bit(1536)) bit_hamming_ops);

常见用法是:

1
2
先用二值 HNSW 快速召回更多候选
再用原始 vector 精排

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
WITH candidates AS (
  SELECT id, embedding
  FROM items
  ORDER BY binary_quantize(embedding)::bit(1536)
           <~> binary_quantize($1::vector)
  LIMIT 100
)
SELECT *
FROM candidates
ORDER BY embedding <=> $1::vector
LIMIT 10;

这种方式更省空间,但召回和排序精度会下降,通常需要 rerank。


10. Milvus 一般用哪种算法?

Milvus 支持多种向量索引,不是只有一种算法。

常见包括:

1
2
3
4
5
6
7
8
AUTOINDEX
HNSW
IVF_FLAT
IVF_PQ
DISKANN
FLAT
SPARSE_INVERTED_INDEX
GPU_CAGRA

一般选择:

场景 推荐
不想调参 AUTOINDEX
高召回、内存充足 HNSW
内存有限、可接受中等召回 IVF_FLAT
需要进一步压缩 IVF_PQ
数据大到内存放不下 DISKANN
小数据、要精确结果 FLAT
稀疏向量 / BM25 / SPLADE SPARSE_INVERTED_INDEX
有 GPU GPU_CAGRA / GPU_IVF_FLAT / GPU_IVF_PQ

对于普通文本 embedding / RAG 检索:

1
2
3
新项目优先 AUTOINDEX
手动调优优先 HNSW
内存不足考虑 IVF_PQ 或 DiskANN

11. pgvector 和 Milvus 的差异

pgvector 是 PostgreSQL 扩展。

优势:

  • SQL 原生
  • 事务一致性
  • 权限、备份、复制、生态成熟
  • 可以和关系数据直接 JOIN / WHERE
  • 运维简单,适合已有 Postgres 系统

不足:

  • 极大规模纯向量检索,不一定比专用向量数据库更快
  • 索引和查询受 PostgreSQL buffer/cache 机制影响
  • 高并发、大规模 ANN 调优空间相对有限

Milvus 是专门的向量数据库。

优势:

  • 面向大规模向量检索设计
  • 支持更多索引类型
  • 支持分布式扩展
  • 支持 GPU、DiskANN、量化等更丰富能力

不足:

  • 系统复杂度更高
  • 需要额外服务和运维
  • 与关系数据结合通常不如 PostgreSQL 直接

简单理解:

1
2
已有 Postgres + 中等规模向量检索:pgvector
专门做大规模向量搜索平台:Milvus

12. 工程选型建议

12.1 如果使用 pgvector

普通文本 embedding 检索可以先用:

1
2
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);

查询:

1
2
3
4
SELECT *
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;

如果想提高召回:

1
SET hnsw.ef_search = 100;

如果想降低索引体积:

1
2
CREATE INDEX ON documents
USING hnsw ((embedding::halfvec(1536)) halfvec_cosine_ops);

12.2 如果使用 Milvus

不想调参:

1
AUTOINDEX

高召回、内存足:

1
HNSW

超大规模、内存放不下:

1
DISKANN

内存敏感:

1
IVF_PQ

12.3 过滤后候选很少时,可以不建向量索引

在 pgvector 中,如果查询会先通过普通字段过滤,并且过滤后剩下的候选行数不多,可以不建 HNSW / IVFFlat 向量索引。

例如:

1
2
3
4
5
6
SELECT *
FROM documents
WHERE tenant_id = 123
  AND category = 'faq'
ORDER BY embedding <=> $1::vector
LIMIT 10;

如果 tenant_id + category 过滤后只剩几百或几千行,直接对这些候选行计算向量距离通常就够了。

这种场景应优先给过滤字段建普通索引:

1
CREATE INDEX ON documents (tenant_id, category);

然后用 EXPLAIN ANALYZE 验证实际耗时:

1
2
3
4
5
6
7
EXPLAIN ANALYZE
SELECT *
FROM documents
WHERE tenant_id = 123
  AND category = 'faq'
ORDER BY embedding <=> $1::vector
LIMIT 10;

简单判断:

过滤后候选行数 建议
几十到几千 通常不用向量索引
几万 看延迟和并发
十万以上 考虑 HNSW / IVFFlat
过滤条件很弱 考虑向量索引

需要注意的是,pgvector 的 ANN 索引常见执行方式是先用向量索引找近邻,再应用 WHERE 过滤。 如果过滤条件很强,例如每个租户只占全表很小比例,向量索引可能先召回大量其他租户的数据,再被过滤掉,导致效果不理想。

这种情况下,常见方案是:

  • 先用 B-tree 索引过滤,再对小候选集精确排序
  • 对固定高频条件建立 partial vector index
  • 按租户、业务域或时间做分区
  • 必要时增大 hnsw.ef_search,用更多候选换召回

例如 partial vector index:

1
2
3
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WHERE category = 'faq';

一句话:

如果普通字段已经能把候选集缩到很小,B-tree 过滤 + 精确向量排序,往往比 ANN 索引更简单、更准确,也可能更快。


13. 总结

向量检索可以分成三层理解:

1
2
3
相似度计算:cosine / dot product / L2
索引算法:HNSW / IVFFlat / DiskANN
工程系统:pgvector / Milvus / Faiss / Qdrant

HNSW 的核心是:

多层近邻图,先粗定位,再局部搜索。

IVFFlat 的核心是:

聚类分桶,只搜索最近的几个桶。

pgvector 的 HNSW:

是 PostgreSQL 的磁盘持久化索引,不是纯内存结构;默认不压缩,但可以用 halfvec 或 binary_quantize 做降精度索引。

Milvus:

支持多种索引,新项目可先用 AUTOINDEX,手动调优常见选择是 HNSW,大规模数据可考虑 DiskANN 或 IVF_PQ。

最终选型要看:

1
2
3
4
5
6
数据规模
内存预算
召回要求
延迟要求
是否已有 PostgreSQL
是否需要分布式向量检索

没有绝对最好的索引,只有适合当前业务约束的索引。


参考资料