Skip to content

fix(fts): preserve exact wand score bounds - #8666

Merged
BubbleCal merged 2 commits into
mainfrom
yang/oss-1603-01-wand-exactness
Aug 20, 2026
Merged

fix(fts): preserve exact wand score bounds#8666
BubbleCal merged 2 commits into
mainfrom
yang/oss-1603-01-wand-exactness

Conversation

@BubbleCal

@BubbleCal BubbleCal commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Bug Fix

WAND can visit clauses in a different order from the query's canonical scoring order. Comparing that dynamic f32 sum to the competitive floor before canonical rescoring can drop exact ties. Grouped BM25 and document-weight bounds also need outward rounding, and a zero competitive floor must not erase zero-score membership.

This PR:

  • preserves query-order f32 scoring for final results across classic WAND, MAXSCORE, and bulk AND
  • uses conservative widened sums only for pruning bounds
  • separates inclusive compound-scoring floors from standalone exclusive top-k floors
  • widens BM25 and grouped-posting upper bounds
  • preserves zero-score membership in composable WAND cursors
  • adds adversarial ULP, tie, zero-score, MAXSCORE, bulk-AND, and grouped-bound regressions

This is the first PR in the OSS-1603 stack and only establishes scorer exactness. It does not enable the cross-column planner/executor yet.

Scope boundary

This stack does not include the previously deferred same-column delayed MUST_NOT probing work from OSS-1705. This PR only contains general WAND scoring and bound correctness needed by the cross-column execution path.

Validation

  • cargo test -p lance-index scalar::inverted::wand::tests — 93 passed
  • cargo check -p lance-index --tests
  • cargo clippy -p lance-index --tests -- -D warnings
  • cargo fmt --all -- --check

The full workspace CI matrix is left to GitHub CI.

Stack

  1. This PR: WAND scoring and bound exactness
  2. Posting loading and cache policy
  3. Row-address scorer foundations
  4. Cross-column compound scorer core
  5. Dataset planner / execution integration

Part of OSS-1603.

@github-actions github-actions Bot added A-index Vector index, linalg, tokenizer bug Something isn't working labels Aug 20, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Aug 20, 2026
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Aug 20, 2026

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

Both prior rounding findings are fixed. The revision now preserves canonical f32 scoring while outward-conservative partial, grouped-list, and grouped-block bounds keep inclusive ties and strictly better grouped hits eligible.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Aug 20, 2026
@BubbleCal
BubbleCal merged commit d4e15cc into main Aug 20, 2026
40 checks passed
@BubbleCal
BubbleCal deleted the yang/oss-1603-01-wand-exactness branch August 20, 2026 16:23
BubbleCal added a commit that referenced this pull request Aug 21, 2026
## Other Changes

Read-ahead posting groups reduce storage requests and improve cache
reuse, but a future candidate-driven consumer also needs a way to avoid
reading neighboring token rows on a cold miss.

This PR adds the internal loading infrastructure for that choice:

- introduces `PostingReadPolicy::{ReadAhead, CacheAwareExact}`
- lets exact reads reuse an already-resident read-ahead group
- uses a singleton `[token_id, token_id + 1)` cache entry on a cold
exact miss
- retains cache singleflight for concurrent reads of the same singleton
- coalesces multiple demanded tokens from one group back to one
read-ahead load
- treats repeated logical occurrences of one physical token as one load
demand
- simplifies position-group matching without changing its semantics
- adds deterministic I/O and cache-metric regression coverage

## Behavior and compatibility

There is no public API or file-format change.

The existing production entry point still selects `ReadAhead`. The
`CacheAwareExact` constructor is test-only in this layer and will be
enabled by the later cross-column consumer PR, so merging this PR alone
does not change production query behavior.

A cache-only probe miss is intentionally omitted from the query-level
`MetricsCollector`; only the path that serves the posting records a
query cache hit or miss. The lower-level `LanceCache::stats()` still
observes the probe.

## Scope boundary

This is PR 2 of the OSS-1603 stack and depends only on the exactness
foundation merged in #8666.

It does not include the previously deferred same-column delayed
`MUST_NOT` probing work from OSS-1705, and it does not add or change
Boolean execution.

## Validation

- `cargo fmt --all -- --check`
- `git diff --check origin/main...HEAD`
- regression coverage for cold singleton reads, concurrent singleflight,
warm hits, prewarmed-group reuse, singleton-then-prewarm precedence,
unchanged default read-ahead, same-group multi-term coalescing, and
repeated-token occurrences

Per project workflow, GitHub CI is the authoritative Cargo test and
Clippy run for this rebased head.

## Performance validation

No end-to-end performance benefit is claimed for this layer because
there is no production `CacheAwareExact` caller yet. A main-vs-PR2 query
benchmark would execute the same `ReadAhead` path on both builds and
measure noise rather than this code.

