AI-LECTURER 速報課|2026-08-20|約 30 分鐘

省下 87% 記憶體還比 FAISS 快:用 turbovec 打造不出機房的向量搜尋

取材:Turbovec – Google's TurboQuant for vector search in Rust(Hacker News(AI 高人氣))
📍 真實場景
阿凱,一間醫療新創的後端工程師,正在幫合作診所打造「病歷語意問答」小工具

他要把院內 200 萬筆病歷摘要轉成向量,讓值班醫生用一句話就能搜到相關病歷,而且病歷不能離開院內主機、不能上雲

😖 卡住的地方:用最直接的做法(float32 存向量)記憶體一路飆到快 6GB,主管說機器不能再加大,而且用來比對的量化工具大多要先拿一批資料「訓練」才能用,院內資料還在持續增加,訓練一次沒多久又要重做
💡 這課會帶他認識 turbovec:一個不用訓練就能用、把記憶體壓到近八分之一、搜尋還更快、而且完全跑在本機的向量搜尋工具,剛好解決他手上這個案子的三個卡點

🧭 這到底是什麼(白話版)

如果你要做一個『讓 AI 讀懂你自己資料庫再回答問題』的系統(也就是 RAG(檢索增強生成)讓 AI 回答問題前先去資料庫查資料,再依照查到的內容作答,而不是憑記憶亂猜,答案比較準也比較不容易亂編),核心步驟之一就是 向量搜尋把文字轉換成一串代表「意思」的數字(向量),要找「意思相近」的內容時,去比較這些數字誰跟誰最接近:先把每一份文件轉成一串數字,使用者問問題時把問題也轉成數字,再去資料庫裡找數字最接近的那幾份文件。問題是,這些數字通常用最原始的格式(float32)存,資料一多,光是把這些數字放進記憶體就會吃光機器資源。

turbovec 是一個用 Rust 寫成、附 Python 介面的向量搜尋工具,核心是 Google Research 提出的 TurboQuant 演算法——一種 量化(quantization)把原本很精細的數字(float32,佔很多空間)壓縮成更粗略、佔更少空間的格式,犧牲一點點精確度換取大量省空間 技術。跟很多量化方法不一樣的是,TurboQuant 不用先拿一批資料「訓練」出壓縮規則,向量一加進來就直接被壓縮索引,不用等、不用重建。

實際效果是:1000 萬筆文件原本用 float32 存要 31 GB 記憶體,換成 turbovec 之後只要 4 GB,而且搜尋速度在多數情況下比業界標準工具 FAISSMeta(前身 Facebook)開源的向量搜尋函式庫,是這個領域最多人用的標準工具,常被拿來當比較基準 的量化版本還快,靠的是針對不同 CPU 手寫的 SIMDCPU 一種「一次算很多筆數字」的加速指令,讓程式不用一筆一筆算,能大幅加快速度 加速指令。

更重要的是,turbovec 完全跑在你自己的機器或內部網路上,不用連任何雲端服務,資料完全不出你的環境——對醫療、金融這類「資料不能上雲」的產業,這代表你終於能用比較小台的機器,做出一個內部也能用的語意搜尋/RAG 系統。

🎯 為什麼值得你花時間

記憶體是本地部署能不能成立的關卡很多需要「資料不能出本機/VPC」的場景(醫療、金融、內部知識庫),瓶頸往往不是算力而是記憶體——31GB 壓到 4GB,代表原本要用昂貴大記憶體伺服器才跑得動的索引,現在一般規格的機器就裝得下。
免訓練量化,省掉一整套維運流程傳統向量量化(像 Product Quantization)通常要先拿樣本資料訓練出一份「壓縮規則」,資料分布變了還要重新訓練。turbovec 用的是不用訓練、對資料一視同仁的量化方式,資料一直長大也不用回頭重做,維運負擔小很多。
把存取控制放進搜尋本身,不是搜完再擋多租戶或有權限分級的系統(例如「這個使用者只能查自己部門的資料」),如果是搜完再篩選,可能篩到剩不足 k 筆結果;turbovec 把篩選做進搜尋核心,兼顧效能與正確性,這是設計「安全查詢系統」時常被忽略的細節。

⚙️ 它是怎麼運作的

