To decide whether a segment is sorted by the queried field, the range query takes the segment's primary sort field and compares names with sf.getField().equals(field). Sort.getPrimarySortField returns a nameless field (DOC or SCORE) on purpose, so the comparison dereferences null instead of skipping the shortcut.
- A segment can be sorted by a field, its index sort. A reader reports it through
LeafReader.getMetaData().sort();nullmeans the documents are in no particular order. - Since 10.0.0, doc-values range queries on a field with a skip index use it: if the segment is sorted by the query's field, the matches form one run of doc ids, found without a scan.
Sort.INDEXORDERmeans sorted by document id. Its only sort field,SortField.FIELD_DOC, has no field name:getField()returnsnull(so doesFIELD_SCORE).
![A flow diagram on a light card: a doc-values range query for f in ["b", "c"] reaches a guard, outlined in red as the place of the bug, that asks whether the leaf is sorted by f first; the yes branch shows a leaf sorted by f whose matching documents form one run of doc IDs that the shortcut finds with the skip index, the no branch shows an unsorted leaf whose matches are scattered and are found by the normal path, and a bottom panel shows a leaf that reports Sort.INDEXORDER, whose first sort field has a null name, with the expected result (the guard says no and the normal path returns 2 hits) beside the actual result (the guard calls .equals on the null name and throws NullPointerException).](../assets/LFV-1205/concept.png)
Sort.INDEXORDER. The range query asks Sort.getPrimarySortField for the primary sort field, gets FIELD_DOC with getField() == null, and getField().equals("f") throws; with the null-safe comparison the shortcut is skipped and the count is 2. Select the still to open it at full size. Generated with AI, checked against the code.- Index 3 documents in one segment with
SortedDocValuesField.indexedField("f", ...): valuesa,b,c. - Wrap the segment in a
FilterLeafReaderwhosegetMetaData()reportsSort.INDEXORDER: documents are in index order, which is true for any segment. - Count
SortedDocValuesField.newSlowRangeQuery("f", b, c, true, true)on it. You expect 2 (bandc), as on the unwrapped segment. - Instead it throws
NullPointerException, with assertions on and off. The query's index-sort shortcut asksSort.getPrimarySortFieldfor the primary sort field, getsSortField.FIELD_DOC, whosegetField()is null, and callsgetField().equals("f")on it instead of skipping the shortcut. - The same call is in the numeric doc-values range query and in the query
LongField.newRangeQueryreturns.IndexWriterrefuses DOC and SCORE index sorts, so in practice only an application's own reader gets there.
In practice only applications whose own LeafReader reports an index sort starting with a DOC or SCORE field, such as Sort.INDEXORDER, through the public LeafMetaData constructor. IndexWriter rejects DOC and SCORE index sorts, so indexes written with Lucene's own sort fields are never affected. The query then fails loudly: no wrong hits, no index change.
Mechanism, terms and numbers598 words
Since 10.0.0, doc-values range queries on a skip-indexed, single-valued field have a shortcut for segments sorted by the queried field. To decide, they ask Sort.getPrimarySortField(reader) for the segment's primary index sort field and compare its name with the query's field, sf.getField().equals(field). getPrimarySortField returns a sort field without a name on purpose ("Custom field that we don't know anything about, so return it as primary"), and SortField.FIELD_DOC and FIELD_SCORE have no name. So on a leaf whose reader reports Sort.INDEXORDER (a true statement about any segment) the comparison throws NullPointerException instead of skipping the shortcut, and IndexSearcher.search and count fail. The same unguarded comparison is in SortedNumericDocValuesRangeQuery and in three places of IndexSortSortedNumericDocValuesRangeQuery. IndexWriter rejects DOC and SCORE index sorts and Lucene's readers report one only through a merge-only expert wrapper, so in practice only applications with their own LeafReader are hit; the failure is loud and no wrong hits are returned.
- The shortcut is tried for a field with a doc-values skip index and single-valued sorted values (
document/SortedSetDocValuesRangeQuery.java:121-131, conditionsingleton != null && skipper != nullat:124).SortedDocValuesField.newSlowRangeQueryandnewSlowExactQuerybuild this query (document/SortedDocValuesField.java:96-116). - It asks for the primary sort field:
canUsePrimarySortShortcutcallsSort.getPrimarySortField(reader)(:288-290). - The helper returns a nameless field on purpose.
getPrimarySortField(search/Sort.java:145-148) usesprimarySortFieldIndex(:154-183), which skips sort fields that are no-ops in the segment but returns a field with no name at once:if (field == null) { // Custom field that we don't know anything about, so return it as primary(:162-164).Sort.INDEXORDER = new Sort(SortField.FIELD_DOC)(:44),FIELD_DOC = new SortField(null, Type.DOC)(search/SortField.java:124), andgetField()"Could return null if the sort is by SCORE or DOC" (:380-386). - The caller dereferences it (the fix site).
if (sf == null || sf.getField().equals(field) == false)(:291) throws, although the method's javadoc says it returns "nullif the shortcut cannot be applied" (:280-287). - Twin sites, same expression.
document/SortedNumericDocValuesRangeQuery.java:240(reached from:141-147, for example byNumericDocValuesField.newSlowRangeQueryon anindexedField), andsearch/IndexSortSortedNumericDocValuesRangeQuery.java(reached byLongField.newRangeQueryandIntField.newRangeQuery,document/LongField.java:138-146, with no skip index needed) at:433(ingetDocIdSetIteratorOrNullFromBkd, reached fromcountand from the scorer path; the executed throw site),:206(incount, when the field's points do not cover every document) and:536(ingetDocIdSetIteratorOrNull, after:433has already been reached, so it never throws first; fixed for consistency). - The other operations tested on that leaf work.
TermQuery,FieldExistsQuery, sorted searches (by a string field, a long field andSort.INDEXORDER, including their early-termination check) andIndexWriter.addIndexesall accept a leaf that reportsSort.INDEXORDER; of the operations tested, only the range-query shortcuts throw.
- The repro: 3 documents (
a,b,c), rangeb..c. The unwrapped segment counts 2 (the diagram probe, same documents); the wrapped one throws atSortedSetDocValuesRangeQuery.java:291. - A fix-check probe (10 documents, values 0..9, range 2..5) on the same kind of leaf: all three query classes throw, for
searchand forcount(:291,SortedNumericDocValuesRangeQuery:240,IndexSortSortedNumericDocValuesRangeQuery:433); the unwrapped segment gives 4 hits each. - A leaf that reports
new Sort(SortField.FIELD_SCORE)throws the same way, but a score is not a document order, so it is not the headline case. - By reading only (not executed): since 10.6.0 a leading sort field that is a no-op in the segment and is followed by
FIELD_DOCalso reaches the dereference; 10.0.0 to 10.5.2 compared onlygetSort()[0].
Exposure and likelihood297 words
- Who. Applications with their own
LeafReader(aFilterLeafReader, aFilterCodecReaderor a custom reader) whosegetMetaData()reports an index sort whose first effective field isFIELD_DOC(such asSort.INDEXORDER) orFIELD_SCORE, built with the publicLeafMetaDataconstructor ("Expert: Sole constructor. Public for use by custom LeafReader impls."; the record is@lucene.experimental). - Queries hit. Executed:
SortedDocValuesField.newSlowRangeQueryandnewSlowExactQueryandNumericDocValuesField.newSlowRangeQueryon skip-indexed single-valued fields, andLongField.newRangeQueryon aLongFieldthat is single-valued in the segment (for a multi-valued one onlycountcan reach the call, when the points cover every document; by reading). By reading:SortedSetDocValuesField(single-valued in the segment) andIntField. - Default vs opt-in. Not reachable in normal use of
IndexWriteror Lucene's readers:IndexWriterConfig.setIndexSort(Sort.INDEXORDER)throws "Cannot sort index with sort field <doc>", andSorterrejects such fields too. Two contrived in-tree routes exist, and the claim rests on neither. The expertSortingCodecReader.wrap(reader, null, sort)reports the caller's sort without checking it and gives the same exception (executed), but its javadoc reserves it for merging. And, by reading only (not executed),setIndexSortchecks nothing but the sort field'sIndexSorter, so an application-definedSortFieldsubclass with no field name, its ownIndexSorterand its own registered sort-field provider would pass that check and be stored in the segment; Lucene's own sort-field providers all read a field name, so this too needs application code. - Effect. An unchecked
NullPointerExceptionfromIndexSearcher.searchorcountfor that query; no wrong or missing hits returned silently, no index change. - Scale. Any size; 3 documents suffice.
- Likelihood: contrived. An application has to advertise a DOC or SCORE sort as its leaves' index sort (rather than
nullor the wrapped reader's metadata) and then run one of these range queries; no public report of it was found since 9.5.0.
One JUnit test method on the public API: 3 documents in one segment, a FilterLeafReader that reports Sort.INDEXORDER, and a count of newSlowRangeQuery("f", b, c). Deterministic and single-threaded; it fails with assertions on and off.
/*
* 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.document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.FilterLeafReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafMetaData;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Sort;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;
public class TestDocValuesRangeQueryIndexOrderSort extends LuceneTestCase {
public void testRangeQueryOnIndexOrderSortedLeaf() throws Exception {
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w =
new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
for (String v : new String[] {"a", "b", "c"}) {
Document doc = new Document();
doc.add(SortedDocValuesField.indexedField("f", new BytesRef(v)));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(w)) {
LeafReader leaf = reader.leaves().get(0).reader();
LeafMetaData m = leaf.getMetaData();
LeafMetaData indexOrder =
new LeafMetaData(m.createdVersionMajor(), m.minVersion(), Sort.INDEXORDER, false);
LeafReader wrapped =
new FilterLeafReader(leaf) {
@Override
public LeafMetaData getMetaData() {
return indexOrder;
}
@Override
public CacheHelper getCoreCacheHelper() {
return null;
}
@Override
public CacheHelper getReaderCacheHelper() {
return null;
}
};
// Expected: 2 hits (b, c), as on the unwrapped leaf; actual: NullPointerException.
assertEquals(
2,
new IndexSearcher(wrapped)
.count(
SortedDocValuesField.newSlowRangeQuery(
"f", new BytesRef("b"), new BytesRef("c"), true, true)));
}
}
}
}
cp $REPRO/TestDocValuesRangeQueryIndexOrderSort.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDocValuesRangeQueryIndexOrderSort -Ptests.seed=1205DEAD
# and with -Ptests.asserts=false: the same failure
- Expected
countreturns 2 (documentsbandc), as on the unwrapped segment.- Observed
NullPointerException: Cannot invoke "String.equals(Object)" because the return value of "org.apache.lucene.search.SortField.getField()" is nullatSortedSetDocValuesRangeQuery.canUsePrimarySortShortcut(SortedSetDocValuesRangeQuery.java:291).
More files, environment, full output176 words
TestDocValuesRangeQueryIndexOrderSort (one method, extends LuceneTestCase, package org.apache.lucene.document of the lucene/core tests). The writer uses the default codec explicitly (TestUtil.getDefaultCodec()), so the doc-values skip index exists whatever codec the seed draws; it failed on every recorded seed. The wrapper returns the segment's own metadata with only the sort replaced, and disables caching (null cache helpers), as a FilterLeafReader that changes metadata must.
As a regression test the same method fits in TestDocValuesQueries.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 10.0.0 and 10.6.0 jars: fails; 10.0.0 to 10.6.0 (13 releases): the hunt's equivalent test fails; 9.12.3: does not compile (no skip-indexed doc values) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (nothing platform-specific) |
cp $REPRO/TestDocValuesRangeQueryIndexOrderSort.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDocValuesRangeQueryIndexOrderSort -Ptests.seed=1205DEAD
# and with -Ptests.asserts=false: the same failure
TestDocValuesRangeQueryIndexOrderSort > testRangeQueryOnIndexOrderSortedLeaf FAILED
java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because the return value of "org.apache.lucene.search.SortField.getField()" is null
at __randomizedtesting.SeedInfo.seed([1205DEAD:FA19303DE5B1D211]:0)
at org.apache.lucene.document.SortedSetDocValuesRangeQuery.canUsePrimarySortShortcut(SortedSetDocValuesRangeQuery.java:291)
at org.apache.lucene.document.SortedSetDocValuesRangeQuery$1.scorerSupplier(SortedSetDocValuesRangeQuery.java:126)
at org.apache.lucene.search.LRUQueryCache$CachingWrapperWeight.scorerSupplier(LRUQueryCache.java:830)
at org.apache.lucene.search.IndexSearcher.searchLeaf(IndexSearcher.java:842)
at org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:814)
...
at org.apache.lucene.search.IndexSearcher.count(IndexSearcher.java:528)
at org.apache.lucene.document.TestDocValuesRangeQueryIndexOrderSort.testRangeQueryOnIndexOrderSortedLeaf(TestDocValuesRangeQueryIndexOrderSort.java:70)
1 test completed, 1 failed- The shortcut's javadoc says it returns
nullwhen it cannot apply; theLongFieldrange query promises a fallback. Sort.getPrimarySortFieldreturns the nameless field on purpose, andSortField.getField()documentsnullfor DOC and SCORE.- Index order is a true description of any segment, and the other queries and sorted searches tested accept the same leaf.
The full argument220 words
- The callers' own contracts.
canUsePrimarySortShortcutreturns "the primary sort field if it is safe to use the ... range shortcuts for this query, ornullif the shortcut cannot be applied" (SortedSetDocValuesRangeQuery.java:280-287; the same words atSortedNumericDocValuesRangeQuery.java:229-236).IndexSortSortedNumericDocValuesRangeQuery's class javadoc: when "its primary sort is on the same field as the query" does not hold, "the search is delegated tofallbackQuery" (:43-53). - The callee returns such a field by design, with a comment saying so (
Sort.java:162-164), andSortField.getField()documentsnullfor SCORE and DOC (SortField.java:380-381). - The metadata is truthful and accepted.
LeafMetaData.sortis "the order in which documents from this index are sorted, ornullif documents are in no particular order" (LeafMetaData.java:32-33); index order is true for any segment, and the constructor checks only the version fields, not the sort (:49-61). No documentation was found that calls reporting this redundant DOC order either supported or unsupported; that is the maintainers' call. - Sibling code is null-safe.
SortingCodecReadercomparesfield.equals(sf.getField())(SortingCodecReader.java:901);FieldValueHitQueue.canEarlyTerminatecompares whole sort fields (FieldValueHitQueue.java:202-214). - No test, CHANGES entry or discussion says DOC or SCORE leaf sorts are unsupported:
TestGetPrimarySortFieldandTestDocValuesQuerieshave no nameless-field case, and the #13592 and #16424 reviews do not mention DOC or SCORE sorts.
The devil's-advocate check144 words
The strongest case for "intended": advertising Sort.INDEXORDER as an index sort is misuse. IndexWriter rejects DOC and SCORE index sorts, LeafMetaData is expert and experimental, and null is the canonical "no particular order".
Why it fails: none of that is a documented precondition. The writer rejects DOC and SCORE because it has no IndexSorter to reorder documents with, which is about applying a sort, not about describing an order that needs no reordering. The helper the queries call returns such fields on purpose, and the queries' own javadocs promise null or a fallback. The cross-model lenses rated this argument moderate and rejected it; the devil's-advocate verifier found no documented precondition. A maintainer may still call it a "don't do that" corner and give it low priority, which is a fair triage outcome for a Low bug; but a NullPointerException is not the documented answer.
Recommended Null-safe comparison at every site (fix A)
Compare null-safely, field.equals(sf.getField()), at the 5 sites in the 3 range-query classes. The repro passes with assertions on and off, and 85 tests of the repro and the 4 closest suites pass. Alternative B, Sort.getPrimarySortField returning null for a nameless primary, passes the same tests.
SortField sf = Sort.getPrimarySortField(reader);
if (sf == null || field.equals(sf.getField()) == false) {
return null;
}All options and trade-offs2 options ยท 346 words
Both options were built and checked through gradle on 5adff78bb168, one at a time, and with a fix-check probe over all three query classes.
field.equals(sf.getField()) instead of sf.getField().equals(field) at SortedSetDocValuesRangeQuery.java:291, SortedNumericDocValuesRangeQuery.java:240 and IndexSortSortedNumericDocValuesRangeQuery.java:206, :433, :536 (5 lines in 3 files). field is never null: the constructors call Objects.requireNonNull.
SortField sf = Sort.getPrimarySortField(reader);
if (sf == null || field.equals(sf.getField()) == false) {
return null;
}Trade-offs
Smallest and local; covers DOC, SCORE and any other nameless primary; matches SortingCodecReader.java:901; one string comparison per leaf, as before. It leaves getPrimarySortField free to return nameless fields, so a future caller has to remember the same guard.
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seed1205DEAD: the repro fails without the fix (assertions on and off) and passes with it (assertions off). - With the fix and assertions on: the repro (1),
TestDocValuesQueries(41),TestIndexSortSortedNumericDocValuesRangeQuery(29),TestGetPrimarySortField(9) andTestSortedSkipperScorerSupplier(5): 85 tests, 0 failures.checkGoogleJavaFormatpasses with the fix and the repro in place. - The fix-check probe gives 4 hits for
searchandcountin all three classes on the index-order leaf. The full core suite was not run.
In Sort.getPrimarySortField (Sort.java:145-148), return null when the effective primary sort field has no name. primarySortFieldIndex is unchanged, so early termination in FieldValueHitQueue is not affected.
SortField sf = reader.getMetaData().sort().fields[idx];
return sf.getField() == null ? null : sf;Trade-offs
One place, and it protects future callers. But it changes what a public static method returns, for external callers too (only the in-tree callers were tested), so its javadoc ("or null if none is configured") needs a sentence, and so does primarySortFieldIndex's ("the field that getPrimarySortField would return").
What the checks validated
The repro passes (assertions on), the probe gives 4 hits everywhere, and the repro plus the same 4 suites pass through gradle with B instead of A: 85 tests, 0 failures.
Downstream products are affected only if their own reader wrappers report a DOC or SCORE index sort; queries and documents alone cannot create that state.
A proof-obligation audit of SortedSetDocValuesRangeQuery's shortcut contract: the shortcut must return null for every sort field Sort.getPrimarySortField can return. It does not for the nameless DOC and SCORE fields; a public-API JUnit test witnessed the exception. The numeric twin was found the same way.
The method in full131 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder checked
canUsePrimarySortShortcutagainst its javadoc ("ornullif the shortcut cannot be applied") for every valueSort.getPrimarySortFieldcan return, and found that the nameless primary it returns on purpose is dereferenced. - The finder wrote a public-API JUnit test and the one-token fix that makes it pass. A first draft used the merge-only
SortingCodecReader.wrap(reader, null, sort); after the refute-lens review pointed that out, the test was rebuilt on aFilterLeafReaderand every run was redone. - The twin site in
SortedNumericDocValuesRangeQueryhas the same root cause and is part of this finding. An independent reproducer, two executed cross-model reviews, a devil's-advocate verifier and an adjudication reproduced it with their own tests ("Prior reports, history and checks").
No matching public report in apache/lucene issues, PRs and commits (incl. the JIRA archive), the mailing lists, downstream trackers or the web (104 logged queries, 2026-11-03). Nearest: #16885 / #16893, a different defect of the same shortcut.
Searches3 searches
No matching public report was found in the documented searches. Nearest items, none a report of this bug: #16885 / #16893 (the same shortcut, wrong hits with document blocks, fixed by the hasBlocks() check below the dereference), #16424 and #13592 (introduced the code; no discussion of DOC or SCORE sorts), open PRs #13693, #13886, #13973 and #16699 (rework the sort checks and keep the dereference), #16810 (open; a getField() != null guard on an IndexWriter path), elastic/elasticsearch#94407 (2023, the same JVM message, thrown in Elasticsearch's own code and fixed there).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-03; refreshed the same day | two scripted prior-report searches and 22 manual searches: class and method names (canUsePrimarySortShortcut, getPrimarySortField), the JVM message, items created since 2026-10-31; commits on main, branch_11x, branch_10_6 and branch_10x | no report; #16885 / #16893 are the nearest |
| Open PRs | 2026-11-03 | the changed files of all 409 open apache/lucene PRs | 10 touch these files; none adds a null check |
| Mailing lists, downstream, web | 2026-11-03 | dev@, java-user@, issues@ and commits@, Elasticsearch, OpenSearch and Solr trackers, web queries | only elastic/elasticsearch#94407 (the same message from Elasticsearch's own code) |
Introduced by #13592 (579427228d4, 2024-08-22), first released in 10.0.0; #16424 (10.6.0) moved the check and kept the call. The query LongField.newRangeQuery returns has had the same call since 9.5.0 (#12078). Every executed release since fails; not fixed upstream.
Versions tested7 rows
Introduced (the repro's site) by 579427228d4, apache/lucene#13592 "Take advantage of the doc value skipper when it is primary sort" (Ignacio Vera, 2024-08-22), as indexSort.getSort()[0].getField().equals(field) in SortedSetDocValuesRangeQuery and SortedNumericDocValuesRangeQuery; every 10.x tag contains it, no 9.x tag does. #16424 (693a7234bad, Alan Woodward, 2026-07-30; in 10.6.0 as the backport a22c06400da) moved the check into canUsePrimarySortShortcut on top of the new Sort.getPrimarySortField and kept the dereference; #16893 (2026-10-26) added the hasBlocks() check after it. Older site, same root cause: IndexSortSortedNumericDocValuesRangeQuery, moved from sandbox into core and returned by LongField/IntField.newRangeQuery since #12078 (102622842bf, Lu Xugang, 2023-01-13, 9.5.0); the sandbox original (LUCENE-7714, 2019) had the same expression. Not new in 10.6. Not fixed upstream: upstream main 1c2072f0284 (2026-11-02) changes none of the affected files (its only lucene/core change since 5adff78bb168 is a CheckIndex message, #16964), and no open PR changes the dereference.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | JUnit, -ea and -da | fails at :291 |
| 10.6.0, 10.0.0 | minimal test | fails (10.0.0 at the inline form, :253) |
| 10.0.0 to 10.6.0 (13 releases) | 10-document test | fails, same exception |
| 9.12.3 | no API | the test does not compile (3 missing symbols); by reading, no skip-indexed doc values and no shortcut |
| 9.5.0 to 9.12.3, 10.0.0 to 10.6.0, main | LongField probe | fails in IndexSortSortedNumericDocValuesRangeQuery |
| 9.4.2 | no API | the probe does not compile: no LongField |
fix A or fix B on 5adff78bb168 | gradle | the repro and 84 tests of 4 suites pass |
A finder, an independent reproducer, two executed cross-model reviews (OpenAI GPT-5.6, real, high), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it. This doc's gradle runs reproduced it on main and validated both fixes.
Checks and verdicts293 words
The cross-model reviews ran on the older main ea57360912ed; a separate re-run repeated their own tests on 5adff78bb168 (refute lens 2 failures in 6 tests, intended lens 2 in 5, every control passing). Every other check ran on 5adff78bb168 or release jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Own 10-document test fails on main (assertions on and off), 10.6.0 and 10.0.0; the one-site fix makes it pass. |
| independent reproducer | real, Low | Own test (count path, other data, plain-leaf baseline) fails on main (on, off), 10.6.0, 10.5.2 and 10.0.0; the fix gives the right count 3. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own tests fail through a FilterCodecReader with index-order metadata; controls pass; pointed out that SortingCodecReader is merge-only, so the repro was rebuilt on a FilterLeafReader. |
| cross-model review, intended lens (OpenAI GPT-5.6, executed) | real (high); intended argument moderate | Own range and exact tests fail; 3 controls pass; the fix passes them and 37 TestDocValuesQueries tests. |
| devil's-advocate verifier | real, Low | No documented precondition; only the range shortcuts throw on the leaf; suggested fix B. |
| adjudication | confirmed, high, Low | Re-ran two tests with new seeds: both fail at :291. |
| twin-site check (SortedNumericDocValuesRangeQuery) | same root cause | Its own test fails at :240; the same change there passes it and 41 TestDocValuesQueries tests. |
| separate re-runs, audit and final repro check | reproduced; confirmed, Low | Fresh seeds on main and 10.6.0 fail and pass with the fix; one minimal deterministic test; Low, no wrong hits. |
| this doc | reproduced; fixes A and B validated | Gradle on main: the repro fails with assertions on and off; with A (or B) it and 84 tests of 4 suites pass. |
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 471 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,460 words
- Registry title
- Doc-values range queries throw NullPointerException on a leaf whose index sort starts with a DOC or SCORE sort field (for example a custom reader that reports Sort.INDEXORDER), because the primary-sort shortcut calls getField().equals(field) on the null-field SortField that Sort.getPrimarySortField returns (SortedSetDocValuesRangeQuery and SortedNumericDocValuesRangeQuery since 10.0.0, IndexSortSortedNumericDocValuesRangeQuery since 9.5.0)
- Area
- search / doc-values range queries, index-sort primary-sort shortcut (SortedSetDocValuesRangeQuery, SortedNumericDocValuesRangeQuery, IndexSortSortedNumericDocValuesRangeQuery, Sort.getPrimarySortField)
- Priority
- Low, rank 788: Doc-values range and exact queries throw NullPointerException on a leaf whose reader reports an index sort starting with a DOC or SCORE field, in every release since 10.0.0 (9.5.0 for LongField range queries). Lucene's own readers never report such a sort and IndexWriter rejects it, so only an application-written LeafReader with hand-built metadata reaches it, and it fails loudly with no wrong hits.
- Impact class
- minor
- Impact note
- Only applications whose own LeafReader (a FilterLeafReader, FilterCodecReader or other custom reader) reports, through the public LeafMetaData constructor, an index sort whose first field is SortField.FIELD_DOC or FIELD_SCORE (for example Sort.INDEXORDER) are hit: a doc-values range or exact query on a skip-indexed, single-valued field on such a leaf (executed: SortedDocValuesField.newSlowRangeQuery and newSlowExactQuery, NumericDocValuesField.newSlowRangeQuery, LongField.newRangeQuery) then throws NullPointerException from IndexSearcher.search and count, with -ea and -da, at any scale, in every release from 10.0.0 to 10.6.0 (the LongField path from 9.5.0) and on main; segments written by IndexWriter with Lucene's own sort fields never carry such a sort (IndexWriterConfig.setIndexSort rejects DOC and SCORE), and no wrong hits are returned and nothing is written.
- Summary
- You wrap a segment in a FilterLeafReader whose getMetaData() reports Sort.INDEXORDER (true for any segment, whose documents are in index order) and run SortedDocValuesField.newSlowRangeQuery("f", v2, v5) on a skip-indexed field; you expect the 4 matching documents, as on the unwrapped segment. Instead IndexSearcher.search throws NullPointerException 'Cannot invoke "String.equals(Object)" because the return value of "org.apache.lucene.search.SortField.getField()" is null': the query's index-sort shortcut asks Sort.getPrimarySortField for the leaf's primary sort field, which returns the DOC sort field (whose field name is null) on purpose, and then calls getField().equals(field) on it instead of skipping the shortcut. The same unguarded call is in SortedNumericDocValuesRangeQuery and IndexSortSortedNumericDocValuesRangeQuery.
- Introduced
- 579427228d4297837c8c1794ddde98af0c9815fd (apache/lucene#13592 'Take advantage of the doc value skipper when it is primary sort', Ignacio Vera, GitHub iverase; reviewed by gsmiller and jpountz) for the SortedSetDocValuesRangeQuery and SortedNumericDocValuesRangeQuery sites: it added the shortcut guarded by indexSort.getSort()[0].getField().equals(field) [read: git log -S, git tag --contains (all 13 10.x tags, no 9.x tag); exec: the repro fails on 10.0.0 and does not compile on 9.12.3, which has no skip-indexed doc values]. #16424 (693a7234bad, Alan Woodward, author date 2026-07-30, in 10.6.0 via a22c06400da) moved the check into canUsePrimarySortShortcut on top of the new Sort.getPrimarySortField, whose null-field branch returns a DOC/SCORE sort field as primary on purpose, and kept the dereference. Older site of the same root cause: IndexSortSortedNumericDocValuesRangeQuery, in core since #12078 (102622842bf on main, f70530e5840 on branch_9x, LuXugang, 2023-01-13), which moved the LUCENE-7714 sandbox query (5bf023cf194, Julie Tibshirani, 2019-05-10, with the same indexSort.getSort()[0].getField().equals(field)) into core and made LongField/IntField.newRangeQuery return it [exec: auxiliary probe fails on 9.5.0; 9.4.2 has no LongField] (2024-08-22 (579427228d4, author date = commit date; first release 10.0.0); older site 2023-01-13 (#12078; first release 9.5.0))
- First affected version
- 9.5.0 for the row, through the IndexSortSortedNumericDocValuesRangeQuery site that LongField.newRangeQuery returns [exec, auxiliary probe; 9.4.2 has no LongField and the query was still in sandbox]; 10.0.0 for the repro's SortedSetDocValuesRangeQuery site and the SortedNumericDocValuesRangeQuery site [exec; 9.12.3 has no skip-indexed doc values, so the repro does not compile there]
- Affected versions
- Repro site (SortedSetDocValuesRangeQuery): every 10.x release, 10.0.0, 10.1.0, 10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1, 10.5.2 and 10.6.0 (4ae946e5a9b), all executed and failing with the one-test repro (-da; 10.0.0 and 10.6.0 also -ea), and main 5adff78bb168 (12.0.0-SNAPSHOT, -ea and -da) [exec]. SortedNumericDocValuesRangeQuery site: 10.0.0 (earlier inline form of the check), 10.5.2, 10.6.0 and main fail [exec]. IndexSortSortedNumericDocValuesRangeQuery site (auxiliary probe, LongField.newRangeQuery on the same kind of leaf, -da): every release from 9.5.0 to 9.12.3 (14 releases) and from 10.0.0 to 10.6.0 (13 releases), and main, all fail [exec]. Not affected: 9.4.2 and older (no core API) [exec: compile error]. Not fixed upstream: upstream main 1c2072f0284 (2026-11-02) differs from 5adff78bb168 only outside lucene/core, and no open PR changes the dereference [read, 2026-11-03]. Not new in 10.6: #16424 only moved the dereference.
- 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
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches; rechecked the same day): 104 logged queries, among them two scripted prior-report searches over apache/lucene issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, and 22 manual GitHub searches (incl. items created since 2026-10-31); the changed files of all 409 open apache/lucene PRs (10 touch these files, none adds a null check; #13693, #13886, #13973 and #16699 keep the dereference); commits by author date on main, branch_11x, branch_10_6 and branch_10x (newest: ffa9d816eeb #16893, 2026-10-26, and 70a428b5ba9 #16857, 2026-10-29, both in 5adff78bb168, neither changes the null case). Related, not this bug: #16885 / #16893 (same shortcut, wrong hits with document blocks, fixed by the hasBlocks() check below the dereference), #16884 / #16886 (other defects of the IndexSort query), #16424 and #13592 (introduced the code; no discussion of DOC/SCORE sorts), #16810 (open; guards getField() != null on an IndexWriter path, not in these queries), elastic/elasticsearch#94407 (2023, the same NullPointerException text, but thrown in Elasticsearch's own SearchAfterBuilder and fixed there). Refresh 2026-11-03: GitHub search 'canUsePrimarySortShortcut' finds only #16885, 'getPrimarySortField' and 'primary sort NullPointerException' find nothing, and no PR updated since 2026-11-02 touches these sites (#16835 changes only Lucene90DocValuesProducer).
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle on 2026-11-03 in a scratch worktree, seed1205DEAD: the repro without a fix (assertions on and off: fails), with fix A (assertions off: passes; assertions on, with the 4 closest suites: 85 tests, 0 failures;checkGoogleJavaFormatpasses) and with fix B instead (the same 85 tests pass). The minimal test also fails on the 10.0.0 and 10.6.0 jars. The hunt ran its 10-document version of the test on all 13 releases from 10.0.0 to 10.6.0 and aLongFieldprobe on every release from 9.5.0. JDK: OpenJDK 25.0.3; OS: Linux aarch64.
- Finding record
lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java:117-131 (at 5adff78bb168) (scorerSupplier: shortcut tried for a field with a skipper and singleton sorted values, :124-126), :280-304 (canUsePrimarySortShortcut: javadoc 'or null if the shortcut cannot be applied' :280-287; 'sf == null || sf.getField().equals(field) == false' :291)lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java:136-147 (at 5adff78bb168) (scorerSupplier), :229-241 (canUsePrimarySortShortcut, dereference at :240)lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:43-53 (at 5adff78bb168) (class javadoc: otherwise 'the search is delegated to fallbackQuery'), :201-206, :432-433, :535-536 (getPrimarySortField(...).getField().equals(field))lucene/core/src/java/org/apache/lucene/search/Sort.java:41-44 (at 5adff78bb168) (RELEVANCE = new Sort(), INDEXORDER = new Sort(SortField.FIELD_DOC)), :54-56 (new Sort() sorts by FIELD_SCORE), :138-148 (getPrimarySortField), :154-183 (primarySortFieldIndex; null-field branch 'Custom field that we don't know anything about, so return it as primary' :161-165)lucene/core/src/java/org/apache/lucene/search/SortField.java:121-124 (at 5adff78bb168) (FIELD_SCORE and FIELD_DOC have a null field), :379-384 (getField: 'Could return null if the sort is by SCORE or DOC')lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java:25-61 (at 5adff78bb168) (public record; sort 'the order in which documents from this index are sorted, or null if documents are in no particular order'; constructor 'Expert: ... Public for use by custom LeafReader impls', validates only the version fields)lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:505-510 (at 5adff78bb168) (setIndexSort rejects a SortField without an IndexSorter); index/Sorter.java:213 ('Cannot use sortfield ... to sort indexes'); index/SortingCodecReader.java:47-55 (class javadoc 'This reader should only be used for merging'), :899-901 (null-safe field.equals(sf.getField()))
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java:121-131, (at 5adff78bb168) :280-304lucene/core/src/java/org/apache/lucene/document/SortedDocValuesField.java:96-116 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/search/Sort.java:41-44, (at 5adff78bb168) :145-148, :154-183 (:162-164)lucene/core/src/java/org/apache/lucene/search/SortField.java:121-124, (at 5adff78bb168) :380-386lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java:141-147, (at 5adff78bb168) :229-241lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:201-206, (at 5adff78bb168) :430-435, :533-536; document/LongField.java:138-146; document/IntField.java:135-141
- Who is affected
lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java:25-61 (at 5adff78bb168) (:43, :48)lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:505-510; (at 5adff78bb168) index/Sorter.java:208-214; index/SortingCodecReader.java:47-52, :453-475; search/SortField.java:245, :384, :595; codecs/lucene99/Lucene99SegmentInfoFormat.java:148-156, :234-245
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java:280-291; (at 5adff78bb168) document/SortedNumericDocValuesRangeQuery.java:229-241lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:43-53 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/search/Sort.java:154-183; (at 5adff78bb168) search/SortField.java:379-386; index/LeafMetaData.java:25-61lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:895-903; (at 5adff78bb168) search/FieldValueHitQueue.java:196-214
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:899-901; (at 5adff78bb168) search/NumericDocValuesRangeQuery.java:30; document/SortedSetDocValuesRangeQuery.java:58
- Prior reports
https://github.com/apache/lucene/issues/16885, https://github.com/apache/lucene/pull/16893, https://github.com/apache/lucene/pull/16424, https://github.com/apache/lucene/pull/13592, https://github.com/elastic/elasticsearch/issues/94407
- Diagrams
lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java:288-291; (at 5adff78bb168) search/Sort.java:154-183
- TestDocValuesRangeQueryIndexOrderSort.java (lucene/core, package org.apache.lucene.document, 1 test):
repro/LFV-1205/TestDocValuesRangeQueryIndexOrderSort.java(76 lines)
The diagram follows the repro's index and the values a probe on main prints (control count 2, FIELD_DOC with a null name, the throw site).
assets/LFV-1205/bug.png(471 KB)
![Diagram of the bug: one segment with documents a, b, c in field f, wrapped in a reader that reports Sort.INDEXORDER. The range query f in [b, c] asks Sort.getPrimarySortField for the primary sort field, gets SortField.FIELD_DOC, whose getField() is null, and the check sf.getField().equals("f") throws NullPointerException. Expected count 2; with field.equals(sf.getField()) the shortcut is skipped and the count is 2.](../assets/LFV-1205/bug.png)