Skip to content

向量数据库实战(Milvus/Chroma)

从 Embedding 的第一个维度到 RAG 系统的最后一公里——手把手带你玩转向量数据库。


4. 索引算法揭秘——HNSW、IVF、DiskANN

上一章我们知道了:百万级向量上做暴力搜索太慢,需要 ANN 索引来加速。但"索引"到底是怎么工作的?

这一章,我们用直觉而非数学来拆解三大主流索引算法。你不需要记住公式,只需要理解每种算法的核心思想适用场景,就能在实际项目中做出正确的选型。


4.1 HNSW:高速公路式的图索引

HNSW(Hierarchical Navigable Small World) 是目前最流行的向量索引算法,Milvus 和 Chroma 的默认索引都是它。

核心思想:跳表 + 图搜索

HNSW 的灵感来自一个现实类比——城市交通网络

HNSW 的直觉理解——高速公路网络:

  从北京找到一家上海的小餐馆

  方案 A:只走小路(暴力搜索)
  ─────────────────────────────────────
    北京 → 每条街都走一遍 → 效率极低
    → 每个路口都要停下来问路
    → 走了 1000 条路才找到

  方案 B:高速公路 + 省道 + 市道(HNSW)
  ─────────────────────────────────────
    第 3 层(高速公路):北京 ──→ 上海     粗略定位
    第 2 层(省道):    上海 ──→ 浦东区   缩小范围
    第 1 层(市道):    浦东 ──→ 某条街   精确找到
    第 0 层(步行):    某街 ──→ 那家餐馆  到达目标

    → 只走了 4 步就到了
    → 从宏观到微观,逐层细化

HNSW 的多层结构

HNSW 索引的层次结构(简化示意):

  第 3 层(最稀疏):   A ━━━━━━━━━━━ B
  只有极少数"枢纽"节点   │               │
  负责远距离跳转         │               │
                        ▼               ▼
  第 2 层:            A ━━━ C ━━━ B ━━━ D
  更多节点,更细粒度     │       │       │
                        ▼       ▼       ▼
  第 1 层:          A ─ E ─ C ─ F ─ B ─ G ─ D
  大部分节点在这层       │   │   │   │   │   │
                        ▼   ▼   ▼   ▼   ▼   ▼
  第 0 层(最稠密):  A E H C I F J B K G L D
  所有节点都在这层
  每个节点连接最近的邻居

  搜索过程(找和 Q 最近的节点):
  ─────────────────────────────────
    1. 从第 3 层的入口节点 A 开始
    2. 在第 3 层:A 和 B 谁离 Q 更近?→ B
    3. 从 B 下降到第 2 层
    4. 在第 2 层:B 的邻居 C、D 谁离 Q 更近?→ C
    5. 从 C 下降到第 1 层
    6. 在第 1 层:C 的邻居 E、F 谁离 Q 更近?→ F
    7. 从 F 下降到第 0 层
    8. 在第 0 层:F 的邻居 I、J 谁离 Q 更近?→ I
    9. I 的所有邻居都比 I 更远 → I 就是结果

  总共只比较了 ~8 个节点(而非全部 12 个)
  数据量越大,优势越明显

HNSW 的关键参数

两个最重要的参数:

  M(每层的最大邻居数)
  ─────────────────────────────────────
    M = 16(默认)
    → 每个节点最多连接 16 个邻居
    → M 越大 → 连接越多 → 召回率越高 → 但内存占用更多
    → M 越小 → 连接越少 → 速度更快 → 但可能漏掉结果

    推荐值:
      小数据集(< 10 万):M = 8-16
      中数据集(10-100 万):M = 16-32
      大数据集(> 100 万):M = 32-64

  ef_construction(建索引时的搜索宽度)
  ─────────────────────────────────────
    ef_construction = 200(默认)
    → 建索引时,每插入一个节点,搜索 200 个候选邻居
    → 值越大 → 索引质量越高 → 但建索引越慢
    → 通常设为 M 的 8-10 倍

  ef_search / ef(搜索时的搜索宽度)
  ─────────────────────────────────────
    ef = 64(默认)
    → 搜索时,维护一个大小为 ef 的候选列表
    → ef 越大 → 召回率越高 → 但搜索越慢
    → ef 必须 ≥ Top-K

    推荐值:
      追求速度:ef = 64
      平衡模式:ef = 128
      追求精度:ef = 256-512

HNSW 的优缺点

维度表现
搜索速度⭐⭐⭐⭐⭐ 极快,毫秒级
召回率⭐⭐⭐⭐⭐ 95-99%,接近精确搜索
内存占用⭐⭐ 高,所有数据必须在内存中
建索引速度⭐⭐⭐ 中等,支持增量插入
适合场景数据量 < 5000 万,对延迟要求高

一句话总结:HNSW 是"用内存换速度"的典型策略。如果你的数据能装进内存,HNSW 几乎是最优选择。

4.2 IVF:先分区再搜索

IVF(Inverted File Index,倒排文件索引) 的思路更直接——先把数据分成若干区,搜索时只看几个最相关的区。

核心思想:聚类 + 就近搜索

IVF 的直觉理解——图书馆分区:

  传统方式(暴力搜索):
    → 在整个图书馆找一本关于 Python 的书
    → 每一排书架都要扫一遍

  IVF 方式:
    → 图书馆分成 100 个区域(聚类中心)
    → 每个区域放语义相近的书
    → 要找 Python 书?先找到"编程语言区"(最近的聚类中心)
    → 只在"编程语言区"的 2-3 个书架上找
    → 跳过了其他 97 个区域

IVF 的工作流程

IVF 索引的构建与搜索:

  ═══ 建索引阶段 ═══

  Step 1:用 K-Means 聚类把所有向量分成 nlist 个簇
  ─────────────────────────────────────────────

    1000 万个向量,nlist = 1024

    ┌─────────┐  ┌─────────┐       ┌─────────┐
    │ 簇 1     │  │ 簇 2     │ ...  │ 簇 1024  │
    │ ~9766 个  │  │ ~9766 个 │      │ ~9766 个 │
    │ 向量     │  │ 向量     │      │ 向量     │
    │          │  │          │      │          │
    │ 中心 C1  │  │ 中心 C2  │      │ 中心 C1024│
    └─────────┘  └─────────┘       └─────────┘

  Step 2:记录每个向量属于哪个簇(倒排表)

  ═══ 搜索阶段 ═══

  Step 3:查询向量 Q 来了
  ─────────────────────────────────────────────
    1. Q 和 1024 个簇中心比较距离
       → 找到最近的 nprobe 个簇(如 nprobe = 8)
    2. 只在这 8 个簇内做暴力搜索
       → 搜索范围:8 × 9766 ≈ 78000 个向量
       → 而不是全部 1000 万个
       → 搜索量减少到 0.78%

  速度提升:
    暴力搜索:1000 万个向量
    IVF 搜索:~78000 个向量(nprobe=8)
    → 速度提升约 128 倍

IVF 的关键参数

两个核心参数:

  nlist(簇的数量)
  ─────────────────────────────────────
    → 把数据分成多少个区域
    → nlist 越大 → 每个簇越小 → 搜索更快 → 但可能跨簇丢失
    → 经验公式:nlist = √N(N 为向量总数)

    推荐值:
      10 万条 → nlist = 256-512
      100 万条 → nlist = 1024-2048
      1000 万条 → nlist = 4096-8192

  nprobe(搜索时检查的簇数量)
  ─────────────────────────────────────
    → 搜索时扫描多少个簇
    → nprobe 越大 → 召回率越高 → 但速度越慢
    → nprobe = nlist 时退化为暴力搜索

    推荐值:
      追求速度:nprobe = 8-16
      平衡模式:nprobe = 32-64
      追求精度:nprobe = 128-256

  两者的配合关系:
    nprobe / nlist = 扫描比例
    → 8 / 1024 = 0.78%(只看不到 1% 的数据)
    → 64 / 1024 = 6.25%(看 6% 的数据,召回率 > 95%)

