You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ji...@apache.org on 2026/06/03 11:39:41 UTC
(lucene) branch branch_10x updated: Use a single two-phase strategy for doc-values range queries (#16177)
This is an automated email from the ASF dual-hosted git repository.
jimczi pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/lucene.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 34be98d108f Use a single two-phase strategy for doc-values range queries (#16177)
34be98d108f is described below
commit 34be98d108fa6ed55020d8d96f144d4c92d42398
Author: Jim Ferenczi <ji...@apache.org>
AuthorDate: Wed Jun 3 12:21:04 2026 +0100
Use a single two-phase strategy for doc-values range queries (#16177)
Add intoBitSet to TwoPhaseIterator so bulk/SIMD range evaluation is an
override on the two-phase object rather than a separate plain DISI.
DenseConjunctionBulkScorer drives every two-phase clause through one
bit-set path; DocValuesRangeIterator absorbs (and removes)
BatchDocValuesRangeIterator, so a doc-values range stays two-phase end
to end. WIP snapshot.
---
lucene/CHANGES.txt | 6 +-
.../jmh/MultiFieldDocValuesRangeBenchmark.java | 65 +++++-
.../benchmark/jmh/PhraseScorerBenchmark.java | 63 ++++++
.../document/SortedNumericDocValuesRangeQuery.java | 17 +-
.../lucene/search/BatchDocValuesRangeIterator.java | 177 -----------------
.../lucene/search/ConstantScoreBulkScorer.java | 36 +++-
.../lucene/search/ConstantScoreScorerSupplier.java | 9 +-
.../lucene/search/DenseConjunctionBulkScorer.java | 158 ++++++---------
.../lucene/search/DocValuesRangeIterator.java | 68 ++++++-
.../org/apache/lucene/search/TwoPhaseIterator.java | 23 +++
.../apache/lucene/search/TestBooleanScorer.java | 12 +-
.../lucene/search/TestConstantScoreBulkScorer.java | 217 +++++++++++++++++++++
.../search/TestDenseConjunctionBulkScorer.java | 74 ++++++-
.../TestSkipBlockRangeIteratorIntoBitSet.java | 79 ++++----
14 files changed, 660 insertions(+), 344 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index de08b339836..7e1f6487977 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -119,8 +119,10 @@ Improvements
Optimizations
---------------------
-* GITHUB#16050: Add SIMD-accelerated bulk range evaluation for dense numeric doc values via
- BatchDocValuesRangeIterator and DocValuesRangeSupport. (Sagar Upadhyaya)
+* GITHUB#16050, GITHUB#16177: Doc-values range queries are now always two-phase iterators, with
+ SIMD-accelerated bulk range evaluation for dense numeric doc values exposed through
+ TwoPhaseIterator#intoBitSet (overridable for SIMD block evaluation) instead of a separate plain
+ DocIdSetIterator, speeding up conjunctions of range filters. (Sagar Upadhyaya, Jim Ferenczi)
* GITHUB#16153: Use TernaryLongHeap in UpdateGraphsUtils for faster HNSW graph merging. (Prithvi S)
diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/MultiFieldDocValuesRangeBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/MultiFieldDocValuesRangeBenchmark.java
index 2c50b8b6989..acb452ca0b1 100644
--- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/MultiFieldDocValuesRangeBenchmark.java
+++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/MultiFieldDocValuesRangeBenchmark.java
@@ -33,6 +33,10 @@ import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopFieldCollectorManager;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.MMapDirectory;
import org.openjdk.jmh.annotations.Benchmark;
@@ -76,11 +80,19 @@ public class MultiFieldDocValuesRangeBenchmark {
private static final String MIXED = "mixed";
private static final String SORTED = "sorted";
private static final String RANDOM = "random";
+ // All fields random; every clause matches ~70% so the intersection stays dense (stresses the
+ // bit-set intersection path).
+ private static final String DENSE = "dense";
+ // field0 matches ~all docs (a dense clause), every other field matches ~0.1% (very sparse), so a
+ // tiny surviving set must be confirmed against a dense clause (stresses survivor pruning).
+ private static final String DENSE_SPARSE = "dense_sparse";
private Directory dir;
private IndexReader reader;
private Path path;
private BooleanQuery query;
+ private Query singleRange;
+ private Query sparseNoSkipRange;
@State(Scope.Benchmark)
public static class Params {
@@ -90,7 +102,7 @@ public class MultiFieldDocValuesRangeBenchmark {
@Param({"1", "3", "5"})
public int fieldCount;
- @Param({CLUSTERED, MIXED, RANDOM, SORTED})
+ @Param({CLUSTERED, MIXED, RANDOM, SORTED, DENSE, DENSE_SPARSE})
public String dataPattern;
}
@@ -116,6 +128,13 @@ public class MultiFieldDocValuesRangeBenchmark {
long value = generateValue(params.dataPattern, f, i, params.docCount, r);
doc.add(NumericDocValuesField.indexedField("field" + f, value));
}
+ // A sparsely populated field WITHOUT a skip index (plain NumericDocValuesField). Its range
+ // iterator is a DocValuesValueRangeIterator whose approximation reports a real (sparse) cost,
+ // so a range on it routes to ConstantScoreBulkScorer's two-phase path rather than
+ // DenseConjunctionBulkScorer (which is what the skip-indexed fields above always hit).
+ if (i % 100 == 0) {
+ doc.add(new NumericDocValuesField("sparse", i));
+ }
w.addDocument(doc);
}
w.forceMerge(1);
@@ -141,6 +160,12 @@ public class MultiFieldDocValuesRangeBenchmark {
bqBuilder.add(new org.apache.lucene.search.MatchAllDocsQuery(), Occur.FILTER);
}
query = bqBuilder.build();
+
+ long[] range0 = getQueryRange(params.dataPattern, 0, params.docCount);
+ singleRange = SortedNumericDocValuesField.newSlowRangeQuery("field0", range0[0], range0[1]);
+
+ sparseNoSkipRange =
+ SortedNumericDocValuesField.newSlowRangeQuery("sparse", 0, params.docCount / 2);
}
private static long generateValue(
@@ -167,6 +192,8 @@ public class MultiFieldDocValuesRangeBenchmark {
return r.nextLong(0, docCount);
}
case RANDOM:
+ case DENSE:
+ case DENSE_SPARSE:
return r.nextLong(0, docCount);
default:
throw new IllegalArgumentException("Unknown pattern: " + pattern);
@@ -205,6 +232,18 @@ public class MultiFieldDocValuesRangeBenchmark {
long rangeSize4 = docCount / 5;
long lower4 = (docCount - rangeSize4) / 2;
return new long[] {lower4, lower4 + rangeSize4};
+ case DENSE:
+ // Each clause matches ~70% of docs, so the intersection stays dense.
+ return new long[] {0, (long) (docCount * 0.7)};
+ case DENSE_SPARSE:
+ if (fieldIdx == 0) {
+ // Dense clause: matches all docs.
+ return new long[] {0, docCount};
+ }
+ // Very sparse clause: matches ~0.1% of docs.
+ long rangeSize5 = Math.max(1, docCount / 1000);
+ long lower5 = (docCount - rangeSize5) / 2;
+ return new long[] {lower5, lower5 + rangeSize5};
default:
throw new IllegalArgumentException("Unknown pattern: " + pattern);
}
@@ -239,4 +278,28 @@ public class MultiFieldDocValuesRangeBenchmark {
searcher.setQueryCache(null);
return searcher.count(query);
}
+
+ // A bare single range, driven through the bulk scorer. We collect (index-order, no early
+ // termination) rather than count() because SortedNumericDocValuesRangeQuery#count()
+ // short-circuits
+ // via the skip index and never builds a scorer. Selectivity follows the data pattern:
+ // clustered ~ very sparse, random ~ 20%, dense ~ 70%.
+ @Benchmark
+ public TopDocs searchSingleRange() throws IOException {
+ IndexSearcher searcher = new IndexSearcher(reader);
+ return searcher.search(
+ singleRange, new TopFieldCollectorManager(Sort.INDEXORDER, 10, Integer.MAX_VALUE));
+ }
+
+ // A range on the sparse, non-skip-indexed field. Its DocValuesValueRangeIterator reports a real
+ // (sparse) cost, so this is the one shape that routes to ConstantScoreBulkScorer's two-phase path
+ // (ours) / DefaultBulkScorer (#16143's fallback) instead of DenseConjunctionBulkScorer. Forced
+ // through the bulk scorer via a collector (count() would fall back to scoring anyway with no skip
+ // index, but the collector makes the path unambiguous).
+ @Benchmark
+ public TopDocs searchSparseNoSkipRange() throws IOException {
+ IndexSearcher searcher = new IndexSearcher(reader);
+ return searcher.search(
+ sparseNoSkipRange, new TopFieldCollectorManager(Sort.INDEXORDER, 10, Integer.MAX_VALUE));
+ }
}
diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/PhraseScorerBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/PhraseScorerBenchmark.java
index 2b0ba46b948..c073fee3166 100644
--- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/PhraseScorerBenchmark.java
+++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/PhraseScorerBenchmark.java
@@ -21,14 +21,22 @@ import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.SortedNumericDocValuesField;
+import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopFieldCollectorManager;
import org.apache.lucene.search.TopScoreDocCollectorManager;
@@ -90,6 +98,11 @@ public class PhraseScorerBenchmark {
// 50% of docs: no query terms at all
doc.add(new TextField("text", "unrelated words", Field.Store.NO));
}
+ doc.add(NumericDocValuesField.indexedField("num", i));
+ // A term present in every doc, used as a fully-dense lead clause in conjunctions. Unlike
+ // MatchAllDocsQuery (which BooleanQuery#rewrite strips from multi-clause FILTERs), a real
+ // TermQuery survives rewrite, so the conjunction path is actually exercised.
+ doc.add(new StringField("all", "1", Field.Store.NO));
writer.addDocument(doc);
}
}
@@ -105,6 +118,56 @@ public class PhraseScorerBenchmark {
dir.close();
}
+ // A constant-score conjunction of a phrase FILTER clause with a fully-dense term clause routes
+ // through DenseConjunctionBulkScorer (the phrase is a two-phase clause whose approximation
+ // matches ~50% but whose phrase matches ~0.1%), so this exercises the unified two-phase
+ // bit-set/survivor path. The "all" term is present in every doc and forms the dense lead
+ // iterator; it is a real TermQuery rather than MatchAllDocsQuery, which BooleanQuery#rewrite
+ // would strip from a multi-clause FILTER, collapsing this back to the bare phrase.
+ @Benchmark
+ public int benchmarkPhraseFilterConjunction() throws IOException {
+ Query q =
+ new BooleanQuery.Builder()
+ .add(exactQuery, Occur.FILTER)
+ .add(new TermQuery(new Term("all", "1")), Occur.FILTER)
+ .build();
+ return searcher.count(q);
+ }
+
+ // Two phrase clauses (both two-phase) confirming each other.
+ @Benchmark
+ public int benchmarkPhraseConjunction() throws IOException {
+ Query q =
+ new BooleanQuery.Builder()
+ .add(exactQuery, Occur.FILTER)
+ .add(new PhraseQuery("text", "lazy", "dog"), Occur.FILTER)
+ .build();
+ return searcher.count(q);
+ }
+
+ // Phrase (two-phase) ∩ term (plain, dense): the term is the dense clause, the phrase confirms.
+ @Benchmark
+ public int benchmarkPhraseTermConjunction() throws IOException {
+ Query q =
+ new BooleanQuery.Builder()
+ .add(exactQuery, Occur.FILTER)
+ .add(new TermQuery(new Term("text", "quick")), Occur.FILTER)
+ .build();
+ return searcher.count(q);
+ }
+
+ // Cross-type: phrase (two-phase) ∩ doc-values range (two-phase) — both two-phase clauses of
+ // different kinds pruning each other through the unified path.
+ @Benchmark
+ public int benchmarkPhraseRangeConjunction() throws IOException {
+ Query q =
+ new BooleanQuery.Builder()
+ .add(exactQuery, Occur.FILTER)
+ .add(SortedNumericDocValuesField.newSlowRangeQuery("num", 0, 500_000), Occur.FILTER)
+ .build();
+ return searcher.count(q);
+ }
+
@Benchmark
public TopDocs benchmarkExactTopScores() throws IOException {
return searcher.search(exactQuery, 10);
diff --git a/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java b/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
index 173673c6514..f00a4e72c35 100644
--- a/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
@@ -25,7 +25,6 @@ import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.SortedNumericDocValues;
-import org.apache.lucene.search.BatchDocValuesRangeIterator;
import org.apache.lucene.search.ConstantScoreScorerSupplier;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.DocIdSetIterator;
@@ -141,17 +140,13 @@ final class SortedNumericDocValuesRangeQuery extends NumericDocValuesRangeQuery
final SortField primarySortField;
if (singleton != null) {
- if (skipper != null) {
- if ((primarySortField = densePrimarySort(context.reader(), skipper)) != null) {
- return getScorerSupplierFromDensePrimarySort(singleton, skipper, primarySortField);
- }
- // Use batch iterator for bulk block evaluation via intoBitSet()
- return ConstantScoreScorerSupplier.fromIterator(
- new BatchDocValuesRangeIterator(singleton, skipper, lowerValue, upperValue),
- score(),
- scoreMode,
- maxDoc);
+ if (skipper != null
+ && (primarySortField = densePrimarySort(context.reader(), skipper)) != null) {
+ return getScorerSupplierFromDensePrimarySort(singleton, skipper, primarySortField);
}
+ // A single two-phase iterator covers every density: its approximation rides the skipper
+ // (no over-scan), its intoBitSet bulk-evaluates dense blocks, and YES runs collect as
+ // ranges. The bulk scorer unwraps it and picks the right strategy from there.
return ConstantScoreScorerSupplier.fromIterator(
TwoPhaseIterator.asDocIdSetIterator(
DocValuesRangeIterator.forRange(singleton, skipper, lowerValue, upperValue)),
diff --git a/lucene/core/src/java/org/apache/lucene/search/BatchDocValuesRangeIterator.java b/lucene/core/src/java/org/apache/lucene/search/BatchDocValuesRangeIterator.java
deleted file mode 100644
index 9ad12fc2015..00000000000
--- a/lucene/core/src/java/org/apache/lucene/search/BatchDocValuesRangeIterator.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.lucene.search;
-
-import java.io.IOException;
-import org.apache.lucene.index.DocValuesSkipper;
-import org.apache.lucene.index.NumericDocValues;
-import org.apache.lucene.util.FixedBitSet;
-
-/**
- * A {@link DocIdSetIterator} for numeric doc values range queries that batch-evaluates values for
- * MAYBE blocks. Instead of checking one doc at a time through a {@link
- * org.apache.lucene.search.TwoPhaseIterator}, this iterator reads values in a tight loop and sets
- * bits directly in a {@link FixedBitSet}, enabling the {@link DenseConjunctionBulkScorer} to use
- * the faster bitset intersection path.
- *
- * <p>This is used for single-valued numeric fields with a skip index.
- */
-public final class BatchDocValuesRangeIterator extends DocIdSetIterator {
-
- private final SkipBlockRangeIterator blockIterator;
- private final NumericDocValues values;
- private final long minValue;
- private final long maxValue;
- private int doc = -1;
-
- public BatchDocValuesRangeIterator(
- NumericDocValues values, DocValuesSkipper skipper, long minValue, long maxValue) {
- this.blockIterator = new SkipBlockRangeIterator(skipper, minValue, maxValue);
- this.values = values;
- this.minValue = minValue;
- this.maxValue = maxValue;
- }
-
- @Override
- public int docID() {
- return doc;
- }
-
- @Override
- public int nextDoc() throws IOException {
- return advance(doc + 1);
- }
-
- @Override
- public int advance(int target) throws IOException {
- int blockDoc = blockIterator.docID();
- if (blockDoc < target) {
- blockDoc = blockIterator.advance(target);
- }
- if (blockDoc == NO_MORE_DOCS) {
- return doc = NO_MORE_DOCS;
- }
-
- // For YES blocks, all docs have values in range, so return the first doc
- if (blockIterator.getMatch() == SkipBlockRangeIterator.Match.YES) {
- return doc = blockDoc;
- }
-
- // Scan forward through YES_IF_PRESENT and MAYBE blocks to find a matching doc.
- // - YES_IF_PRESENT: all values are in range, but some docs may not have a value.
- // We only need to check presence via advanceExact().
- // - MAYBE: docs may or may not have a value, and values may or may not be in range.
- // We need both a presence check (advanceExact) and a range check (longValue).
- int docToCheck = Math.max(target, blockDoc);
- int currentBlockEnd = blockIterator.blockEnd();
- while (docToCheck != NO_MORE_DOCS) {
- if (values.advanceExact(docToCheck)) {
- // If we landed in a YES_IF_PRESENT block, skip the range check
- if (blockIterator.getMatch() == SkipBlockRangeIterator.Match.YES_IF_PRESENT) {
- return doc = docToCheck;
- }
- // This is a MAYBE block. We need to verify if the value is in range
- long v = values.longValue();
- if (v >= minValue && v <= maxValue) {
- return doc = docToCheck;
- }
- }
- docToCheck++;
- // Check if we've left the current block
- if (docToCheck >= currentBlockEnd) {
- // Move to next matching block
- blockDoc = blockIterator.advance(docToCheck);
- if (blockDoc == NO_MORE_DOCS) {
- return doc = NO_MORE_DOCS;
- }
- docToCheck = blockDoc;
- if (blockIterator.getMatch() == SkipBlockRangeIterator.Match.YES) {
- return doc = docToCheck;
- }
- currentBlockEnd = blockIterator.blockEnd();
- }
- }
- return doc = NO_MORE_DOCS;
- }
-
- @Override
- public long cost() {
- return values.cost();
- }
-
- @Override
- public int docIDRunEnd() throws IOException {
- return blockIterator.docIDRunEnd();
- }
-
- @Override
- public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
- while (doc < upTo) {
- // Advance block iterator if needed
- if (blockIterator.docID() < doc) {
- blockIterator.advance(doc);
- }
- if (blockIterator.docID() >= upTo || blockIterator.docID() == NO_MORE_DOCS) {
- doc = blockIterator.docID() == NO_MORE_DOCS ? NO_MORE_DOCS : blockIterator.docID();
- return;
- }
-
- int blockStart = Math.max(doc, blockIterator.docID());
- SkipBlockRangeIterator.Match match = blockIterator.getMatch();
-
- // Use blockEnd() for MAYBE blocks since docIDRunEnd() is conservative (returns doc+1)
- int blockEnd =
- match == SkipBlockRangeIterator.Match.MAYBE
- ? Math.min(upTo, blockIterator.blockEnd())
- : Math.min(upTo, blockIterator.docIDRunEnd());
-
- switch (match) {
- case YES:
- // All docs in this range match — set all bits
- bitSet.set(blockStart - offset, blockEnd - offset);
- break;
-
- case YES_IF_PRESENT:
- // All values in this block are in range, but the field is sparse so some docs
- // may not have a value. No range check needed here.
- for (int d = blockStart; d < blockEnd; d++) {
- if (values.advanceExact(d)) {
- bitSet.set(d - offset);
- }
- }
- break;
-
- case MAYBE:
- // Use rangeIntoBitSet — SIMD bulk evaluation for the full block.
- // For dense fields, this bypasses advanceExact() overhead entirely.
- values.rangeIntoBitSet(blockStart, blockEnd, minValue, maxValue, bitSet, offset);
- break;
- }
-
- // Move past this block
- doc = blockEnd;
- if (doc <= upTo) {
- blockIterator.advance(doc);
- if (blockIterator.docID() == NO_MORE_DOCS) {
- doc = NO_MORE_DOCS;
- return;
- }
- doc = blockIterator.docID();
- }
- }
- }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreBulkScorer.java b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreBulkScorer.java
index 024e2f911a4..79c04200997 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreBulkScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreBulkScorer.java
@@ -28,17 +28,38 @@ import org.apache.lucene.util.MathUtil;
final class ConstantScoreBulkScorer extends BulkScorer {
private final Scorer scorer;
private final DocIdSetIterator iterator;
+ // When non-null, matches of the approximation must be confirmed via twoPhase#matches() /
+ // twoPhase#intoBitSet rather than taken as-is.
+ private final TwoPhaseIterator twoPhase;
private final FixedBitSet windowMatches = new FixedBitSet(DenseConjunctionBulkScorer.WINDOW_SIZE);
ConstantScoreBulkScorer(float score, ScoreMode scoreMode, DocIdSetIterator iterator) {
+ this(score, scoreMode, iterator, null);
+ }
+
+ /**
+ * Variant that confirms the matches of a {@link TwoPhaseIterator}. It is worthwhile when the
+ * two-phase iterator overrides {@link TwoPhaseIterator#intoBitSet} with a bulk implementation, so
+ * a single clause can be collected window-by-window instead of confirmed one doc at a time.
+ */
+ ConstantScoreBulkScorer(float score, ScoreMode scoreMode, TwoPhaseIterator twoPhase) {
+ this(score, scoreMode, twoPhase.approximation(), twoPhase);
+ }
+
+ private ConstantScoreBulkScorer(
+ float score, ScoreMode scoreMode, DocIdSetIterator iterator, TwoPhaseIterator twoPhase) {
if (scoreMode.needsScores()) {
throw new IllegalArgumentException("ScoreMode must not need scores: " + scoreMode);
}
- if (TwoPhaseIterator.unwrap(iterator) != null) {
+ if (twoPhase == null && TwoPhaseIterator.unwrap(iterator) != null) {
throw new IllegalArgumentException("Iterator must not wrap a TwoPhaseIterator");
}
- this.scorer = new ConstantScoreScorer(score, scoreMode, iterator);
+ this.scorer =
+ twoPhase == null
+ ? new ConstantScoreScorer(score, scoreMode, iterator)
+ : new ConstantScoreScorer(score, scoreMode, twoPhase);
this.iterator = iterator;
+ this.twoPhase = twoPhase;
}
@Override
@@ -46,7 +67,7 @@ final class ConstantScoreBulkScorer extends BulkScorer {
collector.setScorer(scorer);
DocIdSetIterator competitiveIterator = collector.competitiveIterator();
if (competitiveIterator != null) {
- scoreIterator(collector, acceptDocs, iterator, competitiveIterator, min, max);
+ scoreIterator(collector, acceptDocs, iterator, twoPhase, competitiveIterator, min, max);
} else {
scoreIteratorIntoBitSet(collector, acceptDocs, iterator, min, max);
}
@@ -62,6 +83,7 @@ final class ConstantScoreBulkScorer extends BulkScorer {
LeafCollector collector,
Bits acceptDocs,
DocIdSetIterator iterator,
+ TwoPhaseIterator twoPhase,
DocIdSetIterator competitiveIterator,
int min,
int max)
@@ -86,7 +108,7 @@ final class ConstantScoreBulkScorer extends BulkScorer {
}
}
- if (acceptDocs == null || acceptDocs.get(doc)) {
+ if ((acceptDocs == null || acceptDocs.get(doc)) && (twoPhase == null || twoPhase.matches())) {
collector.collect(doc);
}
@@ -110,7 +132,11 @@ final class ConstantScoreBulkScorer extends BulkScorer {
MathUtil.unsignedMin(max, windowBase + DenseConjunctionBulkScorer.WINDOW_SIZE);
assert windowMatches.scanIsEmpty();
- iterator.intoBitSet(windowMax, windowMatches, windowBase);
+ if (twoPhase == null) {
+ iterator.intoBitSet(windowMax, windowMatches, windowBase);
+ } else {
+ twoPhase.intoBitSet(windowMax, windowMatches, windowBase);
+ }
if (acceptDocs != null) {
acceptDocs.applyMask(windowMatches, windowBase);
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorerSupplier.java b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorerSupplier.java
index 48239201443..a8ff43ef9f2 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorerSupplier.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorerSupplier.java
@@ -90,8 +90,13 @@ public abstract class ConstantScoreScorerSupplier extends ScorerSupplier {
twoPhases = Collections.singletonList(twoPhase);
}
return new DenseConjunctionBulkScorer(iterators, twoPhases, maxDoc, score);
- } else if (scoreMode.needsScores() == false && twoPhase == null) {
- return new ConstantScoreBulkScorer(score, scoreMode, iterator);
+ } else if (scoreMode.needsScores() == false) {
+ // Collect window-by-window via intoBitSet. For a two-phase iterator this confirms matches in
+ // its (possibly bulk) intoBitSet; the only overhead over a plain leap-frog is the reusable
+ // window bit set, which buys batched live-docs masking and bulk collection in return.
+ return twoPhase == null
+ ? new ConstantScoreBulkScorer(score, scoreMode, iterator)
+ : new ConstantScoreBulkScorer(score, scoreMode, twoPhase);
} else {
Scorer scorer =
twoPhase == null
diff --git a/lucene/core/src/java/org/apache/lucene/search/DenseConjunctionBulkScorer.java b/lucene/core/src/java/org/apache/lucene/search/DenseConjunctionBulkScorer.java
index 08c71a81d7d..46d2c36394c 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DenseConjunctionBulkScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DenseConjunctionBulkScorer.java
@@ -51,6 +51,14 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
return twoPhase().docIDRunEnd();
}
}
+
+ void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
+ if (twoPhase() == null) {
+ approximation().intoBitSet(upTo, bitSet, offset);
+ } else {
+ twoPhase().intoBitSet(upTo, bitSet, offset);
+ }
+ }
}
// Use a small-ish window size to make sure that we can take advantage of gaps in the postings of
@@ -67,8 +75,7 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
private final FixedBitSet windowMatches = new FixedBitSet(WINDOW_SIZE);
private final FixedBitSet clauseWindowMatches = new FixedBitSet(WINDOW_SIZE);
- private final List<DocIdSetIterator> windowApproximations = new ArrayList<>();
- private final List<TwoPhaseIterator> windowTwoPhases = new ArrayList<>();
+ private final List<DisiWrapper> windowClauses = new ArrayList<>();
static DenseConjunctionBulkScorer of(List<Scorer> filters, int maxDoc, float constantScore) {
List<DocIdSetIterator> iterators = new ArrayList<>();
@@ -181,16 +188,13 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
for (DisiWrapper w : iterators) {
int docIdRunEnd = w.docIDRunEnd();
if (w.docID() > min || docIdRunEnd < minRunEndThreshold) {
- windowApproximations.add(w.approximation());
- if (w.twoPhase() != null) {
- windowTwoPhases.add(w.twoPhase());
- }
+ windowClauses.add(w);
} else {
minDocIDRunEnd = Math.min(minDocIDRunEnd, docIdRunEnd);
}
}
- if (acceptDocs == null && windowApproximations.isEmpty()) {
+ if (acceptDocs == null && windowClauses.isEmpty()) {
// We have a large range of doc IDs that all match.
collector.collectRange(min, minDocIDRunEnd);
return minDocIDRunEnd;
@@ -198,15 +202,8 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
int bitsetWindowMax = MathUtil.unsignedMin(minDocIDRunEnd, WINDOW_SIZE + min);
- if (windowTwoPhases.isEmpty()) {
- scoreWindowUsingBitSet(collector, acceptDocs, windowApproximations, min, bitsetWindowMax);
- } else {
- windowTwoPhases.sort(Comparator.comparingDouble(TwoPhaseIterator::matchCost));
- scoreWindowUsingLeapFrog(
- collector, acceptDocs, windowApproximations, windowTwoPhases, min, bitsetWindowMax);
- windowTwoPhases.clear();
- }
- windowApproximations.clear();
+ scoreWindowUsingBitSet(collector, acceptDocs, windowClauses, min, bitsetWindowMax);
+ windowClauses.clear();
return bitsetWindowMax;
}
@@ -214,7 +211,7 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
private void scoreWindowUsingBitSet(
LeafCollector collector,
Bits acceptDocs,
- List<DocIdSetIterator> iterators,
+ List<DisiWrapper> iterators,
int windowBase,
int windowMax)
throws IOException {
@@ -226,9 +223,9 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
// This happens if all clauses fully matched the window and there are deleted docs.
windowMatches.set(0, windowMax - windowBase);
} else {
- DocIdSetIterator lead = iterators.get(0);
+ DisiWrapper lead = iterators.get(0);
if (lead.docID() < windowBase) {
- lead.advance(windowBase);
+ lead.approximation().advance(windowBase);
}
lead.intoBitSet(windowMax, windowMatches, windowBase);
}
@@ -240,15 +237,42 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
int windowSize = windowMax - windowBase;
int threshold = windowSize / DENSITY_THRESHOLD_INVERSE;
+ // Above this many surviving docs, decoding a two-phase clause's whole window in one shot beats
+ // confirming each survivor one at a time; below it we confirm survivors only.
+ int bulkConfirmThreshold = windowSize / 4;
int upTo = 1; // the leading clause at index 0 is already applied
- for (; upTo < iterators.size() && windowMatches.cardinality() >= threshold; upTo++) {
- DocIdSetIterator other = iterators.get(upTo);
+ for (int cardinality = windowMatches.cardinality();
+ upTo < iterators.size() && cardinality >= threshold;
+ upTo++, cardinality = windowMatches.cardinality()) {
+ DisiWrapper other = iterators.get(upTo);
if (other.docID() < windowBase) {
- other.advance(windowBase);
+ other.approximation().advance(windowBase);
+ }
+ TwoPhaseIterator twoPhase = other.twoPhase();
+ if (twoPhase != null && cardinality < bulkConfirmThreshold) {
+ // Sparse survivors + per-doc confirmation: confirm only the docs that survived the cheaper
+ // clauses (the bit set gates matches()), never decoding a doc another clause excluded.
+ DocIdSetIterator approximation = other.approximation();
+ for (int windowMatch = windowMatches.nextSetBit(0);
+ windowMatch != DocIdSetIterator.NO_MORE_DOCS; ) {
+ int doc = windowBase + windowMatch;
+ int otherDoc = approximation.docID();
+ if (otherDoc < doc) {
+ otherDoc = approximation.advance(doc);
+ }
+ if (otherDoc != doc || twoPhase.matches() == false) {
+ windowMatches.clear(windowMatch);
+ }
+ windowMatch = advance(windowMatches, windowMatch + 1);
+ }
+ } else {
+ // Dense survivors, or a plain iterator: load this clause's matches in bulk and intersect.
+ // For a two-phase clause this still confirms matches() via its (possibly vectorized)
+ // intoBitSet.
+ other.intoBitSet(windowMax, clauseWindowMatches, windowBase);
+ windowMatches.and(clauseWindowMatches);
+ clauseWindowMatches.clear();
}
- other.intoBitSet(windowMax, clauseWindowMatches, windowBase);
- windowMatches.and(clauseWindowMatches);
- clauseWindowMatches.clear();
}
if (upTo < iterators.size()) {
@@ -256,13 +280,15 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
// after applying a few clauses, we finish evaluating the intersection using the traditional
// leap-frog approach. This proved important with a query such as "+secretary +of +state" on
// wikibigall, where the intersection becomes sparse after intersecting "secretary" and
- // "state".
+ // "state". As the leap-frog only visits surviving docs, two-phase clauses confirm matches()
+ // here only on docs that no cheaper clause already excluded.
advanceHead:
for (int windowMatch = windowMatches.nextSetBit(0);
windowMatch != DocIdSetIterator.NO_MORE_DOCS; ) {
int doc = windowBase + windowMatch;
+ // First confirm every remaining approximation is on doc...
for (int i = upTo; i < iterators.size(); ++i) {
- DocIdSetIterator other = iterators.get(i);
+ DocIdSetIterator other = iterators.get(i).approximation();
int otherDoc = other.docID();
if (otherDoc < doc) {
otherDoc = other.advance(doc);
@@ -272,6 +298,14 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
continue advanceHead;
}
}
+ // ...then run the (more expensive) two-phase confirmations, only on surviving docs.
+ for (int i = upTo; i < iterators.size(); ++i) {
+ TwoPhaseIterator twoPhase = iterators.get(i).twoPhase();
+ if (twoPhase != null && twoPhase.matches() == false) {
+ windowMatch = advance(windowMatches, windowMatch + 1);
+ continue advanceHead;
+ }
+ }
collector.collect(doc);
windowMatch = advance(windowMatches, windowMatch + 1);
}
@@ -282,76 +316,6 @@ final class DenseConjunctionBulkScorer extends BulkScorer {
windowMatches.clear();
}
- private static void scoreWindowUsingLeapFrog(
- LeafCollector collector,
- Bits acceptDocs,
- List<DocIdSetIterator> approximations,
- List<TwoPhaseIterator> twoPhases,
- int min,
- int max)
- throws IOException {
- assert twoPhases.size() > 0;
- assert approximations.size() >= twoPhases.size();
-
- if (approximations.size() == 1) {
- // scoreWindowUsingLeapFrog is only used if there is at least one two-phase iterator, so our
- // single clause is a two-phase iterator
- assert twoPhases.size() == 1;
- DocIdSetIterator approximation = approximations.get(0);
- TwoPhaseIterator twoPhase = twoPhases.get(0);
- if (approximation.docID() < min) {
- approximation.advance(min);
- }
- for (int doc = approximation.docID(); doc < max; doc = approximation.nextDoc()) {
- if ((acceptDocs == null || acceptDocs.get(doc)) && twoPhase.matches()) {
- collector.collect(doc);
- }
- }
- } else {
- DocIdSetIterator lead1 = approximations.get(0);
- DocIdSetIterator lead2 = approximations.get(1);
-
- if (lead1.docID() < min) {
- lead1.advance(min);
- }
-
- advanceHead:
- for (int doc = lead1.docID(); doc < max; ) {
- if (acceptDocs != null && acceptDocs.get(doc) == false) {
- doc = lead1.nextDoc();
- continue;
- }
- int doc2 = lead2.docID();
- if (doc2 < doc) {
- doc2 = lead2.advance(doc);
- }
- if (doc != doc2) {
- doc = lead1.advance(Math.min(doc2, max));
- continue;
- }
- for (int i = 2; i < approximations.size(); ++i) {
- DocIdSetIterator other = approximations.get(i);
- int docN = other.docID();
- if (docN < doc) {
- docN = other.advance(doc);
- }
- if (doc != docN) {
- doc = lead1.advance(Math.min(docN, max));
- continue advanceHead;
- }
- }
- for (TwoPhaseIterator twoPhase : twoPhases) {
- if (twoPhase.matches() == false) {
- doc = lead1.nextDoc();
- continue advanceHead;
- }
- }
- collector.collect(doc);
- doc = lead1.nextDoc();
- }
- }
- }
-
@Override
public long cost() {
return iterators.get(0).approximation().cost();
diff --git a/lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java b/lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java
index 918e7ffa580..c20c7ed7cd7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java
@@ -23,6 +23,7 @@ import org.apache.lucene.index.SortedDocValues;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.index.SortedSetDocValues;
import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.IOBooleanSupplier;
import org.apache.lucene.util.LongBitSet;
@@ -52,7 +53,14 @@ public abstract sealed class DocValuesRangeIterator extends TwoPhaseIterator {
return skipper == null
? new DocValuesValueRangeIterator(values, check, 2)
: new DocValuesBlockRangeIterator(
- values, new SkipBlockRangeIterator(skipper, min, max), check, 2, false);
+ values,
+ new SkipBlockRangeIterator(skipper, min, max),
+ check,
+ 2,
+ false,
+ values,
+ min,
+ max);
}
/**
@@ -245,6 +253,11 @@ public abstract sealed class DocValuesRangeIterator extends TwoPhaseIterator {
private final IOBooleanSupplier predicate;
private final float matchCost;
private final boolean alwaysCheckPredicate;
+ // Non-null only for single-valued numeric doc values, in which case a whole block can be
+ // range-evaluated in one shot in intoBitSet rather than confirming matches() one doc at a time.
+ private final NumericDocValues numericValues;
+ private final long minValue;
+ private final long maxValue;
private DocValuesBlockRangeIterator(
DocIdSetIterator disi,
@@ -252,12 +265,27 @@ public abstract sealed class DocValuesRangeIterator extends TwoPhaseIterator {
IOBooleanSupplier predicate,
float matchCost,
boolean alwaysCheckPredicate) {
+ this(disi, blockIterator, predicate, matchCost, alwaysCheckPredicate, null, 0, 0);
+ }
+
+ private DocValuesBlockRangeIterator(
+ DocIdSetIterator disi,
+ SkipBlockRangeIterator blockIterator,
+ IOBooleanSupplier predicate,
+ float matchCost,
+ boolean alwaysCheckPredicate,
+ NumericDocValues numericValues,
+ long minValue,
+ long maxValue) {
super(blockIterator);
this.disi = disi;
this.blockIterator = blockIterator;
this.predicate = predicate;
this.matchCost = matchCost;
this.alwaysCheckPredicate = alwaysCheckPredicate;
+ this.numericValues = numericValues;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
}
private boolean advanceDisi(int target) throws IOException {
@@ -287,6 +315,44 @@ public abstract sealed class DocValuesRangeIterator extends TwoPhaseIterator {
return blockIterator.docIDRunEnd();
}
+ @Override
+ public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
+ if (numericValues == null) {
+ // Generic doc values: confirm each candidate one doc at a time (the TwoPhaseIterator
+ // default). Only single-valued numeric fields can be range-evaluated block at a time.
+ super.intoBitSet(upTo, bitSet, offset);
+ return;
+ }
+ while (blockIterator.docID() < upTo) {
+ int blockStart = blockIterator.docID();
+ SkipBlockRangeIterator.Match match = blockIterator.getMatch();
+ // For MAYBE blocks docIDRunEnd() is conservative (doc+1), so use the full block boundary to
+ // evaluate the whole block at once.
+ int blockEnd =
+ match == SkipBlockRangeIterator.Match.MAYBE
+ ? Math.min(upTo, blockIterator.blockEnd())
+ : Math.min(upTo, blockIterator.docIDRunEnd());
+ switch (match) {
+ case YES -> bitSet.set(blockStart - offset, blockEnd - offset);
+ case YES_IF_PRESENT -> {
+ // All present values are in range, but the field is sparse: set a bit for every
+ // doc that has a value. Delegate to intoBitSet so dense codecs can bulk-set the run
+ // rather than probing one doc at a time. Only advance forward; a preceding YES block
+ // leaves the iterator behind blockStart, while MAYBE/YES_IF_PRESENT blocks leave it
+ // at or past it.
+ if (numericValues.docID() < blockStart) {
+ numericValues.advance(blockStart);
+ }
+ numericValues.intoBitSet(blockEnd, bitSet, offset);
+ }
+ case MAYBE ->
+ numericValues.rangeIntoBitSet(
+ blockStart, blockEnd, minValue, maxValue, bitSet, offset);
+ }
+ blockIterator.advance(blockEnd);
+ }
+ }
+
@Override
public float matchCost() {
return matchCost;
diff --git a/lucene/core/src/java/org/apache/lucene/search/TwoPhaseIterator.java b/lucene/core/src/java/org/apache/lucene/search/TwoPhaseIterator.java
index cb420d330a3..c2ff94758e3 100644
--- a/lucene/core/src/java/org/apache/lucene/search/TwoPhaseIterator.java
+++ b/lucene/core/src/java/org/apache/lucene/search/TwoPhaseIterator.java
@@ -18,6 +18,7 @@ package org.apache.lucene.search;
import java.io.IOException;
import java.util.Objects;
+import org.apache.lucene.util.FixedBitSet;
/**
* Returned by {@link Scorer#twoPhaseIterator()} to expose an approximation of a {@link
@@ -121,4 +122,26 @@ public abstract class TwoPhaseIterator {
public int docIDRunEnd() throws IOException {
return approximation().docID();
}
+
+ /**
+ * Load the doc IDs that both belong to the {@link #approximation()} and {@link #matches() match},
+ * and are in {@code [approximation().docID(), upTo)}, into {@code bitSet}, the document whose ID
+ * is {@code i} being stored at bit {@code i - offset}. Upon return the {@link #approximation()}
+ * is positioned on the first doc that is {@code >= upTo}, mirroring {@link
+ * DocIdSetIterator#intoBitSet}.
+ *
+ * <p>The default implementation walks the {@link #approximation()} and confirms each doc with
+ * {@link #matches()} — i.e. it is functionally identical to the leap-frog evaluation, just
+ * writing into a bit set. Implementations that can confirm matches in bulk — for instance a
+ * vectorized scan over a columnar block — should override it; the bit set window is then
+ * collected in one shot rather than one doc at a time.
+ */
+ public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
+ DocIdSetIterator approximation = approximation();
+ for (int doc = approximation.docID(); doc < upTo; doc = approximation.nextDoc()) {
+ if (matches()) {
+ bitSet.set(doc - offset);
+ }
+ }
+ }
}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
index bb8cd61ee30..adb8db7e3ba 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
@@ -261,7 +261,7 @@ public class TestBooleanScorer extends LuceneTestCase {
1f, ScoreMode.COMPLETE, new CountingDocIdSetIterator(new int[] {1})));
}
- public void testConstantScoreScorerSupplierPreservesTwoPhaseIterator() throws IOException {
+ public void testConstantScoreScorerSupplierBulkCollectsTwoPhaseWithMatcher() throws IOException {
int[] docs = {1, 2, 3};
CountingDocIdSetIterator approximation = new CountingDocIdSetIterator(docs);
DocIdSetIterator iterator =
@@ -291,17 +291,21 @@ public class TestBooleanScorer extends LuceneTestCase {
}
@Override
- public void collect(DocIdStream stream) {
- fail("two-phase iterators must preserve per-doc confirmation");
+ public void collect(DocIdStream stream) throws IOException {
+ stream.forEach(doc -> collected[count[0]++] = doc);
}
};
+ // A no-score two-phase iterator is bulk-collected through ConstantScoreBulkScorer; its matcher
+ // is preserved because TwoPhaseIterator#intoBitSet confirms matches() while loading the window.
BulkScorer bulkScorer =
ConstantScoreScorerSupplier.fromIterator(iterator, 1f, ScoreMode.COMPLETE_NO_SCORES, 9000)
.bulkScorer();
- assertThat(bulkScorer, instanceOf(DefaultBulkScorer.class));
+ assertThat(bulkScorer, instanceOf(ConstantScoreBulkScorer.class));
assertEquals(DocIdSetIterator.NO_MORE_DOCS, bulkScorer.score(collector, null, 0, 9000));
+ // doc 2 is excluded by matches(), so the matcher is honored even via the bulk path.
assertArrayEquals(new int[] {1, 3}, ArrayUtil.copyOfSubArray(collected, 0, count[0]));
+ // Confirmation runs through matches(), not the approximation's own bulk intoBitSet.
assertEquals(0, approximation.intoBitSetCalls);
}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreBulkScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreBulkScorer.java
new file mode 100644
index 00000000000..290503742da
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreBulkScorer.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search;
+
+import java.io.IOException;
+import org.apache.lucene.tests.search.AssertingBulkScorer;
+import org.apache.lucene.tests.search.RandomApproximationQuery.RandomTwoPhaseView;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.tests.util.TestUtil;
+import org.apache.lucene.util.BitSetIterator;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.FixedBitSet;
+
+public class TestConstantScoreBulkScorer extends LuceneTestCase {
+
+ /**
+ * A two-phase iterator that advertises a bulk {@link TwoPhaseIterator#intoBitSet}. Its
+ * approximation is a superset of the actual matches, so {@link #matches()} genuinely filters.
+ */
+ private static class BulkTwoPhase extends TwoPhaseIterator {
+ private final FixedBitSet matchSet;
+
+ BulkTwoPhase(FixedBitSet approximation, FixedBitSet matchSet) {
+ super(new BitSetIterator(approximation, approximation.approximateCardinality()));
+ this.matchSet = matchSet;
+ }
+
+ @Override
+ public boolean matches() {
+ return matchSet.get(approximation.docID());
+ }
+
+ @Override
+ public float matchCost() {
+ return 1f;
+ }
+
+ @Override
+ public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
+ for (int doc = approximation.docID(); doc < upTo; doc = approximation.nextDoc()) {
+ if (matchSet.get(doc)) {
+ bitSet.set(doc - offset);
+ }
+ }
+ }
+ }
+
+ private FixedBitSet modBitSet(int maxDoc, int step) {
+ FixedBitSet bits = new FixedBitSet(maxDoc);
+ for (int i = random().nextInt(step); i < maxDoc; i += step) {
+ bits.set(i);
+ }
+ return bits;
+ }
+
+ private FixedBitSet supersetOf(FixedBitSet matchSet, int maxDoc) {
+ FixedBitSet approx = matchSet.clone();
+ approx.or(modBitSet(maxDoc, TestUtil.nextInt(random(), 2, 8)));
+ return approx;
+ }
+
+ private static FixedBitSet collect(BulkScorer scorer, int maxDoc, Bits acceptDocs)
+ throws IOException {
+ FixedBitSet result = new FixedBitSet(maxDoc);
+ scorer.score(
+ new LeafCollector() {
+ @Override
+ public void setScorer(Scorable scorer) throws IOException {}
+
+ @Override
+ public void collect(int doc) throws IOException {
+ result.set(doc);
+ }
+ },
+ acceptDocs,
+ 0,
+ DocIdSetIterator.NO_MORE_DOCS);
+ return result;
+ }
+
+ public void testBulkTwoPhaseCollectsExactMatches() throws IOException {
+ int maxDoc = 100_000;
+ FixedBitSet match = modBitSet(maxDoc, TestUtil.nextInt(random(), 2, 50));
+ BulkScorer scorer =
+ new ConstantScoreBulkScorer(
+ 1f, ScoreMode.COMPLETE_NO_SCORES, new BulkTwoPhase(supersetOf(match, maxDoc), match));
+ // AssertingBulkScorer randomly splits the scored range into smaller ranges.
+ scorer = AssertingBulkScorer.wrap(random(), scorer, maxDoc);
+ assertEquals(match, collect(scorer, maxDoc, null));
+ }
+
+ public void testBulkTwoPhaseWithAcceptDocs() throws IOException {
+ int maxDoc = 100_000;
+ FixedBitSet match = modBitSet(maxDoc, 5);
+ FixedBitSet acceptDocs = modBitSet(maxDoc, 2);
+ BulkScorer scorer =
+ new ConstantScoreBulkScorer(
+ 1f, ScoreMode.COMPLETE_NO_SCORES, new BulkTwoPhase(supersetOf(match, maxDoc), match));
+ scorer = AssertingBulkScorer.wrap(random(), scorer, maxDoc);
+ FixedBitSet expected = match.clone();
+ expected.and(acceptDocs);
+ assertEquals(expected, collect(scorer, maxDoc, acceptDocs));
+ }
+
+ /** The competitive-iterator path must confirm matches() before collecting. */
+ public void testBulkTwoPhaseWithCompetitiveIterator() throws IOException {
+ int maxDoc = 100_000;
+ FixedBitSet match = modBitSet(maxDoc, 5);
+ FixedBitSet competitive = modBitSet(maxDoc, 7);
+ BulkScorer scorer =
+ new ConstantScoreBulkScorer(
+ 1f, ScoreMode.COMPLETE_NO_SCORES, new BulkTwoPhase(supersetOf(match, maxDoc), match));
+
+ FixedBitSet result = new FixedBitSet(maxDoc);
+ scorer.score(
+ new LeafCollector() {
+ private final DocIdSetIterator competitiveIt =
+ new BitSetIterator(competitive, competitive.approximateCardinality());
+
+ @Override
+ public void setScorer(Scorable scorer) throws IOException {}
+
+ @Override
+ public void collect(int doc) throws IOException {
+ result.set(doc);
+ }
+
+ @Override
+ public DocIdSetIterator competitiveIterator() {
+ return competitiveIt;
+ }
+ },
+ null,
+ 0,
+ DocIdSetIterator.NO_MORE_DOCS);
+
+ FixedBitSet expected = match.clone();
+ expected.and(competitive);
+ assertEquals(expected, result);
+ }
+
+ public void testSupplierRoutesSparseBulkTwoPhase() throws IOException {
+ int maxDoc = 100_000;
+ // Sparse approximation (cardinality well below maxDoc/32) so it is not routed to
+ // DenseConjunctionBulkScorer.
+ FixedBitSet match = modBitSet(maxDoc, 1000);
+
+ ConstantScoreScorerSupplier supplier =
+ ConstantScoreScorerSupplier.fromIterator(
+ TwoPhaseIterator.asDocIdSetIterator(new BulkTwoPhase(match.clone(), match)),
+ 1f,
+ ScoreMode.COMPLETE_NO_SCORES,
+ maxDoc);
+ BulkScorer bulkScorer = supplier.bulkScorer();
+ assertTrue(
+ "expected ConstantScoreBulkScorer but got " + bulkScorer.getClass().getName(),
+ bulkScorer instanceof ConstantScoreBulkScorer);
+ assertEquals(match, collect(bulkScorer, maxDoc, null));
+ }
+
+ public void testSupplierRoutesSparseNonBulkTwoPhase() throws IOException {
+ int maxDoc = 100_000;
+ FixedBitSet match = modBitSet(maxDoc, 1000);
+
+ // Even a two-phase iterator that doesn't override intoBitSet is routed to
+ // ConstantScoreBulkScorer:
+ // its default intoBitSet confirms matches one doc at a time, so this is no worse than leap-frog
+ // while still batching live-docs masking and collection.
+ ConstantScoreScorerSupplier supplier =
+ ConstantScoreScorerSupplier.fromIterator(
+ TwoPhaseIterator.asDocIdSetIterator(
+ new RandomTwoPhaseView(
+ random(), new BitSetIterator(match, match.approximateCardinality()))),
+ 1f,
+ ScoreMode.COMPLETE_NO_SCORES,
+ maxDoc);
+ BulkScorer bulkScorer = supplier.bulkScorer();
+ assertTrue(
+ "expected ConstantScoreBulkScorer but got " + bulkScorer.getClass().getName(),
+ bulkScorer instanceof ConstantScoreBulkScorer);
+ assertEquals(match, collect(bulkScorer, maxDoc, null));
+ }
+
+ /**
+ * The plain-iterator constructor must reject a {@link DocIdSetIterator} that wraps a {@link
+ * TwoPhaseIterator}: such an iterator has to go through the two-phase constructor so its
+ * (upTo-bounded) {@link TwoPhaseIterator#intoBitSet} confirms matches. If it slipped through as a
+ * plain iterator we would scan the bare approximation and lose the over-scan guarantee. {@link
+ * ConstantScoreScorerSupplier} already unwraps unconditionally, but this guard makes the
+ * invariant explicit and fail-fast for any future caller.
+ */
+ public void testRejectsWrappedTwoPhaseOnPlainConstructor() {
+ int maxDoc = 100_000;
+ FixedBitSet match = modBitSet(maxDoc, 5);
+ DocIdSetIterator wrapped =
+ TwoPhaseIterator.asDocIdSetIterator(new BulkTwoPhase(supersetOf(match, maxDoc), match));
+ IllegalArgumentException e =
+ expectThrows(
+ IllegalArgumentException.class,
+ () -> new ConstantScoreBulkScorer(1f, ScoreMode.COMPLETE_NO_SCORES, wrapped));
+ assertEquals("Iterator must not wrap a TwoPhaseIterator", e.getMessage());
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestDenseConjunctionBulkScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestDenseConjunctionBulkScorer.java
index ff220e2d0e2..c05cb11a543 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestDenseConjunctionBulkScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestDenseConjunctionBulkScorer.java
@@ -528,6 +528,68 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
DocIdSetIterator.NO_MORE_DOCS);
}
+ /**
+ * A two-phase clause that reports a real {@link TwoPhaseIterator#docIDRunEnd() run} — as {@link
+ * DocValuesRangeIterator}'s block iterator does for fully-matching YES blocks — must be collected
+ * via {@link LeafCollector#collectRange}, never materialized into a bit set or visited per-doc.
+ * This guards the collectRange fast path for doc-values ranges: the two-phase unification routes
+ * ranges through this scorer as two-phase clauses, so the run has to survive {@code
+ * DisiWrapper#docIDRunEnd()} delegating to the two-phase. The existing two-phase tests use {@code
+ * RandomTwoPhaseView}, whose approximation reports no run, so they exercise only the bit-set
+ * path.
+ */
+ public void testTwoPhaseRunCollectedAsRange() throws IOException {
+ int maxDoc = 100_000;
+ TwoPhaseIterator twoPhase =
+ new TwoPhaseIterator(DocIdSetIterator.range(10_000, 60_000)) {
+ @Override
+ public boolean matches() {
+ return true; // every doc in the run matches, like a YES block
+ }
+
+ @Override
+ public float matchCost() {
+ return 1f;
+ }
+
+ @Override
+ public int docIDRunEnd() throws IOException {
+ return approximation().docIDRunEnd();
+ }
+ };
+ BulkScorer scorer =
+ new DenseConjunctionBulkScorer(
+ Collections.emptyList(), Collections.singletonList(twoPhase), maxDoc, 0f);
+ int[] collectedViaRange = {0};
+ scorer.score(
+ new LeafCollector() {
+ @Override
+ public void setScorer(Scorable scorer) throws IOException {}
+
+ @Override
+ public void collect(int doc) throws IOException {
+ fail("a fully-matching run must be collected via collectRange, not per-doc");
+ }
+
+ @Override
+ public void collect(DocIdStream stream) throws IOException {
+ fail(
+ "a fully-matching run must be collected via collectRange, not as a bit-set stream");
+ }
+
+ @Override
+ public void collectRange(int min, int max) throws IOException {
+ assertTrue(min >= 10_000);
+ assertTrue(max <= 60_000);
+ collectedViaRange[0] += max - min;
+ }
+ },
+ null,
+ 0,
+ DocIdSetIterator.NO_MORE_DOCS);
+ assertEquals(50_000, collectedViaRange[0]);
+ }
+
private static class CountingLeafCollector implements LeafCollector {
int count;
@@ -958,11 +1020,10 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
BulkScorer scorer =
new DenseConjunctionBulkScorer(Collections.emptyList(), clauses, maxDoc, 0f);
- // Matches are collected as a single DocIdStream
+ // Matches arrive in order, as ranges and/or bit-set windows.
scorer.score(
new LeafCollector() {
- private boolean called = false;
private int expected = 30_000;
@Override
@@ -975,8 +1036,6 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
@Override
public void collect(DocIdStream stream) throws IOException {
- assertFalse(called);
- called = true;
stream.forEach(
doc -> {
assertEquals(expected++, doc);
@@ -985,7 +1044,6 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
@Override
public void finish() throws IOException {
- assertTrue(called);
assertEquals(60_001, expected);
}
},
@@ -1023,11 +1081,10 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
new RandomTwoPhaseView(random(), clause3)),
maxDoc,
0f);
- // Matches are collected as a single DocIdStream
+ // Matches arrive in order, as ranges and/or bit-set windows.
scorer.score(
new LeafCollector() {
- private boolean called = false;
private int expected = 30_000;
@Override
@@ -1040,8 +1097,6 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
@Override
public void collect(DocIdStream stream) throws IOException {
- assertFalse(called);
- called = true;
stream.forEach(
doc -> {
assertEquals(expected++, doc);
@@ -1050,7 +1105,6 @@ public class TestDenseConjunctionBulkScorer extends LuceneTestCase {
@Override
public void finish() throws IOException {
- assertTrue(called);
assertEquals(60_001, expected);
}
},
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java b/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java
index 6bc9d2e23db..858b185e29b 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java
@@ -34,8 +34,8 @@ import org.apache.lucene.store.Directory;
import org.apache.lucene.util.FixedBitSet;
/**
- * Tests correctness of {@link BatchDocValuesRangeIterator} and its {@code intoBitSet()} path,
- * including YES, YES_IF_PRESENT, and MAYBE block states.
+ * Tests the {@code intoBitSet()} bulk path of {@link DocValuesRangeIterator} over single-valued
+ * numeric doc values, including YES, YES_IF_PRESENT, and MAYBE block states.
*/
public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTests {
@@ -77,10 +77,10 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
}
/**
- * Verifies that SortedNumericDocValuesRangeQuery wires BatchDocValuesRangeIterator when the field
- * has a skip index and is single-valued.
+ * Verifies that SortedNumericDocValuesRangeQuery wires a DocValuesRangeIterator two-phase when
+ * the field has a skip index and is single-valued.
*/
- public void testBatchIteratorIsWired() throws Exception {
+ public void testTwoPhaseIteratorIsWired() throws Exception {
LeafReaderContext ctx = reader.leaves().get(0);
// Use a range that won't be rewritten to MatchAll or MatchNone
Query q = SortedNumericDocValuesField.newSlowRangeQuery("age", 20, 40);
@@ -92,11 +92,12 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
"ScorerSupplier must be a ConstantScoreScorerSupplier",
ss instanceof ConstantScoreScorerSupplier);
DocIdSetIterator iter = ((ConstantScoreScorerSupplier) ss).iterator(Long.MAX_VALUE);
+ TwoPhaseIterator twoPhase = TwoPhaseIterator.unwrap(iter);
assertTrue(
- "Range query on single-valued field with skip index must use BatchDocValuesRangeIterator"
+ "Range query on single-valued field with skip index must use a DocValuesRangeIterator,"
+ " but got: "
- + iter.getClass().getSimpleName(),
- iter instanceof BatchDocValuesRangeIterator);
+ + (twoPhase == null ? iter.getClass() : twoPhase.getClass()).getSimpleName(),
+ twoPhase instanceof DocValuesRangeIterator);
}
public void testSingleFieldRangeCorrectness() throws Exception {
@@ -171,17 +172,19 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues values = docValues(queryMin, queryMax);
DocValuesSkipper skipper = docValuesSkipper(queryMin, queryMax, true);
- BatchDocValuesRangeIterator iter =
- new BatchDocValuesRangeIterator(values, skipper, queryMin, queryMax);
- assertEquals(0, iter.nextDoc());
+ DocValuesRangeIterator iter =
+ DocValuesRangeIterator.forRange(values, skipper, queryMin, queryMax);
+ DocIdSetIterator approximation = iter.approximation();
+ assertEquals(0, approximation.nextDoc());
FixedBitSet bitSet = new FixedBitSet(2048);
iter.intoBitSet(128, bitSet, 0);
assertEquals("All docs in the YES block must match", 128, bitSet.cardinality());
assertTrue(
- "docID must skip the NO block that starts at the block boundary, but was " + iter.docID(),
- iter.docID() >= 512);
+ "approximation must skip the NO block that starts at the block boundary, but was "
+ + approximation.docID(),
+ approximation.docID() >= 512);
// Continue filling the bitset in a second window, as DenseConjunctionBulkScorer does.
iter.intoBitSet(2048, bitSet, 0);
@@ -232,8 +235,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues dv = ctx.reader().getNumericDocValues("age");
assertNotNull("Field must have a skip index", skipper);
- BatchDocValuesRangeIterator iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
- iter.nextDoc();
+ DocValuesRangeIterator iter = DocValuesRangeIterator.forRange(dv, skipper, 20, 40);
+ iter.approximation().nextDoc();
FixedBitSet actual = new FixedBitSet(windowSize);
iter.intoBitSet(Math.min(maxDoc, windowSize), actual, 0);
@@ -270,8 +273,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues dv = ctx.reader().getNumericDocValues("age");
assertNotNull(skipper);
- BatchDocValuesRangeIterator iter = new BatchDocValuesRangeIterator(dv, skipper, 0, 99);
- iter.nextDoc();
+ DocValuesRangeIterator iter = DocValuesRangeIterator.forRange(dv, skipper, 0, 99);
+ iter.approximation().nextDoc();
FixedBitSet bitSet = new FixedBitSet(maxDoc);
iter.intoBitSet(maxDoc, bitSet, 0);
@@ -302,8 +305,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues dv = ctx.reader().getNumericDocValues("age");
assertNotNull(skipper);
- BatchDocValuesRangeIterator iter = new BatchDocValuesRangeIterator(dv, skipper, 200, 300);
- iter.nextDoc();
+ DocValuesRangeIterator iter = DocValuesRangeIterator.forRange(dv, skipper, 200, 300);
+ iter.approximation().nextDoc();
FixedBitSet bitSet = new FixedBitSet(maxDoc);
iter.intoBitSet(maxDoc, bitSet, 0);
@@ -345,8 +348,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues dv = ctx.reader().getNumericDocValues("sparse");
if (skipper == null) return;
- BatchDocValuesRangeIterator iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
- iter.nextDoc();
+ DocValuesRangeIterator iter = DocValuesRangeIterator.forRange(dv, skipper, 20, 40);
+ iter.approximation().nextDoc();
FixedBitSet bitSet = new FixedBitSet(numDocs);
iter.intoBitSet(numDocs, bitSet, 0);
@@ -394,7 +397,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
NumericDocValues dv = ctx.reader().getNumericDocValues("age");
assertNotNull(skipper);
- BatchDocValuesRangeIterator iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
+ DocIdSetIterator iter =
+ TwoPhaseIterator.asDocIdSetIterator(DocValuesRangeIterator.forRange(dv, skipper, 20, 40));
List<Integer> actual = new ArrayList<>();
for (int d = iter.nextDoc(); d != DocIdSetIterator.NO_MORE_DOCS; d = iter.nextDoc()) {
actual.add(d);
@@ -406,7 +410,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// verifying each advance lands on the correct next matching doc.
skipper = ctx.reader().getDocValuesSkipper("age");
dv = ctx.reader().getNumericDocValues("age");
- iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
+ iter =
+ TwoPhaseIterator.asDocIdSetIterator(DocValuesRangeIterator.forRange(dv, skipper, 20, 40));
Random rng = random();
int idx = 0;
while (idx < expected.size()) {
@@ -422,7 +427,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// advance returns the next matching doc after the target.
skipper = ctx.reader().getDocValuesSkipper("age");
dv = ctx.reader().getNumericDocValues("age");
- iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
+ iter =
+ TwoPhaseIterator.asDocIdSetIterator(DocValuesRangeIterator.forRange(dv, skipper, 20, 40));
int prevDoc = -1;
for (int i = 0; i < expected.size() && i < 20; i++) {
int matchDoc = expected.get(i);
@@ -441,7 +447,8 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// Test 4: advance() past all docs returns NO_MORE_DOCS
skipper = ctx.reader().getDocValuesSkipper("age");
dv = ctx.reader().getNumericDocValues("age");
- iter = new BatchDocValuesRangeIterator(dv, skipper, 20, 40);
+ iter =
+ TwoPhaseIterator.asDocIdSetIterator(DocValuesRangeIterator.forRange(dv, skipper, 20, 40));
int doc = iter.advance(ctx.reader().maxDoc());
assertEquals(DocIdSetIterator.NO_MORE_DOCS, doc);
}
@@ -460,9 +467,9 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
}
/**
- * Tests BatchDocValuesRangeIterator using the fake NumericDocValues and DocValuesSkipper from
- * BaseDocValuesSkipperTests, which exercises all block types (YES, NO, MAYBE, YES_IF_PRESENT)
- * across both dense and sparse regions.
+ * Tests the DocValuesRangeIterator iteration and intoBitSet paths using the fake NumericDocValues
+ * and DocValuesSkipper from BaseDocValuesSkipperTests, which exercises all block types (YES, NO,
+ * MAYBE, YES_IF_PRESENT) across both dense and sparse regions.
*/
public void testAllBlockTypesWithFakeSkipper() throws Exception {
long queryMin = 10;
@@ -484,8 +491,9 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// Test nextDoc() through all block types
NumericDocValues values = docValues(queryMin, queryMax);
DocValuesSkipper skipper = docValuesSkipper(queryMin, queryMax, true);
- BatchDocValuesRangeIterator iter =
- new BatchDocValuesRangeIterator(values, skipper, queryMin, queryMax);
+ DocIdSetIterator iter =
+ TwoPhaseIterator.asDocIdSetIterator(
+ DocValuesRangeIterator.forRange(values, skipper, queryMin, queryMax));
List<Integer> actual = new ArrayList<>();
for (int d = iter.nextDoc(); d != DocIdSetIterator.NO_MORE_DOCS; d = iter.nextDoc()) {
actual.add(d);
@@ -500,7 +508,9 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// - advance past end
values = docValues(queryMin, queryMax);
skipper = docValuesSkipper(queryMin, queryMax, true);
- iter = new BatchDocValuesRangeIterator(values, skipper, queryMin, queryMax);
+ iter =
+ TwoPhaseIterator.asDocIdSetIterator(
+ DocValuesRangeIterator.forRange(values, skipper, queryMin, queryMax));
// advance into YES block — should land exactly on target
assertEquals(50, iter.advance(50));
@@ -520,11 +530,12 @@ public class TestSkipBlockRangeIteratorIntoBitSet extends BaseDocValuesSkipperTe
// Test intoBitSet() across all block types
values = docValues(queryMin, queryMax);
skipper = docValuesSkipper(queryMin, queryMax, true);
- iter = new BatchDocValuesRangeIterator(values, skipper, queryMin, queryMax);
- int firstDoc = iter.nextDoc();
+ DocValuesRangeIterator rangeIter =
+ DocValuesRangeIterator.forRange(values, skipper, queryMin, queryMax);
+ int firstDoc = rangeIter.approximation().nextDoc();
FixedBitSet bitSet = new FixedBitSet(2048);
- iter.intoBitSet(2048, bitSet, 0);
+ rangeIter.intoBitSet(2048, bitSet, 0);
// All expected docs after firstDoc should be set
for (int expectedDoc : expected) {