探秘新一代向量存储格式Lance-format (十六) 向量索引 - IVF 系列

第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?

  1. 数据规模大:1000万 → IVF_PQ 专为大规模优化
  2. 数据相对静态:日更 → 无需实时增量
  3. 吞吐量高:需要 1600 QPS → IVF_PQ 最快
  4. 精度足够:推荐系统 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 索引,它提供了更好的增量更新能力。

相关推荐
语落心生1 小时前
探秘新一代向量存储格式Lance-format (十四) 索引系统架构
架构
语落心生1 小时前
探秘新一代向量存储格式Lance-format (十五) 标量索引实现
架构
5***b972 小时前
什么是射频?射频基本架构?
架构
settingsun12253 小时前
分布式系统架构:百万并发系统设计
云原生·架构·分布式系统
谷隐凡二3 小时前
Kubernetes主从架构简单解析:基于Python的模拟实现
python·架构·kubernetes
c***69303 小时前
超详细:数据库的基本架构
数据库·架构
Mintopia4 小时前
无界微前端:父子应用通信、路由与状态管理最佳实践
架构·前端框架·全栈
L***86535 小时前
【架构解析】深入浅析DeepSeek-V3的技术架构
架构
Peter_Monster5 小时前
大语言模型(LLM)架构核心解析(干货篇)
人工智能·语言模型·架构