Skip to content

fix(mem_wal): build the transient FTS index with positions - #9386

Merged
hamersaw merged 1 commit into
lance-format:mainfrom
hamersaw:feat/memwal-transient-fts-positions
Sep 18, 2026
Merged

hamersaw merged 1 commit into
lance-format:mainfrom
hamersaw:feat/memwal-transient-fts-positions

Conversation

@hamersaw

Copy link
Copy Markdown
Contributor

Problem

A phrase query over a column with no FTS index silently returns nothing from the fresh tier while a plain match on the same rows succeeds.

The base answers that query: plan_phrase_query's no-index arm plans a flat scan, where token positions are implicit. The fresh tier serves an unindexed column through a transient index built over the visible prefix (transient_fts_index_store), and with no persisted index to inherit from it takes InvertedIndexParams::default() — whose with_position is false. search_phrase_tokens then returns Vec::new() for any multi-token phrase (index/fts.rs, "without them phrase search is unsupported, as on disk"). So every phrase hit in un-compacted rows is dropped, silently, on exactly the tables where the fresh tier exists to make those rows visible.

Verified against a live WAL pod (lancedb/sophon): unindexed column, match body:alpha[1, 2]; phrase body:'alpha prose'[].

Change

When no persisted index covers the column, the transient index is built with positions. The persisted-index case is untouched — it still inherits the index's analyzer and positional settings, which is the contract that keeps active rows agreeing with base and SSTable rows. The transient index lives for one query over the visible prefix, so the extra position storage is bounded by that prefix.

Test

phrase_over_an_unindexed_column_reaches_the_active_memtable: an active memtable with a PK index and no FTS index; a match over alpha returns rows 1 and 2 (precondition: the transient path serves the column), and the phrase alpha prose returns row 1 only — row 2 carries the terms reversed.

cargo test -p lance --lib mem_wal::scanner::fts_search: 41 passed.

Sibling of #9363 (multi-match shapes on the fresh tier); independent of it.

🤖 Generated with Claude Code

A phrase query over a column with no FTS index returned nothing from the
fresh tier while a plain match on the same rows succeeded. The base
answers that query — `plan_phrase_query`'s no-index arm plans a flat
scan, where token positions are implicit — but the transient index the
fresh tier builds for an unindexed column took `InvertedIndexParams::
default()`, whose `with_position` is `false`, and `search_phrase_tokens`
returns no hit for a multi-token phrase without positions. Every phrase
hit in un-compacted rows was dropped, silently.

When no persisted index covers the column, the transient index now
carries positions. The persisted-index case still inherits the index's
analyzer and positional settings, which is the contract that keeps the
active rows agreeing with base and SSTable rows. The transient index
lives for one query over the visible prefix, so the extra storage is
bounded by that prefix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Sep 18, 2026
@hamersaw
hamersaw marked this pull request as ready for review September 18, 2026 13:43

@lance-gatekeeper lance-gatekeeper 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.

Gate recommendation: approve.

The fix aligns the transient fresh-tier index with the base table’s unindexed phrase-query semantics while leaving persisted-index analyzer and positional settings unchanged. The added regression covers the no-index path and distinguishes ordered phrases from reversed terms. Reusing the transient index also keeps phrase and compound queries on the existing evaluator instead of adding a separate memtable flat-scan path.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 18, 2026
@hamersaw
hamersaw merged commit 55400e0 into lance-format:main Sep 18, 2026
38 of 39 checks passed
@hamersaw
hamersaw deleted the feat/memwal-transient-fts-positions branch September 18, 2026 16:41
hamersaw added a commit to hamersaw/lance that referenced this pull request Sep 18, 2026
Resolves the test-module conflict in fts_search.rs with lance-format#9386: both
sides added tests at the same anchor; keep all of them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants