3. 向量索引与存储
“Embedding 是文本与向量搜索之间的桥梁。存储优化是生产级 RAG 系统的关键。” —— RAG 基础设施原则
本章涵盖 Embedding 模型选择、批量生成策略、缓存技术、索引算法原理、高级索引策略、向量数据库架 构以及 RAG 系统的生产环境优化。
3.1 理解 RAG 中的 Embedding
Embedding 的角色
Embedding (嵌入) 是连接文本与向量搜索的桥梁。它们将语义含义转换为数值向量,从而可以进行数学上的比较。
核心概念:
- 向量空间:相似的概念在高维空间中彼此接近
- 维度:更高的维度能捕获更多细微差别,但成本更高
- 模型选择:不同的模型针对不同的用例进行了优化
- 批量处理:成批生成 Embedding 以减少 API 调用
- 缓存:缓存 Embedding 以避免重复计算
为什么向量索引很重要
索引的优势对比:
| 方法 | 搜索时间 | 准确度 | 内存占用 | 适用场景 |
|---|---|---|---|---|
| 线性扫描 | 10-30s | 100% | 低 | < 1K 文档 |
| IVF (倒排文件) | 100-500ms | 95% | 中 | 1K-100K 文档 |
| HNSW (分层小世界) | 10-50ms | 98% | 高 | 100K-10M 文档 |
| 量化 (Quantization) | 5-20ms | 90% | 极低 | 10M+ 文档 |
3.2 索引基础
3.2.1 什么是索引?
索引 (Index) 是一种通过避免全表扫描来加速数据检索的数据结构。
书籍索引比喻:
- 无索引:阅读每一页来寻找“机器学习”
- 有索引:在索引中查找“机器学习” → 直接跳转到第 42, 87, 134 页
- 结果:查询速度提升 100 倍
数据库索引 vs. 向量索引:
| 维度 | 传统数据库 (B-tree) | 向量索引 (HNSW/IVF) |
|---|---|---|
| 查询类型 | 精确匹配 (WHERE id = 42) | 相似度 (最近的向量) |
| 结构 | 平衡树 | 图或聚类 |
| 复杂度 | O(log n) 查找 | O(log n) 近似搜索 |
| 用例 | 结构化数据 | 非结构化语义搜索 |
3.2.2 向量搜索的挑战
维度灾难 (The Curse of Dimensionality):
随着维度的增加,距离度量会失去意义,搜索在计算上变得难以处理。
为什么传统索引对向量失效:
- B-tree 不支持相似度搜索:B-tree 按排序键组织数据,而非空间接近度
- 高维度空间是稀疏的:随着维度增加,树状结构变得极低效
- 距离计算开销大:在 1536 维上计算余弦相似度的成本很高
解决方案:近似最近邻 (ANN) 算法通过牺牲极小的准确性来换取巨大的速度提升。
3.2.3 向量索引类型
精确搜索 vs. 近似搜索:
| 索引类型 | 搜索方法 | 时间复杂度 | 准确度 | 适用场景 |
|---|---|---|---|---|
| Flat (线性扫描) | 将查询与每个向量对比 | O(n × d) | 100% | < 1K 文档 |
| IVF (倒排文件) | 仅搜索相关的 Voronoi 单元 | O(√n × d) | 92-95% | 1K-100K 文档 |
| HNSW (小世界图) | 在概率图中导航 | O(log n × d) | 97-99% | 100K-10M 文档 |
| 量化索引 | 压缩向量 + ANN | O(log n × d/4) | 90-95% | 10M+ 文档 |
准确度与速度的权衡:
3.3 索引算法原理
3.3.1 线性扫描 (Flat Index)
工作原理:
将查询向量与数据集中的每个向量进行比较,返回距离最近的前 K 个结果。
对于数据库中的每个文档:
距离 = cosine_similarity(query, document.embedding)
如果距离 < 当前最佳距离:
加入结果集
返回前 K 个结果
特点:
| 属性 | 数值 |
|---|---|
| 构建时间 | 0s (无索引结构) |
| 搜索时间 | O(n × d),n=文档数,d=维度 |
| 内存开销 | 0% (仅存储向量本身) |
| 准确度 | 100% (精确搜索) |
| 最适合 | < 1K 文档 |
何时使用线性扫描效果最好:
- 小数据集:少于 1K 份文档,索引开销不值得
- 准确度至上的应用:法律、医疗、金融等无法容忍 2% 误差的领域
- 动态数据:频繁更新导致索引维护成本过高
- 批量处理:对速度不敏感的离线索引任务
性能示例:
- 1K 文档 × 1536 维度
- 搜索时间:~10ms
- 内存:仅向量占用 6 MB
- 无构建时间
3.3.2 IVF (倒排文件)
IVF 如何工作:
IVF 通过聚类(通常是 k-means)将向量空间划分为 Voronoi 单元 (Voronoi cells)。
Voronoi 单元图解:
簇 1 簇 2 簇 3
(质心 C1) (质心 C2) (质心 C3)
* * * * * * * * *
* * * * * * * * * * * *
* * * * * * * * *
查询 Q 距离 C2 最近 → 仅搜索簇 2
IVF 参数:
| 参数 | 描述 | 默认值 | 影响 |
|---|---|---|---|
| nlist | 簇的数量 | 1000 | 越高 = 精度越好,搜索越慢 |
| nprobe | 搜索的簇数 | 10 | 越高 = 召回率越好,搜索越慢 |
特点:
| 属性 | 数值 |
|---|---|
| 构建时间 | 数分钟 (需要聚类训练) |
| 搜索时间 | O(√n × d),nprobe=10 时 |
| 内存开销 | 中 (存储簇分配关系) |
| 准确度 | 92-95% 召回率 |
| 最适合 | 1K-100K 文档 |
何时使用 IVF 效果最好:
- 中型数据集:1K-100K 文档
- 内存受限:内存开销低于 HNSW
- 批量更新:可以定期重建索引
局限性:
- 需要训练:索引前需要运行 k-means
- 对动态数 据支持差:更新索引成本高
- 簇边界问题:簇边缘附近的文档可能会被遗漏
3.3.3 HNSW (分层导航小世界)
HNSW 如何工作:
HNSW 构建了一个概率跳表 (Probabilistic skip list)(分层图结构),以实现对数级时间的搜索。
图构建算法:
对于每个新加入的点:
1. 确定该点的层级(基于概率)
- 第 0 层:100% 的点
- 第 1 层:10% 的点
- 第 2 层:1% 的点
- 第 3 层及以上:0.1% 的点
2. 找到入口点(顶层的点)
3. 从顶层向下逐层处理:
a. 在该层贪婪搜索最近邻
b. 基于 ef_construction 参数选择 M 个邻居
c. 在点之间建立双向连接
4. 在基础层 (第 0 层):
a. 找到 M 个最近邻
b. 创建双向边
HNSW 参数:
| 参数 | 描述 | 默认值 | 范围 | 影响 |
|---|---|---|---|---|
| M | 每个节点的最大连接数 | 16 | 8-64 | 越高 = 召回率越好,内存占用越多 |
| ef_construction | 索引构建质量 | 100 | 50-200 | 越高 = 索引质量越好,构建越慢 |
| ef_search | 搜索候选数 | 50 | 10-100 | 越高 = 召回率越好,搜索越慢 |
特点:
| 属性 | 数值 |
|---|---|
| 构建时间 | 数分钟 (支持增量构建) |
| 搜索时间 | O(log n × d) |
| 内存开销 | 高 (图结构占用多) |
| 准确度 | 97-99% 召回率 |
| 最适合 | 100K-10M 文档 |
何时使用 HNSW 效果最好:
- 大型数据集:100K-10M 文档
- 实时搜索:延迟低于 50ms 的查询
- 动态数据:支持增量更新
- 高召回率要求:可接受 97-99% 的准确度
为什么 HNSW 这么快:
- 对数搜索:跳表结构实现了 O(log n) 的搜索复杂度
- 贪婪导航:始终朝着目标移动
- 分层方法:由粗到细的搜索减少了距离计算次数
- 概率采样:高层级仅保留极少比例的点
3.3.4 算法对比总结
综合对比表:
| 算法 | 构建时间 | 搜索时间 | 内存占用 | 准确度 | 最适合 | 局限性 |
|---|---|---|---|---|---|---|
| Flat | 0s | 10-30s | 低 | 100% | < 1K 文档,精度至上 | 无法扩展 |
| IVF | 分钟级 | 100-500ms | 中 | 92-95% | 1K-100K 文档 | 对动态数据支持差 |
| HNSW | 分钟级 | 10-50ms | 高 | 97-99% | 100K-10M 文档 | 内存开销大 |
| PQ-HNSW |