第16章:向量索引 - IVF 系列
🎯 核心概览
IVF(Inverted File)和 IVF_PQ 是 Lance 中最重要的向量索引,提供 100-1000 倍的加速。
📊 IVF 原理
markdown
1 亿个向量 → 256 个簇(质心)
构建:
1. KMeans 聚类,得到 256 个质心
2. 为每个向量分配到最近的质心
3. 为每个簇构建向量列表
搜索:
1. 计算查询向量到 256 个质心的距离 (快)
2. 选择最近的 20 个簇
3. 在这 20 个簇中逐个计算距离
4. 返回 Top-K
性能:
- 原始:扫描 1 亿向量
- IVF:只扫描 20/256 ≈ 8% 的向量
- 加速:~10 倍
🔧 IVF_PQ(带量化)
PQ 量化
css
768 维向量 → 8 个 96 维子向量
每个子向量量化为 1 字节(256 个码字)
总计:8 字节代替 3072 字节
压缩率:99.7%
距离计算:
精确距离 = sum(distance(Q_i, V_i)) for i in 0..8
近似距离 = sum(lookup_table[i][Q_i_code][V_i_code])
查表:1ns
计算:100ns
加速:100 倍
完整性能
scss
IVF_PQ 搜索 1 亿向量:
1. 计算到质心:256 × 768 float ops = ~200K ops (0.1ms)
2. 选择 20 簇:按距离排序 256 个 (< 0.1ms)
3. 加载量化数据:20/256 × 3GB = ~120MB (1ms)
4. 计算近似距离:200万 × 查表 = ~2ms
5. Top-K 排序:~1ms
总计:~4-5ms
对比全扫描:
- 全扫描:768维 × 1亿 = 76.8B float ops ≈ 100ms
- 加速:20-25 倍
📚 IVF_FLAT vs IVF_PQ
| 特性 | IVF_FLAT | IVF_PQ |
|---|---|---|
| 精确度 | 100% | 99% |
| 内存 | 高(3KB/向量) | 低(8字节/向量) |
| 速度 | 中等 | 快 |
| 适用 | 精确搜索 | 大规模搜索 |
🔍 IVF 的设计思想
IVF 解决的问题
在没有索引的情况下,搜索 1 亿个向量的最近邻需要:
搜索成本 = 1亿 × 768维 × 浮点运算 = 7.68×10¹⁰ 次操作 ≈ 100ms
IVF 的核心洞察: 不需要扫描所有向量,只需扫描相关的簇
erlang
原始方案:扫描 1 亿向量
IVF 方案:扫描 20 个簇中的 200 万向量(只需 8% 的数据)
加速倍数:20-25 倍
IVF 的分层构建
第一层:KMeans 聚类
- 输入:1000万向量 × 768维
- 过程:迭代 KMeans 学习 256 个质心
- 输出:256 个质心 × 768维
为什么选择 KMeans?
- 目标明确:最小化簇内方差
- 质心有意义:代表整个簇
- 实现简单:易于分布式计算
- 性能平衡:建立时间 vs 搜索质量
第二层:向量分配
每个向量分配到最近的质心(硬分配)
为什么是硬分配?
- 快速:O(1) 时间
- 内存高效:一个向量属于一个簇
- 搜索灵活:可以检查多个相邻簇
第三层:PQ 量化
将向量压缩 99.7%:
yaml
768维向量 → 8个96维子向量 → 8字节码
3072字节 → 8字节,压缩率 99.7%
IVF_PQ 的距离计算优化
scss
精确距离计算:d(q, v) = sqrt(Σ(q_i - v_i)²)
需要 768 个减法、乘法、求和
耗时:~100ns
PQ 近似距离:d_approx = Σ lookup_table[i][code_q[i]][code_v[i]]
只需 8 次数组查找
耗时:~1ns(快 100 倍!)
💻 代码实现示例
Python:IVF_PQ 索引构建与搜索
python
import lance
import numpy as np
import time
# 生成测试数据
data = {
'id': list(range(1_000_000)),
'vector': [np.random.rand(768).astype(np.float32) for _ in range(1_000_000)],
'category': np.random.choice(['A', 'B', 'C'], 1_000_000),
}
# 创建数据集
dataset = lance.write_dataset(data, 'products.lance')
# 步骤1:创建 IVF_PQ 索引
print("开始构建 IVF_PQ 索引...")
start_time = time.time()
dataset.create_index(
'vector',
index_type='IVF_PQ',
num_partitions=256, # 256 个质心簇
metric='L2', # L2 距离
num_bits=8, # PQ 码字簿位数
num_sub_vectors=8, # 768 / 8 = 96 维子向量
max_iters=50, # KMeans 迭代次数
replace=True,
)
build_time = time.time() - start_time
print(f"索引构建完成,耗时: {build_time:.2f}s")
# 步骤2:执行向量搜索
query_vector = np.random.rand(768).astype(np.float32)
print("\n执行向量搜索...")
search_start = time.time()
results = dataset.search(
query_vector,
k=100,
nprobes=32, # 搜索 32 个簇(默认 = num_partitions / 8)
refine_factor=10, # 候选集大小 = k * refine_factor
).to_list()
search_time = (time.time() - search_start) * 1000
print(f"搜索完成,耗时: {search_time:.2f}ms,返回 {len(results)} 个结果")
# 步骤3:性能对比
print("\n=== 性能分析 ===")
print(f"数据集大小: 1,000,000 个向量")
print(f"向量维度: 768")
print(f"索引大小: ~80MB (1M × 8字节)")
print(f"原始数据大小: ~3GB")
print(f"压缩率: 99.7%")
print(f"搜索加速: 100ms / {search_time:.2f}ms ≈ {100/search_time:.1f}x")
Rust:IVF 索引构建
rust
use lance::dataset::builder::DatasetBuilder;
use lance::index::vector::{IvfBuildParams, PQBuildParams, VectorIndexParams};
use lance::index::IndexType;
use lance_linalg::distance::DistanceType;
#[tokio::main]
async fn main() -> Result<()> {
// 步骤1:配置 IVF_PQ 参数
let ivf_params = IvfBuildParams {
num_partitions: 256, // KMeans 簇数
sample_rate: 256,
max_iters: 50,
};
let pq_params = PQBuildParams {
num_sub_vectors: 8, // 768 / 8 = 96
num_bits: 8, // 2^8 = 256 码字
max_iters: 100,
};
// 步骤2:创建索引参数
let params = VectorIndexParams::with_ivf_pq_params(
DistanceType::L2,
ivf_params,
pq_params,
);
// 步骤3:构建索引
println!("开始构建 IVF_PQ 索引...");
let start = std::time::Instant::now();
// ... 构建逻辑 ...
let build_time = start.elapsed();
println!("索引构建完成: {:?}", build_time);
Ok(())
}
📊 参数调优指南
num_partitions(分区数)
diff
选择原则:num_partitions ≈ √N (N = 向量数)
例子:
100 万向量 → √1M ≈ 1000 个簇
1 亿向量 → √100M ≈ 10000 个簇
10 亿向量 → √1B ≈ 31623 个簇
权衡:
- 簇数 ↑ → 搜索快(扫描少)但训练慢
- 簇数 ↓ → 训练快但搜索慢
nprobes(搜索的分区数)
ini
默认值:nprobes = num_partitions / 8
256 个簇 → nprobes = 32
调优:
nprobes=10 → 快(5ms)但精度低(78% recall)
nprobes=20 → 平衡(92% recall)
nprobes=32 → 推荐(95% recall) ✓
nprobes=64 → 准确(98% recall)但较慢(10ms)
📈 性能对比
IVF_FLAT vs IVF_PQ(1000万向量)
yaml
指标 IVF_FLAT IVF_PQ
────────────────────────────────
搜索延迟 40ms 5ms ✓
精确度 100% 99%
内存占用 30GB 1GB ✓
QPS/机器 500 1600 ✓
不同数据规模的性能
yaml
向量数 构建时间 搜索延迟 内存占用
─────────────────────────────────
100万 30s 3ms 100MB
1000万 7m 5ms 1GB
1亿 90m 10ms 10GB
10亿 900m 15ms 100GB
🎯 应用场景:电商推荐系统
背景
- 商品库:1000 万
- 向量维度:768
- QPS:10 万
- 更新频率:日更
为什么选择 IVF_PQ?
- 数据规模大:1000万 → IVF_PQ 专为大规模优化
- 数据相对静态:日更 → 无需实时增量
- 吞吐量高:需要 1600 QPS → IVF_PQ 最快
- 精度足够:推荐系统 99% recall 可接受
实现架构
python
class ECommerceRecommender:
def __init__(self):
self.dataset = lance.open('products.lance')
def daily_reindex(self):
"""每天凌晨重建索引"""
self.dataset.create_index(
'vector',
index_type='IVF_PQ',
num_partitions=3162, # √10M
replace=True,
)
def recommend(self, query_vector, k=100):
"""实时推荐"""
return self.dataset.search(
query_vector,
k=k,
nprobes=32,
).to_list()
总结
- IVF:用簇加速搜索(分而治之)
- IVF_PQ:用量化节省空间和加速计算(99.7% 压缩)
- IVF_FLAT:精确但占用空间(100% recall)
- 选择指南 :
- 数据量 > 10M → IVF_PQ
- 精度优先 → IVF_FLAT
- 速度优先 → IVF_PQ
- 实时增量 → 改用 HNSW
下一章讲 HNSW 索引,它提供了更好的增量更新能力。