Repository navigation
Add a new rescore ANN index for vec0 tables - #276
Merged
Merged
Conversation
Add approximate nearest neighbor infrastructure to vec0: shared distance dispatch (vec0_distance_full), flat index type with parser, NEON-optimized cosine/Hamming for float32/int8, amalgamation script, and benchmark suite (benchmarks-ann/) with ground-truth generation and profiling tools. Remove unused vec_npy_each/vec_static_blobs code, fix missing stdint.h include.
Add rescore index type: stores full-precision float vectors in a rowid-keyed shadow table, quantizes to int8 for fast initial scan, then rescores top candidates with original vectors. Includes config parser, shadow table management, insert/delete support, KNN integration, compile flag (SQLITE_VEC_ENABLE_RESCORE), fuzz targets, and tests.
realgetOff
pushed a commit
to realgetOff/sqlite-vec
that referenced
this pull request
Aug 4, 2026
vec0BestIndex() sets aConstraintUsage[].omit = 1 on constraints against the `distance` column, which tells SQLite the vtab will enforce them itself and lets SQLite drop the term from the query plan. Only the FLAT chunk scan ever read those constraints back out of idxStr, so on rescore, DiskANN and IVF columns the predicate was silently discarded and rows violating it were returned with no error. This was a regression: asg017#166 added the distance constraints, and the ANN backends merged six weeks later (asg017#276, asg017#277, asg017#278) never wired them up. Introduce vec0_distance_constraints_satisfied() as the single point of truth for the predicate and route every backend through it: - FLAT keeps its pre-filter semantics; its four nested switch cases collapse into one loop over the chunk bitmap (net -44 lines, snapshots unchanged). - rescore filters the rescored float distances before the top-k truncation, so a lower-bound constraint still yields k rows. Coarse quantized distances from phase 1 are not comparable to a user-supplied threshold, so the filter cannot be pushed down into the quantized scan. - DiskANN and IVF compact their final result sets. Also fixes a latent issue on the rescore path: when the constraint filters out every candidate, result_k becomes 0 and sqlite3_malloc(0) returns NULL, which the existing check would have misreported as SQLITE_NOMEM. Adds parametrized coverage over flat / rescore-bit / rescore-int8 / diskann. 17 of the new assertions fail without this change and pass with it. Fixes asg017#308
realgetOff
pushed a commit
to realgetOff/sqlite-vec
that referenced
this pull request
Aug 4, 2026
vec0BestIndex() sets aConstraintUsage[].omit = 1 on constraints against the `distance` column, which tells SQLite the vtab will enforce them itself and lets SQLite drop the term from the query plan. Only the FLAT chunk scan ever read those constraints back out of idxStr, so on rescore, DiskANN and IVF columns the predicate was silently discarded and rows violating it were returned with no error. This was a regression: asg017#166 added the distance constraints, and the ANN backends merged six weeks later (asg017#276, asg017#277, asg017#278) never wired them up. Introduce vec0_distance_constraints_satisfied() as the single point of truth for the predicate and route every backend through it: - FLAT keeps its pre-filter semantics; its four nested switch cases collapse into one loop over the chunk bitmap (net -44 lines, snapshots unchanged). - rescore filters the rescored float distances before the top-k truncation, so a lower-bound constraint still yields k rows. Coarse quantized distances from phase 1 are not comparable to a user-supplied threshold, so the filter cannot be pushed down into the quantized scan. - DiskANN and IVF compact their final result sets. Also fixes a latent issue on the rescore path: when the constraint filters out every candidate, result_k becomes 0 and sqlite3_malloc(0) returns NULL, which the existing check would have misreported as SQLITE_NOMEM. Adds parametrized coverage over flat / rescore-bit / rescore-int8 / diskann, plus ivf behind a build-flag skipif mirroring tests/conftest.py. 17 of the new assertions fail without this change on a default build (22 with IVF enabled) and all pass with it. Fixes asg017#308
mceachen
pushed a commit
to photostructure/sqlite-vec
that referenced
this pull request
Nov 2, 2026
A float vector column declared `indexed by rescore(quantizer=bit)` or `indexed by rescore(quantizer=int8)`, with an optional `oversample=N` from 1 to 128 (8 by default), also keeps its vectors quantized in _rescore_chunksNN, in the layout of _vector_chunksNN, and its float32 vectors by rowid in _rescore_vectorsNN. A KNN query scans the quantized vectors for the k * oversample nearest rows, then ranks those candidates by the distances of their float32 vectors. The `oversample=N` command sets the oversample of a connection's queries. A full scan reads a rescore column's vectors row by row through vec0_get_vector_data(), and a flat column's by chunk as before. Ported from asg017#276 (ba0db0b) as asg017's main has it at 04d28bd: with the int8 quantizer that 575371d fixed, the fixes 5e4c557, 82f4eb0 and 5522e86, auxiliary columns (b7fc459), the oversample=N command and its unknown-command message (6e2c4c6), and the distance constraints of the unmerged asg017#309. SQLITE_VEC_ENABLE_RESCORE=0 builds without it. Where this port differs from upstream: - rescore_knn() takes vec0Filter_knn_chunks_iter()'s arguments and results, so vec0Filter_knn() picks between the two and the MMR rerank runs after either. It reports errors as the chunk scan does, so an interrupt stays an interrupt. It picks each chunk's candidates with this fork's min_idx(), whose heap needs no bitmap of taken slots. - Distance constraints with a NULL, TEXT or BLOB value act as they do for a flat column. - optimize, which upstream lacks, writes a moved row's quantized vectors to its new slot and leaves its row of _rescore_vectorsNN, whose rowid does not change, and deletes the old _rescore_chunksNN rows with the old chunks. A DELETE that empties a chunk deletes its _rescore_chunksNN row too. - A point query for a rowid the table does not have returns no row, since vec0_get_vector_data() finds the row's position first, as for a flat column; upstream fails with "Could not fetch vector data". - _rescore_chunksNN declares "rowid INTEGER PRIMARY KEY", as this fork's new tables do, and an oversample=N value must be all digits. The tests are upstream's test-rescore.py and test-rescore-mutations.py, the rescore cases of its test-auxiliary.py and test-rename.py, its rescore fuzz targets, and test-unit.c's bit quantizer, byte size and parser cases. Upstream's int8 quantizer unit tests, and the uniform-vector case of rescore-quantize-edge.c, test the min/max quantizer that 575371d replaced, so they are written here for the current one. Modes 1 and 2 of rescore-quantize-edge.c copy the fuzz bytes out to float alignment before reading them as floats, as asg017#343 has them; upstream's read floats one byte past the mode byte, a misaligned load UBSan reports. New tests check that each row's quantized slot holds the quantized form of its vector through inserts, updates, deletes that empty chunks, and optimize, and rescore-optimize.c fuzzes optimize. test-fullscan.py reads a rescore column beside a flat one, and runs its delete and optimize cases on a rescore column too; read by chunk, a rescore column fails all three with "Could not fetch vector data".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds the first ANN index to
sqlite-vec, called "rescore". A rescoreindex quantizes input vectors into int8/bit vectors, and performs an oversampled
KNN search on that smaller vector space, then re-ranks or "rescores" those
distances with the full-sized vectors.
The
INDEXED BY rescore()is the new syntax that signifies an index on theheadline_embeddingcolumn.In the KNN search shown above,
sqlite-vecwill first perform a coarse KNNsearch on the binary quantized-vectors of the
headline_embeddingcolumn. It"oversamples" on
k * oversample, meaning it will find the top8 * 10closestvectors based on hamming distance of the bit quantized vectors in the index.
Then, it will rescore those 80 vectors based on their full-sized float
embedidngs, and return the top 10 values.
Well-versed
sqlite-vecusers may notice that this is essentially the samething as the
old
sqlite-vecBinary Quantization re-scoringmethod that the doc site recommends:
Besides being way less verbose, this new
rescoreindex is slightly faster thandoing old manual method. Full-size vectors are stored in a key-value table
instead of
vec0's chunked storage, meaning the re-scoring stage are a bunch ofb-tree lookups instead of overflow-page nightmares. Granted it's not a huge
difference, maybe 10-15% faster reads, but it's something!
Benchmarks
First off, benchmarks are a notoriously difficult thing to get right:
better than others.
quantizations, others are not.
lookup faster, others may not.
So take these benchmarks with a grain of salt, try it on your own data.
But for my use-case on my hardware (Macbook Pro M4) on a semantically diverse
datasette of 1 million New York Times headlines embeded with
mixedbread-ai/mxbai-embed-large-v1,I get:
vec0Configurationrescoreint8,oversample=2rescorebit,oversample=8rescorebit,oversample=4The int8 rescore performs extremely well at preserving precision, with perfect
recall at
K=10queries while being more than twice as fast. Again, themixedbread-ai/mxbai-embed-large-v1model was specifically trained on binary atint8 quantization, so your mileage may vary.
The binary rescore index performs very well too -
0.988recall atoversample=8, and a respectable0.962recall at the loweroversample=4.The speed benefits of hamming distance really shows here - nearly 6 times faster
for
os=8, and more than 6 times faster foros=4!You can choose a different oversample value at query time, so you can tune that
for faster queries, at the expense of recall.
Do note that both
int8andbitquantized rescores take up more disk space,since it hold both full-size vectors and quantized vectors. The
int8vectorstake
Dbytes of space per vector, whilebitvectors takeD / 8bytes ofspace.
Drawbacks
This rescore index is technically still a brute-force search, but instead of
doing full-scans on full-size floating point vectors, it brute-forces much small
binary or int8 vectors.
So eventually while your vector database grows, query time with the
rescoreindex will also grow, roughly linearly.
Also, recall will nearly entirely depend on your embedding model and dataset. If
your embedding model was specifically trained on binary or integer quanitzation,
I suspect you'll have a great time using
rescore. If not, YMMV but I've seenrecall drop down to like 70-80. Additionally, if your data isn't very
semantically diverse, than quantization may muddle the results.
But in general, if data/embedding model handles quantization well and you can
take a ~10% accuracy hit, I've found that the
rescoreindex to be fantastic!