One cast: SortedSetDocValuesTermsEnum.seekExact(long) stores currentOrd = (int) ord, which keeps only the low 32 bits of the 64-bit ordinal, then looks up that truncated ordinal.
- Sorted-set doc values store each distinct value once, in sorted order, and number them 0, 1, 2, ...: the ordinal. Ordinals are
long(lookupOrd(long),long getValueCount()), so a view may hold more than 2^31 values. SortedSetDocValues#termsEnum()walks those values as aTermsEnum. You can jump to a value by ordinal withseekExact(long), or save a position withtermState()and come back withseekExact(term, state).- Per-segment doc values of the default codec bring their own enum. Views without one get the default,
SortedSetDocValuesTermsEnum: the top-levelMultiDocValuesview over several segments,SortingCodecReader(meant for merging), SimpleText's multi-valued fields, or your own subclass.
- Take a sorted-set doc-values view with 4,294,967,302 (2^32 + 6) distinct values that keeps the default
termsEnum(): for exampleMultiDocValues.getSortedSetValuesover a huge multi-segment index, or your ownSortedSetDocValuessubclass. - Call
te = values.termsEnum()andte.seekExact(4294967301L). Ordinals arelongandtermsEnum()is documented to supportseekExact(long), so you expectte.ord()to be 4,294,967,301. - Instead
seekExactstorescurrentOrd = (int) ord, which keeps only the low 32 bits: 2^32 + 5 becomes 5.ord()returns 5 andterm()the value of ordinal 5, with no error, assertions on or off. - Between 2^31 and 2^32 - 1 the cast turns negative:
seekExact(2147483655)(2^31 + 7) looks up ordinal -2,147,483,641 and throws. Seeking back with a savedtermState()goes through the same line and fails the same way. - Everything else in the enum (
seekCeil,seekExact(BytesRef),next(),ord()) uses thelongand is right. Dropping the cast fixes it.
Only code that seeks by ordinal (seekExact(long) or seekExact(term, TermState)) on the default enum of a sorted-set view with more than 2^31 distinct values: the top-level MultiDocValues view, SortingCodecReader, SimpleText or your own subclass. No Lucene code does this with an ordinal of 2^31 or more, and per-segment default-codec doc values have their own correct enum.
Mechanism, terms and numbers722 words
SortedSetDocValues#termsEnum() returns, by default, a SortedSetDocValuesTermsEnum and documents that "The enum supports TermsEnum#ord() and TermsEnum#seekExact(long)". Sorted-set ordinals are long, and the enum keeps its position in a long field, but seekExact(long ord) stores currentOrd = (int) ord; before looking the value up. The cast keeps the low 32 bits as a signed int, and the assert one line above checks the un-narrowed long, so assertions do not catch it.
On a view with more than 2^31 distinct values a seek to an ordinal from 2^31 to 2^32 - 1 makes the enum look up a negative ordinal, which Lucene's views reject with an exception; from 2^32 to 2^32 + 2^31 - 1 it silently lands on ord mod 2^32: seekExact(4294967301) leaves the enum on ordinal 5 with the term of ordinal 5. Higher up the two behaviours alternate in bands of 2^31. seekExact(term, TermState) delegates to the same method. Only views that keep the default enum are affected: the top-level MultiDocValues view over several segments, SortingCodecReader's sorted-set view (documented for merging only), SimpleText's multi-valued fields, the test framework's asserting wrapper and application subclasses; per-segment values of the default codec have their own correct enum, and no Lucene code seeks these views with an ordinal of 2^31 or more. The line dates from 2013 (4.2.0) and is unchanged on main; currentOrd = ord; fixes it.
// SortedSetDocValuesTermsEnum (abridged)
private long currentOrd = -1; // :26
public void seekExact(long ord) throws IOException { // :67
assert ord >= 0 && ord < values.getValueCount(); // :68, checks the long
currentOrd = (int) ord; // :69, keeps the low 32 bits
scratch.copyBytes(values.lookupOrd(currentOrd)); // :70
}
public void seekExact(BytesRef term, TermState state) throws IOException { // :114
this.seekExact(((OrdTermState) state).ord); // :116, same cast
}| ordinal | hex (64-bit) | (int) ord | result |
|---|---|---|---|
| 2^31 - 1 = 2,147,483,647 | 0x00000000_7FFFFFFF | 2,147,483,647 | correct |
| 2^31 + 7 = 2,147,483,655 | 0x00000000_80000007 | -2,147,483,641 | lookup of a negative ordinal throws |
| 2^32 - 1 = 4,294,967,295 | 0x00000000_FFFFFFFF | -1 | throws |
| 2^32 + 5 = 4,294,967,301 | 0x00000001_00000005 | 5 | silently on ordinal 5; next() then goes to 6 |
In general an ordinal whose low 32 bits read as a negative int (2^31 to 2^32 - 1, 3 x 2^31 to 2^33 - 1, ...) makes the enum look up a negative ordinal. Lucene's own views reject that with an exception (by code reading; the type depends on the view: IndexOutOfBoundsException from the default codec's bounds check, IllegalArgumentException from the finder's synthetic values), and an application subclass's lookupOrd decides for itself. Any other ordinal of 2^32 or more (2^32 to 2^32 + 2^31 - 1, 2^33 to 2^33 + 2^31 - 1, ...) lands on ord mod 2^32. After a bad seek, ord(), term(), termState() and next() all continue from the wrong position.
The class was created in 2013 as a copy of SortedDocValuesTermsEnum, which has private int currentOrd and the same line; there it is correct, because SortedDocValues ordinals are ints. The copy widened the field to long and kept the line. That it is a copy leftover is an inference: no comment, test or commit message explains it.
MultiDocValues.MultiSortedSetDocValues(@lucene.internal), returned byMultiDocValues.getSortedSetValuesfor a reader with more than one segment when at least one segment has the field (otherwise it returnsnull); its ordinals are global ordinals from anOrdinalMap, so the bound is on distinct values across all segments. Executed by an independent check over a realOrdinalMapwith two synthetic segments: fails, passes with the fix.SortedSetDocValuesWriter.SortingSortedSetDocValues, returned bySortingCodecReader's sorted-set producer.SortingCodecReader's javadoc says it "should only be used for merging", and merging walks these values withnext().- SimpleText's multi-valued sorted-set values (a debugging codec; single-valued SimpleText fields come back as a singleton and use the sorted enum), the test framework's
AssertingLeafReader.AssertingSortedSetDocValueswrapper, the unused debugging helperGenericTermsCollector.verbosein join, and any application or downstreamSortedSetDocValuessubclass that does not overridetermsEnum().FilterSortedSetDocValuesandSingletonSortedSetDocValuesdelegate to the wrapped values. - Not affected: per-segment sorted-set values of the default codec, which return their own terms dictionary enum; its
seekExact(long)works on thelong. The merged view built for sorted-set merges also brings its own enum (MergedTermsEnum).
Exposure and likelihood274 words
- Not Lucene's own code. Merges only iterate these enums with
next(); the cited facet and join classes reject more thanInteger.MAX_VALUEvalues up front; grouping's term facets seekSortedSetDocValues#termsEnum()by ordinal, but narrow the value count tointand seek withintordinals, so they cannot reach 2^31. So no Lucene query, filter, facet, join, grouping or merge returns a wrong result from this (code reading; tests seek these enums only with small ordinals). - Default, not opt-in. No setting is involved: the enum is the default of the public base class. Assertions on and off behave the same.
- Scale. 2^31 = 2,147,483,648 distinct values in one view, 2^32 = 4,294,967,296 for the silent case. The format allows it (the default codec writes the value count as a variable-length long), and a maintainer stated on #11636 / LUCENE-10600 that SORTED_SET fields can exceed
Integer.MAX_VALUEunique values. For a multi-segment view the bound is on distinct values across all segments. - Effect. Ordinals 2^31 to 2^32 - 1, and every later band whose low 32 bits read as a negative
int: the enum askslookupOrdfor a negative ordinal, which Lucene's own views reject with an exception (code reading; every executed view threw); an application subclass decides for itself. Ordinals 2^32 to 2^32 + 2^31 - 1, and every later band whose low 32 bits are non-negative: the enum silently reports ordinalord mod 2^32and its term, andnext()continues from there. No memory unsafety (Java bounds checks). - Likelihood: contrived. It needs billions of distinct values in one view plus an ordinal seek on the default enum; nobody has reported it in 13 years.
One JUnit test, public classes only, about a second: an anonymous SortedSetDocValues with no documents and 2^32 + 6 values (ordinal o's value is o as 8 big-endian bytes) keeps the default termsEnum(), seeks to 2^32 + 5 and checks ord().
/*
* 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.index;
import java.io.IOException;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BitUtil;
import org.apache.lucene.util.BytesRef;
public class TestSortedSetTermsEnumSeekExactLongOrd extends LuceneTestCase {
public void testSeekExactOrdAbove2Pow32() throws IOException {
long ord = (1L << 32) + 5;
SortedSetDocValues values =
new SortedSetDocValues() {
@Override
public long getValueCount() {
return ord + 1;
}
@Override
public BytesRef lookupOrd(long o) {
byte[] term = new byte[Long.BYTES];
BitUtil.VH_BE_LONG.set(term, 0, o);
return new BytesRef(term);
}
@Override
public long nextOrd() {
throw new UnsupportedOperationException();
}
@Override
public int docValueCount() {
throw new UnsupportedOperationException();
}
@Override
public boolean advanceExact(int target) {
return false;
}
@Override
public int docID() {
return -1;
}
@Override
public int nextDoc() {
return DocIdSetIterator.NO_MORE_DOCS;
}
@Override
public int advance(int target) {
return DocIdSetIterator.NO_MORE_DOCS;
}
@Override
public long cost() {
return 0;
}
};
TermsEnum te = values.termsEnum();
te.seekExact(ord);
// expected: positioned on ord 4294967301; actual: silently positioned on ord 5
assertEquals("ord() after seekExact(" + ord + ")", ord, te.ord());
}
}
cp $REPRO/TestSortedSetTermsEnumSeekExactLongOrd.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.index.TestSortedSetTermsEnumSeekExactLongOrd -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
ord()is 4294967301 afterseekExact(4294967301), andterm()is that ordinal's value.- Observed
expected:<4294967301> but was:<5>: the enum sits on ordinal 5, with no exception, assertions on or off.
More files, environment, full output267 words
- Why this case. It is the worst effect, the silent wrong position, in one assertion. The exception between 2^31 and 2^32 - 1 and the
seekExact(term, TermState)overload are the same line and are fixed by the same token. - The strict reading, "only ordinals previously returned by ord()". The bare
seekExact(ord)does not first get the ordinal fromord(), but it is exactly whatord()reports for that term: on the same values,te.seekCeil(values.lookupOrd(ord))returns FOUND withord()4,294,967,301, andseekExactwith thatord()value, or withte.termState()after moving away, givesord()5 (both executed). - Releases. The same file compiles and fails unchanged on 10.6.0.
- As the regression test: this same test, for example as a method of
TestMultiDocValuesor in a newTestSortedSetDocValuesTermsEnum. It allocates nothing big, so it needs no@Monsterannotation. It passes with the fix. - Classes used. Nothing package-private; the term encoding uses
BitUtil.VH_BE_LONG, andBitUtilis public but marked@lucene.internal.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails unchanged on 10.6.0 |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
cp $REPRO/TestSortedSetTermsEnumSeekExactLongOrd.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.index.TestSortedSetTermsEnumSeekExactLongOrd -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on (the same with -Ptests.asserts=false)
TestSortedSetTermsEnumSeekExactLongOrd > testSeekExactOrdAbove2Pow32 FAILED
java.lang.AssertionError: ord() after seekExact(4294967301) expected:<4294967301> but was:<5>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:5F7086835E0F3B8A]:0)
at org.junit.Assert.assertEquals(Assert.java:647)
at org.apache.lucene.index.TestSortedSetTermsEnumSeekExactLongOrd.testSeekExactOrdAbove2Pow32(TestSortedSetTermsEnumSeekExactLongOrd.java:81)
# 10.6.0 release jar, assertions on and off: the same AssertionError
# the exception band, from the finder's earlier test (synthetic values that reject negative ordinals)
testSeekExactByOrd java.lang.IllegalArgumentException: ord out of range: -2147483641
testSeekExactByTermStateOfSameEnum java.lang.IllegalArgumentException: ord out of range: -2147483641SortedSetDocValues#termsEnum()promisesord()andseekExact(long); ordinals arelong, and even an ordinal thatord()reported lands on the wrong term.- Every other method of the enum uses its
longfield; the cast is left over from copying theint-ordinal sibling in 2013. - Maintainers removed the same kind of "bogus cast" in
OrdinalMapin 2014 (LUCENE-5767).
The full argument202 words
- Contract.
SortedSetDocValues#termsEnum(): "Returns a TermsEnum over the values. The enum supports TermsEnum#ord() and TermsEnum#seekExact(long)." Ordinals andgetValueCount()arelong("one plus the maximum ordinal").TermsEnum#seekExact(long): "Seeks to the specified term by ordinal (position) as previously returned by ord(). The target ord may be before or after the current ord, and must be within bounds."seekExact(BytesRef, TermState)is legal with a state from the same enum instance. - The class is long-ordinal everywhere else.
seekCeil,seekExact(BytesRef),next(),ord()andtermState()use thelongfield, andOrdTermState.ordis along. The cast is the only narrowing. - Upstream tests exercise this method.
LegacyBaseDocValuesFormatTestCase.testSortedSetTermsEnumandtestSortedSetEnumAdvanceIndependently(through its helper) seek by ordinal on multi-valued documents, and they run throughTestSimpleTextDocValuesFormat, whose multi-valued sorted-set view uses this enum. Ordinal seeking is supported, just never tested above 2^31. - Precedent. rmuir's commit
7ab10178c38(LUCENE-5767, 2014): "remove bogus cast (in this case can exceed Integer.MAX_VALUE, and the underlying delta reader takes long anyway)", changing(int) ordtoordin the global-ordinals code ofMultiDocValues. - No documented limit. No javadoc, test, comment or CHANGES entry limits sorted-set views to 2^31 values; the facet and join guards are module-local caps for their own
intarrays.
The devil's-advocate check270 words
The strongest case for "works as designed" (built by the intended-behaviour checker): (a) the enum is a package-private, generic fallback that LUCENE-4819 put behind termsEnum(), which codecs can override, and the default codec does; (b) the views that keep it are niche: MultiDocValues is @lucene.internal and @lucene.experimental ("This is very costly"), SortingCodecReader is documented as "should only be used for merging", and SimpleText is a debugging codec ("FOR RECREATIONAL USE ONLY"); (c) no Lucene code seeks them with an ordinal of 2^31 or more: the sorted-set merge view brings its own enum, grouping uses int ordinals, and the cited facet and join classes reject that many values; (d) seekExact(long) takes ordinals "as previously returned by ord()", which the minimal test's bare seekExact(2^32 + 5) is not; (e) no real index with more than 2^31 distinct values was built: the failing view is synthetic.
Why it fails: the public method SortedSetDocValues#termsEnum() promises seekExact(long) and every subclass that does not override it gets this enum; @lucene.internal and @lucene.experimental allow API changes, not a long-ordinal API that narrows to int, and niche, merge-only or debugging views limit reach, not correctness. The strict reading is answered by the round trips: seekCeil returns FOUND with ord() 4,294,967,301, and seeking back with that ord() or with that enum's termState() lands on 5. A synthetic view is the practical way to reach 2^31 values in a test; it uses only public classes, and an independent check reached the same line through a real OrdinalMap and MultiSortedSetDocValues over synthetic segments. "Nobody calls it at this scale" limits severity, not validity. Verdict: not intended, a defect; low priority.
Recommended Drop the cast
Drop the cast: currentOrd = ord; in SortedSetDocValuesTermsEnum.seekExact(long). One token; it also fixes seekExact(term, TermState), which delegates there. The test passes with assertions on and off, and the closest core suites, the SimpleText doc-values suite and :lucene:core:check -x test pass.
--- a/lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java
+++ b/lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java
@@ -66,7 +66,7 @@
@Override
public void seekExact(long ord) throws IOException {
assert ord >= 0 && ord < values.getValueCount();
- currentOrd = (int) ord;
+ currentOrd = ord;
scratch.copyBytes(values.lookupOrd(currentOrd));
}
All options and trade-offs3 options · 290 words
One fix was built and validated; the alternatives are noted with why they are worse.
Store the ordinal as the long it is. Covers seekExact(long) and seekExact(term, TermState).
--- a/lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java
+++ b/lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java
@@ -66,7 +66,7 @@
@Override
public void seekExact(long ord) throws IOException {
assert ord >= 0 && ord < values.getValueCount();
- currentOrd = (int) ord;
+ currentOrd = ord;
scratch.copyBytes(values.lookupOrd(currentOrd));
}
Trade-offs
None found: below 2^31 the behaviour is identical, and it removes an instruction. The sibling SortedDocValuesTermsEnum keeps its cast, which is correct for int ordinals.
What the checks validated
- Scratch worktree of main
645d4fe14a20with the diff applied, gradle, OpenJDK 25.0.3 (2026-11-01): the minimal test passes with assertions on and off. - Closest core suites, seed
DEADBEEF(TestMultiDocValues,TestOrdinalMap,TestSortingCodecReader,TestDocValues,TestLucene90DocValuesFormat*): 7 classes, 526 tests, 55 skipped, 0 failures. TestSimpleTextDocValuesFormatin:lucene:codecs, whose sorted-set tests seek this very enum by ordinal: 148 tests, 10 skipped, 0 failures.:lucene:core:check -x test(formatting, ecj lint, forbidden APIs): passes.- Earlier, standalone: the minimal test on main
645d4fe14a20; the finder's 3 tests, an independent reproducer's 5 tests and itsOrdinalMaptest onea57360912ed; the 4.2.0 class with only the cast removed. All pass.
Throw instead of narrowing.
Trade-offs
Turns the silent wrong position into an exception, but invents a 2^31 limit the long-ordinal API does not have and still rejects valid seeks. Not recommended; not validated.
Give MultiSortedSetDocValues and SortingSortedSetDocValues their own enums.
Trade-offs
Leaves every application or downstream subclass that relies on the default enum broken, and is far more code than one token. Not recommended; not validated.
Downstream, by code reading only: Elasticsearch's composite aggregation (since 8.7.0) seeks this default enum by a global ordinal taken from the request, so on a shard with more than 2^31 distinct values of one keyword field such a request could fail, or past 2^32 walk about 2^32 extra terms before answering correctly.
Code reading. While writing a reference model of the doc-values terms-enum views for a small bounded-exhaustive test harness, an AI tool noticed that one method narrows the long ordinal to int; the harness itself (at most 4 values) cannot reach 2^31. A public-API JUnit test over a synthetic view with billions of values then reproduced it.
The method in full141 words
- The hunt unit checked that terms and postings cursors stay coherent after every move. It modelled the doc-values terms-enum views for a bounded-exhaustive translation-validation harness, with at most 4 values per view, so no tool could reach 2^31; the signal came from reading the types while writing the model.
- The finder then wrote a public-API JUnit over a synthetic
SortedSetDocValueswith 5 x 2^30 values that keeps the defaulttermsEnum(): 3 tests (a seek to 2^31 + 7, a seek to 2^32 + 5, a same-instanceTermStateround trip), all failing on main, 10.5.2 and 9.12.3 with assertions on and off, all passing with the cast removed. - An independent reviewer from another model family (OpenAI GPT-5.6) pointed out that from 2^32 the cast wraps to a non-negative wrong ordinal; that became the silent-case test, and later the one-method minimal repro.
No report found (searched 2026-10-26, re-checked 2026-11-01): 144 queries over GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream projects and the web; 23 hits, none the same bug. Closest: LUCENE-5767 (2014), a similar cast removed from OrdinalMap.
Searches4 searches
Closest items, none the same bug: #6829 / LUCENE-5767 (2014), which removed a similar "bogus cast" of a long ordinal in MultiDocValues.OrdinalMap (different code); #11636 / LUCENE-10600, where a maintainer states SORTED_SET can exceed Integer.MAX_VALUE unique values; #10368 / LUCENE-9328 ("cast is ok: single-valued cannot exceed Integer.MAX_VALUE", the reasoning for when such a cast is safe); #5830 / LUCENE-4765 and #5884 / LUCENE-4819, which created the class and made it the default termsEnum() (all comments read, nothing on ordinal width); #6635 / LUCENE-5573, the only issue naming the class (a class-loading deadlock). The Lucene.NET port has the same line (source code, not a report). On 2026-11-01 a read-only re-check found no new issue naming the class, no open PR touching it, and the cast still on upstream main, branch_11x and branch_10_6. Gaps: GitHub search reads at most 50 results per query and multi-word queries act like phrases; private security reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits and file contents (apache/lucene) | 2026-10-26, re-checked 2026-11-01 | two scripted searches (40 issue and PR, 6 commit queries) plus 21 manual queries: the class name, the cast fragment, open and all PRs by file name; git log -S over the full history | no report; #6635 is the only issue naming the class; no open PR changes the file |
| JIRA archive (LUCENE, SOLR) | 2026-10-26 | 8 scripted queries and 12 manual queries; LUCENE-4765, LUCENE-4819 and LUCENE-5767 read in full | no report; LUCENE-5767 fixed a similar cast elsewhere |
| Mailing lists (dev@, java-user@, issues@, commits@) | 2026-10-26 | 40 scripted queries | no report |
| Downstream projects and the web (Elasticsearch, OpenSearch, Solr, Lucene.NET) | 2026-10-26 | 4 downstream and 12 web queries, plus manual code searches | no report; the Lucene.NET port has the same line |
Introduced in 2013 with SORTED_SET doc values (LUCENE-4765, commit 2dcf80718c2) and never changed. First release 4.2.0; it fails on every version run from 4.2.0 to 10.6.0 and on main; 4.1.0 has no such API.
Versions tested7 rows
- Introduced:
2dcf80718c2"commit current state" (rmuir, 2013-02-11, branchlucene4765), part of LUCENE-4765 "Multi-valued docvalues field" (#5830; no PR, it predates GitHub). It created the class as a copy ofSortedDocValuesTermsEnum, widenedcurrentOrdtolongand kept the cast. Merged to trunk (b44aa618348) and branch_4x (bb1f4d0c61b) on 2013-02-20. - Reached users: in 4.2.0 as a public class with a public constructor; LUCENE-4819 (
b25cc528d9e, first in 4.2.1) made it whatSortedSetDocValues#termsEnum()returns. - Not a regression:
git blameassigns the line to the first commit, later edits ofseekExact(long)touched only the next line, and the cast is at all 120 release tags from 4.2.0 to 10.6.0.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails with assertions on and off (gradle and standalone); passes with the fix |
main ea57360912ed, a9779ef3c193 | affected | the finder's 3-test JUnit fails 3 of 3 |
| 10.6.0 | affected | the minimal test fails unchanged, assertions on and off (release jar) |
| 10.5.2, 9.12.3 | affected | the finder's and an independent reproducer's tests fail, assertions on and off |
| 10.0.0, 9.0.0, 8.11.4, 7.7.3, 7.0.0, 6.6.6, 5.5.5, 4.10.4, 4.9.0, 4.8.1, 4.2.1, 4.2.0 | affected | history probes fail 4 of 4 checks; controls below 2^31 pass (4.8.1 has no control run) |
| 4.2.0 with only the cast removed | not affected | passes 4 of 4 |
| 4.1.0 | API absent | no SortedSetDocValues (compile error) |
The finder, an independent reproducer, an intended-behaviour devil's advocate and the adjudication each reproduced it; an independent reviewer from another model family (OpenAI GPT-5.6) agreed on root cause and fix. Re-run on three main commits and on 10.6.0.
Checks and verdicts237 words
A second reviewer from the other model family, with read-only tools, returned a procedural "not real" ("It is not a substantive refutation"): it could not run Java, and its substantive answers agreed with the bug.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Spotted the cast while modelling the terms enums; its 3-test public-API JUnit fails on main, 10.5.2 and 9.12.3 with assertions on and off and passes with the fix. |
| cross-model review | real, high | OpenAI GPT-5.6: root cause agrees, fix fixes it, trigger legal; added the 2^32 silent-wrap case. |
| independent reproducer | real | Own 5-test JUnit fails 4 of 5 on main, 10.5.2 and 9.12.3; the control passes; the OrdinalMap and MultiSortedSetDocValues path fails too; all pass with the fix. |
| refute lens | not real, low (procedural) | OpenAI GPT-5.6 with read-only tools could not execute anything; root cause agrees, inputs in contract, intended-behaviour argument weak. |
| devil's advocate | real | Built and rejected the strongest works-as-designed case; re-ran the repro on main and 10.5.2. |
| adjudication | confirmed, high | Re-ran the independent reproduction's test with a fresh seed, assertions on and off: fails 4 of 5; passes 5 of 5 with the fix. Priority Low. |
| minimal repro and this doc | fails / fix passes | The one-method test fails on main 645d4fe14a20 (standalone and gradle, assertions on and off) and on 10.6.0; passes with the fix, as do the closest suites. |
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 454 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,408 words
- Registry title
- The default SortedSetDocValues terms enum casts the ordinal to int in seekExact(long) and seekExact(term, TermState): on a view with more than 2^31 values a seek by ordinal throws, and from 2^32 values on it can silently land on the wrong term
- Area
- index / sorted-set doc values terms enum (SortedSetDocValuesTermsEnum, the default SortedSetDocValues#termsEnum())
- Priority
- Low, rank 1201: The int cast in the sorted-set doc-values terms enum seek fails only past 2^31 distinct values and no in-tree caller seeks this way.
- Impact class
- silent wrong results
- Impact note
- Only code that seeks by ordinal (seekExact(long), or seekExact(term, TermState) with the enum's own state) on the default terms enum of a sorted-set doc-values view holding more than 2^31 distinct values is hit: Lucene's MultiDocValues.getSortedSetValues over a composite reader, SortingCodecReader's sorted-set view, SimpleText, or an application or downstream SortedSetDocValues subclass; no in-tree caller makes that call (per-segment default-codec doc values use their own long-capable enum), so it is latent for Lucene's own code; ordinals from 2^31 to 2^32-1 make the seek throw, and ordinals such as 2^32+5 silently land on a much smaller ordinal (5) with no error; default codec and settings, extreme scale, every release since 4.2.0.
- Summary
- You take the terms enum of a sorted-set doc-values view with more than 2^31 distinct values (for example MultiDocValues.getSortedSetValues over a very large multi-segment index), position it on a term with seekCeil, keep ord() or termState(), and later return with seekExact(ord) or seekExact(term, state); you expect to land on that term again, because sorted-set ordinals are longs and termsEnum() is documented to support ord() and seekExact(long). Instead seekExact(long) stores '(int) ord' (SortedSetDocValuesTermsEnum.java:69), so for ordinals from 2^31 to 2^32-1 it asks the values for a negative ordinal and throws, and for 2^32+5 it silently positions on ordinal 5 and returns that term, with -ea and -da. seekCeil, seekExact(BytesRef), next() and ord() use long arithmetic and are correct; the fix is to drop the cast.
- Introduced
- 2dcf80718c21548c85aa3e1c2283721242b11d04 (svn r1444647, branch lucene4765, 'commit current state', 2013-02-11), part of LUCENE-4765 'Multi-valued docvalues field' (now GitHub issue #5830; no pull request, the change predates GitHub). It created SortedSetDocValuesTermsEnum as a copy of SortedDocValuesTermsEnum and widened currentOrd from int to long, but kept 'currentOrd = (int) ord;' in seekExact(long). Reached trunk through the merge b44aa618348 (r1447999) and branch_4x through bb1f4d0c61b (r1448085), both 2013-02-20; the line never changed afterwards [read: git blame, git log -S and git log -L in the full-history clone; exec: 4.1.0 lacks the API, 4.2.0 fails, and the 4.2.0 class with only the cast removed passes] (2013-02-11 (branch commit); 2013-02-20 (merged to trunk and branch_4x))
- First affected version
- 4.2.0 [exec] (the first release with SortedSetDocValues; in 4.2.0 the enum is reached through its then-public constructor, and from 4.2.1 on, LUCENE-4819, it is what SortedSetDocValues#termsEnum() returns); 4.1.0 is not affected [exec: the API is absent, compile error]. Not a regression.
- Affected versions
- every release from 4.2.0 through 10.6.0, and main (ea57360912ed, a9779ef3c193 and 645d4fe14a20) [exec: the minimal test fails on 10.6.0 and main 645d4fe14a20 with -ea and -da; the follow-up's main() probes fail 4 of 4 checks on 4.2.0, 4.2.1, 4.8.1, 4.9.0, 4.10.4, 5.5.5, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (with -ea; also -da on 4.2.0 and main), and the controls below 2^31 pass; the finder's JUnit fails 3 of 3 on 9.12.3, 10.5.2 and main with -ea and -da; the independent reproduction's JUnit fails 4 of 5 on 9.12.3, 10.5.2 and main with -ea and -da] [read: the cast line is present at every release tag from 4.2.0 to 10.5.2 in the history clone; 8.8.2 to 8.11.x are not tagged there, 8.11.4 was executed]; unchanged on upstream main and branch_10x (2026-10-26) and on upstream main f815e679bf6c, branch_11x and branch_10_6 (2026-11-01) [read].
- 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
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26; with the finder's, the independent reproduction's and the intended-behaviour check's searches): two scripted searches (110 queries: 40 GitHub issue and PR, 6 commit, 4 downstream, 40 mailing-list over dev@, java-user@, issues@ and commits@, 8 JIRA-archive and 12 web queries), 21 manual GitHub, commit and contents queries (apache/lucene and the Elasticsearch, OpenSearch, Solr and Lucene.NET repositories; open and all PRs by file name), 12 manual JIRA queries (LUCENE, SOLR) and one git log -S over the full history: 144 queries, 23 hits, none the same bug. Not fixed upstream: line 69 still has the cast on upstream main and branch_10x (GitHub contents API); the newest commit touching the file is 2d6ad2fee6df (2020-12-23, LUCENE-9570 reformatting), and no commit ever removed the line. No open PR modifies SortedSetDocValuesTermsEnum.java (#464 only iterates the class with next(); #16869 touches SortedSetDocValues.java but not termsEnum()). Related, not the same bug: #6829 / LUCENE-5767 (commit 7ab10178c38, 2014, removed a similar 'bogus cast' of a long ordinal in MultiDocValues.OrdinalMap), #11636 / LUCENE-10600 (a maintainer, jpountz, states SORTED_SET can exceed Integer.MAX_VALUE unique values), #10368 / LUCENE-9328, #5830 / LUCENE-4765 and #5884 / LUCENE-4819 (created the class and made it the default termsEnum(); all comments read), #6635 / LUCENE-5573 (the only issue naming the class, a class-loading deadlock). Gaps: GitHub search reads at most 50 results per query and multi-word queries act like phrases (the class name and the cast fragment were searched verbatim); private security reports are invisible.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test with assertions on and off standalone (2026-10-29) and through gradle in a scratch worktree (2026-11-01); the fix in that worktree with the test, the closest core suites,TestSimpleTextDocValuesFormatand:lucene:core:check -x test. Release 10.6.0 (Maven Central jar): the same test, assertions on and off. Earlier runs: mainea57360912edanda9779ef3c193; 10.5.2 and 9.12.3 (assertions on and off); history probes on 4.2.0, 4.2.1, 4.8.1, 4.9.0, 4.10.4, 5.5.5, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0 and 10.0.0; 4.1.0 lacks the API. Read: upstream mainf815e679bf6c(2026-11-01),branch_11xandbranch_10_6still have the cast. All runs used OpenJDK 25.0.3.
- Finding record
lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java:26 (at ea57360912ed) ('private long currentOrd'), :36-63 (seekCeil and seekExact(BytesRef) take the long from lookupTerm), :66-71 (seekExact(long): assert on the long at :68, 'currentOrd = (int) ord;' at :69, lookupOrd(currentOrd) at :70), :73-81 (next() increments the long), :88-91 (ord() returns the long), :113-117 (seekExact(BytesRef, TermState) delegates to seekExact(long)), :119-124 (termState() saves the long ordinal)lucene/core/src/java/org/apache/lucene/index/SortedSetDocValues.java:43 (at ea57360912ed) (long nextOrd()), :60 (lookupOrd(long)), :62-68 (long getValueCount(), 'one plus the maximum ordinal'), :76-95 (lookupTerm in long arithmetic), :97-103 (termsEnum() javadoc 'The enum supports TermsEnum#ord() and TermsEnum#seekExact(long)' and the default new SortedSetDocValuesTermsEnum(this))lucene/core/src/java/org/apache/lucene/index/TermsEnum.java:91-95 (at ea57360912ed) (seekExact(long): 'Seeks to the specified term by ordinal (position) as previously returned by ord() ... must be within bounds'), :97-116 (seekExact(BytesRef, TermState) with a state from the same instance)lucene/core/src/java/org/apache/lucene/index/SortedDocValuesTermsEnum.java:26 (at ea57360912ed) ('private int currentOrd') and :69 (the same cast, correct there because sorted ordinals are ints)lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java:33-34 (at ea57360912ed) (@lucene.experimental, @lucene.internal), :640 (getSortedSetValues), :821-823 (MultiSortedSetDocValues, @lucene.internal, no termsEnum() override); lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesWriter.java:419 (SortingSortedSetDocValues, no termsEnum() override); lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:760-762 (getSortedSet returns it); lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:1812-1814 (TermsDict.seekExact(long) throws IndexOutOfBoundsException for a negative ordinal)
- The finding in 30 seconds
a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesTermsEnum.java:24-26, :66-71, :73-81, :88-91, :113-124a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedSetDocValues.java:43, :60, :62-68, :97-103a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedDocValuesTermsEnum.java:26, :69a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java:31-34, :640, :819-823; lucene/core/src/java/org/apache/lucene/index/SortedSetDocValuesWriter.java:419; lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:760-762; MultiDocValues.java:669-675 (null when no leaf has the field); SortingCodecReader.java:51 ('should only be used for merging')a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/FilterSortedSetDocValues.java:67-68; lucene/core/src/java/org/apache/lucene/index/SingletonSortedSetDocValues.java:118-119; lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java:769, :776-783 (single-valued fields as DocValues.singleton); lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextCodec.java:35; lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java:1240 (AssertingSortedSetDocValues, no termsEnum() override); lucene/join/src/java/org/apache/lucene/search/join/GenericTermsCollector.java:54 (verbose, no caller)a Lucene main checkout lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:1741-1744, :1812-1814; lucene/core/src/java/org/apache/lucene/codecs/DocValuesConsumer.java:1044-1049 (sorted-set merge view: MergedTermsEnum)
- Who is affected
a Lucene main checkout lucene/facet/src/java/org/apache/lucene/facet/sortedset/DefaultSortedSetDocValuesReaderState.java:95; lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java:95; lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreCollector.java:45a Lucene main checkout lucene/grouping/src/java/org/apache/lucene/search/grouping/TermGroupFacetCollectorManager.java:282-283, :290, :358-359 (int ordinals)
- Why it looks unintended
a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedSetDocValues.java:43, :60, :62-68, :97-103a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/TermsEnum.java:91-95, :97-116a Lucene main checkout lucene/test-framework/src/java/org/apache/lucene/tests/index/LegacyBaseDocValuesFormatTestCase.java:2261, :2311-2320, :3392, :3492
- History
GitHub contents API, read-only, 2026-11-01: upstream main f815e679bf6c, branch_11x, branch_10_6 SortedSetDocValuesTermsEnum.java:69
- TestSortedSetTermsEnumSeekExactLongOrd.java:
repro/LFV-449/src/org/apache/lucene/index/TestSortedSetTermsEnumSeekExactLongOrd.java(83 lines)
assets/LFV-449/bug.png(454 KB)