1
向量進來就直接量化跟傳統量化方法不同,turbovec 不用先拿一批資料訓練出壓縮規則,向量一加進來,TurboQuant 演算法就直接把它壓成幾個 bit,索引邊建邊可以用。
2
用固定的位元寬度換取空間你可以自己決定 bit_width(位元寬度)量化後每個數字用幾個位元儲存,數字越小代表壓縮越狠、佔用空間越小,但也可能損失更多精準度(turbovec 支援 2 bit 到 4 bit),TurboQuant 的設計目標是在固定位元數下,讓壓縮前後向量的相對距離盡量不失真,這樣「意思相近」的判斷才不會跑掉。
3
搜尋走 CPU 的高速通道turbovec 針對不同 CPU 手寫了加速指令(ARM 上用 NEON、x86 上用 AVX-512/AVX2),讓比對大量向量一次算一整批數字,而不是一個一個算,所以比同類工具快。
4
篩選條件直接塞進搜尋核心如果你只想搜「某個科別」或「某個時間區間」的資料,把允許的 id 清單直接丟給搜尋函式,turbovec 會在比對的同時順便篩選,保證回傳結果剛好是你要的 k 筆,不會因為篩選而漏算或多算。
5
改動只存差異,不整包重寫資料持續變動時,sync() 每次呼叫都會做一次 fsync告訴作業系統「把資料真的寫進硬碟,不要只留在記憶體緩衝區」,確保程式當機或斷電也不會遺失剛存的資料,只把「這次新增或刪除的部分」寫進硬碟,不用像傳統做法那樣每次都整份索引重存一次。
turbovec vs. 傳統做法(float32 + FAISS IndexPQFastScan)
比較項目傳統做法turbovec
1000 萬筆文件的記憶體用量約 31 GB(float32 原始向量)約 4 GB(4-bit 量化)
要不要先訓練量化器通常要,先拿樣本資料訓練 codebook不用,資料進來就直接量化索引
新增/刪除資料後常常要重新訓練或重建索引線上即時加入,刪除也是 O(1)
搜尋時篩選候選集合通常搜完再篩,可能篩到不足 k 筆篩選條件直接交給搜尋核心,保證拿滿 k 筆且不浪費運算
搜尋速度(同硬體)基準4-bit 平均快 3.4 倍,2-bit 平均快 23%

🔍 程式碼漫遊(點有 ● 的行看白話講解)

官方文件示範怎麼把 turbovec 接進一個真實的多租戶 RAG 架構:先讓資料庫用一般條件(例如「哪個客戶」)篩出候選文件,再讓 turbovec 只在這個候選集合裡做向量比對,兩層架構各司其職。

idx = IdMapIndex(dim=1536, bit_width=4)
💬 先建一個支援自訂 ID 的索引,dim 要跟你的 embedding 模型輸出維度一致(例如常見的 OpenAI/BGE embedding 是 1536 維)。
idx.add_with_ids(vectors, ids)
💬 把向量跟它們在你自己資料庫裡的 id 綁在一起存進去,之後查到的結果可以直接對回原始文件。
allowed = np.array(db.execute("SELECT id FROM docs WHERE tenant=?", (t,)).fetchall(), dtype=np.uint64)
💬 第一階段:用一般資料庫查詢先篩出「這個客戶(tenant)能看的文件」——這一步跟向量搜尋完全無關,就是普通的權限查詢。
scores, ids = idx.search(query, k=10, allowlist=allowed)
💬 第二階段:把剛剛篩出的候選 id 清單丟進 allowlist,turbovec 只會在這個集合裡比對語意相似度,保證回傳的 10 筆都合法,不會多算也不會漏算。

🛠️ 動手做:打造一個「病歷語意搜尋+科別過濾」小型 demo

這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。

📄 demo_turbovec.py ⬇ 下載
# demo_turbovec.py
# 模擬「診所病歷語意搜尋」情境,體驗 turbovec 的四個核心特性:
# 免訓練量化、線上寫入、搜尋時過濾(存取控制)、增量存檔。
# 真實專案中,vectors 應該來自你選用的 embedding 模型(例如 sentence-transformers),
# 這裡先用亂數向量代替,讓你不用先架 embedding 服務就能跑通整個流程。

import numpy as np
from turbovec import IdMapIndex

