- A search sorted by fields keeps its best numHits hits in a priority queue,
FieldValueHitQueue. Each sort field gets a comparator (for the built-in sort types) that stores one value per queue slot, so it allocates an array of numHits values up front. - A Java array holds at most about 2^31 entries. Since 4.7.0 (SOLR-5661) the
PriorityQueueconstructor has checked for sizes too big for an array, "to prevent confusing OOME". Its current form, since 7.5.0, refuses a size ofArrayUtil.MAX_ARRAY_LENGTH(2,147,483,631) or more with anIllegalArgumentException.
newCollector() into FieldValueHitQueue.create, where the comparator's new long[numHits] fails before the PriorityQueue size check is reached; then 10.x's order, where the check runs first. Select the still to open it at full size. Generated with AI, checked against the code.- Sort by a long field and ask for every hit with Java's usual "no limit":
new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector(). Core'sSortRescorer.rescore(searcher, topDocs, Integer.MAX_VALUE)reaches the same code throughnew TopFieldCollectorManager(sort, topN, null, Integer.MAX_VALUE)(SortRescorer.java:49). - No queue can hold 2,147,483,647 slots: Java arrays end just below 2^31. You expect the clear refusal that every release from 7.5.0 to 10.6.0 gives, here 10.5.2 and 10.6.0:
IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647. Main's score-sorted collector still refuses the size too, with anIllegalArgumentExceptionfrom its own copy of the guard. - On main you get
OutOfMemoryError: Requested array size exceeds VM limit, thrown inLongComparator's constructor, with assertions on and off. - Why: the size check sits in the
PriorityQueueconstructor. Since #14873 (2025),FieldValueHitQueue.createfirst builds the sort comparators, each built-in one allocating numHits slots (new long[numHits]), and creates the queue, with its check, only afterwards (FieldValueHitQueue.java:146runs before:153). - Only the error changes: the call is refused either way, and
IndexSearcher.search(query, n, sort)caps n at maxDoc, so it is not affected. But code that catchesIllegalArgumentExceptionmisses it, and a JVM run with-XX:+ExitOnOutOfMemoryErrorexits.
Code on a main or 11.0 branch build that passes numHits of 2,147,483,631 or more, in practice Integer.MAX_VALUE for "all", to a field-sorted collector without capping it: direct TopFieldCollectorManager use, SortRescorer and the expressions rescorer. Any built-in sort type, default settings. IndexSearcher.search(query, n, sort) caps n and is safe. No release is affected. Rare.
Mechanism, terms and numbers584 words
On main, new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector() with a built-in sort (long, string, relevance and index order executed) throws OutOfMemoryError: Requested array size exceeds VM limit. Lucene 9.10.0 to 10.6.0 refuse the same call with IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647, releases from 7.5.0 refuse the equivalent TopFieldCollector.create call the same way, and main's score-sorted TopScoreDocCollectorManager still throws an IllegalArgumentException (with its own message). Since #14873 (39549329509, 2025-07-03), FieldValueHitQueue.create builds the sort comparators, each pre-allocating numHits slots, before it calls the constructor that reaches PriorityQueue's size check, a guard SOLR-5661 added in 4.7.0 "to prevent confusing OOME" and Lucene fixed again for exactly Integer.MAX_VALUE in 2018 (7.5.0). The call never succeeded on any version, so only the error type changes; IndexSearcher.search(query, n, sort) caps n at maxDoc and is not affected, while direct users and SortRescorer are. No release has it; the 11.0 release branch branch_11x has the same code. A small up-front size check in create() restores the old error; it passes the minimal test with assertions on and off, the closest core suites and core's static checks.
TopFieldCollectorManager: builds the collectors for a search sorted bySortfields. Its javadoc says the collectors "pre-allocate a full array of lengthnumHits" (TopFieldCollectorManager.java:44-45,:96-97); its constructor rejects onlynumHits <= 0(:62-64).FieldValueHitQueue: the field-sorted hit queue, aPriorityQueue(@lucene.experimental), built by the staticcreate(fields, size).- The guard:
PriorityQueue's constructor throwsIllegalArgumentExceptionformaxSize < 0 || maxSize >= ArrayUtil.MAX_ARRAY_LENGTH, with the comment "Throw exception to prevent confusing OOME" (PriorityQueue.java:121-125).MAX_ARRAY_LENGTHis 2147483631 here, so on main and since 7.5.0 the guard covers the 17 values 2147483631 to 2147483647.
newCollector()callsFieldValueHitQueue.create(sort.getSort(), numHits)(TopFieldCollectorManager.java:112-114), the only production caller ofcreate.create()(FieldValueHitQueue.java:132-159) first loops over the sort fields and callsfield.getComparator(size, ...)for each (:142-151, the call at:146). Each built-in comparator allocates size slots in its constructor:LongComparator.java:38values = new long[numHits];, and likewise the int, float, double, relevance, string and doc comparators. ACUSTOMsort gets its comparator from user code (SortField.java:511-514); one that does not pre-allocate reaches the guard and gets the normalIllegalArgumentException.- Only then does it call
new FieldValueHitQueue<>(fields, size, ...)(:153-158), whose constructor callssuper(size, lessThan)(:114-115) and so reaches the guard. ForInteger.MAX_VALUEthe first comparator'snew long[2147483647]has already thrownOutOfMemoryError: Requested array size exceeds VM limit, so the guard never runs. - Up to 10.6.0 the queue's constructor called
super(size)first and built the comparators after it (10.5.2 and 10.6.0FieldValueHitQueue.java:122-123). #14873 moved the comparator loop intocreate(), ahead of the constructor, because the newLessThanobject needs the comparators.
numHits = 2147483647 (Integer.MAX_VALUE): main throws OutOfMemoryError: Requested array size exceeds VM limit from LongComparator.<init>:38 via FieldValueHitQueue.create:146; 10.5.2 and 10.6.0 throw IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647. numHits = 2147483631 with a 1 GB heap: main OutOfMemoryError: Java heap space, 10.5.2 the IllegalArgumentException. numHits = 2147483630, one below the window: main, 10.5.2 and 10.6.0 throw OutOfMemoryError: Java heap space (4.7.0, whose older guard was wider, throws its IllegalArgumentException). The score-sorted TopScoreDocCollectorManager on main throws IllegalArgumentException: initialCapacity must be > 0 and < 2147483630 for the window, from TernaryLongHeap's copy of the guard.
Exposure and likelihood232 words
- Who is hit. Code on a main / 11.0 build that passes a
numHitsof 2147483631 or more (in practiceInteger.MAX_VALUEmeaning "all") to a field-sorted collector without capping it: directTopFieldCollectorManagerusers, and callers that forward an uncapped size, such asSortRescorer.rescore(searcher, topDocs, topN)(SortRescorer.java:49) andExpressionRescorer, which extends it. A manager built with such a size also fails insideIndexSearcher.search(query, manager), which callsnewCollector()without capping (IndexSearcher.java:750-752). Any built-inSortFieldtype, includingSort.RELEVANCEandSort.INDEXORDER(aCUSTOMsort depends on its comparator source), default settings. - Not hit.
IndexSearcher.search(query, n, sort)andsearchAftercap n atmax(1, maxDoc)before building the manager (IndexSearcher.java:721,:729,:733). No release: none throws thisOutOfMemoryErrorfrom a comparator (sampled from 4.7.0 to 10.6.0). - What changes. Only the error: the call never succeeded on any version. Code that catches
IllegalArgumentExceptionto report a bad size no longer catches it. With-XX:+ExitOnOutOfMemoryErrorthe JVM terminates on main ("Terminating due to java.lang.OutOfMemoryError: Requested array size exceeds VM limit", exit 3) where 10.5.2 throws a catchable exception and keeps running. Lucene's gradle test JVMs run with-XX:+HeapDumpOnOutOfMemoryError(TestsAndRandomizationPlugin.java:451inbuild-tools/build-infra), so the recorded test run wrote a heap dump. - Scale and likelihood. No index or data plays a part; the first such call fails at once, so a developer meets it in testing. Likelihood rare, priority Low.
One JUnit test on the public API: a collector sorted by a long field with numHits = Integer.MAX_VALUE. It expects the IllegalArgumentException that 10.x throws. No index, deterministic, single thread, about 1.2 s; the same 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.tests.util.LuceneTestCase;
public class TestTopFieldCollectorManagerMaxNumHits extends LuceneTestCase {
public void testNumHitsIntegerMaxValue() {
Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
// Expected: PriorityQueue's IllegalArgumentException (as in 10.x); actual: OutOfMemoryError.
expectThrows(
IllegalArgumentException.class,
() -> new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector());
}
}
cp TestTopFieldCollectorManagerMaxNumHits.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647, as on 10.5.2 and 10.6.0.- Observed
OutOfMemoryError: Requested array size exceeds VM limitatLongComparator.<init>(LongComparator.java:38), viaFieldValueHitQueue.create(FieldValueHitQueue.java:146); the same with assertions off.
More files, environment, full output206 words
TestTopFieldCollectorManagerMaxNumHits builds new Sort(new SortField("price", SortField.Type.LONG)) and asserts expectThrows(IllegalArgumentException.class, () -> new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector()). It sits in org.apache.lucene.search like TestTopFieldCollector but uses only public API; no randomness of its own. Integer.MAX_VALUE gives the heap-independent "Requested array size exceeds VM limit"; at 2147483631, the bottom of the window, main gives the heap-dependent Java heap space (1 GB heap); the values in between were not run. It compiles unchanged against 10.5.2 and 10.6.0, where it passes with assertions on and off.
As the regression test: this same test, for example as a method in TestTopFieldCollector. It passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); passes on 10.5.2 and 10.6.0; no release is affected |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestTopFieldCollectorManagerMaxNumHits.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Dumping heap to java_pid4670.hprof ...
TestTopFieldCollectorManagerMaxNumHits > testNumHitsIntegerMaxValue FAILED
junit.framework.AssertionFailedError: Unexpected exception type, expected IllegalArgumentException but got java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at __randomizedtesting.SeedInfo.seed([DEADBEEF:FCF900E87FF408D6]:0)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1437)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
at app//org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits.testNumHitsIntegerMaxValue(TestTopFieldCollectorManagerMaxNumHits.java:26)
Caused by:
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at org.apache.lucene.search.comparators.LongComparator.<init>(LongComparator.java:38)
at org.apache.lucene.search.SortField.getComparator(SortField.java:503)
at org.apache.lucene.search.FieldValueHitQueue.create(FieldValueHitQueue.java:146)
at org.apache.lucene.search.TopFieldCollectorManager.newCollector(TopFieldCollectorManager.java:114)
at org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits.lambda$testNumHitsIntegerMaxValue$0(TestTopFieldCollectorManagerMaxNumHits.java:28)- The guard says why it exists: "Throw exception to prevent confusing OOME" (
PriorityQueue.java:122). It was added in 4.7.0 (SOLR-5661) and fixed again in 2018 for exactlyInteger.MAX_VALUE. - Main keeps the guard elsewhere: its score-sorted collector still throws an
IllegalArgumentException, fromTernaryLongHeap's copy. - #14873 was an API refactor; its CHANGES entry, migration note and review never mention size checks.
The full argument246 words
- Stated purpose.
PriorityQueue.java:121-125:if ((maxSize < 0) || (maxSize >= ArrayUtil.MAX_ARRAY_LENGTH)) { // Throw exception to prevent confusing OOME:.CHANGES.txt:12883-12884, 4.7.0 "Bug fixes": "SOLR-5661: PriorityQueue now refuses to allocate itself if the incoming maxSize is too large (Raintung Li via Mike McCandless)". - Fixed again for exactly this value. From 4.9.1 to 7.4.0 the guard let
Integer.MAX_VALUEoverflow past it (LUCENE-5844,b293b947cb3, movedheapSize = maxSize + 1ahead of the check), so the call threwNegativeArraySizeException. Michael McCandless fixed that in 2018 (4c646dab3fb, "Fix PriorityQueue constructor to throw IllegalArgumentException if you pass Integer.MAX_VALUE as maxSize (thanks rsaavedraf)", released in 7.5.0), writing today's check and message and addingTestPriorityQueue.testMaxIntSize. - Re-applied in newer heaps.
LongHeap.java:57-58andTernaryLongHeap.java:57-58carry the same check and comment; main'sTopScoreDocCollectorManagerrejects these sizes with anIllegalArgumentExceptionfrom that copy ("initialCapacity must be > 0 and < 2147483630"), so the two top-N collector families now disagree. - A refactor, not a decision. #14873 ("Change PriorityQueue to take a LessThan interface implementation in its constructor rather than overriding the lessThan abstract method",
CHANGES.txt:75-76, 11.0.0 API Changes) moved the comparator loop for a structural reason; its MIGRATE note and review thread discuss the API and performance, not size validation. - Tests.
TestPriorityQueue.testMaxIntSize(TestPriorityQueue.java:378-382) pins theIllegalArgumentExceptionfor the bare queue (added by the 2018 fix); no core search test gives a field-sorted collector a numHits nearInteger.MAX_VALUE(those arguments aretotalHitsThreshold), so nothing caught the reorder.
The devil's-advocate check190 words
The strongest case for "intended" (put forward by an independent reviewer from another model family, OpenAI GPT-5.6, and argued in full by the intended-behaviour check): no javadoc promises the IllegalArgumentException; FieldValueHitQueue is @lucene.experimental and PriorityQueue @lucene.internal; both javadocs document the numHits pre-allocation, and create() says only "Must be greater than zero"; IndexSearcher caps n, and, as Michael McCandless pointed out on SOLR-5661, callers should bound the size they pass (Adrien Grand described top-k collection as designed for "some reasonable value of k" on LUCENE-8542); and on main and in 10.x the guard covers only 17 values: at 2147483630, main, 10.5.2 and 10.6.0 throw a heap OutOfMemoryError (1 GB heap).
Why it fails: the guard is a deliberate bug fix with a stated purpose, Lucene fixed it again in 2018 for exactly Integer.MAX_VALUE, re-applies it in newer heaps, main's other top-N collector still has it, and the refactor did not mean to drop it; core's own SortRescorer forwards an uncapped topN into this path. The honest residue: the fix restores a clearer error, not a capability, and the contract rests on intent and history, not on a javadoc.
Recommended A. Check the size in FieldValueHitQueue.create before building comparators
Check the size in FieldValueHitQueue.create right after its empty-sort check, before any comparator is built, and throw the same IllegalArgumentException as PriorityQueue. It restores the 10.x error for every built-in field sort. With it the test passes with assertions on and off, and the closest core suites and core's static checks pass.
All options and trade-offs3 options · 613 words
Option A was validated through gradle in a scratch worktree of 645d4fe14a20. B and C come from the adjudication's and the reviewers' notes; neither was run.
A. Check the size in FieldValueHitQueue.create before building comparators (validated by the checks) (recommended)
In FieldValueHitQueue.create, right after the empty-sort check, throw the PriorityQueue message when size < 0 || size >= ArrayUtil.MAX_ARRAY_LENGTH, and import ArrayUtil.
diff --git a/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java b/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
index 104b438a721..ff0fbcc1948 100644
--- a/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
+++ b/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
@@ -18,6 +18,7 @@ package org.apache.lucene.search;
import java.io.IOException;
import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.PriorityQueue;
/**
@@ -135,6 +136,12 @@ public class FieldValueHitQueue<T extends FieldValueHitQueue.Entry> extends Prio
if (fields.length == 0) {
throw new IllegalArgumentException("Sort must contain at least one field");
}
+ // the comparators below pre-allocate arrays of this size: reject it before they do, as
+ // PriorityQueue would (SOLR-5661), instead of failing with a confusing OutOfMemoryError
+ if (size < 0 || size >= ArrayUtil.MAX_ARRAY_LENGTH) {
+ throw new IllegalArgumentException(
+ "maxSize must be >= 0 and < " + ArrayUtil.MAX_ARRAY_LENGTH + "; got: " + size);
+ }
int numComparators = fields.length;
FieldComparator<?>[] comparators = new FieldComparator<?>[numComparators];Trade-offs
- Restores the 10.x behaviour exactly, with the same message, at the one place every field-sorted collector goes through, including external callers of the public
create(). - Cost: one comparison per
newCollector(), not per hit. - It duplicates the guard's bound and message; a shared helper could avoid that.
- It turns the main-only
OutOfMemoryErrorof uncapped callers (theSortRescorerand grouping findings LFV-997 and LFV-964) back into this exception without fixing their own sizing. - The diff is against
645d4fe14a20;git applyrejects it on upstream main2f424fd428daand onbranch_11x(#16857 added an import next to its context). The same 7 lines regenerated against2f424fd428dapassgit apply --checkthere and onbranch_11x, but were not compiled or tested on those revisions. - Like
PriorityQueue, it acceptssize == 0, whilecreate()'s javadoc says the size "Must be greater than zero" (FieldValueHitQueue.java:130);TopFieldCollectorManageralready rejectsnumHits <= 0, so only directcreate()callers can pass 0.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seedDEADBEEF, 2026-11-01: without the fix the test fails with assertions on and off; with it, it passes with assertions on and off. - Closest core suites with the fix:
TestTopFieldCollector(13 tests),TestTopFieldCollectorEarlyTermination(5),TestSort(30),TestSortRandom(1),TestSortOptimization(31),TestSortRescorer(3),TestPriorityQueue(18) and the new test: 8 classes, 102 tests, 0 failures, 0 skipped. ./gradlew :lucene:core:check -x testwith the fix and the test as a new file: passes (google-java-format, ecj lint of main and test code, forbidden APIs). The full Lucene test suite was not run.- Earlier: the finder's 4-test JUnit passes with the fix (assertions on and off), and so do
TestTopFieldCollector+TestSort(43 of 43) ona9779ef3c193; the minimal test passes with the fix shadowed into the stock jars.
Next to the existing numHits <= 0 check (TopFieldCollectorManager.java:62-64).
if (numHits >= ArrayUtil.MAX_ARRAY_LENGTH) {
throw new IllegalArgumentException(
"numHits must be < " + ArrayUtil.MAX_ARRAY_LENGTH + "; got: " + numHits);
}Trade-offs
- Fails earlier, at construction, and covers every Lucene caller today:
newCollector()is the only production caller ofcreate(). - Misses direct callers of the public
FieldValueHitQueue.create.
What the checks validated
Not run.
Restore the old order inside create().
Trade-offs
The LessThan handed to the queue's constructor needs the comparators, so this needs a late-bound comparator holder: more change for the same effect.
What the checks validated
Not run.
Applications that forward an unbounded size such as Integer.MAX_VALUE to a field-sorted collector and treat OutOfMemoryError as fatal exit on main where 10.x threw a catchable exception; capping numHits at maxDoc, as IndexSearcher does, avoids it.
Non-formal: an early probe saw SortRescorer with topN = Integer.MAX_VALUE throw OutOfMemoryError on main but IllegalArgumentException on 10.5.2 and the 10.6 release candidate. An AI finder traced that difference by code reading and git history to #14873 and confirmed it with a public-API test across main and releases.
The method in full134 words
- First signal: an executed probe on the stock jars (2026-10-28) ran
SortRescorerand the expressions rescorer with topN =Integer.MAX_VALUEand sawOutOfMemoryError: Requested array size exceeds VM limiton main but thePriorityQueueIllegalArgumentExceptionon 10.5.2 and the 10.6 release candidate. - Root cause: the finder, explaining that main-vs-release difference, read
FieldValueHitQueue.createand its git history and found the comparator loop that #14873 moved ahead of the queue constructor. - Confirmation: a public-API JUnit (long, relevance and string sorts plus a score-sorted control) failed 3 of 4 on main and passed on 10.5.2 and the release candidate. No model or formal tool was involved, so the method is non-formal.
- Later: independent verifiers, cross-model reviews (reading only), an adjudication, the registration check, a credibility audit, the one-test minimal repro (2026-10-29) and this doc's gradle run (2026-11-01).
No matching public report found in GitHub issues, PRs and commits, the JIRA archive, the Lucene mailing lists, downstream projects or the web (searched 2026-10-29, refreshed 2026-11-01). SOLR-5661 (2014) is the fixed original whose guard this bypasses. Not fixed upstream as of 2026-10-30.
Searches3 searches
Related, none a report of this regression: SOLR-5661 (2014, reported with a patch by Raintung Li, committed by Michael McCandless; the same symptom, whose fix added the guard; its trace came from Solr's own ShardFieldSortedHitQueue), #14873 (the change), #14872 (open: remove the last PriorityQueue subclasses; no mention of the size check), #16857 (merged 2026-10-29) and #16947 (merged 2026-10-30), which edit FieldValueHitQueue.java but keep the order, and #16277 (open, 3-ary heap; constructor arity only). SOLR-5661 is the statement of intent, not a report of this bug.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits | 2026-10-29 | two scripted prior-report searches (16 issue/PR and 3 commit queries each) and 19 manual searches: FieldValueHitQueue, TopFieldCollectorManager, the OOM text, "maxSize must be", "confusing OOME", SortRescorer, 14873 regression; the changed files of 141 open PRs updated since 2026-10-01; commit histories of the touched files | no report; #14873 introduced it |
| Mailing lists, JIRA archive, downstream, web | 2026-10-29 | dev@, java-user@, issues@, commits@ (2 years); JIRA incl. SOLR-5661 and its comments; Elasticsearch, OpenSearch and Solr issues; web; 3,476 and 692 distinct hits, 0 high overlap | no report; SOLR-5661 is the fixed original |
| Refresh | 2026-11-01 | by the fact check of this doc: every apache/lucene issue and PR updated since 2026-10-29, filtered for FieldValueHitQueue, TopFieldCollectorManager, "confusing OOME", "Requested array size", "maxSize must be" and 14873; upstream commits on FieldValueHitQueue.java up to 2f424fd428da | only #16857 (merged PR, keeps the order) and the unrelated closed issue #16887; commits #16857 and #16947 keep the order |
Introduced on main by #14873 (39549329509, 2025-07-03), an API refactor of PriorityQueue; bisected by building the commit and its parent; also on branch_11x, the 11.0 release branch. No release is affected: 7.5.0 to 10.6.0 throw the quoted IllegalArgumentException, older releases fail inside PriorityQueue itself. The guard dates from SOLR-5661 (4.7.0), its current form from 7.5.0.
Versions tested9 rows
Introduced by #14873 "Make PriorityQueue use LessThan interface by default" (commit 39549329509, Simon Cooper, 2025-07-03, milestone 11.0.0; cited as the source of the change, not as blame). It turned FieldValueHitQueue from an abstract PriorityQueue subclass, whose constructor called super(size) before building the comparators, into a class built by the static create(), which builds the comparators first. git blame shows no later change to the order; upstream #16857 and #16947 edited the file without touching it. Releases before 4.7.0 throw an OutOfMemoryError from PriorityQueue itself: the original SOLR-5661 bug, long fixed. The 11.0 release branch branch_11x, cut from main at c5ab3e57b9b on 2026-10-28 (#16929), carries #14873 and the same create() with no size check (read at 603af870375; the fact check read the same on GitHub on 2026-11-01), so a fix needs main and branch_11x. branch_10_6 keeps super(size) first; branch_10x was decommissioned on 2026-10-29 (#16911). What older releases throw for Integer.MAX_VALUE depends on the guard's version: 4.7.0 to 4.9.0 an IllegalArgumentException worded "maxSize must be <= 2147483391" (a guard of Integer.MAX_VALUE - 256), 4.9.1 to 7.4.0 a NegativeArraySizeException (LUCENE-5844 let this one value overflow past the guard), and 7.5.0 on the quoted message, after Michael McCandless's 2018 fix 4c646dab3fb.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails with assertions on and off (standalone and gradle); passes with fix A |
main 39549329509 (#14873) vs parent 62e02760321 | bisect | javac builds of core: 0 of 3 field sorts get the IllegalArgumentException at #14873, 3 of 3 at its parent |
upstream main 2f424fd428da (2026-10-30) | read | same order in create() (source read, not executed) |
branch_11x 603af870375 (2026-10-29) | read | same order in create(), no size check (source read, not executed; the fact check read the same on GitHub on 2026-11-01) |
| 10.6.0, 10.5.2 | not affected | minimal test passes (IllegalArgumentException), SHA-1-checked jars |
| 10.0.0, 9.12.3, 9.10.0, 8.11.4, 7.5.0 | not affected | IllegalArgumentException (probes; the quoted message executed on 7.5.0, 8.11.4 and 9.10.0) |
| 4.9.1, 4.10.0, 5.5.5, 6.6.6, 7.3.1, 7.4.0 | guard overflow | NegativeArraySizeException: -2147483648 from PriorityQueue itself (LUCENE-5844 let Integer.MAX_VALUE overflow past the guard; fixed in 7.5.0); not this regression |
| 4.9.0, 4.7.0 | not affected | IllegalArgumentException: maxSize must be <= 2147483391 (an older, wider guard; 4.7.0 also refuses 2147483630) |
| 4.6.1 | original SOLR-5661 | OutOfMemoryError from PriorityQueue itself, before the guard existed; not this regression |
Confirmed by the finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication and a credibility audit, each re-running a test; reviewers from another model family (OpenAI GPT-5.6) read the code but could not run it. This doc's gradle run validated the fix.
Checks and verdicts363 words
Nine checks; none refuted the root cause. The cross-model reviews and the cross-model fact check were reading only.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Its JUnit fails 3 of 4 on main with assertions on and off (control passes) and passes on 10.5.2 and the 10.6 RC; the fix passes it and TestTopFieldCollector + TestSort (43 of 43). |
| Independent reproducer | real, low severity | Own 7-test JUnit outside org.apache.lucene (LONG, RELEVANCE, INDEXORDER, LONG+DOC, STRING at 2147483631, IndexSearcher.search with the manager, a control): 6 of 7 fail on main, 7 of 7 pass on 10.5.2 and 9.12.3. |
| Cross-model adversary (OpenAI GPT-5.6, refute lens, reading only) | not real, by procedure only | Could not run code, so its verdict is procedural; it agreed on the root cause and the trigger. Re-runs for it: 3 of 4 fail on main. |
| Intended-behaviour devil's advocate | real, medium | Built the strongest "intended" case and found none of it holds in full; its boundary probe established the 17-value window on main and 10.5.2. |
| adjudication | confirmed, medium confidence | Re-ran the independent reproduction's JUnit with a new seed: 6 of 7 fail on main, 7 of 7 pass on 10.5.2. |
| Registration check and credibility audit | confirmed | Fresh-seed re-runs reproduce; the -XX:+ExitOnOutOfMemoryError demo exits on main and keeps running on 10.5.2. |
| brief validation (gradle) | fix A validated | On stock 645d4fe14a20 the test fails with assertions on and off; with fix A it passes, the 7 closest core suites pass (8 classes, 102 tests, 0 failures) and :lucene:core:check -x test passes. |
| fact check (2026-11-01) | bug holds; 4 wording and history fixes | Re-ran the minimal test (standalone and gradle, assertions on and off) and fix A with the closest suites; probed 13 releases from 4.6.1 to 10.6.0, which corrected the release history; found branch_11x affected too. All fixed in this doc. |
| cross-model fact check (OpenAI GPT-5.6, reading only) | core bug real; wording fixes | Agreed on the mechanism and the repro; asked to limit the claim to built-in sorts (CUSTOM depends on user code), to describe SortRescorer's call exactly and to scope the 17-value window. All fixed in this doc. |
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 472 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,520 words
- Registry title
- On main, a field-sorted TopFieldCollectorManager with numHits >= ArrayUtil.MAX_ARRAY_LENGTH (e.g. Integer.MAX_VALUE) throws OutOfMemoryError instead of the IllegalArgumentException that 10.x throws, because since #14873 FieldValueHitQueue.create allocates the numHits-sized sort comparators before the PriorityQueue size check runs (unreleased regression of the SOLR-5661 guard)
- Area
- core / search: field-sorted top-N collection (FieldValueHitQueue, TopFieldCollectorManager)
- Priority
- Low, rank 1131: On unreleased main and branch_11x only, a call that was always refused now throws OOM instead of IAE, and IndexSearcher.search caps n, so it is Low.
- Impact class
- wrong error
- Impact note
- Only main and the 11.0 release branch branch_11x (11.0.0-SNAPSHOT, unreleased), since #14873: an application that passes a numHits in [2147483631, 2147483647] (in practice Integer.MAX_VALUE meaning 'all') to TopFieldCollectorManager directly, or through a caller that does not cap it (SortRescorer.rescore, ExpressionRescorer), with any built-in sort and default settings, gets an OutOfMemoryError from newCollector() instead of the IllegalArgumentException that 7.5.0 to 10.6.0 throw; the call is refused either way, IndexSearcher.search(query, n, sort) is not affected because it caps n at maxDoc, and a JVM run with -XX:+ExitOnOutOfMemoryError exits instead of letting the caller catch the error.
- Summary
- You call new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector() with any built-in sort (a long field, a string field, Sort.RELEVANCE), directly or through SortRescorer.rescore(searcher, topDocs, Integer.MAX_VALUE), and expect the IllegalArgumentException 'maxSize must be >= 0 and < 2147483631' that Lucene 7.5.0 to 10.6.0 throw (main's score-ordered TopScoreDocCollectorManager still throws an IllegalArgumentException, with its own message). On main you get OutOfMemoryError 'Requested array size exceeds VM limit' instead ('Java heap space' at 2147483631 with a 1 GB heap). Since #14873, FieldValueHitQueue.create builds the sort comparators, each of which pre-allocates a numHits-sized array, before it calls the PriorityQueue constructor, whose size check exists to prevent exactly this confusing OutOfMemoryError (SOLR-5661).
- Introduced
- 39549329509 (#14873, PR title 'Make PriorityQueue use LessThan interface by default', commit 'Change PriorityQueue to use LessThan interface by default (#14873)', by Simon Cooper (thecoop); base main, milestone 11.0.0): it turned FieldValueHitQueue from an abstract PriorityQueue subclass, whose constructor called super(size) and so reached the SOLR-5661 guard before it built the comparators, into a class built by the static create(), which builds the comparators first and only then calls the constructor [exec: javac core builds of the parent 62e02760321 (3 of 3 field sorts get the IllegalArgumentException) and of 39549329509 (0 of 3, OutOfMemoryError from FieldValueHitQueue.create:146); read: git blame at a9779ef3c193, nothing later changed the order]. #14873 was an API refactor; its CHANGES entry (11.0.0 API Changes), MIGRATE.md and review thread say nothing about size validation. Cited as the source of the change, not as blame. (2025-07-03 (author and commit date of 39549329509, 16:04:41+01:00; PR merged 2025-07-03T15:04:41Z))
- First affected version
- unreleased (main [exec], 12.0.0-SNAPSHOT at e1faeba08bd, and the 11.0 release branch branch_11x [read], 11.0.0-SNAPSHOT; the first affected release would be 11.0.0); every release from 4.7.0 to 10.5.2 and 10.6.0 (released 2026-10-29 from RC 4ae946e5a9b) is unaffected
- Affected versions
- [exec] main from 39549329509 (2025-07-03) through a9779ef3c193 fails: the finder's JUnit 3 of 4 with -ea and -da (the control passes), the independent reproduction's JUnit 6 of 7 with -ea and -da, a history probe 0 of 3 field sorts with the IllegalArgumentException at 39549329509 and at a9779ef3c193. [exec] not affected: 4.7.0, 8.11.4, 9.10.0, 9.12.3, 10.0.0, 10.5.2, the 10.6 RC 4ae946e5a9b and main's parent commit 62e02760321 (IllegalArgumentException from PriorityQueue). [read] branch_11x 603af870375 (the 11.0 release branch, cut from main on 2026-10-28) and upstream main 2f424fd428da (2026-10-30) have the same create() order with no size check; branch_10x 657014b3c57 (decommissioned on 2026-10-29) and branch_10_6 still call super(size) first. Releases before 4.7.0 throw the OutOfMemoryError from PriorityQueue itself: that is the original SOLR-5661 bug (executed on 4.6.1), long fixed and not this regression. Not fixed upstream: through main e1faeba08bd, FieldValueHitQueue.java was changed after 39549329509 only by #16857 (70a428b5ba9) and #16947 (f6e64aa0db3), and neither changes the order in create() [read 2026-11-02]. All runs on Linux aarch64 with JDK 25.0.3. [exec 2026-11-02] main e1faeba08bd still fails (minimal repro 1 of 1 with -ea and -da; the OutOfMemoryError is now raised under FieldValueHitQueue.create:147). [read 2026-11-02] branch_11x da3ea39fa59 has the same FieldValueHitQueue.java blob as main (affected); branch_10_6 91eaab4fb29 (the 10.6.1 work) keeps the 10.6.0 file, which calls super(size) first (not affected).
- 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 “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: code reading
- Upstream and prior reports
- none found (searched 2026-10-29, read-only; the finder's 51-query run 2026-10-28; judge refresh 2026-10-29; a further refresh 2026-10-29): two scripted prior-report searches (GitHub 16, commits 3, downstream 2, lists 20, JIRA 4, web 6 queries each; 3,476 and 692 distinct hits, 0 high), 19 manual GitHub searches, the file lists of the 141 open apache/lucene PRs updated since 2026-10-01 and of the closed PRs updated since 2026-10-26, the commit history of FieldValueHitQueue.java, TopFieldCollectorManager.java, PriorityQueue.java and the comparators, lists.apache.org (dev, java-user, issues), SOLR-5661 on JIRA, and web searches. Nothing reports or fixes a field-sorted collector throwing OutOfMemoryError instead of the IllegalArgumentException on main. Related, not reports of this bug: SOLR-5661 (2014, the fixed original whose guard this regresses: same symptom, different root cause and code), #14873 (the change; its review discussed performance and making PriorityQueue final, not the size check), #14872 (open issue to remove the last PriorityQueue subclasses), #16857 ('Consolidate Sort pruning logic', merged 2026-10-29 as 70a428b5ba9, adds 71 lines to FieldValueHitQueue.java and deletes none, so it leaves the ordering alone; fix A's diff against 645d4fe14a20 does not apply on top of it unchanged) and open #16277 (3-ary queue, constructor arity only). Refresh: upstream main 645d4fe14a20, no commit on FieldValueHitQueue.java after 39549329509; none of the 60 issues and PRs updated since 2026-10-27 is about it; GitHub search 'FieldValueHitQueue updated:>=2026-10-27' finds only #16857; 'Requested array size exceeds VM limit' (updated since 2026-10-01), 'TopFieldCollectorManager OutOfMemoryError' and '"numHits" "MAX_ARRAY_LENGTH"' find nothing.
- Lucene commit tested
- main
645d4fe14a20: the minimal test with assertions on and off, standalone and through gradle; the recommended fix in a scratch worktree with the closest core suites and:lucene:core:check -x test. SHA-1-checked release jars: the minimal test on 10.5.2 and 10.6.0 (passes); earlier probes on 4.6.1, 4.7.0, 8.11.4, 9.10.0, 9.12.3, 10.0.0, 10.5.2 and the 10.6 RC; javac builds of the introducing commit39549329509and its parent62e02760321. Upstream main2f424fd428da(2026-10-30) read: same order increate().branch_11xat603af870375read: same order. Fact check (2026-11-01): the minimal test again on main (standalone and gradle, assertions on and off), fix A through gradle, and a release probe throughFieldValueHitQueue.createandTopFieldCollector.createon 4.6.1, 4.7.0, 4.9.0, 4.9.1, 4.10.0, 5.5.5, 6.6.6, 7.3.1, 7.4.0, 7.5.0, 8.11.4, 9.10.0 and 10.6.0.
- Finding record
lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26 (at a9779ef3c193) (@lucene.experimental), :125-130 (create javadoc: 'pre-allocate a full array of length numHits'; size 'Must be greater than zero'), :132-159 (create: field.getComparator(size, ...) at :146 in the comparator loop, before new FieldValueHitQueue<>(fields, size, ...) at :153 reaches the PriorityQueue constructor)lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39 (at a9779ef3c193) (@lucene.internal), :79 (PriorityQueue(int maxSize, LessThan)), :121-125 ('Throw exception to prevent confusing OOME', maxSize >= ArrayUtil.MAX_ARRAY_LENGTH -> IllegalArgumentException)lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:38 (at a9779ef3c193) (values = new long[numHits])lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45 (at a9779ef3c193) and :96-97 (pre-allocation NOTE), :62-64 (only numHits <= 0 rejected), :112-114 (newCollector -> FieldValueHitQueue.create(sort.getSort(), numHits))lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:588-597 (at a9779ef3c193) and :721-729 (n capped at max(1, maxDoc) before the collector manager is built)lucene/core/src/java/org/apache/lucene/search/SortRescorer.java:38 (at a9779ef3c193) and :49 (rescore passes topN unchanged to new TopFieldCollectorManager)lucene/core/src/java/org/apache/lucene/util/LongHeap.java:57 (at a9779ef3c193) and TernaryLongHeap.java:57 (the same guard re-applied in newer heaps; source of the score-ordered control's IllegalArgumentException)lucene/CHANGES.txt:12866-12867 (at a9779ef3c193) (4.7.0 Bug fixes: 'SOLR-5661: PriorityQueue now refuses to allocate itself if the incoming maxSize is too large (Raintung Li via Mike McCandless)') and :75 (11.0.0 API Changes: GITHUB#14873)Lucene main a9779ef3c193 SECURITY.md lines 28, 38, 46, 50 (threat model)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45, :62-64, :96-97, :112-114 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26, :114-115, :125-130, :132-159 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39, :79, :121-125 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:38 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/TernaryLongHeap.java:57-61 (at 645d4fe14a20)a Lucene clone releases/lucene/10.5.2 and releases/lucene/10.6.0: lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:122-123lucene/core/src/java/org/apache/lucene/search/SortField.java:481-530 (getComparator; CUSTOM at :511-514) (at 645d4fe14a20)
- Who is affected
lucene/core/src/java/org/apache/lucene/search/SortRescorer.java:38, :49 (at 645d4fe14a20)lucene/expressions/src/java/org/apache/lucene/expressions/ExpressionRescorer.java:37 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:721, :729, :733 (at 645d4fe14a20)build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:451; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:750-752 (at 645d4fe14a20)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39, :121-125 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/LongHeap.java:57-58; util/TernaryLongHeap.java:57-58 (at 645d4fe14a20)lucene/CHANGES.txt:75-76 (11.0.0 API Changes, GITHUB#14873), :12883-12884 (4.7.0 Bug fixes, SOLR-5661) (at 645d4fe14a20)lucene/MIGRATE.md:414-421 (PriorityQueue now takes a LessThan) (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/util/TestPriorityQueue.java:378-382 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26, :130 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:62-64, :112-114 (at 645d4fe14a20)
- History
a Lucene clone origin/branch_11x (603af870375), origin/branch_10_6 (7e5aec4370e), 48cf3f515d6 (#16911), c5ab3e57b9b: lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
- TestTopFieldCollectorManagerMaxNumHits.java (JUnit, one test):
repro/LFV-993/src/org/apache/lucene/search/TestTopFieldCollectorManagerMaxNumHits.java(30 lines)
Made in parallel with this doc from the mechanism in the brief and re-rendered after the fact checks (wording: built-in sorts, main and the 11.0 branch, the window's scope); the PNG is the summary frame.
assets/LFV-993/bug.png(472 KB)
![Diagram of the bug. new TopFieldCollectorManager(sort by long field, numHits = Integer.MAX_VALUE).newCollector() calls FieldValueHitQueue.create(fields, 2147483647). On main, create() first builds the LongComparator, whose new long[2147483647] throws OutOfMemoryError: Requested array size exceeds VM limit (FieldValueHitQueue.java:146); the PriorityQueue constructor and its size check ("Throw exception to prevent confusing OOME") come later (:153) and are never reached. In 10.x the queue constructor ran super(size) first, so the check threw IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647 before any allocation.](../assets/LFV-993/bug.png)