Repository navigation
Conversation
KNN queries on vec0 tables using INDEXED BY rescore(...) silently ignored `distance <op> ?` constraints. The standard chunk-scan path filters candidates against parsed distance constraints, but the rescore dispatch (rescore_knn) never applied them. Apply the constraints to the rescored float distances in phase 2, before top-k selection, so they target the same distance the `distance` column reports. Handle the now-possible zero-surviving-candidates case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 branch has not been deployed
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.
Summary
Fixes #308. On
vec0tables created withINDEXED BY rescore(...), KNN queries silently ignoreddistance <op> ?constraints:This returned the top-k by distance without filtering out rows above the threshold.
Root cause
The standard chunk-scan path filters candidates against the distance constraints parsed from
idxStr/argv. Rescore columns dispatch torescore_knn, which received those constraints but never applied them.Fix
Apply the constraints in
rescore_knnphase 2, after the exact float distances are computed and before top-k selection — so they target the final rescored distance (what thedistancecolumn reports), not the coarse quantized distance from phase 1. Candidates failing aGE/GT/LE/LTconstraint are dropped, with an early-out when none survive.Testing
test_knn_distance_constraint_leandtest_knn_distance_constraint_lt_gttotests/test-rescore.py; both fail onmainand pass with this change.🤖 Generated with Claude Code