All findings · Lucene finding · LFV-449 · sortedset-dv-termsenum-seekexact-ord-int-cast

The default SortedSetDocValues terms enum casts the ordinal to int in seekExact(long): a seek to an ordinal of 2^31 or more throws or silently lands on the wrong term

Sorted-set ordinals are long, but the default SortedSetDocValues terms enum stores (int) ord in seekExact(long): on a view with more than 2^31 distinct values a seek to an ordinal from 2^31 to 2^32 - 1 throws, and seekExact(4294967301) (2^32 + 5) silently lands on ordinal 5. No Lucene code seeks such a view with an ordinal that large.

Summary

Priority
Low, rank 1201The 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
silent wrong results
Since
4.2.0 (2013), since the class was createdintroduced 2013-02-11 · over 13 years
Affected
every release from 4.2.0 to 10.6.0, and main
Status
documented
Found by
code reading while modelling the doc-values terms enums, then a JUnit test

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background: sorted-set ordinals and the default terms enum

  1. 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.
  2. SortedSetDocValues#termsEnum() walks those values as a TermsEnum. You can jump to a value by ordinal with seekExact(long), or save a position with termState() and come back with seekExact(term, state).
  3. Per-segment doc values of the default codec bring their own enum. Views without one get the default, SortedSetDocValuesTermsEnum: the top-level MultiDocValues view over several segments, SortingCodecReader (meant for merging), SimpleText's multi-valued fields, or your own subclass.
The cast (int) ord in SortedSetDocValuesTermsEnum.seekExact(long) keeps only the low 32 bits of a 64-bit ordinal: 2,147,483,655 (0x00000000_80000007, 2^31 + 7) becomes -2,147,483,641 and the lookup of a negative ordinal throws; 4,294,967,301 (0x00000001_00000005, 2^32 + 5) becomes 5, so ord() returns 5 and term() the value of ordinal 5, with no error.
The cast at a glance: 2^31 + 7 keeps its low 32 bits as -2,147,483,641 and the lookup throws; 2^32 + 5 keeps 5, and the enum silently sits on ordinal 5. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Take a sorted-set doc-values view with 4,294,967,302 (2^32 + 6) distinct values that keeps the default termsEnum(): for example MultiDocValues.getSortedSetValues over a huge multi-segment index, or your own SortedSetDocValues subclass.
  2. Call te = values.termsEnum() and te.seekExact(4294967301L). Ordinals are long and termsEnum() is documented to support seekExact(long), so you expect te.ord() to be 4,294,967,301.
  3. Instead seekExact stores currentOrd = (int) ord, which keeps only the low 32 bits: 2^32 + 5 becomes 5. ord() returns 5 and term() the value of ordinal 5, with no error, assertions on or off.
  4. 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 saved termState() goes through the same line and fails the same way.
  5. Everything else in the enum (seekCeil, seekExact(BytesRef), next(), ord()) uses the long and is right. Dropping the cast fixes it.

Who is affected

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.

The code (main 645d4fe14a20)

// 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
}

What the cast does

ordinalhex (64-bit)(int) ordresult
2^31 - 1 = 2,147,483,6470x00000000_7FFFFFFF2,147,483,647correct
2^31 + 7 = 2,147,483,6550x00000000_80000007-2,147,483,641lookup of a negative ordinal throws
2^32 - 1 = 4,294,967,2950x00000000_FFFFFFFF-1throws
2^32 + 5 = 4,294,967,3010x00000001_000000055silently 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.

Why the cast is there

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.

Which views use this enum

  • MultiDocValues.MultiSortedSetDocValues (@lucene.internal), returned by MultiDocValues.getSortedSetValues for a reader with more than one segment when at least one segment has the field (otherwise it returns null); its ordinals are global ordinals from an OrdinalMap, so the bound is on distinct values across all segments. Executed by an independent check over a real OrdinalMap with two synthetic segments: fails, passes with the fix.
  • SortedSetDocValuesWriter.SortingSortedSetDocValues, returned by SortingCodecReader's sorted-set producer. SortingCodecReader's javadoc says it "should only be used for merging", and merging walks these values with next().
  • 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.AssertingSortedSetDocValues wrapper, the unused debugging helper GenericTermsCollector.verbose in join, and any application or downstream SortedSetDocValues subclass that does not override termsEnum(). FilterSortedSetDocValues and SingletonSortedSetDocValues delegate 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 the long. 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 than Integer.MAX_VALUE values up front; grouping's term facets seek SortedSetDocValues#termsEnum() by ordinal, but narrow the value count to int and seek with int ordinals, 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_VALUE unique 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 asks lookupOrd for 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 ordinal ord mod 2^32 and its term, and next() 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.

Minimal repro

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().

TestSortedSetTermsEnumSeekExactLongOrd.java83 lines
/*
 * 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());
  }
}
Command
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 after seekExact(4294967301), and term() is that ordinal's value.
Observed
expected:<4294967301> but was:<5>: the enum sits on ordinal 5, with no exception, assertions on or off.

All files of this repro: repro/LFV-449/

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 from ord(), but it is exactly what ord() reports for that term: on the same values, te.seekCeil(values.lookupOrd(ord)) returns FOUND with ord() 4,294,967,301, and seekExact with that ord() value, or with te.termState() after moving away, gives ord() 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 TestMultiDocValues or in a new TestSortedSetDocValuesTermsEnum. It allocates nothing big, so it needs no @Monster annotation. It passes with the fix.
  • Classes used. Nothing package-private; the term encoding uses BitUtil.VH_BE_LONG, and BitUtil is public but marked @lucene.internal.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also fails unchanged on 10.6.0
jdkOpenJDK 25.0.3
osLinux, aarch64
Command (environment)
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)

Observed output

# 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: -2147483641

Why it looks unintended

  • SortedSetDocValues#termsEnum() promises ord() and seekExact(long); ordinals are long, and even an ordinal that ord() reported lands on the wrong term.
  • Every other method of the enum uses its long field; the cast is left over from copying the int-ordinal sibling in 2013.
  • Maintainers removed the same kind of "bogus cast" in OrdinalMap in 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 and getValueCount() are long ("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() and termState() use the long field, and OrdTermState.ord is a long. The cast is the only narrowing.
  • Upstream tests exercise this method. LegacyBaseDocValuesFormatTestCase.testSortedSetTermsEnum and testSortedSetEnumAdvanceIndependently (through its helper) seek by ordinal on multi-valued documents, and they run through TestSimpleTextDocValuesFormat, 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) ord to ord in the global-ordinals code of MultiDocValues.
  • 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 int arrays.
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.

Suggested fix

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.

Drop the cast (validated by the checks) (recommended)

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 645d4fe14a20 with 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.
  • TestSimpleTextDocValuesFormat in :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 its OrdinalMap test on ea57360912ed; the 4.2.0 class with only the cast removed. All pass.

Make the limit explicit with Math.toIntExact(ord)

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.

Override termsEnum() in the affected in-tree views

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 note

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.

How it was found

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 SortedSetDocValues with 5 x 2^30 values that keeps the default termsEnum(): 3 tests (a seek to 2^31 + 7, a seek to 2^32 + 5, a same-instance TermState round 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits and file contents (apache/lucene)2026-10-26, re-checked 2026-11-01two 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 historyno report; #6635 is the only issue naming the class; no open PR changes the file
JIRA archive (LUCENE, SOLR)2026-10-268 scripted queries and 12 manual queries; LUCENE-4765, LUCENE-4819 and LUCENE-5767 read in fullno report; LUCENE-5767 fixed a similar cast elsewhere
Mailing lists (dev@, java-user@, issues@, commits@)2026-10-2640 scripted queriesno report
Downstream projects and the web (Elasticsearch, OpenSearch, Solr, Lucene.NET)2026-10-264 downstream and 12 web queries, plus manual code searchesno report; the Lucene.NET port has the same line

History

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, branch lucene4765), part of LUCENE-4765 "Multi-valued docvalues field" (#5830; no PR, it predates GitHub). It created the class as a copy of SortedDocValuesTermsEnum, widened currentOrd to long and 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 what SortedSetDocValues#termsEnum() returns.
  • Not a regression: git blame assigns the line to the first commit, later edits of seekExact(long) touched only the next line, and the cast is at all 120 release tags from 4.2.0 to 10.6.0.
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails with assertions on and off (gradle and standalone); passes with the fix
main ea57360912ed, a9779ef3c193 affectedthe finder's 3-test JUnit fails 3 of 3
10.6.0 affectedthe minimal test fails unchanged, assertions on and off (release jar)
10.5.2, 9.12.3 affectedthe 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 affectedhistory 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 affectedpasses 4 of 4
4.1.0 API absentno SortedSetDocValues (compile error)

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder reproducedSpotted 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, highOpenAI GPT-5.6: root cause agrees, fix fixes it, trigger legal; added the 2^32 silent-wrap case.
independent reproducer realOwn 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 realBuilt and rejected the strongest works-as-designed case; re-ran the repro on main and 10.5.2.
adjudication confirmed, highRe-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 passesThe 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.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,408 words

Finding record

LFV-449, snapshot of 2026-11-06

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, TestSimpleTextDocValuesFormat and :lucene:core:check -x test. Release 10.6.0 (Maven Central jar): the same test, assertions on and off. Earlier runs: main ea57360912ed and a9779ef3c193; 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 main f815e679bf6c (2026-11-01), branch_11x and branch_10_6 still have the cast. All runs used OpenJDK 25.0.3.

Lucene and upstream sources

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-124
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedSetDocValues.java:43, :60, :62-68, :97-103
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/SortedDocValuesTermsEnum.java:26, :69
  • a 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:45
  • a 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-103
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/TermsEnum.java:91-95, :97-116
  • a 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

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.