Skip to content

6.4 向量数据库与索引

在上一节中,我们详细探讨了文档分块(Chunking)技术。通过递归字符分割、语义分块和 Markdown 结构化分块等策略,长文档被切分为语义自洽的小段落,每一块再经过 Embedding 模型转换为高维向量。至此,我们手中有了一批向量——但问题随即浮现:这些向量存到哪里?又如何高效地从中检索出最相似的结果?

本节将回答这一核心问题。向量数据库是 RAG 系统的"存储与检索引擎",它专门为高维向量的相似度搜索而设计。我们将从原理出发,理解近似最近邻搜索(ANN)的底层逻辑,对比 Chroma、Pinecone、Weaviate 等主流方案,深入剖析 HNSW、IVF、PQ 三大索引算法,并通过完整代码实战完成从存储到检索的全流程。


6.4.1 为什么需要向量数据库

在 RAG 系统中,一个典型的知识库可能包含数十万到数百万份文档。每份文档经过分块后生成若干 chunk,每个 chunk 对应一个 1536 维(或 768 维、3072 维)的浮点向量。当用户发起查询时,系统需要从数百万个向量中找出与查询向量最相似的 Top-K 结果。

如果采用暴力搜索(Brute Force)——逐一计算查询向量与数据库中每个向量的距离,复杂度为 O(N×D)。假设有 100 万个 1536 维向量,单次查询需要执行约 15 亿次浮点运算,耗时数秒,这在交互式场景中完全不可接受。

向量数据库的核心价值在于:通过**近似最近邻搜索(Approximate Nearest Neighbor, ANN)**算法,在精度损失极小(通常 < 1%—5%)的前提下,将检索速度提升数百到数千倍。它不是"精确找到最近的",而是"极快地找到足够近的"——这对于 RAG 这类语义检索场景而言,已经足够。

理解暴力搜索与 ANN 的区别,可以用一个直观的数字来说明。假设知识库中有 100 万条 1536 维向量:

检索方式计算次数单次查询耗时(估算)召回率
暴力搜索100 万 × 1536 = 15 亿次3—5 秒100%
HNSW(ef_search=50)约 5000 × 1536 = 768 万次5—15 毫秒95%—99%
IVF(nprobe=8)约 8000 × 1536 = 1228 万次10—20 毫秒90%—97%

可以看到,ANN 算法以不到 1% 的精度损失换取了 200—1000 倍的速度提升。这种取舍在 RAG 系统中是值得的——因为 LLM 本身对检索结果的微小偏差并不敏感,排名第十和排名第十一的文档对最终生成质量的影响几乎可以忽略。


6.4.2 图书馆索引系统类比

理解向量数据库的工作方式,可以借助一个生活中常见的场景——图书馆

想象一座拥有百万册藏书的大型图书馆。如果你要找"关于容器编排的书籍",有几种方式:

  • 逐架翻阅(暴力搜索):从第一排书架走到最后一排,逐本阅读书名。精度 100%,但耗时不可接受。
  • 杜威十进制分类法(IVF 索引):图书馆将所有书按主题分到不同的"类别区域"。你先找到"计算机科学"区,再在该区域内浏览。这跳过了 95% 以上无关的书籍。
  • 导航地图(HNSW 索引):图书馆在入口处设置了一张"导航地图"——顶层标注各大主题区域的位置,下一层标注子区域,最底层精确到每个书架。你从顶层出发,逐层缩小范围,快速定位到目标书架。
  • 图书摘要手册(PQ 量化):图书馆为每本书编制了一份压缩版摘要手册,虽不完整但足以判断相关性。用 48 字节的摘要替代 6144 字节的完整向量,内存占用骤降两个数量级。

将上述类比映射到向量数据库的术语:

图书馆概念向量数据库概念说明
藏书向量每份文档块对应一个高维向量
书架编号向量 ID唯一标识一个向量
书名标签元数据标签信息(类别、来源、时间等)
杜威分类法IVF 聚类将向量按聚类分组,查询时只搜索部分聚类
导航地图HNSW 多层图分层导航结构,逐层缩小搜索范围
压缩摘要PQ 量化将高维向量压缩为低字节码本表示
检索台查询 API接收查询向量,返回 Top-K 结果
图书管理员索引构建引擎负责构建和维护索引结构

