面向Google编程CHARLES ZHANG

AI DAILY / 2026-09-09

HFresh:Weaviate推出的内存友好型磁盘向量索引

HFresh: Memory-Efficient Vector Search

上下文与记忆Weaviate · 2026-09-09

全文中文翻译 · AI 生成,仅供学习交流

HFresh:面向内存高效的向量检索

HFresh 是 Weaviate 的基于磁盘的向量索引,适用于那些优先考虑更低内存占用而非峰值查询吞吐量的应用。这种取舍对于资源有限的中小型应用以及大型数据集都很有用。例如,Weaviate Cloud 的免费层级(Free Tier)就通过其成本优化(Cost Optimized)配置默认使用 HFresh。

理解相似度检索中 HNSW 的内存瓶颈

在快速查找相似向量这件事上,HNSW(Hierarchical Navigable Small World,分层可导航小世界)已经成为事实上的标杆。它速度快、准确率高,但当数据集规模从数百万扩展到数十亿向量时,HNSW 暴露出一个根本性的限制:它的图结构和向量缓存必须全部常驻内存。

HNSW 是一种基于图的索引,它将向量组织成分层结构。在最顶层,是一个稀疏的图,包含远距离连接,用于快速导航到正确的邻域。随着逐层向下,图的密度越来越大,本地连接越来越多,最终在最底层引导你找到最相似的向量。

原文配图

问题不在于 HNSW 是否优秀。它绝对优秀。如果你追求最极致的低延迟和最高吞吐量,HNSW 几乎无可匹敌。但许多应用更看重低内存占用和更大规模,而不是峰值查询性能。

这时,基于磁盘的索引就显得有价值了。如果能用一部分延迟换取低得多的内存占用,以及扩展到更大数据集的能力,会怎样?

引入 HFresh

HFresh 是一种现代化的基于磁盘的向量索引,专为高召回率(recall)、出色的更新性能以及大规模下可控的查询 I/O 而设计。它建立在 SPFresh 研究论文所提出的思想之上,并 adapted 以复用 Weaviate 中已有的、经过实战检验的组件。

从高层来看,HFresh 属于基于分区(partition-based)的向量索引家族。它不像 HNSW 那样将每个向量连接到全局图中的邻居,而是把向量划分成许多小的区域,叫做 posting(倒排段)。每个 posting 中包含在向量空间中彼此接近的向量,并以 LSM 存储(LSM store)的形式存储在磁盘上。

原文配图

为了让这种布局高效,HFresh 采用了两阶段搜索策略。

首先,一个紧凑的内存中质心索引(centroid index)用来识别向量空间中与查询相关的区域。然后,只有对应的 posting 才会从磁盘读出并进行细粒度搜索。通过将磁盘读取限制在数据集的一个小子集内,HFresh 旨在将 I/O 控制在有限范围内并保持延迟可预测,即使数据集扩展到数十亿规模也是如此。

这种结构专为支持超大规模数据集而设计,同时保持性能可预测。

无需重建的"新鲜度"

SPFresh 背后的核心思想(HFresh 继承了这一思想)是:大多数更新只影响向量空间中的一小块区域。

在传统的基于分区的索引中,更新会不断累积,分区可能发生漂移,最终需要一次完整重建来恢复召回率和延迟,在大规模场景下这个过程可能耗费数小时乃至数天。

SPFresh 证明这通常是不必要的。在一个结构良好的分区索引中,插入或删除一个向量通常只影响向量空间的一个小的邻域。我们不必重建一切,而是可以通过增量再平衡(incremental rebalancing),使用一小组局部操作来维持索引质量。

拆分过大的 posting;合并过小的 posting;当边界发生变化时重新分配向量。这些操作大多以后台异步方式运行,持续修复局部的小失衡,避免它们累积成全局问题。其结果是一个索引能够长期保持新鲜和良好平衡,避免破坏性的重建周期。

HFresh 如何基于 SPFresh 构建

