四十八個德瑞克

RIP,向量資料庫?——turbopuffer v3 把 ANN 從主索引降級為次級索引

抽象樹狀聚類索引結構:根節點、葉節點質心與向量方塊,深色藍綠漸層

譯註:turbopuffer 宣布儲存架構大改版 v3:不再以 ANN 向量位址當主鍵,向量索引降級為「其中一個次級索引」。以下是全文翻譯,以及 Hacker News 討論串的回饋整理。

全文翻譯

RIP, vector database

2026 年 9 月 30 日 • Dan Harrison(工程師)

原文:https://turbopuffer.com/blog/rip-vector-database

我們正在改變 turbopuffer 的儲存架構。新引擎非正式稱為「turbopuffer v3」,改變了文件與索引在 turbopuffer 中的佈局、寫入、壓實(compaction)與查詢方式。它將讓我們能支援更多查詢計畫,並在更大的規模上提供服務。

turbopuffer 以 serverless 向量資料庫之姿誕生,高度專精於以極低成本提供合理快速的向量搜尋。以物件儲存(object storage)作為 source of truth 換來了經濟性,分層的 NVMe SSD/記憶體快取換來了效能。這些特定權衡的價值最早被我們的客戶驗證,包括 Cursor 和 Notion。

隨著時間推進,turbopuffer 已演變成一個通用的搜尋資料庫(以及其他用途)。查詢引擎一路演化以支援這些查詢計畫,但儲存架構大致未變:ANN 向量索引過去是、現在仍是主索引(primary index),所有其他索引與查詢計畫都圍繞它運作。

我們已把主 ANN 索引推到了極限,但該往前走了。我們正在改用新的主索引,並讓 ANN 成為「其中一個」次級索引(secondary index)。我們覺得打開大門、讓大家一路追蹤這個過程應該會很有趣。

在這第一篇更新中,我們先交代為什麼要這麼做。跟我走一段從 tpuf v1 到今天的短旅程。

第一版:文件只有 ID 和向量

在 turbopuffer 第一版中,文件只由一個 ID 和一個向量組成。當時的主流看法是圖式(graph-based)向量索引,但層級式聚類索引與物件儲存更合得來。我們從 SPANN 開始,最終遷移到 SPFresh 以支援增量索引。向量被聚類成群組,群組的質心(centroid)再被聚類,重複此過程形成一顆單一根節點的樹。


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

我們把它實作在一個呈現為鍵值對映(key-value map)、鍵有序且唯一的儲存層之上。每個聚類給一個 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

// C0 的聚類質心本身在樹的下一層被聚類
K::Vector(C1L4) = vec![0.64, -0.48, ...]
K::Id(C1L4) = C0

如上所示,所有東西都以 ClusterId 和 LocalId(例如 C0L1)作鍵,兩者合稱 ANN 位址(ANN address)。這就是我們說「ANN 索引是主索引」的意思。

v2:屬性過濾與全文搜尋

兩種新查詢計畫標誌著 turbopuffer v1 → v2 的非正式轉變:屬性過濾與全文搜尋。

屬性過濾。 自然地,客戶希望能加入屬性值,並以此過濾向量搜尋。為了讓過濾既快又有高召回率,我們將其建模為倒排索引(inverted index),把屬性值映射到包含它的文件之 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 計分所需的(詞項數、文件長度)中繼資料:


K::FTS("description", "Atlantic") -> vec![(C0L0, 2, 37), (C9L4, 1, 42), ...]
K::Attr(C0L0, "description") -> "A sharply dressed black-and-white seabird ..."

隨著時間,我們還推出了數種其他索引結構與查詢引擎:聚合(aggregations)、正規表示式搜尋、模糊比對、稀疏向量搜尋、屬性排序——全部建立在同一套以向量為主的儲存佈局之上。

以向量為主索引的問題

ANN 主索引直到今天大致維持原樣,原因只有一個:它在物件儲存上的 ANN 搜尋表現真的、真的很好。在這套架構之上,我們把向量搜尋推到單一索引 1000 億以上向量、以 1k+ QPS 提供 200 ms p99 讀取。在這裡做任何重大變更都可能引入 ANN 效能回歸。

然而,這個佈局讓我們在非向量查詢形狀上無法達到最先進水準,主要有三個面向:

儲存放大。 turbopuffer 目前把每份文件的完整內容放在其 ANN 位址之下。只有一個向量時,非向量資料與向量一起存放一次即可。但對文件的多向量表示(multi-vector representations),例如文件巢狀(document nesting)或延遲互動(late interaction),就必須為每個向量複製一份內容。這就是我們某些較遺憾的限制的來源。