IVF 的优缺点

维度表现
搜索速度⭐⭐⭐⭐ 快,但不如 HNSW
召回率⭐⭐⭐⭐ 依赖 nprobe 参数,通常 90-98%
内存占用⭐⭐⭐⭐ 较低,支持量化压缩(IVF_PQ)
建索引速度⭐⭐ 需要全量 K-Means 聚类,较慢
适合场景数据量大、内存有限、可接受稍低召回率

IVF 的变体IVF_FLAT(簇内暴力搜索)适合精度要求高的场景;IVF_PQ(簇内量化压缩)适合内存紧张的场景,能把内存占用降低 4-8 倍,代价是召回率下降 2-5%。

4.3 DiskANN:当数据大到内存放不下

当数据量达到亿级,连 IVF 都撑不住了——因为即使是压缩后的向量,内存也装不下。

DiskANN 的解法是:把索引放在 SSD 磁盘上

DiskANN 解决的问题:

  1 亿条 1024 维向量的存储需求:
  ─────────────────────────────────────
    原始大小:100M × 1024 × 4 bytes = 400 GB
    HNSW 索引:~500 GB(含图结构)     ← 需要 500GB 内存!
    IVF_PQ:   ~50 GB(压缩后)        ← 还是太大
    DiskANN:  ~5 GB 内存 + 400 GB SSD  ← ✅ 可行!

  DiskANN 的核心能力:
    → 只在内存中放一个轻量级索引(导航图)
    → 实际向量数据存在 SSD 上
    → 搜索时通过内存索引定位候选区域
    → 再从 SSD 读取少量向量做精确计算

DiskANN 的工作原理

DiskANN 的搜索流程:

  内存(~5 GB)                 SSD 磁盘(~400 GB)
  ┌──────────────────┐         ┌──────────────────────┐
  │ 压缩的导航图       │         │ 完整的向量数据         │
  │ (PQ 量化后的向量)  │         │ (原始精度)             │
  │                  │         │                      │
  │  搜索入口          │         │  向量 1: [0.82, ...]  │
  │    ↓              │         │  向量 2: [0.15, ...]  │
  │  粗略定位候选集     │  ──→   │  向量 3: [0.93, ...]  │
  │  (~100 个候选)     │  读取   │  ......              │
  └──────────────────┘         └──────────────────────┘


                                精确重排序 Top-K

  性能表现(1 亿条 1024 维):
    延迟:~5ms(SSD NVMe)
    召回率:95%+
    内存:5-10 GB
    → 对比 HNSW 需要 500 GB 内存,DiskANN 只要 5 GB

DiskANN 的优缺点

维度表现
搜索速度⭐⭐⭐ 毫秒级,但比纯内存方案慢 2-5 倍
召回率⭐⭐⭐⭐ 95%+,通过参数可调
内存占用⭐⭐⭐⭐⭐ 极低,大部分数据在磁盘
建索引速度⭐⭐ 需要全量构建,耗时较长
适合场景亿级数据、内存有限、可接受 5ms 级延迟

使用门槛:DiskANN 需要高性能 NVMe SSD,HDD 上性能会急剧下降。Milvus 从 2.3 版本开始支持 DiskANN 索引。

4.4 索引选型决策树

三种索引各有所长,实际项目中怎么选?用这棵决策树:

