fix(index): drop stale per-segment rows in vector search after in-place column update#7371
Open
wombatu-kun wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Xuanwo
reviewed
Jun 19, 2026
Xuanwo
left a comment
Collaborator
There was a problem hiding this comment.
- Segment restrictions are applied only after early and late vector-search accounting has already counted stale rows. Stale candidates can consume the shared search budget and leave fewer than
kvalid results. - The commit metadata includes a disallowed co-author trailer. This violates the repository commit policy.
cd6d6ef to
6729af7
Compare
Contributor
Author
|
Xuanwo
reviewed
Jun 21, 2026
| // Drop stale rows from this segment's search output (rows whose | ||
| // fragment the segment no longer owns). The shortcut path in | ||
| // late_search restricts its emitted rows with the same mask. | ||
| let restricted = combined.map(move |batch_res| { |
Collaborator
There was a problem hiding this comment.
This path applies the segment restriction only after the shared early/late search accounting has already counted the unmasked results. A stale hit can satisfy the coordinator and then be dropped, so the query can return fewer than k valid rows or miss the fresh row from the owning segment.
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.
Closes #7370
Problem
A KNN vector query can return an updated row twice after an in-place column update (
LanceFragment.update_columns+LanceOperation.Update) followed byoptimize_indices(num_indices_to_merge=0). One copy carries the stale pre-update vector (from the original index segment) and one carries the new value (from the new delta segment). The same stale segment also misranks the row for queries near its old vector.Root cause
Index coverage is tracked per fragment via
IndexMetadata.fragment_bitmap. An in-place column update keeps the fragment id and row address, and committing theUpdateprunes the fragment from the old segment's bitmap, but the old segment's index file still physically contains the row's old vector. A delta optimize then builds a new segment covering that fragment with the new vector while leaving the old segment in place. At query time the sharedDatasetPreFilteris built from the union of all segment bitmaps, so it cannot tell "fragment N is valid for the delta segment but stale for the old segment", and there is no cross-segment row-id dedup on the vector path.Fix
Restrict each index segment's vector-search output to the fragments it actually owns, in
ANNIvfSubIndexExec(knn.rs):fragment_bitmap, using a mask from the existingDatasetPreFilter::create_restricted_deletion_mask(correct for both row-address and stable-row-id datasets). This removes the stale duplicate served by the old segment.late_search"fewer than k results" shortcut returns prefilter-matched rows the partition search did not reach. It previously emitted the whole set from the first delta. With a per-segment restriction active, each delta now emits only the not-found addresses its own segment owns (segments partition the fragments, so each is emitted exactly once and stays deterministic). Without a restriction the original first-delta-only path is unchanged.Both are gated on every segment having a
fragment_bitmapand are pass-through no-ops for normal append/merge indices.Tests
Adds
test_no_stale_duplicate_after_partial_column_updatetopython/python/tests/test_vector_index.py, which reproduces the multi-segment duplicate and asserts the updated row is returned exactly once (fails before the fix with two copies, passes after). The existing Rusttest_fewer_than_k_results(multi-delta prefilter shortcut) and the #6877 single-segment masking guard continue to pass.Notes
The per-segment mask is reused from
create_restricted_deletion_mask, so it isNone(pass-through) for normal single-segment or fully-merged indices and adds no work there.One narrow limitation remains: the early/late-search coordinator counts a segment's rows before the post-filter drops stale ones. In the rare combination of a selective prefilter plus stale rows from an in-place update where the fresh row lives in an unsearched partition, the owning segment's shortcut may skip re-emitting it (a small recall reduction, never a duplicate or a wrong row). Removing this would require pushing the per-segment restriction into the shared, cross-delta prefilter and its shortcut, a much larger change.