- Classic range facets (
LongRangeFacetCounts,DoubleRangeFacetCounts) count documents whose numeric value (or values, for a multi-valued field) falls into each requested bucket, for example a price bucket from 0 to 10 and another from 10 to 100. - Range-on-range facets (
LongRangeOnRangeFacetCounts,DoubleRangeOnRangeFacetCounts, added in 9.5.0) count documents that store a range, such as an event's [start, end] in aLongRangeDocValuesField, against each requested range, under a relation you choose:INTERSECTS,CONTAINS,WITHINorCROSSES. - Both kinds take the requested ranges as varargs (
LongRange... ranges), so a call with no ranges compiles without a warning.
getEncodedRanges, which reads ranges[0] and throws before the base constructor or the counting run; below, what you expect and what you get. Select the still to open it at full size. Generated with AI, checked against the code.- Index 20 documents, document i storing the range [i, i+5] in a
LongRangeDocValuesField, and count range-on-range facets over all hits: one requested range [0, 2] withINTERSECTScounts 3 (documents 0, 1 and 2). - Now request no ranges, as code does that builds the list at run time and ends up with an empty one:
new LongRangeOnRangeFacetCounts("lr", hits, QueryType.INTERSECTS). The varargs signature allows it, and no javadoc names a minimum. - You expect an empty result, as the classic counter gives for the same empty request (
new LongRangeFacetCounts("n", hits): value 0, child count 0), or at least anIllegalArgumentExceptionthat rejects the empty list. - Instead the constructor throws
ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0: the private helpergetEncodedRangessizes its array fromranges[0].dimswithout checking that a first range exists. The Double class and all four public constructors fail the same way, with assertions on or off; every release from 9.5.0 to 10.6.0 has it.
Applications that use the opt-in range-on-range facet counters (9.5.0 and later) and build the range list at run time, so that it can end up empty, for example when every bucket is deselected. That one request fails with an internal exception; nothing is miscounted. Rare: callers with a fixed list never hit it.
Mechanism, terms and numbers507 words
LongRangeOnRangeFacetCounts and DoubleRangeOnRangeFacetCounts (facet module, since 9.5.0) count, for each requested range, the documents whose indexed range intersects, contains, lies within or crosses it. Their constructors take the requested ranges as varargs and document no minimum. Called with no ranges, every public constructor throws ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0, because the private helper getEncodedRanges evaluates new byte[ranges.length][2 * Long.BYTES * ranges[0].dims] without checking the length (LongRangeOnRangeFacetCounts.java:85, Double :88). It happens before any document is read, with assertions on and off. The classic LongRangeFacetCounts and DoubleRangeFacetCounts, which the new classes were added to mirror, return an empty result for the same request on purpose and test it; the multi-dimensional sibling MatchingFacetSetsCounts rejects empty input with an IllegalArgumentException. Nothing is miscounted: only the kind of error is wrong. The two getEncodedRanges helpers and their constructor call sites are unchanged since #11901 (later commits touched only the base class), so every release from 9.5.0 to 10.6.0 and main has it. A three-line guard fixes it, either with an IllegalArgumentException or with an empty result.
- Range-on-range faceting (
org.apache.lucene.facet.rangeonrange): each document stores a range (1 to 4 dimensions) in aLongRangeDocValuesFieldorDoubleRangeDocValuesField; the counter counts, per requestedLongRange/DoubleRange, the hits whose stored range matches it under aRangeFieldQuery.QueryType. - Encoded ranges: before counting, each requested range is packed into a byte array of
2 * BYTES * dimsbytes (min and max per dimension). The counter takes the number of dimensions from the first requested range.
- Both public constructors of
LongRangeOnRangeFacetCounts(LongRangeOnRangeFacetCounts.java:42-53and, with afastMatchQuery,:67-82) passgetEncodedRanges(ranges)as an argument ofsuper(...)(:51,:80), so the helper is the first code that sees the list. getEncodedRanges(:84-90) runsnew byte[ranges.length][2 * Long.BYTES * ranges[0].dims](:85). Java evaluates both dimension expressions before it allocates, so with no ranges the read ofranges[0]throwsArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0. Nothing checks the list first.DoubleRangeOnRangeFacetCountshas the same code (DoubleRangeOnRangeFacetCounts.java:87-93, the read at:88).- The base class
RangeOnRangeFacetCountsalso readsencodedRanges[0], in anassert(RangeOnRangeFacetCounts.java:60) and incount()for the number of dimensions (:83). It is never reached today, but an empty-result fix must skip it too. - Assertions and data play no part: it is a JVM bounds check, before any document or doc value is read. The rest of the class copes with zero ranges (
Range.getLabelsFromRangesmaps an empty array to an empty one;getAllChildrenandgetTopChildrenloop over the counts).
Twenty documents, document i storing [i, i+5]: one range [0, 2] with INTERSECTS counts 3. The classic new LongRangeFacetCounts("n", fc) with no ranges returns value 0, child count 0, no labels. new LongRangeOnRangeFacetCounts("lr", fc, INTERSECTS) throws at getEncodedRanges:85; an independent JUnit covering all four public constructors (Long and Double, empty varargs and explicit empty arrays) fails 4 of 6, its two controls passing, with assertions on and off.
Exposure and likelihood258 words
- Who is hit. Users of
LongRangeOnRangeFacetCounts/DoubleRangeOnRangeFacetCountsoverLongRangeDocValuesField/DoubleRangeDocValuesFieldwhose code builds the range list from request or configuration data that can be empty and passes it on unchecked (for examplelist.toArray(new LongRange[0])on an empty list). All four public constructors fail, with or without afastMatchQuery. - What happens. The request fails at once with an exception whose stack trace points into Lucene (
getEncodedRanges:85), not at the caller's empty list. Code that catchesIllegalArgumentExceptionfor bad range input from this API (the range constructors andverifyAndEncodethrow it for invalid bounds) does not catch this one. - What does not happen. No wrong counts, no silent acceptance, no hang, no unbounded allocation, nothing written, no effect on the index, the reader or other requests. Same behaviour at any scale and with any data, with assertions on and off.
- How likely. Rare: it needs a dynamic, possibly empty range list, and no prior public report was found since the API was released in 9.5.0 (January 2023).
- Side note, same fix site, not part of this bug.
getEncodedRangesalso does not check that all ranges have the same number of dimensions. An independent side probe (assertions off) saw a 2-dimensional range followed by a 1-dimensional one return without error and count the 1-dimensional range 0 against a 2-dimensional document it overlaps in dimension 0, and the reverse order throwArrayIndexOutOfBoundsException: Index 16 out of bounds for length 9. Mixing dimensions against one field is closer to misuse; it was not searched for prior reports.
One JUnit test on the public API, no index: it constructs LongRangeOnRangeFacetCounts with no ranges over an empty FacetsCollector (the throw comes before getMatchingDocs() is called, so the hits do not matter). It accepts an empty result or an IllegalArgumentException and fails only when the internal ArrayIndexOutOfBoundsException escapes. Deterministic, single thread, about 1 s.
/*
* 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.facet.rangeonrange;
import org.apache.lucene.document.RangeFieldQuery;
import org.apache.lucene.facet.FacetResult;
import org.apache.lucene.facet.FacetsCollector;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRangeOnRangeFacetCountsNoRanges extends LuceneTestCase {
public void testNoRanges() throws Exception {
FacetResult result;
try {
result =
new LongRangeOnRangeFacetCounts(
"field", new FacetsCollector(), RangeFieldQuery.QueryType.INTERSECTS)
.getAllChildren("field");
} catch (IllegalArgumentException acceptable) {
return;
} catch (ArrayIndexOutOfBoundsException e) {
throw new AssertionError(
"expected an empty result or IllegalArgumentException, got " + e, e);
}
assertEquals(0, result.childCount);
}
}
cp TestRangeOnRangeFacetCountsNoRanges.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/
cd $LUCENE && ./gradlew :lucene:facet:test --tests org.apache.lucene.facet.rangeonrange.TestRangeOnRangeFacetCountsNoRanges -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false instead (same result)
# without -Ptests.asserts, Lucene picks the mode from the seed (DEADBEEF picks off)
- Expected
- An empty result (child count 0), as the classic
LongRangeFacetCountsreturns for no ranges, or anIllegalArgumentExceptionthat rejects the empty list. - Observed
ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0atLongRangeOnRangeFacetCounts.getEncodedRanges(LongRangeOnRangeFacetCounts.java:85); the same with assertions off.
More files, environment, full output347 words
The oracle does not pick between the fixes: an empty result (what the classic counters return) and an IllegalArgumentException (what MatchingFacetSetsCounts throws) both pass. The test covers the Long class; the Double class has the same one-line cause (:88) and fails the same way in the independent reproducer's and the devil's advocate's tests. It needs no randomness, so the seed does not matter, and it was compiled and run unchanged on 9.5.0, 10.6.0 and main (the releases in between ran a version-portable test).
As the regression test: the same test, for example as a new method in TestRangeOnRangeFacetCounts, with two cosmetic changes Lucene's static checks require: rename the unused catch parameter acceptable to _, as other facet tests do (ecjLintTest rejects it), and put the AssertionError on one line (google-java-format). With both, :lucene:facet:check -x test passes with either fix, and standalone the test still fails on stock main and passes with either fix, with assertions on and off. The unnamed _ needs Java 22 or later: fine on main (Java 25), but a backport to the 10.x line (Java 21) needs a named parameter that is used, or expectThrows. With fix A it can be tightened to expectThrows(IllegalArgumentException.class, ...); with fix B it can also assert that the value is 0, as the classic counters' empty-range tests do.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); this test also fails on 10.6.0 and 9.5.0; every release from 9.5.0 to 10.6.0 is affected; 9.4.2 has no range-on-range API |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestRangeOnRangeFacetCountsNoRanges.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/
cd $LUCENE && ./gradlew :lucene:facet:test --tests org.apache.lucene.facet.rangeonrange.TestRangeOnRangeFacetCountsNoRanges -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false instead (same result)
# without -Ptests.asserts, Lucene picks the mode from the seed (DEADBEEF picks off)
# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (the same with -Ptests.asserts=false)
TestRangeOnRangeFacetCountsNoRanges > testNoRanges FAILED
java.lang.AssertionError: expected an empty result or IllegalArgumentException, got java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at __randomizedtesting.SeedInfo.seed([DEADBEEF:E938B2FB8B7E7074]:0)
at org.apache.lucene.facet.rangeonrange.TestRangeOnRangeFacetCountsNoRanges.testNoRanges(TestRangeOnRangeFacetCountsNoRanges.java:36)
Caused by:
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at org.apache.lucene.facet.rangeonrange.LongRangeOnRangeFacetCounts.getEncodedRanges(LongRangeOnRangeFacetCounts.java:85)
at org.apache.lucene.facet.rangeonrange.LongRangeOnRangeFacetCounts.<init>(LongRangeOnRangeFacetCounts.java:51)
at org.apache.lucene.facet.rangeonrange.TestRangeOnRangeFacetCountsNoRanges.testNoRanges(TestRangeOnRangeFacetCountsNoRanges.java:29)- Varargs constructors with no stated minimum; #11901 added them "to mirror
RangeFacetCounts", whose counters return an empty result for zero ranges and test it. - The facet-sets counter
MatchingFacetSetsCountsrejects empty input with an explicitIllegalArgumentException. - No javadoc, test or review comment states a minimum; the facet module fixed a similar internal exception on degenerate input as a bug (#13493).
The full argument257 words
- Contract. The class javadoc says "given a range (or list of ranges), this class will count all the documents ..." (
LongRangeOnRangeFacetCounts.java:27-29); the constructors take varargs and document@param ranges ranges we want the counts of(:64), with no minimum and no@throwsfor an empty list. Neither counter class is tagged@lucene.experimental(Rangeis). - Design intent. #11869 asks for "a corresponding
RangeOnRangeFacetCounts" toLongRangeFacetCounts/DoubleRangeFacetCounts; the PR body of #11901: "AddsRangeOnRangeFacetCountswhich supports double ranges and long ranges to mirrorRangeFacetCounts". The classic counters support zero ranges on purpose since LUCENE-9946 (#127):ExclusiveLongRangeCounter.java:155-157("If no ranges were requested ..."),LongRangeCounter.java:195-197, andTestRangeFacetCounts.testEmptyRangesSingleValued/MultiValued(:383,:411) assertdim=field path=[] value=0 childCount=0fromgetAllChildrenandgetTopChildren.TestRangeOnRangeFacetCounts(20 tests) has no empty-range counterpart; its randomized tests draw 1 to 100 ranges. - Siblings.
MatchingFacetSetsCounts.java:74-79(LUCENE-10274, #841: a PR opened by the same author and co-written with Shai Erera and Greg Miller) throwsIllegalArgumentExceptionfor null or empty matchers and for mixed dimensionality; coreLongRangerejects emptymin/maxarrays withIllegalArgumentException("min/max range values cannot be null or empty"); this package's ownRange.failNoMatchthrowsIllegalArgumentException. - Precedent.
CHANGES.txt, 9.11.1 Bug Fixes: "GITHUB#13493: StringValueFacetCounts stops throwing NPE when faceting over an empty match-set." - History. The searched records did not reveal a minimum or any discussion of an empty list: no javadoc, test,
CHANGES.txtorMIGRATE.mdentry, none of the 62 inline review comments on #11901, and no mail found by the mailing-list searches.
The devil's-advocate check192 words
The strongest case for "intended" (built by the refute reviewer, the second cross-model reviewer and the devil's advocate): zero ranges give the counter no dimensionality; the javadoc says "a range (or list of ranges)"; the base-class assert on encodedRanges[0] shows the author assumed a non-empty array; every upstream test passes at least one range; and the call already fails loudly, so it is in effect rejected and only the exception type is wrong.
Why it fails: it argues that zero ranges may be invalid input, not that an internal ArrayIndexOutOfBoundsException is the intended way to reject it. An assert states an internal invariant, not an API precondition; Lucene rejects bad public arguments with IllegalArgumentException, in this package and in MatchingFacetSetsCounts, the facet-sets counter of the same module; and the class was meant to mirror counters that accept the empty list. The repro accepts either an empty result or an IllegalArgumentException, so it does not overclaim. Verdict: not intended, a Low wrong-error defect. Residual risk: a maintainer may answer that only the exception type changes and rank it low priority, which is a priority answer, not a "by design" one.
Recommended A. Reject an empty list with IllegalArgumentException (mirrors MatchingFacetSetsCounts)
Reject the empty list in getEncodedRanges of both classes with IllegalArgumentException("ranges cannot be empty"), as MatchingFacetSetsCounts does: three lines each. The alternative, B, returns an empty result like the classic counters. With either fix the repro passes with assertions on and off, and the whole facet module test suite passes.
private static byte[][] getEncodedRanges(LongRange... ranges) {
+ if (ranges.length == 0) {
+ throw new IllegalArgumentException("ranges cannot be empty");
+ }
byte[][] result = new byte[ranges.length][2 * Long.BYTES * ranges[0].dims];All options and trade-offs2 options · 596 words
Both options were run through gradle in a scratch worktree of 645d4fe14a20. A is recommended (the adjudication's choice: the smallest change, matching the explicit checks of its siblings); B is just as small and matches the classic counters.
A. Reject an empty list with IllegalArgumentException (mirrors MatchingFacetSetsCounts) (validated by the checks) (recommended)
In getEncodedRanges of LongRangeOnRangeFacetCounts (:84-85) and, identically, DoubleRangeOnRangeFacetCounts (:87-88).
private static byte[][] getEncodedRanges(LongRange... ranges) {
+ if (ranges.length == 0) {
+ throw new IllegalArgumentException("ranges cannot be empty");
+ }
byte[][] result = new byte[ranges.length][2 * Long.BYTES * ranges[0].dims];Trade-offs
- The smallest change; it states the precondition the code already relies on (the number of dimensions comes from the first range) with the same exception type as
MatchingFacetSetsCountsand the range constructors (the message wording differs). - Callers that build the list at run time still have to special-case an empty list (with the classic counters they do not).
- Worth adding
@throws IllegalArgumentException if no ranges are givento the four constructors. Anullarray still gives aNullPointerException; the sibling checksnulltoo, which could be folded into the sameif. - Behaviour changes only for a call that fails today; one length check per constructor, nothing on the counting path.
What the checks validated
Gradle on 645d4fe14a20 (a scratch worktree, and the fact check's re-run with -Ptests.asserts=true): the repro fails on stock main and passes with A, each with assertions on and off. The whole facet module (:lucene:facet:test: 40 test classes, including the repro, TestRangeOnRangeFacetCounts with 20 tests, TestRangeFacetCounts and TestMatchingFacetSetsCounts) runs 307 tests, 0 failures; 7 skipped with assertions on (five nightly tests, one monster test and one @Ignored test), 8 with assertions off (the same plus one assertions-only test). :lucene:facet:check -x test (google-java-format, ecj lint, forbidden APIs) passes with A and the lint-clean regression test.
The finder's fix: getEncodedRanges returns new byte[0][] for no ranges, the base constructor skips count(), and its assert allows an empty array.
// RangeOnRangeFacetCounts constructor
- assert encodedRanges[0].length % (2 * numEncodedValueBytes) == 0;
+ assert encodedRanges.length == 0 || encodedRanges[0].length % (2 * numEncodedValueBytes) == 0;
...
- count(field, hits.getMatchingDocs(), encodedRanges, numEncodedValueBytes, queryType);
+ if (encodedRanges.length > 0) {
+ count(field, hits.getMatchingDocs(), encodedRanges, numEncodedValueBytes, queryType);
+ }
// LongRangeOnRangeFacetCounts and DoubleRangeOnRangeFacetCounts
private static byte[][] getEncodedRanges(LongRange... ranges) {
+ if (ranges.length == 0) {
+ return new byte[0][];
+ }Trade-offs
- Matches
LongRangeFacetCounts/DoubleRangeFacetCounts(dim=field path=[] value=0 childCount=0) and #11901's stated aim to mirrorRangeFacetCounts; callers with a dynamic list need no special case. - Touches the base class as well (the
assertand thecount()call); the total stays 0 because no hit is counted, as in the classic counters' empty-range tests. - An empty request is silently accepted, which can hide a caller bug.
count()is skipped, so the hits, the doc values and anyfastMatchQueryare never looked at.getAllChildren/getTopChildrenreturn no children;getAllDimsstill returns a one-element list holding that emptyFacetResult, andgetSpecificValuestays unsupported, as for any range list.
What the checks validated
Same runs: the repro passes with assertions on and off, and the facet module runs 307 tests, 0 failures, with the same skips as for A (7 with assertions on, 8 off). :lucene:facet:check -x test passes with B and the lint-clean regression test. Earlier, with the fix shadowed into the stock jars, the independent reproducer's 6 tests passed 6 of 6, including getTopChildren giving value 0 and child count 0.
Services that let end users choose facet ranges and handle only IllegalArgumentException for bad range input from this API will see this ArrayIndexOutOfBoundsException escape when the list ends up empty.
Non-formal: a randomized differential test that compared the classic and the range-on-range facet counters called both with an empty range list; the classic one returned an empty result and the range-on-range one threw. It was first logged as a low-value lead, then turned into a public-API JUnit test and confirmed by independent reproducers.
The method in full128 words
Method: non-formal (randomized differential testing, then executed JUnit tests). A hunt unit on facet taxonomy and range counting ran a randomized differential baseline on main ea57360912ed: new LongRangeOnRangeFacetCounts("f", fc, INTERSECTS) with no ranges threw ArrayIndexOutOfBoundsException, while new LongRangeFacetCounts("f", fc) returned an empty result. It was logged only as a low-value robustness lead.
On 2026-10-27 a later unit that turns such executed leads into public-API repros re-ran it on main a9779ef3c193 as a JUnit test with the differential oracle "the same result as the classic counter for the same empty request, or an explicit IllegalArgumentException" (the second outcome was added after the first cross-model review). Independent reproducers then wrote their own tests, ran the releases, and it was cut to the one-test repro on 645d4fe14a20.
No prior report found in GitHub issues, PRs and commits, all 409 open PRs, the Lucene mailing lists, downstream repositories or the web (2026-10-27); a refresh on 2026-11-01 found no new issue or PR about it. Not fixed upstream as of 2026-11-01.
Searches4 searches
Closest items, none a report of this bug: #11869 / #11901 (introduced the classes; empty input never discussed in the issue, the PR or its review comments); #127 / #10985 (LUCENE-9946, added the classic counters' empty-range support and tests: background for the expected behaviour); #12049 (test-only dimension overflow); #12408 (zero hits, not zero ranges); #13542, #13328, #14761 (refactors); #12862 (open; the only open PR touching RangeOnRangeFacetCounts.java, adds getBulkSpecificValues, leaves the constructors alone). Limits: GitHub search does not index code or CamelCase sub-words; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits | 2026-10-27 | two scripted prior-report searches (20 issue/PR and 3 commit queries each) plus about 30 manual phrasings: the class names, getEncodedRanges, "Index 0 out of bounds for length 0", range-on-range, no / empty / zero ranges; all 62 inline review comments of #11901 | no report |
| Open PRs | 2026-10-27 | changed files of all 409 open PRs | only #12862 touches RangeOnRangeFacetCounts.java; constructors unchanged |
| Mailing lists, JIRA, downstream, web | 2026-10-27 | 40 queries over dev@, java-user@, issues@ and commits@ plus lists.apache.org directly; 8 JIRA queries (the API postdates the JIRA era); the class names across all of GitHub and the Elasticsearch, OpenSearch and Solr repositories; 6 web queries | only feature, commit and release notices and a naming thread |
| Refresh and upstream main | 2026-11-01 | the 81 issues and PRs updated since 2026-10-27 (#16924 to #16958 new); file contents at upstream main 2f424fd428da (2026-10-30) | none about facets; the unguarded line is still at LongRangeOnRangeFacetCounts.java:85 and DoubleRangeOnRangeFacetCounts.java:88 |
Introduced with the API itself in 9.5.0 by #11901 (cbfed77fd33, merged 2022-12-30); not a regression. Every release from 9.5.0 to 10.6.0 and main fails (all executed); 9.4.2 has no range-on-range API.
Versions tested5 rows
Introduced by #11901 "Github#11869: Add RangeOnRangeFacetCounts" (by Marc D'Mello, merged by gsmiller on 2022-12-30): main commit cbfed77fd33 and branch_9x backport d9fc418914a. git log --all -S 'ranges[0].dims' lists only these two commits, and the parent has no rangeonrange package. Later commits to the package (#13328, #14761) changed only the base class's record accessors and priority queue; getEncodedRanges is unchanged. CHANGES.txt, 9.5.0 New Features: "GITHUB#11869: RangeOnRangeFacetCounts added ...".
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (standalone and gradle); passes with fix A or B |
| 10.6.0 | affected | minimal repro fails with assertions on and off (release jars) |
9.5.0 to 10.5.2 (26 releases) and main a9779ef3c193 | first affected 9.5.0 | a version-portable JUnit fails 2 of 4 (Long :85, Double :88), both controls passing; also with assertions off on 9.5.0, 9.12.3, 10.0.0, 10.5.2 and main; the minimal repro fails on 9.5.0 |
| 9.4.2 | no API | does not compile: only the range-on-range classes are missing |
upstream main 2f424fd428da, branch_10_6 | read | the same unguarded lines (source read, not executed) |
Confirmed with high confidence: the finder, an independent reproducer, an intended-behaviour devil's advocate and an adjudication all reproduced it. A reviewer from another model family, unable to run code, said "not real" at low confidence for procedural reasons only and agreed with the root cause.
Checks and verdicts263 words
Everyone agrees on the root cause and that it is a wrong error only: the call fails loudly and nothing is miscounted.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Public-API JUnit fails 1 of 1 on main with assertions on and off, and on 10.5.2 and 9.12.3; with the empty-result fix it passes, and the 20 upstream TestRangeOnRangeFacetCounts tests pass. |
| cross-model review, refute lens (OpenAI GPT-5.6, read-only) | not real, low (procedural) | Could not run code; root cause agrees; called the contract ambiguous and the intended argument moderate. Acted on: the oracle accepts an IllegalArgumentException too. |
| independent reproducer | real | Its own JUnit, in a package outside rangeonrange, fails 4 of 6 on main (assertions on and off), 10.5.2, 9.12.3 and 9.5.0 (all four constructors); controls pass; 9.4.2 has no API; 6 of 6 with the empty-result fix. |
| cross-model review, intended lens (OpenAI GPT-5.6, read-only) | real, medium | The intended argument is moderate but does not hold. A separate re-run of the finder's JUnit and a Double-only test: both fail. |
| intended-behaviour devil's advocate | real, Low | Built the strongest intended case and rejected it; its 7-test probe fails 3 of 7 on main and 10.5.2, the 4 controls passing (classic counters empty, MatchingFacetSetsCounts throws IllegalArgumentException). |
| adjudication | confirmed, high | Re-ran the independent JUnit (4 of 6 fail, assertions on and off) and the finder's. |
| Registration check and credibility audit | confirmed | Re-runs on main fail with assertions on and off; 6 of 6 pass with the fix shadowed. "A real but trivial API-hygiene bug." |
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 425 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,247 words
- Registry title
- LongRangeOnRangeFacetCounts and DoubleRangeOnRangeFacetCounts throw an internal ArrayIndexOutOfBoundsException from every public constructor when given no ranges, instead of returning an empty result like the classic range facet counters or rejecting the empty list with an IllegalArgumentException
- Area
- facet / range-on-range facet counts (LongRangeOnRangeFacetCounts and DoubleRangeOnRangeFacetCounts constructors: getEncodedRanges reads ranges[0])
- Priority
- Low, rank 1122: The opt-in range-on-range facet counters fail loudly with an internal ArrayIndexOutOfBoundsException at construction for an empty range list and miscount nothing, the same wrong-error-on-degenerate-zero profile as the MoreLikeThis maxQueryTerms=0 NPE.
- Impact class
- wrong error
- Impact note
- Hits only applications that use the opt-in range-on-range facet counters (facet module, over LongRangeDocValuesField or DoubleRangeDocValuesField) and build the range list at run time so that it can be empty: all four public constructors (with or without fastMatchQuery, empty varargs or an explicit empty array) fail at once with 'ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0', with -ea and -da, before any document is read; nothing is miscounted and no other request, reader or index is affected, at any scale, in every release from 9.5.0 to 10.6.0 and on main.
- Summary
- You count range-on-range facets with new LongRangeOnRangeFacetCounts(field, hits, QueryType.INTERSECTS) (or the Double variant) and pass no ranges, a call the varargs signature accepts and no javadoc forbids, and expect an empty FacetResult, as the classic LongRangeFacetCounts returns for the same empty request, or at least an IllegalArgumentException that rejects the empty list. Instead the constructor throws 'ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0', because its private helper getEncodedRanges sizes the encoded array from ranges[0].dims without checking that the list is non-empty.
- Introduced
- cbfed77fd33c24ea57e9acaace2c643bc7979b33 (main; apache/lucene#11901 'Github#11869: Add RangeOnRangeFacetCounts', by mdmarshmallow, merged by gsmiller) and its branch_9x backport d9fc418914aef731eda22c2486cb318f741f7b39, the commit in the 9.5.0 release [read: full-history clone, git log --all -S 'ranges[0].dims' lists only these two commits, and getEncodedRanges is unchanged since; exec: 9.4.2 has no rangeonrange package, 9.5.0 fails]. The defect came with the API; not a regression. (2022-12-30 (merged 2022-12-30T15:38:13Z; the branch_9x backport has the same timestamp))
- First affected version
- 9.5.0 [exec: fails with -ea and -da]; 9.4.2 and older are not affected because the rangeonrange package does not exist there [exec: compile error, only the range-on-range symbols are missing]
- Affected versions
- every release from 9.5.0 to 10.6.0 and main a9779ef3c193 (unreleased 11.0) [exec: the one-test minimal repro fails on the 10.6.0 release jars (SHA-1 checked against Maven Central) with -ea and -da (2026-11-01); one version-portable JUnit (Long and Double cases in separate methods, plus two controls) fails 2 of 4 with the same AIOOBE at LongRangeOnRangeFacetCounts.java:85 and DoubleRangeOnRangeFacetCounts.java:88, both controls passing, on all 26 releases 9.5.0, 9.6.0, 9.7.0, 9.8.0, 9.9.0-9.9.2, 9.10.0, 9.11.0, 9.11.1, 9.12.0-9.12.3, 10.0.0, 10.1.0, 10.2.0-10.2.2, 10.3.0-10.3.2, 10.4.0, 10.5.0-10.5.2 and on main with -ea, and with -da on 9.5.0, 9.12.3, 10.0.0, 10.5.2 and main; the independent reproduction's JUnit fails 4 of 6 on main (-ea, -da), 10.5.2, 9.12.3 and 9.5.0]. branch_10_6 (head 2763db11b0a): the three files are byte-identical to 10.5.2 [read, not executed]. Not fixed upstream: upstream main 20ced71c1e6 has byte-identical files, the 4 main commits after a9779ef3c193 do not touch lucene/facet, and no open PR changes getEncodedRanges or the constructors [read, 2026-10-27]. Executed on Linux aarch64 with JDK 25 only.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal
- Upstream and prior reports
- none found (searched 2026-10-27, read-only; the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches found nothing either): two scripted prior-report searches (20 GitHub issue/PR queries each plus commit, downstream, mailing-list over dev@, java-user@, issues@ and commits@, JIRA and web queries; 1816 and 714 distinct hits, none high overlap), manual GitHub searches over apache/lucene, GitHub-wide and Elasticsearch, OpenSearch and Solr, the file lists of all 409 open apache/lucene PRs, lists.apache.org, and the full review threads of #11901. Nearest items, none the same bug: #11869 and #11901 (introduced the classes; no discussion of an empty range list), #12049 (test-only dimension overflow), #12408 (zero hits, not zero ranges), #127 / LUCENE-9946 (added the classic counters' deliberate empty-range support and its tests), #12862 (open; the only open PR that touches RangeOnRangeFacetCounts.java, adds getBulkSpecificValues, does not touch the constructors). Refresh (2026-10-27, read-only): no apache/lucene PR or issue was created after those already checked (PRs up to #16923, issues up to #16922, none touching lucene/facet); searches for RangeOnRangeFacetCounts, LongRangeOnRangeFacetCounts, DoubleRangeOnRangeFacetCounts, rangeonrange and getEncodedRanges return only #11869, #11901, #12049, #12408 and #13542. Not fixed upstream (main 20ced71c1e6, branch_10_6 2763db11b0a). Gaps: GitHub search reads only the first page per query; list and JIRA search is keyword-based.
- Lucene commit tested
- main
645d4fe14a20(the minimal repro with assertions on and off, standalone and through gradle with-Ptests.asserts=trueand=false; both fix options through gradle, with the whole facet module test suite, assertions on and off); SHA-1-checked release jars: the minimal repro on 10.6.0 and 9.5.0 (assertions on and off); a version-portable JUnit (Long and Double, two controls) on all 26 releases from 9.5.0 to 10.5.2 and on maina9779ef3c193, and on 9.4.2 (does not compile: the API is absent). The three range-on-range files are byte-identical ata9779ef3c193,645d4fe14a20and upstream main2f424fd428da(2026-10-30, read).
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/LongRangeOnRangeFacetCounts.java:27-36 (at a9779ef3c193) (class javadoc: 'given a range (or list of ranges)'), :38-53 and :55-81 (the two public constructors, LongRange... varargs, '@param ranges ranges we want the counts of', no stated minimum), :84-90 (getEncodedRanges: 'new byte[ranges.length][2 * Long.BYTES * ranges[0].dims]' at :85 with no length check; fix site)lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/DoubleRangeOnRangeFacetCounts.java:87-93 (at a9779ef3c193) (the same helper for DoubleRange, ranges[0].dims at :88; fix site)lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/RangeOnRangeFacetCounts.java:60 (at a9779ef3c193) (assert reads encodedRanges[0]) and :83 (count() reads encodedRanges[0] for dims); never reached with no ranges because getEncodedRanges throws firstlucene/facet/src/java/org/apache/lucene/facet/range/ExclusiveLongRangeCounter.java:155-157 (at a9779ef3c193) ('If no ranges were requested ...') and range/LongRangeCounter.java:194-197 (the classic counters handle an empty range list on purpose); lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java:383-438 (testEmptyRangesSingleValued/MultiValued assert an empty result)lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:74-79 (at a9779ef3c193) (sibling that rejects empty and mixed-dimension input with IllegalArgumentException)
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/LongRangeOnRangeFacetCounts.java:26-36, :42-53, :67-82, :84-90 (at 645d4fe14a20)lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/DoubleRangeOnRangeFacetCounts.java:42-56, :70-85, :87-93 (at 645d4fe14a20)lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/RangeOnRangeFacetCounts.java:48-67, :83, :133-174 (at 645d4fe14a20); Range.java:49-51
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/LongRange.java:74-76, :108-121 (checkArgs calls failNoMatch, an IllegalArgumentException); lucene/core/src/java/org/apache/lucene/document/LongRange.java:134-139 (verifyAndEncode: IllegalArgumentException for min > max) (at 645d4fe14a20)
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/LongRangeOnRangeFacetCounts.java:27-29, :64; Range.java:24, :44-46 (at 645d4fe14a20)lucene/facet/src/java/org/apache/lucene/facet/range/ExclusiveLongRangeCounter.java:155-157; range/LongRangeCounter.java:195-197; facetset/MatchingFacetSetsCounts.java:74-79 (at 645d4fe14a20)lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java:383-438; lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCounts.java:835, :991, :1151, :1308 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/document/LongRange.java:110-112; lucene/CHANGES.txt:2756 (9.11.1 Bug Fixes, GITHUB#13493) (at 645d4fe14a20)github.com/apache/lucene/issues/11869 and pull/11901 (bodies; read-only GET 2026-11-01)github.com/apache/lucene pull/841 (opened by Marc D'Mello, merged by Shai Erera; the IllegalArgumentException checks of MatchingFacetSetsCounts came in PR commit 1fbcac901d by Shai Erera; read-only GET, fact check 2026-11-01)
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:74-79; lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java:383-438 (at 645d4fe14a20)lucene/facet/src/java/org/apache/lucene/facet/rangeonrange/RangeOnRangeFacetCounts.java:176-185 (getSpecificValue, getAllDims) (at 645d4fe14a20)
- Prior reports
github.com/apache/lucene contents of the two counter files at main 2f424fd428da (read-only GET 2026-11-01)
- History
GitHub API (read-only, 2026-11-01): commits touching facet/rangeonrange on main (newest #14761) and branch_10_6 (newest #13328)
- TestRangeOnRangeFacetCountsNoRanges.java (JUnit, one test):
repro/LFV-799/src/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCountsNoRanges.java(41 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.
assets/LFV-799/bug.png(425 KB)
![Diagram of the bug in four steps. 1: the user calls new LongRangeOnRangeFacetCounts("price", hits, ...) with no ranges, so ranges.length is 0. 2: the constructor evaluates the arguments of super(...) first, so it calls getEncodedRanges(ranges) (line 51). 3: getEncodedRanges evaluates new byte[ranges.length][2 * Long.BYTES * ranges[0].dims] and throws, because there is no ranges[0] (line 85; Double variant line 88). 4: the base constructor and count() are never reached; no document is read and nothing is miscounted. Expected: an empty FacetResult (childCount=0), as the classic LongRangeFacetCounts gives, or an IllegalArgumentException. Actual: java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0.](../assets/LFV-799/bug.png)