DIM = 128        # 向量維度,這裡用小維度示範;真實 embedding 常見 384 / 768 / 1536
BIT_WIDTH = 4     # 量化後每個數字用 4 bit 儲存,數字越小壓縮越狠、也越可能失真

# 1. 模擬 500 筆「病歷摘要向量」,以及它們在醫院資料庫裡的病歷編號
np.random.seed(42)
vectors = np.random.rand(500, DIM).astype(np.float32)
record_ids = np.arange(1000, 1500, dtype=np.uint64)

# 2. 建立索引:不用先訓練,建立完就能馬上加資料
index = IdMapIndex(dim=DIM, bit_width=BIT_WIDTH)
index.add_with_ids(vectors, record_ids)
print(f"索引完成,共 {len(record_ids)} 筆病歷向量")

# 3. 模擬醫生輸入一個查詢向量,在「全院病歷」裡找出最相近的 5 筆
query = np.random.rand(1, DIM).astype(np.float32)
scores, ids = index.search(query, k=5)
print("全庫搜尋結果(病歷編號):", ids)

# 4. 模擬「醫生只能查自己科別病歷」的存取控制:
#    假設病歷編號 1000~1199 是內科,內科醫生的查詢只該搜到這個範圍
allowed = record_ids[record_ids < 1200]
scores_f, ids_f = index.search(query, k=5, allowlist=allowed)
print("加上科別過濾後的搜尋結果:", ids_f)
print("驗證:過濾後的每一筆都在允許清單內 ->", set(ids_f.tolist()) <= set(allowed.tolist()))

# 5. 模擬新病歷持續進來:加 3 筆新資料,不用重建整個索引
new_vectors = np.random.rand(3, DIM).astype(np.float32)
new_ids = np.array([2001, 2002, 2003], dtype=np.uint64)
index.add_with_ids(new_vectors, new_ids)

# 6. 增量存檔:只把「這次新增的部分」寫進硬碟,且保證斷電也不會壞檔
index.sync("clinic_index.tvim")
print("已將索引存到 clinic_index.tvim(增量、crash-safe)")

# 7. 模擬程式重啟:重新載入索引,確認資料還在
loaded = IdMapIndex.load("clinic_index.tvim")
scores2, ids2 = loaded.search(query, k=5)
print("重新載入索引後再搜尋一次:", ids2)
  1. 開啟 PowerShell,切換到你想放教材的資料夾,例如:cd D:\practice\turbovec-demo(資料夾不存在就先執行 mkdir D:\practice\turbovec-demo 建立)
  2. 建立一個乾淨的 Python 虛擬環境,避免跟電腦上其他套件打架:python -m venv .venv
  3. 啟用虛擬環境:.\.venv\Scripts\Activate.ps1 (如果跳出「不允許執行指令碼」的錯誤,先執行一次 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned,再重新啟用)
  4. 安裝需要的套件:pip install turbovec numpy
  5. 把上面的 demo_turbovec.py 存到同一個資料夾裡
  6. 執行:python demo_turbovec.py
  7. 觀察畫面上的兩行搜尋結果:「全庫搜尋結果」跟「加上科別過濾後的搜尋結果」通常會不一樣,而且下面那行「驗證」會印出 True——這就是 turbovec「搜尋時直接過濾」的效果
  8. 程式執行完後,資料夾裡會多一個 clinic_index.tvim 檔案,這就是增量存檔的索引檔;下次可以直接用 IdMapIndex.load() 載入它繼續用,不用重新索引一次

🧠 工程思維透鏡(資深工程師看到的是什麼)

