Skip to content

fix(fts): re-evaluate overlay-stale rows without segment scans - #7975

Open
Xuanwo wants to merge 11 commits into
mainfrom
xuanwo/fts-overlay-row-level-bench-20260724
Open

fix(fts): re-evaluate overlay-stale rows without segment scans#7975
Xuanwo wants to merge 11 commits into
mainfrom
xuanwo/fts-overlay-row-level-bench-20260724

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Why

FTS indexes cover immutable source rows, but field overlays can replace values after the index is built. Treating any intersecting overlay as invalidating the whole segment means that updating one row can turn an indexed FTS query into a flat scan across every covered fragment.

This keeps modern FTS segments indexed, blocks only the exact stale row addresses from index results, and re-evaluates those rows through a targeted take. Legacy segments without row-level coverage retain the conservative full-scan fallback. Match and phrase queries use the same overlay-aware path, including flat phrase evaluation for stale or unindexed rows.

Benchmark

Measured at commit 6687aa1eaf4b6bfbc3c49e5646a9b86820700eba on an AWS EC2 c7i.4xlarge instance (16 vCPU, 32 GiB), Amazon Linux 2023, using the release-with-debug profile. The dataset contains 1,000,000 rows in 10 fragments and one FTS segment. Each result is the mean of 10 measured queries after one warm-up query.

The FTS query searches for one unaffected row in the same fragment as the stale row:

Text overlay layers Mean latency
0 1.0 ms
1, covering one row 2.0 ms

The same benchmark also checks the shared overlay path for scalar and vector indexes:

Workload Overlay layers Mean latency
BTree, match in overlaid fragment 0 / 1 / 4 / 16 0.7 / 1.6 / 1.6 / 1.6 ms
BTree, match in another fragment 0 / 1 / 4 / 16 0.7 / 1.5 / 1.6 / 1.7 ms
Vector ANN 0 / 1 22.6 / 28.0 ms
cargo test -p lance --lib --profile release-with-debug -- overlay_index_masking::bench --ignored --nocapture --test-threads=1

These are same-head overlay-overhead measurements, not a cross-version speedup claim. A plan-level regression test separately asserts that stale FTS rows are re-evaluated through a targeted take instead of a fragment scan.

At the same head, the focused AWS validation passed all 27 non-benchmark overlay/index tests and both FTS segment-selection tests.

Current head ad7fa79dc920e6df1e97ba8c2cd6fc0a2497c472 only adds the explicit empty-fragment short circuit and review-requested regression tests, so it does not affect the benchmarked non-empty query path. On the same AWS host, the current head passed both new focused tests, the existing delete-all inverted-index regression, cargo fmt --all -- --check, and cargo clippy --all --tests --benches -- -D warnings.


Most changes in this PR are tests.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

FTS phrase and overlay execution

Layer / File(s) Summary
Flat phrase matching core
rust/lance-index/src/scalar/inverted/index.rs
Adds slop-aware phrase tokenization, position matching, phrase filtering during BM25 scoring, list-document support, a public phrase-search stream, and unit tests.
Executor phrase and overlay wiring
rust/lance/src/io/exec/fts.rs, rust/lance/src/io/exec/utils.rs
Adds phrase-mode flat execution and propagates overlay row masks through FTS executors and prefilters.
Row-level overlay planning
rust/lance/src/dataset/scanner.rs, rust/lance/src/dataset/overlay.rs
Plans unchanged, row-level, and full-scan overlay paths, masks indexed stale rows, evaluates stale and unindexed rows flat, and combines results.
Phrase and overlay validation
rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs
Expands stable-row-id, phrase, stale-row, fast-search, filtered-scan, execution-plan, and FTS benchmark coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scanner
  participant fts_overlay_plan
  participant PhraseQueryExec
  participant FlatMatchQueryExec
  participant flat_phrase_search_stream_with_metrics
  Scanner->>fts_overlay_plan: compute stale rows and overlay mode
  fts_overlay_plan->>PhraseQueryExec: apply overlay block to indexed execution
  fts_overlay_plan->>FlatMatchQueryExec: provide stale and unindexed rows
  FlatMatchQueryExec->>flat_phrase_search_stream_with_metrics: evaluate phrase slop
  Scanner->>Scanner: combine indexed and flat results
Loading

Possibly related PRs

Suggested reviewers: wjones127, brendanclement, yanghua

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main FTS overlay fix and the move away from segment-wide scans.
Description check ✅ Passed The description directly matches the PR's FTS overlay handling changes, tests, and benchmarks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch xuanwo/fts-overlay-row-level-bench-20260724

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added A-index Vector index, linalg, tokenizer bug Something isn't working labels Jul 24, 2026
@Xuanwo
Xuanwo marked this pull request as ready for review July 24, 2026 09:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
rust/lance/src/dataset/scanner.rs-3389-3391 (1)

