Skip to content

Add BlockCursor (postings a block at a time, with block-max bounds) and optional mergeable/releasable interfaces - #87

Draft
capemox wants to merge 5 commits into
masterfrom
perseg/block-cursor
Draft

capemox wants to merge 5 commits into
masterfrom
perseg/block-cursor

Conversation

@capemox

@capemox capemox commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Draft, do not merge. This is one layer of a stacked checkpoint (scorch_segment_api → freeway → zapx → vellum → bleve). The branches carry local replace directives in go.mod that point at sibling checkouts, so they only build with those checkouts side by side. Opened for review of the design and the code, not for merging as is.

What

Two additions to scorch_segment_api, both optional capabilities so existing implementations keep compiling:

  • BlockPostingsIterator (block_iterator.go): a variant of PostingsIterator that yields postings a block at a time, for consumers that want only the scalars a scorer needs (doc number, term frequency, norm) and want to pay for an interface call once per batch instead of once per posting.
    • BlockPostingsList (optional, on a PostingsList) has BlockIterator, the counterpart of Iterator, which returns a BlockPostingsIterator.
    • BlockPostingsIterator fills a PostingsBlock of up to 128 docs, freqs and norms with NextBlock / SeekBlock; deleted docs never appear. Count and LiveCount give the number of postings, the latter exactly and without moving the iterator.
    • BlockMaxPostingsIterator (optional, on a BlockPostingsIterator) bounds the scores of a list: the bounds of the block holding a target without decoding or moving anything (BoundsAt), of the block last decoded, and of the whole list. This is what block-max pruning (WAND / MAXSCORE) is made of.
  • MergeablePostingsList / MergeablePostingsIterator and Releasable (segment.go): lets an implementation opt into segment-merge support and pooled reuse without changing the base PostingsList / PostingsIterator contract.

New dependency (read this before merging)

PostingsBlockLen is bitpack.BlockLen from blevesearch/freeway instead of a second 128: the block length is a property of the codec, and it is now defined once and handed on from here (zapx and bleve use segment.PostingsBlockLen).

  • This module now requires github.com/blevesearch/freeway, which has no dependencies of its own.
  • freeway's go directive is 1.25.0, so this module's go line moves from 1.24.0 to 1.25.0 (bleve and zapx v17 / v18 are already on 1.25.0).
  • freeway is currently a private repository. This module is public, so freeway has to be public (or otherwise resolvable by everyone who builds with this module) before this can merge.

Why

The bleve per segment search path (blevesearch/bleve, perseg/term-path) reads postings through these interfaces instead of one Next per posting.

Testing

go build ./... and go vet ./.... The package has no tests of its own; the interfaces are exercised by the zapx implementation and bleve's tests in the PRs above and below this one.

Stack (review order)

# PR what
1 freeway#1 float32 TF-IDF / BM25 SIMD kernels (bitpack.BlockLen is the block length everything else uses)
2 this PR scorch_segment_api#87 BlockPostingsIterator interfaces; PostingsBlockLen from freeway
3 zapx#451 BlockPostingsIterator over bit-packed postings (base bitpack-simd)
4 vellum#30 FST traversal performance work (independent)
5 bleve#2404 the per segment search path

Only the last one is the design under review. freeway, scorch_segment_api and zapx are the pieces it reads postings and scores through (the block length is defined once, in freeway, and handed on by scorch_segment_api), and vellum is independent FST work that bleve builds against.

🤖 Generated with Claude Code

capemox and others added 2 commits September 15, 2026 23:28
Lets a PostingsList/PostingsIterator implementation opt into
segment-merge support and pooled reuse without changing the base
PostingsList/PostingsIterator contract, so existing implementations
(older zapx major versions) keep compiling unchanged.
An optional capability of a PostingsList, for consumers that want the scalars a
scorer needs -- doc number, term frequency, norm -- and nothing else, and want
to pay for an interface call once per batch of postings rather than once per
posting.

* BlockCursorProvider (on PostingsList) hands out a BlockCursor.
* BlockCursor fills a PostingsBlock of up to 128 docs, freqs and norms with
  NextBlock or SeekBlock; deleted docs never appear. Count and LiveCount give
  the number of postings, the latter exactly and without moving the cursor
  (free when nothing is deleted).
* BlockMaxCursor (optional, on a BlockCursor) bounds the scores of a list: the
  bounds of the block holding a target without decoding or moving anything
  (BoundsAt), of the block last decoded, and of the whole list. This is what
  block-max pruning (WAND) is made of.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
capemox and others added 3 commits October 4, 2026 09:20
The postings list hands out the variant of PostingsIterator that yields
postings a block at a time, and its method is named for that. The types
(BlockCursor, BlockCursorProvider, BlockMaxCursor) keep their names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PostingsList.Iterator returns a PostingsIterator, so the postings list that can
hand out postings a block at a time has BlockIterator, which returns a
BlockPostingsIterator. Following the names of the other optional interfaces of
the package (MergeablePostingsList, MergeablePostingsIterator):

  BlockCursorProvider -> BlockPostingsList
  BlockCursor         -> BlockPostingsIterator
  BlockMaxCursor      -> BlockMaxPostingsIterator
  method BlockCursor  -> BlockIterator

PostingsBlock, BlockBounds, NextBlock and SeekBlock keep their names. The file is
block_iterator.go.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The length of a block of postings is that of the bit-packed format's codec, so it
comes from there instead of being written down a second time. This makes freeway a
dependency of the module (it has none of its own), and the module's go directive
becomes 1.25.0, which is freeway's.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant