Apply KNN distance constraints in every index backend - #316
Open
realgetOff wants to merge 1 commit into
Open
Conversation
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
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.
Fixes #308. Diagnosis and reproduction numbers are in this comment.
The bug
vec0BestIndexaccepts constraints on thedistancecolumn and unconditionally setsomit = 1:That tells SQLite the vtab enforces the constraint itself, so SQLite drops the
WHEREterm from the query plan and never re-checks it. But the constraint was only ever read back out ofidxStrinvec0Filter_knn_chunks_iter(FLAT).rescore_knn,vec0Filter_knn_diskannandivf_query_knnnever looked at it, so on those columns the predicate silently vanished and violating rows came back with no error.It's a regression rather than a design gap: #166 landed the distance constraints on 2026-02-13, and the ANN backends (#276, #277, #278) merged on 2026-03-31 without wiring them up.
Measured on 200-300 random
float[8]rows,k = 20, threshold at the median observed distance:DiskANN is the one that bites in practice, since it's on by default (
SQLITE_VEC_ENABLE_DISKANN 1) while IVF is opt-in.AND distance < -1.0also returned a full page of rows instead of none.The change
One helper,
vec0_distance_constraints_satisfied(), becomes the single point of truth for the predicate, soomit = 1has exactly one place it can be honored and a future backend has an obvious thing to call:switchcases collapse into a single loop over the chunk bitmap that calls the helper — net -44 lines, and all 204 existing snapshots stay byte-identical.It also fixes a latent bug on the rescore path that the new filter would otherwise have exposed: when every candidate is filtered out,
result_kbecomes 0,sqlite3_malloc(0)returnsNULL, and the existingif (!out_rowids || !out_distances)check would have reported that asSQLITE_NOMEM.Caveat I'd rather state than hide
On DiskANN and IVF, a lower-bound constraint (
distance >,>=) can return fewer thankrows, because the filter runs over the final result set rather than the candidate pool. Matching FLAT's semantics there means reaching intodiskann_search/ivf_query_knnto widen the search, which felt like scope creep for a correctness fix. The tests encode this distinction explicitly (INDEX_DEFS_FILLING_K) rather than glossing over it — happy to go further if you'd prefer uniform semantics.Verification
New parametrized tests in
tests/test-knn-distance-constraints.pycover all four operators across flat / rescore-bit / rescore-int8 / diskann, plus ivf behind a build-flagskipifmirroringtests/conftest.py.main(22 with IVF enabled), all pass with this changeConflict check: this shouldn't collide with #313 (IVF internals in
sqlite-vec-ivf.c; I only touch the dispatch site insqlite-vec.c) or #311 (DiskANN quantization, not filtering).One unrelated snag for the record:
vendor/isn't in the repo or a submodule, somake loadableandmake clidon't work from a fresh clone — that's #291, and it's the only thing that stood between me and a one-command build.