他要把院內 200 萬筆病歷摘要轉成向量,讓值班醫生用一句話就能搜到相關病歷,而且病歷不能離開院內主機、不能上雲
如果你要做一個『讓 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 系統。
| 比較項目 | 傳統做法 | 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)idx.add_with_ids(vectors, ids)allowed = np.array(db.execute("SELECT id FROM docs WHERE tenant=?", (t,)).fetchall(), dtype=np.uint64)scores, ids = idx.search(query, k=10, allowlist=allowed)這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。
# 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)