🔭 為什麼 turbovec 選擇「不用訓練」的量化方式,而不是像傳統 Product Quantization 那樣先訓練 codebook?
傳統量化方法先訓練出一份壓縮規則,通常能把壓縮率做得更極致,但代價是:資料分布改變時(新主題、新語言的文件湧入)壓縮規則會失準,得重新訓練整個索引,而且訓練前得先湊到一批有代表性的資料,對「邊蒐集邊上線」的系統很不友善。turbovec 用 data-oblivious(不看資料分布)的量化方式,換來的是資料隨時加入都能直接用、不用停機重建——這是用一點理論上的壓縮效率,換維運上的簡單與彈性,是典型「工程上夠好就好」而非「理論上最優」的取捨。
🔭 為什麼要把過濾(filter)直接做進搜尋核心,而不是搜尋完之後再篩選?
如果搜尋完再篩選,你得先抓比 k 大很多的候選數量(過度抓取),才能保證篩完還剩 k 筆——抓多少才夠很難抓準,抓少了會漏掉真正該出現的結果,抓多了浪費運算。把 allowlist 直接交給搜尋核心,等於是資料庫界「把篩選條件下推(predicate pushdown)」的概念搬到向量搜尋——篩選跟比對在同一輪運算裡一次做完,結果保證剛好 k 筆而且不浪費算力,是效能與正確性一起解決的設計。
🔭 sync() 為什麼要保證「每次呼叫都 fsync、任何一個 byte 中斷都安全」,而不是像 write() 一樣整檔覆寫就好?
整檔覆寫邏輯簡單,但索引一大,每次改一點點資料就要重寫整個檔案,I/O 成本會隨資料量線性增加,資料越多改動越貴。sync() 走的是「只寫變動、確保寫入完整」的增量持久化設計,概念上跟資料庫的 write-ahead log 或日誌型檔案系統類似:犧牲一點程式複雜度,換來單次存檔成本只跟「這次改了多少」成正比,而不是跟「整個索引多大」成正比,這對長時間持續運作的系統是關鍵設計。
🔭 為什麼要另外提供 IdMapIndex,讓你自訂穩定 ID,而不是直接用向量在陣列裡的順序當 ID?
如果用陣列順序當 ID,一旦你刪除或搬動了某一筆資料,後面所有向量的「編號」全部跟著偏移,你在外部資料庫(如病歷系統、文件庫)存的關聯就全部對不上。IdMapIndex 讓你用自己系統原有的唯一鍵(像資料庫的 doc id)當作向量的身分證,這樣不管索引內部怎麼搬動資料,查到的 id 永遠能正確對回原始資料——這是分散式/長期運作系統中「穩定識別碼」設計的常見考量,也是把一個純數學工具接進真實業務系統時最容易踩坑的地方。

📝 隨堂考(點選答案,立即回饋)

Q1. turbovec 為什麼可以把 1000 萬筆文件的索引從 31GB 壓到 4GB?
✅ 文中提到用 TurboQuant 演算法把 float32 向量量化成 2~4 bit 儲存,才做到 31GB 壓到 4GB,並沒有刪資料或換模型。
Q2. 關於 turbovec 的「線上寫入(online ingest)」,下列何者正確?
✅ 官方說明明確提到 no train step, no parameter tuning, no rebuilds as the corpus grows,向量一加入就已經被索引。
Q3. 為什麼 turbovec 要把「過濾」直接做在搜尋核心裡,而不是搜尋完再篩選?
✅ 文中提到把 id allowlist 直接交給 search(),可以「拿到剛好 k 筆」且「不用過度抓取、不會犧牲 recall」。
Q4. IdMapIndex 跟 TurboQuantIndex 最大的差異是?
✅ 文中示範 IdMapIndex 用 add_with_ids 搭配自訂的 uint64 外部 id,並可用 remove(id) 做 O(1) 刪除,適合 id 需要對回外部系統的情境。
Q5. sync() 這個方法的用途是?
✅ 文中說明 sync(path) 只 persist 上次同步後改動的部分,且是 crash-safe,跟整檔覆寫的 write() 不同。

🃏 翻牌記憶卡(先想答案,再點開對答)

量化(quantization)點我翻面
把精細的浮點數壓縮成更省空間的粗略格式,犧牲一點準確度換取大量省空間。
TurboQuant點我翻面
Google Research 提出的量化演算法,不用事先訓練,資料進來就能直接壓縮索引。
data-oblivious(不看資料分布)點我翻面
不用先看過整批資料做訓練/統計,加一筆是一筆,對持續成長的資料集很友善。
SIMD 加速點我翻面
CPU 一次處理一大批數字的指令集(如 AVX-512、NEON),比一筆一筆算快很多。
filter at search time(搜尋時過濾)點我翻面
把篩選條件(如允許的 id 清單)直接交給搜尋核心處理,確保拿到剛好 k 筆且不浪費運算。
增量存檔(sync)點我翻面
只寫入自上次存檔後變動的部分,且保證斷電/當機也不會毀損索引檔。

✅ 離開前自測(全勾=這課真的學會了)

0%