turbovec
ライブラリRyanCodrai/turbovec
TurboQuant に基づく効率的なベクトルインデックス。Rust コアと Python バインディング。
概要
Google TurboQuant アルゴリズムに基づき、高圧縮と高速検索を実現し、フィルタリングクエリと安定した ID をサポート、多くのベンチマークで FAISS を上回る。Rust で記述され、Python バインディングを提供しており、容易に統合可能。
README プレビュー
\n \n\n\n\n \n \n \n \n\n\n---\n\n**A 10 million document corpus takes 31 GB of RAM as float32. turbovec fits it in 4 GB - and searches it faster than FAISS.**\n\nturbovec is a Rust vector index with Python bindings, built on Google Research's [**TurboQuant**](https://arxiv.org/abs/2504.19874) algorithm — a data-oblivious quantizer that matches the Shannon lower bound on distortion, with no codebook training and no separate train phase.\n\n- **Online ingest.** Add vectors, they're indexed — no train step, no parameter tuning, no rebuilds as the corpus grows.\n- **Faster than FAISS.** Hand-written NEON (ARM) and AVX-512BW (x86) kernels beat FAISS IndexPQFastScan by 12–20% on ARM and match-or-beat it on x86.\n- **Filter at search time.** Pass an id allowlist (or a slot bitmask) to `search()` and the kernel honours it directly. You always get up to `k` results from the allowed set — no over-fetching, no recall hit on selective filters.\n- **Pure local.** No managed service, no data leaving your machine or VPC. Pair with any open-source embedding model for a fully air-gapped RAG stack.\n\nBuilding RAG where privacy, memory, or latency matters? **You're in the right place.**\n\n## Python\n\n```bash\npip install turbovec\n```\n\n```python\nfrom turbovec import TurboQuantIndex\n\nindex = TurboQuantIndex(dim=1536, bit_width=4)\nindex.add(vectors)\nindex.add(more_vectors)\n\nscores, indices = index.search(query, k=10)\n\nindex.write("my_index.tq")\nloaded = TurboQuantIndex.load("my_index.tq")\n```\n\nNeed stable ids that survive deletes? Use `IdMapIndex`:\n\n```python\nimport numpy as np\nfrom turbovec import IdMapIndex\n\nindex = IdMapIndex(dim=1536, bit_width=4)\nindex.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))\n\nscores, ids = index.search(query, k=10) # ids are your uint64 external ids\nindex.remove(1002) # O(1) by id\n\nindex.write("my_index.tvim")\nloaded = IdMapIndex.load("my_index.tvim")\n```\n\n### Hybrid retrFAQ (5)
トラブル対応32K以上のベクトルを含むインデックスでTurboVec 1.1.0にアップグレードした後、マスク検索やallowlist検索が3〜4倍遅くなる問題を修正するにはどうすればよいですか?
TurboVec 1.1.1 以降にアップグレードしてください。このリリースには、32,768以上のベクトルを持つインデックスで使用されるステージド AVX-512/ARM ルートにおけるマスク付き単一クエリ検索の修正 #556 および #558 が含まれています。1.1.0 での一時的な回避策として、以下のようにしてクラシックカーネルを強制してください:
export TURBOVEC_4BIT_PLANES=0
./your_search_binary
または、単一のコマンドに対しては:
TURBOVEC_4BIT_PLANES=0 ./your_search_binary
このリグレッションは、1.1.0 のステージドルートが符号スキャンおよび再スコアリングの際に低速なレーン単位のスカラーパスを通じてマスクを適用していたために発生しました。選択的なマスクでは、マスク付き検索がマスクなしスキャンよりも遅くなる可能性がありました。
トラブル対応TurboQuantIndex.swap_remove や IdMapIndex.remove が並行検索時にハングしたり数十秒かかったりする問題を修正するには?
これは通常のロック競合ではなく、lock_write_gil_aware におけるライブロックです。この関数は try_write() を呼び出し、WouldBlock の場合に py.detach() 内でブロッキング書き込みロックを取得しますが、再試行の前にすぐにそれをドロップします。並行する検索スレッドが先に読み取りロックを再取得できるため、削除は競合に繰り返し負ける可能性があります。
swap_remove と IdMapIndex.remove の呼び出し箇所を修正し、通常のブロッキング lock_write を使用して、1つのデタッチされたクロージャ内で削除を実行してください:
py.detach(|| {
let _guard = lock_write(&lock);
// perform swap_remove/remove here; the guard is !Send
});
ロックを取得してすぐにドロップしてはいけません。競合がない場合の高速パスを維持することが重要な場合は、アタッチされた状態で1回 try_write() を使用し、WouldBlock が返された場合は、デタッチされたブロッキングによる取得にちょうど1回だけフォールバックし、そのクロージャ内で操作を実行してください。これはすでに上限が設けられている add パスと一致します。
パッチを適用したビルドでは、8つの並行 swap_remove 呼び出しにかかる時間が、合計 86.1 s / 最悪 25.8 s から、合計 1.13 s / p50 144 ms に短縮されました。issue #484 の修正を含むリリースにアップグレードするか、その PR を適用してください。一時的な回避策としては、削除と検索を直列化するか、swap_remove()/remove() を呼び出す前に検索スレッドを一時停止してください。
トラブル対応TurboVec の staged search が最近傍を取りこぼし、k=10 での recall が低下する問題を修正するにはどうすればよいですか?
TurboVec を 1.1.2 にアップグレードしてください。このバージョンでは、MedCPT や bge-small のような密にクラスタ化された埋め込みにおける、段階的検索の再現率(リコール)低下の問題が修正されています。
python -m pip install -U "turbovec>=1.1.2"
影響を受けるバージョンは 1.1.0 と 1.1.1 です。これらのリリースでは、最初の sign-bit ステージで max(256, 20 × k) 個の候補しか保持されないため、強い共有ベクトル方向を持つコーパスでは、k=10 の場合に再スコアリングの前に真の最近傍が破棄される可能性があります。バージョン 1.0.0 は影響を受けません。
すぐにアップグレードできない場合は、回避策として、より大きな k で検索して結果を切り詰めてください。たとえば search(query, k=64) を呼び出し、最初の 10 件の結果を使用します。TurboVec 1.1.1 で MedCPT 4-bit を使用した場合、この方法によりテストしたすべてのクエリで真の最近傍が上位 10 件に入りましたが、search(query, k=10) では約 0.37〜0.40% のクエリで真の最近傍が取りこぼされました。
アップグレード後は、フルスキャンと照合して再現率を確認してください。報告されている 101K PubMed スライスでは、4-bit の k=10 における完全一致率が、MedCPT では 82.6% から 99.1% に、bge-small では 75.6% から 96.6% に向上し、再現率はフルインデックススキャンとほぼ同等になりました。
トラブル対応なぜ turbovec は、宛先のベース名が 235 バイト以上になると .tmp.* ファイルをリークして、一切クリーンアップしないのですか?
これは、一時ファイルの作成と古い一時ファイルのスイープ処理で異なる名前プレフィックスが使われるために発生します。tmp_sibling は base + suffix が NAME_MAX(255バイト)を超えると宛先のベース名を切り詰めますが、sweep_stale_tmps は切り詰められていない完全なベース名と照合します。一時ファイルのサフィックスは21バイトなので、ベース名の長さが235から切り詰めが始まります。修正内容は、sweep_stale_tmps で同じ切り詰め位置を計算して <base[..cut]>.tmp. と照合するようにし、既存の is_our_tmp_suffix と1時間の mtime ガードは維持するというものです。これは PR #524 で修正され、sync()/write_full パスを対象とするフォローアップも追加されました。これらの変更を含むビルドにアップグレードしてください。回避策としては、宛先のベース名を234バイト以下に保ってください。古い一時ファイルのみを手動で回収するには、まず以下で確認します:
find /path/to/dest -maxdepth 1 -type f -regextype posix-extended \
-regex '.+\.tmp\.[0-9]+\.[0-9]+\.[0-9a-fA-F]{8}$' -mmin +60 -print
その後、-print を -delete に置き換えて、確認が取れたファイルを削除してください。
トラブル対応なぜ TurboVec の allowlist 検索は、1 スレッドの場合よりも複数の Rayon スレッドを使用した場合の方が遅いのですか?
これは、選択的マスクに関する TurboVec 1.0.0 の既知のリグレッションです。単一クエリのブロック並列しきい値が、マスクで許可されたブロック数ではなく、インデックス内の全ブロック数をカウントしてしまいます。32,768 ベクター(1,024 ブロック)を超えると、1 つの ID しか持たない許可リストであっても並列パスに入り、Rayon プールへの受け渡しコストが発生するため、16 スレッドの検索が 1 スレッドの検索より約 2 倍遅くなります。
修正方法: TurboVec を 1.1.1 以降にアップグレードしてください。1.1.1 ではマスク付き単一クエリ検索が高速経路を維持し、関連するマスク付きステージ経路の修正も含まれています:
cargo add turbovec@1.1.1
# or, with an existing lockfile
cargo update -p turbovec --precise 1.1.1
cargo build --release
解決されたバージョンは次のコマンドで確認できます:
cargo tree -p turbovec
アップグレードできない場合の一時的な回避策: レイテンシに敏感な選択的単一クエリ検索を、Rayon ワーカー 1 つで実行してください:
RAYON_NUM_THREADS=1 ./target/release/your-binary
これにより選択的マスクでの並列受け渡しは回避できますが、プロセス全体の Rayon 並列処理も無効になるため、密な検索やマスクなしの検索が遅くなる可能性があります。