All findings · Lucene finding · LFV-1205 · dv-range-shortcut-npe-on-null-field-primary-sort

Doc-values range queries throw NullPointerException on a leaf that reports Sort.INDEXORDER as its index sort

A doc-values range query on a leaf whose reader reports Sort.INDEXORDER as its index sort throws NullPointerException: its index-sort shortcut calls getField().equals(field) on the doc-order sort field, whose name is null. In practice only custom readers report such a sort; every release since 10.0.0 has it.

Summary

Priority
Low, rank 788Doc-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
minor
Since
10.0.0 (#13592, 2024); 9.5.0 for LongField/IntField range queriesintroduced 2024-08-22 · about 2 years
Affected
9.5.0 to 10.6.0 and main (10.0.0+ for the doc-values field queries); not fixed upstream
Status
documented
Found by
semi-formal: a proof-obligation audit of SortedSetDocValuesRangeQuery's shortcut contract, witnessed by an executable repro

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

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.

Background: the index-sort shortcut

  1. A segment can be sorted by a field, its index sort. A reader reports it through LeafReader.getMetaData().sort(); null means the documents are in no particular order.
  2. 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.
  3. Sort.INDEXORDER means sorted by document id. Its only sort field, SortField.FIELD_DOC, has no field name: getField() returns null (so does FIELD_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).
The range query's index-sort shortcut is only for leaves sorted by the queried field, and its guard crashes instead of saying no when the leaf's first sort field has no name, as with Sort.INDEXORDER. Generated with AI, checked against the code.
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.
The repro's leaf reports 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.

The finding, step by step

  1. Index 3 documents in one segment with SortedDocValuesField.indexedField("f", ...): values a, b, c.
  2. Wrap the segment in a FilterLeafReader whose getMetaData() reports Sort.INDEXORDER: documents are in index order, which is true for any segment.
  3. Count SortedDocValuesField.newSlowRangeQuery("f", b, c, true, true) on it. You expect 2 (b and c), as on the unwrapped segment.
  4. Instead it throws NullPointerException, with assertions on and off. The query's index-sort shortcut asks Sort.getPrimarySortField for the primary sort field, gets SortField.FIELD_DOC, whose getField() is null, and calls getField().equals("f") on it instead of skipping the shortcut.
  5. The same call is in the numeric doc-values range query and in the query LongField.newRangeQuery returns. IndexWriter refuses DOC and SCORE index sorts, so in practice only an application's own reader gets there.

Who is affected

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 mechanism (main 5adff78bb168, lucene/core/src/java/org/apache/lucene/)

  1. The shortcut is tried for a field with a doc-values skip index and single-valued sorted values (document/SortedSetDocValuesRangeQuery.java:121-131, condition singleton != null && skipper != null at :124). SortedDocValuesField.newSlowRangeQuery and newSlowExactQuery build this query (document/SortedDocValuesField.java:96-116).
  2. It asks for the primary sort field: canUsePrimarySortShortcut calls Sort.getPrimarySortField(reader) (:288-290).
  3. The helper returns a nameless field on purpose. getPrimarySortField (search/Sort.java:145-148) uses primarySortFieldIndex (: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), and getField() "Could return null if the sort is by SCORE or DOC" (:380-386).
  4. 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 "null if the shortcut cannot be applied" (:280-287).
  5. Twin sites, same expression. document/SortedNumericDocValuesRangeQuery.java:240 (reached from :141-147, for example by NumericDocValuesField.newSlowRangeQuery on an indexedField), and search/IndexSortSortedNumericDocValuesRangeQuery.java (reached by LongField.newRangeQuery and IntField.newRangeQuery, document/LongField.java:138-146, with no skip index needed) at :433 (in getDocIdSetIteratorOrNullFromBkd, reached from count and from the scorer path; the executed throw site), :206 (in count, when the field's points do not cover every document) and :536 (in getDocIdSetIteratorOrNull, after :433 has already been reached, so it never throws first; fixed for consistency).
  6. The other operations tested on that leaf work. TermQuery, FieldExistsQuery, sorted searches (by a string field, a long field and Sort.INDEXORDER, including their early-termination check) and IndexWriter.addIndexes all accept a leaf that reports Sort.INDEXORDER; of the operations tested, only the range-query shortcuts throw.

The numbers

  • The repro: 3 documents (a, b, c), range b..c. The unwrapped segment counts 2 (the diagram probe, same documents); the wrapped one throws at SortedSetDocValuesRangeQuery.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 search and for count (: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_DOC also reaches the dereference; 10.0.0 to 10.5.2 compared only getSort()[0].
Exposure and likelihood297 words
  • Who. Applications with their own LeafReader (a FilterLeafReader, a FilterCodecReader or a custom reader) whose getMetaData() reports an index sort whose first effective field is FIELD_DOC (such as Sort.INDEXORDER) or FIELD_SCORE, built with the public LeafMetaData constructor ("Expert: Sole constructor. Public for use by custom LeafReader impls."; the record is @lucene.experimental).
  • Queries hit. Executed: SortedDocValuesField.newSlowRangeQuery and newSlowExactQuery and NumericDocValuesField.newSlowRangeQuery on skip-indexed single-valued fields, and LongField.newRangeQuery on a LongField that is single-valued in the segment (for a multi-valued one only count can reach the call, when the points cover every document; by reading). By reading: SortedSetDocValuesField (single-valued in the segment) and IntField.
  • Default vs opt-in. Not reachable in normal use of IndexWriter or Lucene's readers: IndexWriterConfig.setIndexSort(Sort.INDEXORDER) throws "Cannot sort index with sort field <doc>", and Sorter rejects such fields too. Two contrived in-tree routes exist, and the claim rests on neither. The expert SortingCodecReader.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), setIndexSort checks nothing but the sort field's IndexSorter, so an application-defined SortField subclass with no field name, its own IndexSorter and 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 NullPointerException from IndexSearcher.search or count for 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 null or the wrapped reader's metadata) and then run one of these range queries; no public report of it was found since 9.5.0.

Minimal repro

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.

TestDocValuesRangeQueryIndexOrderSort.java (lucene/core, package org.apache.lucene.document, 1 test)76 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.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)));
      }
    }
  }
}
Command
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
count returns 2 (documents b and c), as on the unwrapped segment.
Observed
NullPointerException: Cannot invoke "String.equals(Object)" because the return value of "org.apache.lucene.search.SortField.getField()" is null at SortedSetDocValuesRangeQuery.canUsePrimarySortShortcut(SortedSetDocValuesRangeQuery.java:291).

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

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.

Environment

lucenemain 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)
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (nothing platform-specific)
Command (environment)
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

Observed output

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

Why it looks unintended

  • The shortcut's javadoc says it returns null when it cannot apply; the LongField range query promises a fallback.
  • Sort.getPrimarySortField returns the nameless field on purpose, and SortField.getField() documents null for 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. canUsePrimarySortShortcut returns "the primary sort field if it is safe to use the ... range shortcuts for this query, or null if the shortcut cannot be applied" (SortedSetDocValuesRangeQuery.java:280-287; the same words at SortedNumericDocValuesRangeQuery.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 to fallbackQuery" (:43-53).
  • The callee returns such a field by design, with a comment saying so (Sort.java:162-164), and SortField.getField() documents null for SCORE and DOC (SortField.java:380-381).
  • The metadata is truthful and accepted. LeafMetaData.sort is "the order in which documents from this index are sorted, or null if 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. SortingCodecReader compares field.equals(sf.getField()) (SortingCodecReader.java:901); FieldValueHitQueue.canEarlyTerminate compares whole sort fields (FieldValueHitQueue.java:202-214).
  • No test, CHANGES entry or discussion says DOC or SCORE leaf sorts are unsupported: TestGetPrimarySortField and TestDocValuesQueries have 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.

Suggested fix

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.

Null-safe comparison at every site (fix A) (validated by the checks) (recommended)

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, seed 1205DEAD: 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) and TestSortedSkipperScorerSupplier (5): 85 tests, 0 failures. checkGoogleJavaFormat passes with the fix and the repro in place.
  • The fix-check probe gives 4 hits for search and count in all three classes on the index-order leaf. The full core suite was not run.

Sort.getPrimarySortField returns null for a nameless primary (fix B) (validated by the checks)

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 note

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.

How it was found

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 canUsePrimarySortShortcut against its javadoc ("or null if the shortcut cannot be applied") for every value Sort.getPrimarySortField can 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 a FilterLeafReader and every run was redone.
  • The twin site in SortedNumericDocValuesRangeQuery has 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").

Prior reports, history and checks

Prior reports

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

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive)2026-11-03; refreshed the same daytwo 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_10xno report; #16885 / #16893 are the nearest
Open PRs2026-11-03the changed files of all 409 open apache/lucene PRs10 touch these files; none adds a null check
Mailing lists, downstream, web2026-11-03dev@, java-user@, issues@ and commits@, Elasticsearch, OpenSearch and Solr trackers, web queriesonly elastic/elasticsearch#94407 (the same message from Elasticsearch's own code)

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 5adff78bb168 JUnit, -ea and -dafails at :291
10.6.0, 10.0.0 minimal testfails (10.0.0 at the inline form, :253)
10.0.0 to 10.6.0 (13 releases) 10-document testfails, same exception
9.12.3 no APIthe 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 probefails in IndexSortSortedNumericDocValuesRangeQuery
9.4.2 no APIthe probe does not compile: no LongField
fix A or fix B on 5adff78bb168 gradlethe repro and 84 tests of 4 suites pass

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder reproducedOwn 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, LowOwn 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 moderateOwn range and exact tests fail; 3 controls pass; the fix passes them and 37 TestDocValuesQueries tests.
devil's-advocate verifier real, LowNo documented precondition; only the range shortcuts throw on the leaf; suggested fix B.
adjudication confirmed, high, LowRe-ran two tests with new seeds: both fail at :291.
twin-site check (SortedNumericDocValuesRangeQuery) same root causeIts 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, LowFresh 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 validatedGradle on main: the repro fails with assertions on and off; with A (or B) it and 84 tests of 4 suites pass.

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

Finding record

LFV-1205, snapshot of 2026-11-06

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, seed 1205DEAD: 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; checkGoogleJavaFormat passes) 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 a LongField probe on every release from 9.5.0. JDK: OpenJDK 25.0.3; OS: Linux aarch64.

Lucene and upstream sources

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-304
  • lucene/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-386
  • lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java:141-147, (at 5adff78bb168) :229-241
  • lucene/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-241
  • lucene/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-61
  • lucene/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

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

Repro files

About the diagrams

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

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.