All findings · Lucene finding · LFV-122 · sortednumericsortfield-javadoc-string-missing

SortedNumericSortField's javadoc on main suggests STRING_FIRST / STRING_LAST for missing values, but a numeric sort cannot use them

On main, SortedNumericSortField's javadoc says missing values sort first or last via SortField.STRING_FIRST / STRING_LAST, but a numeric sort cannot use these string markers: it accepts them, then the first search or setIndexSort throws ClassCastException. Only the sentence is wrong; a numeric sort takes a number such as Long.MAX_VALUE.

Summary

Priority
Low, rank 1269SortedNumericSortField's main-branch javadoc suggests STRING_FIRST/STRING_LAST, which throws ClassCastException on first use, a loud doc error in unreleased code.
Impact
minor
Since
main only, #15486 (2025-12-09)introduced 2025-12-09
Affected
main (unreleased 11.0.0); no release; documentation only
Status
documented
Found by
non-formal: code reading while writing a bounded-exhaustive sort 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

The 5-argument constructor stores any Object as the missing value (SortedNumericSortField.java:99-114): SortField checks the missing value only for Type.STRING, and this class passes Type.CUSTOM. The first search or index sort then casts it to Integer, Long, Float or Double (:316, :383 for LONG), and a marker is a plain Object.

Background: missing values in Lucene sorts

  1. A sort's missing value says where documents without a value in the sort field go.
  2. String sorts (SortField with Type.STRING, SortedSetSortField) take one of two markers: SortField.STRING_FIRST or SortField.STRING_LAST.
  3. Numeric sorts take a number of the sort's type that such documents sort as: Long.MIN_VALUE puts them first and Long.MAX_VALUE last (the other way round in reverse order); without one they sort as 0.
  4. On main, SortField.setMissingValue is gone (#15486): the missing value is a constructor argument of type Object.
Diagram of the bug in four steps: the user follows the class javadoc and builds new SortedNumericSortField("f", LONG, false, MIN, SortField.STRING_LAST); the constructor accepts it, because SortField checks missing values only for Type.STRING and this class passes Type.CUSTOM; getComparator (search) and getIndexSorter (index sort) cast it with (Long) missingValue and throw ClassCastException; the fix corrects the javadoc to a missing value of the numeric type, e.g. Long.MAX_VALUE for last, optionally rejecting others with IllegalArgumentException. Expected: the document without a value sorts last; actual: ClassCastException at first use.
The javadoc points a numeric sort at the string marker STRING_LAST; the constructor stores it, and the first search or index sort casts it with (Long) missingValue and throws. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. One document has no value in the LONG field f, and you want such documents to sort last. The class javadoc says: "this also supports sorting missing values as first or last, via SortField#STRING_FIRST or SortField#STRING_LAST".
  2. So you build new SortedNumericSortField("f", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, SortField.STRING_LAST). The constructor accepts it.
  3. You expect searcher.search(MatchAllDocsQuery.INSTANCE, 1, new Sort(sortField)) to return the document, sorted last, or the constructor to reject the marker.
  4. Instead the search throws ClassCastException: class org.apache.lucene.search.SortField$2 cannot be cast to class java.lang.Long, because the LONG comparator casts the missing value to Long. IndexWriterConfig.setIndexSort with the same sort fails the same way, and so do all 4 numeric types with both markers.
  5. What works is a number of the sort's type: Long.MAX_VALUE puts missing documents last, Long.MIN_VALUE first, as the migration guide and the upstream tests do. Releases through 10.6.0 are unaffected: their javadoc points to setMissingValue(Object).

Who is affected

Only users of unreleased main (11.0.0-SNAPSHOT) who follow this sentence for a numeric SortedNumericSortField (INT, LONG, FLOAT or DOUBLE), at search time or as an index sort. With otherwise default settings, at any index size, they get an immediate ClassCastException, before any hit is collected, so no wrongly ordered result comes back.

Mechanism, terms and numbers543 words

On main, the class javadoc of SortedNumericSortField says: "Like sorting by string, this also supports sorting missing values as first or last, via {@link SortField#STRING_FIRST} or {@link SortField#STRING_LAST}." Those two constants are markers for string sorts. A numeric SortedNumericSortField (INT, LONG, FLOAT or DOUBLE) accepts them without complaint, because its 5-argument constructor stores any Object as the missing value, and then the first search or index-sort setup casts the value to Integer, Long, Float or Double and throws ClassCastException.

The code is correct and intended: a numeric sort takes a number of its own type as the missing value (Long.MIN_VALUE / Long.MAX_VALUE to sort missing documents first / last), as the migration guide and the upstream tests show. The sentence came in with #15486, which removed setMissingValue and replaced the old pointer "via setMissingValue(Object)" with the two string markers in both SortedNumericSortField and SortedSetSortField; that is correct only for SortedSetSortField. Main only: no release has the sentence. The proposal is a javadoc fix, optionally with a constructor type check that fails fast.

The mechanism (main 645d4fe14a20)

  1. The javadoc names the string markers. SortedNumericSortField.java:44-45: "Like sorting by string, this also supports sorting missing values as first or last, via {@link SortField#STRING_FIRST} or {@link SortField#STRING_LAST}." The constructor's @param missingValue says only "the sort value to use for documents with no value in the field" (:97).
  2. No type check on the way in. The 5-argument constructor calls super(field, SortField.Type.CUSTOM, reverse, missingValue) and stores the object (:99-114). SortField.validateField checks the missing value only for Type.STRING (SortField.java:337-349, the check at :343-348).
  3. The markers are plain objects. SortField.java:309-325: STRING_FIRST and STRING_LAST are anonymous Object instances (hence SortField$1 / SortField$2 in the message), documented as "Pass this as a missingValue constructor parameter to have missing string values sort first/last".
  4. First use casts to the numeric type. getComparator passes (Integer) missingValue, (Float), (Long) or (Double) to the comparator (:272, :294, :316, :338); getIndexSorter does the same (:380, :383, :386, :389); IndexWriterConfig.setIndexSort calls getIndexSorter() on every sort field (IndexWriterConfig.java:505-510). So the index sort fails at configuration time, before any document is indexed.
  5. What a numeric missing value means. The comparators substitute it for absent values and use 0 when it is null (comparators/LongComparator.java:37, likewise for Int, Float and Double), so Long.MIN_VALUE / Long.MAX_VALUE sort missing documents first / last and reverse flips that (upstream TestIndexSorting.testMissingMultiValuedLongFirst runs both directions).
  6. Why the same sentence is right in SortedSetSortField. That class reads the missing value only as one of the two markers (SortedSetSortField.java:128-130, :176, :191).
  7. Type-safe entry points exist. LongField.newSortField(field, reverse, selector, long missingValue) (document/LongField.java:186-189) and its Int, Float and Double counterparts take a primitive, so they cannot get the wrong type.

What is not claimed

The late ClassCastException for a wrong-typed missing value is old and general: it was executed on 9.12.3 and 10.3.2 (through setMissingValue(STRING_LAST)) and on 10.4.0 and 10.5.2 (through the 5-argument constructor), and plain new SortField(f, SortField.Type.LONG, false, SortField.STRING_LAST) behaves the same on main and 10.5.2. On those releases the javadoc did not suggest the markers. Only the sentence is new.

Exposure and likelihood183 words
  • Who. Users of main who read this class javadoc to sort missing values first or last on a numeric SortedNumericSortField.
  • Default vs opt-in. The input shape is opt-in (a string marker as the missing value of a numeric sort) and only this sentence suggests it; everything else is default: IndexWriterConfig, codec and IndexSearcher, assertions on or off. The cast fails before any doc values are read, so no codec or setting avoids it.
  • What goes wrong. An immediate ClassCastException on each search with that sort, or from IndexWriterConfig.setIndexSort before an IndexWriter exists. Nothing hangs and nothing is written; the exception comes before any hit is collected, so no wrongly ordered result is returned.
  • Scale. Any index size; one document is enough.
  • Why fix it before 11.0. setMissingValue is gone on main, so a SortedNumericSortField gets its missing value only at construction: through this 5-argument constructor, whose class javadoc is where users look for how, or through typed helpers such as LongField.newSortField(field, reverse, selector, long missingValue), which cannot take a marker. 11.0.0 would be the first release with the sentence.

Minimal repro

One JUnit test, one method, fixed seed: one document with no value, ByteBuffersDirectory, new IndexWriterConfig() (the codec does not matter), a plain IndexSearcher. It asserts what the javadoc promises: STRING_LAST either works or is rejected by the constructor (an IllegalArgumentException counts as a pass, so it also tests the optional check).

TestSortedNumericSortFieldStringLast.java52 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.search;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestSortedNumericSortFieldStringLast extends LuceneTestCase {

  public void testStringLastMissingValue() throws Exception {
    SortField sortField;
    try {
      sortField =
          new SortedNumericSortField(
              "f", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, SortField.STRING_LAST);
    } catch (IllegalArgumentException rejected) {
      return;
    }
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
      w.addDocument(new Document());
      try (DirectoryReader reader = DirectoryReader.open(w)) {
        new IndexSearcher(reader).search(MatchAllDocsQuery.INSTANCE, 1, new Sort(sortField));
      } catch (ClassCastException e) {
        throw new AssertionError(
            "class javadoc: STRING_LAST sorts missing values last (or the constructor should"
                + " reject it); actual: "
                + e,
            e);
      }
    }
  }
}
Command
cp $REPRO/TestSortedNumericSortFieldStringLast.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.search.TestSortedNumericSortFieldStringLast -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
Expected
The document without a value is returned, sorted last, or the constructor rejects STRING_LAST with IllegalArgumentException.
Observed
IndexSearcher.search throws ClassCastException: class org.apache.lucene.search.SortField$2 cannot be cast to class java.lang.Long at SortedNumericSortField.getComparator(SortedNumericSortField.java:315), which the test reports as an AssertionError.

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

More files, environment, full output327 words
  • Reproducible. One document, no random input on the failing path. The test framework randomizes the codec, locale and time zone from the seed (the gradle run used Asserting(Lucene104)), but the cast fails in getComparator before any doc values are read, so the outcome does not depend on them. Run with seed DEADBEEF (and 5EED1234 standalone), assertions on and off: same failure.
  • It transcribes the current javadoc. With option A (javadoc only) it keeps failing (executed) and becomes obsolete, because the documented contract changes. With option B it passes unchanged, so it is the regression test for B.
  • Format. google-java-format (./gradlew tidy) wraps the constructor call over five lines (whitespace only).
  • Index-sort path. new IndexWriterConfig().setIndexSort(new Sort(sortField)) throws the same exception from getIndexSorter (SortedNumericSortField.java:382) <- IndexWriterConfig.setIndexSort (IndexWriterConfig.java:507), before any document is indexed (executed on 645d4fe14a20 by the finder's two-method test, 2026-10-29); the minimal test keeps only the search path.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT)
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp $REPRO/TestSortedNumericSortFieldStringLast.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.search.TestSortedNumericSortFieldStringLast -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true
TestSortedNumericSortFieldStringLast > testStringLastMissingValue FAILED
    java.lang.AssertionError: class javadoc: STRING_LAST sorts missing values last (or the constructor should reject it); actual: java.lang.ClassCastException: class org.apache.lucene.search.SortField$2 cannot be cast to class java.lang.Long (org.apache.lucene.search.SortField$2 is in unnamed module of loader 'app'; java.lang.Long is in module java.base of loader 'bootstrap')
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:F511CF93BA407CCF]:0)
        at org.apache.lucene.search.TestSortedNumericSortFieldStringLast.testStringLastMissingValue(TestSortedNumericSortFieldStringLast.java:44)
        Caused by:
        java.lang.ClassCastException: class org.apache.lucene.search.SortField$2 cannot be cast to class java.lang.Long (...)
            at org.apache.lucene.search.SortedNumericSortField.getComparator(SortedNumericSortField.java:315)
            at org.apache.lucene.search.FieldValueHitQueue.create(FieldValueHitQueue.java:146)
            at org.apache.lucene.search.TopFieldCollectorManager.newCollector(TopFieldCollectorManager.java:114)
            at org.apache.lucene.search.TopFieldCollectorManager.newCollector(TopFieldCollectorManager.java:29)
            at org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:752)
            at org.apache.lucene.search.IndexSearcher.searchAfter(IndexSearcher.java:735)
            at org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:678)
            at org.apache.lucene.search.TestSortedNumericSortFieldStringLast.testStringLastMissingValue(TestSortedNumericSortFieldStringLast.java:42)
1 test completed, 1 failed
# -Ptests.asserts=false: the same failure, same frames
# option A (javadoc only, reworded): checkGoogleJavaFormat and renderJavadoc pass; the test still fails (executed; comment-only change)
# option B (javadoc + constructor check): the test passes, assertions on and off; TestSortedNumericSortField 9/9, TestIndexSorting 62/62 (first run: 184 core + 42 queries tests)

Why it looks unintended

  • The class's own javadoc prescribes the markers, through its public constructor and constants.
  • An accidental edit: #15486 replaced "via setMissingValue(Object)" with the string markers in both SortedSetSortField (correct there) and this class; nobody discussed it.
  • Everything else says numeric: the markers' javadoc ("missing string values"), the migration guide (0L), the upstream tests (Integer.MIN_VALUE / MAX_VALUE).
