Skip to content

perf(fts): bound classic conjunction scoring in dense windows - #9033

Merged
BubbleCal merged 1 commit into
mainfrom
yang/fts-stack-certified-and
Sep 10, 2026
Merged

perf(fts): bound classic conjunction scoring in dense windows#9033
BubbleCal merged 1 commit into
mainfrom
yang/fts-stack-certified-and

Conversation

@BubbleCal

@BubbleCal BubbleCal commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Classic 4/5-term AND evaluation can decode and score every follower even when a partial exact score already proves the candidate cannot compete. Applying score-first evaluation universally is costly when posting overlap is low. This PR enables it only inside a metadata-certified dense range.

  • Prove continuous coverage from each current posting block's first/last/count metadata, while retaining actual follower membership checks.
  • Build conservative suffix bounds only for eligible live windows and lazily cache exact BM25 norm addends.
  • Preserve canonical query-order f32 scores and inclusive tie behavior.
  • Reuse an unbounded zero-floor window and invalidate it when local, shared, or compound floors become positive; clear eligibility before phrase evaluation.

The classic path remains used by compound WandCursor and explicit classic searches after #9030 enables standalone wide-query bulk routing.

Most of this diff is regression coverage: approximately 327 production-section additions and 1,334 test additions. Shared fixtures retain the distinct dense/sparse, malformed-metadata, short-tail, high-DocID, score-bit, tie, norm-fallback, seek/shallow, and floor-transition cases. No isolated main A/B result is claimed for this layer.

