Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ $(BUILD_DIR): $(prefix)
mkdir -p $@


$(TARGET_LOADABLE): sqlite-vec.c sqlite-vec.h $(prefix)
VEC_INCLUDED_SOURCES=sqlite-vec-ivf.c sqlite-vec-ivf-kmeans.c sqlite-vec-rescore.c sqlite-vec-diskann.c

$(TARGET_LOADABLE): sqlite-vec.c sqlite-vec.h $(VEC_INCLUDED_SOURCES) $(prefix)
$(CC) \
-fPIC -shared \
-Wall -Wextra \
Expand All @@ -101,7 +103,7 @@ $(TARGET_LOADABLE): sqlite-vec.c sqlite-vec.h $(prefix)
$(CFLAGS) \
$< -o $@

$(TARGET_STATIC): sqlite-vec.c sqlite-vec.h $(prefix) $(OBJS_DIR)
$(TARGET_STATIC): sqlite-vec.c sqlite-vec.h $(VEC_INCLUDED_SOURCES) $(prefix) $(OBJS_DIR)
$(CC) -Ivendor/ $(CFLAGS) -DSQLITE_CORE -DSQLITE_VEC_STATIC \
-O3 -c $< -o $(OBJS_DIR)/vec.o
$(AR) rcs $@ $(OBJS_DIR)/vec.o
Expand Down Expand Up @@ -204,7 +206,7 @@ test-loadable-watch:
watchexec --exts c,py,Makefile --clear -- make test-loadable

test-unit:
$(CC) -DSQLITE_CORE -DSQLITE_VEC_TEST -DSQLITE_VEC_ENABLE_RESCORE -DSQLITE_VEC_ENABLE_DISKANN=1 tests/test-unit.c sqlite-vec.c vendor/sqlite3.c -I./ -Ivendor $(CFLAGS) -o $(prefix)/test-unit && $(prefix)/test-unit
$(CC) -DSQLITE_CORE -DSQLITE_VEC_TEST -DSQLITE_VEC_ENABLE_RESCORE -DSQLITE_VEC_ENABLE_DISKANN=1 -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1 tests/test-unit.c sqlite-vec.c vendor/sqlite3.c -I./ -Ivendor $(CFLAGS) -o $(prefix)/test-unit && $(prefix)/test-unit

# Standalone sqlite3 CLI with vec0 compiled in. Useful for benchmarking,
# profiling (has debug symbols), and scripting without .load_extension.
Expand Down
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,100 @@ limit 2;
*/
```

## Approximate nearest neighbor (ANN) indexes

By default a `vec0` table performs an **exact** KNN search — a brute-force
sequential scan that compares the query against every stored vector. This is
simple and always returns the true nearest neighbors, but its cost grows
linearly with both the number of rows and the vector dimensionality.

For larger collections you can attach an **approximate** index to a vector
column with an `indexed by` clause. Approximate indexes trade a small amount of
recall (they may miss some true neighbors) for substantially faster queries.
Three index types are available; each is enabled at compile time:

| Index | Compile flag | Idea | Best for |
| --- | --- | --- | --- |
| `rescore` | `-DSQLITE_VEC_ENABLE_RESCORE` | Scan a compact quantized copy, then re-rank the top candidates with full-precision vectors | Keeping near-exact recall while shrinking the scan |
| `ivf` | `-DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1` | Cluster vectors with k-means into `nlist` cells; at query time only scan the `nprobe` nearest cells | Large collections where you can afford a training step |
| `diskann` | `-DSQLITE_VEC_ENABLE_DISKANN=1` | Build a navigable graph and greedily walk it toward the query | Very large collections (millions of vectors) |

```sql
-- rescore: bit-quantized coarse scan, re-ranked with float32
create virtual table vec_rescore using vec0(
embedding float[1024] indexed by rescore(quantizer=bit, oversample=8)
);

