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

101 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 怎么选?
- 带过滤条件的向量检索为什么可能召回不足?怎么解决?