SQLite-Vector is a cross-platform, ultra-efficient SQLite extension that brings vector search capabilities to your embedded database.
SQLite-Vector is a cross-platform, ultra-efficient SQLite extension that brings vector search capabilities to your embedded database.
Building RAG or semantic search? SQLite-Vector ships as an extension you can drop into any SQLite app. Need it managed with sync and auth? SQLite Cloud free tier gives you 512 MB and 20 connections, no credit card.
SQLite Vector is a cross-platform, ultra-efficient SQLite extension that brings vector search capabilities to your embedded database. It works seamlessly on iOS, Android, Windows, Linux, and macOS, using just 30MB of memory by default. With support for Float32, Float16, BFloat16, Int8, UInt8, 1Bit, and TurboQuant 2/3/4-bit quantization, plus highly optimized distance functions, it's the ideal solution for Edge AI applications.
SQLite-Vector includes TurboQuant, a compact data-oblivious vector quantizer inspired by the Google Research paper TurboQuant: Online Vector Quantization with Near-Optimal Distortion Rate. It stores each vector as low-bit scalar codes plus one scale value, then scores directly from SIMD lookup-table kernels without reconstructing full vectors.
BLOBs in ordinary tablesqtype=TURBO| Feature | SQLite-Vector | Traditional Solutions |
|---|---|---|
| Works with ordinary tables | ✅ | ❌ (usually require special virtual tables) |
| Doesn't need preindexing | ✅ | ❌ (can take hours for large datasets) |
| Doesn't need external server | ✅ | ❌ (often needs Redis/FAISS/Weaviate/etc.) |
| Memory-efficient | ✅ | ❌ |
| TurboQuant low-bit scanning | ✅ | ❌ |
| Easy to use SQL | ✅ | ❌ (often complex JOINs, subqueries) |
| Offline/Edge ready | ✅ | ❌ |
| Cross-platform | ✅ | ❌ |
Unlike other vector databases or extensions that require complex setup, SQLite-Vector just works with your existing database schema and tools.
Download the appropriate pre-built binary for your platform from the official Releases page:
-- In SQLite CLI
.load ./vector
-- In SQL
SELECT load_extension('./vector');
Or embed it directly into your application.
You can download the WebAssembly (WASM) version of SQLite with the SQLite Vector extension enabled from: https://www.npmjs.com/package/@sqliteai/sqlite-wasm
…
To add your machine to the table, one command — pass the CPU name and nothing else:
make benchmark HARDWARE="Apple M5 Pro"
It builds test/benchmark.c at -O3 with the same per-translation-unit SIMD flags the
shipped extension uses, runs k=20 over 1,000,000 vectors of dimension 768 with cosine
distance and 20 queries reporting the best, and prints two rows ready to paste, unedited,
into the table below.
Two things it does so a pasted row cannot be wrong. The backend is appended by the
binary from what the build actually selected, not typed by hand, so a row cannot claim
AVX512 on a build that fell back to SSE2. And a run whose parameters differ from the
ones the table is built on prints an explanation instead of rows, because a row
measured on a different workload would sit in that table looking comparable without being
comparable.
That second guard exists because the parameters are adjustable, just not for this table:
make benchmark NVECS=100000 DIM=384 K=10 DISTANCE=l2
That run prints the mode table for whatever you asked for, and no paste-ready rows.
Common to every row: the database is a file, never :memory: — an in-memory database
puts the whole index in the process no matter how it is configured, which makes any memory
figure meaningless. Vectors are uniform random with a fixed seed, so two machines measure
the same data. The INT8 index is 740 MB on disk against 2930 MB of raw FLOAT32.
Recall is the overlap with the exact FLOAT32 scan, the baseline everything is compared
against, 100% by definition; on the reference machine that scan takes 484 ms/query,
because it reads 3 GB per query.
The two rows per machine are the two ways the same index gets deployed. Preloaded holds
it in the process after vector_quantize_preload(). Streamed walks it through a bounded
buffer set by max_memory=30MB, the default, and what a device with less RAM than the
index actually does. Max memory is measured, not the parameter echoed back: it is the
peak the extension and SQLite had allocated during the scan.
| Hardware | Vectors | Index | Max memory | ms/query | Mvec/s | Recall@20 |
|---|---|---|---|---|---|---|
| Apple M5 Pro - NEON | 1,000,000 | INT8 preloaded |
740 MB | 37.6 | 26.6 | 99.5% |
| Apple M5 Pro - NEON | 1,000,000 | INT8 streamed |
30 MB | 114.4 | 8.7 | 99.5% |
Results from other CPUs welcome — run the command above and open a PR adding the two rows it prints.
The trade is 25x less memory for 3x the latency, with recall untouched: both rows read the same index, only how much of it is resident differs.
Two things the timings do not show. They are best-of-20, so the file is in the operating
system's page cache by then — that cache lives outside the process and is evicted under
pressure, so it is not in the memory column, but it is why the streamed row is not paying
for storage reads. On a device where the index genuinely does not fit in RAM, add
index size / storage bandwidth to the streamed number. And the preloaded row is almost
unaffected by where the database lives, because after the one-time preload the scan reads
the extension's own buffer and never goes back to SQLite.
Recall is repeated on every row on purpose: it depends on the data, not the hardware, so a row that disagrees with the others is a sign that machine selected a different SIMD backend than it should have.
INT8 is in the table above because it is the mode to reach for first. The others, same
machine, same data, all preloaded:
| Mode | Index | ms/query | vs exact | Recall@20 |
|---|---|---|---|---|
FLOAT32 exact |
2930 MB | 484.4 | 1.0x | 100.0% |
UINT8 |
740 MB | 37.3 | 13.0x | 33.8% |
INT8 |
740 MB | 37.6 | 12.9x | 99.5% |
1BIT |
99 MB | 2.5 | 195x | 10.0% |
TURBO2 |
195 MB | 48.0 | 10.1x | 45.2% |
TURBO4 |
378 MB | 151.5 | 3.2x | 81.8% |
The data is uniform random, the worst case for every quantizer: real embeddings have structure quantization exploits, so recall on your own vectors will be higher, often much higher. Read that column as a floor and a way to rank the modes, not as a prediction.
Three things are worth knowing before choosing.
For cosine, use INT8, not UINT8. Same size, same speed, 33.8% recall against 99.5%.
Unsigned quantization subtracts the dataset minimum before scaling, and cosine measures
angle, which that shift destroys. UINT8 is right for L2, where a common translation
cancels. If you omit qtype the extension picks UINT8 for non-negative data — correct
for L2, wrong for cosine — so set it explicitly when you use cosine.
1BIT is a filter, not an answer. 195x faster than exact and 30x smaller, at 10%
recall here. It earns its place as a first pass whose survivors you re-rank at full
precision.
TurboQuant buys memory against INT8, not speed. TURBO4 is 3.2x faster than the
exact scan, so it is a real win over brute force — but INT8 is 4x faster again at twice
the size, and TURBO2 is both smaller and faster than TURBO4 if 45% recall is enough.
TurboQuant's lookup scan is one table gather per row: at dimension 768 that is 384 gathers
into a 384 KB table per vector, already about one lookup per cycle, so its current storage
layout has no headroom left (#57).
Reach for it when the memory budget is what binds.
TurboQuant can be selected with qtype=TURBO,qbits=N, where N is 2, 3, or 4. Shorthand aliases are also available: TURBO2, TURBO3, and TURBO4.
-- Highest recall TurboQuant mode currently recommended as the default
SELECT vector_quantize('images', 'embedding', 'qtype=TURBO,qbits=4');
-- Smaller edge-oriented representation
SELECT vector_quantize('images', 'embedding', 'qtype=TURBO2');
An earlier synthetic benchmark on this dataset reported speedups of 15x for 4-bit and 38x
for 2-bit against vector_full_scan(), with DOT distance and k=10. The Benchmark
section above measures the same shape with cosine and k=20 and lands lower — 3.2x for
4-bit, 10.2x for 2-bit — mostly because the distance kernels have since been rewritten,
which made the full-precision baseline it is compared against substantially faster. The
direction is the same: TurboQuant beats brute force, and INT8 beats TurboQuant on speed
while costing twice the memory.
For comparison, the raw FLOAT32 vectors alone are about 3.07 GB for 1M x 768 before SQLite row/page overhead. TurboQuant 4-bit reduces the scan representation to about 13% of that raw vector payload, TurboQuant 3-bit to about 10%, and TurboQuant 2-bit to about 7%. Actual resident memory depends on whether the database is in-memory or file-backed, SQLite cache settings, preloading, page cache behavior, and the host allocator.
The TurboQuant scan backend can be checked separately from the regular distance backend:
SELECT vector_backend(), vector_turboquant_backend();
For edge deployments, vector_quantize_memory(table, column) estimates the quantized scan representation. TurboQuant stores each row as rowid + scale + packed_codes, roughly rows * (8 + 4 + ceil(dim * qbits / 8)) bytes before allocator and SQLite cache overhead. The synthetic benchmark in test/benchmark_turboquant.c also supports PRELOAD=0 to compare the lower-RAM, non-preloaded path.
Real-dataset recall can be reproduced with test/recall_turboquant_real.py, which downloads Fashion-MNIST in the ANN-Benchmarks HDF5 format and compares TurboQuant against vector_full_scan() using L2 distance. Example run on macOS ARM64/NEON with 10,000 base vectors, 50 queries, and k=10:
| Mode | Quantized storage | Full scan / query | TurboQuant / query | Speedup | Recall@10 |
|---|---|---|---|---|---|
| TurboQuant 4-bit | 4.04 MB | 16.32 ms | 4.80 ms | 3.40x | 0.948 |
| TurboQuant 3-bit | 3.06 MB | 16.32 ms | 8.28 ms | 1.97x | 0.868 |
| TurboQuant 2-bit | 2.08 MB | 16.32 ms | 1.86 ms | 8.78x | 0.596 |
qbits=4 is the recommended starting point when recall matters. qbits=2 is useful for tighter edge memory budgets, but should be validated on the target embeddings because recall can drop significantly depending on the dataset.
You can [add this repository as a package dependency to your Swift project](https://
No open issues yet, or sync has not completed.