寫入放大。 任何時候文件被插入、更新或刪除,SPFresh 可能會重新平衡向量以確保它們維持良好聚類(否則召回率會下降)。因為文件中的一切都以其向量的 ANN 位址作鍵存放,這次重新平衡會連動搬移完整的文件內容,以及引用它的所有倒排(屬性與 FTS)索引。只更新一個向量,就可能搬動數百個屬性及其索引。這種寫入放大嚴重到我們調整索引吞吐量的努力已開始出現邊際效益遞減。

向量化受限。 現代查詢引擎是向量化(vectorized)的:它們對數值區塊跑緊區塊跑緊湊迴圈,攤平每個區塊的固定成本、壓縮效果更好、讓 CPU 流水線保持滿載,並解鎖 SIMD。例如 DuckDB 以 2,048 列為一批,ClickHouse 最多約 65k,Lucene 的 posting 區塊是 128 份文件,而我們的 ANN 索引在約 100–200 份文件的群組上運作最佳。每個查詢計畫都有其最佳區塊大小,但今天它們全被 ANN 主索引綁架了。一個想要數千份文件的區塊來讓 CPU 飽和的計畫,仍然被困在 100–200。

我們已經記錄過這在 turbopuffer 中有多重要。第一版全文搜尋沿 ANN 聚類邊界分割 posting list,中位區塊只容納約 1.5 個 postings。FTS v2 把 postings 改造成約 256 的固定區塊後,索引縮小 10 倍、查詢最快加快 20 倍。Posting list 做得到,是因為它們單獨存放、指向文件,其佈局不必跟隨聚類。而聚合與其他掃描直接讀取文件本身,文件是一個聚類一個區塊存放——只要 ANN 位址仍是主鍵,它們的區塊大小就受聚類大小限制,即使它們想要更大。

RIP,主向量索引

這些問題的解法很簡單:不要以 ANN 位址作鍵。這正是 turbopuffer v3 所做的改變。可以想見,這不是個平凡的改動。

我們最近達成一個重大里程碑:turbopuffer v3 的 CI 100% 通過。但目前它相較生產版 turbopuffer 有顯著的效能回歸。這不令人意外!到目前為止,我們專注在基礎設計與正確性,還沒有開始調優。看著數字往下掉很有趣,所以我們想讓大家在效能打磨的第零天就進場。

我們會隨著基準測試的進展發布結果。沿途我們會深入新架構的細節,以及最終會讓 turbopuffer 在更多查詢計畫上更快的優化。


Hacker News 回饋整理

(297 分、79 則留言,討論串:https://news.ycombinator.com/item?id=49923466)

1. 最高讚的類比:這就是 Postgres vs MySQL/InnoDB 的老論戰

gopalv(首讚)指出,這與 Postgres/MySQL 如何建立索引是直接平行的:設計選擇從「Postgres 模式」變成「MySQL 模式」——差別在於 reindex 成本 vs lookup 成本。MySQL 索引指向穩定的 primary key,所以只要不改主鍵,就不用更新所有屬性索引;Postgres 索引指向每次更新都會變的 row-id。他並點名 Uber 2016 年從 Postgres 遷移到 MySQL 以避免索引放大的經典文章,說它就是這篇文章的鏡像。

blakeashleyjr 作了最精準的一句總結:「『更新一個向量就搬動數百個屬性及其索引』基本上就是 Uber 2016 寫入放大文章的搜尋版,解法也一樣:別把索引指向資料列所在的位置。」但他也提出技術追問:當多一次的 lookup 是 S3 GET 而不是 B-tree 跳躍時代價多大?cluster 是否保留向量副本讓搜尋本身仍本地化、只有結果抓取付出間接成本?否則冷資料的 p99 恐怕會變差。

反方意見:

2. 「新的主索引是什麼?」——官方親自回答

3. 「你不是第一個這麼做的人」——同方向的既有方案

4. 爭議:這文章是不是 AI 寫的?

5. 發散討論:要不要乾脆「殺死資料庫」?

6. 對效能宣稱的驗證要求

7. 離題但有趣的插曲

總體調性

技術討論的主軸是正面且認同問題診斷的——最多人共識是「這正是 Uber 2016/InnoDB 解過的經典題,turbopuffer 現在才走到這一步」;質疑集中在三處:新索引的具體設計(官方已回答)、S3 上多一次 indirection 的真實代價、效能宣稱待基準測試驗證。另有一條平行的「是不是 AI 寫的」文風爭論(作者已否認),以及大量離題的 LLM 留言機器人與「手刻資料庫」發散討論。


(轉譯自 Turbopuffer 2026-09-30 部落格文章〈RIP, vector database〉,原文:https://turbopuffer.com/blog/rip-vector-database 。網友回饋整理自 Hacker News 討論串:https://news.ycombinator.com/item?id=49923466 ,資料截至 2026-10-02。)

Exit mobile version