Skip to content

Sparse vector top-K carries a large fixed per-query cost: 100k tier at 8.94 ms p50 vs 1.45 ms before the 26.8.1 pruning work #5467

Description

@lvca

Split out of #5388, where it has been an unresolved side thread since the round-1 measurement.

Summary

LSM_SPARSE_VECTOR top-K at the 100k tier costs 8.94 ms p50 where it used to cost 1.45 ms. The regression appeared in the 26.8.1 development line, has not moved since the Block-Max MaxScore work landed (a0a79da51), and is not addressed by the block-payload-copy bound on issue-5388-block-payload-copy, which is worth about -12.5% here.

The 1M tier is fine and improving. This is specifically about small and mid-size corpora, where a fixed per-query cost that is invisible at 1M dominates the measurement.

Timeline (all @tae898's numbers, Big-ANN sparse track, real SPLADE, embedded INT8, settled index, N=5)

build 100k p50 1M p50
dev0 / dev3 1.45 ms 165 ms
dev5 (BMW block-skip) 23.1 ms 166 ms
dev6 (Block-Max MaxScore) 8.94 ms 53.6 ms
dev20 (current line) 8.94 ms 52.49 ms

MaxScore recovered 23.1 -> 8.94 ms and cut 1M by 3.1x, but 100k is still 6.2x its original cost, and two development cycles have not moved it.

Why this is a fixed-cost problem, not a scaling problem

Scaling from 100k to 1M went from 114x per 10x of data on dev0 to 5.9x per 10x now. The corpus-proportional part of the work is in good shape - it is the constant that grew. The dev0 100k number was cheap because the old exhaustive traversal had almost no setup and a tiny corpus finished before setup could matter; today's traversal has real per-query preparation that a 100k corpus cannot amortise.

Candidate contributors, in the order I intend to measure them:

  1. Per-query cursor setup. PaginatedSparseVectorEngine.topK opens one merged cursor per query term. A real SPLADE query has 30-120 terms, so anything O(terms) with a meaningful constant is paid 30-120 times before a single posting is scored.
  2. MaxScore preparation. Computing per-term sigma, sorting for the promotion order, and building the essential/non-essential partition is O(terms log terms) plus per-term metadata touches, all before the first candidate.
  3. Merged-cursor construction across memtable and every active segment, including segments that hold no postings for the term.
  4. Block-header and skip-list touches that scale with the index, not with what pruning ends up decoding. At 1M only about a third of blocks get decoded; the rest of the metadata is still walked.
  5. Anything O(active segments) per term when the 100k index settles into a different segment count than the 1M one.

Why it matters now

Against the same data and ground truth, Qdrant and Milvus return recall@10 = 1.00 at 2.9 ms and 9.3 ms p50 on the 1M tier. Our exact path is at 52.5 ms. Per-query constants that cost 7.5 ms at 100k are the same constants sitting inside the 1M number, so this is not only about the small tier - it is the cheapest available lever on both.

Scope

  • Attribute the 100k per-query cost to specific call sites with a profile at that tier (the existing profile is 1M, where these constants are invisible).
  • Remove or amortise whatever is genuinely fixed per query.
  • Guard the result with a benchmark that reports a small-corpus tier, so a fixed-cost regression cannot hide behind a healthy 1M number again.

Reporter's harness is staged to re-measure, and the tier builds in seconds.

/cc @tae898

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions