Skip to content

Apply filter before scoring in MaxScoreBulkScorer - #16515

Open
kkewwei wants to merge 10 commits into
apache:mainfrom
kkewwei:optimze_msbs
Open

Apply filter before scoring in MaxScoreBulkScorer#16515
kkewwei wants to merge 10 commits into
apache:mainfrom
kkewwei:optimze_msbs

Conversation

@kkewwei

@kkewwei kkewwei commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Description

Apple filter before scoring in MaxScoreBulkScorer. Resolves #16514

@kkewwei
kkewwei marked this pull request as draft August 15, 2026 14:09
Comment thread lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java Outdated
throws IOException {
do {
for (top.scorer.nextDocsAndScores(innerWindowMax, acceptDocs, docAndScoreBuffer);
for (nextDocsAndScores(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if there's a way of doing this that merges the filter into acceptDocs? That way we don't need to add new methods to TermScorer and we don't need to pass two parallel Objects around.

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.

Given that acceptDocs and filterMatches are never used simultaneously, it seems unnecessary to encapsulate them together.

@kkewwei kkewwei Aug 18, 2026

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.

Creating a FilterBits also seems like a reasonable approach. If you're okay with it, I'll make the change accordingly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in fillScoreBufferViaBitSet(), this exists:

if (acceptDocs != null) {
  acceptDocs.applyMask(filterMatches, innerWindowMin);
}

But it never passes the acceptDocs to the collect:

collectEssentialScoresIntoWindow(top, innerWindowMax, innerWindowMin, null, filterMatches);

So there is a problem if acceptDocs is null, in which point, there should be a way to have filterMatches be turned into its own (offset-acknowledging) Bits object. But other than that, the acceptDocs should be able to be used on its own. Though acceptDocs.applyMask(filterMatches, innerWindowMin) might be able to be optimized with a better implementation for this use case.

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.

Thanks, this is exactly the motivation for this change.

Whether acceptDocs is null or not, filterMatches is the mask used to filter the candidates in this path. When acceptDocs is present, we first merge live-doc filtering into filterMatches

Also, when acceptDocs is backed by FixedBits, applyMask(FixedBitSet, offset) uses the optimized word-level implementation, so this merge should already be efficient.

If you see another way to pass the combined mask efficiently, I would be very interested in exploring it.

@kkewwei

kkewwei commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@romseygeek Thank you very much for your review.

I'm still doing benchmark, seems like perf drops when acceptDocs is null, but I'm not 100% sure yet. I'll post the results as soon as I have them.

@kkewwei

kkewwei commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

I'm still doing benchmark, seems like perf drops when acceptDocs is null, but I'm not 100% sure yet.

The int overflow caused the benchmark results to be highly misleading.

@romseygeek Please help review in your spare time.

In this MaxScoreBulkScorerFilterBenchmark, the aggregate SHOULD cost is approximately 30%+ of documents . Therefore, I benchmarked filter densities from 1% to 30%(filterMatches != null )

Results:

SHOULD clauses Filter density Baseline ops/ms Candidate ops/ms Improvement
10 1% 0.258 0.669 +159.9%
10 5% 0.202 0.431 +113.3%
10 10% 0.217 0.389 +79.3%
10 20% 0.229 0.313 +36.4%
10 30% 0.204 0.311 +52.2%
20 1% 0.159 0.425 +167.5%
20 5% 0.150 0.299 +99.1%
20 10% 0.147 0.280 +90.2%
20 20% 0.149 0.233 +56.5%
20 30% 0.142 0.207 +45.9%

The change improves throughput across all tested configurations, by 36% to 167%. As expected, the largest gains are with sparse filters, where more candidate documents can be discarded before score.

Note: The changes from “Support two-phase filters in MaxScoreBulkScorer bitset path” are not included in the benchmark results. I will submit a separate PR for that change.

@kkewwei
kkewwei marked this pull request as ready for review August 18, 2026 16:35
Comment thread lucene/CHANGES.txt Outdated
@kkewwei kkewwei changed the title Apple filter before scoring in MaxScoreBulkScorer Apply filter before scoring in MaxScoreBulkScorer Aug 18, 2026
Comment thread lucene/core/src/java/org/apache/lucene/search/TermScorer.java Outdated
throws IOException {
do {
for (top.scorer.nextDocsAndScores(innerWindowMax, acceptDocs, docAndScoreBuffer);
for (nextDocsAndScores(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in fillScoreBufferViaBitSet(), this exists:

if (acceptDocs != null) {
  acceptDocs.applyMask(filterMatches, innerWindowMin);
}

But it never passes the acceptDocs to the collect:

collectEssentialScoresIntoWindow(top, innerWindowMax, innerWindowMin, null, filterMatches);

So there is a problem if acceptDocs is null, in which point, there should be a way to have filterMatches be turned into its own (offset-acknowledging) Bits object. But other than that, the acceptDocs should be able to be used on its own. Though acceptDocs.applyMask(filterMatches, innerWindowMin) might be able to be optimized with a better implementation for this use case.

Comment thread lucene/core/src/java/org/apache/lucene/search/Scorer.java Outdated
@kkewwei

kkewwei commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

I I added a benchmark task set in luceneutil with wikimediumall:
mikemccand/luceneutil@10be613

After 19 benchmark rounds, the final results are:

                            TaskQPS baseline      StdDevQPS my_modified_version      StdDev                Pct diff p-value
                   MSBSFilter100        1.02      (4.9%)        1.03      (4.1%)    1.1% (  -7% -   10%) 0.441
                     MSBSControl       12.35      (5.2%)       12.76      (5.6%)    3.4% (  -7% -   14%) 0.048
                    MSBSFilter50        3.19      (5.7%)        3.30      (4.7%)    3.7% (  -6% -   14%) 0.026
                        PKLookup      143.38      (8.4%)      149.41      (6.0%)    4.2% (  -9% -   20%) 0.068
                    MSBSFilter33        3.62      (6.6%)        3.82      (5.3%)    5.4% (  -6% -   18%) 0.004
                    MSBSFilter20        3.69      (6.5%)        4.06      (5.5%)   10.1% (  -1% -   23%) 0.000
                    MSBSFilter10        4.06      (5.6%)        4.55      (7.0%)   12.1% (   0% -   26%) 0.000
                     MSBSFilter5        4.03      (6.5%)        4.79      (7.3%)   18.8% (   4% -   34%) 0.000
                     MSBSFilter1        4.60      (6.6%)        5.82      (5.5%)   26.6% (  13% -   41%) 0.000

The more docs filterd, the better the returns.

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.

Apply filter before scoring in MaxScoreBulkScorer

4 participants