
2024年10月22日,Cloudflare 发布了一篇博客,详细介绍了他们如何构建 Vectorize —— 一个全球分布式的向量数据库,旨在帮助开发者使用 Cloudflare Workers 构建全栈、AI驱动的应用。Vectorize 通过优化查询嵌入(向量)—— 这些是为机器学习模型和语义搜索算法设计的文本、图像、音频等值或对象的表示 —— 使得查询变得更快、更容易且成本更低。
什么是向量数据库?
向量数据库是一种可查询的向量存储。向量是一个由大量数值组成的数组,称为向量维度。向量数据库支持相似性搜索查询:给定一个输入向量,它返回根据指定度量最接近的向量,可能还会根据它们的元数据进行过滤。
向量数据库被用于支持语义搜索、文档分类、推荐和异常检测,以及为大型语言模型(LLM)生成的答案提供上下文(检索增强生成,RAG)。
为什么向量需要特殊的数据库支持?
传统的数据结构,如 B 树或二叉搜索树,期望它们索引的数据易于比较且遵循一维线性排序。它们利用数据的这一属性来组织数据,使搜索变得高效。字符串、数字和布尔值就是具有这种属性的数据示例。
由于向量是高维的,以一维线性方式对它们进行排序对于相似性搜索是无效的,因为得到的排序不捕捉向量在高维空间中的接近性。这种现象通常被称为维度的诅咒。
此外,使用对相似性搜索有用的距离度量来比较两个向量是一个计算成本高昂的操作,需要数据库特定的技术来克服。
查询处理架构
Vectorize 利用 Cloudflare 的全球网络将快速的向量搜索带到用户附近,并依赖许多组件来实现这一点。
Vectorize 组件
Vectorize 在每个 Cloudflare 数据中心运行,在支持 Cloudflare Workers 的基础设施上。它服务于来自 Worker 绑定的流量,以及通过我们的 API 网关来自 Cloudflare REST API 的流量。
每个查询都在它进入的数据中心的服务器上处理,以一种分散负载到该数据中心所有服务器的方式进行选择。
在该服务器上运行的 Vectorize DB 服务(一个 Rust 二进制文件)通过读取 R2 上的索引数据来处理查询,Cloudflare 的对象存储。它通过读取 Cloudflare 的缓存来加速 I/O 操作。
搜索向量和索引以加速搜索
作为一个向量数据库,Vectorize 具有相似性搜索查询功能:给定一个输入向量,它返回根据指定度量最接近的 K 个向量。
相似性搜索的三个步骤
- 评估查询向量与索引中每个向量的接近程度。
- 根据它们的接近度“得分”对向量进行排序。
- 返回最佳匹配项。
虽然这种方法准确有效,但计算成本高昂且不适用于包含数百万向量的索引(见**为什么向量需要特殊数据库支持?**上述)。
为了做得更好,我们需要修剪搜索空间,即避免对每个查询扫描整个索引。
为此,我们需要找到一种方法来丢弃我们知道与查询无关的向量,同时集中精力在可能相关的向量上。
使用 IVF 索引向量
Vectorize 使用一种称为 IVF(倒排文件索引)的索引技术来修剪查询的搜索空间。
IVF 根据它们的相对接近度对索引向量进行聚类。对于每个聚类,它然后确定其质心,即该聚类的中心点,这是一个最小化与聚类中每个向量距离的高维点。
一旦确定了质心列表,每个质心都被赋予一个数字。然后我们通过将每个向量放置在一个以其最接近的质心命名的文件中来在存储上组织数据。
处理查询时,我们可以通过仅查看与查询向量最接近的质心文件中的相关向量来修剪搜索空间。
使用 PQ 压缩向量
Vectorize 支持高达 1536 维的向量。以每个维度 4 字节(32 位浮点数)计算,这意味着每个向量高达 6 KB。这意味着我们需要从存储中提取高达 6 GB 的未压缩向量数据并将其放入内存中,每百万个向量就需要这么多。
为了在限制 CPU、内存和 I/O 的情况下处理数百万向量索引,Vectorize 使用了一种称为 PQ(乘积量化)的降维技术。PQ 以保留大部分特异性的同时大大减小它们的大小的方式压缩向量数据 —— 类似于对图片进行下采样以减小文件大小,同时仍然能够精确地识别图片中的内容 —— 使 Vectorize 能够高效地对这些较轻的向量执行相似性搜索。
除了存储压缩向量外,它们的原始数据也保留在存储上,并且可以通过 API 请求;压缩向量数据仅用于加速搜索。
近似最近邻搜索和结果精度细化
通过修剪搜索空间和压缩向量数据,我们已经设法提高了查询操作的效率,但现在可能产生与真实最近匹配集合不同的匹配集合。我们通过执行近似最近邻搜索,以速度换取结果精度,达到了约 80% 的精度。
为了将结果精度提高到超过 95%,Vectorize 然后在顶部近似匹配上执行结果细化传递,使用未压缩的向量数据,并返回最佳细化匹配。
最终一致性和快照版本控制
每当您查询 Vectorize 索引时,您保证收到的结果是从一个一致的、不可变的快照中读取的 —— 即使您同时在索引上写入。在我们的系统中,写入以它们到达的严格顺序应用,并且它们被引导到一个异步过程。我们通过读取旧版本、进行更改并将其作为 R2 中的新对象写入来更新索引文件。每个索引文件都有自己的版本号,并且可以独立于其他文件进行更新。在两个索引版本之间,我们可能会更新数百甚至数千个 IVF 和元数据索引文件,但即使我们更新文件,您的查询将一致地使用当前版本,直到切换的时候。
每个 IVF 和元数据索引文件都有自己的版本。构成索引的快照版本的所有版本化文件的列表包含在一个 manifest 文件 中。每个版本的索引都有自己的清单。当我们基于上一版本写入新的清单文件时,我们只需要更新引用被修改的索引文件;如果有未修改的文件,我们简单地保留对上一版本的引用。
我们使用 root manifest 作为索引当前版本的权威。这是变更的支点。root manifest 是一个特定版本的清单文件的副本,它被写入一个确定的位置(索引的 R2 桶的根)。当我们的异步写入过程完成处理向量,并将所有新的索引文件写入 R2 时,我们通过覆盖当前的 root manifest 与新清单的副本来 提交。R2 中的 PUT 操作是原子的,所以这有效地使我们的更新原子化。一旦清单更新,运行在我们网络上的 Vectorize DB 服务实例将拾取它,并使用它来服务读取。
因为我们保留了过去的索引和清单文件版本,我们有效地维护了您索引的版本化快照。这意味着我们有一条直接的路径来构建一个时间点恢复功能(类似于 D1 的时间旅行功能)。
您可能已经注意到,因为我们的写入过程是异步的,这意味着 Vectorize 是 最终一致的 —— 也就是说,在成功完成写入索引的请求和最终在查询中看到这些更新之间存在延迟。这并不总是适用于所有数据存储用例。例如,想象两个用户使用在线机票预订应用程序,他们都购买了同一个座位 —— 一个用户成功预订了机票,另一个用户最终会收到一个错误,说座位已经被占用,他们需要重新选择。因为向量索引通常不作为这些事务用例的主数据库,我们决定最终一致性是一个值得的权衡,以确保 Vectorize 查询快速、高吞吐量且即使索引大小增长到数百万也能保持低成本。
分布式写入协调:WAL 中的另一块
在上述部分中,我们提到了我们的最终一致性、异步写入过程。现在我们将更深入地了解我们的实现。
WAL
写入前日志(WAL)是数据库系统中用于进行原子和持久写入的常用技术。Vectorize 的 WAL 使用 SQLite 在 Durable Objects 中实现。
在 Vectorize 中,每个更新的有效载荷都被赋予一个 ID,写入 R2,并且该有效载荷的 ID 被交给 WAL Durable Object,它将其作为“块”持久化。因为它只是数据的指针,所以块是每个变异的轻量级记录。
Durable Objects(DO)有许多好处 —— 强大的事务保证、计算和存储的新颖组合以及高度的水平扩展 —— 但是单个 DO 是小额的内存和计算分配。然而,更新索引的过程非常资源密集 —— 单个写入可能包括数千个向量,这意味着读取和写入 R2 中存储的数千个数据文件,并将大量数据存储在内存中。这超过了单个 DO 可以处理的范围。
因此,我们设计了 WAL 以利用 DO 的优势,并使其成为一个协调器。它通过将繁重的工作委托给更强大的计算资源实例(我们称之为“执行器”)来控制更新索引的步骤,但是使用其事务属性确保步骤以强一致性完成。它保护过程免受流氓或停滞的执行器的影响,并确保 WAL 处理继续向前推进。DO 易于扩展,因此我们为每个 Vectorize 索引创建一个新的 DO 实例。
WAL 执行器
执行器从一个由所有 WAL 共享的计算资源池中运行。我们使用一个简单的生产者-消费者模式,使用 Cloudflare 队列。WAL 将请求入队,执行器轮询队列。当它们收到请求时,它们调用 WAL 的 API 请求被 分配 到请求。
WAL 确保一个执行器一次只被分配给那个写入。当执行器写入时,索引文件和更新的清单被写入 R2,但尚未可见。最后一步是执行器调用 WAL 的另一个 API 来 提交 更改 —— 这是关键 —— 它传递更新的清单。WAL 负责用更新的清单覆盖根清单。根清单是原子更新的支点:一旦写入,更改将对 Vectorize 的数据库服务可见,并且更新的数据将出现在查询中。
从一开始,我们就设计了这个过程来考虑非确定性错误。我们首先列举失败模式,只有在断言它们处理了失败的可能性后才继续可能的设计选项。例如,如果一个执行器停滞,WAL 会找到一个新的执行器。如果第一个执行器回来,协调器将拒绝其提交更新的尝试。即使那个第一个执行器在已经写入的旧版本上工作,并在 R2 中写入了新的索引文件和新的清单,它们也不会覆盖从提交版本写的文件。
批量更新
现在我们已经讨论了一般流程,我们可以回到我们最喜欢的 WAL 功能之一。在执行器上,写入过程中最耗时的部分是从 R2 读取和写入许多文件。即使我们使读取和写入并发以最大化吞吐量,更新单个文件中的数千个向量的成本也被网络 I/O 的总延迟所掩盖。因此,一次性执行中处理更多的向量更有效。
所以我们这样做:我们批量离散更新。当 WAL 准备请求执行器的工作时,它会从 WAL 中获取一块“块”,从下一个未写入的块开始,并保持块的顺序。它将在 SQLite 表中写入一个新的“批次”记录,该记录将该块序列、索引的版本和被分配给批次的执行器的 ID 联系起来。
用户可以在单个插入或 upsert 调用中批量更新多个向量。由于每个更新的大小可能不同,WAL 通过计算每个块代表的更新数量来自适应地计算其批次的最佳大小。它将一次性批量处理多达 200,000 个向量(我们在自己的测试后得出的值),限制为 1,000 个块。有了这种吞吐量,我们已经能够快速地将数百万向量加载到索引中(每次 upsert 5,000 个向量)。此外,WAL 不会暂停自己以收集更多的写入以批量处理 —— 相反,它在到达时立即开始处理写入。因为 WAL 一次只处理一个批次,这在其工作流程中创建了一个自然暂停,以批量处理在此期间到达的写入。
重新训练索引
WAL 还协调我们重新训练索引的过程。我们偶尔会重新训练索引,以确保 IVF 质心的映射最好地反映索引中的当前向量。这维持了向量搜索的高准确性。
重新训练会产生一个全新的索引。所有索引文件都已更新;向量已在索引空间中重新洗牌。因此,所有索引都有一个第二个版本戳 —— 我们称之为 代 —— 以便我们可以区分重新训练的索引。
WAL 跟踪索引的状态,并控制何时开始训练。我们有一个第二个进程池,称为“训练器”。WAL 在队列中入队一个请求,然后一个训练器拾起请求并开始训练。
训练可能需要几分钟才能完成,但我们不会暂停当前代的写入。WAL 将继续正常处理写入。但训练是从索引的一个固定快照运行的,并且会随着并行更新的实时索引而变得过时。一旦训练器完成,它将向 WAL 发信号,然后 WAL 将开始一个多步骤的过程来切换到新的代。它进入一个模式,继续在 WAL 中记录写入,但不会在当前索引上使这些写入可见。然后它将开始赶上自训练器向 WAL 发信号以来的所有更新。一旦它赶上了索引中存在的所有数据,它将切换到新的重新训练的索引。这防止了新索引看起来“回到过去”。所有随后的写入将应用于新索引。
这一切都通过批量记录无缝建模。因为它将索引版本与 WAL 块的范围关联起来,多个批次可以跨越相同的块序列,只要它们属于不同的代。我们可以这样说:单个 WAL 块可以与许多批次关联,只要这些批次在不同的代中。从概念上讲,批次作为第二层 WAL 层在 WAL 块之上。
Vectorize 支持向量相似性查询上的元数据过滤器。这允许查询将向量相似性搜索集中在索引数据的一个子集上,产生否则不会是顶部结果的匹配。
例如,这使我们能够查询与 color: “blue” 和 category: “robe” 最匹配的向量。
从概念上讲,处理此示例查询需要:
- 通过扫描所有元数据,确定匹配
color: “blue”的向量集。 - 通过扫描所有元数据,确定匹配
category: “robe”的向量集。 - 将两个集合相交(在过滤器中进行布尔 AND),以识别同时匹配颜色和类别过滤器的向量。
- 对交集中的所有向量进行评分,并返回顶部匹配项。
虽然这种方法有效,但它不适用于大规模索引。对于包含数百万向量的索引,以这种方式处理查询将非常资源密集。更糟糕的是,它阻止我们使用我们的 IVF 索引来识别相关的向量数据,如果过滤后的向量集合很大,我们不得不在可能数百万的向量上计算接近度分数。
为了做得更好,我们需要像我们对向量数据一样对元数据搜索空间进行索引,并找到一种有效的方法将元数据索引产生的向量集合与我们的 IVF 向量索引结合起来。
使用分块排序列表索引元数据
Vectorize 维护每个可过滤属性的一个元数据索引。每个可过滤的元数据属性都使用分块排序列表索引进行索引。
分块排序列表索引是数据中所有不同值的排序列表,每个值都映射到具有该值的向量 ID 集合。这使得 Vectorize 能够在 O(log n) 复杂度中对元数据索引中的值进行二分搜索,换句话说,这是在大型数据集上进行搜索的速度。
因为它在大型索引上可能变得非常大,所以排序列表被分块成匹配目标重量的 KB 块,以保持索引状态获取高效。
一个轻量级的块描述符列表在索引清单中维护,跟踪列表块及其下限/上限值。这个块描述符列表可以二分搜索以识别包含搜索元数据值的块。
一旦确定了候选块,Vectorize 就从索引数据中获取该块,并在其中二分搜索,如果找到,则获取与元数据值匹配的向量 ID 集合,如果没有找到,则获取一个空集合。
我们以这种方式识别每个查询的元数据过滤器的匹配向量集,然后在内存中交集这些集合以确定最终匹配过滤器的向量集。
这只是查询处理的一半。我们现在需要在匹配元数据过滤器的向量中识别与查询向量最相似的向量。
连接元数据和向量索引
向量相似性查询总是伴随着一个输入向量。我们可以基于与查询向量的接近度对 IVF 向量索引的所有质心进行排名。
由元数据过滤器匹配的向量集合包含每个向量的 ID 和 IVF 质心编号。
从这个,Vectorize 推导出每个 IVF 质心匹配查询过滤器的向量数量,并确定根据查询请求的匹配数量需要扫描哪些以及多少顶级 IVF 质心。
然后 Vectorize 执行 IVF 索引向量搜索(见上述 搜索向量并索引以加速搜索 部分),同时仅考虑过滤后的元数据向量集合中的向量。
因为我们有效地使用元数据过滤器修剪了向量搜索空间,所以过滤查询通常可以比它们的未过滤等价物更快。
查询性能
系统的绩效以延迟和吞吐量来衡量。
延迟是相对于单个查询的度量,评估处理查询所需的时间,通常以毫秒为单位。这是最终用户感知服务“速度”的指标,因此较低的延迟是可取的。
吞吐量是相对于索引的度量,评估它在一段时间内可以并发处理的查询数量,通常以每秒请求数或 RPS 表示。它使应用程序能够扩展到数千个同时用户,因此更高的吞吐量是可取的。
Vectorize 旨在实现高索引吞吐量,并针对低查询延迟进行优化,以为苛刻的应用提供出色的性能。查看我们的基准测试。
查询延迟优化
作为一个保持其数据状态在 blob 存储上的分布式数据库,Vectorize 的延迟主要由索引数据的获取驱动,并且严重依赖 Cloudflare 的缓存网络以及各个服务器 RAM 缓存来保持低延迟。
因为 Vectorize 数据是快照版本化的,每个版本的索引数据都是不可变的,因此高度可缓存,增加了 Vectorize 从依赖 Cloudflare 的缓存基础设施中获得的延迟优势。
为了保持索引数据精简,Vectorize 使用技术来减少其重量。除了乘积量化,索引文件使用针对运行时性能优化的空间高效二进制格式,Vectorize 在获取后无需解析即可使用。
索引数据被分割成最小化处理查询所需的数据量的方式。维护辅助索引以限制要获取的片段数量,通过直接跳转到大规模存储上的相关内容来减少过度获取。
Vectorize 通过利用 SIMD CPU 指令以及通过组织两阶段向量搜索来提升所有向量接近度计算,有效地平衡了延迟/结果精度比。
当通过 Worker 绑定使用时,每个查询都在靠近服务 Worker 请求的服务器上处理,因此靠近最终用户,最小化了最终用户、Worker 应用程序和 Vectorize 之间的网络延迟。
查询吞吐量
Vectorize 在 Cloudflare 的每个数据中心运行,在全世界数千个服务器上运行。
由于每个索引的数据都是快照版本化的,每个服务器都能同时并发地服务索引,而不会在状态上发生争用。
这意味着 Vectorize 索引可以随着其分布式流量的水平扩展而弹性扩展,为最苛刻的 Worker 应用程序提供非常高的吞吐量。
增加索引大小
我们很高兴我们的 Vectorize 升级版本可以支持高达 500 万个向量,这是 beta 版限制(20 万个向量)的 25 倍改进。我们在这篇博客文章中讨论的所有改进都有助于向量存储的增加。随着存储的增加,查询性能和吞吐量也得到了提高。
然而,500 万可能对某些用例来说是有限的。我们已经收到了这个反馈。这个限制来自于构建一个全新的全球分布式有状态服务的约束,以及我们希望快速迭代并使 Vectorize 普遍可用,以便构建者可以自信地将其用于他们的生产应用程序。
我们相信构建者可以将 Vectorize 作为他们的主要向量存储,无论是使用单个索引还是通过多个索引分片。但如果这个限制对您来说太有限了,请告诉我们。告诉我们您的用例,让我们看看是否可以一起工作,使 Vectorize 适合您。
立即尝试!
每个免费计划的开发者都可以尝试一下 Vectorize。您可以访问我们的开发者文档开始。
如果您正在寻找构建的灵感,请查看结合 Workers AI 和 Vectorize 进行文档搜索的语义搜索教程,完全运行在 Cloudflare 上。或者看看如何结合 OpenAI 和 Vectorize 给 LLM 更多上下文并显著提高其答案的准确性的例子。
如果您对如何使用 Vectorize 有产品和工程团队的问题,或者只是想与其他在 Workers AI 上构建的开发者交流想法,请加入我们的 Developer Discord 上的 #vectorize 和 #workers-ai 频道。
Cloudflare 的连接云保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序,抵御 DDoS 攻击,抵御黑客,并可以帮助您在零信任之旅中。
访问任何设备上的1.1.1.1,开始使用我们的免费应用程序,使您的互联网更快、更安全。