-- IVF: 128 k-means cells, probe the 16 nearest at query time
create virtual table vec_ivf using vec0(
embedding float[1024] indexed by ivf(nlist=128, nprobe=16)
);
-- IVF requires a one-time training step once rows are inserted:
insert into vec_ivf(vec_ivf) values ('compute-centroids');

-- DiskANN: graph index with int8-quantized neighbor vectors
create virtual table vec_diskann using vec0(
embedding float[1024] indexed by diskann(neighbor_quantizer=int8)
);
```

KNN queries use the same `match ... order by distance limit k` syntax regardless
of the index. IVF exposes a few runtime commands via its shadow (`compute-centroids`
to (re)train, `nprobe=N` to change the probe count without rebuilding).

### Performance

The table below benchmarks each index against the default sequential scan on
20,000 synthetic clustered vectors, measuring per-query latency, speedup, and
recall@10 (the fraction of the true 10 nearest neighbors returned). Numbers are
shown for both 256-dimensional and 1024-dimensional vectors.

| Index (256-dim) | ms/query | speedup | recall@10 |
| --- | --- | --- | --- |
| flat (sequential scan) | 1.69 | 1.0x | 1.000 |
| ivf nprobe=8 | 0.29 | 5.8x | 0.444 |
| ivf nprobe=20 | 0.69 | 2.4x | 0.664 |
| ivf int8, oversample=4 | 0.85 | 2.0x | 0.657 |
| rescore bit, oversample=8 | 0.98 | 1.7x | 0.282 |
| rescore int8, oversample=4 | 3.26 | 0.5x | 1.000 |
| diskann int8 | 2.41 | 0.7x | 0.698 |

| Index (1024-dim) | ms/query | speedup | recall@10 |
| --- | --- | --- | --- |
| flat (sequential scan) | 7.32 | 1.0x | 1.000 |
| ivf nprobe=8 | 1.08 | 6.8x | 0.267 |
| ivf nprobe=20 | 2.57 | 2.9x | 0.459 |
| ivf int8, oversample=4 | 3.55 | 2.1x | 0.419 |
| rescore bit, oversample=8 | 1.08 | 6.8x | 0.293 |
| rescore int8, oversample=4 | 13.83 | 0.5x | 0.994 |
| diskann int8 | 7.65 | 1.0x | 0.599 |

**How the picture changes from 256 to 1024 dimensions:**

- The **sequential scan gets ~4.3x slower** (1.69 → 7.32 ms) because its cost is
dominated by per-vector distance math, which scales with dimensionality. This
is precisely why an approximate index becomes more valuable as vectors grow.
- **`rescore` with bit quantization jumps from 1.7x to 6.8x.** Its coarse scan
reads a 1-bit-per-dimension copy — 32x smaller than float32 — and that memory
saving matters far more when each full vector is 4&nbsp;KB. It becomes as fast
as IVF, though bit quantization is lossy, so recover recall by raising
`oversample`.
- **IVF keeps its 2–7x speedup** with the same tunable recall/speed trade-off
via `nprobe`.
- **`rescore` with int8 preserves near-perfect recall** (0.994) at a latency
cost, since it still touches every vector in the coarse pass.
- **DiskANN's per-row insert cost scales poorly with dimension** (roughly 68s to
load 20k rows at 256-dim vs 250s at 1024-dim). Graph indexes are designed for
much larger collections than this benchmark and, for bulk loads, the batched
insert path (`buffer_threshold`) rather than the default per-row path.

> **Note on recall:** the recall figures above use synthetic Gaussian clusters,
> whose neighborhoods overlap heavily at high dimensionality (the "curse of
> dimensionality"), so the 1024-dim recall looks pessimistic. Real embeddings
> carry genuine semantic structure and recall considerably better — treat these
> tables as a latency/throughput comparison rather than an absolute recall
> guide, and always measure recall on your own data.

## Sponsors

Development of `sqlite-vec` is supported by multiple generous sponsors! Mozilla
Expand Down
Loading