2023 年 11 月 Notion AI Q&A 上線那一刻,數百萬 workspace 同時排上了候補名單;負責後端向量搜尋的小組買進了一批 dedicated pod,storage 與 compute 綁在同一台機器,按「資料庫開機時間」計費。兩年之後,這些 pod 全部消失,取而代之的是 Turbopuffer 的 object-storage namespace、DynamoDB 裡的 xxHash64、以及一條跑在 Ray Serve 上的 embedding 服務——總成本砍掉九成、p50 query latency 從 100 ms 壓到 50 ms。
Notion 向量搜尋的兩年——pod、Turbopuffer、Ray Serve
這篇是 Notion 向量搜尋兩年的演進——四個階段,每一段都對應一條成本曲線或一個架構瓶頸,沒有 big-bang rewrite。組件本身並不稀奇(Spark、Kafka、Turbopuffer、Ray、DynamoDB 都是現成的),值得記下的是每一次切換背後的判斷:什麼條件下,「換掉這一塊」比「再優化原本那塊」便宜。
四段的順序不是任意的:每段都依賴前段提供的可量測信號。pod → serverless 拆掉的是 over-provision 固定成本,順帶把容量成本從 dedicated SSD 變成 object storage;後者一旦切到,下一段才有條件再砍重複工作(hash diff 的儲存建在 DynamoDB 上、跟 vector storage 解耦)。下面這個 scroll-driven walkthrough 把時間線攤開——每一格的重點不是「畫了哪些 box」,而是「這一段砍掉了什麼計費邊界」。
Stage 1 · 上線即爆量
storage 與 compute 綁在同一台 pod;workspace-ID 範圍分片、靠 generation 路由抗擴容;按開機時間計費,所以 over-provision 是必繳費用。
這一段付的:headroom 的固定成本。
Stage 2 · 砍 over-provision
同廠商內把 storage 與 compute 解耦,計費從「開機時間」改成「實際 query 與儲存用量」。對波動明顯的流量立刻省下 50% 峰值成本,但年費仍以百萬美元計。
這一段砍的:恆定 provisioning 的固定成本。
Stage 3 · 換引擎、拆路由
vector index 直接建在 object storage 上;workspace = namespace,application 端不再分片、不再維護 generation 路由。搜尋成本 −60%、EMR −35%、p50 70-100ms → 50-70ms。
這一段砍的:dedicated SSD 容量的結構性成本。
Stage 4 · 砍重複工作 + 收 GPU
Page State + xxHash64 把 ingestion 工作量砍 70%;embedding 服務從外部 API + 內部 GPU pod 整合到 Ray Serve,preprocessing 與 inference 共用機器,embedding infra −90%。
這一段砍的:沒變內容仍要 re-embed 的隱性成本 + 外部 API 的 per-token 邊界。
互動圖表
四階段:pod→serverless→Turbopuffer→xxHash+Ray;onboarding ×600、p50 −50 ms、成本 −90%。
2023 年的 dedicated pod——storage 與 compute 為什麼綁在一起就撐不住
第一版架構在 2023 年 11 月 Notion AI Q&A 上線那天就跑起來了。兩條 ingestion path 同時餵向量資料庫:offline 那條由 Apache Spark 跑批次、對既有頁面做 chunking、呼叫 embedding API、bulk-load 到向量索引;online 那條由 Kafka consumer 處理即時編輯,端到端延遲控制在 sub-minute。Dual-path 的設計後來成為所有引擎切換的 silent enabler——新引擎可以掛在同一條 Kafka 後面雙寫,query path 用 feature flag 切讀。
痛點在「向量資料庫長什麼樣」。當時用的是 dedicated pod——每個 pod 是一整台機器,storage 跟 compute 綁在一起。Notion 把 workspace ID 當分片 key、用 range partitioning 散到不同 pod 群,這個設計刻意模仿了既有 Postgres 的分片邏輯,讓 oncall 對「workspace 在哪台機器」的回答跨服務一致。
為了應付容量擴張,他們做了一個 generation-based 路由:一組 index 接近容量上限時,provision 新 generation 的 index、把新 workspace onboarding 導向新 generation,舊資料不動。不去 reshard 是因為在 pod 模型下要麼 pause 寫入要麼承擔 race condition,對「不能停 onboarding」是不可接受的。這個 generation 思維後來被沿用到 Turbopuffer 搬遷——把 cohort 維度埋進系統設計的早期,後面所有大遷移都會更便宜。
問題出在計費:Pod 廠商按「資料庫開機時間」計費,不是按實際使用量——預留出來不管用不用,帳單持續累積。Notion AI 上線一個月後 index 就接近容量,要保留 headroom 就得長期 over-provision;白天高峰、夜間低谷的業務波形跟「按開機時間付費」完全反向,沒辦法在低谷把錢省回來。同時多代共存讓 oncall 變成「先看 workspace 在哪一代 → 看那一代的健康 → 看那台 pod 的狀態」,alert 還得逐代調 threshold,配置文件單調膨脹。
// 2023 年的查詢路徑——pod model 下的路由
def search(workspace_id, query_text):
generation = lookup_generation(workspace_id) // 哪一代
pod_id = range_shard(workspace_id, generation) // 哪台 pod
query_vec = embed(query_text) // 走外部 API
return pods[pod_id].ann_search(query_vec, top_k=20)
// generation 滿了:
// 不 reshard 既有資料,而是 provision 新的 index set、
// 把新 workspace 都導向 generation N+1。
// 代價:路由表越來越胖、跨 generation reindex 是噩夢。
2023 年底到 2024 年 4 月之間,這套架構靠著「持續加 pod、持續調 generation」撐住業務擴張。具體數字:daily onboarding 容量提升 600 倍(從每天數百個 workspace 到單日數十萬量級),active workspace 15 倍、vector DB 容量 8 倍,4 月時把累積的百萬等級候補清空。但帳單也以類似節奏在增加——候補清空提供了一個自然的休息窗口,問題從「能不能跑」切到「跑得起多久」。
# Pod model 下的「能 scale 但不能省」
# 假設一個 workspace 平均使用率 30%,pod 在 50% headroom 下 provision
# → 實際付費容量 = 1.5x 預期峰值 ≈ 5x 平均實際使用
# → 每多 1 個 workspace,固定成本是平均使用的 ~5 倍
# → 沒有空間用「夜間關機」省錢,因為 pod 是恆定 provisioned
storage: bound to pod, paid by uptime
compute: bound to pod, paid by uptime
scaling: add pods → add fixed cost
shrink: ~impossible (data lives on the pod)
2024 年 serverless 化:解耦 + 換 Turbopuffer,砍 60% 搜尋成本
2024 年 5 月做的第一個動作是同一家廠商內部的方案切換——從 dedicated pod 改成 serverless 模式。技術差別只有一個關鍵字:storage 與 compute 解耦,計費從「資料庫開機時間」變成「實際 query 與儲存用量」。對 Notion 這種「白天高峰、夜間低谷、跨時區但仍有明顯波形」的查詢流量,serverless 立刻把峰值的 over-provision 浪費省掉,整體成本相對 pod 模式降了 50%。容量限制也消失了——不需要再為「快滿了」provision 新 generation,理論上 generation 路由表可以開始凍結。
這個切換本身的工程量並不小(要驗證 query latency 沒有 regression、要設計 traffic 遷移節奏),但比起後面要做的 Turbopuffer 遷移仍算「同一家廠商內」的局部優化。Notion 的描述是「省下幾百萬美元」——對單一基礎設施項目來說已經是顯著金額,但仍不滿足成本目標。
但 50% 不夠。Serverless 之後年度跑率仍然是「以百萬美元為單位」,向量資料庫單一項目就佔了很大一塊預算。同時 Notion 工程團隊系統性地評估了當時市場上的其他向量搜尋引擎——這個評估的關鍵候選是 Turbopuffer,一個把整個 vector index 建在 object storage 之上的新進向量資料庫。
選 Turbopuffer 的判斷有三層:
- 成本結構——object storage 比 dedicated SSD 容量便宜一個數量級,對「冷資料佔比高、熱查詢集中」的工作量友善。Notion 的 workspace 分佈長尾很長:少數活躍 workspace 貢獻多數查詢,大量低活躍 workspace 的資料只是偶爾被讀。把這些長尾 namespace 放在 object storage 上,每 GB 月費可以從 dedicated SSD 的「以 cents 計」降到「以 fractions of cents 計」。
-
架構簡化——Turbopuffer 把每個 namespace 視為獨立 index,不需要 application 層維護分片表,workspace-ID 直接對應一個 namespace,原本的 range partitioning + generation 路由整套可以拆掉。對 application 程式碼來說,「我要查 workspace X」變成
turbopuffer.namespace(workspace_id).query(...),路由邏輯從 application 完全消失。 - 批次寫入支援——對 Notion 這種「會做大批量 re-indexing」的工作量提供了相對便宜的 bulk modification API,讓「換 embedding model 重 embed 全站」這種動作的成本可控。
第二層其實是最隱性也最大筆的回報——把 application 程式碼裡的「路由邏輯」整層刪掉,意味著後續所有 retrieval feature(BM25 hybrid、metadata filter)都不再需要繞 generation router。一次性 SDK 遷移成本 vs 長期累積的 abstraction tax,多數情況前者勝。下面這張 chart 把四階段成本與 p50 latency 一起畫——兩條曲線同時往好的方向走,這在基礎設施重構裡並不常見。
Notion 向量搜尋兩年——相對 vector DB 成本(左軸,以 2023.11 pod 為 100%)與 p5…
成本 100%→10%、p50 100 ms→50 ms,兩條曲線同步改善,重構不是成本與速度的取捨。
舊架構需要 application 維護分片表 + generation 路由 + 兩座 pod 群;新架構讓 Tur…
切換 Turbopuffer 後刪掉 application routing layer,搜尋成本 −60%、EMR −35%。
2024 年 5 月到 2025 年 1 月,Notion 用約八個月把多達數十億筆 vector object 從 pod 模式搬到 Turbopuffer。搬遷策略沿用了原本就在用的 generation-based 思路——不去 reshard 既有 generation,而是把新 workspace cohort 導向「新一代」的 Turbopuffer namespace,逐代切換、逐代驗證;每搬完一代就可以把對應的 pod cluster 退役。這個策略最大的好處是避開了「為了搬遷而 pause onboarding」的風險——onboarding 流量在整個搬遷期間沒有中斷。
搬遷的安全網來自 Notion 早期就一直在做的雙寫實踐:在切換的中段,新舊兩個引擎同時收寫入、應用層的讀取路徑可以按 feature flag 切到任一邊;當對應 cohort 的「新引擎讀寫驗證」連續一段時間 zero diff,就把這 cohort 的讀路徑切過去、舊引擎進入唯讀狀態;唯讀一段時間沒有客訴或差異後,舊引擎這份 cohort 的資料才會真的被刪掉。這個模式對任何「資料層引擎更換」都適用——關鍵不是技術細節,而是把「驗證 → 切讀 → 退役」三段顯式化,每段都有可以 rollback 的視窗。
同時做了兩件附帶優化:(a)藉著 full re-indexing 的機會把 embedding model 換成更新版本,後者在召回率與語意理解上都更好;(b)把 Spark offline pipeline 的 throughput 重新調過,配合 Airflow 的 scheduling 讓白天/夜間的 onboarding 容量可以靈活伸縮。「順手換 model」這個動作在獨立做的時候是個 painful 的全站重 embed 工程,但既然遷移本身就要全資料量過一遍 Spark + embedding pipeline,這個成本被 amortize 掉了——好的遷移時機是「順便」做其他原本要單獨付成本的事。
完成後的數字:搜尋引擎成本 −60%、AWS EMR compute −35%、p50 query latency 從 70-100 ms 降到 50-70 ms。latency 改善的一半來自引擎本身、另一半來自路由層消失(application 不需要做 generation lookup → shard lookup → pod address resolution 這一連串 lookup)——對 tail latency 來說後者價值通常更大。EMR −35% 是順手的紅利:原本分片路由讓 Spark 按 generation 切批次、每代獨立啟動成本;切到 namespace 模型後 Spark 直接以 workspace 為單位 parallel,啟動成本均攤掉。同時 full re-indexing 的門檻也下降,「想換新 embedding model」這種實驗變便宜。
2025 年 Page State Project:xxHash64 把 re-embed 量砍 70%
2025 年 7 月做的第一件事是 Page State Project——這個專案處理一個原本一直被忽略的浪費:頁面只要被編輯,無論改動是「整段重寫」還是「一個字元變動」,原本的 ingestion pipeline 都會把整頁重新 chunk、重新 embed、重新上傳。對 Notion 這種高編輯頻率的產品,這個語意意味著大量的「重複工作」——同一個 paragraph 因為旁邊的 typo 修正而被反覆 re-embed。
這個浪費為什麼能存在這麼久?因為它在帳單上不會自己跳出來——embedding API 按 token 計費,月底拿到總量,沒有任何維度告訴你「其中有多少 token 對應的內容其實沒變」。要看出這個浪費,必須在 application 層先建立可以跨時間 dedupe 的 span ID,後續的 hash diff、Merkle tree、CDC 模式才能落地。
解法的概念很直接:對每個 span(chunked 之後的文字單位)算兩個 hash——一個 hash text 內容、一個 hash metadata(權限、作者、頁面屬性等)——把這對 hash 與 span ID 一起存進 DynamoDB,一個頁面一筆 record、record 內含這個頁面所有 span 的 hash struct。下次同一頁變更時,先把新版的所有 span 算一次 hash,跟 DynamoDB 裡的舊版逐 span 對:
// Page State Project:用 xxHash64 判斷哪些 span 真的變了
def reindex_page(page_id, new_spans):
old_state = dynamodb.get(page_id) // span_id -> (text_hash, meta_hash)
new_state = {}
to_embed = []
to_patch = []
for span in new_spans:
new_th = xxhash64(span.text)
new_mh = xxhash64(span.metadata)
new_state[span.id] = (new_th, new_mh)
if span.id not in old_state:
to_embed.append(span) // 新 span:完整 embed
else:
old_th, old_mh = old_state[span.id]
if old_th != new_th:
to_embed.append(span) // 內容變了:re-embed
elif old_mh != new_mh:
to_patch.append(span) // 只有 metadata 變:PATCH,不重 embed
// 兩者都相同:什麼都不做
dynamodb.put(page_id, new_state)
if to_embed: embed_and_upsert(to_embed)
if to_patch: turbopuffer.patch_metadata(to_patch)
這個邏輯把 ingestion 工作拆成三個 case:完全沒變的 span 不動;只有 metadata 變了的 span(例如權限調整、作者改名)走 PATCH 路徑、直接更新 Turbopuffer 裡的 metadata 而不重新呼叫 embedding API;真正內容變動的 span 才走完整 chunk → embed → upsert 流程。在 Notion 的實際編輯分佈下,這個拆分把 ingestion pipeline 的資料量砍了 70%——同時省 embedding API 費用與 Turbopuffer 寫入費用,這兩塊都是按用量計費。
metadata-only PATCH 那條特別關鍵:權限是頁面層級屬性,所有 span 共享同一個 metadata,舊版 pipeline 動一次牽動全頁 re-embed。企業場景「把某文件分享給新部門」很頻繁,把 text 與 metadata 拆成兩個獨立 hash 後,這條昂貴路徑變成廉價 PATCH。為什麼選 xxHash64 而非 SHA-256 或 32-bit hash?這個 hash 不是用於 cryptographic 安全(沒人會刻意造 collision),純粹是 change detection——這個前提下 xxHash64 就是甜蜜點。下面這張表把取捨並排起來,點 header 可以排序。
| hash | size (bytes) | throughput (GB/s) | collision @ 1e9 | 適用情境 |
|---|---|---|---|---|
| xxHash64 | 8 | 7.5 | 可忽略 | change detection(Notion 選這個) |
| SHA-256 | 32 | 0.4 | cryptographically negligible | cryptographic identity / signing |
| xxHash32 | 4 | 7.5 | 可觀察(~10%) | small key dedupe, 不要在 1e9 規模上用 |
| MD5 | 16 | 0.7 | cryptographically broken | legacy compatibility(沒有新理由選) |
| CRC32 | 4 | 12 | 不適用 | 傳輸層 checksum,不是 content fingerprint |
| BLAKE3 | 32 | 3.5 | cryptographically negligible | 需要密碼學安全且 SHA-256 太慢的情境 |
Hash 演算法取捨表——點欄位 header 排序
xxHash64:8 bytes、7.5 GB/s、10^9 筆碰撞可忽略;SHA-256 僅 0.4 GB/s,非加密場景過重。
關鍵欄位是「適用情境」——選 hash 不是看哪個「最強」,而是看哪個對應你的 threat model。Change detection 的 threat 是「兩個不同 span 偶然算出同一個 hash」,這個機率在 64-bit + 1e9 scale 上遠低於 disk error rate;沒理由再多付 SHA-256 那 20x CPU 與 4x storage。反過來如果攻擊者可以選擇 input(例如 build cache 要抗 supply chain attack),那 xxHash64 不夠,BLAKE3 才是甜蜜點——hash 選擇是個三維問題(速度、size、threat model),不是一維比較。
Ray Serve 收尾——拆掉 GPU pod 的雙重計算
同個月(2025 年 7 月)做的第二件事是 embedding 服務的整個搬家:從原本掛在外部 embedding API 與內部 GPU pod 上的兩條路徑,整個搬到 Ray / Anyscale 上跑。要解的痛點有三個:
- 「雙重計算」問題——舊架構下,Spark 做 preprocessing(chunking、normalization、metadata 抽取),完成後把結果丟給 embedding API,又被計算一次 token。同樣的 token stream 在不同層被算兩次,這是純粹的浪費。
- 外部 API 可靠性綁定——embedding API 的延遲、rate limit、故障時間,直接決定 ingestion pipeline 與 query path 的可用性,沒有降級路徑。Embedding model provider 一旦發 incident,Notion 的 ingestion 就會 backlog;reactor 沒有「先用本地 fallback model」的選項,因為架構上根本沒有本地 model。
- 難看的 pipelining——為了配合 rate limit,工程師得寫一堆 S3 batching 跟 retry 邏輯把資料分批送過去,這些 batch 是手寫的、不容易調整。每次想換 batch size、加 retry policy,都得改 Spark job 的 partitioning,跟 embedding pipeline 的物理特性綁太緊。
Ray 的賣點對應這三點:
- Ray 把 preprocessing 與 inference 放在同一個 compute layer,GPU-bound 的 embedding inference 跟 CPU-bound 的 preprocessing 可以在同一台機器上 pipeline 起來,把 GPU 利用率推高同時消掉 double compute。Notion 引用的措辭是「pipelining GPU-bound inference with CPU-bound preprocessing on the same machines, keeping utilization high」——這句話的具體含義是:當 GPU 在算 batch N 的 embedding 時,同一台機器的 CPU 正在 chunk batch N+1、normalize batch N+2,三件事 overlap 進行,GPU 不會空等資料。
- Embedding model 改成在 Ray 上跑開源模型,不再受外部 API gate。Notion 在文章裡強調的 model flexibility 包括「想試新 model 不需要等 provider 開放」、「可以用 fine-tune 過的內部模型」、「可以隨時 fallback 到 cheaper model」——這些自由度在綁外部 API 時都不存在。
-
Ray Serve把模型包成持久 GPU-resident 的 deployment,配置用 YAML 描述 batching、replication、autoscaling,model serving 是普通 Python——對 query path 來說,embedding 從「外部 HTTP 呼叫」變成「同 cluster 內的 RPC」,把第三方 API 的網路 hop 從 critical path 上拿掉。一次外部 HTTP 來回在跨區情境通常是 20-50 ms 起跳;同 cluster RPC 通常 1-5 ms。對 query path 的 p50 50 ms 預算來說,這是值得砍的一塊。
下面這個架構圖把 2025.07 之後的最終 stack 攤開來——點選任一個元件可以看它的責任與「它不知道的東西」。每個元件的責任邊界都是有意識劃出來的:xxHash64 不知道 span 的語意(它只看 byte stream)、Turbopuffer 不知道 workspace 的 ACL(namespace 只是字串)、Ray Serve 不知道 vector 之後會被誰查(它只 embed 然後回傳)。這種「明確的不知道」是組件可以被獨立替換的前提。
Kafka · 責任
承接所有 page edit 事件,提供 at-least-once delivery、partition 化的事件流; 是 ingestion 「online」路徑的唯一入口。
不知道:事件的內容語意、哪些事件需要 re-embed、哪些只要 metadata patch。 後面的 xxHash64 diff 才能回答這個問題。
Spark · 責任
處理 bootstrapping 與 bulk re-indexing——把整個 workspace 的內容 chunk、 normalize、走後面的 hash diff + embedding 流程。
不知道:online edit 怎麼走(那是 Kafka 路徑)、單個 span 是否需要 re-embed (由 xxHash64 diff 決定)。Spark 純粹是 throughput 引擎。
xxHash64 diff + DynamoDB · 責任
給每個 span 算 text hash + metadata hash、查 DynamoDB 裡的舊版、輸出三種 決策:skip / metadata-only PATCH / full re-embed。把「重複工作」這個浪費類別從整條 pipeline 拿掉。
不知道:span 的語意內容、哪個 embedding model 在用、最終結果寫到哪個 Turbopuffer namespace。它只看 byte stream。
Ray Serve · 責任
把 embedding model 包成 GPU-resident 的常駐 deployment,提供 dynamic batching、autoscaling;CPU preprocessing 與 GPU inference 在同一機 pipeline。把外部 embedding API 與內部 GPU pod 兩條路一起收掉。
不知道:哪些 span 應該被 embed(diff 已經 filter 過)、結果要寫到哪。 它只接 token batch、回傳 vector。
Turbopuffer · 責任
vector index on object storage;每個 workspace 對應一個 namespace; ANN search、metadata filter、bulk modify 都在 namespace scope 內。
不知道:workspace 的 ACL(namespace 只是字串)、誰會查 query、上游用了哪個 embedding model。它只認 vector + metadata。
Query path · 責任
把使用者查詢字串送進 Ray Serve embed → 拿 query vector → 對應 workspace 的 Turbopuffer namespace ANN search → 回傳 top-K。p50 50 ms。
不知道:vector 是怎麼產生的、什麼時候更新的、underlying storage 是 SSD 還是 object storage。對 query path 來說,這些都是封裝下面的細節。
點擊任一元件查看細節。資訊邊界與責任邊界對齊——每個 box 都有它「明確不知道」的事,這是可以被獨立替換的前提。
互動圖表
2025.07 架構:Kafka→xxHash64+DynamoDB→Ray Serve→Turbopuffer;元件間的「不知道」邊界讓各層可獨立替換。
Embedding 基礎設施成本 −90%,來自:拿掉外部 API per-token 計費、用開源 model 取代付費 model、preprocessing 與 inference 共用機器推高 GPU 利用率、query path 從外部 HTTP 變成同 cluster RPC。Ray 上的 embedding model 是 GPU-resident 常駐 deployment,不是 cold-start serverless function——request 進來時 model 已在 GPU 上,搭配 sub-millisecond 窗口的 dynamic batching 把 batch=1 的 kernel launch overhead 避掉,同時把每筆 query 額外等待控制在 1-2 ms。窗口長度是 tail latency vs throughput 的 tradeoff,必須為它設 explicit budget。
把四個階段串起來看到什麼
四個階段背後其實是同一條 thesis:不要在錯誤的計費邊界上付固定成本。第一段拆掉的是「按開機時間計費」的固定成本(換 serverless);第二段拆掉的是「dedicated SSD 容量」的固定成本(換 object-storage based 引擎);第三段拆掉的是「為了沒變的 span 重做工作」的隱性固定成本(hash diff);第四段拆掉的是「外部 API 的 per-token + 雙重 compute」的固定成本(Ray 收編)。每一段都對應「停止為某種不必要的恆定支出付費」。
對工程同行有四個可以直接拿去用的「決策觸發條件」:(1)資料庫帳單裡「未使用容量」佔比超過 30%,檢查計費模型是不是按 uptime 而非用量——這是換 serverless / object-storage 引擎的訊號。(2)application 程式碼裡路由邏輯開始長出多層 lookup(generation → shard → pod),重新評估底層能不能用 namespace 抽象掉。(3)ingestion pipeline cost 與「實際變動量」的比值大於 2x,引入 content-defined diff(hash、Merkle、CDC)——重複工作被計費系統隱藏得最深,需要主動量測才看得見。(4)embedding / inference 成本超過總基礎設施支出 20%,認真評估「自己跑 vs API 呼叫」——這個門檻會隨開源模型品質持續下移。
這四個觸發條件不依賴 Notion 的具體業務形態——任何「資料規模單調增、編輯頻率高、有 retrieval 需求」的系統(Slack、Linear、Copilot 的 codebase indexer、Glean、所有 enterprise search 產品)都會撞到這四道牆,只是各自處於不同階段。Notion 演進跨度兩年、平均每階段 6-7 個月:這個節奏不是巧合,每段都需要時間把上一段穩定下來、量出真實 cost shape、設計下一步。一年做完大概率每段都做不徹底;三年才做完業務已被成本壓垮。
所有四階段順序之所以對,是因為每段都依賴前段提供的可量測信號——day-one 的 Notion 沒有「未使用容量佔比」、沒有「重複 embed 量」、甚至沒有 span ID。「先撐住、再量、再砍」不是延誤,而是必要的時序。把 Notion 兩年的故事壓回一句話:架構演進的真正生產力來自「把不對的計費邊界找出來、然後一段段拆掉」。技術選型(Turbopuffer、Ray、xxHash64)只是執行這個動作的工具;哪條邊界該動、什麼時候動、什麼順序動,才是真正值得記下的內容。對讀者下週的工作具體含義:先別急著評估「換 Pinecone / Qdrant / Weaviate」,先打開最近 3 個月雲端帳單,把 vector DB 與 embedding 支出按「固定 provision、按用量、按 token」三類拆——失衡的那一類就是該動的邊界。
What changes:兩年下來,Notion 把「向量搜尋」從一個寫死在外部供應商與 GPU pod 上的固定支出,重組成一條 namespace-on-object-storage + hash-diff + 自家 Ray 集群的可伸縮 pipeline——對任何要從「demo 規模」走到「平台規模」的 retrieval 系統,這四個階段的順序(解耦計費 → 換引擎降結構性成本 → 砍重複工作 → 整合 compute layer)就是預設模板。