双塔(dual-encoder)已经把问题和文档变成了向量。现在库里有 1000 万条,每条 768 维:每次提问,都要和它们比一遍吗?

可以,这叫精确检索(retrieval)。问题是,算完需要多久、机器装不装得下。近似最近邻(ANN)检索允许少漏一点,以换取速度或存储空间。这里的“漏”,先指漏掉同一距离函数下的近邻,不是漏掉所有真正相关的答案。表示学错了,精确检索也救不了。

先读过双塔与训练目标会更容易理解。下面的数字都是教学算例,不是某个产品的性能测试。

1. 先把内存和比较标准算明白

只存 float32 向量,需要:

$$ 10^7\times768\times4=30{,}720{,}000{,}000\ \text{bytes}\approx28.61\ \text{GiB}. $$

这还没有算 ID、索引结构、元数据、复制副本和查询缓冲区。所谓“压缩 48 倍”,如果只算向量编码,就不能拿来当整个服务省了 48 倍内存。

比较索引前,先固定 embedding 版本、候选(candidate)库、过滤条件和距离函数。余弦相似度不是普通点积(inner product);对非零向量先做单位归一化,才有:

$$ \|\hat q-\hat x\|_2^2=2-2\hat q^\top\hat x. $$

这时最大点积和最小平方 L2 的排序一致。零向量要另外处理;如果训练目标本来就使用向量范数,不要为了套余弦检索悄悄改掉它。Faiss 距离说明

2. IVF:先选几个区域,再仔细找

IVF 先把库向量分到若干个簇,每个簇保存自己的向量列表。查询先找近的簇,再扫描选中的列表。nlist 是总簇数,nprobe 是一次查几个簇。Faiss 索引说明

假设有 100 万条、1000 个簇,且每簇刚好 1000 条。查 10 个簇,就扫描约 1 万条,而不是 100 万条。除此之外还要支付找簇、维护 top-k 等成本。真实数据的簇大小不均匀,所以 nprobe / nlist 只是粗略扫描比例,不能直接换算成加速倍数。

同一查询 q
  ├─ nprobe = 1 → 扫描最近的簇 A
  └─ nprobe = 2 → 扫描簇 A、B
                         ↑
               真正最近的向量可能在 B 的边缘

簇中心最近,不代表簇内一定有最近的点。多查几个簇能减少这类遗漏,但会更慢。IVF-Flat 仍存原始向量:它主要少算一些距离,并没有顺便把向量压小。

动画 IVF:最近的簇里不一定有最近的点

这张图需要启用 JavaScript。

3. PQ:存编号,但距离不是编号的差

乘积量化(PQ)把一个向量切成若干段,每段使用自己的码本,用最近的码字代表它。比如把 768 维分成 64 段,每段用 256 个码字,则每段编号占 8 bit,向量编码共 64 bytes。原始 float32 是 3072 bytes,仅编码部分缩小 48 倍。码本由训练样本拟合,不是随便切几段就完成了量化。

查询时常用 ADC:查询保留原值,库向量用码字重建值估计距离。对平方 L2,可以把各段距离相加;SDC 则连查询也量化。它们的区别是距离估计中哪一侧被量化,不是“一个查表、一个不查表”。PQ 原论文 §II–III

把维度缩到 4,亲手算一次。查询是 [1, 2 | 3, 4],两个子空间的码本分别有 3 个码字。以下代码只演示 ADC,不负责训练码本或选择最近的编码;示意编号不按 bit 打包。

def adc_squared_distance(query_parts, codebooks, codes):
    if not (len(query_parts) == len(codebooks) == len(codes)):
        raise ValueError("one query part and code per codebook required")
    total = 0.0
    for query_part, codebook, code in zip(query_parts, codebooks, codes):
        if not isinstance(code, int) or not 0 <= code < len(codebook):
            raise ValueError("invalid code")
        center = codebook[code]
        if len(query_part) != len(center):
            raise ValueError("dimension mismatch")
        total += sum((value - centroid) ** 2
                     for value, centroid in zip(query_part, center))
    return total


query_parts = [[1.0, 2.0], [3.0, 4.0]]
codebooks = [
    [[0.0, 0.0], [1.0, 1.0], [4.0, 4.0]],
    [[0.0, 0.0], [3.0, 3.0], [5.0, 5.0]],
]
assert adc_squared_distance(query_parts, codebooks, [1, 1]) == 2.0
assert adc_squared_distance(query_parts, codebooks, [2, 1]) == 14.0