Validation

  • Per-layer inverted-index suite: 603 passed.
  • Required all-workspace Clippy and formatting: passed.
  • PR metadata/gatekeeper checks: pending. Full hosted test workflows only run for main/release/** bases; this intermediate layer has been tested and linted locally. The full hosted matrix will run after its base is retargeted to main following the parent merge.

Stack, 3/4: #9031#9032this PR#9030. Base: #9032. Uses the lazy posting-cursor support introduced in #9031.

Merge in stack order; retarget/restack the remaining PRs after their parent merges.

lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Sep 7, 2026
BubbleCal added a commit that referenced this pull request Sep 8, 2026
Before a competitive floor exists, AND queries still decoded frequencies
and prepared bounds while looking for intersections. This PR keeps that
work lazy, particularly when the final intersection has fewer than `k`
matches.

- Split DocID and frequency decoding for full posting blocks and tails,
retaining the existing encoded format and combined decoding entry
points.
- Advance and intersect DocIDs before decoding frequencies; initialize
score bounds when a positive competitive floor makes them useful.
- Avoid loading document lengths for partitions whose dictionaries
cannot satisfy all required tokens.
- Use SIMD catch-up for the existing classic 4/5-clause path, with
unsigned comparisons covering the full `u32` DocID range and a
binary-search fallback without AVX2.

Tests cover empty/underfilled intersections, delayed frequency and bound
activation, block/tail transitions, required-token misses, and
scalar/SIMD boundary behavior. Code and its regression tests stay
together in this PR.

No isolated `main` A/B measurement is claimed for this layer. The later
bulk-intersection measurements belong to
[#9030](#9030).

## Validation

- Local inverted-index suite: 530 passed.
- Required all-workspace Clippy: passed.
- CI: pending.

Stack, 1/4: **this PR** →
[#9032](#9032) →
[#9033](#9033) →
[#9030](#9030). Base: `main`.

Merge in stack order; retarget/restack the remaining PRs after their
parent merges.
@BubbleCal
BubbleCal force-pushed the yang/fts-stack-certified-and branch from 0311000 to f694a58 Compare September 8, 2026 06:25
Base automatically changed from yang/fts-stack-prepared-scoring to main September 8, 2026 16:05
Certify dense posting ranges before progressive scoring, retain conservative
suffix bounds and exact query-order scores, and reuse zero-floor windows
and lazy norm lookup. Preserve sparse, phrase, and compound-cursor behavior.

Share test fixtures without removing assertions or parameter coverage.
BubbleCal added a commit that referenced this pull request Sep 8, 2026
Fully prewarmed FTS queries still pay asynchronous document-frequency
aggregation overhead, and segment searches clone an already prepared
canonical scorer. This PR reuses the resident metadata and scorer.

- Aggregate corpus statistics and posting lengths synchronously only
when every required value is already loaded. The probe never starts I/O;
incomplete residency uses the existing asynchronous path.
- Reuse the canonical prepared `Arc<MemBM25Scorer>` across modern
segments. Legacy and externally supplied scorers retain their existing
fallback behavior.
- Check statistic shapes and integer overflow while merging global
counts.
- Provide `LANCE_FTS_SYNC_DF=off` and
`LANCE_FTS_REUSE_PREPARED_SCORER=off` switches.

The accompanying tests cover cold/partial/full residency, multiple
segments, missing terms, overflow, injected scorers, fuzzy preparation,
and exact result parity. Shared fixtures remove repetition while
retaining the relevant execution modes and assertions.

This layer does not change the query API or persistent format. No
isolated `main` A/B performance result is claimed.

## Validation

- Local inverted-index suite: 542 passed.
- Required all-workspace Clippy and formatting: passed.
- PR metadata/gatekeeper checks: pending. Full hosted test workflows
only run for `main`/`release/**` bases; this intermediate layer has been
tested and linted locally. The full hosted matrix will run after its
base is retargeted to `main` following the parent merge.

Stack, 2/4: [#9031](#9031) →
**this PR** → [#9033](#9033) →
[#9030](#9030). Base:
[#9031](#9031); this diff
contains only resident-statistic and prepared-scorer changes with their
tests.

Merge in stack order; retarget/restack the remaining PRs after their
parent merges.
@BubbleCal
BubbleCal force-pushed the yang/fts-stack-certified-and branch from f694a58 to 50c5d9b Compare September 8, 2026 16:05
@lance-gatekeeper lance-gatekeeper Bot removed K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Sep 8, 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 with a non-blocking risk.

The rebase onto main leaves the score-first implementation unchanged. Its strict eligibility, conservative suffix bounds, canonical score reconstruction, and focused parity/transition coverage continue to support correctness.

The non-blocking risk remains performance: no isolated latency/CPU comparison establishes the net benefit or possible short-tail overhead. Keep the scope narrow until representative classic 4/5-term workloads confirm the trade-off; if the incremental gain is small, progressive scoring after full DocID alignment is the simpler alternative.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Sep 8, 2026
@BubbleCal
BubbleCal merged commit a3db533 into main Sep 10, 2026
38 of 40 checks passed
@BubbleCal
BubbleCal deleted the yang/fts-stack-certified-and branch September 10, 2026 09:24
BubbleCal added a commit that referenced this pull request Sep 10, 2026
Wide root Match AND queries still spend substantial time in per-document
iterator advancement. This PR enables the existing bulk scoring route
for four or more modern block-256, non-grouped clauses, and adds bounded
pairwise SIMD/scalar DocID intersection before the competitive floor
becomes positive. Posting offsets are recovered only for final
survivors; positive-floor scoring retains the existing bulk
implementation.

Ordinary root Match AND queries directly use `Wand.search`. Compound
`WandCursor` continues to use the classic path covered by
[#9033](#9033). The separate
Boolean MUST-child floor-propagation change in #9015 is not included.

## Measurements

The comparison is **incremental**, from
`85ec700bb4aacbc4e87f826f93fe7bc78fe53e41` to measured candidate
`02c5f5a73fb29a47a0ff33293936e38c8f90c470`. The current stacked tip,
`0181df1136be876a04b327f24a0f1a4709263b58`, has identical production
code; subsequent changes only deduplicate tests and exclude benchmark
files. The parent stack layer likewise retains the measured baseline's
production code. These measurements establish the final
bulk-routing/intersection increment, not the combined stack's speedup
versus `main`.

| Scenario / metric | Baseline | This PR | Benefit |
| --- | ---: | ---: | ---: |
| 4 terms, 0 matches / latency | 1.237869 ms | 0.335107 ms | 3.694x
speedup |
| 5 terms, 5 matches / latency | 1.552503 ms | 0.448852 ms | 3.459x
speedup |
| 8 terms, 0 matches / latency | 3.984348 ms | 0.841667 ms | 4.734x
speedup |
| 16 terms, 5 matches / latency | 8.309449 ms | 1.882632 ms | 4.414x
speedup |
| 4 terms, contiguous 32,768 matches / latency | 2.627815 ms | 1.309050
ms | 2.007x speedup |
| 16 terms, contiguous 32,768 matches / latency | 13.304164 ms |
3.344204 ms | 3.978x speedup |
| 16 terms, sparse correlated 32,768 matches / latency | 13.401886 ms |
3.383210 ms | 3.961x speedup |

Lower latency is better; benefit is baseline/current. Same AWS
`c7i.8xlarge`, Xeon Platinum 8488C, Ubuntu 24.04, Rust 1.97.0/clang 18,
repository `release-with-debug` with thin LTO and configured
Haswell/AVX2 target. Both versions use Auto mode, CPU 2, one query in
flight, `LANCE_CPU_THREADS=1`, and `RAYON_NUM_THREADS=1`.

The complete comparison contains 42 frozen cases: 65,536-document
scaling/additional-distribution corpora and a 131,072-document control
corpus. All use `k=10`, V3/block256, no positions/filter, and fully
resident prewarm. High-hit scaling holds the intersection and each
term's DF at 32,768 across term counts. Four independent processes per
version/case run in ABBAABBA order, each with 2 s warmup, 5 s
measurement, and 30 samples. Values are the median of four run medians.
Independent scoring oracles and exact row/score-bit signatures passed.

Eight of 15 short-query controls show observed median regressions of
approximately 0.6–2.2%. Some baseline runs have wider between-process
spread; these are descriptive repeated measurements, not significance
claims. Results cover warm index-query CPU work, not a 2.86B-row
production or end-to-end dataset workload. Combined performance versus
`main` remains unverified. Benchmark drivers and raw evidence are
archived separately; no benchmark files are included in this stack.

## Validation

- The equivalent combined implementation passed 762 inverted-index tests
on ARM and x86.
- Six dataset integration cases passed: N4/N16 × 0/5/high matches, two
fragments, V3/block256, independent BM25 row/score-bit oracle, verified
MatchQuery plans, and a strict 10th/11th score gap.
- Thirteen existing WAND/certificate/tie tests passed, including bounded
completion and exact overflow replay.
- Per-layer inverted-index suite: 762 passed; required all-workspace
Clippy and formatting passed.
- Dataset integration rerun: 6 root Match AND cases and 13
WAND/certificate/tie cases passed. CI: pending.

Stack, 4/4: [#9031](#9031) →
[#9032](#9032) →
[#9033](#9033) → **this PR**.
Base: [#9033](#9033).

Merge in stack order; retarget/restack the remaining PRs after their
parent merges.
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 K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants