ordinalRangeIntoBitSet (new in 10.6.0) fills a bit set with the documents in [fromDoc, toDoc) whose ordinal is in a range. Its default reads the ordinal of the document the iterator is on without checking that the document has one.
- A
SortedDocValuesiterates over the documents that have a value;nextDoc()andadvance()only stop on such documents.ordValue()returns the current document's ordinal: its value's rank in the field's sorted dictionary ("a" = 0, "b" = 1). advanceExact(d)moves to exactlydand returns whetherdhas a value. Afterfalse,docID()isdbut there is nothing to read: callingordValue()then is illegal.ordinalRangeIntoBitSet(fromDoc, toDoc, minOrd, maxOrd, bits, offset)is a bulk call. The default codec overrides it for some encodings, such as a field every document has; a sparse field with two or more values runs the default inSortedDocValues.
- Index three documents with the default codec into a SORTED doc-values field
f: doc 0 = "a", doc 1 has no value, doc 2 = "b". - On the segment's
SortedDocValues,advanceExact(1)returns false: doc 1 has no value, and the iterator now sits on doc 1. On the same iterator, callordinalRangeIntoBitSet(1, 3, ord("a"), ord("a"), bits, 0): which documents in [1, 3) have "a"? - You expect none: doc 1 has no value and doc 2 has "b". The per-document
advanceExact/ordValueloop that the javadoc describes gives exactly that, and so does the same call on a fresh iterator. - Instead bit 1 is set, with no error. The default starts at the current document and calls
ordValue()there, which the API calls illegal after a falseadvanceExact; the codec's sparse iterator still points at doc 0's value, "a". - If no earlier document has a value (doc 0 empty,
advanceExact(0)), the same call throws instead:RuntimeException(EOFException)on most directories,ArrayIndexOutOfBoundsExceptionon a plainByteBuffersDirectory. In this call pair only the first document of the call can be wrong, and only as an extra match.
Only code that calls SortedDocValues#ordinalRangeIntoBitSet itself, on the same iterator, right after advanceExact(d) returned false, on a SORTED field that some documents lack (default codec). Lucene's own queries never do this. Each call gets at most one extra document. Released in 10.6.0 (2026-10-29); also on main.
Mechanism, terms and numbers953 words
SortedDocValues#ordinalRangeIntoBitSet(fromDoc, toDoc, minOrd, maxOrd, bitSet, offset), added in 10.6.0 by #16180, fills bitSet with "the doc IDs in [fromDoc, toDoc) whose ordinals are in [minOrd, maxOrd]", and its javadoc says the default "falls back to per-doc evaluation via advanceExact and ordValue". The default instead starts at the current document whenever docID() >= fromDoc and calls ordValue() there (SortedDocValues.java:117-118 on main 645d4fe14a20). After advanceExact(d) returned false the iterator sits on d, which has no value, and ordValue() is documented as illegal in that state. The default codec's sparse SORTED iterator (two or more distinct values, some documents without one) has no override, and its ordValue() reads with the stale index that advanceExact left: in the tested layouts doc d is judged by the previous valued document's ordinal (in a DENSE block the next one's, or the padding after the last ordinal when none follows), or the call throws when no earlier document has a value (RuntimeException(EOFException), or ArrayIndexOutOfBoundsException on a plain ByteBuffersDirectory). Wrappers that inherit the default (FilterSortedDocValues, MultiDocValues) are affected even with one distinct value. With docs {0: "a", 1: none, 2: "b"}, advanceExact(1) then ordinalRangeIntoBitSet(1, 3, ord("a"), ord("a"), bits, 0) sets bit 1 instead of nothing, with assertions on and off. In this call pair only the start document can be wrong. No Lucene query, sort, facet or merge path makes this call order, and the method is new in 10.6.0 (released 2026-10-29), so the priority is low. Skipping a start document without a value (8 lines) fixes it; the test and 14 doc-values suites pass with the fix.
- The cursor after a false
advanceExact. "After this method returns,docID()returnstarget" (DocValuesIterator.java:24-28); the note below names onlyintoBitSetanddocIDRunEnd()as illegal after a false return (:30-31). - The default (
SortedDocValues.java:117-122):
for (int doc = docID() >= fromDoc ? docID() : advance(fromDoc); doc < toDoc; doc = nextDoc()) {
long ord = ordValue();
if (ord >= minOrd && ord <= maxOrd) {
bitSet.set(doc - offset);
}
}With docID() == fromDoc it starts on that document and calls ordValue() (:118), which "is illegal to call ... after advanceExact(int) returned false" (:38-39).
- Which iterator runs it. For ordinals stored as one packed block with more than one value (
Lucene90DocValuesProducer.java:1473-1477), the dense branch overrides the method with random access (:1548-1555) and is correct; the sparse branch (:1557) has no override, and itsordValue()is(int) values.get(disi.index())(:1569-1571). The generic iterator (one distinct value, or ords not in one block) delegates to the numericrangeIntoBitSet(:1654-1661) and answers correctly when called directly.FilterSortedDocValues(so alsoExitableDirectoryReader's wrapper),MultiDocValues.MultiSortedDocValuesandAssertingSortedDocValuesdo not override the method, so wrapped iterators run the faulty default, over the generic iterator too: thereordValue()reachesords.longValue(), which for one distinct value is the constantentry.minValue(:1090-1096), so a wrapper marks the value-less document whenever that ordinal is in range (AssertingSortedDocValueswith-eafailsassert existsinstead). - What the stale index holds (the tested layouts). After a miss in a SPARSE block,
IndexedDISIsteps the index back to the last valued document before the target, or leaves -1 if there is none (IndexedDISI.java:607-616); in a DENSE block the index is the rank of the next valued document (:712-713), which is one past the last stored ordinal when no valued document follows. Hence doc 0's ordinal in the example, the next document's ordinal in a DENSE block, the padding after the last ordinal (read as 0) when nothing follows, andvalues.get(-1), an exception, when nothing precedes. These are the executed layouts, not everyIndexedDISIstate: a target inside a 65,536-document block with no values (advanceBlock,:495-516), for example, was not run. - Only the start document. After it the loop moves with
nextDoc(), which lands only on documents with a value, so later bits are right and no later valued document is dropped. Separate, pre-existing behaviour, not this bug: whendocID() > fromDocthe default starts atdocID()and never evaluates[fromDoc, docID()), valued documents included; the fixes below keep that (a forward-only iterator cannot go back).
| layout (default codec) | call | expected | actual |
|---|---|---|---|
{0: a, 1: none, 2: b} | advanceExact(1) false, then range [ord(a), ord(a)] over [1, 3) | {} | {1} (doc 0's ordinal) |
{0: none, 1: a, 2: b} | advanceExact(0) false, then range [ord(a), ord(a)] over [0, 3) | {1} | RuntimeException(EOFException) (on MMapDirectory "read past EOF (pos=2305843009213693951)" = (-1L) >>> 3; the same on NIOFSDirectory and MockDirectoryWrapper); ArrayIndexOutOfBoundsException: Index 536870912 out of bounds for length 1 on a plain ByteBuffersDirectory |
| DENSE block: 5,000 docs, docs 0 and 1 without a value, doc 2 = b | advanceExact(0) false, then range [ord(b), ord(b)] over [0, 4) | {2} | {0, 2} (the next valued doc's ordinal) |
DENSE block, nothing after d: 5,000 docs, docs 0..4499 = a, b alternating, 4500..4999 without a value | advanceExact(4600) false, then range [ord(a), ord(a)] over [4600, 5000) | {} | {4600}, no exception (one past the last stored ordinal; the padding reads as 0 = a) |
| 40 random sparse layouts (seed DEADBEEF) | for every value-less d: advanceExact(d), then a range from d | 390 checks | 176 wrong bitsets (always one extra bit at d), 22 exceptions; the unpositioned control 390 of 390 right (seed FC41906: 178 wrong and 18 exceptions in 436) |
wrapped field with one distinct value, {0: a, 1: none} (FilterSortedDocValues and ExitableDirectoryReader views; the finder's harness) | advanceExact(1) false, then range [ord(a), ord(a)] over [1, 2) | {} | {1} |
Exposure and likelihood280 words
- No in-tree caller reaches it. The only caller,
DocValuesRangeIterator.BulkSortedRangeIterator#intoMaybeBlock, passesfrom = Math.max(blockStart, docID())(DocValuesRangeIterator.java:513-519) on an iterator it moves withadvance()only (advanceDisi,:296-301), sodocID()always has a value there. An executed probe (8,400 query executions per run over 14 random skip-indexed indexes, raw, asserting and exitable readers, assertions on and off) found 0 mismatches while the default ran about 13,000 times per reader; the direct call pair on the same indexes misbehaved in 250 of 327 trials. Open PR #16897 adds a second caller, which also positions withadvance(). - Who can hit it. Application code that probes a document with
advanceExactand then bulk-evaluates from it; the test of open PR #16835 makes the analogous call pair on the numeric sibling. Default codec and configuration; any index size (three documents suffice). - Effect. Silent wrong results: one extra document per call, judged by a neighbouring document's ordinal from the same field and segment, or an exception (
RuntimeException(EOFException), orArrayIndexOutOfBoundsExceptionon a plainByteBuffersDirectory). No hang, no resource use, no write. - Not affected. Unpositioned iterators, iterators moved by
nextDoc()/advance()or by anadvanceExactthat returned true; dense SORTED fields; the unwrapped generic iterator (one distinct value); the default implementations ofNumericDocValues#rangeIntoBitSetandSortedNumericDocValues#rangeIntoBitSet, which are per-docadvanceExactloops. Not every codec override of those siblings is safe: the sparse multi-valued Lucene90SortedNumericDocValues#rangeIntoBitSetoverride (Lucene90DocValuesProducer.java:2202-2255, start check:2210-2213) has the same flaw; that is LFV-415, classed known because open PR #16835 guards it for table-encoded entries. - Releases. 10.6.0 (where #16180 is listed), released 2026-10-29 from the RC1 bits, which contain the bug. Priority low.
The test shows that on a default-codec segment with SORTED docs {0: "a", 1: no value, 2: "b"}, advanceExact(1) returns false and then ordinalRangeIntoBitSet(1, 3, ord(a), ord(a), bits, 0) on the same iterator marks doc 1.
package org.apache.lucene.index;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.FixedBitSet;
public class TestSortedOrdinalRangeAfterFalseAdvanceExact extends LuceneTestCase {
public void testDocWithoutValueIsNotMarked() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w =
new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
for (String value : new String[] {"a", null, "b"}) {
Document doc = new Document();
if (value != null) {
doc.add(new SortedDocValuesField("f", new BytesRef(value)));
}
w.addDocument(doc);
}
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
SortedDocValues dv = getOnlyLeafReader(reader).getSortedDocValues("f");
int ordA = dv.lookupTerm(new BytesRef("a"));
assertFalse(dv.advanceExact(1));
FixedBitSet bits = new FixedBitSet(3);
dv.ordinalRangeIntoBitSet(1, 3, ordA, ordA, bits, 0);
// expected: doc 1 (no value) is not marked; actual: marked with doc 0's ordinal
assertFalse("doc 1 has no value but was marked", bits.get(1));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-419/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.index.TestSortedOrdinalRangeAfterFalseAdvanceExact
- Expected vs actual
- Expected: doc 1 not marked (it has no value; the javadoc promises only docs whose ordinal is in range); actual (-ea and -da): bit 1 is set from doc 0's ordinal (
doc 1 has no value but was marked).
More files, environment, full output38 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: doc 1 has no value but was marked
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.assertTrue(Assert.java:42)
org.junit.Assert.assertFalse(Assert.java:65)
org.apache.lucene.index.TestSortedOrdinalRangeAfterFalseAdvanceExact.testDocWithoutValueIsNotMarked(TestSortedOrdinalRangeAfterFalseAdvanceExact.java:33)- The javadoc promises the documents in
[fromDoc, toDoc)with an ordinal in range, by per-docadvanceExact/ordValueevaluation, which is right here. - The default itself calls
ordValue()where the API forbids it, not the caller. - From the same state the dense override, the unwrapped single-value path and the numeric siblings' defaults answer correctly; the sparse SORTED path marks doc 1.
The full argument254 words
- Contract.
SortedDocValues.java:99-105(the method's javadoc);:38-39(ordValue());DocValuesIterator.java:30-31lists onlyintoBitSetanddocIDRunEnd()as illegal after a falseadvanceExact. The method has no@lucene.internalor@lucene.experimentaltag, whileDocIdSetIterator#intoBitSetis@lucene.internal(DocIdSetIterator.java:203). - Encoding-dependent answers. An executed consistency probe on one codec: a SORTED field with one distinct value (unwrapped) and a
NumericDocValuesfield give the per-doc answer from the same state; a sparse SORTED field with two values marks the value-less document. - The test framework flags it.
AssertingSortedDocValues.ordValueassertsexists(AssertingLeafReader.java:1084), so the default trips Lucene's own assertion. - History. The first revision of #16180 (
f5a83a8d11a) was the per-docadvanceExactloop. A reviewer (jimczi) asked "WhyadvanceExactper-doc here instead of iterating withadvance/nextDoc? This is really just a range scan over the docs that have a value, which is the same pattern as the defaultDocIdSetIterator.intoBitSetand the oldBulkOrdinalRangeIterator." They gave a performance reason (per-docadvanceExactwalks every gap document of a sparse field) and proposed today's loop; revision03ccba9729badopted it and left the javadoc unchanged. Nobody discussed a start state reached by a falseadvanceExact. - Why tests missed it. Three tests reach the method, all through
DocValuesRangeIterator, which moves the iterator withadvance()and never leaves it on a falseadvanceExact.TestDocValuesRangeIteratorIntoBitSet#testOrdinalRangeIntoBitSetMatchesPerDocEvaluationand#testOrdinalIntoBitSetMatchesLinearScanindex real documents that all have a value;TestDocValuesOrdinalRangeIterator#testSortedDocValuesIntoBitSetMatchesLinearScanruns the stock default on a mock iterator in which some documents have no value, but never from a falseadvanceExact.
The devil's-advocate check256 words
Strongest case (the intended-behaviour check, the adjudication, the prior-report search): the default was modelled on DocIdSetIterator#intoBitSet on purpose: jimczi's #16180 review chose the scan over present documents so that sparse fields do not walk every gap document. In #14529 a maintainer (jpountz) wrote "It shouldn't be legal to call intoBitSet after advanceExact returns false", which became the note on advanceExact; that settled the rule for DocIdSetIterator#intoBitSet, not for every bulk method. So this call order could be called misuse, fixed by adding the method to that note; the method is also new in 10.6.0, no in-tree caller reaches the state, and the default already skips [fromDoc, docID()) when docID() > fromDoc, an implied precondition (the hunt's credibility audit).
Why it fails: intoBitSet is relative to docID(), while this method takes an explicit absolute fromDoc and its javadoc still describes the per-doc loop; nothing documents a precondition; the answer changes with the encoding; one layout throws a codec exception (EOFException, or ArrayIndexOutOfBoundsException on a plain ByteBuffersDirectory) rather than a clean argument check. Open PR #16835 (kkewwei, approved by sgup432, not merged) accommodates this call order in sibling code: for table-encoded entries it adds exactly this guard to the Lucene90 numeric and sorted-numeric overrides, and its test calls advanceExact(fromDoc) before the bulk call; the PR is framed as a speed-up and does not call the sequence a public API contract. Residual risk: maintainers may prefer to document a precondition; the stale javadoc and the codec exception would still need fixing then.
Recommended Skip a start document that has no value
In the default, when the iterator already sits at or past fromDoc, check the current document with advanceExact(docID()) and skip it with nextDoc() if it has no value (SortedDocValues.java, +8/-1). The test and 14 doc-values suites (728 tests) pass, with assertions on and off.
- for (int doc = docID() >= fromDoc ? docID() : advance(fromDoc); doc < toDoc; doc = nextDoc()) {
+ int doc = docID();
+ if (doc < fromDoc) {
+ doc = advance(fromDoc);
+ } else if (doc < toDoc && advanceExact(doc) == false) {
+ // positioned by advanceExact() on a doc without a value: skip it
+ doc = nextDoc();
+ }
+ for (; doc < toDoc; doc = nextDoc()) {
long ord = ordValue();All options and trade-offs3 options · 563 words
Two options were built and run in a scratch worktree of 645d4fe14a20; a documentation-only change is not recommended alone.
The finder's fix, with the guard written as doc < toDoc, the guard shape kkewwei uses in open PR #16835 (table-encoded numeric overrides). It keeps the scan over documents with a value that jimczi asked for in the #16180 review, and the existing acceptance of docID() > fromDoc; like the stock code, it then starts at docID() and does not evaluate [fromDoc, docID()).
- for (int doc = docID() >= fromDoc ? docID() : advance(fromDoc); doc < toDoc; doc = nextDoc()) {
+ int doc = docID();
+ if (doc < fromDoc) {
+ doc = advance(fromDoc);
+ } else if (doc < toDoc && advanceExact(doc) == false) {
+ // positioned by advanceExact() on a doc without a value: skip it
+ doc = nextDoc();
+ }
+ for (; doc < toDoc; doc = nextDoc()) {
long ord = ordValue();Trade-offs
- One
advanceExact(docID())per call when the iterator is already at or pastfromDoc(legal:target >= docID()); for the in-tree caller one cheap check per MAYBE block. No benchmark was run. - Caveat to state (the adjudication, from the cross-model refute review): flush-time
BufferedSortedDocValuesand some merge views throwUnsupportedOperationExceptionfromadvanceExact, but they also throw fromadvance(), so the stock default already fails on them whendocID() < fromDoc; none of them is passed to this method in-tree.
What the checks validated
- Stock + test: fails with assertions on and off; + fix: passes with assertions on and off.
- + fix: 14 suites, 728 tests (65 skipped), 0 failures, assertions on and off:
TestDocValuesRangeIteratorIntoBitSet,TestDocValuesRangeIterator,TestDocValuesOrdinalRangeIterator,TestDocValuesOrdinalSetIterator,TestDocValuesQueries,TestSkipBlockRangeIteratorBitSetOperations,TestBulkScorerConsistency,TestSortedSkipperScorerSupplier,TestSortedSetDocValuesSetQuery, the threeTestLucene90DocValuesFormat*suites,TestMultiDocValuesand the new test;:lucene:core:check -x testpasses. - With the patched file shadowed, the finder's 2 tests (incl. the
EOFExceptionlayout) and the independent reproduction's 7 tests (random sparse layouts, the DENSE block,MMapDirectory) pass. Not run: the full test suite, nightly tests. - The finder's own form of the fix, shadowed together with the numeric sibling's fix, took the harness class LEAD-B from 1,770 mismatches (1,350 on SORTED views, this bug; 420 on SORTED_NUMERIC views, LFV-415) to 0.
for (int doc = Math.max(fromDoc, docID()); doc < toDoc; doc++) { if (advanceExact(doc)) { ... } }, as in the first revision of #16180 (plus Math.max, so docID() > fromDoc stays legal; like the recommended fix, it then starts at docID()). Built and run: the test and the same 14 suites pass with assertions on.
Trade-offs
Matches the javadoc word for word and the numeric siblings, but visits every document in the range instead of only those with a value, the cost the #16180 review asked to avoid on sparse fields.
Add ordinalRangeIntoBitSet to the "illegal after advanceExact returned false" note on DocValuesIterator#advanceExact, as #14529 did for intoBitSet.
Trade-offs
Also needs the javadoc's "per-doc evaluation via advanceExact" sentence fixed, and leaves a silent extra hit or a codec exception for callers who miss the note, while the dense override and the unwrapped single-value path keep answering correctly.
No public downstream code calls this method yet (it is new in 10.6.0, released 2026-10-29); code that probes a document with advanceExact and then bulk-evaluates from it would get one extra hit, or an exception, per call.
A bounded-exhaustive check: a harness made every legal sequence of up to three iterator moves on tiny segments, for 26 doc-values and norms views, and compared each step with a fresh nextDoc() enumeration. The mismatch class "advanceExact false, then a bulk range call" led here; it was then reduced to a JUnit on the public API.
The method in full198 words
Method: bounded-exhaustive translation validation (semi-formal), a hunt unit. The harness drives the stock jar through every legal move sequence of depth up to 3 over nextDoc, advance, advanceExact, intoBitSet, docIDRunEnd, intoArray, longValues, rangeIntoBitSet and ordinalRangeIntoBitSet on segments of up to 5 documents (skip index on and off), plus random sequences on 70,000-document segments covering SPARSE, DENSE and ALL blocks and two-cursor interleavings, for 26 doc-values and norms views, and compares each step with a fresh nextDoc() enumeration of the same view: about 990M sequences over two batches. Mismatch class LEAD-B (advanceExact(t) == false, then a bulk range call starting at t) had 1,770 mismatches in a small-scope run: 1,350 on SORTED views (this bug) and 420 on SORTED_NUMERIC views (the numeric sibling LFV-415); 0 with this fix and the sibling's fix shadowed. The SORTED mismatches came from the segment's own iterator, its merge instance, the ParallelLeafReader, FilterSortedDocValues, ExitableDirectoryReader and AssertingLeafReader views (that run recorded wrong bits there, not an assertion failure), and the multi-segment views. The finder then wrote a public-API JUnit on the stock main jars; this doc's test is a one-method version of it.
No prior report found: 196 queries on 2026-10-26 (GitHub issues, PRs and commits, four mailing lists, the JIRA archive, downstream repositories, the web), refreshed on 2026-10-29. Related: #14529 made intoBitSet illegal after a false advanceExact; open PR #16835 guards the same state in the numeric overrides.
Searches5 searches
Related, not reports of this bug: #16180 introduced it (the review swapped the per-doc loop for the scan; nobody discussed this start state); #14529 resolved the same kind of start state for intoBitSet by declaring the call illegal, and the resulting note does not name this method; open #16835 (kkewwei) adds the advanceExact(currentDoc) == false guard to the Lucene90 numeric and sorted-numeric overrides, for table-encoded entries only (it is why the numeric sibling LFV-415 is classed known); open #16869 adds the SortedSetDocValues sibling with a correct per-doc default; open #16897 adds a second caller that positions with advance().
Judgment call recorded in the registry: under the same-fix-site rule this bug stays new; counting "the same hazard is guarded elsewhere" as partial overlap would make it known.
Since the open-PR file scan: open PR #16911 (javanna, "Decommission branch 10x", opened 2026-10-26 against branch_10x, 7,630 changed files) removes branch_10x's files, SortedDocValues.java among them. It does not touch main or branch_10_6, but a re-scan of open PRs by file would list it.
| Where | When | How | Found |
|---|---|---|---|
| GitHub (apache/lucene issues, PRs, commits; elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr) | 2026-10-26 | three scripted prior-report searches (145 queries incl. lists and JIRA) and 42 manual searches: ordinalRangeIntoBitSet with advanceExact, ordValue, EOFException; "advanceExact returned false"; "docID() >= fromDoc"; "falls back to per-doc evaluation"; full threads of 37 PRs and issues | no report; #16180, #14529, #16835, #16869, #16897 are related |
| Open apache/lucene PRs | 2026-10-26 | changed-file lists of all 411 open PRs | none touched SortedDocValues.java then; #16911 ("Decommission branch 10x"), opened later that day against branch_10x, removes it there |
| Mailing lists and JIRA archive | 2026-10-26 | dev@, java-user@, issues@, commits@ through a scripted prior-report search; JIRA for ordinalRangeIntoBitSet, rangeIntoBitSet, intoMaybeBlock | no report (the method postdates the JIRA era) |
| Web | 2026-10-26 | 4 web searches | nothing relevant |
| Refresh | 2026-10-29 | upstream branch heads; issues and PRs naming ordinalRangeIntoBitSet; items updated since 2026-10-25 for advanceExact, SortedDocValues, ordValue, "ordinal range"; the files of #16835, #16869, #16897 (the open-PR file scan was not repeated) | main still 645d4fe14a20; no new item; none of the three PRs touches SortedDocValues.java |
Introduced with the method by #16180 (04d33b4ce5d, merged 2026-07-29, milestone 10.6.0); its first revision was a correct per-doc loop. Affects 10.6.0 (released 2026-10-29) and main; no earlier release has the method.
Versions tested7 rows
#16180 "add bulk intoBitSet for SortedDocValues ordinal ranges" (iprithv, merged by jimczi) is 04d33b4ce5d on main and 8c58bbf22ba on branch_10x, contained in branch_10_6; CHANGES lists it in the 10.6.0 section. It added the public method and its default together, with overrides for the dense and generic SORTED iterators but none for the sparse one. Inside the PR, revision f5a83a8d11a (2026-07-04) used a per-doc advanceExact loop; after a review question, 03ccba9729b (2026-07-27) switched to the docID() >= fromDoc ? docID() : advance(fromDoc) start and left the javadoc unchanged. No later commit touched the method.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the test fails, assertions on and off (gradle and standalone) |
branch_10_6 4ae946e5a9b (10.6.0 RC1) | affected | the test fails on jars built at this revision (assertions off); the finder's 2 tests fail 2 of 2 (assertions on) |
main ea57360912ed, branch_10_6 cb17f8f865c | affected | the finder's tests fail 2 of 2, assertions on and off |
04d33b4ce5d (#16180) vs its parent 62f29b6c915 | bisect | #16180 fails 2 of 2; its parent has no such method (compile error) |
| 10.5.2, 10.5.1, 9.12.3 | method absent | the test does not compile: the method does not exist |
branch_10x 657014b3c579 | read | the same lines (by reading, not run) |
with the fix (main 645d4fe14a20) | not affected | the test and 14 doc-values suites pass |
The finder, the independent reproduction, the intended-behaviour check and an adjudication rate it real (adjudication: confirmed, high), as does the intended-lens cross-model review (the cross-model review, reading only); the refute-lens review, also reading only, returned a low-confidence procedural "not real" but agreed on the mechanism. Fact checks found this doc right in substance; their corrections are applied.
Checks and verdicts395 words
Every check re-ran a repro on the stock jars. The reviewer from another model family (OpenAI GPT-5.6) could not execute code in either lens; its execution evidence is a separate re-run of the finder's repro.
| Check | Verdict | What it found |
|---|---|---|
| finder (a hunt unit) | candidate | Harness class LEAD-B: 1,770 mismatches (1,350 on SORTED views, this bug; 420 on SORTED_NUMERIC views, the sibling LFV-415), 0 with this fix and the sibling's fix shadowed; JUnit fails 2 of 2 on main with -ea and -da, passes with the fix. |
| cross-model review, refute lens | not real, low, procedural only | "not a substantive refutation": it could not run code, agreed on the root cause, judged the inputs in contract and flagged the narrow reach and the fix caveat; a separate run re-ran the repro (2 of 2 fail). |
| Independent reproducer | real | Own public-API JUnit: 5 of 7 fail on main (-ea, -da, two seeds), 2 controls pass; found the DENSE-block variant; 7 of 7 pass with the fix. |
| Cross-model review, intended lens | real, medium (reading only) | No required iterator state in the javadoc; the default makes the forbidden ordValue() call; the numeric siblings' defaults correct. |
| Devil's advocate | real, low impact | Executed consistency probe: encoding-dependent answers from the same call pair; the "modelled on intoBitSet" case does not survive the explicit fromDoc. |
| adjudication | confirmed, high | Fresh seed: finder 2 of 2 fail (-ea, -da), the independent reproduction 5 of 7 fail, 7 of 7 with the fix; duplicate of none. |
| this doc's runs | reproduced, fix validated | The one-method test on main 645d4fe14a20 (gradle and standalone, -ea, -da) and the RC1 jars; the fix with 14 suites and check; the alternative fix with the same suites. |
| Independent fact and diagram checks | right in substance; errors fixed | A fact check re-ran the test (gradle and standalone, -ea and -da, the RC1 jars, 10.5.2), fix A, option B and 4 suites, and added probes; a cross-model fact check (OpenAI GPT-5.6, reading only) re-read every citation; a diagram check compared every frame with the code. Their corrections (the exception by Directory, the wrapper and sibling scope, the [fromDoc, docID()) caveat, the tests that reach the method, the harness split, the query count, the review tallies) are applied. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 540 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,299 words
- Registry title
- SortedDocValues#ordinalRangeIntoBitSet's default implementation, called after advanceExact(d) returned false, reads ordValue() for doc d, which has no value, so it marks d using a neighbouring document's ordinal (or throws EOFException)
- Area
- index / SortedDocValues bulk ordinal range (default implementation; Lucene90 sparse SORTED iterator)
- Priority
- Low, rank 596: This new 10.6.0 bulk-API default sets a wrong bit only for an advanceExact-false-then-bulk call pattern that Lucene itself never uses.
- Impact class
- silent wrong results
- Impact note
- Only application code that calls the public, untagged SortedDocValues#ordinalRangeIntoBitSet (new in 10.6.0, released 2026-10-29) itself, from a document d on which advanceExact(d) just returned false on the same iterator, over a SORTED field that some documents lack, is hit, on the default codec; each such call marks at most one extra document (d, judged by the previous valued document's ordinal in a SPARSE block or the next one's in a DENSE block) or throws RuntimeException(EOFException) when no earlier document in a SPARSE block has a value; no Lucene query, sort, facet or merge path makes this call pair (the only in-tree caller positions the iterator with advance()), ordinary scale, 10.6.0 (released 2026-10-29) and main only.
- Summary
- On a default-codec index with docs {0: 'a', 1: no value, 2: 'b'} in a SORTED doc-values field, you call advanceExact(1) on the field's SortedDocValues (it returns false) and then ordinalRangeIntoBitSet(1, 3, ord(a), ord(a), bits, 0) on the same iterator. You expect no bit, because neither doc 1 nor doc 2 has the value 'a', and that is what the per-document advanceExact/ordValue evaluation the javadoc describes returns. Instead the default starts at docID() == 1, calls ordValue() there (which the API calls illegal after a false advanceExact), gets doc 0's ordinal and marks doc 1; with {0: no value, 1: 'a', 2: 'b'} and advanceExact(0), the same call throws RuntimeException(EOFException).
- Introduced
- 04d33b4ce5dd9f0466ce7b525c3ef5a770925eb2 (GITHUB#16180 'add bulk intoBitSet for SortedDocValues ordinal ranges', by GitHub iprithv, merged by jimczi, milestone 10.6.0; branch_10x cherry-pick 8c58bbf22ba, contained in branch_10_6) added both the public method and its faulty default [exec: the introducing commit fails the repro 2 of 2 with -ea and -da; its parent 62f29b6c915 has no such method (compile error); read: git blame, git log -L and -S on SortedDocValues.java list only 04d33b4ce5d]. Inside the PR, the first revision f5a83a8d11a (2026-07-04) used a correct per-doc advanceExact loop; after a review question about using advance/nextDoc instead, revision 03ccba9729b (2026-07-27) switched to the 'docID() >= fromDoc ? docID() : advance(fromDoc)' start and left the javadoc unchanged. Not a regression of a released API: the bug is as old as the method (2026-07-29 (04d33b4ce5d on main; cherry-pick 8c58bbf22ba on branch_10x the same day))
- First affected version
- 10.6.0, released 2026-10-29 (#16180 is in the 10.6.0 section of CHANGES, and the branch_10_6 head cb17f8f865c fails the repro [exec]; lucene-core 10.6.0 was not yet published on 2026-10-26); no earlier release contains the method
- Affected versions
- main ea57360912ed [exec, -ea and -da]; branch_10_6 head cb17f8f865c (10.6.0-SNAPSHOT) [exec, -ea and -da]; introducing commit 04d33b4ce5d [exec, -ea and -da]; branch_10x head 42df6b34707 (10.7.0-SNAPSHOT) and upstream main cf398bfb142 [read, same lines, 2026-10-26]; not applicable: 9.12.3, 10.5.1, 10.5.2 and the parent 62f29b6c915 (method absent; the repro does not compile) [exec]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (the prior-report record verdict new, searched 2026-10-26, read-only, 196 logged queries: three scripted prior-report searches (145 queries) over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web; 42 manual GitHub searches; full threads of 37 PRs and issues read; the changed-file lists of all 411 open PRs scanned, none touches SortedDocValues.java). Not fixed upstream: main cf398bfb142, branch_10x and branch_10_6 keep the code of #16180. Related, not reports of this bug: #16180 (introduced it; the review swapped the per-doc advanceExact loop for an advance/nextDoc scan, nobody discussed the advanceExact()==false start state); #14529 (2025; for intoBitSet the same kind of start state was resolved by declaring the call illegal, the note on DocValuesIterator#advanceExact does not name ordinalRangeIntoBitSet); open #16835 (GitHub kkewwei, approved, not merged) adds the same advanceExact()==false guard to Lucene90 numeric rangeIntoBitSet overrides only, never touches or mentions SortedDocValues; open #16869 (SortedSetDocValues sibling, per-doc advanceExact default, correct here); open #16897 (a second caller that positions with advance()); #8922, #9226, #9186, #11432 (older, unrelated)
- Lucene commit tested
- main
645d4fe14a20(commit of 2026-10-28, upstream main on 2026-10-29): the repro test through gradle in a scratch worktree and through the standalone runner, assertions on and off, stock, with the recommended fix and with the alternative fix; jars built at branch_10_64ae946e5a9b(10.6.0 RC1), assertions off; the 10.5.2 release jars (the method is absent, the test does not compile). The hunt's earlier runs (finder, verifiers, adjudication, follow-up, including the introducing commit04d33b4ce5d, its parent, branch_10_6cb17f8f865c, 10.5.1 and 9.12.3) used mainea57360912ed, where every cited source file is identical.
- Finding record
lucene/core/src/java/org/apache/lucene/index/SortedDocValues.java:36-43 (ordValue: 'It is illegal to call this method after advanceExact(int) returned false'), :99-123 (javadoc: 'Fills bitSet with the doc IDs in [fromDoc, toDoc) whose ordinals are in [minOrd, maxOrd]'; 'The default implementation falls back to per-doc evaluation via advanceExact and ordValue'; code :117 'for (int doc = docID() >= fromDoc ? docID() : advance(fromDoc); ...', :118 'long ord = ordValue();')lucene/core/src/java/org/apache/lucene/index/DocValuesIterator.java:24-33 (advanceExact: target >= docID(), docID() == target afterwards; after false only intoBitSet and docIDRunEnd are illegal)lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:1549 (dense SORTED override, random access, correct), :1566-1571 (sparse SORTED iterator: no override; ordValue() = values.get(disi.index())), :1655 (generic SORTED override, correct)lucene/core/src/java/org/apache/lucene/codecs/lucene90/IndexedDISI.java:597 (SPARSE advanceExactWithinBlock: on a miss index stays on the previous valued doc, -1 if none), :694 (DENSE advanceExactWithinBlock: index = rank of the next valued doc)lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java:296 (advanceDisi positions with advance() only), :490-518 (the only in-tree caller: from = max(blockStart, docID()), then sortedValues.ordinalRangeIntoBitSet)lucene/CHANGES.txt:438, :635 (#16180 in the 10.6.0 section)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/SortedDocValues.java:37-44, :99-123a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/DocValuesIterator.java:24-33a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:1473-1477, :1548-1555, :1557-1607, :1654-1661a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/codecs/lucene90/IndexedDISI.java:485-493, :597-623, :694-715a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/FilterSortedDocValues.java:24-90 (no ordinalRangeIntoBitSet override); index/ExitableDirectoryReader.java:205; index/MultiDocValues.java:684a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:1090-1096 (bitsPerValue 0: longValue() = entry.minValue); codecs/lucene90/IndexedDISI.java:495-516 (advanceBlock)
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/DocValuesRangeIterator.java:296-301, :493-520a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/NumericDocValues.java:113-140; index/SortedNumericDocValues.java:40-74a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:2202-2255 (the sibling override, LFV-415)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/SortedDocValues.java:38-39, :99-105a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/DocValuesIterator.java:30-31; search/DocIdSetIterator.java:186-210a Lucene main checkout (645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java:1082-1088a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIteratorIntoBitSet.java:688-753a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestDocValuesRangeIteratorIntoBitSet.java:591-641; TestDocValuesOrdinalRangeIterator.java:40-42, :116-178, :285-288
- Prior reports
GitHub apache/lucene#16911 (gh pr view, read-only, 2026-10-29: title, base branch_10x, created 2026-10-26, 7,630 changed files)
- History
a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:653 (10.6.0 section from :456)a Lucene clone: git show 4ae946e5a9b / 657014b3c579 :lucene/core/src/java/org/apache/lucene/index/SortedDocValues.java (lines 117-118)
- TestSortedOrdinalRangeAfterFalseAdvanceExact.java (JUnit test):
repro/LFV-419/src/org/apache/lucene/index/TestSortedOrdinalRangeAfterFalseAdvanceExact.java(37 lines)
It is made in parallel with this doc; every on-screen value comes from the code at 645d4fe14a20 or the diagram's executed probe. Its fix section shows the finder's form of the fix (guard start != NO_MORE_DOCS); the recommended fix above is the same change with the guard written as doc < toDoc, and both were run.
assets/LFV-419/bug.png(540 KB)
