Files
zhuangang/topics/ai/vector-db/01-ann-metrics.md
T

5.7 KiB
Raw Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
id title type domain stack tags level prerequisites est_hours related week order
ai.vector-db.ann-metrics 向量检索原理:距离度量、暴力检索、HNSW / IVF 近似索引 lesson ai
vector-db
向量检索
余弦相似度
内积
L2
HNSW
IVF
PQ
召回率
basic
ai.rag.pipeline-chunking
2.5
ai.vector-db.selection-pgvector
ai.rag.hybrid-rerank
ai.vector-db.interview
5
1

向量检索原理:距离度量、暴力检索、HNSW / IVF 近似索引

解释说明

嵌入(embedding) 把一段文本变成一个固定长度的浮点数组(例如 768、1024、1536 维),语义相近的文本向量也相近。向量数据库要解决的问题就是:给定一个查询向量,从几百万个向量中快速找出最相近的 k 个(k 近邻,kNN)。

距离度量

度量 含义 什么时候用
余弦相似度(cosine) 两个向量夹角的余弦,只看方向不看长度 文本嵌入最常用
内积(inner product, IP / dot) 对应元素相乘求和 向量已归一化时和余弦等价且更快
欧氏距离(L2) 两点之间的直线距离 部分模型推荐;归一化向量的 L2 排序和余弦排序一致

规则:用嵌入模型文档推荐的度量;如果向量已归一化(长度为 1),三种度量的排序结果一致。注意“距离”和“相似度”方向相反:余弦距离 = 1 − 余弦相似度,越小越相近。

暴力检索和近似检索

  • 暴力检索(精确):查询向量和所有向量逐一计算,复杂度 O(N·d)。几万条以内完全够用(numpy 一次矩阵乘法),而且结果是精确的。
  • 近似最近邻(ANN):用索引结构牺牲一点准确度,换取数量级的速度提升。衡量指标是召回率:近似结果和精确结果的重合比例。

常见索引

索引 原理 关键参数 特点
HNSW 多层“小世界”图,从稀疏的上层快速定位,逐层向下精细搜索 M(每个点的连接数)、ef_construction(建索引时的候选数)、ef_search(查询时的候选数) 查询快、召回高,最常用;内存占用大,建索引慢
IVF(倒排文件) 先用聚类把向量分成 nlist 个桶,查询时只搜最近的 nprobe 个桶 nlist / lists、nprobe / probes 建索引快、省内存;要先有数据才能训练聚类中心,数据分布变化后要重建
PQ(乘积量化) 把向量切段并压缩成编码 段数、码本大小 大幅省内存,精度下降,常和 IVF 组合(IVF_PQ)
DiskANN 等磁盘索引 索引放在 SSD 上 —— 数据量超出内存时使用

参数的规律:ef_search / nprobe 调大 → 召回率上升、延迟上升。用自己的数据和评测集测出合适的值,不要照抄。

过滤和近似索引的冲突

带条件的查询(只搜某租户 / 某部门)在近似索引上有一个经典问题:索引先返回 top-N 个近邻,再按条件过滤,如果满足条件的数据很少,过滤后可能不足 k 个甚至为空。解决办法:数据库内置的过滤遍历(例如 pgvector 0.8 的迭代索引扫描、Milvus 的过滤搜索)、增大候选数、按租户分区 / 分集合、过滤条件非常严格时直接走精确检索。

案例

制度问答知识库有 3 万个块:直接用 numpy 暴力检索,单次查询几毫秒,没有召回损失。业务扩展到 50 个租户、800 万个块后,暴力检索需要几百毫秒,改用 HNSW 索引;上线前用 200 个评测问题对比 HNSW 和暴力检索的结果,调整 ef_search 使召回率达到 0.95 以上,同时 P99 延迟满足要求。

代码示例

用 numpy 实现暴力检索,验证“归一化后余弦 = 内积,L2 排序一致”:

import numpy as np

rng = np.random.default_rng(0)
docs = rng.normal(size=(1000, 64)).astype(np.float32)
q = rng.normal(size=64).astype(np.float32)

docs_n = docs / np.linalg.norm(docs, axis=1, keepdims=True)
q_n = q / np.linalg.norm(q)

cos = docs_n @ q_n                                # 余弦相似度(越大越近)
ip = docs_n @ q_n                                 # 归一化后内积就是余弦
l2 = np.linalg.norm(docs_n - q_n, axis=1)         # 欧氏距离(越小越近)

top_cos = np.argsort(-cos)[:5]
top_l2 = np.argsort(l2)[:5]
assert list(top_cos) == list(top_l2)              # 归一化后排序一致
print(top_cos, np.round(cos[top_cos], 3))

# 未归一化时内积会偏向长度大的向量,排序可能不同
top_ip_raw = np.argsort(-(docs @ q))[:5]
print("未归一化内积 top5:", top_ip_raw)

近似索引和精确检索的召回率对比(Chroma HNSW、pgvector HNSW)见 examples/vector-stores。

易错点

  • 嵌入模型和度量不匹配:模型输出未归一化却用内积,结果偏向长向量。
  • 入库和查询使用了不同的嵌入模型(或同一模型的不同版本):向量空间不一致,检索结果完全不对。换模型必须全量重新嵌入。
  • 把“距离”当“相似度”用:阈值方向写反。
  • 只看近似检索的结果,不和暴力检索对比,不知道召回率是多少。
  • 数据量很小也上复杂的向量数据库:几万条以内 numpy / pgvector 精确检索就够了。
  • IVF 索引在空表上建立:聚类中心没有意义,召回率很低。应在导入有代表性的数据之后再建。

面试常问

  • 余弦相似度、内积、L2 的区别?什么时候结果一样?
  • HNSW 的原理和关键参数?ef_search 调大会怎样?
  • HNSW 和 IVF 怎么选?
  • 带过滤条件的向量检索为什么可能召回不足?怎么解决?