老张管理着一间快递仓库,货架上堆了一亿只包裹。客户递来一张照片说“帮我找同款”,老张必须在一亿件里翻出最像的那几件。一开始他逐件翻找——暴力搜索,准确但慢到让人崩溃。后来他悟出两件事:第一,把仓库按品类分成几十个区域,收到查询先奔最可能的那几个区(倒排文件IVF);第二,每个包裹不需要登记几十项复杂数据,只要拆成几个标准部件、各记一个编号就行(乘积量化PQ)。分区负责缩小寻找范围,编号负责压缩存储和加快比对。两者一配合,亿级检索从几秒降到几毫秒。这篇文章就用老张的仓库故事,带你拆解IVF和PQ这两把钥匙的每一道齿轮。
理解IVF+PQ,是读懂Faiss、Milvus等向量数据库索引配置的第一步。无论你从事搜索、推荐还是大模型RAG,召回阶段的效率与成本都从这里来——学会它,你就掌握了亿级向量检索的核心心法。
目录
可视化演示
展示PQ向量分解与码本量化、IVF倒排文件索引、IVF-PQ组合检索和召回率分析,含星座网络背景和发光动画,交互切换各环节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
索引演进:从翻遍仓库到分区查找
老张的仓库刚开张时只有几百个包裹,客户报个特征,他翻十分钟也能找到。可当包裹堆到上亿件时,翻完整个仓库要花几十秒,客户早就转身走了。老张第一次意识到:找得快,靠的不是腿脚勤快,而是聪明的组织方式。
向量检索面对同样的困境。数据库里有 N 条向量(对应包裹),查询时拿到一条向量(对应客户给的描述),要找出最相似的 top-K 条。最直接的办法是暴力搜索:逐条计算欧氏距离,复杂度随 N 线性膨胀。亿级数据、高维特征下,一次查询就是几十亿次浮点运算,任何在线服务都扛不住。于是近似最近邻搜索(ANN)出现了——允许一点精度损失,换取几个数量级的速度提升。哈希加速等方式也存在局限,精确哈希不够鲁棒、语义哈希训练复杂,很难兼顾速度与召回。
暴力搜索的代价可以精确量化。每条向量有 d 个维度,计算一次欧氏距离需要 d 次减法和 d 次平方累加:
老张明白了:一亿件包裹不能靠蛮力翻找。他暗下决心,先给仓库分区,再给包裹编号——这两步,正是后面故事的开始。
- 向量总量 N$N$ 是库中向量的总条数,相当于仓库里的包裹总数,包裹越多,逐件翻找越慢。
- 特征维度 d$d$ 是每条向量的维度,比如文本向量常是384维,图像向量可能是1024维,维度越高单次比较越贵。
- 线性复杂度 O$\mathcal{O}(N\times d)$ 表示总计算量随 N 与 d 线性增长,想提速只能靠减少比较次数,而不是优化单次计算。
倒排文件IVF:先分区,后查找
老张的第一步:把仓库按品类隔成几十个房间,服装区、数码区、日用品区。新包裹入库时,管理员判断品类直接放到对应房间。客户要“红白条纹T恤”,老张直奔服装区,原本要翻一亿件,现在只用翻几百万件。
倒排文件索引(IVF)就是这个思路。建库阶段,用粗量化器——通常是一个简单的KMeans——把全部向量聚成 nlist 个簇,每个簇有一个质心向量。向量入库时,计算它与各质心的距离,归入最近的簇,建立倒排列表:每个簇一张表,记录属于它的所有向量的编号。
查询阶段,拿到查询向量后先算出它与所有质心的距离,选出距离最近的 nprobe 个簇,只在这些簇内部逐条比较。于是比较范围从“全库 N 条”缩小到"N/nlist × nprobe"条:
但老张发现:分区只解决了“去哪找”,每个区里还有几十万件包裹,逐件量尺寸依然太慢。他还需要一个让包裹更“轻”的办法——压缩编号。
- nlist 分区总数$nlist$ 是建库时划分的簇数,相当于仓库隔出的房间数量,nlist 越大每个房间越空。
- nprobe 扫描簇数$nprobe$ 是查询时检查的最近簇个数,它直接决定速度与精度的权衡点。
- 候选规模$\frac{N}{nlist}\times nprobe$ 是实际被逐条比对的向量条数,nprobe 越小、nlist 越大,候选越少、速度越快。
乘积量化PQ:拆开再压缩
老张给每件包裹登记了几十项测量数据:长宽高、重量、材质、颜色编号……一张登记卡写得密密麻麻。他灵机一动:为什么不把这些测量项拆成几组,每组装进一个“标准箱型”?登记时只需写箱型编号。
乘积量化(PQ)正是把向量“拆组编号”。先把 d 维向量切成 m 段子向量,每段长度 d/m。对每一段子向量在它的子空间里做 KMeans 聚类,得到 k 个聚类中心,这一组中心称为该段的码本。编码时,每段子向量用最近的聚类中心编号表示,整条向量就浓缩成 m 个编号。
压缩率是 PQ 最直观的收益。原始向量每个维度用 32 位浮点存储,PQ 编码后整条向量只需 m×log₂(k) 比特:
登记卡从几大页变成一行箱号,货架空间省了几十倍。但老张马上追问:这些“标准箱型”到底怎么设计?答案在下一节。
- 切分数 m$m$ 是子向量段数,m 越大每段越短、码本越小,但每段描述越粗糙。
- 码本大小 k$\log_2 k$ 是每个编号需要的比特数,k=256 时每个编号只需 8 比特,对照原始 32 位浮点大幅缩水。
- 原始存储量$d\times 32$ 是原始 float 向量的总位数,32 是单精度浮点的比特数,作为压缩对比的分母。
码本训练:标准箱型从哪来
标准箱型不能拍脑袋定。老张翻出仓库里所有历史包裹的测量数据,把数据按测量组分摊开,用“物以类聚”的办法:测量值接近的包裹自然聚成一堆,每堆取一个“平均尺寸”作为标准箱。
技术上这是 KMeans 聚类。码本训练的目标是:给定一组子向量样本,找到 k 个聚类中心,使得所有样本到其最近中心的平方距离之和最小:
实际训练中,先随机选 k 个样本作为初始中心,再交替执行“分配样本 → 更新中心”直至收敛。样本量过大时先均匀采样几万条,既保证代表性又控制训练成本。码本一旦训练完成,标准箱就“定型”了。此后新包裹入库,每段测量值对照码本选最近的编号,压缩流程正式运转。
- 样本向量$x_i$ 是参与训练的某段子向量,通常从全量数据中均匀抽样而来,保证代表性。
- 聚类中心$c_j$ 是待学习的第 j 个标准箱中心,所有中心合起来就是该段的码本。
- 双层最小化$\min_{j}\|x_i-c_j\|^2$ 先算每个样本离哪个中心最近,外层再调所有中心位置让总误差最小。
编码检索:查表代替运算
包裹都变成箱号后,老张发现比较两件包裹变得出奇简单:不需要打开箱子,甚至不需要完整测量,只要对着型号表查差异就行。
查询阶段最常用的是 ADC——非对称距离计算。“非对称”指查询向量保持原始精度,库中向量用码本 ID 表示。对查询向量的每一段,先预计算它与该段所有聚类中心的距离平方,得到一张 k 长的表;再扫描库中向量,逐段查表累加:
另一种 SDC 对称距离会先把查询向量也量化为码本 ID 再比较,省去建表过程,但精度略低。工程中 ADC 更常见:多花一点点建表时间,换回明显更好的召回。老张的团队从此比较包裹全靠“查型号表”,又快又省力。不过箱号毕竟是近似,误差不可避免——怎么把误差的影响压到最低?组合方案给出答案。
- 查询向量分段$x^{(j)}$ 是查询向量第 j 段子向量,保持原始数值不量化,这是“非对称”的关键。
- 码本查表$c_j[y_j]$ 表示查第 j 段码本的第 $y_j$ 个中心向量,用它近似库中向量的第 j 段。
- 逐段累加$\sum_{j=1}^{m}$ 把 m 段的距离平方求和,得到近似距离,用于排序取 top-K。
IVF+PQ组合:双剑合璧
老张把两招合二为一:客户要红白条纹 T 恤,先直奔服装区(IVF 粗筛),到了服装区,不再逐件量尺寸,而是对照箱号快速锁定最接近的目标(PQ 精排)。
这就是现代向量数据库中最经典的索引结构——IVF+PQ。粗量化器负责缩小搜索范围,PQ 负责在候选集内做快速的近似距离计算。整体开销为:
但精度与效率永远在博弈。nprobe 太小时,真正的最近邻可能恰好落在没被扫描的簇里,造成漏检;m 或 k 太小时,量化过粗,近似距离失真。工程常用补偿手段有:适度增大 nprobe、用更大的 k 加密码本、对候选 top-K 做重排序(用原始向量精算一次准确距离)、或者把粗量化器从 KMeans 升级为 HNSW 多层图索引。老张的仓库终于高速运转。他总结出四个旋钮:nlist 管分区多少,nprobe 管搜索广度,m 管压缩粒度,k 管量化精度。理解这四个旋钮的相互牵制,就掌握了向量检索系统的调优心法。
- IVF 粗筛$\frac{N}{nlist}\times nprobe$ 是 IVF 阶段锁定的候选向量数,控制了搜索的广度。
- PQ 精排$\times m$ 表示每条候选向量只需做 m 次查表,而不是 d 维浮点运算,控制了比较的深度。
- 总代价$T_{total}$ 综合了分区与压缩的收益,实际工程中还需加上建距离表、质心计算等常数开销。
AI 应用场景
电商以图搜图
阶段:召回阶段
解决的问题:用户上传一张商品图片,系统需在千万商品库中秒级返回相似款。传统暴力计算无法满足商业延迟;单独用 IVF 有分区误差,单独用 PQ 存储虽小但检索范围仍大。老张的“分区 + 编号”组合让“先分区、再压缩比对”成为可能。
具体案例:某电商平台“拍立淘”功能,将商品主图用 CNN 提取为 128 维向量,写入 IVFPQ 索引(nlist=4096,nprobe=16),单次查询约 10ms,召回率保持在 90% 以上,支撑大促峰值流量。
推荐系统海量召回
阶段:召回阶段
解决的问题:推荐系统需从亿级物品中为每个用户召回数百候选再做精排。暴力检索的内存和 CPU 成本不可接受,IVF+PQ 为每件物品生成“分区+压缩编号”,召回延迟降到毫秒级。
具体案例:短视频 App 使用用户兴趣向量(256 维)在 5000 万视频向量库中检索,IVFPQ 索引将存储压缩至 1/32,候选召回从全量降为 3000 条再交给精排模型,日均调用上亿次。
RAG知识库检索
阶段:文档检索阶段
解决的问题:大模型私有知识库问答需先检索相关文档片段,知识库常达百万级文档向量,暴力检索无法满足实时对话要求。
具体案例:企业客服 RAG 系统存储 200 万条 FAQ 向量,采用 IVFPQ 索引并配置 nprobe=8、m=64,P@10 达 85%,平均检索延迟约 60ms,相比暴力检索提速 13 倍且存储减少 70%。
Python 演示
import numpy as np class PQCodec: """乘积量化编解码器:将高维向量压缩为码本ID序列""" def __init__(self, m=4, k=64, rg=None): self.m = m self.k = k self.rg = rg or np.random.RandomState(42) self.centroids_list = None self.sub_dim = None def fit(self, X): """在训练集上为每一段子向量训练码本""" n, d = X.shape assert d % self.m == 0, "向量维度必须能被m整除" self.sub_dim = d // self.m self.centroids_list = [] for j in range(self.m): Xj = X[:, j * self.sub_dim:(j + 1) * self.sub_dim] self.centroids_list.append(self._kmeans(Xj)) return self def _kmeans(self, X, max_iter=20): """KMeans聚类:返回k个聚类中心""" idx = self.rg.choice(len(X), self.k, replace=False) centers = X[idx].copy() for _ in range(max_iter): dist = ((X[:, None, :] - centers[None, :, :]) ** 2).sum(-1) labels = dist.argmin(1) for ci in range(self.k): mask = labels == ci if mask.any(): centers[ci] = X[mask].mean(0) return centers def encode(self, X): """将向量矩阵转成码本ID矩阵""" codes = np.zeros((len(X), self.m), dtype=np.int32) for j in range(self.m): Xj = X[:, j * self.sub_dim:(j + 1) * self.sub_dim] dist = ((Xj[:, None, :] - self.centroids_list[j][None, :, :]) ** 2).sum(-1) codes[:, j] = dist.argmin(1) return codes def adc_dist(self, query, codes): """非对称距离:查询向量不量化,库向量用码本近似""" dists = np.zeros(len(codes)) for j in range(self.m): qj = query[j * self.sub_dim:(j + 1) * self.sub_dim] table = ((self.centroids_list[j] - qj) ** 2).sum(-1) dists += table[codes[:, j]] return dists class IVFIndex: """倒排文件索引:先按粗聚类分区,再在部分分区内检索""" def __init__(self, nlist=16, nprobe=2, rg=None): self.nlist = nlist self.nprobe = nprobe self.rg = rg or np.random.RandomState(0) self.centroids = None self.last_hits = None def fit(self, X): """训练粗量化器并构建倒排列表""" idx = self.rg.choice(len(X), self.nlist, replace=False) self.centroids = X[idx].copy() dist = ((X[:, None, :] - self.centroids[None, :, :]) ** 2).sum(-1) labels = dist.argmin(1) self.last_hits = [np.where(labels == i)[0] for i in range(self.nlist)] return self def search(self, query, X, topk=10): """查询:只扫描最近的nprobe个分区""" cent_dist = ((query[None, :] - self.centroids) ** 2).sum(-1) probe = np.argsort(cent_dist)[:self.nprobe] cand = np.concatenate([self.last_hits[i] for i in probe]) true_dist = ((query[None, :] - X[cand]) ** 2).sum(-1) order = np.argsort(true_dist)[:topk] return cand[order] def recall_at_k(pred, true, k): """计算Top-k召回率""" return len(set(pred) & set(true)) / k if __name__ == "__main__": rg = np.random.RandomState(123) X = rg.randn(20000, 16).astype(np.float32) pq = PQCodec(m=4, k=64, rg=rg).fit(X) codes = pq.encode(X) raw_bytes = X.nbytes / len(X) pq_bytes = codes.nbytes / len(X) print(f"原始存储: {raw_bytes:.1f} 字节/条") print(f"PQ压缩后: {pq_bytes:.1f} 字节/条") print(f"压缩率: {pq_bytes / raw_bytes * 100:.1f}%") ivf = IVFIndex(nlist=32, nprobe=3, rg=rg).fit(X) query = X[0] true_top = np.argsort(((query[None, :] - X) ** 2).sum(-1))[:10] hits = ivf.search(query, X, topk=10) print(f"IVF检索 Top10 召回率: {recall_at_k(hits, true_top, 10):.2f}")
pq = PQCodec(m=4, k=64, rg=rg).fit(X)设置每段聚成 64 个中心,在训练集上逐段训练码本codes = pq.encode(X)把每条 16 维向量编码成 4 个码本 ID,存储从 64 字节降到 16 字节true_top = np.argsort(((query[None, :] - X) ** 2).sum(-1))[:10]用暴力法算出真正的 Top10 作为召回率参照ivf = IVFIndex(nlist=32, nprobe=3, rg=rg).fit(X)建库时把 2 万条向量分成 32 个簇,查询时只扫最近的 3 个簇hits = ivf.search(query, X, topk=10)在候选簇内精确计算距离,返回 Top10 向量下标recall_at_k(hits, true_top, 10)计算检索结果与暴力结果的交集比例,衡量 IVF 的近似精度
常见误区
误区我一直以为 PQ 就是把向量每个维度的数值四舍五入成整数,类似图片压缩里的量化,反正都是牺牲精度换体积。
正解标量量化确实是逐维度压缩数值精度,但 PQ 完全不同。PQ 是把向量切段、对每段子向量做聚类,用“中心编号”代替整段数值,是一个结构性压缩。形象地说,标量量化是给每张照片降低分辨率,PQ 是把照片里的人物、景物分别识别成图标,再用图标编号描述整张照片。
为何常见:两者名字都带“量化”两个字,且都牺牲精度换存储空间,初学者很容易把这两个概念混为一谈。
误区nlist 和 nprobe 同样是调节聚类数量的,调大调小效果类似,所以知道一个就够了。
正解nlist 是建库时划分的分区总数,nprobe 是查询时扫描的分区个数。前者决定“仓库隔成几间”,后者决定“找包裹时打开几间门”。调大 nlist 会增加建库和存储开销,调大 nprobe 会增加查询耗时——两者作用于不同阶段,绝不能混用。
为何常见:IVF 的调参界面里两个参数紧挨着,名称又相似,新手在配置向量数据库时极易张冠李戴。
误区既然 PQ 在工业界广泛使用,那么压缩后的向量距离应该和原始距离基本一致,召回率不会受影响。
正解PQ 是有损压缩,量化误差必然存在,召回率会受 m、k、nprobe 等参数影响。如果 m 太小、k 太小,压缩过度,近似距离失真,真正最近邻可能排不到前面。工程上需要靠调参、重排序等补偿手段拉回精度。
为何常见:教程常宣传 PQ“压缩几十倍且几乎不掉点”,容易让人忽略这背后有精心调参的前提。
误区只要用 IVF,最相似的向量一定落在被扫描的 nprobe 个分区里,所以结果就是全局最近邻。
正解IVF 是近似检索。如果查询向量恰好处在两个簇的边界附近,真正的最近邻可能落在第 nprobe+1 个簇里,此时 IVF 会漏检。增大 nprobe 或对粗量化器做更细致的设计可以降低漏检率,但无法完全消除。
为何常见:日常实验中数据分布规整、检索结果正好命中,会让人误以为 IVF 是无损的,直到遇到真实世界的长尾分布才发现问题。
面试题
Q1. 乘积量化PQ中,原始向量被切分为m个___
- A.子向量并分别量化← 正确答案
- B.完整向量整体聚类
- C.标量值逐个量化
- D.随机抽样部分向量
答案:A。PQ 的核心是把向量切段后分别量化,像把一件包裹拆成几个标准部件再分别编号。B 是整向量聚类的思路,C 是标量量化,D 随机抽样没有依据。
Q2. IVF中nlist的含义是___
- A.建库时划分的聚簇总数← 正确答案
- B.查询时扫描的聚簇数
- C.每个簇容纳向量的上限
- D.查询向量的维度数量
答案:A。nlist 是建库阶段把向量库分成的簇数,相当于仓库隔出的房间数。B 说的是 nprobe,C 是容量限制,D 是维度概念,都与 nlist 无关。
Q3. 增大nprobe最直接的影响是___
- A.召回率提高但查询变慢← 正确答案
- B.召回率下降查询变快
- C.存储空间得到显著压缩
- D.码本训练时间大幅缩短
答案:A。nprobe 是查询时扫描的簇数,增大它等于多打开几间房门,找到目标概率更高但耗时增加。B 方向反了,C 与存储无关,D 影响的是建库阶段而非查询阶段。
Q4. ADC中“非对称”的含义是___
- A.查询向量不量化库向量量化← 正确答案
- B.查询向量量化库向量不量化
- C.两侧向量都被量化处理
- D.两侧向量都不做量化
答案:A。ADC(非对称距离计算)中查询向量保持原始精度,库中向量用码本 ID 近似,两边“不对等”而得名。B 恰好反了,C 是 SDC 的做法,D 是暴力计算。
Q5. PQ编码的完整流程包括以下哪些步骤?
- A.切分向量为m段子向量← 正确答案
- B.每段独立KMeans聚类← 正确答案
- C.用中心编号表示子向量← 正确答案
- D.全维度共享同一个码本
答案:ABC。PQ 流程是切段、分段聚类、编号替换三步。D 错误——PQ 各段用独立码本,就像包裹拆成三部分要用三套不同的标准箱型号,而不是一套箱型装所有部件。
Q6. PQ的压缩率主要由哪两个参数决定?
- A.划分数m与码本大小k← 正确答案
- B.nlist与nprobe的取值
- C.聚类迭代次数设置
- D.训练样本总量的多少
答案:A。压缩率公式 R = m×log₂(k)/(d×32) 只与 m 和 k 有关。B 是 IVF 的调参项,C 和 D 影响训练成本但不影响压缩后每向量占用的比特数。
Q7. IVF+PQ组合相比暴力搜索的优势包括___
- A.IVF缩小候选搜索范围← 正确答案
- B.PQ压缩向量存储成本← 正确答案
- C.距离计算完全无信息损失
- D.比对退化为查表操作← 正确答案
答案:ABD。IVF 负责把范围从全库缩小到几个簇,PQ 负责压缩存储并让距离计算变成查表。C 错误,PQ 是有损压缩,量化误差必然存在,只是可通过参数与重排序补偿。
Q8. 下列哪项不是量化误差的补偿手段?
- A.增大nprobe扩大候选范围
- B.对TopK结果做重排序
- C.把粗量化器换成图索引
- D.减小k值压缩码本规模← 正确答案
答案:D。增大 nprobe、重排序、用 HNSW 图索引做粗量化都是常见的补偿手段。D 减小 k 会让码本更稀疏、量化更粗糙,误差反而增大,所以不是补偿手段。
Q9. PQ中码本训练最常用的算法是___
- A.KMeans聚类算法← 正确答案
- B.精确最近邻排序
- C.线性回归模型
- D.逻辑回归分类
答案:A。码本训练用 KMeans 对每段子向量聚类,迭代更新中心直到收敛。B 是检索中的排序操作,C 和 D 分别是回归与分类算法,与码本生成无关。
Q10. 下列哪些场景适合采用IVF+PQ?
- A.亿级向量近似召回← 正确答案
- B.在线低延迟检索← 正确答案
- C.需要精确逐条比对
- D.存储成本敏感环境← 正确答案
答案:ABD。IVF+PQ 适合大规模近似、低延迟、存储受限的场景,像老张的亿件仓库必须分区编号才能高效运转。C 要求精确逐条比对时应使用暴力搜索或保留原始向量的方案。