一个完整的向量数据库架构包含四个核心组件:

  1. 数据写入 API:接收向量及其元数据,支持 Python SDK 或 REST 接口
  2. 索引构建引擎:使用 HNSW、IVF 等算法构建索引结构
  3. 存储层:持久化向量数据与元数据,支持磁盘和内存分层
  4. 相似度引擎:接收查询向量,通过 ANN 算法快速检索 Top-K

6.4.3 主流向量数据库对比

当前向量数据库生态蓬勃发展,以下三种方案最具代表性。

Chroma —— 轻量级嵌入式首选

Chroma 是一款开源的嵌入式向量数据库,设计理念是"pip 安装即用",无需部署独立服务。

  • 部署方式pip install chromadb,数据存储在本地文件夹
  • 数据规模:适合 10 万条以内的原型开发和小型应用
  • 索引算法:HNSW
  • 核心优势:零配置、学习曲线极低、与 LangChain / LlamaIndex 深度集成
  • 局限:不支持分布式部署、无内置混合搜索、大规模场景性能有限

适用场景:RAG 原型验证、个人知识库、小型问答系统。

Pinecone —— 全托管云服务

Pinecone 是一款商业闭源的全托管向量数据库,用户无需关心任何基础设施运维。

  • 部署方式:注册账号后通过 API 密钥连接,所有数据存储在云端
  • 数据规模:按需弹性扩展,支持亿级向量
  • 索引算法:专有算法(基于 HNSW 的优化实现)
  • 核心优势:零运维、自动扩缩容、支持命名空间隔离和元数据过滤
  • 局限:闭源、数据存储在第三方云端、按用量计费可能成本较高
  • 定价:按存储量和查询量计费,有免费额度

适用场景:不想管运维的团队、快速上线产品、SaaS 应用的向量检索后端。

Weaviate —— 混合搜索与多模态

Weaviate 是一款开源的向量搜索引擎,以 GraphQL API 和内置的模块化 Embedding 处理见长。

  • 部署方式:Docker / Kubernetes 自托管,或使用 Weaviate Cloud
  • 数据规模:百万到亿级
  • 索引算法:HNSW(支持 HNSW + PQ 混合)
  • 核心优势:内置 Embedding 模型(可直接存文本,自动生成向量)、混合搜索(BM25 + 向量)、支持多模态(文本、图像、视频)
  • 局限:架构较重、学习曲线中等、GraphQL API 对初学者不友好
  • 许可证:BSD-3-Clause

适用场景:需要混合搜索(关键词 + 语义)、多模态向量存储、GraphQL 查询需求。

三者横向对比:

特性ChromaPineconeWeaviate
类型嵌入式云服务混合(自托管/云)
部署pip install注册即用Docker/K8s/云
数据量< 10 万亿级弹性百万—亿级
开源Apache 2.0闭源BSD-3
索引算法HNSW专有HNSW + PQ
混合搜索是(BM25+向量)
内置 Embedding否(需外部传入)是(内置多种模型)
元数据过滤基础强大强大
学习曲线极低中等
适合场景原型/小项目快速上线/零运维多模态/混合搜索

选型决策参考:

  • 快速原型 / 个人项目 → Chroma:零配置,三行代码起步
  • 不想运维 / 快速上线 → Pinecone:全托管,按需付费
  • 需要混合搜索 / 多模态 → Weaviate:内置 Embedding,GraphQL 查询
  • 企业级 / 超大规模 → Milvus:分布式架构,11 种索引可选
  • 极致性能 / Rust 生态 → Qdrant:Rust 编写,高性能过滤

除了上述三种代表性方案,Milvus 和 Qdrant 也值得关注:

Milvus 由 Zilliz 团队开发,是国内使用最广泛的开源向量数据库。它采用存算分离的分布式架构,支持 11 种索引类型(HNSW、IVF_FLAT、IVF_SQ8、IVF_PQ、SCANN 等),能够处理十亿级以上向量。Milvus 2.x 基于 Go 和 Rust 重写,支持云原生部署(Kubernetes Operator),是大规模生产环境的可靠选择。其劣势在于部署和运维复杂度较高,学习曲线偏陡。

Qdrant 由德国团队用 Rust 编写,以极致性能著称。它在元数据过滤与向量检索的联合优化上做得尤为出色——通过预构建过滤索引(Payload Index),即使在高选择性过滤条件下仍能保持高 QPS。Qdrant 支持 HNSW 索引,提供丰富的量化选项(标量量化 SQ、乘积量化 PQ),适合对延迟和内存都敏感的高性能检索场景。

在实际项目中,选型并非"非此即彼"。不少团队采用 Chroma 验证原型、Pinecone 过渡到 MVP、最终迁移到 Milvus 或 Qdrant 支撑生产流量的渐进路径。关键在于:先用最简单的方案验证业务逻辑,再根据实际瓶颈选择升级方向


6.4.4 索引算法详解

向量数据库的速度优势来自索引算法。以下三种是工业界最常用的 ANN 算法。

HNSW —— 分层可导航小世界图

HNSW 是目前最流行、最通用的 ANN 算法,绝大多数向量数据库的默认索引。

核心思想:构建一个多层图结构。顶层节点稀疏,负责"长距离跳跃"快速定位到目标区域;底层节点密集,负责精细搜索找到精确的最近邻。查询时从顶层入口节点出发,逐层向下导航,每一层找到离查询点最近的节点作为下一层入口。

HNSW 多层图结构:

第 2 层(稀疏):  ●──────●         ← 长距离跳跃,快速定位区域

第 1 层(中等):  ●──●──●──●       ← 中等距离导航
                   │  │  │
第 0 层(密集):  ●─●─●─●─●─●─●    ← 精细搜索,找到最近邻

查询过程:从顶层开始,逐层向下,每层找最近的节点作为下一层入口

关键参数

参数默认值作用调优方向
M16每个节点的最大连接数增大 → 精度↑、内存↑
ef_construction200构建时的搜索宽度增大 → 索引质量↑、构建时间↑
ef_search50查询时的搜索宽度增大 → 召回率↑、延迟↑
python
# Chroma 中配置 HNSW 参数
import chromadb                                    # 导入 Chroma SDK

client = chromadb.PersistentClient(path="./chroma_db")  # 创建持久化客户端,数据写入 ./chroma_db

collection = client.create_collection(             # 创建集合(类似数据库中的"表")
    name="my_collection",                          # 集合名称
    metadata={
        "hnsw:space": "cosine",                    # 距离度量:cosine(余弦)/ l2(欧氏)/ ip(内积)
        "hnsw:construction_ef": 200,               # 构建时搜索宽度:越大索引质量越高,但构建越慢
        "hnsw:search_ef": 100,                     # 查询时搜索宽度:越大召回率越高,但延迟越大
        "hnsw:M": 32,                              # 每个节点的最大连接数:越大精度越高,但内存占用越大
    }
)

适用场景:数据量 < 100 万条、对精度要求高、内存充足。绝大多数 RAG 场景的首选。

IVF —— 倒排文件索引

IVF 适合百万级以上的大规模数据。

核心思想:先对所有向量执行 K-Means 聚类,得到 N 个聚类中心。构建索引时,每个向量归入离它最近的聚类中心的"倒排列表"。查询时,只搜索离查询向量最近的 nprobe 个聚类,大幅缩小搜索范围。

IVF 工作流程:

训练阶段:
  所有向量 ──→ K-Means 聚类 ──→ N 个聚类中心

索引阶段:
  新向量 ──→ 找到最近的聚类中心 ──→ 存入该聚类的倒排列表

查询阶段:
  查询向量 ──→ 找最近的 nprobe 个聚类 ──→ 只在这些聚类中搜索

关键参数

