turbovec: Een vectorindex gebaseerd op TurboQuant, geschreven in Rust met Python-bindings

turbovec is een vectorindex in Rust met Python-bindings, gebouwd op het TurboQuant-algoritme van Google Research. Dit is een data-onafhankelijke quantizer met bijna optimale vervorming (distortion) en zonder aparte trainingsfase.

Belangrijkste kenmerken

  • Online ingest: Voeg vectoren toe en ze worden direct geïndexeerd. Er is geen trainingsstap, geen parameter-tuning en er zijn geen herbouwacties nodig naarmate het corpus groeit.
  • Snelle SIMD-zoekopdrachten: Handgeschreven kernels — NEON SDOT/SMMLA op ARM, AVX-512 VNNI en vpermb op x86, met AVX2- en scalaire fallbacks. Deze presteren in elke gemeten configuratie beter dan FAISS IndexPQFastScan, met een gemiddelde van 3,4× sneller bij 4-bit en 23% sneller bij 2-bit over de acht cellen van elke breedte, op beide architecturen.
  • Incrementele opslag: sync(path) bewaart alleen wat is gewijzigd sinds de laatste synchronisatie — één fsync per aanroep, crash-veilig op byte-niveau. Verwijdering of een kleine toevoeging kost milliseconden, ongeacht de grootte van de index. De functies write en load blijven beschikbaar voor snapshots van het volledige bestand.
  • Filteren tijdens het zoeken: Geef een id-allowlist (of een slot-bitmasker) mee aan search() en de kernel houdt hier direct rekening mee. Je krijgt altijd tot $k$ resultaten uit de toegestane set — zonder over-fetching of verlies van recall bij selectieve filters.
  • Volledig lokaal: Geen beheerde service, geen data die je machine of VPC verlaat. Combineer het met elk open-source embedding-model voor een volledig air-gapped RAG-stack.

Gebruik in Python

Installatie

pip install turbovec

Basisgebruik

from turbovec import TurboQuantIndex

# Initialisatie
index = TurboQuantIndex(dim=1536, bit_width=4)

# Vectoren toevoegen
index.add(vectors)
index.add(more_vectors)

# Zoeken
scores, indices = index.search(query, k=10)

# Opslaan en laden
index.write("my_index.tv")
loaded = TurboQuantIndex.load("my_index.tv")

# Incrementele duurzame opslag na wijzigingen
index.sync("my_index.tv")

Let op: vectoren en queries moeten 2D float32-arrays zijn met de vorm (n, dim). Andere datatypes worden geweigerd; gebruik indien nodig np.asarray(x, dtype=np.float32).

Gebruik van stabiele ID's (IdMapIndex)

Voor stabiele ID's die bewaard blijven na verwijderingen, gebruik je IdMapIndex:

import numpy as np
from turbovec import IdMapIndex

index = IdMapIndex(dim=1536, bit_width=4)

# Toevoegen met externe ID's
index.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))

# Zoeken (retourneert externe uint64 ID's)
scores, ids = index.search(query, k=10)

# Verwijderen op basis van ID (O(1))
index.remove(1002)

# Opslaan en laden
index.write("my_index.tvim")
loaded = IdMapIndex.load("my_index.tvim")
index.sync("my_index.tvim")

Hybride retrieval (gefilterd zoeken)

Beperk de resultaten tot een kandidaatenset die is gegenereerd door een ander systeem (bijv. SQL, BM25, ACL, tijdvenster):

import numpy as np
from turbovec import IdMapIndex

idx = IdMapIndex(dim=1536, bit_width=4)
idx.add_with_ids(vectors, ids)

# Stap 1: Extern systeem vernauwt de set tot kandidaat-ID's
allowed = np.array(db.execute("SELECT id FROM docs WHERE tenant=?", (t,)).fetchall(),
                   dtype=np.uint64)

# Stap 2: Dense rerank binnen de kandidaatenset
scores, ids = idx.search(query, k=10, allowlist=allowed)

Filtering vindt plaats binnen de SIMD-kernel op een granulariteit van 32-vectorblokken. Blokken zonder toegestane slots worden overgeslagen voordat er LUT-lookups of scoring-werk plaatsvindt. Individuele niet-toegestane slots binnen gescorede blokken worden gedropt bij de heap-insertie. Hierdoor vermijden selectieve allowlists het grootste deel van de SIMD-kosten.

De outputlengte is min(k, n_allowed). Wanneer er minder vectoren zijn toegestaan dan $k$, krijg je exact dat aantal resultaten zonder padding.

Framework-integraties

turbovec biedt drop-in vervangers voor de interne referentie-vector/document-stores in diverse frameworks. De publieke interface, persistentie-semantiek en pipeline-koppelingen blijven gelijk:

  • LangChain: pip install turbovec[langchain] (vervangt langchain_core.vectorstores.InMemoryVectorStore)
  • LlamaIndex: pip install turbovec[llama-index] (vervangt llamaindex.core.vectorstores.SimpleVectorStore)
  • Haystack: pip install turbovec[haystack] (vervangt haystack.documentstores.inmemory.InMemoryDocumentStore)
  • Agno: pip install turbovec[agno] (vervangt agno.vectordb.lancedb.LanceDb)

Gebruik in Rust

Installatie

cargo add turbovec

Basisgebruik

