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.
- A sort's missing value says where documents without a value in the sort field go.
- String sorts (
SortFieldwithType.STRING,SortedSetSortField) take one of two markers:SortField.STRING_FIRSTorSortField.STRING_LAST. - Numeric sorts take a number of the sort's type that such documents sort as:
Long.MIN_VALUEputs them first andLong.MAX_VALUElast (the other way round in reverse order); without one they sort as 0. - On main,
SortField.setMissingValueis gone (#15486): the missing value is a constructor argument of typeObject.
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.- 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". - So you build
new SortedNumericSortField("f", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, SortField.STRING_LAST). The constructor accepts it. - You expect
searcher.search(MatchAllDocsQuery.INSTANCE, 1, new Sort(sortField))to return the document, sorted last, or the constructor to reject the marker. - 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 toLong.IndexWriterConfig.setIndexSortwith the same sort fails the same way, and so do all 4 numeric types with both markers. - What works is a number of the sort's type:
Long.MAX_VALUEputs missing documents last,Long.MIN_VALUEfirst, as the migration guide and the upstream tests do. Releases through 10.6.0 are unaffected: their javadoc points tosetMissingValue(Object).
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 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 missingValuesays only "the sort value to use for documents with no value in the field" (:97). - 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.validateFieldchecks the missing value only forType.STRING(SortField.java:337-349, the check at:343-348). - The markers are plain objects.
SortField.java:309-325:STRING_FIRSTandSTRING_LASTare anonymousObjectinstances (henceSortField$1/SortField$2in the message), documented as "Pass this as a missingValue constructor parameter to have missing string values sort first/last". - First use casts to the numeric type.
getComparatorpasses(Integer) missingValue,(Float),(Long)or(Double)to the comparator (:272,:294,:316,:338);getIndexSorterdoes the same (:380,:383,:386,:389);IndexWriterConfig.setIndexSortcallsgetIndexSorter()on every sort field (IndexWriterConfig.java:505-510). So the index sort fails at configuration time, before any document is indexed. - 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), soLong.MIN_VALUE/Long.MAX_VALUEsort missing documents first / last andreverseflips that (upstreamTestIndexSorting.testMissingMultiValuedLongFirstruns both directions). - 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). - 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.
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 andIndexSearcher, 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
ClassCastExceptionon each search with that sort, or fromIndexWriterConfig.setIndexSortbefore anIndexWriterexists. 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.
setMissingValueis gone on main, so aSortedNumericSortFieldgets 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 asLongField.newSortField(field, reverse, selector, long missingValue), which cannot take a marker. 11.0.0 would be the first release with the sentence.
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).
/*
* 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);
}
}
}
}
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_LASTwithIllegalArgumentException. - Observed
IndexSearcher.searchthrowsClassCastException: class org.apache.lucene.search.SortField$2 cannot be cast to class java.lang.LongatSortedNumericSortField.getComparator(SortedNumericSortField.java:315), which the test reports as anAssertionError.
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 ingetComparatorbefore any doc values are read, so the outcome does not depend on them. Run with seedDEADBEEF(and5EED1234standalone), 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 fromgetIndexSorter(SortedNumericSortField.java:382) <-IndexWriterConfig.setIndexSort(IndexWriterConfig.java:507), before any document is indexed (executed on645d4fe14a20by the finder's two-method test, 2026-10-29); the minimal test keeps only the search path.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
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)
# 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)- 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)}":
398af4596bdon trunk (first released in 6.0.0) and itsbranch_4xcounterpartd1522d5fe78(first released in 4.9.0). #15486 (f4f6d70c0c4, 2025-12-09) changed that one javadoc line and removed thesetMissingValueoverride in this class; the constructor,getComparator,getIndexSorterand 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 showsnew SortedNumericSortField("price", SortField.Type.LONG, false, SortedNumericSelector.Type.MIN, 0L)(:273-274) andSTRING_LASTonly forSortedSetSortField(:276-277);TestSortedNumericSortField.testMissingFirst/testMissingLastwithInteger.MIN_VALUE/Integer.MAX_VALUE(TestSortedNumericSortField.java:158,:194); no upstream test pairs the markers withSortedNumericSortField. - 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.)
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.
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
ToParentBlockJoinSortFielduses 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
typethrowsAssertionErroringetComparator/getIndexSorter,:361-362,:396-397), and it does not call the markers "for string sorts only":BinarySortFieldtakes them too (BinarySortField.java:31-40,:95-97), as doesSortFieldwithType.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, soPOSITIVE_INFINITYdoes 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:applyGoogleJavaFormatproduced the wrapping shown;checkGoogleJavaFormatandrenderJavadocpass; the rendered paragraph's links toInteger,Long,Float,Double,Long.MIN_VALUE,Long.MAX_VALUE,SortField.STRING_FIRSTandSortField.STRING_LASTresolve. - With the change applied, the minimal test still fails with the same
ClassCastException(executed, seedDEADBEEF, 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.
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
setIndexSortto construction, with a clear message. It checks only those four: for any othertypethedefaultbranch yieldsnull, no check runs, and the sort still fails at first use with theAssertionErrorofgetComparator/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.newSortFieldand its siblings, theMultiValued*FieldSourceclasses, 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 inSortField.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) andrenderJavadocpass on:lucene:core; the minimal test passes with assertions on and off (the constructor rejectsSTRING_LAST);TestSortedNumericSortField(9) andTestIndexSorting(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:queriesTestValueSources(41) andTestDocValuesFieldSources(1).
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,toStringand serialization would see the translated number, floats and doubles need a choice (infinities), and real extreme values tie with missing documents. - Overlaps the typed
newSortFieldhelpers onIntField/LongField/FloatField/DoubleField. Not built or run.
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.
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
SortedNumericSortFieldnamedSTRING_FIRST/STRING_LASTfor missing values, taken from the class javadoc. - Probe. A small program built the sort with
STRING_LASTand ran a search and an index sort: both threwClassCastException(SortField$2cannot be cast toLong). - 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene, incl. migrated JIRA) | 2026-10-26 | scripted 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, #15720 | no report |
| Every issue (364) and PR (1,288) updated since 2025-12-01 | 2026-10-26 | dump of titles, bodies, comments and reviews, grepped for the markers, the class name, the sentence and SortField$2 | no item names the markers or the sentence |
| Open PRs | 2026-10-26 and 2026-11-01 | changed files of all 413 open PRs (the 4 touching the sort-field files read); 2026-11-01: open PRs matching the class name | none changes the sentence or adds missing-value validation |
| Mailing lists, downstream projects, web | 2026-10-26 | dev@, java-user@, issues@, commits@; Elasticsearch, OpenSearch, Solr; general web | no report; a 2022 java-user thread explains numeric missing first/last via MIN_VALUE / MAX_VALUE |
| GitHub recheck | 2026-11-01 | 11 searches: class name, markers, exception, "missing value javadoc", items updated since 2026-10-26, open PRs on the class | nothing new |
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.javafinds only this commit (without the path it also finds #15727 inToParentBlockJoinSortFieldand olderSortedSetSortFieldcommits). Its diff inSortedNumericSortField.java: the javadoc line plus the removed non-validatingsetMissingValueoverride. - Earlier pieces: the sentence dates from LUCENE-5748 (2014), pointing to
setMissingValue(Object):398af4596bdon trunk (first released in 6.0.0) andd1522d5fe78onbranch_4x(first released in 4.9.0); #15483 (10.4.0) added the 5-argument constructor without changing the javadoc; #16756 (main) added theMIGRATE.mdexample with0L. - Upstream now: main
2f424fd428d(2026-10-30) still has the sentence and the same casts (read, not executed).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, gradle and standalone, assertions on and off |
main ea57360912ed | affected | finder and verifier tests fail, assertions on and off; 16/16 type x marker x entry-point combinations throw |
| 10.6.0, branch_10_6 | javadoc | javadoc 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 behaviour | same accept-then-ClassCastException, but the javadoc does not suggest the markers (executed) |
| 9.12.3, 10.3.2 | old behaviour | setMissingValue(STRING_LAST) accepted, then ClassCastException (executed); no 5-argument constructor |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder (a hunt unit) | candidate | Probe 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, high | OpenAI 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, doc | 8 of 8 type x marker combinations accepted, then throw; the numeric control passes; fix sketch passes 4 of 4. |
| cross-model adversary | real, high | Rated 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 only | The intended case covers the code, not the sentence. |
| adjudication | confirmed, high, doc, minor | Re-ran the independent reproduction's test with assertions on and off (4 run, 3 failed each). |
| re-verification on 645d4fe (2026-10-29) | still reproduces | Finder 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 validated | Gradle 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 fixed | An 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. |
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 409 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,239 words
- 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 checkecjLintMainpasses, 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 on645d4fe14a20(2026-10-29: minimal test, and the finder's index-sort test) and, by the study's verifiers, onea57360912ed; the same accept-then-fail behaviour (without the misleading sentence) on 10.5.2, 10.4.0, 10.3.2 and 9.12.3.
- 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)
- TestSortedNumericSortFieldStringLast.java:
repro/LFV-122/src/org/apache/lucene/search/TestSortedNumericSortFieldStringLast.java(52 lines)
assets/LFV-122/bug.png(409 KB)
