← 文章 / 数据与数据库
Hacker News 53分钟前 · 2026-10-02 14:27:07 · 0 阅读

再见,向量数据库

我们正在调整 turbopuffer 的存储架构,将搜索能力推向新高度。turbopuffer v3 重新设计了文档与索引的布局、写入、压缩及查询方式。这将使搜索在所有维度上——包括文本、正则表达式和向量搜索——都快人一步,同时也为将更多 SQL 查询迁入 turbopuffer 并实现高性能执行奠定基础。

turbopuffer 最初作为 Serverless 向量数据库(v1) 面世,专为提供极低成本且速度合理的向量搜索而设计。采用对象存储作为数据源保证了经济成本,分级 NVMe SSD 与内存缓存则确保了性能。最早一批客户(包括 Cursor 和 Notion)的验证证明,这种特定的权衡策略价值巨大。

turbopuffer 随后演进,强化了文本与正则搜索能力(v2),并开始应用于多种非搜索场景,例如 Linear 的同步引擎。查询引擎一路升级以支持所有这些查询计划,但存储架构基本未变:ANN 向量索引始终是核心主索引,其他索引与查询计划均围绕它展开。这一设计限制了部分查询计划,如 GROUP BY 和聚合查询。

我们将“向量优先”架构推到了极限,现在该向前迈进。我们正在切换新的主索引,让 ANN 降级为“普通”的二级索引。我们觉得不妨打开大门,让大家见证这一过程。

在首期更新中,我们先交代初衷:为什么要做这件事?请跟我简短回顾一下从 tpuf v1 到今天的演变路径。

v1:仅含 ID 与向量

turbopuffer 首个版本中的文档仅由 ID 和向量组成。当时的主流观点是图式向量索引,但分层聚类索引更契合对象存储。我们从 SPANN 起步,随后迁移至 SPFresh 以支持增量索引。向量被聚类成分组,各组的质心再次聚类,循环往复,最终形成一棵以单根节点为顶点的树。


                      ┌───────────────────┐
                      │   root centroid   │
                      └───────────────────┘
                     ╱          │          ╲
                    ╱           │           ╲
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│   leaf centroid   │ │   leaf centroid   │ │   leaf centroid   │
└───────────────────┘ └───────────────────┘ └───────────────────┘
      ╱       ╲             ╱       ╲             ╱       ╲
     ╱         ╲           ╱         ╲           ╱         ╲
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ vector │ │ vector │ │ vector │ │ vector │ │ vector │ │ vector │
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘

      ┌───────────────┐
      │ root centroid │
      └───────────────┘
        ╱     │     ╲
       ╱      │      ╲
┌────────┐┌────────┐┌────────┐
│  leaf  ││  leaf  ││  leaf  │
│centroid││centroid││centroid│
└────────┘└────────┘└────────┘
   ╱  ╲      ╱  ╲      ╱  ╲
┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐
│vec││vec││vec││vec││vec││vec│
└───┘└───┘└───┘└───┘└───┘└───┘

我们在一个提供键值映射的存储层上实现了这套结构,键是有序且唯一的。每个集群分配一个 ClusterId,集群内的每个向量再分配一个紧凑的 LocalId。

// leaf vectors
K::Vector(C0L0) = vec![0.45, 0.32, ...]
K::Id(C0L0) = 7
K::Vector(C0L1) = vec![-0.28, 0.96, ...]
K::Id(C0L1) = 13

// cluster centroid for C0 is itself clustered at the next level of the tree
K::Vector(C1L4) = vec![0.64, -0.48, ...]
K::Id(C1L4) = C0

如上所示,所有内容都以 ClusterId 和 LocalId(例如 C0L1)作为键,二者合起来称为 ANN 地址。这就是我们说 ANN 索引即主索引的含义。

v2:属性过滤与全文搜索

两种新的查询计划标志着 turbopuffer 从 v1 到 v2 的非正式过渡:属性过滤和全文搜索。

属性过滤

客户自然希望添加属性值,并基于这些属性对向量搜索进行过滤。为了让过滤既快速又能保持高召回率(见 make filtering fast and high-recall),我们把它建模为倒排索引:将属性值映射到包含该值的文档的 ANN 地址。

K::AttrIndex("family", "Alcidae") -> vec![C0L3, C1L2, C1L3, ...]
K::AttrIndex("genus", "Fratercula") -> vec![C0L3, C1L2, C1L9, ...]

对于投影(include_attributes),我们将文档属性与 ID 和向量一并存储。