use turbovec::TurboQuantIndex;

let mut index = TurboQuantIndex::new(1536, 4).unwrap();
index.add(&vectors);
let results = index.search(&queries, 10);

index.write("index.tv").unwrap();
let loaded = TurboQuantIndex::load("index.tv").unwrap();

Gebruik van IdMapIndex

use turbovec::IdMapIndex;

let mut index = IdMapIndex::new(1536, 4).unwrap();
index.add_with_ids(&vectors, &[1001, 1002, 1003]).unwrap();
let (scores, ids) = index.search(&queries, 10);
index.remove(1002);

index.write("index.tvim").unwrap();
let loaded = IdMapIndex::load("index.tvim").unwrap();

Performance Analyse

Recall (Terugwinning)

Vergelijking tussen TurboQuant en FAISS IndexPQ (LUT256, nbits=8) met 100K vectoren, k=64.

In de tests met OpenAI-embeddings ($d=1536$ en $d=3072$) verslaat de gekalibreerde versie (TQ+) FAISS op R@1 in drie van de vier cellen (met 0,9–2,9 punten). Beide bereiken een recall van 1,0 bij $k=8$. Bij GloVe ($d=200$), een lastiger regime vanwege de lage dimensionaliteit, presteert TQ+ beter dan FAISS bij R@1 voor beide bit-breedtes (+1,9 bij 4-bit, +0,8 bij 2-bit).

Zoeksnelheid

Benchmarks uitgevoerd met 100K vectoren, 1K queries, k=64.

  • ARM (GCP c4a-standard-8, Google Axion): TurboQuant is in elke configuratie sneller dan FAISS FastScan, gemiddeld 3,5× sneller bij 4-bit en 26% sneller bij 2-bit.
  • x86 (Intel Xeon Platinum 8481C / Sapphire Rapids): TurboQuant wint in elke configuratie, gemiddeld 3,4× sneller bij 4-bit en 20% sneller bij 2-bit.

Latentie bij invoegen en verwijderen

  • Invoegen: Een enkele add() duurt 6,3–19,7 $\mu$s (7,6–13,9× sneller dan FAISS). Bij batches van 100 vectoren daalt dit naar 4,6–16,3 $\mu$s per vector.
  • Verwijderen: IdMapIndex.remove(id) (O(1) swap-and-pop) duurt 0,44–1,37 $\mu$s per operatie. In contrast hiermee kost de vergelijkbare operatie in FAISS (die de codes bij elke aanroep opnieuw inpakt) tussen de 0,19 en 1,02 seconden bij 100K vectoren.

Opslaan en laden

TurboQuant serialiseert naar één .tv-bestand met een fsync en atomaire hernoeming.

Hoe het werkt

TurboQuant comprimeert richtingen op een hoog-dimensionale hypersfeer via de volgende stappen:

  1. Normalisatie: De lengte (norm) wordt verwijderd en apart opgeslagen als een float. Elke vector wordt zo een eenheidsrichting.
  2. Willekeurige rotatie: Alle vectoren worden vermenigvuldigd met dezelfde willekeurige orthogonale matrix. Hierdoor volgt elke coördinaat onafhankelijk een Beta-distributie die in hoge dimensies convergeert naar een Gaussische $N(0, 1/d)$, ongeacht de inputdata.
  3. Per-coördinaat kalibratie (TQ+): Om afwijkingen bij finite dimensies te corrigeren, past TQ+ twee scalars per coördinaat aan (shift en scale). Dit gebeurt via index.calibrate(sample) met een representatieve steekproef ($\approx 1024$ rijen).
  4. Lloyd-Max scalaire kwantisering: Omdat de distributie bekend is, worden optimale buckets vooraf berekend (4 buckets voor 2-bit, 16 voor 4-bit) om de gemiddelde kwadratische fout te minimaliseren.
  5. Bit-packing: Coördinaten worden opgeslagen als kleine integers (0-3 of 0-15) en compact in bytes gepakt. Een 1536-dim vector gaat van 6144 bytes (FP32) naar 384 bytes (2-bit), wat een compressie van 16× oplevert.
  6. Lengte-gernormaliseerde scoring: Om de onderschatting van inproducten door kwantisering te corrigeren, wordt per vector een scalar berekend en opgeslagen. De search-kernel vermenigvuldigt de score met deze scalar, waardoor de estimator onbevooroordeeld wordt zonder extra kosten tijdens het zoeken.

Zoekproces: In plaats van elke database-vector te decompresseren, wordt de query één keer geroteerd naar hetzelfde domein en direct gescoord tegen de codebook-waarden met behulp van SIMD-intrinsics.

Bouwen en Installeren

Python (via maturin)

pip install maturin
cd turbovec-python
maturin build --release
pip install target/wheels/*.whl

Rust

cargo build --release

Opmerking: x8664-builds targeten x86-64-v2 (SSE4.2). AVX-512 en AVX2 kernels worden tijdens runtime geselecteerd via isx86featuredetected!.

Referenties

  • TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate (ICLR 2026) — het basisalgoritme.
  • RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search (SIGMOD 2024) — bron voor de lengte-renormalisatie correctie.
  • FAISS: Fast accumulation of PQ and AQ codes — de x86 SIMD-kernel van turbovec is gebaseerd op de FastScan pack-layout en u16 accumulator strategie.