You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ro...@apache.org on 2026/02/02 09:11:12 UTC
(lucene) branch branch_10x updated: Use a more coarse-grained competitive iterator for skipper-based numeric sorts (#15632)
This is an automated email from the ASF dual-hosted git repository.
romseygeek 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 0bf85ad186f Use a more coarse-grained competitive iterator for skipper-based numeric sorts (#15632)
0bf85ad186f is described below
commit 0bf85ad186f72a84c5a085729c94c67991bfaaf8
Author: Alan Woodward <ro...@apache.org>
AuthorDate: Mon Feb 2 09:05:09 2026 +0000
Use a more coarse-grained competitive iterator for skipper-based numeric sorts (#15632)
Numeric sorts against a field with DocValuesSkippers enabled currently use
DocValuesRangeIterator to implement competitive iterators. This has a couple
of disadvantages:
- DVRI cannot efficiently implement docIDRunEnd() or intoBitSet(), meaning that
bulk conjunction filtering may end up falling into slower code paths
- For field value distributions that are essentially random, DVRI falls back to
doc-by-doc value checking, meaning that no skipping happens at all, but adding
overhead.
This commit adds a new SkipBlockRangeIterator that only skips whole blocks
where no document will be competitive, avoiding any individual doc-by-doc value
checks. The docIDRunEnd() and intoBitSet() implementations are very fast and
mean that bulk conjunction filtering will be efficient. The overheads as a whole
are very low, so randomly distributed values are much less adversarial, while
queries against indexes where the document order is roughly correlated with the
query sort get significant boosts.
---
lucene/CHANGES.txt | 3 +
.../codecs/lucene90/Lucene90DocValuesProducer.java | 3 +-
.../lucene/search/SkipBlockRangeIterator.java | 98 +++++++++++
.../search/comparators/NumericComparator.java | 28 +--
.../lucene/search/BaseDocValuesSkipperTests.java | 196 +++++++++++++++++++++
.../lucene/search/TestDocValuesRangeIterator.java | 170 +-----------------
.../lucene/search/TestSkipBlockRangeIterator.java | 53 ++++++
.../tests/search/AssertingLeafCollector.java | 3 +-
8 files changed, 360 insertions(+), 194 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index e9947ffb165..78599983ff7 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -171,6 +171,9 @@ Optimizations
* GITHUB#15585: Added shared prefetch counter to maintain count across clones and slices of MemorySegmentIndexInput. (Shubham Sharma)
+* GITHUB#15632: Use a coarser-grained competitive iterator with lower construction costs for
+ numeric sorts against fields with DocValuesSkippers. (Alan Woodward)
+
Bug Fixes
---------------------
* GITHUB#14161: PointInSetQuery's constructor now throws IllegalArgumentException
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
index 9a48b788e8e..1b40c5f58d9 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
@@ -1899,7 +1899,8 @@ final class Lucene90DocValuesProducer extends DocValuesProducer {
}
} else {
// find next interval
- assert target > maxDocID[0] : "target must be bigger that current interval";
+ assert target > maxDocID[0]
+ : "target " + target + " must be bigger that current interval " + maxDocID[0];
while (true) {
levels = input.readByte();
assert levels <= SKIP_INDEX_MAX_LEVEL && levels > 0
diff --git a/lucene/core/src/java/org/apache/lucene/search/SkipBlockRangeIterator.java b/lucene/core/src/java/org/apache/lucene/search/SkipBlockRangeIterator.java
new file mode 100644
index 00000000000..fa58efc3690
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/search/SkipBlockRangeIterator.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util.FixedBitSet;
+
+/**
+ * A DocIdSetIterator that returns all documents within DocValuesSkipper blocks that have minimum
+ * and maximum values that fall within a specified range.
+ */
+public class SkipBlockRangeIterator extends AbstractDocIdSetIterator {
+
+ private final DocValuesSkipper skipper;
+ private final long minValue;
+ private final long maxValue;
+
+ /**
+ * Creates a new SkipBlockRangeIterator
+ *
+ * @param skipper the DocValuesSkipper to use to check block bounds
+ * @param minValue only return documents that lie within a block with a maximum value greater than
+ * this
+ * @param maxValue only return documents that lie within a block with a minimum value less than
+ * this
+ */
+ public SkipBlockRangeIterator(DocValuesSkipper skipper, long minValue, long maxValue) {
+ this.skipper = skipper;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ }
+
+ @Override
+ public int nextDoc() throws IOException {
+ return advance(doc + 1);
+ }
+
+ @Override
+ public int advance(int target) throws IOException {
+ if (target <= skipper.maxDocID(0)) {
+ // within current block
+ if (doc > -1) {
+ // already positioned, so we've checked bounds and know that we're in a matching block
+ return doc = target;
+ }
+ } else {
+ // Advance to target
+ skipper.advance(target);
+ }
+
+ // Find the next matching block (could be the current block)
+ skipper.advance(minValue, maxValue);
+ return doc = Math.max(target, skipper.minDocID(0));
+ }
+
+ @Override
+ public long cost() {
+ return DocIdSetIterator.NO_MORE_DOCS;
+ }
+
+ @Override
+ public int docIDRunEnd() throws IOException {
+ int maxDoc = skipper.maxDocID(0);
+ int nextLevel = 1;
+ while (nextLevel < skipper.numLevels()
+ && skipper.minValue(nextLevel) < maxValue
+ && skipper.maxValue(nextLevel) > minValue) {
+ maxDoc = skipper.maxDocID(nextLevel);
+ nextLevel++;
+ }
+ return maxDoc + 1;
+ }
+
+ @Override
+ public void intoBitSet(int upTo, FixedBitSet bitSet, int offset) throws IOException {
+ while (doc < upTo) {
+ int end = Math.min(upTo, docIDRunEnd());
+ bitSet.set(doc - offset, end - offset);
+ advance(end);
+ }
+ }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/search/comparators/NumericComparator.java b/lucene/core/src/java/org/apache/lucene/search/comparators/NumericComparator.java
index e39ecbbd666..1aa704b36c5 100644
--- a/lucene/core/src/java/org/apache/lucene/search/comparators/NumericComparator.java
+++ b/lucene/core/src/java/org/apache/lucene/search/comparators/NumericComparator.java
@@ -26,13 +26,12 @@ import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.search.DocIdSetIterator;
-import org.apache.lucene.search.DocValuesRangeIterator;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.LeafFieldComparator;
import org.apache.lucene.search.Pruning;
import org.apache.lucene.search.Scorable;
import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.TwoPhaseIterator;
+import org.apache.lucene.search.SkipBlockRangeIterator;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.IntsRef;
@@ -502,27 +501,11 @@ public abstract class NumericComparator<T extends Number> extends FieldComparato
private class DVSkipperCompetitiveDISIBuilder extends CompetitiveDISIBuilder {
private final DocValuesSkipper skipper;
- private final TwoPhaseIterator innerTwoPhase;
- DVSkipperCompetitiveDISIBuilder(DocValuesSkipper skipper, NumericLeafComparator leafComparator)
- throws IOException {
+ DVSkipperCompetitiveDISIBuilder(
+ DocValuesSkipper skipper, NumericLeafComparator leafComparator) {
super(leafComparator);
this.skipper = skipper;
- NumericDocValues docValues =
- leafComparator.getNumericDocValues(leafComparator.context, field);
- innerTwoPhase =
- new TwoPhaseIterator(docValues) {
- @Override
- public boolean matches() throws IOException {
- final long value = docValues.longValue();
- return value >= minValueAsLong && value <= maxValueAsLong;
- }
-
- @Override
- public float matchCost() {
- return 2; // 2 comparisons
- }
- };
postInitializeCompetitiveIterator();
}
@@ -553,9 +536,8 @@ public abstract class NumericComparator<T extends Number> extends FieldComparato
@Override
void doUpdateCompetitiveIterator() {
- TwoPhaseIterator twoPhaseIterator =
- new DocValuesRangeIterator(innerTwoPhase, skipper, minValueAsLong, maxValueAsLong, false);
- competitiveIterator.update(TwoPhaseIterator.asDocIdSetIterator(twoPhaseIterator));
+ competitiveIterator.update(
+ new SkipBlockRangeIterator(skipper, minValueAsLong, maxValueAsLong));
}
}
}
diff --git a/lucene/core/src/test/org/apache/lucene/search/BaseDocValuesSkipperTests.java b/lucene/core/src/test/org/apache/lucene/search/BaseDocValuesSkipperTests.java
new file mode 100644
index 00000000000..6e1f5430e0e
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/BaseDocValuesSkipperTests.java
@@ -0,0 +1,196 @@
+/*
+ * 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.tests.util.LuceneTestCase;
+
+public abstract class BaseDocValuesSkipperTests extends LuceneTestCase {
+
+ /**
+ * Fake numeric doc values so that: - docs 0-256 all match - docs in 256-512 are all greater than
+ * queryMax - docs in 512-768 are all less than queryMin - docs in 768-1024 have some docs that
+ * match the range, others not - docs in 1024-2048 follow a similar pattern as docs in 0-1024
+ * except that not all docs have a - value
+ */
+ protected static NumericDocValues docValues(long queryMin, long queryMax) {
+ return new NumericDocValues() {
+
+ int doc = -1;
+
+ @Override
+ public boolean advanceExact(int target) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int docID() {
+ return doc;
+ }
+
+ @Override
+ public int nextDoc() throws IOException {
+ return advance(doc + 1);
+ }
+
+ @Override
+ public int advance(int target) throws IOException {
+ if (target < 1024) {
+ // dense up to 1024
+ return doc = target;
+ } else if (doc < 2047) {
+ // 50% docs have a value up to 2048
+ return doc = target + (target & 1);
+ } else {
+ return doc = DocIdSetIterator.NO_MORE_DOCS;
+ }
+ }
+
+ @Override
+ public long longValue() throws IOException {
+ int d = doc % 1024;
+ if (d < 128) {
+ return (queryMin + queryMax) >> 1;
+ } else if (d < 256) {
+ return queryMax + 1;
+ } else if (d < 512) {
+ return queryMin - 1;
+ } else {
+ return switch ((d / 2) % 3) {
+ case 0 -> queryMin - 1;
+ case 1 -> queryMax + 1;
+ case 2 -> (queryMin + queryMax) >> 1;
+ default -> throw new AssertionError();
+ };
+ }
+ }
+
+ @Override
+ public long cost() {
+ return 42;
+ }
+ };
+ }
+
+ /**
+ * Fake skipper over a NumericDocValues field built by an equivalent call to {@link
+ * #docValues(long, long)}
+ */
+ protected static DocValuesSkipper docValuesSkipper(
+ long queryMin, long queryMax, boolean doLevels) {
+ return new DocValuesSkipper() {
+
+ int doc = -1;
+
+ @Override
+ public void advance(int target) throws IOException {
+ doc = target;
+ }
+
+ @Override
+ public int numLevels() {
+ return doLevels ? 3 : 1;
+ }
+
+ @Override
+ public int minDocID(int level) {
+ int rangeLog = 9 - numLevels() + level;
+
+ // the level is the log2 of the interval
+ if (doc < 0) {
+ return -1;
+ } else if (doc >= 2048) {
+ return DocIdSetIterator.NO_MORE_DOCS;
+ } else {
+ int mask = (1 << rangeLog) - 1;
+ // prior multiple of 2^level
+ return doc & ~mask;
+ }
+ }
+
+ @Override
+ public int maxDocID(int level) {
+ int rangeLog = 9 - numLevels() + level;
+
+ int minDocID = minDocID(level);
+ return switch (minDocID) {
+ case -1 -> -1;
+ case DocIdSetIterator.NO_MORE_DOCS -> DocIdSetIterator.NO_MORE_DOCS;
+ default -> minDocID + (1 << rangeLog) - 1;
+ };
+ }
+
+ @Override
+ @SuppressWarnings("DuplicateBranches")
+ public long minValue(int level) {
+ int d = doc % 1024;
+ if (d < 128) {
+ return queryMin;
+ } else if (d < 256) {
+ return queryMax + 1;
+ } else if (d < 768) {
+ return queryMin - 1;
+ } else {
+ return queryMin - 1;
+ }
+ }
+
+ @Override
+ public long maxValue(int level) {
+ int d = doc % 1024;
+ if (d < 128) {
+ return queryMax;
+ } else if (d < 256) {
+ return queryMax + 1;
+ } else if (d < 768) {
+ return queryMin - 1;
+ } else {
+ return queryMax + 1;
+ }
+ }
+
+ @Override
+ public int docCount(int level) {
+ int rangeLog = 9 - numLevels() + level;
+
+ if (doc < 1024) {
+ return 1 << rangeLog;
+ } else {
+ // half docs have a value
+ return 1 << rangeLog >> 1;
+ }
+ }
+
+ @Override
+ public long minValue() {
+ return Long.MIN_VALUE;
+ }
+
+ @Override
+ public long maxValue() {
+ return Long.MAX_VALUE;
+ }
+
+ @Override
+ public int docCount() {
+ return 1024 + 1024 / 2;
+ }
+ };
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIterator.java b/lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIterator.java
index 2942c224914..838eec1377b 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIterator.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIterator.java
@@ -20,9 +20,8 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.lucene.index.DocValuesSkipper;
import org.apache.lucene.index.NumericDocValues;
-import org.apache.lucene.tests.util.LuceneTestCase;
-public class TestDocValuesRangeIterator extends LuceneTestCase {
+public class TestDocValuesRangeIterator extends BaseDocValuesSkipperTests {
public void testSingleLevel() throws IOException {
doTestBasics(false);
@@ -154,72 +153,6 @@ public class TestDocValuesRangeIterator extends LuceneTestCase {
assertEquals(DocIdSetIterator.NO_MORE_DOCS, rangeApproximationWithGaps.advance(2048));
}
- // Fake numeric doc values so that:
- // docs 0-256 all match
- // docs in 256-512 are all greater than queryMax
- // docs in 512-768 are all less than queryMin
- // docs in 768-1024 have some docs that match the range, others not
- // docs in 1024-2048 follow a similar pattern as docs in 0-1024 except that not all docs have a
- // value
- private static NumericDocValues docValues(long queryMin, long queryMax) {
- return new NumericDocValues() {
-
- int doc = -1;
-
- @Override
- public boolean advanceExact(int target) throws IOException {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public int docID() {
- return doc;
- }
-
- @Override
- public int nextDoc() throws IOException {
- return advance(doc + 1);
- }
-
- @Override
- public int advance(int target) throws IOException {
- if (target < 1024) {
- // dense up to 1024
- return doc = target;
- } else if (doc < 2047) {
- // 50% docs have a value up to 2048
- return doc = target + (target & 1);
- } else {
- return doc = DocIdSetIterator.NO_MORE_DOCS;
- }
- }
-
- @Override
- public long longValue() throws IOException {
- int d = doc % 1024;
- if (d < 128) {
- return (queryMin + queryMax) >> 1;
- } else if (d < 256) {
- return queryMax + 1;
- } else if (d < 512) {
- return queryMin - 1;
- } else {
- return switch ((d / 2) % 3) {
- case 0 -> queryMin - 1;
- case 1 -> queryMax + 1;
- case 2 -> (queryMin + queryMax) >> 1;
- default -> throw new AssertionError();
- };
- }
- }
-
- @Override
- public long cost() {
- return 42;
- }
- };
- }
-
private static TwoPhaseIterator twoPhaseIterator(
NumericDocValues values, long queryMin, long queryMax, AtomicBoolean twoPhaseCalled) {
return new TwoPhaseIterator(values) {
@@ -237,105 +170,4 @@ public class TestDocValuesRangeIterator extends LuceneTestCase {
}
};
}
-
- private static DocValuesSkipper docValuesSkipper(long queryMin, long queryMax, boolean doLevels) {
- return new DocValuesSkipper() {
-
- int doc = -1;
-
- @Override
- public void advance(int target) throws IOException {
- doc = target;
- }
-
- @Override
- public int numLevels() {
- return doLevels ? 3 : 1;
- }
-
- @Override
- public int minDocID(int level) {
- int rangeLog = 9 - numLevels() + level;
-
- // the level is the log2 of the interval
- if (doc < 0) {
- return -1;
- } else if (doc >= 2048) {
- return DocIdSetIterator.NO_MORE_DOCS;
- } else {
- int mask = (1 << rangeLog) - 1;
- // prior multiple of 2^level
- return doc & ~mask;
- }
- }
-
- @Override
- public int maxDocID(int level) {
- int rangeLog = 9 - numLevels() + level;
-
- int minDocID = minDocID(level);
- return switch (minDocID) {
- case -1 -> -1;
- case DocIdSetIterator.NO_MORE_DOCS -> DocIdSetIterator.NO_MORE_DOCS;
- default -> minDocID + (1 << rangeLog) - 1;
- };
- }
-
- @Override
- @SuppressWarnings("DuplicateBranches")
- public long minValue(int level) {
- int d = doc % 1024;
- if (d < 128) {
- return queryMin;
- } else if (d < 256) {
- return queryMax + 1;
- } else if (d < 768) {
- return queryMin - 1;
- } else {
- return queryMin - 1;
- }
- }
-
- @Override
- public long maxValue(int level) {
- int d = doc % 1024;
- if (d < 128) {
- return queryMax;
- } else if (d < 256) {
- return queryMax + 1;
- } else if (d < 768) {
- return queryMin - 1;
- } else {
- return queryMax + 1;
- }
- }
-
- @Override
- public int docCount(int level) {
- int rangeLog = 9 - numLevels() + level;
-
- if (doc < 1024) {
- return 1 << rangeLog;
- } else {
- // half docs have a value
- return 1 << rangeLog >> 1;
- }
- }
-
- @Override
- public long minValue() {
- return Long.MIN_VALUE;
- }
-
- @Override
- public long maxValue() {
- return Long.MAX_VALUE;
- }
-
- @Override
- public int docCount() {
- return 1024 + 1024 / 2;
- }
- };
- }
}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIterator.java b/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIterator.java
new file mode 100644
index 00000000000..3f0423c8802
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIterator.java
@@ -0,0 +1,53 @@
+/*
+ * 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 org.apache.lucene.index.DocValuesSkipper;
+import org.apache.lucene.util.FixedBitSet;
+
+public class TestSkipBlockRangeIterator extends BaseDocValuesSkipperTests {
+
+ public void testSkipBlockRangeIterator() throws Exception {
+
+ DocValuesSkipper skipper = docValuesSkipper(10, 20, true);
+ SkipBlockRangeIterator it = new SkipBlockRangeIterator(skipper, 10, 20);
+
+ assertEquals(0, it.nextDoc());
+ assertEquals(256, it.docIDRunEnd());
+ assertEquals(100, it.advance(100));
+ assertEquals(768, it.advance(300));
+ assertEquals(1024, it.docIDRunEnd());
+ assertEquals(1100, it.advance(1100));
+ assertEquals(1280, it.docIDRunEnd());
+ assertEquals(1792, it.advance(1500));
+ assertEquals(2048, it.docIDRunEnd());
+ assertEquals(DocIdSetIterator.NO_MORE_DOCS, it.advance(2050));
+ }
+
+ public void testIntoBitSet() throws Exception {
+ DocValuesSkipper skipper = docValuesSkipper(10, 20, true);
+ SkipBlockRangeIterator it = new SkipBlockRangeIterator(skipper, 10, 20);
+ assertEquals(768, it.advance(300));
+ FixedBitSet bitSet = new FixedBitSet(2048);
+ it.intoBitSet(1500, bitSet, 768);
+
+ FixedBitSet expected = new FixedBitSet(2048);
+ expected.set(0, 512);
+
+ assertEquals(expected, bitSet);
+ }
+}
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingLeafCollector.java b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingLeafCollector.java
index d481daa858a..288cef41544 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingLeafCollector.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingLeafCollector.java
@@ -122,7 +122,8 @@ class AssertingLeafCollector extends FilterLeafCollector {
assert docID() != -1;
assert docID() != NO_MORE_DOCS;
int nextNonMatchingDocID = in.docIDRunEnd();
- assert nextNonMatchingDocID > docID();
+ assert nextNonMatchingDocID > docID()
+ : "docIDRunEnd=" + nextNonMatchingDocID + ", docID=" + docID();
return nextNonMatchingDocID;
}
};