编号 [1, 1] 表示重建向量 [1, 1, 3, 3],距离就是 0² + 1² + 0² + 1² = 2。实际扫描会先为查询建立“子空间 × 码字”的距离表,再按编码查表求和。

动画 PQ:查表再相加,得到近似距离

这张图需要启用 JavaScript。

编号本身没有几何意义。 把某个码本的编号重新排列,同时更新库里的编码,重建向量和 ADC 距离都不变;直接比较编号的汉明距离却可能变。需要汉明预过滤时,可以研究专门学习编码排列的 Polysemous Codes,不能把普通 PQ 编号直接当二进制哈希。

4. IVF-PQ:两种近似,两个误差来源

IVF 负责少找几个区域,PQ 负责少存一些数。常见的 IVFADC 还会编码向量相对粗簇中心的残差,不只是机械地把两个算法串起来。PQ 原论文 §IV

出错位置 一个具体例子 怎么分开检查
没有扫描到 好文档在没选中的簇里 固定编码,增加 nprobe
距离估错了 两个候选的近似距离顺序反了 固定扫描范围,对比原始向量的分数
最后仍不相关 精确近邻也是主题相似、答非所问 看相关性标注与 embedding,不只调索引

先取一个较大的近似候选集,再用原始向量重算距离,能修正集合内部的排序,不能找回从没入选的向量。而且原始向量必须存放在某处,读取它们也有成本。这里的精确距离重排,不等于联合阅读 query 和文档的 cross-encoder。

5. Flat、HNSW、IVF-PQ 怎么选?

HNSW用分层邻接图寻找近邻:先在稀疏的上层移动,再在下层扩大搜索。它没有要求把所有向量压成 PQ 码;图结构本身也占内存。

它有三个要调的参数:M 控制图的连接度,越大图越密、内存越多。pgvector v0.8.7 的实现里,上层每个节点最多 M 个邻居,底层则是 2M(m=16 时为 32);efConstruction 是建图时保留的候选数,越大建得越慢、图质量越好;查询时的 ef(很多库叫 efSearch)是搜索时保留的候选数,越大 recall 越高、延迟也越高。以 pgvector 为例,三者对应 m、ef_construction 和 hnsw.ef_search,默认值分别是 16、64 和 40(pgvector 文档,v0.8.7,2026-10-16 核对)。

方案 先试它的理由 不能省略的检查
Flat 数据量不大、批量计算合适,或者需要精确基线(baseline) 全扫描延迟、吞吐与内存;精确不代表实现一定最慢
HNSW 想在内存中做低延迟搜索 图的额外内存、构建成本、搜索宽度;删除行为依实现而定
IVF-Flat 想单独观察“少扫一些区域”的影响 簇是否偏斜、nprobe 与尾延迟
IVF-PQ 向量存储成为主要限制 码长、量化误差、训练样本代表性,以及原始向量重排成本

这是选实验起点,不是规模到某个阈值就必须换算法。硬件、并发、过滤比例和更新频率都能改变结果。Flat 的批量扫描本质上是一次大矩阵乘法,可能把硬件算力用得很满,所以精确不一定最慢;怎么判断一个操作受算力还是带宽限制,见性能:roofline。

6. 别把 ANN recall 当作业务 recall

设精确 top-10 是 A,近似 top-10 是 B。|A ∩ B| / 10 衡量索引保住了多少精确近邻;相关性 Recall@10 则以标注相关文档为参照。前者很高,只能说明“近似得不错”,不能证明“表示得好”。

建议先固定查询集与库快照,再扫索引参数,同时记录:

  • ANN recall、真实相关性指标;
  • p50 / p95 / p99 延迟、吞吐、峰值内存和构建时间;
  • 严格过滤后的返回数量、稀有主题和短查询的表现;
  • embedding 更新、文档新增或删除后,索引是否仍与元数据版本一致。

权限过滤还会改变可搜索集合。不能先找到未授权原文发给模型,再靠 prompt 要求它不要透露。下一篇看RAG 怎样保住证据、又怎样检查答案。

本页术语 6
双塔 dual-encoder
用户和内容各一个编码器,映到同一向量空间靠内积召回
检索 retrieval
从外部语料里找出相关内容
近似最近邻 ANN
在向量索引里近似地取 top-K,first-stage 检索的基础设施
候选 candidate
召回阶段捞出来、等待精排挑选的内容
点积 inner product
两个向量对应分量相乘再求和,双塔的打分方式
基线 baseline
用来比较新方法的参考方法或配置