图博弗(turbopuffer)正改变存储架构以提升搜索。v3 改变文档与索引的布局、写入、压缩和查询方式,使向量等搜索更快,并为迁移更多 SQL 查询打基础。图博弗(turbopuffer)起初作为无服务器向量数据库 v1 推出,专攻廉价快速向量搜索,以对象存储为源、分层 NVMe SSD/内存缓存提供性能,早期客户含光标(Cursor)与诺申(Notion)。
正文
RIP,向量数据库
2026 年 9 月 30 日 · Dan Harrison(工程师)
我们要改变 turbopuffer 的存储架构,把搜索推上一个新台阶。turbopuffer v3 改变了文档与索引在 turbopuffer 中的布局、写入、压缩和查询方式。它会让搜索在各个方面都更快,包括向量搜索,同时也为把更多 SQL 查询搬到 turbopuffer 上并让它们跑得很快打下基础。
turbopuffer 发布时是一个无服务器向量数据库(v1),高度专门化于以极低成本提供速度尚可的向量搜索。用对象存储作为真相来源带来经济性,用分层的 NVMe SSD/内存缓存带来性能。这一取舍的价值被最早的客户验证,包括 Cursor 和 Notion。
后来 turbopuffer 发展出很强的文本和正则搜索(v2),也被用在许多非搜索场景,比如 Linear 的同步引擎。查询引擎一路演进以支持这些查询计划,但存储架构基本没变:ANN 向量索引过去是、现在仍然是主索引,其他所有索引和查询计划都围着它转。这个设计限制了好几种查询计划,比如 GROUP BY 和聚合。
我们已经把向量主索引这套架构推到了极限,是时候往前走了。我们正在迁移到一个新的主索引,让 ANN 变成「又一个」二级索引。我们觉得打开大门、让你跟着看会挺有意思。
这第一篇更新,先把我们为什么这么做交代清楚。跟我走一段从 tpuf v1 到今天的小旅程。
v1:一个 ID 和一个向量
在 turbopuffer 最早的版本里,文档只有 ID 和向量两样东西。当时流行的做法是基于图的向量索引,但分层聚类索引跟对象存储配合得更好。我们先用 SPANN,后来迁移到 SPFresh 以支持增量索引。向量被聚成簇,簇的质心再被聚类,如此反复,形成一棵只有一个根节点的树:根质心下面分出叶子质心,每个叶子质心下挂一组向量。
这一切实现在一个表现为键值映射的存储层之上,键有序且唯一。每个簇分配一个 ClusterId,簇内的向量分配一个稠密的 LocalId。比如叶子向量 K::Vector(C0L0) = vec![0.45, 0.32, ...],对应的 ID 是 K::Id(C0L0) = 7;而簇 C0 的质心本身会在树的上一层被聚类,K::Vector(C1L4) = vec![0.64, -0.48, ...],其 ID 就是 C0。
可以看到,所有东西都以 ClusterId 和 LocalId(如 C0L1)为键,两者合起来我们称为 ANN 地址。这就是我们说 ANN 索引是主索引的意思。
v2:属性过滤与全文搜索
两种新的查询计划标志着 turbopuffer 从 v1 到 v2 的非正式过渡:属性过滤和全文搜索。
属性过滤
客户自然希望能在文档上加属性值,并据此过滤向量搜索。为了让过滤又快又高召回,我们把这些建模成一个倒排索引,把属性值映射到包含它的那些文档的 ANN 地址:
K::AttrIndex("family", "Alcidae") -> vec![C0L3, C1L2, C1L3, ...]
对于投影(include_attributes),我们把文档属性与 ID、向量存在一起,例如 K::Attr(C0L0, "genus") = "Fratercula"。
全文搜索
BM25 全文搜索是另一个显而易见、呼声很高的查询计划。与属性搜索类似,全文搜索先找出包含查询词的文档(通常称为 postings)。对 FTS 索引,我们还要存 BM25 打分所需的 (词频, 文档长度) 元数据:
K::FTS("description", "Atlantic") -> vec![(C0L0, 2, 37), (C9L4, 1, 42), ...]
此后我们还陆续上线了聚合、正则搜索、模糊匹配、稀疏向量搜索和按属性排序,全都建立在同一套向量主索引的存储布局之上。
向量主索引的问题
ANN 主索引到今天基本没动过,原因很简单:它在对象存储上的 ANN 搜索表现得非常非常好。基于这套架构,我们把向量搜索推到了单个索引 100B+ 向量、200 ms p99 读取、1k+ QPS。在这里做任何大改动,都有让 ANN 性能回退的风险。
但这个布局让我们在非向量查询形态上无法做到业界最好,主要有三方面:存储放大、写入放大,以及向量化受限。
存储放大。 turbopuffer 目前把每个文档的全部内容放在它的 ANN 地址下。只有一个向量时,非向量数据只跟着向量存一份。但对文档嵌套、late interaction 这类文档的多向量表示,我们就得为每个向量复制一遍内容。这也是我们某些比较难看的限制的来源。
写入放大。 只要文档被插入、更新或删除,SPFresh 就可能重平衡向量,以保证它们聚得够好(否则召回会下降)。因为文档里的一切都以该文档向量的 ANN 地址为键存储,这种重平衡会连带搬动整个文档内容,以及所有引用它的倒排(属性和 FTS)索引。更新一个向量,可能搬走数百个属性及其索引。这种写入放大大到我们调优索引吞吐已经开始收益递减。
向量化受限。 现代查询引擎是向量化的:在一批批值上跑紧凑循环,摊薄每块的固定开销,压缩率更好,CPU 流水线保持满载,还能用上 SIMD。比如 DuckDB 按 2,048 行一批,ClickHouse 最大约 65k,Lucene 的 posting 块是 256 个文档,而我们的 ANN 索引在每簇 100–200 个文档时表现最好。每种查询计划都有自己最优的块大小,但今天它们都被 ANN 主索引框住:一个想要几千个文档一块来喂饱 CPU 的计划,仍然卡在 100–200。
这件事的分量我们在 turbopuffer 里已经记录过。第一版全文搜索把 posting list 沿 ANN 簇边界分区,中位块只有约 1.5 条 posting。FTS v2 把 postings 重做成约 256 条一块的固定块,索引小了 10 倍,查询快了最多 20 倍。posting list 能做到这一点,是因为它单独存储、指向文档,布局不必跟着簇走。而聚合和其他扫描读取的是文档本身,文档是每簇一块存的。只要 ANN 地址还是主键,它们的块大小就被限制在簇大小,哪怕它们更想要更大的块。
RIP,主向量索引
这些问题的解法很简单:不要用 ANN 地址做键。这正是 turbopuffer v3 所做的改变。可想而知,这并非一个小改动。
v3 是一个新基础,将让所有查询计划获得显著的性能提升。这个月早些时候我们达成了一个重要里程碑:turbopuffer v3 上 100% 的 CI 通过。我们先专注正确性,接下来要让它既正确又快。看 benchmark 数字往下掉非常有趣,所以我们想让你从性能打磨的第零天就参与进来。未来几周我们会公开分享 benchmark,朝着性能追平(并超越)努力,然后再把 v3 推向生产环境。
turbopuffer 是一个快速搜索引擎,托管着 1T+ 文档,承担 10M+ 写入/秒,服务 25k+ 查询/秒。我们准备好了承接远多于此的负载。
评论
讨论围绕一篇剖析「向量主索引」问题的文章展开:多数人认可把 ANN 降级为指向文档 ID 的二级索引这一方向,并把它读成 Postgres 与 MySQL 索引设计史的重演;分歧主要在于该不该干脆放弃独立的向量数据库、用带原生向量支持的 SQL 取而代之,以及对高性能场景下真实数据缺失的追问。少数人则把话题推向「数据库是否会被神经网络或专用编译版本取代」的更大想象。
认为这是 Postgres 与 MySQL 索引设计之争的重演
- 旧设计让倒排/索引指向数据的物理位置(也就是 ANN 槽位),于是一次再平衡就要重写所有触及该文档的索引;InnoDB 的解法是让二级索引指向主键,读取时多付一次间接寻址,把成本从写挪到读。(较多人)
- 更准确地说,Postgres 的索引指向行 ID,行更新后该值会变;MySQL 假设存储引擎可插拔,索引指向稳定的主索引项。前者为查找优化,后者为写入时的建索引优化,代价是读多一跳。(较多人)
- 这也和 Uber 当年从 Postgres 迁到 MySQL 以抑制索引写放大的案例互为镜像,解法相同:别再让索引指向数据所在的位置。(较多人)
- 由此引出的实际疑问是:这次改动之后,新的主索引究竟是什么;以及如果这一跳从 B 树节点变成一次 S3 GET,成本会有多高。(较多人) > 把索引指向行所在的位置,正是这次改动要纠正的事情,但它远不是一次轻松的改动。(@gopalv)
认为纯向量数据库在企业检索中本就是错误抽象
- 企业检索真正需要的是向量相似度与全文检索、过滤、连接、排序、关系谓词共处一个查询里;多租户隔离、ACL、文档版本、时间过滤都应该是普通谓词,而不是事后硬贴到向量库上的补丁。(较多人)
- 把向量能力建在已有 SQL 数据库之上,可以少一个活动部件,也免去数据更新时同步两套系统这一最容易出错的环节。(较多人)
- 从命名角度看,「向量数据库」这个词本来就偏了:它卖的是检索而不是向量或存储,只是这个词太成功,公司抱着它太久了。(少数人) > 我们早该承认,这个说法从来讲的都是检索,而不是向量或存储,只是它粘得太牢了。(@gk1)
认为把 ANN 当二级索引早有实现,并非新问题
- 有人自称在做同类系统时就得出了相同结论,并强调把稠密/稀疏向量、后期交互、词法检索、正则索引、过滤与自定义打分放进同一个查询,是可以从零做出来的。(少数人)
- LanceDB 采用同样的思路:行存放在 fragment 中,向量索引不会移动这些行,这一点和文章描述的新设计一致,而且它是开源的。(少数人) > 行留在 fragment 里,向量索引从不搬动它们,这跟文章里说的做法是一回事。(@Tsarp)
猜测数据库会被神经网络或专用编译版本取代
- 有人设想干脆取消传统数据库,让数据库本身就是一个自编码器式的神经网络。(少数人)
- 也有人设想用 Rust 直接实现所需 API、去掉动态开销的「为 LLM 优化过的编译版数据库」,并认为它最终会扩展到整个操作系统,但数据库是最好的起点。(少数人)
质疑缺少同规模下的性能数据
- 希望看到在同等规模、每秒上千次查询下的 p99 延迟表现,而不是只有设计层面的论证。(少数人)
单纯调侃与情绪表达
- 一条「AI 垃圾,不读」的短评,反映了部分人对当前 AI 相关内容的不耐烦;还有人打趣等着 Qdrant 的 CEO 下场应战。(少数人)
有价值的补充
- 冷启动路径值得追问:如果集群各自保留一份向量副本,搜索本身可以保持本地,只有结果取回要付间接寻址的代价;否则冷 p99 很可能会变差。
- 写放大确实已经到瓶颈:当更新一个向量会牵动数百个属性及其索引时,继续调优索引吞吐的收益会递减。
- NIMBLE 文件格式中与宽表相关的一些设计怪癖,和这次讨论的思路有相通之处,可作为参考。
- 文章刚发布时页面一度打不开,后来才恢复,提醒服务器负载对首发内容的影响是真实的。