参数推荐值作用
nlist4×√N聚类数量,越大搜索越快但精度越低
nprobenlist 的 1%—10%查询时搜索的聚类数,越大精度越高但速度越慢

适用场景:百万到千万级数据、可接受轻微精度损失。

在实际使用 IVF 时,需要注意聚类中心的训练是一次性的——这意味着如果后续新增大量数据,原有的聚类中心可能不再准确反映数据分布。解决方案是定期重新训练聚类中心(即重建索引),或使用 Milvus 等支持在线聚类的数据库。另外,nprobe 的选择是一个典型的精度—速度权衡:nprobe=1 时只搜索一个聚类,速度最快但可能遗漏相关结果;nprobe=nlist 时搜索所有聚类,退化为暴力搜索。生产实践中,通常从 nlist 的 1% 开始调参,逐步增大直到召回率满足要求。

PQ —— 乘积量化

PQ 是一种极致压缩技术,适合内存受限的大规模场景。

核心思想:将原始高维向量拆分为 M 段,每段独立执行 K-Means 量化,用码本索引(1 字节)替代原始浮点值(4 字节),实现 32 倍压缩。

PQ 压缩原理:

原始向量 [1536 维 float32]

    ▼ 拆分为 M 段(如 48 段,每段 32 维)

    ▼ 每段量化到最近的码字(codebook 有 256 个码字)

压缩向量 [48 字节 uint8]  ← 存储减少 32 倍!

1536 × 4 bytes = 6144 bytes  →  48 × 1 byte = 48 bytes

适用场景:十亿级向量、内存受限、可接受精度损失。常与 IVF 组合使用(IVF_PQ)。

需要指出的是,PQ 量化会带来不可逆的精度损失——压缩后的向量无法还原为原始向量。M 段的划分数量和每段的码本大小共同决定了压缩率和精度的平衡。常见的配置是 M=48、码本 256(即每段 1 字节),此时 1536 维向量从 6144 字节压缩到 48 字节。如果对精度要求更高,可以增加 M 值(如 M=96)或增大码本(如 512 个码字),但压缩率会相应降低。在 RAG 场景中,建议先使用未压缩的 HNSW 索引建立基线,再尝试 PQ 压缩并对比召回率变化,选择可接受精度损失范围内的最大压缩率。

索引选型速查
数据量推荐索引原因
< 10 万HNSW最简单,精度最高
10 万—100 万HNSW通用首选,精度与速度兼顾
100 万—1000 万IVF + PQ大规模下平衡精度和内存
> 1000 万IVF + PQ + 磁盘需要分布式方案(如 Milvus)
内存极度受限PQ极致压缩,精度可接受

除了上述三种基本索引,工业界还广泛使用组合索引策略。最常见的是 IVF_PQ:先用 IVF 将向量空间划分为若干聚类(解决"搜索范围太大"的问题),再在每个聚类内使用 PQ 压缩向量(解决"内存不够"的问题)。这种组合在十亿级场景下能将内存占用从 TB 级降到 GB 级,同时保持可接受的召回率。另一种组合是 HNSW+PQ:在 HNSW 图的每个节点中存储 PQ 压缩后的向量,兼顾导航效率和内存效率,Weaviate 即采用了这种方案。


6.4.5 实战:Chroma 从零到一

以下完整代码演示如何使用 Chroma 完成从建库、写入、检索到更新的全流程。

这段代码涵盖了 RAG 向量存储的典型生命周期:创建数据库实例、配置 Embedding 模型、写入文档与元数据、执行语义检索、带条件过滤检索、以及文档的更新与删除。建议在阅读代码时重点关注每一步的参数含义和返回数据结构。

python
# 安装依赖(首次运行时执行)
# pip install chromadb                              # 安装 Chroma 向量数据库

import chromadb                                     # 导入 Chroma 主模块
from chromadb.utils import embedding_functions      # 导入内置 Embedding 工具

# ====== 第一步:创建客户端 ======
client = chromadb.PersistentClient(path="./my_chroma_db")
# PersistentClient:持久化模式,数据写入磁盘文件夹 ./my_chroma_db
# 进程重启后数据依然存在;若用 chromadb.Client() 则为纯内存模式

# ====== 第二步:选择 Embedding 模型 ======
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key="your-api-key",                         # 替换为你的 OpenAI API Key
    model_name="text-embedding-3-small"             # 使用 OpenAI 的 text-embedding-3-small 模型
)
# 也可使用免费的 Sentence-Transformers(无需 API Key):
# sentence_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
#     model_name="all-MiniLM-L6-v2"                 # 384 维向量,适合本地运行
# )

# ====== 第三步:创建集合 ======
collection = client.create_collection(
    name="tech_docs",                               # 集合名称,类似数据库中的"表"
    embedding_function=openai_ef,                   # 指定 Embedding 模型
    metadata={"hnsw:space": "cosine"}               # 使用余弦相似度作为距离度量
)

# ====== 第四步:准备并写入文档 ======
documents = [                                       # 待入库的文档列表
    "Kubernetes 是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用。",
    "Docker 是一个容器化平台,允许开发者将应用打包到轻量级的容器中。",
    "Redis 是一个开源的内存数据结构存储,用作数据库、缓存和消息代理。",
    "PostgreSQL 是一个功能强大的开源关系型数据库,支持复杂查询和事务。",
    "Nginx 是一个高性能的 HTTP 和反向代理服务器,也作为邮件代理服务器。",
]

metadatas = [                                       # 每条文档对应的元数据(用于过滤)
    {"category": "容器", "difficulty": "高级"},
    {"category": "容器", "difficulty": "初级"},
    {"category": "数据库", "difficulty": "中级"},
    {"category": "数据库", "difficulty": "高级"},
    {"category": "网络", "difficulty": "中级"},
]

ids = [f"doc_{i}" for i in range(len(documents))]   # 为每条文档生成唯一 ID

collection.add(                                     # 批量写入:Chroma 自动调用 Embedding 模型生成向量
    documents=documents,                             # 文本内容
    metadatas=metadatas,                            # 元数据
    ids=ids,                                        # 唯一标识
)

print(f"Collection 中共有 {collection.count()} 条文档")  # 输出当前文档总数

# ====== 第五步:语义检索 ======
query = "容器编排平台"                               # 用户的自然语言查询
results = collection.query(                         # 执行相似度检索
    query_texts=[query],                            # Chroma 自动将查询文本转为向量
    n_results=3,                                    # 返回 Top-3 最相似的结果
)

print(f"\n查询: {query}")
print("Top-3 结果:")
for i, (doc_id, doc, distance) in enumerate(zip(
    results["ids"][0],                              # 结果 ID 列表
    results["documents"][0],                        # 结果文本列表
    results["distances"][0]                         # 距离值列表(越小越相似)
)):
    print(f"  {i+1}. [{doc_id}] 距离={distance:.4f} | {doc}")

# ====== 第六步:带元数据过滤的检索 ======
print("\n带过滤的检索 (category='数据库'):")
results_filtered = collection.query(
    query_texts=["数据存储"],                        # 查询文本
    n_results=2,                                    # 返回 Top-2
    where={"category": "数据库"}                    # 元数据过滤:只在 category 为"数据库"的文档中检索
)

for i, (doc_id, doc) in enumerate(zip(
    results_filtered["ids"][0],
    results_filtered["documents"][0]
)):
    print(f"  {i+1}. [{doc_id}] {doc}")

# ====== 第七步:更新与删除 ======
collection.update(                                   # 更新已有文档(向量会自动重新生成)
    ids=["doc_0"],                                   # 指定要更新的文档 ID
    documents=["Kubernetes (K8s) 是一个强大的容器编排平台..."],  # 新的文本内容
)

collection.delete(ids=["doc_4"])                     # 按 ID 删除文档

print(f"\n更新后 Collection 中共有 {collection.count()} 条文档")

6.4.6 实战:Milvus Lite 快速上手

当数据量增长到百万级以上,Chroma 的嵌入式架构将力不从心。Milvus 是企业级分布式向量数据库的首选。以下使用 Milvus Lite(无需 Docker 的轻量版本)演示核心流程。

与 Chroma 的"自动 Embedding"不同,Milvus 要求开发者自行管理 Embedding 生成过程——你需要先调用 Embedding 模型 API 将文本转为向量,再将向量写入数据库。这种设计虽然增加了少量代码,但也带来了更大的灵活性:你可以自由选择任意 Embedding 模型(OpenAI、Cohere、本地模型等),甚至为不同字段使用不同的模型。

python
# 安装依赖
# pip install pymilvus "pymilvus[model]"            # 安装 Milvus Python SDK 及模型工具包

from pymilvus import MilvusClient, DataType         # 导入 Milvus 客户端和数据类型枚举

# ====== 第一步:创建客户端(Milvus Lite 模式) ======
client = MilvusClient("./milvus_demo.db")
# Milvus Lite:数据存储在本地 .db 文件中,无需启动 Docker
# 生产环境建议使用 Milvus 标准版或集群版

# ====== 第二步:定义 Schema(字段结构) ======
schema = client.create_schema(                      # 创建 Schema 对象
    auto_id=False,                                  # 不自动生成 ID,由用户指定
    enable_dynamic_field=True,                     # 允许动态字段(可存额外元数据)
)

schema.add_field(                                   # 添加主键字段
    field_name="id",                                # 字段名
    datatype=DataType.INT64,                        # 数据类型:64 位整数
    is_primary=True,                                # 标记为主键
)

schema.add_field(                                   # 添加文本字段
    field_name="text",                              # 字段名
    datatype=DataType.VARCHAR,                      # 数据类型:变长字符串
    max_length=500,                                 # 最大长度 500 字符
)

schema.add_field(                                   # 添加向量字段
    field_name="vector",                            # 字段名
    datatype=DataType.FLOAT_VECTOR,                 # 数据类型:浮点向量
    dim=1536,                                        # 维度:与 Embedding 模型输出维度一致
)

# ====== 第三步:配置索引 ======
index_params = client.prepare_index_params()        # 创建索引参数构建器
index_params.add_index(                             # 为 vector 字段添加索引
    field_name="vector",                            # 索引作用于哪个字段
    index_type="HNSW",                             # 索引类型:HNSW(也可选 IVF_FLAT、IVF_PQ)
    metric_type="COSINE",                          # 相似度度量:余弦相似度
    params={"M": 16, "efConstruction": 200}         # HNSW 参数:M=连接数,efConstruction=构建宽度
)

# ====== 第四步:创建 Collection ======
client.create_collection(
    collection_name="rag_docs",                     # Collection 名称
    schema=schema,                                  # 绑定 Schema
    index_params=index_params,                      # 绑定索引配置
)

# ====== 第五步:生成 Embedding 并写入数据 ======
from openai import OpenAI                           # 导入 OpenAI SDK

openai_client = OpenAI()                            # 初始化 OpenAI 客户端

def get_embedding(text):
    """调用 OpenAI API 获取文本的向量表示"""
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",             # 使用 text-embedding-3-small 模型
        input=text,                                 # 待向量化的文本
        dimensions=1536                             # 输出维度:需与 Schema 中定义的 dim 一致
    )
    return response.data[0].embedding               # 返回向量(1536 维浮点列表)

documents = [                                       # 待入库的文档列表
    "Milvus 是一个开源的向量数据库,专为 AI 应用设计。",
    "HNSW 是一种高效的近似最近邻搜索算法。",
    "向量数据库通过 ANN 算法加速相似度检索。",
]

data = []
for i, doc in enumerate(documents):                 # 逐条生成向量并组装数据
    data.append({
        "id": i,                                    # 主键
        "text": doc,                               # 文本内容
        "vector": get_embedding(doc),              # 调用 Embedding API 生成向量
    })

client.insert(collection_name="rag_docs", data=data)  # 批量插入数据
print(f"插入 {len(data)} 条数据")

# ====== 第六步:向量检索 ======
query_vec = get_embedding("什么是向量数据库?")       # 将查询文本转为向量

results = client.search(                            # 执行相似度搜索
    collection_name="rag_docs",                     # 指定 Collection
    data=[query_vec],                               # 查询向量(注意是列表,支持批量查询)
    limit=2,                                        # 返回 Top-2 结果
    output_fields=["text"],                         # 指定返回哪些字段
)

print("\n检索结果:")
for i, hits in enumerate(results):                 # results 是嵌套列表,外层对应每个查询向量
    for hit in hits:                               # 内层是每个查询的命中结果
        print(f"  id={hit['id']}, distance={hit['distance']:.4f}")  # 输出 ID 和距离值
        print(f"  text: {hit['entity']['text']}")  # 输出匹配的文本内容

# ====== 第七步:清理资源 ======
client.drop_collection("rag_docs")                  # 删除 Collection(开发测试后清理)

6.4.7 运维调优要点

在生产环境中,向量数据库的性能不仅取决于索引选型,还与写入策略和监控体系密切相关。良好的运维习惯能让系统的查询延迟降低一个数量级,也能在数据增长时从容应对。

批量写入优化:避免单条写入带来的 API 调用开销,建议每 500—1000 条为一批进行插入。单条写入时每条记录都会触发一次网络往返和一次索引更新,而批量写入可将这些操作合并,吞吐量提升可达 10 倍以上。

python
def batch_insert(collection, documents, embeddings, batch_size=500):
    """批量写入函数:将大列表分批插入向量数据库"""
    for i in range(0, len(documents), batch_size):  # 按批次大小步进
        batch_docs = documents[i:i+batch_size]      # 切片取当前批次的文档
        batch_embs = embeddings[i:i+batch_size]    # 切片取当前批次的向量
        batch_ids = [f"doc_{j}" for j in range(i, i+len(batch_docs))]  # 生成连续 ID
        collection.add(                            # 批量写入当前批次
            documents=batch_docs,
            embeddings=batch_embs,
            ids=batch_ids,
        )
        print(f"已写入 {i+len(batch_docs)}/{len(documents)}")  # 进度提示

索引构建时机:建议先写入所有数据,再统一构建索引。边写边建会导致频繁的索引重组,速度慢 3—5 倍。Milvus 的推荐流程为 flush()create_index()load(),即先刷盘确保数据持久化,再创建索引结构,最后将索引加载到内存中供查询使用。

容量规划:在项目初期就需要预估数据规模,为后续扩容预留空间。一个 1536 维 float32 向量占用约 6KB 存储,100 万条向量约需 6GB 磁盘空间。加上索引开销(HNSW 索引约为原始数据的 1.5—2 倍),100 万条数据实际需要约 10—12GB 内存。如果使用 PQ 量化,内存可压缩到 1/8—1/16,但精度会相应下降。在做容量规划时,建议按照预估数据量的 2 倍来准备硬件资源,以应对索引膨胀和未来增长。

关键监控指标

指标含义告警阈值参考
QPS每秒查询数根据容量规划设定
P95 延迟95% 请求的响应时间> 100ms 需关注
P99 延迟99% 请求的响应时间> 500ms 需优化
Recall@KTop-K 召回率< 90% 需调整索引参数
内存使用率已用/总容量> 80% 需扩容
磁盘使用率已用/总容量> 85% 需扩容

6.4.8 常见误区

在实际开发中,向量数据库的使用存在不少容易被忽视的陷阱。以下是六个最常见的问题及其解决方案,每一条都来自真实的生产经验。

误区一:认为向量检索是精确的

ANN 算法本质上是"近似"的。HNSW 的召回率通常在 95%—99%,IVF+PQ 可能在 90%—95%。如果 RAG 系统出现"明明应该检索到却没检索到"的情况,首先检查索引参数——ef_searchnprobe 调大可以提升召回率。对精度要求极高的场景(法律、医疗),可在 ANN 结果上追加一轮精确计算做二次校验。

误区二:忽略距离度量的选择

余弦相似度(Cosine)、欧氏距离(L2)和内积(Inner Product)给出不同的排序结果。文本语义检索通常使用余弦相似度,因为它只关注向量方向(语义方向)而非模长。如果错误地使用了 L2,模长较大的向量会被偏好性地排序靠前,导致检索偏差。务必根据 Embedding 模型的推荐选择度量方式——OpenAI 的模型推荐 Cosine,部分 Sentence-Transformers 模型推荐 L2 或 Inner Product。

误区三:先建索引再写数据

不少开发者习惯在创建 Collection 时就配置索引,然后边写边查。这在数据量小时没有明显问题,但在大规模写入时会导致索引频繁重建,严重影响写入速度。正确做法是:先写入所有数据,再统一构建索引。如果是增量更新场景,可设置定时任务在低峰期重建索引。

误区四:忽视元数据过滤的性能影响

元数据过滤(如 where={"category": "数据库"})在向量检索之前或之后执行。如果过滤条件命中数据过少,可能导致 ANN 搜索空间不足,返回结果质量下降。对于强过滤需求,建议预先对数据进行分区或使用支持"过滤+向量检索联合优化"的数据库(如 Qdrant 的过滤索引)。

误区五:向量维度不匹配

Embedding 模型输出的维度必须与向量数据库 Schema 中定义的 dim 完全一致。例如,text-embedding-3-small 输出 1536 维,all-MiniLM-L6-v2 输出 384 维。如果维度不匹配,写入时直接报错。在更换 Embedding 模型时,必须重建整个 Collection 并重新生成所有向量。一个常见的错误是:团队先使用免费模型(384 维)完成原型,上线后切换到 OpenAI 模型(1536 维),但忘记重建 Collection,导致查询返回空结果或异常错误。建议在 Collection 的元数据中记录所用的 Embedding 模型名称和维度,以便后续追溯。

误区六:忽视 Embedding 模型版本管理

Embedding 模型会随时间更新迭代(如 OpenAI 从 text-embedding-ada-002 升级到 text-embedding-3-small)。不同版本的模型生成的向量空间不兼容——即使维度相同,语义空间的映射也不同。如果知识库中一部分向量用旧模型生成、另一部分用新模型生成,检索质量会显著下降。正确做法是:记录每条向量对应的模型版本,在模型升级时统一重新生成全量向量。


6.4.9 本节小结

本节围绕"向量数据库与索引"展开了系统讲解,核心要点如下:

  1. 向量数据库解决的核心问题:通过 ANN 算法将高维向量检索从 O(N×D) 的暴力搜索降低到亚秒级响应,在精度损失 < 5% 的前提下实现数百倍加速。

  2. 图书馆索引系统类比:HNSW 对应导航地图(分层导航),IVF 对应杜威分类法(聚类分区),PQ 对应压缩摘要(码本量化)——三种算法各有适用场景。

  3. 主流方案选型:Chroma 适合原型(嵌入式、零配置),Pinecone 适合快速上线(全托管、零运维),Weaviate 适合多模态和混合搜索。选型应根据数据量、运维能力和功能需求综合决策。

  4. 索引算法:HNSW 是通用首选(< 100 万数据),IVF 适合大规模聚类分区,PQ 提供极致压缩。实际生产中常组合使用(如 IVF_PQ)。

  5. 运维关键:批量写入、先写后建索引、监控 QPS/延迟/召回率,是保障向量数据库稳定运行的三大要点。

  6. 常见陷阱:向量检索是近似的而非精确的、距离度量必须匹配 Embedding 模型、维度不匹配会导致写入失败、Embedding 模型版本升级需全量重建——这些是生产中最容易踩的坑。


在下一节中,我们将进入 RAG 检索链路的核心环节——检索策略。向量数据库解决了"存什么、怎么存"的问题,但"怎么查"同样重要。我们将探讨 Top-K 检索的参数调优、多路召回(Multi-Query Retrieval)与重排序(Reranking)策略,以及如何通过混合检索(Hybrid Search)融合关键词匹配与语义搜索的优势,最终将检索质量推到 RAG 系统的可用上限。