The first consumer PR will benchmark the policy independently with cold
singleton-demand, cold same-group multi-demand, and warm/prewarmed ABBA
cases, including bytes, requests, posting loads, cache metrics, and
exact result digests.

## Stack

1. #8666 — WAND scoring and bound exactness (merged)
2. **This PR:** posting loading and cache policy
3. Row-address scorer foundations
4. Cross-column compound scorer core
5. Dataset planner / execution integration

Part of
[OSS-1603](https://linear.app/lancedb/issue/OSS-1603/add-candidate-driven-execution-for-cross-column-boolean-fts-queries).
BubbleCal added a commit that referenced this pull request Aug 21, 2026
## Feature

Each FTS index source scores in its own partition-local DocId domain.
Cross-column compound queries need to compose those scorers in one
globally ordered row-address domain without assuming independently built
indices share local document layouts.

This PR adds the cross-column scorer core:

- validates and caches strictly ordered row-address projections
- maps local scorer iteration, shallow bounds, and advances into
row-address order
- materializes an exact fallback for reordered projections
- rejects duplicate row-address mappings that could combine content from
different local documents
- lazily merges multiple physical sources for one semantic leaf
- preserves conservative bounds, competitive floors, and equal-score
row-address ties
- analyzes compound plans for positive generator coverage, cost,
feasibility, and conservative score bounds
- supports staged positive generation followed by candidate-scoped
required, optional, and prohibited probing
- performs one exact global top-k collection after composing Match,
Phrase, Boolean, MultiMatch, and Boost scorers
- uses candidate-scoped row-address and scoring-length reads when
staging is selective

The dataset Scanner does not call this core yet; that integration
remains in the final consumer PR.

## API and compatibility

There is no file-format change.

This PR exports the low-level async Rust entry point used by the later
`lance` planner integration:

```rust
pub async fn cross_column_compound_search(
    columns: &[(String, Vec<Arc<InvertedIndex>>)],
    query: &FtsQuery,
    params: &FtsSearchParams,
    prefilter: Arc<dyn PreFilter>,
    metrics: Arc<dyn MetricsCollector>,
) -> Result<(Vec<u64>, Vec<f32>)>
```

The API requires a bounded limit, preserves exact `(score DESC,
row_address ASC)` ordering, and returns an error for unsupported or
internally inconsistent scorer state.

Existing `PartitionDocuments::resolve_addresses` and
`estimated_address_read_bytes` behavior remains unchanged from `main`,
so current production cache and I/O behavior is not altered before
Scanner integration.

## CI failure addressed

The previous head split scorer foundations from their production
consumer, leaving 43 groups of private items unused under `-D
dead-code`. This revision folds the cross-column core into the same PR
so those components have real production call paths.

Unused leaf-role metadata was deleted. No `allow(dead_code)`, test-only
gating, or visibility workaround was added.

## Scope boundary

This is PR 3 of the OSS-1603 stack and builds on #8666 and #8667, both
merged.

It does not include the previously deferred same-column delayed
`MUST_NOT` probing work from OSS-1705. Cross-column prohibited clauses
are supported as ordinary query semantics, but this PR does not change
the same-column `BooleanScorer` or its probing strategy.

## Validation

- `cargo fmt --all -- --check`
- `git diff --check origin/main...HEAD`
- deterministic projection tests for ordered, deleted, remapped,
duplicate, and out-of-order layouts
- materialized fallback and collision regressions
- merge-scorer ordering, lazy initialization, advance, shallow-bound,
floor, tie, and duplicate tests
- seeded randomized exhaustive oracle coverage for SUM, MAX, MUST,
required-optional, signed Boost, and nested prohibited shapes
- staged generator, candidate resolution, phrase, visibility, and
quantized-length tests
- static reachability audit covering every item reported by the failed
CI jobs

Per project workflow, GitHub CI is the authoritative Cargo test and
Clippy run for this revised head.

## Performance validation

No end-to-end performance benefit is claimed yet because the dataset
Scanner does not call this entry point in this PR. A main-vs-this-PR
dataset benchmark would execute the existing fallback path and measure
noise.

The final consumer PR will enable this core and report independent
warm/cold ABBA results with latency, throughput, CPU, bytes, requests,
cache metrics, and exact result digests.

## Stack

1. #8666 — WAND scoring and bound exactness (merged)
2. #8667 — posting loading and cache policy (merged)
3. **This PR:** row-address and cross-column compound scorer core
4. Dataset planner / execution integration and end-to-end benchmark

Part of
[OSS-1603](https://linear.app/lancedb/issue/OSS-1603/add-candidate-driven-execution-for-cross-column-boolean-fts-queries).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer 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