From 296163ebefca70dbe4e7d382b55cefd42a99cb71 Mon Sep 17 00:00:00 2001 From: Prithvi S Date: Mon, 8 Jun 2026 21:42:26 +0530 Subject: [PATCH 1/3] Implement WANDScorer.advanceShallow() to enable block-max optimizations when nested in conjunctions --- lucene/CHANGES.txt | 2 + .../org/apache/lucene/search/WANDScorer.java | 13 +++-- .../apache/lucene/search/TestWANDScorer.java | 47 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 02113d05d85b..dab20dc1c3a5 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -632,6 +632,8 @@ Optimizations * GITHUB#16228: Reuse scratch int[] for ordinal translation. (Tim Brooks) +* GITHUB#16220: Implement WANDScorer.advanceShallow() to enable block-max optimizations when nested in conjunctions. (Prithvi S) + Bug Fixes --------------------- * GITHUB#15754: Fix HTMLStripCharFilter to prevent tags from incorrectly consuming subsequent diff --git a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java index 94c82a92d8a4..8f111294b49b 100644 --- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java @@ -566,17 +566,20 @@ public float score() throws IOException { @Override public int advanceShallow(int target) throws IOException { - // Propagate to improve score bounds + // Propagate to sub-scorers. Scorers past target constrain the boundary to docID - 1. + int newUpTo = DocIdSetIterator.NO_MORE_DOCS; for (Scorer scorer : allScorers) { - if (scorer.docID() < target) { - scorer.advanceShallow(target); + if (scorer.docID() <= target) { + newUpTo = Math.min(newUpTo, scorer.advanceShallow(target)); + } else if (scorer.docID() != DocIdSetIterator.NO_MORE_DOCS) { + newUpTo = Math.min(newUpTo, scorer.docID() - 1); } } if (target <= upTo) { + // Within current block, return existing boundary. Sub-scorers already propagated. return upTo; } - // TODO: implement - return DocIdSetIterator.NO_MORE_DOCS; + return newUpTo; } @Override diff --git a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java index 32d11daa44e8..5dd0fe6b2f23 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java @@ -895,6 +895,53 @@ private void doTestRandomSpecialMaxScore(float maxScore) throws IOException { dir.close(); } + /** Test advanceShallow when WAND is nested inside a conjunction with BlockScoreQueryWrapper. */ + public void testRandomNestedWAND() throws IOException { + Directory dir = newDirectory(); + IndexWriter w = new IndexWriter(dir, newIndexWriterConfig()); + int numDocs = atLeast(1000); + for (int i = 0; i < numDocs; ++i) { + Document doc = new Document(); + int numValues = random().nextInt(1 << random().nextInt(5)); + int start = random().nextInt(10); + for (int j = 0; j < numValues; ++j) { + doc.add(new StringField("foo", Integer.toString(start + j), Store.NO)); + } + w.addDocument(doc); + } + IndexReader reader = DirectoryReader.open(w); + w.close(); + // turn off concurrent search to avoid Random object used across threads resulting into + // RuntimeException, as WANDScorerQuery#createWeight has reference to this searcher, + // but will be called during searching + IndexSearcher searcher = newSearcher(reader, true, true, false); + + for (int iter = 0; iter < 10; ++iter) { + int start = random().nextInt(10); + int numClauses = random().nextInt(1 << random().nextInt(5)); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i = 0; i < numClauses; ++i) { + builder.add( + maybeWrap(new TermQuery(new Term("foo", Integer.toString(start + i)))), Occur.SHOULD); + } + Query wandQuery = new WANDScorerQuery(builder.build(), random().nextBoolean()); + + int blockLength = TestUtil.nextInt(random(), 2, 16); + Query wrappedWand = new BlockScoreQueryWrapper(wandQuery, blockLength); + + int filterTerm = random().nextInt(30); + Query nestedQuery = + new BooleanQuery.Builder() + .add(wrappedWand, Occur.MUST) + .add(new TermQuery(new Term("foo", Integer.toString(filterTerm))), Occur.MUST) + .build(); + + CheckHits.checkTopScores(random(), nestedQuery, searcher); + } + reader.close(); + dir.close(); + } + private static class MaxScoreWrapperScorer extends FilterScorer { private final int maxRange; From ccdec7d8eac0d79f8883b01e212c1229d2b72e2c Mon Sep 17 00:00:00 2001 From: Prithvi S Date: Tue, 23 Jun 2026 22:49:19 +0530 Subject: [PATCH 2/3] review changes --- lucene/CHANGES.txt | 4 ++-- .../src/java/org/apache/lucene/search/WANDScorer.java | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index dab20dc1c3a5..97b3eee74c83 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -323,6 +323,8 @@ Optimizations --------------------- * GITHUG#16280: Single-pass writeString fast path for short strings in ByteBuffersDataOutput (neoremind) +* GITHUB#16220: Implement WANDScorer.advanceShallow() to enable block-max optimizations when nested in conjunctions. (Prithvi S) + * GITHUB#16146: Speed up no-score filtered-optional Boolean queries by routing dense conjunctions with cached FixedBitSet filters through ConstantScoreBulkScorer, which batches matches in bulk via DocIdStream. (Costin Leau) @@ -632,8 +634,6 @@ Optimizations * GITHUB#16228: Reuse scratch int[] for ordinal translation. (Tim Brooks) -* GITHUB#16220: Implement WANDScorer.advanceShallow() to enable block-max optimizations when nested in conjunctions. (Prithvi S) - Bug Fixes --------------------- * GITHUB#15754: Fix HTMLStripCharFilter to prevent tags from incorrectly consuming subsequent diff --git a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java index 8f111294b49b..d22a4051b481 100644 --- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java @@ -569,17 +569,16 @@ public int advanceShallow(int target) throws IOException { // Propagate to sub-scorers. Scorers past target constrain the boundary to docID - 1. int newUpTo = DocIdSetIterator.NO_MORE_DOCS; for (Scorer scorer : allScorers) { + // Use <= instead of < so we still propagate and use the block boundary when already at + // target. if (scorer.docID() <= target) { newUpTo = Math.min(newUpTo, scorer.advanceShallow(target)); } else if (scorer.docID() != DocIdSetIterator.NO_MORE_DOCS) { newUpTo = Math.min(newUpTo, scorer.docID() - 1); } } - if (target <= upTo) { - // Within current block, return existing boundary. Sub-scorers already propagated. - return upTo; - } - return newUpTo; + // If within the current block, prefer the tighter freshly-computed boundary. + return target <= upTo ? Math.min(upTo, newUpTo) : newUpTo; } @Override From b9b7c9df0163a82d6b1fd5fb00e49b3b38d95f9a Mon Sep 17 00:00:00 2001 From: Prithvi S Date: Mon, 27 Jul 2026 22:56:14 +0530 Subject: [PATCH 3/3] review changes --- lucene/core/src/java/org/apache/lucene/search/WANDScorer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java index d22a4051b481..46a7369f65ce 100644 --- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java @@ -577,8 +577,7 @@ public int advanceShallow(int target) throws IOException { newUpTo = Math.min(newUpTo, scorer.docID() - 1); } } - // If within the current block, prefer the tighter freshly-computed boundary. - return target <= upTo ? Math.min(upTo, newUpTo) : newUpTo; + return newUpTo; } @Override