HFresh 采纳了 SPFresh 背后的核心思想,并 adapted 它以契合 Weaviate 的架构。目标不是逐组件复现论文,而是保留这一设计最引人入胜之处:局部维护替代重建;可控的查询 I/O;内存路由层与基于磁盘的 posting 之间清晰分离。在此基础上,我们做出了一系列务实选择:没有引入全新的 ANN(Approximate Nearest Neighbor,近似最近邻)机制,而是复用 Weaviate 中已有的、经过实战检验的模块,并改造它们以服务于这种新布局。也就是说既保留 SPFresh 的整体哲学,又重新思考其中一些组件,让它们更好地匹配 Weaviate 在索引、过滤、压缩和更新方面的优势。

将 HNSW 用作质心索引

HFresh 的一个关键设计选择是使用 HNSW 作为质心索引,而不是 SPTAG。

SPTAG 是微软最初的 SPANN 设计中使用的 ANN 索引,并被 SPFresh 沿用。它将分区树与图相结合,使查询能快速向最近的质心导航,并由此找到正确的 posting 列表。然而对 Weaviate 来说,HNSW 才是更自然的选择:它已经很好地扮演了这个角色。

HNSW 是 Weaviate 中使用最广泛的向量索引,也是系统中经过最充分实战检验的部分之一。我们在各种工作负载和数据集规模的生产环境中深入理解它的行为。在质心搜索中复用它,使 HFresh 能够构建在已经过验证的基础设施之上,而不是引入一个全新的机制。

HNSW 也非常契合质心索引的实际需求。在 HFresh 中,质心层负责把查询快速、准确地路由到正确的 posting。也就是说它必须保持紧凑、低延迟,并支持随着 posting 不断演变而产生的频繁更新。

质心并非静态:拆分、合并和重新分配不断重塑向量空间的划分,因此质心索引必须能够吸收大量插入和删除,而无需昂贵的重建。

另一个优势是质心索引自身也可以被量化。因为质心层只用于识别向量空间中有潜力的区域,HFresh 可以对这些向量进行足够激进的压缩,以降低内存占用,同时维持强召回所需的精度。事实上,HFresh 使用的是带 RQ8 的 HNSW,将质心的内存占用减少了 4 倍。

在此处使用 HNSW 也意味着 HNSW 的改进会自动惠及 HFresh。一个很好的例子是 ACORN,它通过让图遍历在仅部分数据匹配过滤器时更高效,从而改善过滤搜索。由于 HFresh 依赖 HNSW 作为质心层,这些改进并非孤立的:它们直接强化了 HFresh 的查询路径。

量化

HFresh 在两个地方使用了旋转量化(Rotational Quantization),分别对应两种不同的压缩级别。

原因是搜索的两个阶段承担不同职责。质心索引负责把查询路由到正确的 posting,因此它需要足够的精度,避免把查询发送到向量空间的错误区域。而 posting 则用于生成候选集:它们的近似分数并非最终排序,因为 HFresh 之后会使用原始未压缩向量对最佳候选重新打分(rescore)。

旋转量化的原理是:先将向量旋转到一种更易于压缩的表示形式,再降低每一维的精度。HFresh 使用:

- 对质心使用 RQ8,将质心向量内存减少 4 倍;
- 对 posting 使用 RQ1,相比 32 位浮点数(float)将存储的向量数据减少最高 32 倍。

质心索引使用 RQ8

HFresh 搜索的第一阶段使用一个针对质心的 HNSW 索引。这个内存中的索引负责识别哪些 posting 可能包含查询的最近邻。

HFresh 在这里使用 RQ8,因为路由失误代价昂贵。如果质心搜索漏掉了正确的区域,后续的 posting 扫描就可能根本看不到真正的最近邻。RQ8 提供了一个很好的折中:在包含开销之前,它使质心向量的有效负载减小约 4 倍,同时保留足够的精度以保证路由准确。

这一点之所以特别有效,是因为质心索引远小于完整向量数据集。HFresh 可以对质心使用更高精度的压缩格式,同时仍然保持内存层紧凑。

posting 使用 RQ1

在 HFresh 选出最有希望的 posting 之后,它会扫描其中存放的向量。这个阶段有不同的取舍。

posting 存放在磁盘上,因此它们的大小直接影响存储成本和查询 I/O。HFresh 使用 RQ1 存储 posting 向量,其中每一维仅用 1 个比特表示。相比 32 位浮点向量,也就是说向量存储最高减少 32 倍。

这种激进的压缩使每个 posting 更小、读取更廉价。它也让第一遍距离计算足够快,从而能扫描大量候选。

HFresh 并不依赖 RQ1 的分数做最终排序。RQ1 仅用于构建一个候选集。然后 HFresh 会取回排名靠前候选的原始未压缩向量,在重打分阶段重新计算精确距离。这既把磁盘读取控制得很小,又不放弃最终排序的质量。

原文配图

后台操作

HFresh 之所以能长期保持平衡而无需重建,是因为维护机制被内建到索引本身。它不是让失衡不断累积、再一次性通过大型离线任务去修复,而是把维护拆解成一组可以持续处理的小型后台任务。

实际上,大多数前台写入操作都很简单:向量被快速追加,后续工作被推送到后台队列。这些任务被持久化到专用的磁盘队列,因此它们能在重启后存活,并由调度器增量地排空。

HFresh 将这些工作组织成三种主要的后台任务类型:拆分(split)、合并(merge)、重新分配(reassign)。

拆分(Split):当一个 posting 增长得过大时,HFresh 会对其进行拆分。它会加载该 posting,垃圾回收掉陈旧条目,使用 Balanced K-Means(均衡 K 均值)算法把向量分成两个均衡的组,并创建两个新质心来替换旧质心。这避免了 posting 过度膨胀,否则会让磁盘读取变重、路由精度下降。

合并(Merge):相反的问题是 posting 变得过小。这可能在删除之后发生,也可能只是因为数据分布自然演化。在这种情况下,HFresh 会寻找一个邻近的、能吸收较小 posting 而自身又不会过大的 posting。如果找到合适的候选,它会把两者合并,并移除多余的质心。这避免了索引碎片化成大量微小 posting。

重新分配(Reassign):在拆分或合并之后,某些向量可能已经不再属于它们当前所在的 posting。这就是重新分配登场的地方。SPFresh 论文描述了一个名为 LIRE(Lightweight Incremental Rebalancing,轻量级增量再平衡)的协议:在拆分之后,它会检查是否有某些向量在新质心下、或者甚至在邻近 posting 下能更好地归属;在合并之后,它会检查某些被吸收进来的向量是否其实应该被移到别处。这些重新分配使 HFresh 能逐步修正错误,而不是试图一步到位地做到完美。

这些后台操作共同构成一个连续的平衡循环。插入带来局部变化,而索引在此之后悄然自我整理。其结果是一个索引能够长期保持新鲜,无需破坏性的重建周期。

带过滤的搜索

带过滤的向量检索带来另一个约束:结果既要与查询向量相似,又要满足过滤条件。例如,在电商搜索中,这可能意味着找到来自特定品牌、价格区间内且当前有货的相似商品。

HFresh 使用一个以位图(bitmap)表示的允许列表(allow list)来跟踪满足过滤条件的文档 ID。然后,它会根据匹配向量数量在两种搜索策略之间进行选择。

对于高选择性的过滤,运行完整的 HFresh 流水线可能比直接搜索匹配子集代价更高。当允许列表中包含的 ID 少于 5000 时,HFresh 会绕过质心路由和 posting 扫描,直接取出这些 ID 对应的原始向量,并计算精确距离。当前的 5000 ID 阈值是一个固定的内部启发式阈值,既不是可配置的参数,也不是通用的调优建议。

感知 posting 的过滤

对于更宽泛的过滤,HFresh 使用其常规的两阶段搜索,并结合一种感知 posting 的预过滤形式。过滤器识别匹配对象,而质心 HNSW 则把查询路由到 posting,而非单个对象。HFresh 通过记录每个 posting 包含哪些向量 ID 的元数据,在这两个层级之间搭起桥梁。

流程如下:

1. 使用 posting 元数据,将对象级允许列表转换为 posting 资格信息。
2. 使用 ACORN 导航质心 HNSW,挑选出至少能贡献一个匹配向量的 posting。
3. 读取这些 posting,在扫描其压缩向量时重新应用原始允许列表。
4. 使用原始未压缩向量对排名靠前的候选进行重打分。

由于 HFresh 可以将一个向量复制到多个 posting 中,它还会跟踪哪些匹配向量已经对 posting 选择做出了贡献,从而避免仅仅因为同一匹配的副本出现在多个 posting 中就去重复读取多个 posting。posting 级选择与向量级检查的结合,既减少了不必要的磁盘读取,又保证最终结果满足过滤条件。

如何使用 HFresh

要使用 HFresh,在创建集合(collection)时将其配置为向量索引即可。

下面是一个使用 Python 客户端的示例:

from weaviate.classes.config import Configure, VectorDistances

client.collections.create(
    name="Article",
    vector_config=Configure.Vectors.self_provided(
        name="Title",
        vector_index_config=Configure.VectorIndex.hfresh(
            distance_metric=VectorDistances.COSINE,
        ),
    ),
)

调优 HFresh

先使用默认值。如果召回率过低,可以增加:

- search_probe,让每个查询搜索更多 posting;
- quantizer.rescore_limit,用全精度向量对更多候选进行重打分。

这两个设置都可以在不重建索引的情况下修改。

实验

我们在 DBpedia OpenAI 1M 数据集上对未压缩 HNSW、带 RQ1 的 HNSW、带 RQ8 的 HNSW 以及 HFresh 进行了基准测试。HNSW 索引使用 efConstruction=256maxConnections=16 构建。

堆内存使用情况

这些指标度量的是 Go 堆(heap)内存使用情况,而非进程或系统的总内存。HFresh 还依赖操作系统管理的缓存来访问磁盘数据,这部分不会被堆内存数据所体现。

最显著的差异体现在空闲时的堆内存占用上。HFresh 仅使用 239 MB 堆内存,而未压缩 HNSW 则使用 6.67 GB。

HFresh 也比量化后的 HNSW 使用更少的堆内存。带 RQ1 的 HNSW 使用 715 MB,约为 HFresh 的 3 倍;带 RQ8 的 HNSW 使用 2.38 GB,约为 HFresh 的 10 倍。

这种差异直接源自 HFresh 的架构。HFresh 把 RQ8 压缩的质心索引和支撑元数据保留在内存中,而将 posting 存放在磁盘上。posting 成员关系、向量版本与删除状态、以及 posting 大小本身也会占用堆内存,并随数据集增长而增长,但这种布局显著降低了索引数据所需的堆内存。

查询吞吐量

QPS-recall 曲线展示了 HFresh 在堆内存优势之外、查询性能方面的情况。

在相当的召回率下,HNSW 及其量化变体的吞吐量远高于 HFresh。这是预期的:HNSW 搜索的是内存中的图,而 HFresh 则需要从磁盘读取选定的 posting,并取回原始向量进行重打分。

这些结果明确了何时该选用哪种索引。当最低延迟和最高吞吐量最重要时,HNSW 仍是更好的选择。HFresh 则面向那些将完整索引放在内存中代价过高、且能接受以较低吞吐量换取显著更小堆内存占用的工作负载。

扩展到十亿向量

除了 DBpedia 上的基准测试,我们还使用十亿个随机生成的 256 维向量测试了 HFresh 索引的构建过程。我们使用了默认的索引构建设置,并以 12 个 worker 并行导入,每批 1000 条。

| 指标 | 结果 |
| --- | --- |
| 数据集 | 10 亿个 256 维向量 |
| 计算资源 | 32 vCPU、256 GB RAM(n2-highmem-32) |
| 存储 | 4 TB SSD 持久化磁盘(pd-ssd) |
| 虚拟机峰值内存使用 | 204 GB |
| 重启后虚拟机内存使用 | 54 GB |
| 重启后 Go 堆内存 | 47 GB |
| 磁盘峰值使用 | 3.09 TB |
| 导入完成后磁盘使用 | 2.28 TB |该次运行成功构建了十亿向量的 HFresh 索引。该规模下未收集召回率或 QPS 数据。

虚拟机内存指标来自 GCP 系统指标,堆内存指标则来自 Go 堆内存 profile。

结论

HFresh 为 Weaviate 提供了一种基于磁盘的、面向内存高效向量检索的索引,覆盖从中小型应用到大规模可变数据集的场景。通过将紧凑的质心索引和支撑元数据保留在内存中,并以增量方式维护磁盘上的 posting,它在无需完整重建的前提下降低了索引数据所需的堆内存。当最低延迟和最高吞吐量最重要时,HNSW 仍是更好的选择;HFresh 则面向那些更小的堆内存占用足以补偿较低查询吞吐量的工作负载。

原文配图