K::Vector(C0L0) = vec![0.45, 0.32, ...]
K::Id(C0L0) = 7
K::Attr(C0L0, "family") = "Alcidae"
K::Attr(C0L0, "genus") = "Fratercula"

全文搜索

BM25 全文搜索是另一个直观且被广泛需要的查询计划。与属性搜索类似,全文搜索首先查找包含查询词的文档(通常称为“倒排索引”或“postings”)。对于全文搜索(FTS)索引,我们还包含 BM25 打分所需的 (term count, document length) 元数据:

K::FTS("description", "Atlantic") -> vec![(C0L0, 2, 37), (C9L4, 1, 42), ...]
K::Attr(C0L0, "description") -> "A sharply dressed black-and-white seabird with a \
huge, multicolored bill, the Atlantic Puffin is often \
called the clown of the sea. It breeds in burrows on \
islands in the North Atlantic, and winters at sea."

随着时间推移,我们陆续发布了几种其他的索引结构和查询引擎: 聚合、 正则搜索、 模糊匹配、 稀疏向量搜索,以及 属性排序 — 所有这些都围绕同样的向量为主存储布局构建。

向量主索引的问题

近似最近邻(ANN)主索引直到今天基本保持不变,原因很简单:它在对象存储上的 ANN 搜索中表现极其出色。基于此架构,我们将向量搜索能力推进到 单一索引容纳超过 1000 亿个向量,支持 200 ms p99 读延迟和 1k+ QPS。 在此处进行任何重大更改都可能引发 ANN 性能回退。

然而,这种布局阻碍了我们成为支持的非向量查询形态中的最先进水平,主要体现在三个方面:存储放大、写入放大和有限的向量化。

存储放大

如前所述,turbopuffer 目前将每个文档的完整内容存放在其 ANN 地址之下。当只有一个向量时,非向量数据仅随该向量存储一次。

然而,对于文档的多向量表示形式(例如文档嵌套或晚期交互),这意味着我们需要为每个向量复制一份内容。这也是部分限制存在的原因。

写放大

每当文档被插入、更新或删除时,SPFresh 可能会重新平衡向量,以确保它们保持良好聚类(否则召回率会受损)。由于文档中的所有数据都以文档向量的 ANN 地址为键存储,这种重新平衡会级联导致完整文档内容移动,以及引用该内容的倒排索引(属性索引和全文搜索索引)。仅仅更新一个向量,就可能需要移动数百个属性及其对应的索引。

这种写放大效应相当显著,导致我们提升索引吞吐量的努力开始面临边际效益递减。

受限的向量化

现代查询引擎是向量化的:它们对数值块执行紧凑循环,从而分摊每个块的固定开销,获得更好的压缩率,保持 CPU 流水线满载,并启用 SIMD。例如,DuckDB 以 2,048 行为批次工作,ClickHouse 最高可达约 65k,Lucene 的倒排块包含 256 个文档,而我们的 ANN 索引在每簇约 100–200 个文档时表现最佳。每个查询计划都有最优块大小,但目前它们全受限于 ANN 主索引。即便计划希望使用数千个文档的大块来保持 CPU 饱和,仍卡在 100–200 的规模上。

我们在 turbopuffer 上已经充分记录了这个问题有多严重。我们的第一版全文检索把倒排表按 ANN 聚类边界切分,结果中位 block 只包含约 1.5 条 posting。FTS v2 将倒排表改造成约 256 条的固定 block,索引体积缩小了 10 倍,查询最快提升 20 倍。倒排表能做到这一点,是因为它独立存储、指向文档,布局不必跟随聚类。而聚合和其他扫描操作要读取文档本身,文档却是每个聚类存成一个 block。只要 ANN 地址还是主键,block 大小就被聚类大小限制住,哪怕它们更想要更大的 block。

再见,主向量索引

解决这些问题的办法很简单:别再用 ANN 地址做主键。这正是 turbopuffer v3 做出的改变。可以想象,这绝非易事。

v3 是一个新基础,将显著提升所有查询计划的性能。本月早些时候我们达成了一个重要里程碑:turbopuffer v3 的 CI 通过率达到 100%。我们先专注正确性,接下来要做到既正确又快。看着 benchmark 数字不断下降是一件乐事,所以我们想从性能优化的第一天起就带你一起见证。在 v3 上线生产环境之前,我们会朝着(乃至超越)性能对齐的目标前进,并在未来几周陆续公开 benchmark 数据。

原始来源: Hacker News

评论 (0)