厌倦了用 [pgvector] 索引 1 亿向量要等两天多?
VectorChord 1.0 登场:再也别说索引是“通宵大项目”了!
VectorChord 1.0 让你直呼“哇塞”:
总结: VectorChord 1.0 不只是让你跑赢基准测试,而是让你告别通宵等索引的噩梦,专注于开发!
阅读全文, 看 50 小时变成 20 分钟的“魔法”!
以下内容翻译自: https://blog.vectorchord.ai/vectorchord-10-developer-first-vector-search-on-postgres-100x-faster-indexing-than-pgvector
两年前,当我们发布第一篇 [pgvecto.rs] 博客文章时,我们做了一个赌注: Postgres 是进行向量搜索的最佳场所。从那时起,我们一直在围绕这个赌注进行迭代——从带有过滤向量搜索的 VBASE,到更长的向量支持,再到 [RaBitQ](量化方案)以及磁盘友好的索引布局。
借助 VectorChord 1.0,我们再次取得了重大突破。在 16 个 vCPU 的机器上,我们现在可以在 20 分钟内构建超过 1 亿个向量的索引。在相同的规模下,[pgvector] 需要 50 多个小时。这个数字听起来令人印象深刻,但本次发布的重点不仅仅是为了在基准测试幻灯片中获胜。它的目标是让你的实际开发和迭代周期大大加快。
本文分为三个部分:
当人们第一次与我们谈论 VectorChord 时,最常见的问题之一非常直接:
“你们在使用 [HNSW] 吗?我听说 [HNSW] 总是比 [IVF] 更好。”
从理论上讲,[HNSW](分层可导航小世界)是一个精妙的算法。在许多孤立的基准测试中,它都表现出色。但我们并非在“真空”中运行向量搜索——我们是在 [Postgres] 内部运行它,它有自己的存储模型、清理(vacuuming)、MVCC(多版本并发控制)和操作习惯。
如果你从这个角度来看待它,权衡取舍会发生很大变化。
[HNSW] 使用分层图结构。每个新向量都是一个节点,可能会连接到多个层上的其他几个节点。每次删除都可能移除许多其他节点所依赖的节点。
在一个独立的向量引擎中,你可以围绕这种结构设计整个存储引擎。但在 [Postgres] 内部,你不能。你必须适应现有的表/索引模型,与清理(vacuum)协作,并在 MVCC 下表现良好。
这就是痛苦的开始:
这些都不是不可能解决的,但只要你有频繁的更新,它就会导致高成本。
VectorChord 的核心索引是 [IVF](倒排文件索引)+ [RaBitQ](量化方案),带有简单的倒排列表(posting lists)。向量被路由到粗略的聚类中,在每个聚类内部,我们存储一个微小的量化代码,而不是一个 768 维的浮点数。在查询时,几乎所有的工作都发生在这些位压缩代码上,利用了查表和廉价的整数运算。
因为索引主要扫描压缩代码,即使触及许多条目,倒排列表扫描仍然很快。在我们的基准测试中,这使得 VectorChord 明显快于比较全精度向量的朴素 [IVF],并且仍快于 [pgvector] 的 [HNSW] 索引,后者需要遍历其图结构并使用全精度算术对邻居进行评分。
人们有时会问“使用 [HNSW] + 量化向量怎么样?”你可以这样做,它可以在搜索的第一阶段加速——遍历量化向量并选择候选者。但你仍然需要第二阶段,根据这些候选者来获取和评分全精度向量,这部分与你使用的索引无关。在典型的工作负载中,第一阶段只占总延迟的大约 40%,所以即使扫描速度提高 2 倍,也只能将端到端时间缩短大约 20%——而且由于 [HNSW] 无法以紧凑、对批量处理友好的快速扫描格式布局数据,即使是这种理论上的增益在实践中也可能难以实现。
使用 [IVF] + [RaBitQ],倒排列表只是压缩条目的数组。插入只需追加新代码;删除只需清除代码。没有需要修复的全局图结构,因此频繁的更新不会触发级联工作。在操作上,它的行为就像一个普通的索引,在我们的测试中,这种设计能够处理大约 [pgvector] 的 [HNSW] 索引 10 倍的更新吞吐量,同时保持延迟稳定。有关更多的性能数据——尤其是查询性能方面——请参阅我们早期的博客: **[Vector Search Over PostgreSQL: A Comparative Analysis of Memory and Disk Solutions]**(PostgreSQL 上的向量搜索:内存和磁盘解决方案的比较分析)。
最重要的是,正是这种简单性使得接下来的改进成为可能。如果你的索引结构已经极其错综复杂,那么每一个额外的优化都会增加更多的活动部件。通过保持核心设计更简单,我们为自己留出了空间,使索引构建和开发者工作流程得到显著改善。
让我们谈谈标题中的数字: 索引速度比 [pgvector] 快 100 倍。
在 LAION 1 亿向量数据集上,这种比较在理论上看起来是这样的:
但对你来说,改变更简单:
我们通过攻克 [IVF] 构建的两个阶段实现了这一目标:找到质心的 KMeans 步骤,以及将每个向量分配给其最近质心的插入步骤。
为了实现这一目标,我们首先解决了问题,而不仅仅是优化代码。首先,我们没有对 1 亿个点上的朴素 768 维、16 万个质心的 KMeans 进行微优化,而是使用 Johnson–Lindenstrauss 引理将向量投影到一个小得多的空间中,这将 KMeans 的计算和内存占用减少了大约 7 倍。其次,我们分两个阶段运行分层 KMeans,这样我们只对较小的数据子集进行聚类,而不是一次处理 16 万个质心,这在理论上加速了 400 倍。对于插入,我们在高一个级别重用了相同的思路,即在质心本身上构建一个 [IVF],这样每个数据向量只需使用量化代码与一小部分候选质心桶进行比较。总而言之,这些更改将大部分工作从受 CPU 限制的距离计算中移除了:对于 1 亿向量的构建,主导成本变成了 [Postgres] 以大约 SSD 限制的速度分配和写入索引页。在收紧分配路径和锁粒度,以便我们可以大块流式输出页面之后,实际结果是完全重建可以舒适地在几分钟而不是几天内完成。有关更多细节,我们专门写了一篇博客 [How We Made 100M Vector Indexing in 20 Minutes Possible on PostgreSQL] (我们如何在 PostgreSQL 上实现 20 分钟内完成 1 亿向量索引的)。
这些优化可以轻松地通过以下 SQL 完成:
CREATE INDEXON laion USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.pin = 2
[build.internal]
lists = [400, 160000] -- Hierarchical KMeans
build_threads = 16
spherical_centroids = true
kmeans_algorithm.hierarchical = {}
kmeans_dimension = 100-- Dimension Reduction
sampling_factor = 32
$$);
VectorChord 1.0 还增加了一组在基准测试中几乎不会出现,但在你每天使用系统时却非常重要的功能。它们都是为了帮助你了解索引的行为,并让你能够轻松使用现代模型和部署。
所有近似最近邻(ANN)索引都会随着时间漂移。数据分布会发生变化,构建时表现出色的索引可能会悄然退化。
VectorChord 不会让你去猜测,而是可以持续为你测量召回率([Recall])。它会自动采样真实查询向量,使用更精确的方法重新评估它们的邻居,并跟踪索引返回“正确”答案的频率。
这使得一种模糊的感觉——“最近搜索似乎有点不对劲”——变成了一个你可以查看的图表。如果召回率稳定,你就知道可以推迟重建。如果它呈下降趋势,你可以在用户注意到之前计划重建。它还为你提供了一些具体的东西,可以馈送到你现有的可观测性堆栈中,或者馈送到 Prometheus 中,这样你就可以在与你的其他 SLO 相同的仪表板上关注召回率。
实际上,你可以通过一个 SQL 调用来评估真实查询模式的召回率,然后将该数字导出为指标。例如:
SELECT vchordrq_evaluate_query_recall(query => $$
SELECT ctid FROM items ORDERBY embedding <-> '[3,1,2]'LIMIT10
$$);
-- With sampled vector recorded from query
SELECTAVG(recall_value) FROM (
SELECT vchordrq_evaluate_query_recall(
format(
'SELECT ctid FROM %I.%I ORDER BY %I OPERATOR(%s) %L LIMIT 10',
lq.schema_name,
lq.table_name,
lq.column_name,
lq.operator,
lq.value
)
) AS recall_value
FROM vchordrq_sampled_queries('items_embedding_idx') AS lq
) AS eval_results;
我们支持高达 16,000 维的向量。这听起来像一个枯燥的规范,但它具有清晰的实际效果:你可以接入更新的模型,包括长上下文或多模态模型,而无需立即压缩或截断它们的输出,只为了让索引满意。
你可以从模型自然产生的表示开始,感受性能和质量,然后决定是否要应用更激进的量化或维度降低。索引不应该是强迫你对模型选择做出妥协的东西。
并非每个文档都适合单个向量。现代检索增强生成(RAG)系统通常将一个段落表示为一组向量——例如,每个标记一个或每个句子一个——然后将该集合与一组查询向量进行比较。这种“后期交互”风格通常称为多向量检索。
VectorChord 通过对向量数组的 MaxSim 式运算符原生支持这种模式。从概念上讲,对于每个查询向量,你寻找最匹配的文档向量,计算它们的点积,然后将这些最佳得分相加。在 VectorChord 中,这作为基于距离的 @# 运算符暴露在 vector[] 列上:左侧是文档的向量数组,右侧是查询的向量数组。
入门与单向量搜索非常相似。你为每行存储一个向量数组并构建一个专用索引:
CREATE TABLE items (
id bigserial PRIMARY KEY,
embeddings vector(3)[]
);
CREATE INDEX ON items
USING vchordrq (embeddings vector_maxsim_ops);
在查询时,你传入一个查询向量数组,并按 @# 分数排序:
SELECT *
FROM items
ORDER BY embeddings @# ARRAY[
'[3,1,2]'::vector,
'[2,2,2]'::vector
]
LIMIT 5;
在底层,VectorChord 将其 ANN 机制应用于这些向量数组,因此你获得了多向量模型的表达能力,同时拥有与单向量 [IVF] 索引相同的性能——所有这些都在 [Postgres] 和纯 SQL 内部完成。
VectorChord 为 x86_64、ARM 和 IBM 架构提供 SIMD 加速。在运行时,我们会检测可用的最佳指令集——如果可用,则使用 AVX512 及类似指令集——而无需你调整构建标志或维护单独的二进制文件。
我们还有一个结合 DiskANN 和 2 位 [RaBitQ] 的实验性索引类型。在某些数据集和召回率目标上,它可以提供比 [IVF] + [RaBitQ] 更高的 QPS(每秒查询次数)。其权衡是索引和更新明显更慢、更复杂。
CREATE INDEX ON items USING vchordg (embedding vector_l2_ops);
我们不建议将其作为默认选择。它适用于有非常特定工作负载、确切知道自己在做什么,并愿意为参数空间的一个狭窄切片中的更高 QPS 支付更高运营成本的团队。对于其他人来说,[IVF] + [RaBitQ] 仍然是推荐的主力。
有时你不只是想要最近邻;你想要“这个半径内的一切,最多 N 行”。在 SQL 中编写最自然的方式是使用 WHERE 子句中的距离,加上 ORDER BY 和 LIMIT:
SELECT *
FROM items
WHERE embedding <-> '[0,0,0]' < 0.1
ORDER BY embedding <-> '[0,0,0]'
LIMIT 10;
这会返回正确的答案,但当只有少数点落在半径内时,它的性能很差。[Postgres] 仍然必须继续扫描索引,直到找到十行或穷尽搜索空间,因为距离检查只是在 ANN 扫描之后应用的过滤器。
VectorChord 增加了一个“相似性过滤器”语法,允许将距离阈值下推到索引本身。你将查询向量和半径包装在一个 sphere() 值中,并使用 <<->> 运算符,这样索引就知道一旦搜索区域超出该球体,它就可以停止:
SELECT *
FROM items
WHERE embedding <<->> sphere('[0,0,0]'::vector, 0.1)
ORDER BY embedding <-> '[0,0,0]'
LIMIT 10;
在实际应用中,向量搜索几乎不会孤立发生。你按租户、权限、时间范围或内容类型进行过滤,然后按向量相似性进行排名,有时顺序相反。
VectorChord 允许你在索引级别选择预过滤(prefiltering)或后过滤(postfiltering)。在低选择性场景中,预过滤可以充分减少候选集,从而将 QPS 提高大约五倍。在其他情况下,你可能希望先搜索,然后只过滤靠前的结果以获得更好的召回率。重要的是,你可以在 [Postgres] 中自然地表达这两种模式。
为会话启用预过滤是一个简单的 SQL 语句:
SET vchordrq.prefilter = on;
最后,我们添加了 VectorChord-BM25 扩展,将强大的文本搜索直接引入你的 [Postgres] 实例。它支持多种语言和高级分词,旨在在许多工作负载中与 ElasticSearch 风格的相关性竞争。
我们的想法不是取代所有专用的搜索引擎,而是让你构建一个系统,其中 BM25 和向量搜索并存,在同一个数据库内部,使用相同的操作工具。对于许多团队来说,仅这一点就消除了大量的部署和维护负担。
在实践中,你可以通过纯 SQL 调用它,将其与你的嵌入(embeddings)结合,并按统一分数排序。例如:
SELECT id,
passage,
embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL', 'bert')) AS rank
FROM documents
ORDER BY rank
LIMIT 10;
VectorChord 1.0 可以简单地概括为“在 1 亿向量上比 [pgvector] 快 100 倍的索引速度”。这个标题是真实的,但它并不是主要的焦点。
我们的目标是让 VectorChord 成为在 [Postgres] 上进行检索的最佳方式之一,从第一个原型到数十亿规模的数据集。如果你已经在用 [pgvector],我们希望你能用你的真实工作负载试用 VectorChord 1.0,并告诉我们它在哪里有帮助,在哪里可以做得更好。我们也要感谢 EnterpriseDB 团队在整个工作中提供的宝贵反馈。
除了VectorChord, 国内数据库厂商也在做pgvector的优化, 而且国内厂商是“我都要”的风格, 不仅支持向量, 当然还要支持全文搜索、标量搜索、混合搜索. 例如海量vastbase的向量版, OceanBase的seekdb, 大家说说还有哪些?