Attribution counters
Speculative completion rate, fallback rate, pruned blocks, and cache composition — then re-measure selective-without-fallback.
V1/V2 store a token's whole posting list as one row, with each block's max score buried in the first bytes of its compressed payload. V3 gives every 128-doc block its own row and pulls the skip metadata out into narrow columns — so the executor can prove a block useless before paying to read it.
One row of invert.lance holds a token's entire posting list: a List<LargeBinary> where each element is one 128-doc compressed block. The 8-byte header of every block carries block_max_score and first_doc_id — exactly the two numbers WAND needs to decide whether the block is worth reading.
Header width exaggerated for visibility — it is 8 B in a ~1 KB block.
_max_score and _length are separate columns — you can project them without touching payload.Split the storage by access pattern instead of by term. The skip data leaves the payload and becomes narrow columns you can project alone; each block becomes an independently addressable row; positions align to the same row ids; a coarse skip layer aggregates block groups for long-range advance(target).
V2: six blocks welded into one row. The blue headers are the decision inputs — trapped inside the decision objects.
V3: skip columns are narrow and projectable; terms maps token_id → block_start + num_blocks; payload and positions rows are fetched only when selected. The payload codec stays FTS-private — Lance only provides row addressing, range reads, and column projection.
One term, twelve blocks, three of them can beat the current top-k threshold. Watch what crosses the network in each version.
Illustrative sizes for one mid-frequency term: 12 × 128-doc blocks ≈ 1 KB each, skip metadata 12 B per block; widths not to scale. Striped bars are bytes in flight. Outlined slots in V3 are blocks the executor knows about but never fetches.
Drag the top-k threshold. A block issues a payload read only if its block_max_score — an upper bound on any score inside it — can still beat the threshold. As the heap fills during a real query, the threshold rises and the read plan shrinks.
terms → block row rangeFull-prewarm is no longer the only cache semantic. Selective prewarm reads the skip metadata for workload terms, ranks their block rows by block_max_score, and warms only the top slice — plus a workload-scoped fallback posting-list cache so a failed proof never sends query IO back to S3.
Accent bars are warmed payload (and, when requested, positions) rows. Everything else stays cold on the object store — but its skip metadata is cached, so the executor can still prove what it isn't missing.
lance-bench-ec2, one S3 dataset, 120k docs, 160 queries, limit=10. Resident cache is much larger than bytes read (decompressed postings, positions, entry overhead), so the MiB ratios do not extrapolate to GB-scale indexes.
| Mode | QPS | Query IO | Resident cache |
|---|---|---|---|
| V2 cold | 13.6 | 494 reads / 14.6 MB | 65 entries |
| V3 cold | 11.2 | 594 reads / 14.4 MB | 48 entries |
| V3 top-block cache, no fallback | 66.6 | 235 reads / 778 KB | 484 entries |
| V3 selective cache (top 10%) | 1376–1408 | 0 reads / 0 B | 4.09 MiB |
| V3 full-prewarm | 1452 | 0 reads / 0 B | 66.08 MiB |
| V2 full-prewarm | 1426 | 0 reads / 0 B | 55.84 MiB |
The layout already exposes the row-level metadata these layers need.
Speculative completion rate, fallback rate, pruned blocks, and cache composition — then re-measure selective-without-fallback.
Keep block metadata and coarse skip resident across queries and coalesce metadata ranges, to close the cold regression.
Selective prewarm issues 409 ranges vs full-prewarm's 35 for similar bytes; targeted token chunking should close that gap.
Extend selection from a fixed query list to query-log weighting or global score-aware prewarm under a memory budget.
Shrink full-prewarm's 240k-entry overhead. Complementary, not a substitute: it still caches every payload row.