--- id: ai.vector-db.ann-metrics title: 向量检索原理:距离度量、暴力检索、HNSW / IVF 近似索引 type: lesson domain: ai stack: [vector-db] tags: [向量检索, 余弦相似度, 内积, L2, HNSW, IVF, PQ, 召回率] level: basic prerequisites: [ai.rag.pipeline-chunking] est_hours: 2.5 related: [ai.vector-db.selection-pgvector, ai.rag.hybrid-rerank, ai.vector-db.interview] week: [5] order: 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 排序一致”: ```python 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](examples/vector-stores/)。 ## 易错点 - 嵌入模型和度量不匹配:模型输出未归一化却用内积,结果偏向长向量。 - 入库和查询使用了**不同的嵌入模型**(或同一模型的不同版本):向量空间不一致,检索结果完全不对。换模型必须全量重新嵌入。 - 把“距离”当“相似度”用:阈值方向写反。 - 只看近似检索的结果,不和暴力检索对比,不知道召回率是多少。 - 数据量很小也上复杂的向量数据库:几万条以内 numpy / pgvector 精确检索就够了。 - IVF 索引在空表上建立:聚类中心没有意义,召回率很低。应在导入有代表性的数据之后再建。 ## 面试常问 - 余弦相似度、内积、L2 的区别?什么时候结果一样? - HNSW 的原理和关键参数?`ef_search` 调大会怎样? - HNSW 和 IVF 怎么选? - 带过滤条件的向量检索为什么可能召回不足?怎么解决?