IVF_PQ (倒排文件 + 乘积量化) 详解
IVF_PQ (Inverted File Index with Product Quantization) 是一种结合了倒排文件索引 (IVF) 和乘积量化 (PQ) 的高级近似最近邻 (ANN) 算法。它旨在同时解决大规模高维向量数据的存储效率和查询速度问题,是目前许多主流向量数据库(如 Faiss、Milvus)中常用且高效的索引类型之一。IVF_PQ 通过“粗粒度 + 细粒度”的两阶段搜索策略,在保证较高召回率的同时,显著降低了内存占用和查询延迟。
核心思想:首先利用 IVF 的分区策略进行粗粒度筛选,将搜索范围缩小到少数几个簇;然后在这些选定的簇中,利用 PQ 对残差向量进行高效压缩和加速距离计算,实现高效的细粒度搜索。
一、为什么需要 IVF_PQ?结合两者的优势
我们已经了解了 IVF 和 PQ 各自的优势和局限:
IVF (倒排文件索引)
- 优势:通过将数据划分为多个簇(Voronoi 单元)和构建倒排列表,实现了对搜索空间的粗粒度筛选,大大加速了查询速度。
- 局限:如果倒排列表中存储的是原始浮点向量,随着向量维度和数据集规模的增大,内存占用仍然会非常高,且在倒排列表内部的精确距离计算依然是 $O((N/nlist) \cdot D)$,可能仍是瓶颈。
PQ (乘积量化)
- 优势:通过向量分解和子向量独立量化,实现了对高维向量的极高压缩率,显著减少内存占用并加速距离计算(通过查表)。
- 局限:直接使用 PQ 进行全局搜索时,由于其有损压缩特性,召回率可能不够理想,尤其是在整个空间进行近似距离计算时。
IVF_PQ 的出现正是为了结合两者的优势,弥补各自的不足:
- IVF 负责将整个数据集划分,将相似的向量聚集在一起,减少了 PQ 需要处理的“噪声”和歧义,为 PQ 提供了一个更“干净”、更有局部特性的子集。
- PQ 则负责在 IVF 选定的局部区域内,对向量进行高效压缩和距离计算,从而进一步降低内存和计算开销。
通过这种组合,IVF_PQ 能够在保证相对较高精度的前提下,实现对大规模高维向量数据的超高压缩率和极快查询速度。
二、IVF_PQ 的核心原理
IVF_PQ 的工作原理可以分解为索引构建和查询两个阶段,每个阶段都融合了 IVF 和 PQ 的思想。
2.1 索引构建阶段
全局聚类 (Coarse Quantization):
- 首先,对整个数据集(或其代表性子集)运行 K-Means 算法,得到
nlist个簇中心 (centroids)。这些簇中心将整个向量空间划分为nlist个 Voronoi 单元。这一步与纯 IVF 索引的构建相同。 - Faiss 中,这个阶段被称为 Quantizer 的训练。
- 首先,对整个数据集(或其代表性子集)运行 K-Means 算法,得到
残差向量计算 (Residual Vector Computation):
- 对于数据库中的每个原始向量 $\mathbf{x}$,首先找到其最近的簇中心 $\mathbf{c}_j$。
- 然后,计算该向量相对于其簇中心的残差向量 (residual vector):$\mathbf{r} = \mathbf{x} - \mathbf{c}_j$。
- 残差向量的目的是捕获向量在所属簇内的局部信息,因为簇中心已经捕获了大部分全局信息。
残差向量的乘积量化 (Product Quantization of Residuals):
- 对于所有残差向量组成的集合,进行全局的乘积量化训练。这意味着对所有残差向量,将其分解为 $M$ 个子向量,并为每个子向量空间独立地训练一个子码本。
- 训练完成后,每个子码本包含 $K$ 个码字。
- 重要:这里的 PQ 训练是在残差向量空间上进行的,而不是原始向量空间。
倒排列表构建与编码 (Inverted List Construction and Encoding):
- 构建
nlist个倒排列表。 - 对于每个原始向量 $\mathbf{x}$,在找到其最近的簇中心 $\mathbf{c}_j$ 后,计算其残差向量 $\mathbf{r} = \mathbf{x} - \mathbf{c}_j$。
- 将残差向量 $\mathbf{r}$ 使用训练好的 PQ 编码器进行编码,得到一个由 $M$ 个码字索引组成的短序列:$\text{code}(\mathbf{r}) = [idx_1, idx_2, \ldots, idx_M]$。
- 将这个编码后的短序列 $\text{code}(\mathbf{r})$(通常是 $M$ 字节)存储到簇中心 $\mathbf{c}_j$ 对应的倒排列表中。
- 构建
2.2 查询阶段
IVF_PQ 的查询过程是一个两阶段的近似搜索:
粗粒度搜索 (Coarse Search / Quantizer Search):
- 给定一个查询向量 $Q$,首先计算 $Q$ 到所有
nlist个簇中心的距离。 - 选择距离 $Q$ 最近的
nprobe个簇中心,这些中心对应的倒排列表将构成细粒度搜索的候选空间。
- 给定一个查询向量 $Q$,首先计算 $Q$ 到所有
细粒度搜索 (Fine Search / Product Quantization Search):
- 对于每个被选中的
nprobe个簇中心 $\mathbf{c}_j$:- 计算查询向量 $Q$ 相对于该簇中心的残差查询向量:$Q_r = Q - \mathbf{c}_j$。
- 使用非对称距离计算 (ADC) 策略:预计算 $Q_r$ 的 $M$ 个子向量到 PQ 各个子码本中所有码字的距离,生成一个距离查找表 (DLT)。
- 遍历该簇中心 $\mathbf{c}_j$ 对应的倒排列表。对于列表中的每个编码残差向量 $\text{code}(\mathbf{r}) = [idx_1, idx_2, \ldots, idx_M]$:
- 通过查表,快速计算 $Q_r$ 与 $\text{code}(\mathbf{r})$ 对应的近似距离:$D(Q, \mathbf{x}) \approx \sum_{m=1}^{M} D(C_m[idx_m], Q_{r,m})$。
- 将所有从
nprobe个倒排列表中获得的近似距离和对应的向量 ID 进行聚合。
- 对于每个被选中的
结果聚合与排序 (Result Aggregation and Sorting):
- 从所有候选结果中,选择距离查询向量 $Q$ 最近的 Top-K 个向量作为最终结果。
IVF_PQ 索引概念图
flowchart TB
%% ========================================================
%% 阶段一:IVF-PQ 索引构建
%% ========================================================
subgraph BuildStage ["📦 阶段一:IVF-PQ 离线索引构建 (Build Phase)"]
direction TB
subgraph CoarseCluster ["1. 粗量化聚类 (Coarse Clustering)"]
direction LR
RawVecs[("原始向量集 X<br/>(N 个 D 维 float32)")]:::rawNode
KMeans[["K-Means 聚类训练"]]:::algoNode
Centroids[("粗聚类中心集合<br/>[C₁, C₂, ..., C_nlist]")]:::centroidNode
RawVecs -->|抽样| KMeans --> Centroids
end
subgraph ResidualQuant ["2. 残差计算与 PQ 训练 (Residual PQ)"]
direction LR
CalcResidual[["分配至最近中心并计算残差<br/>r = x - C_j"]]:::algoNode
TrainPQ[["训练 PQ 编码器<br/>(空间正交切分 M 个子空间)"]]:::algoNode
Codebooks[("M 个子空间码本<br/>(每码本 256 码字)")]:::codebookNode
CalcResidual -->|残差集| TrainPQ --> Codebooks
end
subgraph InvertedLists ["3. 倒排列表存储 (Inverted Lists Storage)"]
direction LR
IL1["倒排列表 1 (簇 C₁)"]:::invNode
IL2["倒排列表 2 (簇 C₂)"]:::invNode
IL_dots["..."]:::ghostNode
ILn["倒排列表 nlist (簇 C_nlist)"]:::invNode
IL1 -.-> R1["[ID + M 字节残差码字]"]:::byteNode
IL2 -.-> R2["[ID + M 字节残差码字]"]:::byteNode
ILn -.-> Rn["[ID + M 字节残差码字]"]:::byteNode
end
RawVecs ==> CalcResidual
Centroids -.->|"粗中心基准"| CalcResidual
CalcResidual ==>|"PQ 压缩残差"| InvertedLists
Codebooks -.->|"量化查表编码"| InvertedLists
end
%% ========================================================
%% 阶段二:IVF-PQ 在线查询
%% ========================================================
subgraph QueryStage ["⚡ 阶段二:IVF-PQ 在线 ADC 检索 (Query Phase)"]
direction TB
subgraph CoarseSearch ["1. 粗粒度过滤 (Coarse Filter)"]
direction LR
QueryVec[/"🔍 查询向量 Q (D 维)"/]:::queryNode
CoarseDist[["计算 Q 与所有粗中心距离"]]:::algoNode
TopCentroids(["选出最近的 nprobe 个簇中心<br/>[C_p1, C_p2, ..., C_pnprobe]"]):::highlightCentroid
QueryVec --> CoarseDist --> TopCentroids
end
subgraph BuildDLT ["2. 预计算距离查找表 (Precompute DLT)"]
direction LR
CalcQueryRes[["计算查询残差<br/>Q_r = Q - C_pi"]]:::algoNode
DLT[("距离查找表 (DLT)<br/>M × 256 查表矩阵")]:::dltNode
TopCentroids --> CalcQueryRes --> DLT
end
subgraph FineScan ["3. 倒排表查表遍历 (ADC Fine Search)"]
direction LR
Probe1["遍历倒排列表 C_p1<br/>(M 次查表快速求和)"]:::probeNode
Probe2["遍历倒排列表 C_p2<br/>(M 次查表快速求和)"]:::probeNode
Probe_dots["..."]:::ghostNode
Proben["遍历倒排列表 C_pn<br/>(M 次查表快速求和)"]:::probeNode
Probe1 --> Cand1["候选集 1"]:::candNode
Probe2 --> Cand2["候选集 2"]:::candNode
Proben --> Candn["候选集 n"]:::candNode
end
TopCentroids ==>|"多路并行探测"| FineScan
DLT -.->|"查表加速 O(M)"| FineScan
Cand1 & Cand2 & Candn ==>|"Top-K 堆排序"| FinalResult[/"🎯 最终 Top-K 最近邻"/]:::resultNode
end
%% 阶段间共享关系
Centroids ==>|"共享粗中心"| CoarseDist
Codebooks ==>|"共享子码本"| DLT
%% ========================================================
%% 样式与暗色主题配色规范 (Dark Mode Palette)
%% ========================================================
classDef rawNode fill:#1e1b4b,stroke:#818cf8,stroke-width:1.5px,color:#e0e7ff;
classDef queryNode fill:#172554,stroke:#60a5fa,stroke-width:2px,color:#dbeafe;
classDef resultNode fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#ecfdf5;
classDef algoNode fill:#1e293b,stroke:#64748b,stroke-width:1.5px,color:#f8fafc;
classDef centroidNode fill:#431407,stroke:#fb923c,stroke-width:1.5px,color:#ffedd5;
classDef highlightCentroid fill:#7c2d12,stroke:#f97316,stroke-width:2px,color:#fff7ed;
classDef codebookNode fill:#3b0764,stroke:#c084fc,stroke-width:1.5px,color:#faf5ff;
classDef dltNode fill:#4a044e,stroke:#e879f9,stroke-width:2px,color:#fae8ff;
classDef invNode fill:#1e293b,stroke:#38bdf8,stroke-width:1.5px,color:#e0f2fe;
classDef probeNode fill:#1e293b,stroke:#a855f7,stroke-width:1.5px,color:#f3e8ff;
classDef byteNode fill:#0f172a,stroke:#334155,stroke-width:1px,color:#94a3b8;
classDef candNode fill:#111827,stroke:#6b7280,stroke-width:1px,color:#9ca3af;
classDef ghostNode fill:transparent,stroke:none,color:#64748b;
style BuildStage fill:#0d1117,stroke:#30363d,stroke-width:1.5px,color:#94a3b8;
style QueryStage fill:#0d1117,stroke:#30363d,stroke-width:1.5px,color:#94a3b8;
style CoarseCluster fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
style ResidualQuant fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
style InvertedLists fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
style CoarseSearch fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
style BuildDLT fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
style FineScan fill:#131822,stroke:#1e293b,stroke-width:1px,stroke-dasharray: 2 2,color:#64748b;
说明:IVF_PQ 构建时,首先通过 K-Means 划分簇,然后对所有向量的残差进行 PQ 训练。每个向量存储时,不再是原始向量或残差向量,而是其残差向量的 PQ 编码。查询时,先通过查询向量与簇中心的距离确定搜索范围 (nprobe 个簇),然后在这些簇内,通过预计算的距离查找表 (DLT) 快速计算查询残差与存储的 PQ 编码残差之间的近似距离。
三、IVF_PQ 的参数调优
IVF_PQ 的性能和精度受多个参数影响,主要包括:
nlist(Number of Centroids / Clusters):- 与纯 IVF 相同,决定了粗粒度分区的数量。
- 影响:更大的
nlist意味着更细的分区,每个倒排列表更短,通常有助于 PQ 在局部区域更好地工作,可能提高精度,但索引构建更慢,且查找nprobe个簇中心时的开销可能更大。 - 选择建议:通常
nlist介于 $\sqrt{N}$ 到 $N/39$ 之间,具体取决于数据集大小和分布。
M(Number of subvectors for PQ):- 与纯 PQ 相同,决定了残差向量被分解成子向量的数量。
- 影响:
- 更大的
M:压缩率更高,每个编码残差向量占用字节数更少,查询时的查表次数增多。子向量维度 $D/M$ 降低,可能影响 PQ 训练精度。 - 更小的
M:压缩率更低,每个编码残差向量占用字节数更多,子向量维度 $D/M$ 升高,PQ 训练精度可能更高。
- 更大的
- 选择建议:通常 $M$ 的值在 4 到 64 之间,使得 $D/M \ge 16$ 左右。常见的 $M=8$ 或 $M=16$。
nbits(Number of bits per subvector for PQ):- 与纯 PQ 相同,决定了每个子码本中的码字数量 $K = 2^{nbits}$。
- 影响:几乎总是取
nbits=8(即 $K=256$),因为 1 字节索引是最高效的存储方式,且能提供合理的精度。 - 选择建议:通常固定为 8。
nprobe(Number of Probed Inverted Lists):- 与纯 IVF 相同,决定了查询时要检查的倒排列表的数量。
- 影响:
- 更大的
nprobe:召回率(精度)更高,但查询时间更长。 - 更小的
nprobe:查询速度更快,但召回率可能下降。
- 更大的
- 选择建议:在运行时动态调整以平衡精度和速度的关键参数。
四、IVF_PQ 的优势与局限性
4.1 优势
- 高压缩率:通过 PQ 对残差向量进行编码,极大减少了每个向量的存储空间,从而允许在内存中加载数十亿级的向量数据。
- 超快查询速度:结合了 IVF 的粗粒度筛选和 PQ 的细粒度查表距离计算,查询时间通常是近似对数级。
- 良好的精度-速度-内存平衡:通过细致的参数调优(特别是
nlist和nprobe),可以在极低的内存占用和极高的查询速度下,实现可接受甚至优秀的召回率。 - 适用于超大规模数据集:是处理 TB 级甚至 PB 级高维向量数据的首选索引类型之一。
4.2 局限性
- 索引构建复杂且耗时:需要先进行 K-Means 聚类训练 (IVF 部分),再对所有残差向量进行 PQ 训练。这两个训练阶段都可能非常耗时,尤其是在数据集非常大时。
- 有损压缩,存在精度损失:残差向量的 PQ 编码是有损的,必然会引入量化误差,从而导致查询结果并非 100% 精确。
- 参数调优复杂:
nlist,nprobe,M,nbits等参数的组合和选择对最终性能和精度影响巨大,需要经验和实验来找到最优配置。 - 动态更新成本高:与纯 IVF 类似,增加或删除向量可能需要重新计算残差并重新编码,甚至在大量更新后需要重新训练聚类中心,影响了索引的动态性。
五、Python 示例:使用 Faiss 实现 IVF_PQ 索引
1 | import faiss |
示例解读:
IndexIVFPQ(quantizer, D, nlist, M, nbits, metric):创建 IVF_PQ 索引。这里的参数融合了 IVF 和 PQ 的特性。- 训练 (
train):IVF_PQ 的训练过程比纯 IVF 或纯 PQ 更复杂,它需要同时完成粗粒度聚类(quantizer部分)和残差向量的乘积量化训练(PQ 部分)。 - 添加 (
add):在添加向量时,每个向量首先被归属到最近的簇,然后计算残差,残差再被 PQ 编码成M字节的短序列,存储在对应的倒排列表中。 - 内存占用:可以看到,IVF_PQ 的内存占用比原始向量显著减少,是其主要优势之一。
nprobe调优:与纯 IVF 类似,nprobe是在查询时动态调整精度和速度的关键参数。通过增加nprobe,可以提高召回率,但查询时间也会增加。
六、总结
IVF_PQ 是目前最强大、最通用的近似最近邻算法之一,广泛应用于需要处理大规模、高维向量数据的场景。它通过巧妙地结合 IVF 的分区策略和 PQ 的向量压缩及距离计算加速,实现了存储效率、查询速度和召回率之间的卓越平衡。理解 IVF_PQ 的工作原理、参数调优及其优势与局限性,是构建高性能向量搜索系统和处理海量非结构化数据挑战的关键。
