从内存密集型 HNSW 到量化 SPANN:Pinterest Manas 搜索平台的演进

2026-09-16 13 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

当搜索和推荐系统从百万级向海量向量数据扩展时,真正先撞上的瓶颈往往不是算法复杂度,而是内存、存储和成本。Pinterest 对 Manas 搜索平台的持续改造,展示了一条很有代表性的路径:从内存占用较高的 HNSW,逐步引入 Scalar Quantization、Product Quantization 和基于 SSD 的 SPANN 架构,在降低资源消耗的同时尽量保持召回率,并进一步为多向量模型准备基础设施。

这类演进的核心并不是简单地“换一个索引”,而是重新分配不同硬件的职责:内存保存更紧凑的路由信息,SSD 承载规模更大的向量数据,量化负责压缩表示,精排阶段再用更准确的向量恢复相关性。

为什么单纯扩大 HNSW 内存并不理想

HNSW 的优势很明确:查询路径短、延迟低、工程实现成熟,适合把大量向量放在内存中进行近似最近邻搜索。但随着向量数量、维度和副本数量增长,成本会变得明显:

  • 图结构本身需要额外内存;
  • 每个节点通常还要保存完整的浮点向量;
  • 多分片、多副本和在线更新会放大内存开销;
  • 当数据规模超过内存容量后,依赖随机磁盘访问会带来严重的尾延迟。

因此,扩大机器内存只能延后问题,而不能改变成本曲线。对于搜索和发现系统,还需要同时考虑召回率、P99 延迟、索引构建时间、SSD 读放大以及更新策略。

量化:把内存中的每一位都用在刀刃上

量化的基本思路是用更少的比特表示原本的浮点向量。

Scalar Quantization

Scalar Quantization(标量量化)通常独立处理每个维度,把浮点数映射为较低精度的整数,例如 INT8。它实现简单、压缩效果直接,也比较容易利用 SIMD 指令加速。代价是每个维度的精度都会下降,量化范围和校准数据的选择会直接影响召回率。

下面是一个可运行的简化示例。它使用固定范围把向量压缩为 8 位无符号整数,再解码并进行近似打分。这个例子不是生产级 ANN 索引,但可以帮助理解量化误差从哪里产生。

运行方式:将代码保存为 scalar_quantization_demo.py,执行 python scalar_quantization_demo.py。生产环境中应使用真实数据分布校准范围,并配合候选集精排。

from math import sqrt


def normalize(vector):
    norm = sqrt(sum(value * value for value in vector))
    return [value / norm for value in vector] if norm else vector[:]


def quantize_int8(vector, min_value=-1.0, max_value=1.0):
    """将每个浮点维度映射到 0~255。"""
    scale = 255.0 / (max_value - min_value)
    return [
        max(0, min(255, round((value - min_value) * scale)))
        for value in vector
    ]


def dequantize_int8(vector, min_value=-1.0, max_value=1.0):
    scale = (max_value - min_value) / 255.0
    return [value * scale + min_value for value in vector]


def dot(left, right):
    return sum(a * b for a, b in zip(left, right))


# 假设这些向量已经经过模型生成并归一化。
documents = {
    "pin_1": normalize([0.82, 0.12, 0.35, 0.08]),
    "pin_2": normalize([0.10, 0.91, 0.05, 0.25]),
    "pin_3": normalize([0.71, 0.20, 0.38, 0.11]),
}
query = normalize([0.80, 0.15, 0.30, 0.10])

compressed = {
    doc_id: quantize_int8(vector)
    for doc_id, vector in documents.items()
}

approximate_scores = []
for doc_id, encoded in compressed.items():
    decoded = dequantize_int8(encoded)
    approximate_scores.append((dot(query, decoded), doc_id))

for score, doc_id in sorted(approximate_scores, reverse=True):
    print(f"{doc_id}: approximate_score={score:.6f}")

在真实系统中,通常不会只依赖解码后的量化向量完成全部排序。更常见的做法是先用压缩表示快速找出较大的候选集,再使用原始向量或更高精度表示重新计算分数。这种“近似召回 + 精确重排”可以在成本和相关性之间取得更稳定的平衡。

Product Quantization

Product Quantization(乘积量化)会把一个高维向量拆成多个子空间,分别对每个子空间学习码本,再用较短的编码表示完整向量。相比独立的标量量化,PQ 能更充分地利用维度之间的局部结构,在极高压缩比下仍有机会保留更好的距离信息。

但 PQ 的工程复杂度也更高:需要训练码本,在线查询要构造查找表,并且要根据数据分布定期评估码本是否失效。数据分布变化、模型升级和不同业务场景混合,都可能让原来的量化参数不再合适。

从全内存图索引到 SSD 友好的 SPANN

量化解决了“一个向量占多少内存”的问题,但海量数据仍然可能无法完全放入内存。SPANN 类架构的关键方向,是把内存和 SSD 组合成分层存储:

  1. 内存保存聚类中心、路由结构或其他较小的导航信息;
  2. 查询先在内存中定位可能相关的分区;
  3. 再从 SSD 读取这些分区中的压缩向量或候选数据;
  4. 最后进行距离计算和精排。

这使系统可以利用 SSD 的容量,同时尽量减少随机读取次数。设计时需要重点关注几个问题:

  • 分区大小:分区过小会增加元数据和读取次数,过大则会带来更多无关候选;
  • 探测数量:探测更多分区通常能提升召回,但会增加 SSD I/O 和 CPU 计算;
  • 数据布局:相关向量应尽量连续存储,降低读放大;
  • 缓存策略:高频分区可以放入内存或本地缓存;
  • 尾延迟:平均延迟不够,必须单独监控 P95、P99 和 SSD 队列深度。

换句话说,SPANN 的性能不只由 ANN 算法决定,还取决于索引布局、批量读取、缓存命中率和硬件调度。把一个内存算法直接搬到 SSD 上,通常不会自动得到理想结果。

多向量模型带来的下一轮变化

单向量表示便于索引,但它可能把复杂对象压缩得过于激进。例如,一张内容丰富的图片、一篇长文本或一个包含多个兴趣主题的用户画像,使用一个向量表达时,局部语义容易被平均掉。

多向量模型会为同一对象生成多个向量,让查询可以与对象的不同局部表示分别匹配。这有机会改善细粒度相关性,但也会放大基础设施压力:

  • 同一个对象对应更多索引条目;
  • 候选合并和去重逻辑更复杂;
  • 内存、SSD 容量和 I/O 数量随向量数增长;
  • 需要定义对象级别的最终打分,而不是只排序单个向量。

因此,从单向量到多向量不能只修改模型服务。索引分片、量化参数、缓存、过滤、候选合并和精排都需要一起设计。量化与 SSD 分层在这里变得更重要,因为多向量会进一步放大存储规模。

落地时如何做取舍

可以把一次迁移拆成几个可验证的阶段,而不是直接替换线上索引:

  1. 建立基线:记录当前 HNSW 的 Recall@K、QPS、P50/P95/P99 延迟、内存和索引构建时间;
  2. 单独评估量化误差:在相同候选集上比较 FP32、Scalar Quantization 和 PQ 的召回差异;
  3. 引入候选精排:为量化索引保留一个更高精度的重排路径;
  4. 模拟 SSD 行为:测量不同分区大小、探测数量和缓存策略下的尾延迟;
  5. 灰度多向量:先在离线评估和小流量场景验证对象级合并逻辑;
  6. 设置回滚条件:当召回、P99 延迟或 SSD 错误率超过阈值时,自动回到旧索引。

一个实用的评估表至少应包含以下指标:

维度 需要观察的指标
相关性 Recall@K、NDCG、人工评估或业务转化指标
延迟 P50、P95、P99、最慢分片
资源 内存、SSD 容量、读带宽、CPU 利用率
运维 构建耗时、更新延迟、失败重试、回滚时间
成本 每百万向量的存储与计算成本

结语:让索引适配数据规模,而不是反过来

Manas 的技术演进反映了大规模向量搜索的一个普遍规律:当数据量持续增长时,单一索引和单一存储介质很难同时满足成本、延迟与召回率要求。量化降低表示成本,SPANN 利用 SSD 扩展容量,候选精排修正压缩误差,多向量模型则提升匹配的细粒度。

对工程团队而言,最稳妥的路线不是追求某个算法名词,而是围绕业务指标做分层设计:先测量,再压缩;先验证召回,再优化 I/O;最后才把多向量模型引入线上。只要每一步都有可比较的基线和明确的回滚条件,索引架构就能随着数据规模演进,而不必被内存容量牵着走。


相关推荐