第 5 章 向量索引:用 Recall 换速度 没有索引,向量检索只能暴力比对全部向量(Flat),百万级数据就要等几秒。索引的本质是一场交易——用一点点精度(Recall),换取速度和规模的质变。这一章把 Zvec 的五种索引讲清楚,并给你一张选型决策表,让你面对任何场景都知道该选哪个。 5.1 先理解:精确检索 vs 近似检索(ANN) 向量索引大多基于 ANN(近似最近邻,Approximate Nearest Neighbor) 算法。理解 ANN,要先理解它放弃的是什么: 精确检索(Flat/暴力):把查询向量和库里每一个向量都比一遍,结果绝对准确,但规模一上去就慢到不可用。 近似检索(ANN):不再追求"绝对最近",而是"高度接近的最近"。
没有索引,向量检索只能暴力比对全部向量(Flat),百万级数据就要等几秒。索引的本质是一场交易——用一点点精度(Recall),换取速度和规模的质变。这一章把 Zvec 的五种索引讲清楚,并给你一张选型决策表,让你面对任何场景都知道该选哪个。
向量索引大多基于 ANN(近似最近邻,Approximate Nearest Neighbor) 算法。理解 ANN,要先理解它放弃的是什么:
衡量 ANN 精度的指标是 Recall@k(召回率):
近似方法返回的 top-k 中,真实最近邻的数量 Recall@k = ──────────────────────────────────────────── k
💡 为什么 RAG 能接受近似? 因为 LM 生成本身就有容错性——top-10 里混进 1 个"第 11 名"的文档,几乎不影响最终答案质量。业界经验:Recall ≥ 96% 时,近似检索和精确检索在 RAG 效果上几乎无差别。所以这笔交易几乎稳赚。
Zvec 提供五种索引,对应不同的"交易策略":
| 索引 | 一句话定位 | 查询复杂度 | 内存 | 适用规模 |
|---|---|---|---|---|
| Flat | 暴力基线,绝对精确 | O(N) | 中 | 几万以内 / 调试 |
| HNSW | 分层图,生产首选 | O(log N) | 高 | 百万~千万,内存够 |
| HNSW-RaBitQ | HNSW + 量化,省内存 | O(log N) | 低 | 大规模、要省内存(需 x86+AVX2) |
| IVF | 聚类倒排桶,参数敏感 | O(N/n_list × n_probe) | 低 | 海量、内存敏感 |
| DiskANN | 磁盘图 + PQ,面向十亿级 | 含磁盘 I/O | 极低 | 十亿级、内存装不下(仅 Linux) |
绝大多数 RAG 场景,从 HNSW 开始就对了。它官方推荐为生产环境首选,在速度、精度、稳定性间平衡最好。
HNSW(Hierarchical Navigable Small World)构建一个多层图,每层节点代表一个向量,边按相似度连接:
Layer 2 (稀疏,长距离"高速公路") ●─────●─────● │ Layer 1 (中等) ●─●───●─● │ │ Layer 0 (稠密,精细) ●─●─●─●─●─●─●─●─●─●─●─● ← 所有节点都在这层
查询时从顶层入口出发,逐层贪心下降:上层快速跳到目标区域(像走高速),下层精细搜索局部邻域(像走小路找门牌)。这就是它"既快又准"的原因——上层负责速度,底层负责精度。
| 参数 | 作用 | 调参方向 |
|---|---|---|
m |
每节点最大邻居数(构建时) | ↑ 召回更高、内存更多;默认即可起步 |
ef_construction |
构建时候选池大小 | ↑ 图质量更好、构建更慢;不影响查询速度 |
ef(查询时) |
查询时候选池大小 | 调召回率的第一旋钮:↑ 召回更高、延迟增加 |
quantize_type |
量化压缩 | 见 5.5 节 |
💡 调参顺序心法:先从默认值起步,优先调查询时的
ef来平衡召回和延迟(它不改索引、零成本切换)。只有召回还不够时,才加大ef_construction或m重建索引——因为后两者会增加构建时间和内存。
IVF(Inverted File)的思路是先把向量空间聚类分桶,查询时只搜最近的几个桶。
索引构建:N 个向量 ──k-means──► n_list 个簇(每簇一个质心) │ 查询: 查询向量 ──► 找最近 n_probe 个质心 ──► 只在这些桶里暴力搜
它把全量扫描变成"只扫几个桶",复杂度约 O(N/n_list × n_probe)。n_list 越大、n_probe 越小,越快但召回可能降。
| 参数 | 作用 | 调参建议 |
|---|---|---|
n_list |
聚类数(桶数) | 初始值 ≈ √N(N 为向量数);↑ 桶更小更快但构建更贵 |
n_iters |
质心优化迭代数 | ↑ 聚类质量更好、构建更慢 |
n_probe(查询时) |
查询时搜多少个桶 | 调召回率的旋钮:↑ 召回更高、更慢 |
⚠️ IVF 是"参数敏感型"选手:
n_list和n_probe对效果影响大,适合愿意做系统化调参的场景。它的优势是内存效率高(向量存紧凑的倒排列表里),常与乘积量化组合成 IVF-PQ 应对海量数据。
当数据集远超内存(十亿级),HNSW 装不下时,DiskANN 登场。它把压缩编码放内存、全精度向量放磁盘,用 Vamana 图导航 + PQ 距离估计 + 带缓存的束搜索。
| 特性 | 说明 |
|---|---|
| 内存 | 极低(仅 PQ 编码驻留,每向量每 chunk 1 字节) |
| Recall | 高(最终候选重新算精确距离) |
| 延迟 | 比内存索引高(有磁盘 I/O),QPS 较低 |
| 平台 | 仅 Linux,需 libaio |
⚠️ DiskANN 适合"对延迟不敏感、但必须在有限内存下跑超大规模"的批处理/离线场景。如果你的数据能装进内存,老老实实用 HNSW 或 HNSW-RaBitQ,延迟低得多。参数核心是查询时的
list_size:↑ 召回更高但磁盘 I/O 更多。
当 HNSW 内存吃紧时,不必换索引——开启**量化(Quantization)**即可。量化把 FP32 向量压缩成更紧凑的表示,Zvec 同时存原始向量和量化版(构建/检索只加载量化版,省内存;需要时可取回原始向量)。
| 量化类型 | 每维比特 | 精度损失 | 适用 |
|---|---|---|---|
| FP16 | 16 bit | 极小 | 想接近 FP32 精度又省一半内存 |
| INT8 | 8 bit | 小(多数任务可接受) | 速度/大小/精度的平衡点 |
| INT4 | 4 bit | 较大 | 极致省内存、延迟敏感、可接受精度损失 |
# 在 VectorSchema 里加 quantize_type 即可开启 zvec.VectorSchema( name="dense", data_type=zvec.DataType.VECTOR_FP32, dimension=768, index_param=zvec.HnswIndexParam( metric_type=zvec.MetricType.COSINE, quantize_type=zvec.QuantizeType.INT8, # 量化压缩 ), )
⚠️ 量化是有损且不可逆的压缩,会可能降低召回。开启后务必验证对检索质量的影响。一个补救手段是查询时开精排(refiner):对头部候选重新用原始向量算精确分数,挽回精度损失(代价是增加延迟)。
# 查询时开启精排,配合量化使用,找回精度 result = collection.query( queries=zvec.Query( field_name="dense", vector=q_vec, param=zvec.HnswQueryParam(ef=500, is_using_refiner=True), # 精排 ), topk=10, )
把前面五种索引浓缩成一张决策表。从上到下,第一个满足条件的即是推荐选择:
你的数据规模 / 约束 │ ├─ 调试、小数据(< 几万)、要绝对精确 │ └─► Flat │ ├─ 数据能装进内存(百万~千万级,常规 RAG) │ ├─ 内存充裕 ──► HNSW(生产首选) │ └─ 内存吃紧(x86+AVX2)──► HNSW-RaBitQ 或 HNSW + 量化(INT8/FP16) │ └─ 数据远超内存(十亿级,仅 Linux) └─► DiskANN
| 场景 | 推荐索引 | 理由 |
|---|---|---|
| 个人/小团队 RAG 起步 | HNSW | 平衡最好,调参简单(主要调 ef) |
| 中大型 RAG、内存紧张 | HNSW + INT8 量化 | 省内存,开 refiner 保精度 |
| 超大规模、内存装不下 | DiskANN | 唯一能在有限内存跑十亿级 |
| 愿意精调、内存敏感 | IVF | 内存高效,但参数敏感 |
索引不仅在构建时可调,查询时也有专属参数,这是召回调优的"第二战场"。这些参数通过 Query 的 param 传入,不改索引、可随时切换:
# HNSW 查询:调大 ef 提召回 collection.query( queries=zvec.Query(field_name="dense", vector=q_vec, param=zvec.HnswQueryParam(ef=600)), topk=10, ) # IVF 查询:调大 n_probe 提召回 collection.query( queries=zvec.Query(field_name="dense", vector=q_vec, param=zvec.IVFQueryParam(nprobe=100)), topk=10, ) # DiskANN 查询:调大 list_size 提召回(更多磁盘 I/O) collection.query( queries=zvec.Query(field_name="dense", vector=q_vec, param=zvec.DiskAnnQueryParam(list_size=200)), topk=10, )
| 索引 | 查询参数 | 还有个通用招 |
|---|---|---|
| HNSW | ef、radius、is_using_refiner、is_linear(强制暴力,仅调试) |
radius:设距离阈值,剔除低质量匹配 |
| IVF | n_probe、radius、is_linear |
radius:内积时 radius=0.6 只留分数>0.6 |
| DiskANN | list_size |
适合"宁可少返回也要高质量"的场景 |
⚠️ 参数类必须匹配索引类型:对 HNSW 索引用
IVFQueryParam会报错。索引类型和查询参数类是一一对应的。
ef 调参:固定数据,分别用 ef=100/300/600 查询,记录 Recall 和延迟,画出"召回-延迟"曲线,体会第一旋钮的效果。ef);IVF 内存友好但参数敏感;DiskANN 面向十亿级磁盘场景。is_using_refiner 精排可挽回精度。ef/n_probe/list_size)是召回调优第二战场,不改索引随时切。索引选好了,接下来真正开始检索。详见第 6 章。