索引选型决策树:

  你的数据量是多少?

  ├── < 1 万条
  │   → 不需要索引,直接暴力搜索(FLAT)
  │   → Chroma 默认就是这样

  ├── 1 万 ~ 500 万条
  │   │
  │   ├── 对延迟要求极高(< 5ms)?
  │   │   └── ✅ HNSW(内存够的前提下)
  │   │
  │   ├── 内存紧张?
  │   │   └── ✅ IVF_PQ(牺牲一些精度换内存)
  │   │
  │   └── 不确定?
  │       └── ✅ HNSW(默认最优选)

  ├── 500 万 ~ 5000 万条
  │   │
  │   ├── 内存 > 64 GB?
  │   │   └── ✅ HNSW(M=32, ef=128)
  │   │
  │   ├── 内存 16-64 GB?
  │   │   └── ✅ IVF_PQ(nlist=4096, nprobe=64)
  │   │
  │   └── 内存 < 16 GB?
  │       └── ✅ DiskANN(需要 NVMe SSD)

  └── > 5000 万条

      ├── 有大内存服务器(> 256 GB)?
      │   └── ✅ HNSW(分布式部署 + 分片)

      └── 普通服务器?
          └── ✅ DiskANN(唯一可行方案)

三大索引横向对比

维度HNSWIVF_FLATDiskANN
搜索延迟~1ms~5ms~5-10ms
召回率95-99%90-98%95%+
内存占用极低
建索引速度中(增量)慢(全量)慢(全量)
支持增量插入❌ 需重建❌ 需重建
最大数据规模~5000 万~1 亿10 亿+
硬件要求大内存中等内存NVMe SSD
Milvus 支持✅(v2.3+)
Chroma 支持✅(默认)

在 Milvus 和 Chroma 中配置索引

python
# --- Milvus:创建 HNSW 索引 ---
from pymilvus import Collection

collection = Collection("my_docs")

index_params = {
    "metric_type": "COSINE",    # 距离度量
    "index_type": "HNSW",       # 索引类型
    "params": {
        "M": 16,                 # 每层最大邻居数
        "efConstruction": 200    # 建索引搜索宽度
    }
}
collection.create_index("embedding", index_params)

# 搜索时设置 ef
search_params = {"metric_type": "COSINE", "params": {"ef": 128}}


# --- Milvus:创建 IVF_FLAT 索引 ---
index_params = {
    "metric_type": "COSINE",
    "index_type": "IVF_FLAT",
    "params": {"nlist": 1024}    # 簇的数量
}
collection.create_index("embedding", index_params)

# 搜索时设置 nprobe
search_params = {"metric_type": "COSINE", "params": {"nprobe": 64}}


# --- Chroma:默认使用 HNSW,可调参数 ---
import chromadb

client = chromadb.Client()
collection = client.create_collection(
    name="my_docs",
    metadata={
        "hnsw:space": "cosine",        # 距离度量
        "hnsw:M": 16,                  # 邻居数
        "hnsw:construction_ef": 200,   # 建索引宽度
        "hnsw:search_ef": 128          # 搜索宽度
    }
)

实用建议:如果你刚开始,不需要纠结索引选型——用 HNSW 默认参数就好。等数据量真正到了百万级,再根据实际的延迟和内存表现来调优。过早优化是万恶之源。


本章小结

知识点要点
HNSW多层图索引,搜索最快(~1ms),但吃内存
HNSW 参数M=16(邻居数)、ef=128(搜索宽度)是好的起点
IVF先聚类分区再搜索,内存友好,召回率依赖 nprobe
IVF 参数nlist=√N,nprobe=nlist 的 1-10%
DiskANN索引放 SSD,亿级数据唯一方案,需要 NVMe
选型原则数据 < 500 万用 HNSW,内存紧张用 IVF_PQ,亿级用 DiskANN
默认建议不确定就用 HNSW 默认参数,够用再调

下一章预告:Chroma 实战——5 分钟上手向量数据库。终于要动手操作了!我们会从安装到 CRUD,完整走通 Chroma 的所有核心操作,包括嵌入式模式、元数据过滤、持久化存储,让你在 5 分钟内拥有一个可用的向量数据库。

坚持是一种品格