The query takes long bounds, but on an int sort it narrows them with a plain (int) cast before its binary search: setTopValue((int) topValue) on the doc-values path, IntPoint.pack((int) bound) on the 4-byte points path. A bound outside the int range keeps only its low 32 bits, so the search returns the wrong doc-ID range.
- An index can be sorted by a field (
IndexWriterConfig.setIndexSort). Each segment then stores its documents in that field's order, so the documents that match a range on the field sit next to each other. IndexSortSortedNumericDocValuesRangeQueryuses this: it binary-searches the first and the last matching document and returns the doc-ID range between them, instead of checking every document. When the shortcut does not apply, it runs the fallback query you pass in, which must match the same documents.- Its bounds are inclusive
longs. The doc-values range queries write an open end asLong.MIN_VALUEorLong.MAX_VALUE, and this class's ownrewrite()does the same.
(int) cast turns Long.MIN_VALUE into 0, so the query returns the doc-ID range [1, 2) instead of [0, 2) and misses -5. Select the still to open it at full size. Generated with AI, checked against the code.- Sort the index by
new SortField("f", SortField.Type.INT)and add three documents whose doc valuefis -5, 3 and 20. - Ask for "f <= 10":
new IndexSortSortedNumericDocValuesRangeQuery("f", Long.MIN_VALUE, 10, fallback), writing the open lower end asLong.MIN_VALUE, the way the doc-values range queries document it. You expect 2 hits, -5 and 3, the same as the fallbackNumericDocValuesField.newSlowRangeQuery("f", Long.MIN_VALUE, 10). - You get 1 hit, and
count()says 1: the document with -5 is missing. No error. - Why: the query binary-searches the sorted doc values, and for an int sort it hands each bound to the comparator as
(int) bound.(int) Long.MIN_VALUEis 0, so it searches 0 <= f <= 10.(int) Long.MAX_VALUEis -1, so "f >= 0" written as[0, Long.MAX_VALUE]finds nothing (0 instead of 2). - An
IntField(4-byte points) takes the same cast on its points path: the same wrong counts, extra hits for some bounds, or anIllegalArgumentException. A bound inside the int range is not changed by the cast.
Applications that sort the index by an int field (SortField.Type.INT, or SortedNumericSortField with Type.INT on an IntField) and build this query themselves with a bound outside the int range, most naturally Long.MIN_VALUE or Long.MAX_VALUE as an open end. Default codec, three documents suffice. IntField.newRangeQuery, LONG sorts in the setups SortField.Type.LONG allows, and in-range bounds are not affected.
Mechanism, terms and numbers687 words
IndexSortSortedNumericDocValuesRangeQuery takes long bounds and, when the index is sorted by the queried field, binary-searches the doc-ID range of the matches instead of running its fallback query. When that sort is SortField.Type.INT (or a SortedNumericSortField with Type.INT), the doc-values binary search narrows the bound with fieldComparator.setTopValue((int) topValue), and when the field has 4-byte points (IntField) the points binary search packs IntPoint.pack((int) lowerValue) / IntPoint.pack((int) upperValue) (lines 633 and 461-462 on main 645d4fe14a20). A bound outside the int range keeps only its low 32 bits: (int) Long.MIN_VALUE is 0 and (int) Long.MAX_VALUE is -1, the documented open ends of the doc-values range queries this class wraps. The search then covers the wrong doc-ID range: [Long.MIN_VALUE, 10] over the values -5, 3 and 20 returns 1 document instead of 2, [0, Long.MAX_VALUE] returns 0 instead of 2, count() agrees with the wrong result, other bounds add documents, and on the points path some bounds throw IllegalArgumentException. The class javadoc requires the same documents as the fallback, and no documented int-range precondition was found. Every core release from 9.5.0 to 10.6.0 and main are affected; matching nothing when the range lies outside the int range and clamping the bounds otherwise fixes it.
All lines are IndexSortSortedNumericDocValuesRangeQuery.java on main 645d4fe14a20.
- Entry.
scorerSupplier(:160-168, throughgetDocIdSetIteratorOrNull,:517-545) tries the points binary search first (getDocIdSetIteratorOrNullFromBkd,:430-515), then, for an INT or LONG primary sort on the field, the doc-values binary search (getDocIdSetIterator,:559-619). Only when neither applies does the fallback run.Weight#count(:177-226) uses the points search only when every document has a point (:193), and the doc-values search only when that holds or the sort's missing value lies outside the range (:214-215); otherwise it returns the fallback's count (:225). In the minimal test neither holds, and the fallback's count is -1 there (no doc-values skip index), soIndexSearcher.countcounts the documents the query's scorer returns, which come from the wrong range. - Doc-values path.
getDocIdSetIteratorbinary-searches the first document with value >=lower(:569-583) and the first with value >upper(:585-602). Each probe compares against a comparator fromloadComparator(:626-646), which forType.INTdoesfieldComparator.setTopValue((int) topValue)(:632-633): the int comparator needs anInteger, and the cast keeps the low 32 bits of the long. - Points path. For 4-byte points,
:460-462packIntPoint.pack((int) lowerValue)andIntPoint.pack((int) upperValue)and then binary-search the point tree (:479-504). A wrapped upper end below the wrapped lower end can make the start doc greater than the end doc, andDocIdSetIterator.rangethen throws "minDoc must be < maxDoc" (DocIdSetIterator.java:47-50). - The repro's numbers. In index-sort order the documents are -5, 3, 20.
(int) Long.MIN_VALUE= 0, so the first document with value >= 0 is the one with 3, and the first with value > 10 is the one with 20: the returned range holds one document.
rewrite() turns [Long.MIN_VALUE, Long.MAX_VALUE] into a FieldExistsQuery (:131-133), so only one open end at a time reaches the binary search. A bound inside the int range survives the cast unchanged. A bounded-exhaustive check (14 bound values, 8 inside and 6 outside the int range, both long extremes included; values Integer.MIN_VALUE, -5, 0, 3, Integer.MAX_VALUE; forward and reverse sort; with and without a document missing the field; three missing values) against a brute-force count found, on the three supported INT-sort setups (NumericDocValuesField, SortedNumericDocValuesField, IntField), 2,382 wrong results in 7,056 checks, every one with a bound outside the int range (0 wrong of the 2,304 in-range checks). search() and count() are wrong together, or throw together (138 cases, all on the points path). Examples on the IntField path: [Long.MIN_VALUE, -2147483649] should match nothing and returns 3 documents; [Long.MIN_VALUE, -6] throws. With the fix, 0 of all 14,112 checks are wrong. (The run also covers IntField under a LONG sort, 798 more wrong results; SortField.Type.LONG requires LongPoint, so those are left out of the claim.)
Exposure and likelihood226 words
- Who is hit. Applications that (a) sort the index by an int field, either
SortField.Type.INTon aNumericDocValuesFieldorSortedNumericSortFieldwithType.INTon anIntField, and (b) constructIndexSortSortedNumericDocValuesRangeQuerythemselves with alongbound outside the int range. The natural case is an open end written asLong.MIN_VALUE/Long.MAX_VALUE, the convention of thelong-typed doc-values range factories this query wraps. - Default vs opt-in. The index sort is opt-in configuration, and the query is an expert
@lucene.experimentalclass built by hand. Within that: default codec, no scale, assertions on or off, single thread. - What is wrong. Silently missed and extra documents, and a wrong
count()/totalHits; on the points path some bounds throwIllegalArgumentException. Deterministic for a given index and bound. - Not affected.
IntField.newRangeQuery(it passesintbounds), any LONG sort in a setup thatSortField.Type.LONGallows (doc values only, orLongPoint/LongField), and bounds inside the int range (the cast leaves them unchanged). AnIntFieldunder a LONG sort, a setupSortField.Type.LONGrules out, does take the 4-byte points cast. The INTEGER field mappers of two major downstream search servers pass int bounds too. - How likely: rare. It needs the hand-built query; no matching public report was found in the recorded searches (2026-10-29, re-checked 2026-10-30); the code shipped in 9.2.0 (sandbox, the doc-values cast) and 9.5.0 (core).
One JUnit method on the public API, in org.apache.lucene.search like the upstream tests: an INT index sort, three documents and the range [Long.MIN_VALUE, 10]. The fallback assertion passes, the query assertion fails. Deterministic; 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.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestIndexSortIntSortLongBound extends LuceneTestCase {
public void testOpenLowerBoundOnIntSort() throws Exception {
IndexWriterConfig iwc = new IndexWriterConfig();
iwc.setIndexSort(new Sort(new SortField("f", SortField.Type.INT)));
try (Directory dir = newDirectory();
IndexWriter w = new IndexWriter(dir, iwc)) {
for (int v : new int[] {-5, 3, 20}) {
Document doc = new Document();
doc.add(new NumericDocValuesField("f", v));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(w)) {
IndexSearcher searcher = new IndexSearcher(reader);
Query fallback = NumericDocValuesField.newSlowRangeQuery("f", Long.MIN_VALUE, 10);
Query query =
new IndexSortSortedNumericDocValuesRangeQuery("f", Long.MIN_VALUE, 10, fallback);
assertEquals(2, searcher.search(fallback, 10).scoreDocs.length);
// expected 2 hits (-5 and 3) like the fallback, actual 1: -5 is missed
assertEquals(2, searcher.search(query, 10).scoreDocs.length);
}
}
}
}
cp $REPRO/TestIndexSortIntSortLongBound.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestIndexSortIntSortLongBound -Ptests.seed=DEADBEEF
- Expected
- 2 hits (-5 and 3), the same as the fallback.
- Observed
- 1 hit:
AssertionError: expected:<2> but was:<1>at the query assertion (line 46); the fallback assertion passes.
More files, environment, full output233 words
TestIndexSortIntSortLongBound (50 lines with the license header, one test method, public API only). The fallback assertion on line 44 passes and the query assertion on line 46 fails. As a regression test the method can go into TestIndexSortSortedNumericDocValuesRangeQuery unchanged (with its imports). :lucene:core:checkGoogleJavaFormat passes with it in the tree.
The test also fails on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars (first run with the hunt's copy of it, same code with other comments, then with this test itself). An independent reproducer's own public-API test outside the Lucene package, with the other open end ([0, Long.MAX_VALUE] over 42, -7, 1), returns 0 instead of 2 on main, 10.6.0, 10.5.2 and 9.12.3.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (plain Java casts, nothing platform-specific) |
cp $REPRO/TestIndexSortIntSortLongBound.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestIndexSortIntSortLongBound -Ptests.seed=DEADBEEF
# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF
TestIndexSortIntSortLongBound > testOpenLowerBoundOnIntSort FAILED
java.lang.AssertionError: expected:<2> but was:<1>
at org.apache.lucene.search.TestIndexSortIntSortLongBound.testOpenLowerBoundOnIntSort(TestIndexSortIntSortLongBound.java:46)
# standalone runner, unpatched main jars
main 645d4fe14a20, -ea, seed DEADBEEF tests_run=1 failures=1 expected:<2> but was:<1>
main 645d4fe14a20, -da, seed DEADBEEF tests_run=1 failures=1 expected:<2> but was:<1>
main 645d4fe14a20, -da, seed 1234ABCD tests_run=1 failures=1 expected:<2> but was:<1>
# count() probe, same data (NumericDocValuesField / IntField with SortedNumericSortField INT)
[Long.MIN_VALUE, 10] fallback=2 search=1 totalHits=1 count=1
[0, Long.MAX_VALUE] fallback=2 search=0 totalHits=0 count=0- The javadoc lists
SortField.Type.INTas supported and says the fallback "should produce the same documents": here the fallback finds 2, the query 1. - The constructor takes
longbounds unchecked; its ownrewrite()and the doc-values range factories useLong.MIN_VALUE/Long.MAX_VALUEas open ends. - No documented int-range precondition was found; the upstream INT-sort test only uses the
Integerextremes.
The full argument281 words
- Contracts. The class javadoc (
IndexSortSortedNumericDocValuesRangeQuery.java:43-58): the optimization applies when "The sort field is of typeSortField.Type.LONGorSortField.Type.INT", and "This fallback must be an equivalent range query -- it should produce the same documents and give constant scores". Its example builds the query directly fromlongbounds (:59-65). The constructor takeslongbounds and checks nothing (:81-85); the superclass is "A Query over a range of long values" (NumericDocValuesRangeQuery.java:22).rewrite()treatsLong.MIN_VALUE/Long.MAX_VALUEas open ends (:131-133) and says "This query matches the same documents as its fallback" (:136). - The fallback's convention.
NumericDocValuesField.newSlowRangeQuery, the natural fallback for a doc-values-only int field: "You can have half-open ranges ... by settinglowerValue = Long.MIN_VALUEorupperValue = Long.MAX_VALUE" (NumericDocValuesField.java:94-95; the same inSortedNumericDocValuesField.java:90-91). - A legal setup.
SortField.Type.INT: "Fields must either be not indexed, or indexed with IntPoint" (SortField.java:76-80); the repro's field has doc values only. - No precondition found. No int-range precondition was found in the javadocs, CHANGES or MIGRATE entries, tests or review threads that were read (#11502 / #749, #687, #12078, #14732).
- History. The doc-values cast came with LUCENE-10466 (#11502 / PR #749), whose stated expectation was that the query "should not fail with class cast but correctly convert the numeric values". Its review discussed which sort types to support, not bounds; #687 (the points-path cast) did not discuss bounds either.
- Tests. The upstream INT-sort test
testSameHitsAsPointRangeQueryIntSortusesInteger.MIN_VALUE/Integer.MAX_VALUEas its open ends (TestIndexSortSortedNumericDocValuesRangeQuery.java:106-150), while the LONG tests use the long extremes as open ends (:93-95,:780-782): the case is untested, not asserted.
The devil's-advocate check160 words
The strongest case for "caller error": with an INT sort the values are ints (SortField.Type.INT: "Sort values are Integer"), IntField.newRangeQuery only ever passes int bounds, the upstream INT tests use Integer sentinels, the class is @lucene.experimental, a maintainer wrote in 2022 that it "should no longer be exposed to users" (jpountz on #749, 2022-03-30), and its deprecation was floated on #16699 (romseygeek, 2026-09-22).
Why it fails: the class is public in core, its javadoc shows users constructing it from long bounds, the constructor accepts any long, no documented int-range precondition was found, and the class's own rewrite() and its natural fallback use the long extremes as open ends. Narrow use and a possible future deprecation limit the impact, not the correctness: today the query silently returns other documents than the fallback the javadoc says it must equal. The intended-behaviour reviewer from another model family rated the argument moderate, the devil's-advocate check moderate-to-weak; both called the bug real.
Recommended Match nothing outside the int range, else clamp
On the INT doc-values path and the 4-byte points path, match nothing when the whole range lies outside the int range, and otherwise clamp each bound to [Integer.MIN_VALUE, Integer.MAX_VALUE] before the cast (+17/-3, one file). The test then passes, and so do the 161 tests of the closest suites.
// 4-byte points path, and the same early return for Type.INT in getDocIdSetIterator:
if (lowerValue > Integer.MAX_VALUE || upperValue < Integer.MIN_VALUE) {
return IteratorAndCount.empty(); // no int value can be in the range
}
queryLowerPoint = IntPoint.pack(clampToInt(lowerValue)).bytes;
queryUpperPoint = IntPoint.pack(clampToInt(upperValue)).bytes;
// loadComparator:
fieldComparator.setTopValue(clampToInt(topValue));
private static int clampToInt(long value) {
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, value));
}All options and trade-offs4 options ยท 634 words
One fix was validated with gradle in a scratch worktree at 645d4fe14a20 (JDK 25, -Ptests.seed=DEADBEEF); the other options were not built here.
IndexSortSortedNumericDocValuesRangeQuery.java, +17/-3: an early IteratorAndCount.empty() on the 4-byte points path and, for Type.INT, at the top of getDocIdSetIterator; a clampToInt helper in place of the three casts.
// 4-byte points path, and the same early return for Type.INT in getDocIdSetIterator:
if (lowerValue > Integer.MAX_VALUE || upperValue < Integer.MIN_VALUE) {
return IteratorAndCount.empty(); // no int value can be in the range
}
queryLowerPoint = IntPoint.pack(clampToInt(lowerValue)).bytes;
queryUpperPoint = IntPoint.pack(clampToInt(upperValue)).bytes;
// loadComparator:
fieldComparator.setTopValue(clampToInt(topValue));
private static int clampToInt(long value) {
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, value));
}Trade-offs
- It assumes the field's values are ints, which is what an INT sort sorts (
SortField.Type.INT: "Sort values are Integer"; the sort's missing value is anInteger). For int values, clamping a bound to the int range does not change which documents match, and a range that misses the int range entirely matches none. The assumption is not enforced:NumericDocValuesFieldaccepts any long, and the fix was not tested with stored values outside the int range under an INT sort (the setup of the separate LFV-779, where the sort compares only the low 32 bits). Ask the maintainers to confirm it, or pick the fallback option. - Keeps the index-sort shortcut for open-ended bounds; a few comparisons per segment (the range check once, and one clamp each time the binary search loads a comparator, O(log maxDoc) times), nothing per document.
What the checks validated
- Gradle,
:lucene:core:testwith the minimal test and the closest suites:TestIndexSortIntSortLongBound1,TestIndexSortSortedNumericDocValuesRangeQuery29,TestIndexSorting62,TestSortOptimization31,document.TestField38: 161 tests, 0 failures;:lucene:core:checkGoogleJavaFormatpasses. Without the fix the same gradle run of the minimal test fails. The whole core suite was not run. - Earlier runs of the same diff on
645d4fe14a20: the minimal test with-eaand-da; the earliest finder's five tests; the bounded-exhaustive check, 0 wrong of 14,112. The cross-model reviewers' own tests pass on other bases: the intended-lens reviewer's with this fix on the older mainea57360912ed; the refute-lens reviewer's with its own copy of the same change onea57360912ed, and again with this fix on645d4fe14a20.
Replace the three casts with a clamp and nothing else. An independent reproducer's shadow class did exactly this and its test passed.
Trade-offs
Incomplete: a range entirely above the int range, such as [2^31, 2^32], clamps to [Integer.MAX_VALUE, Integer.MAX_VALUE] and matches the document whose value is exactly Integer.MAX_VALUE; the same at the low end. Executed with a clamp-only shadow on 645d4fe14a20, on the IntField and the doc-values path: [2^31, 2^32] returns the Integer.MAX_VALUE document, [-2^32, -2^31-1] and [Long.MIN_VALUE, -2147483649] the Integer.MIN_VALUE document, instead of none; the recommended fix returns none.
Return null from the two binary-search helpers for an INT sort or 4-byte points when a bound is outside the int range, so the equivalent fallback runs.
Trade-offs
Correct by the class's own contract and the smallest change in behaviour: for a bound outside the int range the result is the fallback's, whatever the stored values (no int-value assumption). But open-ended long bounds, the common case, lose the shortcut.
romseygeek suggested on #16699 (2026-09-22) moving the index-sort shortcut into the point and doc-values range queries and deprecating this class as redundant; HoustonPutman, the PR's author, agreed and is aiming for 11.0 (the PR's milestone is 11.0.0); the PR is labelled Stale and nothing has landed.
Trade-offs
Does not fix 9.x and 10.x, where the class stays public; can be combined with the first option.
An application that uses such a range as a restriction filter, with a bound outside the int range, can let through documents outside the range.
Code reading while writing a Z3 model of how the point and doc-values range queries handle their bounds: the model's obligations were satisfiable for both casts and unsatisfiable once the bounds are restricted to the int range. A second hunt found the same root through count(); a one-method JUnit and a bounded-exhaustive check confirmed it.
The method in full115 words
Method: semi-formal. The hunt's hunt on point and doc-values query equivalence (2026-10-25) raised it from code reading while writing a Z3 translation-validation model of the bound handling; the model's obligations were satisfiable for both casts and unsatisfiable once the bounds are restricted to the int range, and a bounded-exhaustive detector on the stock jar and replayed witnesses confirmed it.
- A second hunt, on
count()andtotalHitsshortcuts (2026-10-25), found the same root while modelling the count shortcuts with a bit-precise Z3 check; both findings are collapsed into this one. - An adjudication unit reduced the earliest finder's five tests to the one-method test, ran the 14,112-check bounded-exhaustive differential against a brute-force count, and wrote the fix.
No prior report found (2026-10-29, re-checked 2026-10-30) in GitHub issues and PRs including the JIRA archive, the mailing lists, three downstream projects or the web. Related: #749 and #687 introduced the casts; open #16699 keeps them and floated deprecating the class.
Searches4 searches
Related, not reports of this bug: #11502 / #749 (LUCENE-10466, introduced the doc-values cast while fixing a ClassCastException), #687 (LUCENE-10425, the points-path cast), #14732 / #14736 (INT-sort missing-value fix, other code), #16884 / #16886 / #16893 (multi-valued fields and document blocks, same class, other causes), #736 (LUCENE-10458, missing-value count), open #16698 / #16699 (float/double support; its diff keeps both casts), open draft #13886 (drops only the points-path cast, as a side effect), open #13973 (sort validation). Limits: GitHub search does not index diff code (the open PRs that touch the file were checked by their patches); private reports to the security list are not visible.
| Where | When | How | Found |
|---|---|---|---|
| Search tool (GitHub issues/PRs, commits, downstream repositories, dev@, java-user@, issues@, commits@, JIRA archive, web) | 2026-10-29 | two runs of 51 queries: the class name, setTopValue, IntPoint.pack, cast, narrowing, clamp, Long.MAX_VALUE / Integer.MAX_VALUE, wrong results | 211 and 1,418 hits, none about this bug |
| GitHub search API (apache/lucene, elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr) | 2026-10-29 | 26 queries: the class name (all, open, recently updated), cast fragments, "minDoc must be < maxDoc", overflow and out-of-range phrasings | no report |
| Open PRs that change the file, and the file's history | 2026-10-29 | patches of #16699, #13886, #13973, #16510; commits on main, branch_10x and branch_10_6 | none guards or clamps the bounds |
| GitHub, re-check | 2026-10-30 | apache/lucene items naming the class updated since 2026-10-29; upstream main and branch_10_6 file contents | only #16810 (unrelated); the casts are still there |
Introduced by LUCENE-10466 (#749, 2022; the doc-values cast, in the sandbox module from 9.2.0) and LUCENE-10425 (#687, 2022; the points-path cast, first released in 9.5.0); in core since 9.5.0 (#12078). Fails on 9.5.0, 9.12.3, 10.5.2, 10.6.0 and main; sandbox 9.1.0 threw ClassCastException instead, so no release ever handled these bounds.
Versions tested7 rows
Doc-values cast: 737ce42c1c0 on main (03dd543572c on branch_9x), LUCENE-10466 (#11502, PR #749 by reta, 2022-04-04), "Ensure IndexSortSortedNumericDocValuesRangeQuery handles sort types besides LONG": it replaced setTopValue(topValue), which threw ClassCastException on INT sorts, with setTopValue((int) topValue). Points-path cast: 5b24a233bdf (f31292c1441 on branch_9x), LUCENE-10425 (#687, 2022-09-22); git blame now shows 8671e29929c (#12003), which only moved the lines. Into core: #12078 (102622842bf, 2023-01-13), so 9.5.0 is the first core release. Upstream main and branch_10_6 still have all three casts (2026-10-30); the last commit to the file is ffa9d816eeb on main and its backport afeeeff2d14 on branch_10_6 (#16893).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle; standalone -ea and -da) |
| 10.6.0 | affected | fails (release jar, -ea and -da) |
| 10.5.2, 9.12.3 | affected | fail (-ea and -da) |
| 9.5.0 | first core release | fails (-da) |
| sandbox 9.2.0, 9.4.2 | sandbox | the sandbox class fails the same test (-da) |
| sandbox 9.1.0 | fails loudly | ClassCastException in IntComparator.setTopValue instead (the LUCENE-10466 bug) |
the fix (scratch worktree at 645d4fe14a20) | not affected | the test and 161 tests of the closest suites pass |
An independent reproducer, reviewers from another model family (OpenAI GPT-5.6), an intended-behaviour devil's advocate and an adjudication each ran their own or the finder's tests; all call it real and agree on the root cause and the fix.
Checks and verdicts254 words
The devil's-advocate check asked to drop two reach claims that rest on setups SortField documents as unsupported (LongField under an INT sort, IntField under a LONG sort); this doc does not make them.
| Check | Verdict | What it found |
|---|---|---|
| finder (judge unit) | candidate | Reduced the earliest finder's repro to the one-method test (fails -ea and -da, 5 seeds); the 14,112-check differential, 3,180 wrong on stock and 0 with the fix; the fix. |
| cross-model review, intended lens | real, high | OpenAI GPT-5.6. Own tests on the doc-values and IntField paths: 0 instead of 2; controls pass; the intended-behaviour argument moderate and rejected. |
| Independent reproducer | real | Own public-API test outside the Lucene package, [0, Long.MAX_VALUE]: 0 instead of 2 on main, 10.6.0, 10.5.2 and 9.12.3 (-ea, -da); a shadow that only clamps the casts passes. |
| Cross-model review, refute lens | real, high | OpenAI GPT-5.6. Own 4-test suite: 0 instead of 3 on both paths over several seeds, also without the randomized test directory; controls pass; the fix passes. |
| Intended-behaviour devil's advocate | real | Strongest counter-case moderate-to-weak: no documented int-range precondition was found. |
| adjudication | confirmed, high | Re-ran the finder's test (-ea, -da) and the independent reproduction's (-da) on main: both fail. Not noise; not security-sensitive. |
| this doc | gradle repro and fix | Ran the minimal test with gradle on a clean 645d4fe14a20 (fails) and validated the recommended fix with gradle in a scratch worktree; re-checked upstream on 2026-10-30. |
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 475 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,283 words
- Registry title
- IndexSortSortedNumericDocValuesRangeQuery casts its long bounds to int on an INT index sort, so Long.MIN_VALUE / Long.MAX_VALUE open ends (or any bound outside the int range) silently return wrong hits and counts
- Area
- search / IndexSortSortedNumericDocValuesRangeQuery (index-sort range shortcut: INT sort comparator path and 4-byte points path)
- Priority
- Medium, rank 83: Both give deterministic silent wrong hits and counts in a rare index-sort setup, and the natural Long.MIN_VALUE/Long.MAX_VALUE open-end idiom makes this more than the type mistake behind LFV-779, but it needs the expert query built by hand.
- Impact class
- silent wrong results
- Impact note
- Hits applications that sort the index by an int field (SortField.Type.INT, or SortedNumericSortField with Type.INT over an IntField) and build IndexSortSortedNumericDocValuesRangeQuery directly with a bound outside the int range, most naturally Long.MIN_VALUE or Long.MAX_VALUE as an open end: search() and count() silently miss or add documents (on the 4-byte points path some bounds throw IllegalArgumentException 'minDoc must be < maxDoc' instead); default codec, no scale, -ea or -da; IntField.newRangeQuery passes int bounds and is not affected, nor are LONG sorts or bounds inside the int range.
- Summary
- You sort the index by SortField("f", SortField.Type.INT), index the values -5, 3 and 20 in a NumericDocValuesField, and search new IndexSortSortedNumericDocValuesRangeQuery("f", Long.MIN_VALUE, 10, fallback), where the fallback is NumericDocValuesField.newSlowRangeQuery("f", Long.MIN_VALUE, 10) (f <= 10, written with the open-end convention that the fallback documents). You expect the 2 documents the fallback returns, as the class javadoc requires. You get 1: the doc-values binary search narrows the bound with (int) Long.MIN_VALUE, which is 0, so -5 is missed; (int) Long.MAX_VALUE is -1 in the same way, and the 4-byte points path has the same cast.
- Introduced
- 737ce42c1c0a8697009b97d72386f2c165d07add on main (branch_9x 03dd543572c), LUCENE-10466 (#11502, PR #749 by reta): replaced a ClassCastException on INT sorts with the unguarded fieldComparator.setTopValue((int) topValue) in the then-sandbox class [read; release boundary exec: sandbox 9.1.0 throws ClassCastException, sandbox 9.2.0 fails the repro]. The points-path cast IntPoint.pack((int) lowerValue / (int) upperValue) came with 5b24a233bdf (branch_9x f31292c1441), LUCENE-10425 (PR #687) [read]; git blame shows 8671e29929c (#12003), which only moved the lines. #12078 (102622842bf, branch_9x f70530e5840) moved the class into core (2022-04-04 (author date of 737ce42c1c0; points-path cast 2022-09-22; move to core 2023-01-13))
- First affected version
- 9.5.0 [exec] (first core release, after #12078 moved the class out of sandbox); the same code was in the sandbox module from 9.2.0 [exec; sandbox is out of hunt scope]
- Affected versions
- [exec] main 645d4fe14a20 (-ea and -da: finder, the independent reproduction, the cross-model review check, the intended-behaviour check, judge and the re-run), 10.6.0 (Maven Central core jar, commit 4ae946e5a9b; the independent reproduction -ea and -da, follow-up -da), 10.5.2 and 9.12.3 (the independent reproduction -ea and -da, follow-up -da), 9.5.0 (-da); sandbox 9.2.0 and 9.4.2 fail (-da), sandbox 9.1.0 throws ClassCastException instead (the LUCENE-10466 bug), so no release ever handled these bounds correctly. [read] every core release from 9.5.0 to 10.6.0; upstream main (casts at lines 461, 462 and 633 via the contents API, 2026-10-29; last change to the file ffa9d816eeb, author date 2026-10-26, already in 645d4fe), branch_11x (same last commit) and branch_10_6 7e5aec4370e. Released in 10.6.0: tag releases/lucene/10.6.0 exists (tag object e29d290eeb05, peels to 4ae946e5a9b; git ls-remote 2026-10-29). Not new in 10.6.
- 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: code reading; also reached by Z3, exhaustive check
- Upstream and prior reports
- none found (2026-10-29, read-only). The prior-report record (verdict new): two scripted prior-report searches (51 queries each over GitHub issues and PRs, commits, downstream repositories, dev@, java-user@, issues@ and commits@, the JIRA archive and the web; 211 and 1,418 hits, 0 with high overlap), 26 manual GitHub search-API queries (apache/lucene incl. items created or updated since 2026-10-27, Elasticsearch, OpenSearch, Solr), the patches of #16699, #13886, #13973, #16510, #16810, #16947 and #16917, the file's commit history on main, branch_10x and branch_10_6, the 10.6.0 tag contents and the dev@ thread; plus the candidate's 51-query pass, the independent reproduction's and the intended-behaviour check's searches and the refresh (items naming the class created or updated since 2026-10-28: only #16810, unrelated; PRs created on 2026-10-29, #16938-#16949, touch other files; the casts are still on upstream main). Related, not reports: #11502 / #749 (LUCENE-10466, introduced the doc-values cast while fixing a ClassCastException), #687 (LUCENE-10425, points-path cast), #14732 (INT-sort missing-value fix, different code), #16884 / #16886 / #16893 (multi-valued count and document blocks, same class, different causes), #736 (LUCENE-10458, missing-value count), open #16698 / #16699 (float/double support; its diff keeps both casts; a maintainer suggested deprecating the class, not decided), open #13886 (draft; drops only the points-path cast incidentally), open #13973 (sort validation), elastic/elasticsearch#162656 (nested documents)
- Lucene commit tested
- main
645d4fe14a20: the one-method test through gradle (fails; with the fix it passes together withTestIndexSortSortedNumericDocValuesRangeQuery,TestIndexSorting,TestSortOptimizationanddocument.TestField, 161 tests, andcheckGoogleJavaFormat) and through the standalone runner (-ea,-da, two seeds); the test on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars (and, earlier, the hunt's copy of it with other comments on those jars and the sandbox class of 9.4.2, 9.2.0 and 9.1.0); an independent reproducer's own test on 10.6.0, 10.5.2 and 9.12.3 (-ea,-da); a 14,112-check bounded-exhaustive differential on main, stock and fixed.
- Finding record
lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:43-58 (at 645d4fe14a20) (Type.INT listed as supported; 'This fallback must be an equivalent range query -- it should produce the same documents'), :67 (@lucene.experimental), :131-136 (rewrite(): Long.MIN_VALUE / Long.MAX_VALUE as open ends; 'matches the same documents as its fallback'), :461-462 (IntPoint.pack((int) lowerValue / (int) upperValue) for 4-byte points), :632-633 (fieldComparator.setTopValue((int) topValue) for Type.INT)lucene/core/src/java/org/apache/lucene/document/NumericDocValuesField.java:91-95 (at 645d4fe14a20) and SortedNumericDocValuesField.java:87-91 (newSlowRangeQuery: bounds included; half-open ranges by Long.MIN_VALUE / Long.MAX_VALUE)lucene/core/src/java/org/apache/lucene/document/IntField.java:141 (at 645d4fe14a20) and LongField.java:144 (the in-tree factories that build this query; IntField passes int bounds)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:43-58 (javadoc: Type.INT supported; equivalent fallback), :81-85 (long bounds, no check), :131-136 (rewrite open ends; 'matches the same documents as its fallback'), :160-226 (scorerSupplier, count), :430-515 (points path; :460-462 IntPoint.pack((int) ...)), :517-545, :559-619 (doc-values binary search), :626-646 (loadComparator; :632-633 setTopValue((int) topValue))a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/DocIdSetIterator.java:47-50 (minDoc must be < maxDoc)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/SortField.java:76-92 (Type.INT: 'not indexed, or indexed with IntPoint'; Type.LONG: LongPoint)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:193, :214-215, :225 (Weight#count conditions); document/SortedNumericDocValuesRangeQuery.java:168-195 (fallback count, -1 without a skip index); search/TotalHitCountCollector.java:47-53 (counts the scorer's documents when count is -1)
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/IntField.java:135-143 (newRangeQuery passes int bounds)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:67 (@lucene.experimental)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:43-67, :81-85, :131-136a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/NumericDocValuesRangeQuery.java:22; document/NumericDocValuesField.java:90-98; document/SortedNumericDocValuesField.java:86-94; search/SortField.java:76-80a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestIndexSortSortedNumericDocValuesRangeQuery.java:93-95, :106-150, :780-782github.com/apache/lucene/issues/11502 (issue body: 'should not fail with class cast but correctly convert the numeric values', read 2026-10-30); github.com/apache/lucene/pull/749 (jpountz comment 2022-03-30); github.com/apache/lucene/pull/16699 (romseygeek and HoustonPutman comments 2026-09-22)
- Suggested fix
github.com/apache/lucene/pull/16699 (comments of 2026-09-22; open, labelled Stale)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/SortField.java:76-80, :494; search/SortedNumericSortField.java:272 (the INT missing value is cast to Integer); search/comparators/IntComparator.java:81; IndexSortSortedNumericDocValuesRangeQuery.java:570-602 (comparator reloads)
- Diagrams
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSortSortedNumericDocValuesRangeQuery.java:632-633
- TestIndexSortIntSortLongBound.java (lucene/core, package org.apache.lucene.search, 1 test):
repro/LFV-1049/TestIndexSortIntSortLongBound.java(50 lines)
A small diagram, made in parallel with this doc from the executed runs.
assets/LFV-1049/bug.png(475 KB)
![Summary of the bug: the public-API code (an INT index sort; values -5, 3 and 20; a query and its fallback over [Long.MIN_VALUE, 10]); the results on main: the fallback finds 2 documents, the query 1, count() 1, and "f >= 0" as [0, Long.MAX_VALUE] finds 0 instead of 2; a number line where the range should cover -5 and 3 but main searches [0, 10]; the doc IDs in index-sort order, expected [0, 2) and main [1, 2); line 633, fieldComparator.setTopValue((int) topValue).](../assets/LFV-1049/bug.png)