Skip to content

Add ContainsCursor.NextGeq: successor queries from cursor state#47

Merged
aliszka merged 5 commits into
mainfrom
feat/contains-cursor-nextgeq
Jul 7, 2026
Merged

Add ContainsCursor.NextGeq: successor queries from cursor state#47
aliszka merged 5 commits into
mainfrom
feat/contains-cursor-nextgeq

Conversation

@amourao

@amourao amourao commented Jul 3, 2026

Copy link
Copy Markdown

Adds NextGeq(x) (uint64, bool) to ContainsCursor: the smallest set value ≥ x, resolved from the cursor's existing cache and leaving the cursor positioned on the result — so interleaved Contains/NextGeq with non-decreasing arguments stay on their fast paths. Follows up #44; independent of #45/#46.

Why

This is the successor primitive for leapfrog intersection. Downstream (Weaviate BM25 filtered search), the merged tombstone+filter bitmap keeps only 2–8% of docs on update-heavy segments; the scan currently walks posting lists candidate by candidate and probes the filter on each (~49% of the filtered query loop, at the per-probe memory floor). With NextGeq, the roles invert when the filter is sparse: jump straight to the filter's next admissible docID and advance the posting lists to it — skipping the reject walks and, when postings are dense relative to the filter, whole block decodes. Verified planning estimate for that wiring: 19–41% of the filtered query loop.

How

  • Resolution reuses the Contains machinery unchanged: same-key cache, maxKey short-circuit, searchFrom gallop forward, plain search on backward jumps.
  • Walks forward past empty (or unknown-type) containers; for containers after x's own, the successor is the container's minimum.
  • New bitmap.nextGeq(y): word-masked scan for the next set bit at or after a position (mask 0xFFFF >> (y & 0xF) matches the MSB-first bit order used by bitmapMask and the iterator).
  • On success the cursor state is updated to the returned value's container/position (preserving the lower-bound invariants); on "no successor" the state is left untouched.

Verification

Differential against a sort.Search reference over ToArray() on both fixture shapes (~600K probes each): leapfrog ascending, fully random (forward + backward jumps), interleaved with Contains, and exact boundary probes (first/last/beyond). Edge cases: nil cursor/bitmap, empty bitmap, emptied container skipped, container-gap crossing, dense bitmap-container scans. Race-clean; full suite green.

Benchmark

BenchmarkNextGeqLeapfrog-14    18.5 ns per successor jump    0 B/op    0 allocs/op

(dense fixture, ascending jumps with random 1–2000 gaps — the sparse-filter iteration pattern)

NextGeq(x) returns the smallest set value >= x, sharing the cursor's cache and
leaving the cursor positioned on the returned value, so interleaving Contains
and NextGeq with non-decreasing arguments keeps both on their fast paths.

This is the successor primitive for leapfrog intersection: a caller scanning a
stream against a sparse bitmap can jump directly to the bitmap's next
admissible value instead of probing rejected candidates one by one. Resolution
reuses the Contains machinery (same-key cache, maxKey short-circuit,
searchFrom gallop, plain search on backward jumps), walks past empty
containers, and adds a word-masked scan (bitmap.nextGeq) for the next set bit
at or after a position inside a bitmap container.

Verified against a sort.Search reference over ToArray on both fixture shapes
(~600K probes each: leapfrog, random with backward jumps, interleaved with
Contains, exact boundaries), plus empty/emptied-container and container-gap
edge cases; race-clean. Leapfrog benchmark: 32 ns per successor jump, 0 allocs.

@orca-security-eu orca-security-eu Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

@amourao

amourao commented Jul 3, 2026

Copy link
Copy Markdown
Author

After testing, performance is worse compared to regular BMW advance, so let's avoid adding more complexity to sroar

EDIT: reopened as there is a case for filter driven advance

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a successor query primitive to the existing ContainsCursor acceleration mechanism, enabling efficient “next ≥ x” advancement while preserving cursor locality. This supports leapfrog-style intersection by allowing consumers to advance directly to the next admissible value rather than probing candidate-by-candidate.

Changes:

  • Adds (*ContainsCursor).NextGeq(x) (uint64, bool) to return the smallest set value ≥ x while updating the cursor position.
  • Introduces bitmap.nextGeq(y) to find the next set bit within a bitmap container using a masked word scan.
  • Adds equivalence, edge-case, and benchmark coverage for NextGeq against a reference successor implementation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
contains_cursor.go Adds NextGeq successor API and cursor-state updates during successor resolution.
container.go Adds bitmap.nextGeq for in-container successor scanning in bitmap containers.
contains_cursor_nextgeq_test.go Adds differential tests, edge-case tests, and a benchmark for NextGeq.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread contains_cursor.go
Comment thread contains_cursor.go
Comment thread contains_cursor.go Outdated
amourao added 2 commits July 7, 2026 12:31
Review follow-ups:
- Same-key probes now resolve within the cached container without touching
  the keys node or the arena; the maxKey range check folds into the
  resolution switch so cache hits skip it (sound: lastKey is a resolved
  key <= maxKey or the unmatchable sentinel). Leapfrog successor jumps:
  31.7 -> 18.5 ns/op.
- Extract arrayGeqPos (lower bound from the cached position) out of
  arrayHas and share it between Contains and NextGeq; Contains keeps its
  single-call structure and its benchmarks are unchanged.
- Fix the no-successor comment: also reached when x is past the last set
  value of a normal trailing container.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

… array fast path

BenchmarkNextGeqLeapfrog runs on denseBitmap (all array containers, ~650
values each) with ~1-2000 jumps, so 98% of probes stay in one container and
0% ever reach a bitmap container: it measures only the arrayGeqPos same-key
fast path, never bitmap.nextGeq or the cross-container walk. Its comment is
corrected to say so.

Add two benchmarks that exercise the paths the PR actually adds:
- BenchmarkNextGeqBitmap: an all-bitmap-container fixture (every 5th value
  per key) so the timed path is bitmap.nextGeq (verified: 100% bitmap landings).
- BenchmarkNextGeqCrossContainer: jumps > 65536 over mixedBitmap so every
  probe changes key and hits the searchFrom gallop + fresh-container walk,
  including empty-gap skips (verified: 100% cross-key, mixed array/bitmap).

Also add targeted TestNextGeqEdgeCases cases for the same-key "cached
container exhausted -> idx=lastIdx+1 -> later container" branch, for both the
bitmap and array container shapes (the random differential reaches the array
shape but never the bitmap one).
The arrayGeqPos refactor dropped arrayHas's equality and in-gap early
returns, adding a caller-side re-compare and an unconditional arrPos store
to every probe — the two shapes that dominate reject-heavy Contains
workloads (+3-5% on the new gap-shape bench, ~+1.5% end-to-end on filtered
BM25 reject walks). Restore arrayHas and derive NextGeq's successor from
the lower-bound invariant arrayHas already leaves in arrPos: on a miss the
successor is c[arrPos]. Same-key jumps keep their fast path (18.8ns vs
30.5ns without it); at_cursor_hit probes recover 3.6%.
@aliszka aliszka merged commit ba20c59 into main Jul 7, 2026
6 checks passed
@aliszka aliszka deleted the feat/contains-cursor-nextgeq branch July 7, 2026 14:17
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.

3 participants