Repository navigation
Conversation
MayCXC
force-pushed
the
knn-topk-heap-photostructure
branch
2 times, most recently
from
October 22, 2026 18:48
0eebde1 to
94ff073
Compare
This was referenced Oct 22, 2026
Closed
min_idx() chose the k nearest candidates of a chunk by scanning the whole chunk once per result, k times n comparisons. With the default chunk size of 1024, a KNN query with k of 1024 or more spent about a million comparisons per chunk: on a 10,000-row float[768] table, k = 1000 and k = 4096 took 17.8 and 18.0 ms against 8.7 ms for k = 1. Upstream has the fix behind a build option: asg017/sqlite-vec 0de765f added SQLITE_VEC_EXPERIMENTAL_MIN_IDX, a min_idx() that keeps the k best candidates in a binary heap in the output array, whose root is the one that ranks last, so a chunk costs n log k comparisons, then sorts the heap in place. That heap compares distances alone, so tied rows come back in another order than the scan's: built with the option, 779 of 1,000 tie-heavy queries on asg017's main return other rows or another order. This makes that heap the only min_idx() and gives it one total order, min_idx_after(): by distance, between equal distances the larger index first, as the scan's <= chose, and a NaN distance, which a vec0 table cannot produce because it refuses NaN and infinite elements but a static blob can, after every number instead of wherever its position put it. The bitmap of taken slots the callers allocated for the scan goes with it, from the KNN loop, the static blob filter and the declaration in tests/sqlite-vec-internal.h. Measured on that table (medians of 5 interleaved runs of 7 queries), k = 1000 and k = 4096 take 9.3 ms; k = 100 takes 8.8 ms instead of 9.2, and k = 1 8.7 ms either way. Over 4,000 random KNN queries on tables built to tie heavily (vectors on a 5 by 3 grid, chunk sizes 8 to 64, deletes, metadata filters, k from 1 to 1000), every result matches the previous selection row for row; with the tie rule reversed, 827 of 1,000 such queries differ. tests/test-knn-topk.py checks the k nearest against a brute-force order for k below, at and above the chunk size, with and without a filter, pins the order of tied rows, and checks that a static blob vector with a NaN element comes back last; the scan returned it first, from slot 0.
MayCXC
force-pushed
the
knn-topk-heap-photostructure
branch
from
October 23, 2026 03:33
94ff073 to
696cff5
Compare
This was referenced Oct 23, 2026
mceachen
added a commit
that referenced
this pull request
Oct 23, 2026
#2 ranks a NaN distance after every number within a chunk, but merge_sorted_lists() put a later chunk's NaN ahead of the numbers already merged, which a vec0 table written before 2.1.0 can still produce. Its CHANGELOG entry now also says what changes on such tables.
Member
|
Landed as 8ffbdf1, thanks. Follow-up a791836: |
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.
min_idx()chose the k nearest candidates of a chunk by scanning the whole chunk once per result, k times n comparisons. With the default chunk size of 1024, a KNN query with k of 1024 or more spends about a million comparisons per chunk. On a 10,000-rowfloat[768]table (medians of 5 interleaved runs, each the median of 7 queries):Upstream already has the fix behind a build option: asg017/sqlite-vec@0de765f added
SQLITE_VEC_EXPERIMENTAL_MIN_IDX, amin_idx()that keeps the k best candidates in a binary heap in the output array, whose root is the one that ranks last, so a chunk costs n log k comparisons, then sorts the heap in place. That heap compares distances alone, so tied rows come back in another order than the scan's: built with the option, 779 of 1,000 tie-heavy queries on asg017's main return other rows or another order than its default build.This makes that heap the only
min_idx()and gives it one total order,min_idx_after(): by distance, between equal distances the larger index first, as the scan's<=chose, and a NaN distance after every number instead of wherever its position put it. Avec0table cannot produce a NaN distance, since it rejects NaN and infinite elements, but a static blob can. The bitmap of taken slots the callers allocated for the scan goes with it, from the KNN loop, the static blob filter and the declaration intests/sqlite-vec-internal.h.To check the order is unchanged, I ran 4,000 random KNN queries against main and this branch on tables built to tie heavily (vectors on a 5 by 3 grid, chunk sizes 8 to 64, deletes, metadata filters, k from 1 to 1000), each build in its own process: every result matched row for row. With the tie rule reversed, 827 of 1,000 such queries differ.
tests/test-knn-topk.pychecks the k nearest against a brute-force order for k below, at and above the chunk size, with and without a filter, pins the order of tied rows, and checks that a static blob vector with a NaN element comes back last. On main the first two pass and the third fails: the scan returns the NaN vector first, from slot 0. The 289 snapshot tests, the full suite,make test-unitandmake test-asanpass, andtest-loadable.pyandtest-knn-topk.pypass against the AddressSanitizer build. The commit adds a### Changedentry under[Unreleased]in CHANGELOG.md.The first version of this branch carried a heap of its own with a separate scratch buffer; it was force-pushed to this port of upstream's, which needs no scratch and runs as fast. The timings above were re-measured on an idle machine, which is why they are lower than the first version's.
The same change is open on vlasky/sqlite-vec as vlasky#18 and upstream as asg017#337, which makes the build option's heap the default. There it also fixes a wrong answer: asg017 accepts zero vectors in cosine columns, their distance is NaN, and its scan returned such a row as the nearest whenever it sat in a chunk's first free slot.