3389-3391: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Comment describes the old fragment-level behavior. This PR moves overlay handling to row granularity: stale rows are masked out of the index result and re-evaluated through a targeted take, not "drop stale segments" and re-scan "affected fragments." Please update the wording to match the row-level semantics.

📝 Suggested rewording
-        // Data overlay masking: match and phrase queries drop stale segments and re-evaluate the
-        // affected fragments on their flat-text paths. This keeps stale index hits out while
-        // preserving current-value matches unless `fast_search` explicitly skips flat evaluation.
+        // Data overlay masking: match and phrase queries block overlay-stale rows from the indexed
+        // result and re-evaluate just those rows (plus any unindexed fragments) on the flat-text
+        // path. This keeps stale index hits out while preserving current-value matches unless
+        // `fast_search` explicitly skips flat evaluation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/scanner.rs` around lines 3389 - 3391, Update the data
overlay masking comment near the scanner logic to describe row-level behavior:
stale rows are masked from index results and re-evaluated through a targeted
take, while current-value matches remain preserved unless fast_search skips flat
evaluation. Remove references to stale segments and affected-fragment rescans.

Source: Coding guidelines

🧹 Nitpick comments (1)
rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs (1)

793-892: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parameterize the new row-level masking/targeted-take tests over stable_row_ids.

test_fts_overlay_row_level_masking_under_fast_search and test_fts_overlay_flat_path_takes_only_stale_rows validate the core row-level re-evaluation feature introduced by this PR, but both hardcode create_text_dataset(false). The sibling stale-drop tests were upgraded to #[rstest] + #[values(false, true)] specifically because overlay masking is expected to behave consistently across stable vs. non-stable row-id modes; these two new tests should get the same treatment to catch row-addressing regressions specific to enable_stable_row_ids.

