Skip to content

Build the AcceptDocs bit set from the raw filter iterator - #16588

Open
john-mlika wants to merge 1 commit into
apache:mainfrom
john-mlika:arc-1-acceptdocs-raw-iterator
Open

Build the AcceptDocs bit set from the raw filter iterator#16588
john-mlika wants to merge 1 commit into
apache:mainfrom
john-mlika:arc-1-acceptdocs-raw-iterator

Conversation

@john-mlika

@john-mlika john-mlika commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

DocIdSetIteratorAcceptDocs hands createBitSet an iterator already wrapped for live docs. The wrapper has no intoBitSet, so on any segment with deletions the bit set is filled one nextDoc() at a time and live docs are applied twice. createBitSet already takes liveDocs into account, so pass the raw iterator. Details and measurements are in #16586.

Adds TestAcceptDocs#testDenseIteratorIsConsumedInBulkWhenSegmentHasDeletions, which counts nextDoc() calls on the source iterator and fails on main when the segment has deletions and the accept set is dense enough for the bit-set branch, and testRandomBitsAreMatchesIntersectedWithLiveDocs, which checks the bits are always the matches intersected with live docs on both sides of the dense/sparse threshold. Also adds FilteredKnnVectorQueryBenchmark, the benchmark the numbers in the issue come from.

With the benchmark's 95%-selective cached filter and 5% deleted docs: 2.05 -> 1.23 ms/query.

Relates to #16586

@michaeljmarshall michaeljmarshall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice find!

Comment on lines +179 to +181
// Pass the raw iterator: #createBitSet applies liveDocs itself, and filtering upfront
// would hide DocIdSetIterator#intoBitSet behind a wrapper that has no bulk implementation.
DocIdSetIterator iterator = Objects.requireNonNull(iteratorSupplier.get());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you consider implementing intoBitSet on the FilteredDocIdSetIterator object? I'm imagining something like the following in the getFilteredDocIdSetIterator method:

            @Override
            public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
              getDelegate().intoBitSet(upTo, bitSet, offset);
              liveDocs.applyMask(bitSet, offset);
            }

As it is, your solution improves performance when iterator.cost() >= threshold, but leaves the other branch without the benefit of the intoBitSet path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that was my first instinct, but the javadoc says intoBitSet must not clear bits that are already set, and delegating then applyMask masks the whole window, including bits a previous clause set. the accumulating callers share one destination, BooleanScorer ORs every clause iterator into the same window bitset, with a comment at the call site saying live docs get applied later. so a clause that clears bits erases its siblings' hits, and doing it safely needs a scratch bitset per window.

it also can't reach the branch you're pointing at: createBitSet only takes that path when cost < maxDoc >> 7, and BitSet.of picks a SparseFixedBitSet at that same cost, so intoBitSet, which only exists for FixedBitSet destinations, is never called there. that branch walks on the order of maxDoc/128 docs anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the explanation, I hadn't read that part of the intoBitSet javadoc and missed the detail that only FixedBitSet uses the intoBitSet.

@michaeljmarshall michaeljmarshall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment on lines +179 to +181
// Pass the raw iterator: #createBitSet applies liveDocs itself, and filtering upfront
// would hide DocIdSetIterator#intoBitSet behind a wrapper that has no bulk implementation.
DocIdSetIterator iterator = Objects.requireNonNull(iteratorSupplier.get());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the explanation, I hadn't read that part of the intoBitSet javadoc and missed the detail that only FixedBitSet uses the intoBitSet.

@john-mlika
john-mlika force-pushed the arc-1-acceptdocs-raw-iterator branch from 58674a3 to 2a065d2 Compare September 1, 2026 19:40
michaeljmarshall added a commit to elastic/elasticsearch that referenced this pull request Sep 4, 2026
…nt (#157797)

Without this override, we rely on the default implementation for
intoBitSet, which is a for loop over the nextDoc() method. Now, we will
instead call the wrapped method's intoBitSet method, which may be the
default DocIdSetIterator#intoBitSet method, or may be a much more
performant implementation.

As a consequence, we may lose some insight into the number of nextDoc()
calls performed, but in doing so, we gain a more accurate insight
into the real runtime performance of a query's intoBitSet performance,
which is an overall win for us.

This is expected to improve profiled query performance by bringing it
into closer alignment with runtime query performance.

Note: there are some known cases in lucene where we rely on the default
intoBitSet() implementation (iteratively calling `nextDoc()`). We expect
a future upgrade of lucene will improve that performance. One example:
apache/lucene#16588
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants