All findings · Lucene finding · LFV-964 · groupingsearch-limits-not-capped-at-maxdoc

GroupingSearch throws for groupLimit or groupDocsLimit = Integer.MAX_VALUE instead of returning all groups

Ask GroupingSearch for all groups with Integer.MAX_VALUE, which IndexSearcher.search accepts, and it throws NegativeArraySizeException: -2147483648 or an exception naming that number, even on a 1-document index. It never caps the limits at maxDoc, and it adds offset + limit in int; every release tested since 4.0.0 fails.

Summary

Priority
Low, rank 640GroupingSearch fails loudly on the usual Integer.MAX_VALUE 'all' idiom in a module and never returns wrong groups, like its UnifiedHighlighter MAX_VALUE sibling LFV-438.
Impact
wrong error
Since
4.0.0 (2012); LUCENE-3778 added GroupingSearchintroduced 2012-03-22 · over 14 years
Affected
4.0.0 to 10.6.0, and main
Status
documented
Found by
code reading and an executed probe, then a differential check

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

The "give me everything" value that IndexSearcher.search accepts makes GroupingSearch throw. One document is enough:

Two things to know first

  1. GroupingSearch groups the hits of a query by a field value. search(searcher, query, groupOffset, groupLimit) returns one page of groups; setGroupDocsLimit(n) sets how many documents each group returns (default 1).
  2. Top-N collectors allocate their arrays for N up front. IndexSearcher.search(query, n) therefore caps n at maxDoc first, so Integer.MAX_VALUE ("all") is safe there; that cap fixed the same NegativeArraySizeException in 2009 (LUCENE-2119).
GroupingSearch on a 1-document index (group a, maxDoc = 1). IndexSearcher.search(q, Integer.MAX_VALUE) returns 1 hit because it caps the count at maxDoc. GroupingSearch.search(searcher, q, 0, 1) returns 1 group. GroupingSearch.search(searcher, q, 0, Integer.MAX_VALUE) throws NegativeArraySizeException: -2147483648, because the first-pass collector computes 2147483647 + 1 in int; with offset 1 it throws 'topNGroups must be >= 1 (got -2147483648)'. With the limits capped at maxDoc, both searches return the expected groups.
One document in group a. With limit maxDoc = 1, GroupingSearch returns the group; with Integer.MAX_VALUE, 2147483647 + 1 wraps to -2147483648 and the search throws. IndexSearcher.search caps the count at maxDoc first; the same cap in GroupingSearch returns the group. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You index 1 document whose group field g is a. IndexSearcher.search(q, Integer.MAX_VALUE) returns its 1 hit, and new GroupingSearch("g").search(searcher, q, 0, 1) returns 1 group.
  2. To get every group you pass Integer.MAX_VALUE as the limit: search(searcher, q, 0, Integer.MAX_VALUE). You expect the same 1 group; the javadoc calls the limit "the number of groups to return", with no upper bound.
  3. You get NegativeArraySizeException: -2147483648, with assertions on or off. The next page (offset 1) throws topNGroups must be >= 1 (got -2147483648), a number you never passed; setGroupDocsLimit(Integer.MAX_VALUE) fails too.
  4. Why: GroupingSearch hands the limit on unchanged, and the collector sizes an array by limit + 1 in int: 2147483647 + 1 wraps to -2147483648. IndexSearcher caps the count at maxDoc (here 1) before sizing anything; GroupingSearch never does.
  5. Below the wrap, the same missing cap costs memory: one search with groupDocsLimit = 10,000,000 allocated about 600 MB on a 20-document index.

Who is affected

Applications that group with GroupingSearch (the opt-in grouping module) and pass Integer.MAX_VALUE for all groups or all documents per group, or forward an uncapped page size. Every grouping mode; a 1-document index is enough. The search fails loudly; no wrong result observed. Workaround: for a valid offset and limit, cap both at maxDoc before the call, with the fix's rule.

Mechanism, terms and numbers591 words

GroupingSearch, the grouping module's convenience class, throws when the caller passes Integer.MAX_VALUE ("all") as groupLimit or as groupDocsLimit, although the same search with the limit set to reader.maxDoc() works and IndexSearcher.search(query, Integer.MAX_VALUE) works. With offset 0 the search throws NegativeArraySizeException: -2147483648, OutOfMemoryError: Requested array size exceeds VM limit or a queue-size IllegalArgumentException; with offset 1 it throws IllegalArgumentExceptions that name a value the caller never passed: "topNGroups must be >= 1 (got -2147483648)", "numHits must be > 0" or "maxDocsPerGroup must be >= 1 (got -2147483648)". Every grouping mode fails (term, function, term with caching enabled, the Supplier constructor, doc block), with assertions on and off; a large finite limit such as 10,000,000 does not throw but allocates memory in proportion to the limit. The cause is one missing cap: GroupingSearch hands the caller's offsets and limits unchanged to collector managers that add offset + limit in int and pre-size their queues and comparators by the limit, while IndexSearcher caps the count at maxDoc since LUCENE-2119 (2009). No silently wrong result was observed in 21,040 differential searches. Every release tested since 4.0.0 (2012) fails, as does main; capping the limits at maxDoc in GroupingSearch fixes it, and the grouping module's tests pass with that change.

The code (main 645d4fe14a20, package org.apache.lucene.search.grouping)

GroupingSearch.search(searcher, query, groupOffset, groupLimit)      GroupingSearch.java:128-138  values passed on unchanged
 term / function / cached: groupByFieldOrFunction                    :141
   new FirstPassGroupingCollectorManager(.., groupOffset, groupLimit, ..)   :146-148
     FirstPassGroupingCollectorManager.java:92-97  checks only offset >= 0, topNGroups >= 1
     FirstPassGroupingCollectorManager.java:110    groupOffset + topNGroups     <- int: 1 + MAX_VALUE = -2^31
     FirstPassGroupingCollector.java:97-98         "topNGroups must be >= 1 (got " + topNGroups + ")"
     FirstPassGroupingCollector.java:116           getComparator(topNGroups + 1) <- MAX_VALUE + 1 = -2^31
                                                   -> new float[-2147483648]: NegativeArraySizeException
   new TopGroupsCollectorManager(.., groupDocsOffset, groupDocsLimit, ..)   :197-205
     TopGroupsCollectorManager.java:107            withinGroupOffset + maxDocsPerGroup   <- int sum
     TopGroupsCollector.java:127-147               per-group top-N collectors sized by it
 doc block: groupByDocBlock                                          :221
   new BlockGroupingCollectorManager(.., groupDocsOffset + groupDocsLimit)  :226-235  <- int sum at :235
     BlockGroupingCollectorManager.java:143        groupOffset + topNGroups     <- int sum
     BlockGroupingCollector.java:217, :237         queue and comparators sized by topNGroups

IndexSearcher.searchAfter (core):  :588 limit = Math.max(1, reader.maxDoc());  :597 cappedNumHits = Math.min(numHits, limit)
  • There are at most maxDoc groups and at most maxDoc documents per group, so a limit above maxDoc never changes the result; it only sizes the collectors. GroupingSearch sizes them for the caller and has no cap.
  • Every failing value passes the API's own checks (offset >= 0, limit >= 1).

Every form fails (main, 8 cases of the finder's JUnit, assertions on and off)

groupingargumentoffset 0offset 1
termgroupLimit = MAX_VALUENegativeArraySizeException: -2147483648topNGroups must be >= 1 (got -2147483648)
termgroupDocsLimit = MAX_VALUEOutOfMemoryError: Requested array size exceeds VM limit (field sort; relevance: initialCapacity must be > 0 and < 2147483630)numHits must be > 0
doc blockgroupLimit = MAX_VALUEmaxSize must be >= 0 and < 2147483631; got: 2147483647topNGroups must be >= 1 (got -2147483648)
doc blockgroupDocsLimit = MAX_VALUEOutOfMemoryError: Requested array size exceeds VM limitmaxDocsPerGroup must be >= 1 (got -2147483648)

Function grouping, term grouping with caching enabled (setCaching; it fails in the first pass, before any cached replay) and the Supplier<GroupSelector> constructor fail the same way (with groupLimit = MAX_VALUE, assertions on and off). Below the wrap, memory grows with the limit: on a 20-document index (5 groups of 4, field sort), one term-grouping search allocated 600,119,480 bytes with groupDocsLimit = 10,000,000 and 187,228,760 bytes with groupLimit = 10,000,000, against 56,488 bytes for IndexSearcher.search(q, 10,000,000, sort); with the cap, 120,680 and 120,264 bytes at every limit.

Exposure and likelihood316 words
  • Who is exposed. Applications that use GroupingSearch and pass Integer.MAX_VALUE as groupLimit or groupDocsLimit to mean "all", or forward an uncapped user-chosen page size or offset. Term, function, cached and doc-block grouping all fail; the default groupDocsLimit is 1, so the per-group path needs an explicit setGroupDocsLimit.
  • What happens. That search throws an unchecked exception instead of returning the groups; with offset 1 or more the message names a value the caller never passed. No silent wrong result was observed in 21,040 differential searches, and the index does not change.
  • Scale. None for Integer.MAX_VALUE: a 1-document index is enough. Large finite limits cost memory in proportion to the limit (about 600 MB for groupDocsLimit = 10,000,000 on a 20-document index).
  • Versions. Every release tested from 4.0.0 (2012) to 10.6.0 fails, and main; releases not run are inferred affected (the same int sums in every tag read, no capping commit).
  • How likely. Rare: it fails on the first call, so developers who try this value switch to a finite value; Lucene's own DatasetSplitter passes setGroupDocsLimit(originalIndex.maxDoc()), and no public report was found.
  • Workaround. For a valid offset and limit (offset >= 0, limit >= 1), cap both before the call, with the rule of the fix: maxDoc = Math.max(1, reader.maxDoc()), offset = Math.min(offset, maxDoc), limit = Math.max(1, Math.min(limit, maxDoc - offset)), for the group page and for setGroupDocsOffset / setGroupDocsLimit. Leave other values as they are (or validate them first), as the fix does; applied to limit 0 or a negative limit, the formula would turn it into 1 instead of letting the API reject it. Capping only the limit (Math.min(limit, reader.maxDoc())) is not enough: a large offset still wraps offset + limit (e.g. offset Integer.MAX_VALUE - 1 with limit 20), and an empty index gives limit 0, which the API rejects ("topNGroups must be >= 1 (got 0)").

Minimal repro

One JUnit test for the grouping module, public API only, no randomness: a 1-document index, default GroupingSearch settings, search(searcher, MatchAllDocsQuery, 0, Integer.MAX_VALUE), which must return the index's 1 group.

TestGroupingSearchLimitMaxValue.java (one test)54 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.search.grouping;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;

public class TestGroupingSearchLimitMaxValue extends LuceneTestCase {

  public void testGroupLimitMaxValue() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        doc.add(new SortedDocValuesField("g", new BytesRef("a")));
        w.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        IndexSearcher searcher = new IndexSearcher(reader);
        TopGroups<BytesRef> groups;
        try {
          GroupingSearch gs = new GroupingSearch("g");
          groups = gs.search(searcher, new MatchAllDocsQuery(), 0, Integer.MAX_VALUE);
        } catch (NegativeArraySizeException e) {
          // expected: the 1 group that groupLimit = maxDoc returns; actual: limit + 1 wraps
          throw new AssertionError("expected the 1 group of the index, got " + e, e);
        }
        assertEquals(1, groups.groups.length);
      }
    }
  }
}
Command
./gradlew :lucene:grouping:test --tests "org.apache.lucene.search.grouping.TestGroupingSearchLimitMaxValue" -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
Expected
search(searcher, MatchAllDocsQuery, 0, Integer.MAX_VALUE) returns the index's 1 group, as the same search with limit maxDoc does.
Observed
NegativeArraySizeException: -2147483648 from FirstPassGroupingCollector.<init>(FirstPassGroupingCollector.java:116), assertions on and off.

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

More files, environment, full output196 words

Copy the file into lucene/grouping/src/test/org/apache/lucene/search/grouping/ and run the command. The test body is deterministic (ByteBuffersDirectory, a plain IndexWriterConfig), though it runs under Lucene's randomized test framework; pass -Ptests.asserts explicitly, because by default the build picks it from the seed. With the file added, :lucene:grouping:check -x test passes.

The exception is wrapped in an AssertionError that states expected and actual. The other symptoms (offset 1, groupDocsLimit, doc-block grouping) come from the same missing cap. The test is the suggested regression test as it stands.

Environment

lucenemain 645d4fe14a20 (gradle); also fails on 10.6.0 and on the 4.0.0 to 10.5.2 releases tested
jdkOpenJDK 25.0.3
osLinux aarch64
Command (environment)
./gradlew :lucene:grouping:test --tests "org.apache.lucene.search.grouping.TestGroupingSearchLimitMaxValue" -Ptests.seed=DEADBEEF -Ptests.asserts=true

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (the same with false)
TestGroupingSearchLimitMaxValue > testGroupLimitMaxValue FAILED
    java.lang.AssertionError: expected the 1 group of the index, got java.lang.NegativeArraySizeException: -2147483648
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:9E3B10358EB89447]:0)
        at org.apache.lucene.search.grouping.TestGroupingSearchLimitMaxValue.testGroupLimitMaxValue(TestGroupingSearchLimitMaxValue.java:48)

        Caused by:
        java.lang.NegativeArraySizeException: -2147483648
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.FieldComparator$RelevanceComparator.<init>(FieldComparator.java:149)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.SortField.getComparator(SortField.java:485)
            at org.apache.lucene.search.grouping.FirstPassGroupingCollector.<init>(FirstPassGroupingCollector.java:116)
            at org.apache.lucene.search.grouping.FirstPassGroupingCollectorManager.newCollector(FirstPassGroupingCollectorManager.java:108)
            at org.apache.lucene.search.grouping.FirstPassGroupingCollectorManager.newCollector(FirstPassGroupingCollectorManager.java:52)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.MultiCollectorManager.newCollector(MultiCollectorManager.java:53)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:752)
            at org.apache.lucene.search.grouping.GroupingSearch.groupByFieldOrFunction(GroupingSearch.java:175)
            at org.apache.lucene.search.grouping.GroupingSearch.search(GroupingSearch.java:131)
            at org.apache.lucene.search.grouping.TestGroupingSearchLimitMaxValue.testGroupLimitMaxValue(TestGroupingSearchLimitMaxValue.java:45)
:lucene:grouping:test (FAILURE): 1 test, 1  failure

# with the recommended fix, assertions on and off:
:lucene:grouping:test (SUCCESS): 1 test
BUILD SUCCESSFUL

Why it looks unintended

  • The javadoc calls groupLimit "The number of groups to return from the specified group offset", with no bound, and every failing value passes the API's own checks.
  • "topNGroups must be >= 1 (got -2147483648)" blames the caller for a number Lucene computed by overflowing int.
  • Core fixed the same symptom as a bug: LUCENE-2119 caps IndexSearcher.search at maxDoc.
The full argument208 words
  • No documented bound. groupLimit: "The number of groups to return from the specified group offset" (GroupingSearch.java:123); groupDocsLimit: "The number of documents to return inside a group" (:321). No javadoc, package doc, CHANGES or MIGRATE entry limits them, and every failing value passes the API's own checks (FirstPassGroupingCollectorManager.java:92-97, BlockGroupingCollectorManager.java:112-128).
  • The wrapped values have no rationale. "topNGroups must be >= 1 (got -2147483648)" and "maxDocsPerGroup must be >= 1 (got -2147483648)" report sums that Lucene computed (FirstPassGroupingCollectorManager.java:110, BlockGroupingCollectorManager.java:143, GroupingSearch.java:235).
  • Core precedent. lucene/CHANGES.txt, Bug fixes of 2.9.3 / 3.0.2: "LUCENE-2119: Don't throw NegativeArraySizeException if you pass Integer.MAX_VALUE as nDocs to IndexSearcher search methods." The fix is the cap at IndexSearcher.java:588/:597; FacetsCollectorManager and DrillSideways cap the same way.
  • Integer.MAX_VALUE per group was made to work elsewhere. In LUCENE-4832 (#5897, 4.3.0), Michael McCandless asked in review "Could we just fix it so passing in Integer.MAX_VALUE for maxDocsPerGroup works?" for the join module's ToParentBlockJoinCollector, and the committed fix capped it with the group size.
  • The module's grouped-facet test (the grouped-facet API, not GroupingSearch) passes Integer.MAX_VALUE as a limit and guards the offset + limit sum itself (TestTermGroupFacetCollectorManager.java:106-118); no upstream test asserts the exception.
The devil's-advocate check161 words

The strongest case for "intended": the LUCENE-2119 issue text (#3195) calls Integer.MAX_VALUE "a nonsense value to pass in, since our PQ impl allocates the array up front"; core documents that TopScoreDocCollectorManager instances "pre-allocate a full array of length numHits"; no Lucene doc says that Integer.MAX_VALUE means "all" for these limits, and no index can hold that many documents (IndexWriter.MAX_DOCS is smaller); GroupingSearch is @lucene.experimental; the defaults are safe; Solr caps in its own layer; and the doc-block offset-0 case already gives an honest queue-size message.

Why it fails: LUCENE-2119 itself was resolved as a bug by capping in the convenience layer, and GroupingSearch is the convenience layer for grouping, which sizes the collectors on the caller's behalf; the pre-allocation note is on the core collector managers, not on GroupingSearch's limits; and no design reading explains an exception that names -2147483648. The defence fits the memory cost of large finite limits best, so present that part as secondary.

Suggested fix

Recommended A. Cap the offsets and limits at maxDoc in GroupingSearch (validated)

Cap the offsets and limits at maxDoc inside GroupingSearch, as IndexSearcher.search does: offset' = min(offset, maxDoc), limit' = max(1, min(limit, maxDoc - offset')), for groups and for documents per group. Validated: the repro passes with assertions on and off, all 57 grouping-module tests pass, and all 21,040 differential searches match an exact reference.

All options and trade-offs3 options · 930 words

Option A fixes every GroupingSearch caller. Code that builds the collector managers directly still gets the wrapped sums; option B covers it and can be added to A. No CHANGES entry is drafted.

A. Cap the offsets and limits at maxDoc in GroupingSearch (validated) (recommended)

In GroupingSearch.search, cap groupOffset and groupLimit; for the documents per group, pass capped groupDocsOffset and groupDocsLimit to TopGroupsCollectorManager and BlockGroupingCollectorManager. Invalid values (negative offset, limit < 1) pass through unchanged, so they behave exactly as today: the group-page checks reject them, and invalid per-group values give the same result with and without the patch. +29/-4 lines in GroupingSearch.java. The code below is the complete patch; git apply takes it on 645d4fe14a20.

diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
index 88b21b50686..2edabe58ce7 100644
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
@@ -127,6 +127,15 @@ public class GroupingSearch {
   @SuppressWarnings("unchecked")
   public <T> TopGroups<T> search(
       IndexSearcher searcher, Query query, int groupOffset, int groupLimit) throws IOException {
+    // A page of groups cannot extend past maxDoc (there are at most maxDoc groups): cap the
+    // caller's values, as IndexSearcher#search caps numHits, so that "offset + limit" cannot
+    // overflow and the collectors are not sized beyond the index. Invalid values are passed
+    // through unchanged so that the collectors still reject them.
+    final int maxDoc = Math.max(1, searcher.getIndexReader().maxDoc());
+    if (groupOffset >= 0 && groupLimit >= 1) {
+      groupOffset = Math.min(groupOffset, maxDoc);
+      groupLimit = Math.max(1, Math.min(groupLimit, maxDoc - groupOffset));
+    }
     if (grouperFactory != null) {
       return groupByFieldOrFunction(searcher, query, groupOffset, groupLimit);
     } else if (groupEndDocs != null) {
@@ -137,6 +146,22 @@ public class GroupingSearch {
     }
   }
 
+  /** groupDocsOffset capped at maxDoc (same rule as the group page in #search). */
+  private int cappedDocsOffset(IndexSearcher searcher) {
+    final int maxDoc = Math.max(1, searcher.getIndexReader().maxDoc());
+    return groupDocsOffset >= 0 && groupDocsLimit >= 1
+        ? Math.min(groupDocsOffset, maxDoc)
+        : groupDocsOffset;
+  }
+
+  /** groupDocsLimit capped so that the capped offset plus the limit is at most maxDoc + 1. */
+  private int cappedDocsLimit(IndexSearcher searcher) {
+    final int maxDoc = Math.max(1, searcher.getIndexReader().maxDoc());
+    return groupDocsOffset >= 0 && groupDocsLimit >= 1
+        ? Math.max(1, Math.min(groupDocsLimit, maxDoc - cappedDocsOffset(searcher)))
+        : groupDocsLimit;
+  }
+
   @SuppressWarnings({"unchecked", "rawtypes"})
   protected <T> TopGroups<T> groupByFieldOrFunction(
       IndexSearcher searcher, Query query, int groupOffset, int groupLimit) throws IOException {
@@ -200,8 +225,8 @@ public class GroupingSearch {
             topSearchGroups,
             groupSort,
             sortWithinGroup,
-            groupDocsOffset,
-            groupDocsLimit,
+            cappedDocsOffset(searcher),
+            cappedDocsLimit(searcher),
             includeMaxScore);
 
     TopGroups<T> secondResult;
@@ -231,8 +256,8 @@ public class GroupingSearch {
             groupSort.needsScores() || sortWithinGroup.needsScores(),
             groupEndDocs,
             sortWithinGroup,
-            groupDocsOffset,
-            groupDocsOffset + groupDocsLimit);
+            cappedDocsOffset(searcher),
+            cappedDocsOffset(searcher) + cappedDocsLimit(searcher));
 
     return searcher.search(query, bcm);
   }

Trade-offs

A few cheap maxDoc() reads per search (the patch reads it up to 4 times for term and function grouping and 5 times for doc block, because the helpers recompute it and cappedDocsLimit calls cappedDocsOffset); no result changed in the differential. Direct users of FirstPassGroupingCollectorManager, TopGroupsCollectorManager and BlockGroupingCollectorManager are not covered (option B). Computing the capped values once is a possible clean-up.

What the checks validated

  • Gradle in a scratch worktree of main 645d4fe14a20, seed DEADBEEF, assertions on and off: the repro alone fails stock and passes with the fix.
  • The repro with TestGroupingSearch, the upstream test class of GroupingSearch: stock 1 failure (the repro), with the fix 4 of 4 pass, each way.
  • All of :lucene:grouping:test with the fix, assertions on: 15 classes (14 upstream), 57 tests, 0 failures.
  • :lucene:grouping:check -x test (google-java-format, ecjLint, forbiddenApis) passes with the fix and the repro added; the hunt's diff needed its 4-line comment re-wrapped (three of its lines were 101 columns).
  • Re-run in a second fresh scratch worktree of 645d4fe14a20 (2026-10-30): stock, the repro fails with assertions on and off; with the fix, the repro and TestGroupingSearch pass 4 of 4 each way and all 57 tests of :lucene:grouping:test pass. It did not repeat check -x test.
  • The fact check's own fresh scratch worktree of 645d4fe14a20 (2026-10-30), with the patch extracted from this doc and applied with git apply: stock, the repro command fails with assertions on and off; with the patch, it passes both ways, the repro and TestGroupingSearch pass 4 of 4 each way, all 57 tests of :lucene:grouping:test pass and :lucene:grouping:check -x test passes.
  • Earlier, in the repro harness: the finder's 8-case JUnit passes 8 of 8, 29 upstream grouping tests pass, a 21,040-search differential agrees 21,040 of 21,040, and a z3 bit-vector model proves, for the int arithmetic and an abstract model of the group window (not the collectors' code), that the capped window selects the same groups and no sum can wrap for any maxDoc up to IndexWriter.MAX_DOCS; the allocation no longer grows with the limit.

B. Checked or saturating sums in the collector managers (not run)

Make the offset + limit sums in FirstPassGroupingCollectorManager.newCollector, TopGroupsCollectorManager.newCollector, BlockGroupingCollectorManager.newCollector and GroupingSearch.groupByDocBlock overflow-safe: reject offset > Integer.MAX_VALUE - limit in the constructors with a clear message, or saturate the sum.

Trade-offs

Removes the misleading -2147483648 messages for every caller, but on its own does not make "all" work: the first-pass collector still computes topNGroups + 1, and the queues are still pre-sized by the limit. Best as a complement to A.

C. Document and validate an upper bound (not run)

State in the javadoc of search and setGroupDocsLimit that the limits pre-allocate and must not exceed maxDoc, and reject larger values with a clear message.

Trade-offs

Honest, but callers still need their own cap, and it differs from IndexSearcher.search, which accepts Integer.MAX_VALUE.

Downstream note

A service that forwards a user-chosen group page size or offset to GroupingSearch without capping it fails that request (or allocates in proportion to the size); Solr caps these sums in its own grouping layer.

How it was found

Code reading left the grouping module's int offset + limit sums as a lead, and a probe on main showed the -2147483648 exceptions. A public-API JUnit, a differential check of 21,040 grouped searches against an exact reference (0 silent mismatches) and a z3 model of the eight int size sites followed.

The method in full194 words
  • First signal (non-formal). Code reading of the PriorityQueue / TopDocs.merge area (2026-10-26) noted the grouping module's int offset + limit sums as a lead without running them; a planning probe on 2026-10-28 ran them on main, 10.5.2 and branch_10_6 and showed NegativeArraySizeException: -2147483648 and "topNGroups must be >= 1 (got -2147483648)".
  • Then (semi-formal). A public-API JUnit (term and doc-block grouping x groupLimit or groupDocsLimit = Integer.MAX_VALUE x offset 0 or 1, each compared with the same search at limit maxDoc): 8 of 8 fail. A bounded-exhaustive differential harness against an exact long-arithmetic reference: 6 seeded indexes of 2-15 documents in 1-3 segments, 5 grouping modes, field and relevance sorts, sequential and executor searchers, a boundary grid of offsets and limits: 21,040 searches, 7,872 agree, 13,168 throw, 0 silent mismatches; 21,040 of 21,040 agree with the cap. A z3 bit-vector model of the 8 int size sites: a wrap witness at every site, the wrap region is exactly offset + limit >= 2^31, and, in an abstract model of the group window, the cap is proved result-preserving and overflow-free; two fix mutants refuted.
  • The repro was later minimized to the one test above.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-28 and rechecked 2026-10-30: GitHub issues and PRs, the JIRA archive, four mailing lists, downstream repositories, the web and the changed files of 195 open PRs). Closest: the same class fixed elsewhere, LUCENE-2119 (core) and LUCENE-4832 (join).

Searches6 searches

Read and not this bug: #3195 (LUCENE-2119, core IndexSearcher, 2009, fixed by capping at maxDoc); #5897 (LUCENE-4832, join module, made Integer.MAX_VALUE per group work); SOLR-4164 (Solr's own grouping layer, group.limit = -1); #10342 (LUCENE-9302, a different overflow in TopGroups.merge hit counts); #12942 (an honest "topNGroups must be >= 1 (got 0)"); #15574, #15603, #15936, #15995 and #16247, which introduced the collector managers and moved or added the sums (#15995 added the within-group offset and with it the TopGroupsCollectorManager sum, with no overflow discussion; one #15936 review discusses whether the offset belongs in the sum, not overflow). Open PRs #14996, #15210, #15494, #16548 and #16569 and the draft #16847 (long global doc ids: docBase as long, group heads per leaf, the IndexSearcher cap kept as a long) touch the grouping sources without changing the sums or capping a limit. Gaps: GitHub search does not fully cover PR review comments; some StackExchange queries were rate-limited; private reports cannot be searched.

What was searched, when, and what turned up
WhereWhenHowFound
scripted prior-report search (two runs)2026-10-2866 queries (GitHub 26, commits 3, downstream 2, the dev@, java-user@, issues@ and commits@ lists 24, JIRA 5, web 6; 540 distinct hits, 0 high) and the finder's 51-query run (0 high, 0 medium)no report
GitHub, JIRA and the lists (manual)2026-10-2824 searches over apache/lucene, apache/solr, Elasticsearch, OpenSearch and Lucene.NET (GroupingSearch, groupLimit, topNGroups, maxDocsPerGroup, the three messages, grouping with Integer.MAX_VALUE); 17 JQL queries (6 LUCENE, 11 SOLR, as listed in the prior-report record); every java-user@ thread about GroupingSearch and setGroupDocsLimit; the changed files of 195 open PRsonly precedents at other fix sites (LUCENE-2119, LUCENE-4832, SOLR-4164)
GitHub recheck2026-10-30GroupingSearch and topNGroups updated since 2026-10-28, groupDocsLimit, grouping Integer.MAX_VALUE; the 17 PRs created since 2026-10-28; the newest lucene/grouping commitsno report; no new grouping commit or PR
GitHub recheck (round 2)2026-10-30GroupingSearch updated today, topNGroups, groupDocsLimit, "-2147483648" grouping, grouping updated today; the PRs and issues created since the first recheck (#16953-#16955); the open #16952, which edits IndexSearcher bulk-scoring bounds only; the newest lucene/grouping and IndexSearcher.java commitsno report; no new grouping commit; the IndexSearcher cap is unchanged
GitHub recheck (round 3)2026-10-30GroupingSearch, topNGroups and grouping updated today, groupDocsLimit; items created or updated since a recent cutoff (#16952, #16790); "Integer.MAX_VALUE" grouping (7 items, incl. the draft #16847, whose 438 changed files were listed in full); the newest lucene/grouping and IndexSearcher.java commitsno report; #16847 touches grouping files but not the sums or the limits; no new grouping commit
GitHub recheck (round 4)2026-10-30the round-3 queries again; items updated since a recent cutoff now also include #16937 (the tracking issue for the AI-assisted bug list; its body and comments do not mention this bug) and #16900 (the openIfChanged issue, unrelated); the newest lucene/grouping and IndexSearcher.java commits; 645d4fe14a20...mainno report; no upstream fix (no lucene/grouping or IndexSearcher.java change on main since 645d4fe14a20)

History

Born with the class: LUCENE-3778 (2012) added GroupingSearch with the uncapped int sums; first release 4.0.0. The 2026 collector managers moved the sums without changing them. Executed on 12 releases from 4.0.0-ALPHA to 10.5.2, 10.6.0 and main: all fail.

Versions tested10 rows

Introduced by 89b55566d83 "LUCENE-3778: Added a grouping utility class that makes it easier to use result grouping for pure Lucene apps." (2012-03-22, by martijnvg; migrated issue #4851; Subversion era, no PR, trunk only). The new GroupingSearch computed int topN = groupOffset + groupLimit and int topNInsideGroup = groupDocsOffset + groupDocsLimit and passed the caller's limits on with no cap. Core's IndexSearcher had capped at maxDoc since LUCENE-2119 (ae72952fa41, 2009).

Later moves, bug kept: in 2026 gaobinlong's collector managers took over the sums. FirstPassGroupingCollectorManager (#15574, 636e8e9f587) and BlockGroupingCollectorManager (#15936, d27d080d101) add groupOffset + topNGroups. TopGroupsCollectorManager was introduced by #15603 (e26a959e2c7) with maxDocsPerGroup passed on unchanged; its sum withinGroupOffset + maxDocsPerGroup came with #15995 (dc190df0955, 2026-05-04), which added the within-group offset. #16247 (3afffeef5a8) switched GroupingSearch's term path to the managers. The first-pass pre-sizing by topNGroups + 1 dates from LUCENE-1421 (9083cedb922, 2011), a caller-sized low-level collector that is not itself the bug.

First release. 4.0.0 (2012-10-11; the pre-release 4.0.0-ALPHA also fails). 3.x has no GroupingSearch. The offset-1 message "topNGroups must be >= 1 (got -2147483648)" is the same in every version; the offset-0 symptom varies with the collector internals. Point releases not run are inferred affected: the sums are in every tag read and no commit ever capped them.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe one-test repro fails through gradle and the harness, assertions on and off; the finder's 8-case JUnit 8 of 8; a 9-case probe of every grouping mode 9 of 9, assertions on and off
10.6.0 affectedthe one-test repro fails on the Maven Central jars, assertions on and off (the same commit as branch_10_6 4ae946e5a9b; listed apart because different tests ran)
main a9779ef3c193 affectedthe finder's JUnit 8 of 8 (assertions on and off); an independent JUnit 8 of 10, both controls pass (assertions on and off)
branch_10_6 4ae946e5a9b affectedthe finder's JUnit 8 of 8 (assertions off) (the commit of the 10.6.0 release tag)
10.5.2, 10.0.0 affectedthe finder's JUnit 8 of 8; version probe 4 of 4
9.12.3, 9.0.0, 8.11.4, 7.7.3, 6.6.6, 5.5.5, 5.0.0 affectedversion probe: 4 of 4 term-grouping cases fail (9.12.3 also: independent JUnit 8 of 10)
4.10.4, 4.0.0 firstversion probe: 8 of 8 term and doc-block cases fail; 4.0.0 is the first release with GroupingSearch
4.0.0-ALPHA affectedpre-release: 8 of 8 fail
3.6.2, 3.5.0 API absentno GroupingSearch class
the fix on main 645d4fe14a20 not affectedthe repro and TestGroupingSearch pass (assertions on and off), and all 57 tests of :lucene:grouping:test

Independent checks

An independent reproducer, an intended-behaviour devil's advocate and an adjudication re-ran it and found it real; a reviewer from another model family could not run code and agreed on substance (a lens answered "not real" on procedural grounds only). This doc re-ran the one-test repro through gradle on 645d4fe14a20 and validated the fix.

Checks and verdicts629 words

The cross-model reviews were reading-only (they could not run code); every claim they read was executed by the other checks. The hunt's checks ran on main a9779ef3c193, whose grouping module equals 645d4fe14a20.

Each independent check and its verdict
CheckVerdictWhat it found
Independent reproducer realOwn 10-test JUnit outside the grouping package (an 8-document index of 4 blocks, a maxDoc-limit oracle with non-vacuity checks): 8 fail on main (assertions on and off), 10.5.2 and 9.12.3; both controls pass (IndexSearcher.search(q, Integer.MAX_VALUE) returns all hits; limits of 1000 give the maxDoc result). Its allocation probe confirms that memory grows with a large finite limit.
Cross-model review, refute lens not real (high), proceduralAn independent reviewer from another model family (OpenAI GPT-5.6), reading only: voted not real on procedure only, because it could not run Java; on substance it agreed with the root cause, judged the inputs in contract and rated the intended-behaviour defence weak. A separate re-run failed 8 of 8.
Cross-model review, intended lens real (medium)The same reviewer family, reading only: real, intended-behaviour defence weak. A separate run re-ran main with assertions on and 10.5.2 with assertions off: 8 of 8 fail.
Intended-behaviour devil's advocate realBuilt the strongest works-as-designed case (LUCENE-2119's "nonsense value", documented pre-allocation, @lucene.experimental, Solr's own cap) and rejected it; re-ran main (assertions on and off) and 10.5.2: 8 of 8 fail.
Adjudication confirmed (high)Re-ran the independent reproduction's JUnit on main with assertions on: 10 run, 8 failed, both controls pass.
History, prior art and security screen since 4.0.0; new; not security-sensitive14 versions from 3.5.0 to 10.5.2 executed (3.5.0 and 3.6.2: API absent; 4.0.0-ALPHA onward: all fail); no prior report; no documented untrusted-input guarantee covers these limits.
Registration check, audit and repro minimization reproduces; one testFresh seeds re-ran the finder's JUnit on main (8 of 8 fail, assertions on and off); the repro was reduced to one 1-document test on 645d4fe14a20, which fails with assertions on and off and passes with the fix shadowed.
Fix validation (this doc) fix validatedGradle in a scratch worktree of 645d4fe14a20: the repro fails stock and passes with option A, assertions on and off; with the fix TestGroupingSearch, all 57 tests of :lucene:grouping:test and :lucene:grouping:check -x test pass. The repro also fails on the 10.6.0 jars. A second fresh-worktree run later the same day reproduced the stock failure (assertions on and off) and the fixed passes (the repro with TestGroupingSearch each way, the 57-test module); it did not repeat check -x test or the 10.6.0 jars.
Fact checks of this doc (two rounds) real; doc wording correctedRound 1 (2026-10-30) re-ran the one-test repro through gradle on 645d4fe14a20 (fails, assertions on and off) and the fix (the repro, TestGroupingSearch, all 57 tests of :lucene:grouping:test and :lucene:grouping:check -x test pass), and ran a 9-case probe of every grouping mode (9 of 9 fail, assertions on and off). Round 2, in its own fresh worktree, re-ran the doc's exact command (fails with assertions on and off, the output line for line) and the patch as printed here (the repro, TestGroupingSearch, all 57 tests and check -x test pass), the harness on main and on the 10.6.0 jars (fails, assertions on and off; passes with the patch shadowed), a probe of 10 invalid per-group values (the same output with and without the patch) and every round-3 prior-art query (identical results). A reviewer from another model family (OpenAI GPT-5.6, reading only) checked both rounds and agreed on the mechanism. The wording they flagged (evidence paths, the workaround and its validity guard, maxDoc reads, credits, the recheck times, the scope of the second fix run, overclaims) is corrected; see an unpublished file.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,634 words

Finding record

LFV-964, snapshot of 2026-11-06

Registry title
GroupingSearch with groupLimit or groupDocsLimit = Integer.MAX_VALUE ('all remaining groups/documents') throws NegativeArraySizeException, OutOfMemoryError or an IllegalArgumentException naming -2147483648, because it does not cap the limits at maxDoc as IndexSearcher.search does and its collectors add offset + limit in int (every release since 4.0.0)
Area
grouping / GroupingSearch (term, function, cached two-pass and doc-block grouping) x the first-pass, TopGroups and block collector managers
Priority
Low, rank 640: GroupingSearch fails loudly on the usual Integer.MAX_VALUE 'all' idiom in a module and never returns wrong groups, like its UnifiedHighlighter MAX_VALUE sibling LFV-438.
Impact class
wrong error
Impact note
Hits applications on every release from 4.0.0 to 10.6.0 and on main that pass a 'no limit' value such as Integer.MAX_VALUE as GroupingSearch's groupLimit (search) or groupDocsLimit (setGroupDocsLimit), with default settings in every grouping mode and with -ea or -da: the search throws instead of returning the groups, and with an offset of 1 or more the exception names a value the caller never passed (-2147483648); large finite limits allocate memory in proportion to the limit, not to the index (about 600 MB for groupDocsLimit = 10,000,000 on a 20-document index); the failure is loud on the first such search, no result is ever silently wrong, and capping the limit at reader.maxDoc() avoids it.
Summary
You call GroupingSearch.search(searcher, query, 0, Integer.MAX_VALUE) or setGroupDocsLimit(Integer.MAX_VALUE) to get every group, or every document of each group, and expect the same TopGroups as with a limit of maxDoc, the way IndexSearcher.search(query, Integer.MAX_VALUE) returns every hit. Instead the search throws: with offset 0 a NegativeArraySizeException -2147483648, an OutOfMemoryError 'Requested array size exceeds VM limit' or a queue-size IllegalArgumentException, and with offset 1 an IllegalArgumentException such as 'topNGroups must be >= 1 (got -2147483648)' or 'numHits must be > 0'. GroupingSearch hands the limits on uncapped to collectors that add offset + limit in int and pre-size their queues by the limit; nothing is ever returned wrong.
Introduced
89b55566d8392657be86e1441a329373e9132257 (2012-03-22, 'LUCENE-3778: Added a grouping utility class that makes it easier to use result grouping for pure Lucene apps', by martijnvg; LUCENE-3778 is apache/lucene#4851; JIRA-patch era, no pull request; committed to trunk only, first shipped in 4.0.0): the new GroupingSearch computed 'int topN = groupOffset + groupLimit' and 'groupDocsOffset + groupDocsLimit' and passed the caller's limits on with no cap [read: git log -S on the full history; exec: 4.0.0-ALPHA fails 8 of 8 and 3.5.0/3.6.2 have no GroupingSearch]. The bug was born with the class and is not a regression. The sums later moved into FirstPassGroupingCollectorManager (636e8e9f587, #15574), TopGroupsCollectorManager (e26a959e2c7, #15603) and BlockGroupingCollectorManager (d27d080d101, #15936), and GroupingSearch's term path switched to the managers in 3afffeef5a8 (#16247), all by gaobinlong in 2026, with the same effect. The first-pass pre-sizing by topNGroups + 1 dates from LUCENE-1421 (9083cedb922, 2011), a caller-sized low-level collector that is not itself the bug. (2012-03-22 (author and commit date of 89b55566d83))
First affected version
4.0.0 (released 2012-10-11) [exec; the pre-release 4.0.0-ALPHA also fails 8 of 8]; 3.6.x is not affected because it has no GroupingSearch (LUCENE-3778 went to trunk only; 3.5.0 and 3.6.2 executed: API absent; 3.6.0/3.6.1 jar listings)
Affected versions
[exec] 4.0.0-ALPHA, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 fail with the follow-up's version-specific history probes (8 of 8 cases in 4.x, 4 of 4 term-grouping cases from 5.0.0 on, -da), the finder's JUnit fails 8 of 8 on 10.0.0, 10.5.2, branch_10_6 head 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193 (-ea and -da), the independent reproduction's own JUnit fails 8 of 10 (both controls pass) on main (-ea and -da), 10.5.2 and 9.12.3, and the registration check re-ran the finder's JUnit on main (-da, seed 5EED0387: 8 run, 8 failed). [read] every other point release from 4.0.0 to 10.5.2 is inferred affected: the int sums are present in every tag read and GroupingSearch's history has no capping change. The wrapped-sum message 'topNGroups must be >= 1 (got -2147483648)' for groupOffset 1 is identical in every version; the offset-0 symptoms vary with the collector internals (field-sorted groupDocsLimit = MAX_VALUE: OutOfMemoryError on 4.0.x and main, NegativeArraySizeException 4.10.4-6.6.6, PriorityQueue maxSize IllegalArgumentException 7.7.3-10.5.2). Not fixed upstream: upstream main 645d4fe14a20 (author date 2026-10-28) has no grouping commit after a9779ef3c193. All runs on Linux aarch64 with JDK 25.0.3.
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; also reached by Z3, exhaustive check
Upstream and prior reports
none found (the prior-report record, verdict new, searched 2026-10-28, read-only; finder's search 2026-10-28; refresh 2026-10-28): a 66-query scripted prior-report search (GitHub 26, commits 3, downstream 2, the dev@, java-user@, issues@ and commits@ lists 24, JIRA 5, web 6; 540 distinct hits, 0 high, 5 medium) and the finder's 51-query run (168 hits, 0 high, 0 medium); 24 manual GitHub searches over apache/lucene, apache/solr, elastic/elasticsearch, opensearch-project/OpenSearch and apache/lucenenet; LUCENE and SOLR JIRA queries; lists.apache.org searches; web searches; the changed files of every open apache/lucene PR updated since 2026-06-01 (195) and of the open grouping PRs. Nothing reports GroupingSearch or the grouping collector managers failing on huge or Integer.MAX_VALUE limits, or offset + limit wrapping in int. Precedents for the same class at other fix sites, not reports of this bug: #3195 (LUCENE-2119, core IndexSearcher, fixed in 2009 by capping n at maxDoc, before the grouping module existed), #5897 (LUCENE-4832, join ToParentBlockJoinCollector, 2013), SOLR-4164 (Solr's own grouping layer). The open PRs that touch the grouping sources (#14996, #15210, #15494, #16548, #16569) neither change the sums nor cap the limits; #10342 (LUCENE-9302, int total hit counts in TopGroups.merge) is another root cause. Refresh: no apache/lucene issue or PR updated since 2026-10-26 mentions GroupingSearch; 'topNGroups' finds only #15936, #12942 and #4851; 'groupDocsLimit' and '"got -2147483648"' find nothing; none of the 20 PRs created since 2026-10-26 (#16907-#16936) touches lucene/grouping (#16931 concerns MMapDirectory arena grouping, unrelated); the newest lucene/grouping commit on upstream main 645d4fe14a20 is 3e4a7a02ce0 (2026-10-07, #16779, unrelated).
Lucene commit tested
main 645d4fe14a20 (2026-10-28) through gradle in a scratch worktree (removed afterwards), seed DEADBEEF, assertions on and off: stock, the one-test repro fails; with the recommended fix it passes, together with TestGroupingSearch, all of :lucene:grouping:test (57 tests, assertions on) and :lucene:grouping:check -x test. A second fresh scratch worktree (2026-10-30) reproduced the stock failure (assertions on and off) and the fixed passes (the repro with TestGroupingSearch each way, the 57-test module); it did not repeat check -x test. The fact check's own fresh worktree repeated the stock failure, the fixed passes, the 57-test module and check -x test. Through the repro harness: the one-test repro on 645d4fe14a20 (assertions on and off, fix shadowed) and on the 10.6.0 jars from Maven Central (assertions on and off); a 1-document diagram probe and the finder's 8-case JUnit on 645d4fe14a20; a 9-case probe of every grouping mode on 645d4fe14a20 (assertions on and off, the doc's fact check); on main a9779ef3c193 the finder's JUnit, an independent 10-test JUnit and a devil's-advocate re-run (assertions on and off) and an adjudication re-run (assertions on); the finder's JUnit on branch_10_6 4ae946e5a9b (assertions off) and on 10.0.0 and 10.5.2; the independent JUnit on 10.5.2 and 9.12.3; version probes on 12 releases from 4.0.0-ALPHA to 10.5.2; a 21,040-search differential harness and an allocation probe on a9779ef3c193. The grouping module and IndexSearcher are identical at a9779ef3c193 and 645d4fe14a20. OpenJDK 25.0.3, Linux aarch64 (the only platform run).

Lucene and upstream sources

Finding record
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:44 (at a9779ef3c193) (@lucene.experimental), :123 ('@param groupLimit The number of groups to return from the specified group offset', no bound), :128-138 (search passes groupOffset and groupLimit unchanged), :146-148 (FirstPassGroupingCollectorManager gets the raw groupOffset and groupLimit), :197-205 (TopGroupsCollectorManager gets the raw groupDocsOffset and groupDocsLimit), :226-235 (groupByDocBlock, int sum groupDocsOffset + groupDocsLimit at :235), :321 ('@param groupDocsLimit The number of documents to return inside a group', no bound)
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java:92-97 (at a9779ef3c193) (validation accepts topNGroups = Integer.MAX_VALUE), :110 (int sum groupOffset + topNGroups); FirstPassGroupingCollector.java:97-98 ('topNGroups must be >= 1 (got ...)' reports the wrapped sum), :116 (getComparator(topNGroups + 1): NegativeArraySizeException at topNGroups = Integer.MAX_VALUE)
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollectorManager.java:107 (at a9779ef3c193) (int sum withinGroupOffset + maxDocsPerGroup); TopGroupsCollector.java:89-98 (per-group top-N collectors pre-sized by maxDocsPerGroup)
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollectorManager.java:125-128 (at a9779ef3c193) ('maxDocsPerGroup must be >= 1 (got ...)' reports the wrapped sum), :143 (int sum groupOffset + topNGroups); BlockGroupingCollector.java:217 and :237 (queue and comparators pre-sized by topNGroups)
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:588-599 (at a9779ef3c193) and :721-733 (core caps numHits: cappedNumHits = Math.min(numHits, Math.max(1, maxDoc)))
  • lucene/grouping/src/test/org/apache/lucene/search/grouping/TestTermGroupFacetCollectorManager.java:106-118 (at a9779ef3c193) (the module's own test passes Integer.MAX_VALUE limits to its facet API and guards the offset + limit sum itself)
  • Lucene main a9779ef3c193 SECURITY.md lines 28, 38, 42, 46, 50 (threat model; via follow-up the security record)
The finding in 30 seconds
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:41-46 (convenience class, @lucene.experimental), :55 (groupDocsLimit = 1), :123, :128-138, :141-148, :197-205, :221-235, :321 @ 645d4fe14a20
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java:92-97, :105-112; FirstPassGroupingCollector.java:97-98, :116; TopGroupsCollectorManager.java:100-108; TopGroupsCollector.java:122-148; BlockGroupingCollectorManager.java:112-128, :140-144; BlockGroupingCollector.java:210-240 @ 645d4fe14a20
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:587-601, :719-733; FieldComparator.java:148-149 (RelevanceComparator: new float[numHits]) @ 645d4fe14a20
Who is affected
  • lucene/classification/src/java/org/apache/lucene/classification/utils/DatasetSplitter.java:124 @ 645d4fe14a20
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java:92-97 (the checks), :110 (groupOffset + topNGroups); GroupingSearch.java:313, :324 (setGroupDocsOffset, setGroupDocsLimit) @ 645d4fe14a20
Why it looks unintended
  • lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:41-46, :115-126, :318-327 @ 645d4fe14a20
  • lucene/CHANGES.txt:18412, :18433, :18439-18441 (LUCENE-2119 under Bug fixes, 2.9.3 / 3.0.2), :13905, :13957, :14007-14008 (LUCENE-4832, 4.3.0) @ 645d4fe14a20
  • lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollectorManager.java:45-46; lucene/facet/src/java/org/apache/lucene/facet/FacetsCollectorManager.java:257; DrillSideways.java:199, :231 @ 645d4fe14a20
  • lucene/grouping/src/test/org/apache/lucene/search/grouping/TestTermGroupFacetCollectorManager.java:106-118 @ 645d4fe14a20

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

Repro files

About the diagrams

It is made separately from this text; its numbers come from the executed diagram probe on main.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.