The full argument173 words
  • Contract. SortedNumericSortField.java:44-45 (quoted above). A user who follows it violates no precondition stated on the class.
  • History. The sentence dates from LUCENE-5748 (2014) with "via {@link #setMissingValue(Object)}": 398af4596bd on trunk (first released in 6.0.0) and its branch_4x counterpart d1522d5fe78 (first released in 4.9.0). #15486 (f4f6d70c0c4, 2025-12-09) changed that one javadoc line and removed the setMissingValue override in this class; the constructor, getComparator, getIndexSorter and the serialization were not touched. The javadoc was not discussed in #15480, #15483, #15486 or #15720.
  • Every other source says numeric. SortField.java:309, :318 (the markers are for "missing string values"); lucene/MIGRATE.md:258-278, which shows new SortedNumericSortField("price", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, 0L) (:273-274) and STRING_LAST only for SortedSetSortField (:276-277); TestSortedNumericSortField.testMissingFirst / testMissingLast with Integer.MIN_VALUE / Integer.MAX_VALUE (TestSortedNumericSortField.java:158, :194); no upstream test pairs the markers with SortedNumericSortField.
  • A sibling class states the rule. ToParentBlockJoinSortField (join module): "For numeric types use the corresponding boxed type (e.g. {@link Integer} for Type.INT)" (ToParentBlockJoinSortField.java:139-142), and it validates that (:166-198, #15727).
The devil's-advocate check113 words

The strongest case for "intended": numeric sorts take a number, the markers are documented for strings, a wrong-typed Object missing value has always failed late (executed on 9.12.3, 10.3.2, 10.4.0 and 10.5.2), and plain SortField behaves the same, so this is user error.

Why it fails: it defends the code, not the sentence. The javadoc of the very class being called tells users to pass the markers, so following the documentation produces the exception. That is why this is a javadoc defect, and why a constructor type check is an optional usability improvement, not a regression fix. (Such a check would be new here: this class's own setMissingValue override never validated the value.)

Suggested fix

Recommended A. Correct the class javadoc (recommended)

Correct the class javadoc: a numeric sort takes a missing value of its own boxed type, for example Long.MIN_VALUE / Long.MAX_VALUE to sort missing documents first / last, and the string markers cannot be used here. No behaviour change. Validated: the format check and the javadoc render pass, and the new text links resolve.

- * <p>Like sorting by string, this also supports sorting missing values as first or last, via {@link
- * SortField#STRING_FIRST} or {@link SortField#STRING_LAST}.
+ * <p>Documents without a value sort as the {@code missingValue} passed to the constructor, or as 0
+ * when it is null. It must match {@code type}: an {@link Integer} for {@code Type.INT}, a {@link
+ * Long} for {@code Type.LONG}, a {@link Float} for {@code Type.FLOAT} or a {@link Double} for
+ * {@code Type.DOUBLE}. To sort them first or last, pass a value that sorts before or after every
+ * value in the field, for example {@link Long#MIN_VALUE} or {@link Long#MAX_VALUE} (the other way
+ * round when {@code reverse} is true). {@link SortField#STRING_FIRST} and {@link
+ * SortField#STRING_LAST} cannot be used here.
All options and trade-offs3 options ยท 988 words

Three options. A and B were applied in a scratch worktree of main 645d4fe14a20 and run with gradle; C was not built. A's wording was revised after the fact checks (the first draft was too broad) and both were re-run with it. A is the proposal, because the code is intended and only the sentence is wrong; B is a compatible extra that turns the late ClassCastException into an early, clear error.

A. Correct the class javadoc (recommended) (validated by the checks)

Replace the sentence (SortedNumericSortField.java:44-45) with the numeric rule, naming the four supported types. Optionally extend the constructor's @param missingValue (:97) the same way (not run).

- * <p>Like sorting by string, this also supports sorting missing values as first or last, via {@link
- * SortField#STRING_FIRST} or {@link SortField#STRING_LAST}.
+ * <p>Documents without a value sort as the {@code missingValue} passed to the constructor, or as 0
+ * when it is null. It must match {@code type}: an {@link Integer} for {@code Type.INT}, a {@link
+ * Long} for {@code Type.LONG}, a {@link Float} for {@code Type.FLOAT} or a {@link Double} for
+ * {@code Type.DOUBLE}. To sort them first or last, pass a value that sorts before or after every
+ * value in the field, for example {@link Long#MIN_VALUE} or {@link Long#MAX_VALUE} (the other way
+ * round when {@code reverse} is true). {@link SortField#STRING_FIRST} and {@link
+ * SortField#STRING_LAST} cannot be used here.

Trade-offs

  • No behaviour change; it states what the code, the migration guide and the upstream tests already do, close to the wording ToParentBlockJoinSortField uses for its numeric missing values. It does not catch a wrong-typed value early: a marker still fails at first use, now against clear documentation.
  • It names only the four types the class supports (any other type throws AssertionError in getComparator / getIndexSorter, :361-362, :396-397), and it does not call the markers "for string sorts only": BinarySortField takes them too (BinarySortField.java:31-40, :95-97), as does SortField with Type.STRING_VAL.
  • A document whose value equals the chosen value sorts together with the missing documents (ties fall back to the next sort field, then the doc id). For FLOAT and DOUBLE the comparators use Float.compare / Double.compare, which order NaN after positive infinity, so POSITIVE_INFINITY does not sort missing documents after NaN values.

What the checks validated

  • Gradle, fresh scratch worktree of 645d4fe14a20 (2026-11-01, after the fact checks): :lucene:core:applyGoogleJavaFormat produced the wrapping shown; checkGoogleJavaFormat and renderJavadoc pass; the rendered paragraph's links to Integer, Long, Float, Double, Long.MIN_VALUE, Long.MAX_VALUE, SortField.STRING_FIRST and SortField.STRING_LAST resolve.
  • With the change applied, the minimal test still fails with the same ClassCastException (executed, seed DEADBEEF, assertions on), as expected for a comment-only change: it transcribes the old sentence and becomes obsolete; no test is needed for this change.
  • The brief's first draft of this text ("must be of the boxed type that matches type", markers "for string sorts such as SortedSetSortField only") was too broad for the reasons above and is replaced.

B. A plus a constructor type check (optional extra) (validated by the checks)

In the 5-argument constructor, for the four numeric types, reject a non-null missing value whose class does not match the type, with IllegalArgumentException, as ToParentBlockJoinSortField does for its child missing value (#15727).

     if (type == null) {
       throw new NullPointerException();
     }
+    if (missingValue != null) {
+      Class<?> expected =
+          switch (type) {
+            case INT -> Integer.class;
+            case LONG -> Long.class;
+            case FLOAT -> Float.class;
+            case DOUBLE -> Double.class;
+            // $CASES-OMITTED$
+            default -> null;
+          };
+      if (expected != null && expected.isInstance(missingValue) == false) {
+        throw new IllegalArgumentException(
+            "Missing value for Type."
+                + type
+                + " must be a "
+                + expected.getName()
+                + ", got "
+                + missingValue);
+      }
+    }
     this.selector = selector;
     this.type = type;
   }

Trade-offs

  • For INT, LONG, FLOAT and DOUBLE, moves the failure of a wrong-typed missing value from the first search or setIndexSort to construction, with a clear message. It checks only those four: for any other type the default branch yields null, no check runs, and the sort still fails at first use with the AssertionError of getComparator / getIndexSorter. A wrong-typed value always failed at first use already, so only a sort that is built but never used would newly fail; still worth a CHANGES entry.
  • Every in-tree caller passes the matching type (LongField.newSortField and its siblings, the MultiValued*FieldSource classes, the tests), and the core and queries tests below pass.
  • Plain new SortField(f, SortField.Type.LONG, false, SortField.STRING_LAST) has the same late failure and is not covered; a general check would belong in SortField.validateField (a wider change, not run).

What the checks validated

  • Re-run with the reworded javadoc (the check code is unchanged) in the fresh scratch worktree (2026-11-01): checkGoogleJavaFormat, ecjLintMain (executed, not up to date) and renderJavadoc pass on :lucene:core; the minimal test passes with assertions on and off (the constructor rejects STRING_LAST); TestSortedNumericSortField (9) and TestIndexSorting (62) pass, assertions on.
  • The brief's first run, with the first-draft javadoc and the same check code, seed DEADBEEF, assertions on: 9 core suites, 184 tests, 0 failures (TestSortedNumericSortField (9), TestIndexSorting (62), TestIndexSortSortedNumericDocValuesRangeQuery (29), TestDocValuesQueries (41), TestSortedNumericDocValuesUpdates (19), TestLucene99SegmentInfoFormat (17), TestSortingCodecReader (3), TestSegmentReordering (3) and the minimal test (1)), plus :lucene:queries TestValueSources (41) and TestDocValuesFieldSources (1).

C. Make the markers work for numeric sorts (not recommended)

Translate STRING_FIRST / STRING_LAST in the constructor into the type's extreme value, taking reverse into account, so that the current sentence becomes true.

Trade-offs

  • A new feature rather than a fix: getMissingValue, equals, toString and serialization would see the translated number, floats and doubles need a choice (infinities), and real extreme values tie with missing documents.
  • Overlaps the typed newSortField helpers on IntField / LongField / FloatField / DoubleField. Not built or run.

Downstream note

Products that offer a "missing first / last" option on numeric fields should map it to a typed number such as Long.MAX_VALUE, not to the string markers; the Solr migration to Lucene 11 already passes numeric values.

How it was found

Non-formal. While writing a bounded-exhaustive test for the sorting and top-field-collector unit, the finder read the unit's contract lines, which included this javadoc sentence. A first probe on main showed the ClassCastException for search and index sort, then a public-API JUnit test confirmed it.

The method in full106 words
  • Lead. The unit's list of contract lines for SortedNumericSortField named STRING_FIRST / STRING_LAST for missing values, taken from the class javadoc.
  • Probe. A small program built the sort with STRING_LAST and ran a search and an index sort: both threw ClassCastException (SortField$2 cannot be cast to Long).
  • JUnit. A public-API test failed 2 of 2 on main with assertions on and off, and the same way on 10.5.2 (behaviour only: there the javadoc points to setMissingValue).
  • Verification. A cross-model review (OpenAI GPT-5.6), an independent reproducer, a cross-model adversary, an intended-behaviour devil's advocate and an adjudication followed; all classify it as a documentation defect.

Prior reports, history and checks

Prior reports

No matching public report found in the documented searches (2026-10-26, 161 queries: apache/lucene issues and PRs incl. the JIRA archive, a full-text grep of every issue and PR updated since 2025-12-01, all open PRs' changed files, the mailing lists, downstream projects and the web; rechecked 2026-11-01). Upstream main still has the sentence.

Searches5 searches

Read, not this bug: #15486 (introduced the sentence); #15483 (added the missing-value constructors); #15480 (open umbrella "Make SortField immutable", which says constructor missing values "could also enhance type safety"); #15720 (closed duplicate deprecation PR); #15727 / #15548 (per-type missing-value check in ToParentBlockJoinSortField, a precedent for the optional check); #8004 (LUCENE-6947, origin of the SortField.setMissingValue type check that #15486 removed); #11247 (a different index-sort defect); apache/solr#4651 (Solr's Lucene 11 migration passes numeric missing values). Two 2016 downstream tickets (HSEARCH-2332, HSEARCH-2482) noticed the old late ClassCastException for wrong-typed numeric missing values. Limits: GitHub search indexes STRING_FIRST / STRING_LAST poorly (covered by the local grep); review-comment diff hunks and private channels were not searched.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene, incl. migrated JIRA)2026-10-26scripted prior-report search first pass (29 queries) plus GraphQL search variants: class names, the markers, the exception text, javadoc, validation, immutability; the threads of #15480, #15483, #15486, #15720no report
Every issue (364) and PR (1,288) updated since 2025-12-012026-10-26dump of titles, bodies, comments and reviews, grepped for the markers, the class name, the sentence and SortField$2no item names the markers or the sentence
Open PRs2026-10-26 and 2026-11-01changed files of all 413 open PRs (the 4 touching the sort-field files read); 2026-11-01: open PRs matching the class namenone changes the sentence or adds missing-value validation
Mailing lists, downstream projects, web2026-10-26dev@, java-user@, issues@, commits@; Elasticsearch, OpenSearch, Solr; general webno report; a 2022 java-user thread explains numeric missing first/last via MIN_VALUE / MAX_VALUE
GitHub recheck2026-11-0111 searches: class name, markers, exception, "missing value javadoc", items updated since 2026-10-26, open PRs on the classnothing new

History

Introduced on main by #15486 (f4f6d70c0c4, 2025-12-09), which removed setMissingValue and rewrote the pointer. No release has the sentence: 10.6.0 and earlier say "via setMissingValue(Object)". The accept-then-fail behaviour itself is old (executed on 9.12.3 to 10.5.2), so the commit changed the text, not the behaviour.

Versions tested5 rows
  • Introduced: f4f6d70c0c4 "Remove deprecated SortField.setMissingValue method (#15486)" (romseygeek, merged to main 2025-12-09, no backport). In the full history, git log -S 'STRING_FIRST} or' main -- lucene/core/src/java/org/apache/lucene/search/SortedNumericSortField.java finds only this commit (without the path it also finds #15727 in ToParentBlockJoinSortField and older SortedSetSortField commits). Its diff in SortedNumericSortField.java: the javadoc line plus the removed non-validating setMissingValue override.
  • Earlier pieces: the sentence dates from LUCENE-5748 (2014), pointing to setMissingValue(Object): 398af4596bd on trunk (first released in 6.0.0) and d1522d5fe78 on branch_4x (first released in 4.9.0); #15483 (10.4.0) added the 5-argument constructor without changing the javadoc; #16756 (main) added the MIGRATE.md example with 0L.
  • Upstream now: main 2f424fd428d (2026-10-30) still has the sentence and the same casts (read, not executed).
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, gradle and standalone, assertions on and off
main ea57360912ed affectedfinder and verifier tests fail, assertions on and off; 16/16 type x marker x entry-point combinations throw
10.6.0, branch_10_6 javadocjavadoc says "via setMissingValue(Object)" (read; neither contains the commit; branch_10_6 at 7e5aec4370e, 2026-10-29). branch_10x had the same text on 2026-10-26 and was decommissioned on 2026-10-29 (#16911)
10.4.0, 10.5.2 old behavioursame accept-then-ClassCastException, but the javadoc does not suggest the markers (executed)
9.12.3, 10.3.2 old behavioursetMissingValue(STRING_LAST) accepted, then ClassCastException (executed); no 5-argument constructor

Independent checks

Every check agrees: real, a documentation defect. The finder, a cross-model review (OpenAI GPT-5.6), an independent reproducer, a cross-model adversary, an intended-behaviour devil's advocate and the adjudication; a re-verification on 645d4fe14a20 and this brief's gradle run reproduce it.

Checks and verdicts334 words

The study ran on main ea57360912ed; SortedNumericSortField.java there differs from 645d4fe14a20 only in readSortField's default branch (#16919), which shifts the line numbers by 2.

Each independent check and its verdict
CheckVerdictWhat it found
finder (a hunt unit) candidateProbe plus public-API JUnit: 2 of 2 fail on main with assertions on and off; 10.5.2 behaves the same.
cross-model review, intended lens real, highOpenAI GPT-5.6: its own tests fail on main and 10.5.2; the fix sketch passes them and upstream TestSortedNumericSortField 9 of 9.
independent reproducer real, doc8 of 8 type x marker combinations accepted, then throw; the numeric control passes; fix sketch passes 4 of 4.
cross-model adversary real, highRated the intended case strong for the code, but it does not defeat the explicit javadoc; 16 of 16 combinations throw.
devil's advocate real, doc onlyThe intended case covers the code, not the sentence.
adjudication confirmed, high, doc, minorRe-ran the independent reproduction's test with assertions on and off (4 run, 3 failed each).
re-verification on 645d4fe (2026-10-29) still reproducesFinder test 2 of 2, the independent reproduction 3 of 4; the rebased fix sketch applies cleanly and the independent reproduction passes 4 of 4.
this brief (2026-11-01) reproduces; fix validatedGradle in a scratch worktree of 645d4fe14a20: the minimal test fails with assertions on and off; option A passes the format check and the javadoc render; with option B the minimal test passes (assertions on and off) and 184 core and 42 queries tests pass.
fact checks and diagram check (2026-11-01) bug confirmed; wording fixedAn independent fact check and a cross-model fact check (OpenAI GPT-5.6) confirmed the bug and found wording errors only: the history command's path restriction, the test framework's codec randomization, the 4.9.0 commit, the active branch_10_6, and option A's first-draft wording and option B's scope (both re-run). The diagram check found one missing brace in the diagram's step 6, now re-rendered.

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,239 words

Finding record

LFV-122, snapshot of 2026-11-06

Registry title
SortedNumericSortField's class javadoc on main says missing values sort first or last via SortField.STRING_FIRST / STRING_LAST, but a numeric sort cannot use these string sentinels: the constructor accepts them, then every search or index sort with them throws ClassCastException
Area
search / sorting (SortedNumericSortField missing values; javadoc)
Priority
Low, rank 1269: SortedNumericSortField's main-branch javadoc suggests STRING_FIRST/STRING_LAST, which throws ClassCastException on first use, a loud doc error in unreleased code.
Impact class
minor
Impact note
Only users of unreleased main (11.0.0-SNAPSHOT) who follow this one javadoc sentence and pass SortField.STRING_FIRST or STRING_LAST as the missing value of a numeric SortedNumericSortField (INT, LONG, FLOAT or DOUBLE), with the default IndexWriterConfig, codec and searcher, -ea or -da, at any index size: they get an immediate ClassCastException on the first search with that sort (getComparator) or when they set it as an index sort (IndexWriterConfig.setIndexSort, before any document is indexed); results are never wrong, and the code behaves as designed, only the documentation is wrong.
Summary
You follow the SortedNumericSortField class javadoc on main ('Like sorting by string, this also supports sorting missing values as first or last, via SortField#STRING_FIRST or SortField#STRING_LAST') and build new SortedNumericSortField("f", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, SortField.STRING_LAST). You expect documents without a value to sort last, or, if the sentinels are not supported, the constructor to reject them. Instead the constructor accepts the sentinel, and IndexSearcher.search with that sort, or IndexWriterConfig.setIndexSort with it, throws ClassCastException 'SortField$2 cannot be cast to class java.lang.Long', because a numeric sort needs a missing value of its own numeric type (for example Long.MAX_VALUE to sort missing documents last), as MIGRATE.md and the upstream tests show.
Introduced
f4f6d70c0c4 (apache/lucene#15486 'Remove deprecated SortField.setMissingValue method', by romseygeek, main only, no backport) [read: git log -S 'STRING_FIRST} or' in the full-history clone returns only this commit; its diff replaces '{@link #setMissingValue(Object)}' with '{@link SortField#STRING_FIRST} or {@link SortField#STRING_LAST}' in the class javadocs of both SortedNumericSortField and SortedSetSortField, which is correct only for SortedSetSortField; exec: the finder repro fails the same way on 10.4.0 and 10.5.2 (before the commit, where the javadoc points to setMissingValue) and on main (after it), so the commit changed the text, not the behaviour; not executed at commit granularity, because the diff has no executable change on the repro's path]; the sentence itself dates from LUCENE-5748 (2014-06-12): 398af4596bd on trunk, first released in 6.0.0, and its branch_4x counterpart d1522d5fe78, first released in 4.9.0, where it pointed to setMissingValue(Object) and was accurate as a pointer (2025-12-09 (f4f6d70c0c4 merged to main))
First affected version
unreleased (main only, 11.0.0-SNAPSHOT; no release tag contains f4f6d70c0c4, so it would first ship in 11.0.0 unless fixed)
Affected versions
main from f4f6d70c0c4 (2025-12-09) through 645d4fe14a2 [exec: the finder repro fails 2/2 with -ea and -da on ea57360912ed and on 645d4fe14a2 (2026-10-29); the independent reproduction, the cross-model review, the intended-behaviour check and the adjudication reproduced it independently; read: upstream main still carries the sentence on 2026-10-26]; not affected, because the javadoc says 'via setMissingValue(Object)': every release through 10.5.2 [read: all 67 releases/lucene/* and 72 releases/lucene-solr/* tags that have the file lack the sentence; the checked sources jars 9.12.3, 10.3.2, 10.4.0 and 10.5.2 say 'via setMissingValue(Object)'] and branch_10_6 (10.6.0, released 2026-10-29) and the unreleased branch_10x [read on GitHub, 2026-10-26]; the underlying accept-then-ClassCastException behaviour for a wrong-typed missing value is older and is not part of this claim [exec: 10.4.0 and 10.5.2 via the 5-argument constructor; 9.12.3 and 10.3.2 via setMissingValue(STRING_LAST); read: present in the code since LUCENE-5748, 4.9.0]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, 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: 161 queries, including a 29-query first pass of the scripted prior-report search, over apache/lucene issues and PRs (search, plus a local grep of a dump of all 364 issues and 1288 PRs updated since 2025-12-01 with bodies, comments and reviews), git history, the changed-file lists of all 413 open PRs (the 4 that touch the sort-field files read), the dev@, java-user@, issues@ and commits@ lists, the JIRA archive, Elasticsearch, OpenSearch and Solr, and the web; 22 hits read, none of them this bug); upstream main still carries the sentence, branch_10x and branch_10_6 do not, and no open PR changes it or adds missing-value validation to SortedNumericSortField; related prior items, none of them this bug: #15486 (introduced the sentence), #15483 (added the missing-value constructors), #15480 (open umbrella 'Make SortField immutable'), #15720 (closed duplicate deprecation PR), #15727 / #15548 (added per-type missing-value validation to ToParentBlockJoinSortField, a precedent for the optional fix), #8004 (LUCENE-6947, origin of SortField.setMissingValue's type check, which #15486 removed), #11247 (LUCENE-10210, a different index-sort defect), apache/solr#4651 (passes numeric missing values in its Lucene 11 migration), HSEARCH-2332 / HSEARCH-2482 (2016 downstream notes on the old late ClassCastException for wrong-typed missing values)
Lucene commit tested
main 645d4fe14a20 (2026-10-28), through gradle in a scratch worktree (2026-11-01): the minimal test fails on stock main with assertions on and off; with the javadoc fix (reworded after the fact checks and re-run) the format check and the javadoc render pass and the minimal test still fails; with the javadoc fix plus the constructor check ecjLintMain passes, the minimal test passes with assertions on and off, and the related core and queries suites pass (184 core and 42 queries tests in the first run). Also standalone on 645d4fe14a20 (2026-10-29: minimal test, and the finder's index-sort test) and, by the study's verifiers, on ea57360912ed; the same accept-then-fail behaviour (without the misleading sentence) on 10.5.2, 10.4.0, 10.3.2 and 9.12.3.

Lucene and upstream sources

Finding record
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/SortedNumericSortField.java:44-45 (class javadoc sentence), :97 (@param missingValue), :99-114 (5-argument constructor, no type check), :258-340 (getComparator, casts at :270/:292/:314/:336), :374-387 (getIndexSorter casts), :128-156 (readSortField builds numeric missing values only)
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/SortField.java:309-325 (STRING_FIRST/STRING_LAST documented for missing string values), :336-349 (validateField checks the missing value only for Type.STRING)
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/SortedSetSortField.java:41-42 (same sentence, correct there), :74-81 (constructor, no validation), :128-130, :176, :191 (missing value read only as STRING_FIRST/STRING_LAST)
  • a Lucene main checkout lucene/MIGRATE.md:258-278 (SortedNumericSortField example with missing value 0L; STRING_LAST only for SortedSetSortField)
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:505-510 (setIndexSort calls getIndexSorter)
  • a Lucene main checkout lucene/core/src/test/org/apache/lucene/search/TestSortedNumericSortField.java:137-206 (testMissingFirst/testMissingLast use Integer.MIN_VALUE/MAX_VALUE)
  • lucene/core/src/java/org/apache/lucene/search/SortedNumericSortField.java on main 645d4fe14a2: :44-45 (javadoc sentence), :99-114 (constructor), :128-158 (readSortField; default branch :153-155 throws IllegalStateException since #16919), :260-342 (getComparator, casts :272/:294/:316/:338), :376-389 (getIndexSorter)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/search/SortedNumericSortField.java:44-45, 97, 99-114, 260-368 (casts :272/:294/:316/:338), 376-389 (casts :380/:383/:386/:389) (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/SortField.java:309-325, 337-349 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/SortedSetSortField.java:41-42, 74-81, 128-130, 176, 191 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:505-510; lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:37; lucene/core/src/java/org/apache/lucene/document/LongField.java:178-189 (at 645d4fe14a20)
  • lucene/core/src/test/org/apache/lucene/index/TestIndexSorting.java:709-764 (at 645d4fe14a20)
Who is affected
  • lucene/core/src/java/org/apache/lucene/document/LongField.java:186-189 (typed helper, primitive missing value) (at 645d4fe14a20)
Why it looks unintended
  • lucene/MIGRATE.md:258-278; lucene/core/src/test/org/apache/lucene/search/TestSortedNumericSortField.java:137-206 (at 645d4fe14a20)
  • lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinSortField.java:139-142, 166-198 (at 645d4fe14a20)
Suggested fix
  • lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinSortField.java:139-142, 166-198; lucene/core/src/java/org/apache/lucene/search/BinarySortField.java:31-40, 95-97; lucene/core/src/java/org/apache/lucene/search/SortedNumericSortField.java:355-362, 390-397; lucene/core/src/java/org/apache/lucene/search/comparators/FloatComparator.java:43, DoubleComparator.java:43; lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/MultiValuedIntFieldSource.java:40-58 (at 645d4fe14a20)
History
  • a Lucene clone: git show releases/lucene/10.6.0 and origin/main (2f424fd428d) SortedNumericSortField.java:44-45; git merge-base --is-ancestor f4f6d70c0c4 releases/lucene/10.6.0 (false); origin/branch_10_6 (7e5aec4370e) SortedNumericSortField.java:44-45 and is-ancestor (false); origin/branch_10x 48cf3f515d6 (#16911, decommission) and 657014b3c57 (2026-10-26)

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.