♻️ Suggested parameterization
-#[tokio::test]
-async fn test_fts_overlay_row_level_masking_under_fast_search() {
-    let mut dataset = create_text_dataset(false).await;
+#[rstest]
+#[tokio::test]
+async fn test_fts_overlay_row_level_masking_under_fast_search(#[values(false, true)] stable_row_ids: bool) {
+    let mut dataset = create_text_dataset(stable_row_ids).await;

Apply the same change to test_fts_overlay_flat_path_takes_only_stale_rows.

Based on learnings, the graph context notes that the stable_row_ids test parameter exists precisely so overlay masking/stale-row behavior can be verified consistent across both row-id modes; that rationale applies equally to these two new tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs` around lines
793 - 892, Parameterize both
test_fts_overlay_row_level_masking_under_fast_search and
test_fts_overlay_flat_path_takes_only_stale_rows with rstest and values(false,
true) for stable_row_ids. Accept the parameter in each test and pass it to
create_text_dataset instead of hardcoding false, preserving all existing
assertions and test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Other comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 3389-3391: Update the data overlay masking comment near the
scanner logic to describe row-level behavior: stale rows are masked from index
results and re-evaluated through a targeted take, while current-value matches
remain preserved unless fast_search skips flat evaluation. Remove references to
stale segments and affected-fragment rescans.

---

Nitpick comments:
In `@rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs`:
- Around line 793-892: Parameterize both
test_fts_overlay_row_level_masking_under_fast_search and
test_fts_overlay_flat_path_takes_only_stale_rows with rstest and values(false,
true) for stable_row_ids. Accept the parameter in each test and pass it to
create_text_dataset instead of hardcoding false, preserving all existing
assertions and test behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 77225985-da5e-4cfb-8fa3-2e7e143f69f7

📥 Commits

Reviewing files that changed from the base of the PR and between 71b4515 and 6615879.

📒 Files selected for processing (6)
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance/src/dataset/overlay.rs
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs
  • rust/lance/src/io/exec/fts.rs
  • rust/lance/src/io/exec/utils.rs

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.22425% with 121 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/dataset/scanner.rs 73.86% 49 Missing and 20 partials ⚠️
rust/lance-index/src/scalar/inverted/index.rs 88.11% 41 Missing and 7 partials ⚠️
rust/lance/src/io/exec/fts.rs 95.29% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rust/lance/src/dataset/scanner.rs (2)

3562-3677: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Substantial duplicated overlay-planning logic between plan_phrase_query and plan_match_query.

These two functions replicate the same sequence almost line-for-line: computing target_fragments/unindexed_fragments, the "all-unindexed + fast_search" early return, calling fts_overlay_plan and destructuring FtsOverlayPlan, building the overlay block mask, applying it to the indexed exec, and deciding whether to build a flat plan. The only real difference is the query/exec type (PhraseQuery/PhraseQueryExec vs MatchQuery/MatchQueryExec).

Because this duplicated block encodes the overlay-masking safety invariant (blocking stale rows from the indexed path and re-evaluating them via the flat path), a future fix or edge case addressed in one function but missed in the other would silently reintroduce stale-row leakage for one of the two query types. Consider factoring the shared planning steps (fragment partitioning, fts_overlay_plan resolution, overlay-block computation) into a helper that returns the pieces needed by each caller to build its specific *QueryExec.

♻️ Sketch of shared extraction
struct FtsPlanContext {
    target_fragments: Vec<Fragment>,
    unindexed_fragments: Vec<Fragment>,
    stale_rows: HashMap<u32, RoaringBitmap>,
    preset_segments: Option<Vec<IndexMetadata>>,
    overlay_block: Option<RowAddrMask>,
}

async fn resolve_fts_plan_context(
    &self,
    column: &str,
    index_name: &str,
) -> Result<Option<FtsPlanContext>> {
    // shared target_fragments / unindexed_fragments / fts_overlay_plan / overlay_block logic
    // returns None to signal the "return early" cases (all-unindexed, FullScan) are handled by caller
}

Also applies to: 3679-3807

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/scanner.rs` around lines 3562 - 3677, Extract the
duplicated fragment partitioning, all-unindexed/FullScan handling,
FtsOverlayPlan resolution, stale-row mask construction, and flat-plan
eligibility logic from plan_phrase_query and plan_match_query into a shared
helper or context type. Have both callers reuse that shared result while
retaining their query-specific PhraseQueryExec and MatchQueryExec construction,
including identical early-return behavior and overlay masking.

3894-3933: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Remove the duplicate refine filter from plan_flat_fts_input.

In both the new filtered-read path and the prefilter legacy scan_fragments path, the refine predicate is already applied by filtered_read; wrapping filter_plan.refine_expr again runs the same filter twice. Use an unfiltered flat fragment/stale input here, matching the existing KNN flat-fallback pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/scanner.rs` around lines 3894 - 3933, Remove the
explicit LanceFilterExec wrapping filter_plan.refine_expr in
plan_flat_fts_input, since filtered_read already applies the refine predicate.
Update both the filtered-read and legacy scan_fragments paths to use unfiltered
flat fragment/stale inputs, matching the existing KNN flat-fallback pattern,
while preserving the projection and alias behavior.
🧹 Nitpick comments (1)
rust/lance/src/dataset/overlay.rs (1)

141-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused fragment-level stale-overlay API.

collect_overlay_stale_frags has no non-test callers outside rust/lance/src/dataset/overlay.rs, while production planning now uses collect_overlay_stale_rows_for_segment. Keep the row-level variant and its tests; delete the fragment-level sibling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/overlay.rs` around lines 141 - 159, Remove the unused
collect_overlay_stale_frags function from overlay.rs, including any
production-only imports or references made unnecessary by its deletion. Preserve
collect_overlay_stale_rows_for_segment and all tests for the row-level behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 3562-3677: Extract the duplicated fragment partitioning,
all-unindexed/FullScan handling, FtsOverlayPlan resolution, stale-row mask
construction, and flat-plan eligibility logic from plan_phrase_query and
plan_match_query into a shared helper or context type. Have both callers reuse
that shared result while retaining their query-specific PhraseQueryExec and
MatchQueryExec construction, including identical early-return behavior and
overlay masking.
- Around line 3894-3933: Remove the explicit LanceFilterExec wrapping
filter_plan.refine_expr in plan_flat_fts_input, since filtered_read already
applies the refine predicate. Update both the filtered-read and legacy
scan_fragments paths to use unfiltered flat fragment/stale inputs, matching the
existing KNN flat-fallback pattern, while preserving the projection and alias
behavior.

---

Nitpick comments:
In `@rust/lance/src/dataset/overlay.rs`:
- Around line 141-159: Remove the unused collect_overlay_stale_frags function
from overlay.rs, including any production-only imports or references made
unnecessary by its deletion. Preserve collect_overlay_stale_rows_for_segment and
all tests for the row-level behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 10b5e9bd-d303-4a58-9160-e38cf7e4f587

📥 Commits

Reviewing files that changed from the base of the PR and between 6615879 and a2ea2cc.

📒 Files selected for processing (2)
  • rust/lance/src/dataset/overlay.rs
  • rust/lance/src/dataset/scanner.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/overlay.rs`:
- Around line 135-136: Restore complete rustdoc for the public
collect_overlay_stale_rows_for_segment function: retain the behavior
description, add intra-doc links to the relevant overlay/segment structs and
methods, and include a compiling # Examples section using its current signature
and types. Keep the example synchronized with the implementation, or make the
function non-public if it is not intended to be part of the public API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 565b8a32-60f4-4867-802f-4e6d0496eb0e

📥 Commits

Reviewing files that changed from the base of the PR and between a2ea2cc and c4e643b.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/overlay.rs

Comment on lines +135 to +136
/// Compute exactly which row offsets within each covered fragment are stale and accumulate them
/// into `stale` (fragment_id → stale row offsets).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Restore complete documentation for the public API.

This comment documents public collect_overlay_stale_rows_for_segment, but the rewrite removed the required compiling example and links to relevant structs/methods. Add a synchronized # Examples section and intra-doc links, or reduce the function’s visibility if it is not intended as public API.

As per coding guidelines: “Document all public APIs with examples and links to relevant structs and methods; keep examples synchronized with actual signatures.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/overlay.rs` around lines 135 - 136, Restore complete
rustdoc for the public collect_overlay_stale_rows_for_segment function: retain
the behavior description, add intra-doc links to the relevant overlay/segment
structs and methods, and include a compiling # Examples section using its
current signature and types. Keep the example synchronized with the
implementation, or make the function non-public if it is not intended to be part
of the public API.

Source: Coding guidelines

@github-actions github-actions Bot added the A-ci CI / build workflows label Jul 24, 2026
@wjones127
wjones127 self-requested a review July 24, 2026 14:52
@wjones127

Copy link
Copy Markdown
Contributor

FYI this tries to solve the same thing as #7952, so we should probably only merge one. I'll look at the difference and see which is a better direction.

…w-level-bench-20260724

# Conflicts:
#	.github/workflows/rust.yml
#	rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs
#	rust/lance/src/io/exec/fts.rs

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs (1)

690-690: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass the new stable_row_ids argument at every call site.

Line 600 makes create_text_dataset require a boolean, but test_ngram_optimize_preserves_overlay_staleness and test_ngram_remap_excludes_newer_overlay_fragments still use the old zero-argument form. The test module will not compile.

Proposed fix
-    let mut dataset = create_text_dataset().await;
+    let mut dataset = create_text_dataset(false).await;

Apply the same change at both call sites and check for any remaining zero-argument calls.

Also applies to: 783-783

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs` at line 690,
Update both create_text_dataset call sites in
test_ngram_optimize_preserves_overlay_staleness and
test_ngram_remap_excludes_newer_overlay_fragments to pass the required
stable_row_ids boolean, matching the intended test configuration. Search the
test module for any remaining zero-argument create_text_dataset calls and update
them as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs`:
- Line 690: Update both create_text_dataset call sites in
test_ngram_optimize_preserves_overlay_staleness and
test_ngram_remap_excludes_newer_overlay_fragments to pass the required
stable_row_ids boolean, matching the intended test configuration. Search the
test module for any remaining zero-argument create_text_dataset calls and update
them as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 89200b4d-0609-4d22-afc2-fdb70ac41673

📥 Commits

Reviewing files that changed from the base of the PR and between de342d8 and 5b39362.

📒 Files selected for processing (4)
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs
  • rust/lance/src/io/exec/fts.rs
  • rust/lance/src/io/exec/utils.rs

@westonpace westonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem like legitimate points from Claude:

  1. !target_fragments.is_empty() guard changes empty-fragment-list behavior (scanner.rs:3599, 3710). With with_fragments(vec![]), the old flat_fragments.len() == target_fragments.len() was 0 == 0 → flat plan over zero fragments → empty result. Now the guard skips that, fts_overlay_plan returns Unchanged (vacuous all() on an empty iterator), and you build a MatchQueryExec over the whole index with PreFilterSource::None. That returns rows from fragments the caller excluded. Narrow, but it's a silent correctness change; worth an early if target_fragments.is_empty() { return EmptyExec }.

  2. There is no test with unindexed fragments and overlay-stale rows simultaneously — the one case where plan_flat_fts_input returns a UnionExec (scanner.rs:3950). Note that unlike the three sibling union sites (3019, 4235, 4396) this one has no RepartitionExec(1); it's only safe because FlatMatchQueryExec::required_input_distribution() is SinglePartition and EnforceDistribution runs in create_plan. That's legitimate but implicit, and it's exactly the shape that breaks if someone reorders the optimizer rules. A test that appends an unindexed fragment, overlays a row in an indexed one, and asserts both surface would pin it down.

@Xuanwo

Xuanwo commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, fixed both in ad7fa79dc.

Explicit empty fragment selections now return EmptyExec for match and phrase queries without changing the empty-dataset index plan. I also added a combined regression test covering indexed rows, overlay-stale rows, and an unindexed fragment. I kept SinglePartition as the FlatMatchQueryExec distribution contract instead of adding a redundant repartition.

The focused tests, the existing delete-all regression, fmt, and full Rust clippy pass on AWS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ci CI / build workflows A-index Vector index, linalg, tokenizer bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants