All findings · Lucene finding · LFV-1004 · suggester-lookup-presizes-by-num

AnalyzingInfixSuggester, WFSTCompletionLookup and FreeTextSuggester size their results by num, so lookup(key, false, Integer.MAX_VALUE) throws even when one entry matches

Ask AnalyzingInfixSuggester, WFSTCompletionLookup or FreeTextSuggester for up to Integer.MAX_VALUE suggestions and the lookup throws, even on a suggester with one entry, because each reserves room for num results before it collects a single match. A num of 10 million works but costs 38-114 MiB per lookup for that one match.

Summary

Priority
Low, rank 641Passing Integer.MAX_VALUE to mean 'all suggestions' makes widely used suggesters such as AnalyzingInfixSuggester (and Solr's suggest.count through it) throw an OOM or IAE on every lookup, but the failure is loud and results are never wrong.
Impact
crash
Since
3.6.0 (2012, WFSTCompletionLookup), 4.4.0 (AnalyzingInfixSuggester), 4.6.0 (FreeTextSuggester): each allocation came with its classintroduced 2012-02-19 · over 14 years
Affected
every release from 3.6.0 to 10.6.0, and main, has at least one affected class
Status
documented
Found by
a differential probe across the Lookup implementations

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

A suggester (a Lookup) returns up to num completions of a key, best first. Three implementations reserve storage for num results before they collect any match, so a one-entry suggester asked for Integer.MAX_VALUE results requests an array of 2,147,483,647 slots, which the JVM refuses.

Background: suggesters and num

  1. A suggester (a Lookup) is built from a dictionary of entries, each a text with a weight, and answers lookup(key, onlyMorePopular, num) with up to num completions of key, best first.
  2. Lucene ships several. TSTLookup, FSTCompletionLookup and WFSTCompletionLookup complete prefixes from in-memory trees or FSTs; AnalyzingInfixSuggester (and its subclass BlendedInfixSuggester) keeps a small Lucene index and runs a top-N search sorted by weight; FreeTextSuggester predicts the next word from n-grams.
Summary of the bug. A suggester is built from the single entry lucene and asked lookup("luc", false, Integer.MAX_VALUE); expected: the one suggestion, as TSTLookup and FSTCompletionLookup return. Inside, AnalyzingInfixSuggester builds a TopFieldCollectorManager of num slots and WFSTCompletionLookup and FreeTextSuggester call new ArrayList<>(num) before they collect any match, so each throws OutOfMemoryError: Requested array size exceeds VM limit. With num = 10,000,000 the call works but allocates about 114 MiB or 38 MiB. IndexSearcher.search caps n at maxDoc; the suggester's own manager does not. The fix caps the collector at maxDoc and stops pre-sizing the lists.
One entry, num = Integer.MAX_VALUE: the three suggesters reserve num slots before they collect any match and throw; below the limit memory follows num; IndexSearcher caps at maxDoc, the suggester does not; the fix sizes by what can match. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Build an AnalyzingInfixSuggester with its default constructor from one entry, lucene, and ask for every completion of luc: lookup("luc", false, Integer.MAX_VALUE).
  2. You expect [lucene]: num is documented as the "maximum number of results to return", and TSTLookup and FSTCompletionLookup return [lucene] for the same call.
  3. You get OutOfMemoryError: Requested array size exceeds VM limit (on 7.5.0 to 10.6.0 an IllegalArgumentException). WFSTCompletionLookup and FreeTextSuggester throw the same OutOfMemoryError.
  4. Why: before collecting any match, each reserves one slot per possible result, 2,147,483,647 slots, more than a Java array can hold. IndexSearcher.search(query, n, sort) caps n at the number of documents (here 1); the suggester builds the same top-N collector without that cap, and the other two call new ArrayList<>(num).
  5. Below that limit the call works, but memory follows num, not the matches: with num = 10,000,000, one lookup for the one match allocates 114.5 MiB (AnalyzingInfix) or 38.1 MiB (WFST, FreeText); TSTLookup allocates 0.0 MiB.

Who is affected

Code that passes one of these suggesters, or BlendedInfixSuggester (a subclass of the infix one), a num far above its number of entries: Integer.MAX_VALUE for "all suggestions", or a count taken from a request without a bound. Default constructors. Counts in the tens to thousands work; the failure is loud, and no recorded call returned a wrong result.

Mechanism, terms and numbers710 words

Lookup.lookup(key, onlyMorePopular, num) documents num only as the "maximum number of results to return". Three implementations allocate storage for num results before they know how many entries match: AnalyzingInfixSuggester builds new TopFieldCollectorManager(SORT, num, null, 1), whose collectors pre-allocate num slots, and never caps num at maxDoc the way IndexSearcher.search(Query, int, Sort) does for the same manager; WFSTCompletionLookup and FreeTextSuggester call new ArrayList<>(num). So lookup("luc", false, Integer.MAX_VALUE) on a 1-entry suggester throws OutOfMemoryError: Requested array size exceeds VM limit on main (on 7.5.0 to 10.6.0 AnalyzingInfixSuggester throws IllegalArgumentException from PriorityQueue's size guard instead; older releases throw other errors), while TSTLookup and FSTCompletionLookup return the match. A large num below the array limit works as long as the heap can hold num slots and costs memory in proportion to num: 114.5 MiB and 38.1 MiB per lookup at num = 10,000,000 for one match. Each allocation came with its class (3.6.0, 4.4.0, 4.6.0). Default constructors; the failure is loud, and every recorded successful call returned the right results.

The code (main 645d4fe14a20, lucene/suggest/.../suggest/)

  • Contract. Lookup.java:190: "@param num maximum number of results to return" (also :206, :219). No bound, no validation. Lookup and the three classes are @lucene.experimental.
  • AnalyzingInfixSuggester. TopFieldCollectorManager c = new TopFieldCollectorManager(SORT, num, null, 1); (AnalyzingInfixSuggester.java:739), then searcher.search(finalQuery, c) (:741). The manager's javadoc: its collectors "pre-allocate a full array of length numHits" (TopFieldCollectorManager.java:44-45). On main FieldValueHitQueue.create builds the weight sort's comparator first (FieldValueHitQueue.java:146), and LongComparator allocates new long[numHits] (LongComparator.java:38), which the JVM refuses for Integer.MAX_VALUE elements. On 7.5.0 to 10.6.0 the PriorityQueue constructor ran first and rejected the size ("Throw exception to prevent confusing OOME"), hence the IllegalArgumentException; the order changed with #14873 (LFV-993).
  • The cap the suggester skips. IndexSearcher.search(Query query, int n, Sort sort) (IndexSearcher.java:677) reaches final int limit = Math.max(1, reader.maxDoc()); and final int cappedNumHits = Math.min(numHits, limit); (:721, :729) before it builds the same manager (:733). On a 1-document index, searcher.search(q, Integer.MAX_VALUE, sort) returns the 1 hit, while searcher.search(q, new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1)) throws the same OutOfMemoryError (executed). The suggester's own comment just below the search call still shows the capped call as the "slower way": // hits = searcher.search(query, null, num, SORT); (AnalyzingInfixSuggester.java:743-744).
  • WFSTCompletionLookup. An unbuilt suggester (:162-164) or an unmatched prefix (:179-181) returns an empty list; otherwise List<LookupResult> results = new ArrayList<>(num); (WFSTCompletionLookup.java:183). num also goes to Util.shortestPaths (:198), which pre-sizes nothing by it: its TopNSearcher keeps candidate paths in a TreeSet and results in an unsized list (Util.java:205, :296), which grow with the paths found, up to num (:215, :247-248, :467).
  • FreeTextSuggester. List<LookupResult> results = new ArrayList<>(num); (FreeTextSuggester.java:519), before any n-gram model runs.
  • The siblings that work. TSTLookup (with onlyMorePopular = false) loops to maxCnt = Math.min(num, list.size()) (TSTLookup.java:193); FSTCompletion streams lookup(key).limit(num).toList() (FSTCompletion.java:204-208).

The numbers (main 645d4fe14a20, -da, -Xmx2g, bytes allocated by the calling thread during one lookup after a warm-up, not retained heap)

lookup("luc", false, num), 1 entrynum = 1000num = 10,000,000num = Integer.MAX_VALUE
AnalyzingInfixSuggester1 result, 0.0 MiB1 result, 114.5 MiBOutOfMemoryError
WFSTCompletionLookup1 result, 0.0 MiB1 result, 38.1 MiBOutOfMemoryError
FreeTextSuggester1 result, 0.0 MiB1 result, 38.1 MiBOutOfMemoryError
TSTLookup, FSTCompletionLookup1 result, 0.0 MiB1 result, 0.0 MiB1 result

38.1 MiB is almost all Object[num], 10^7 four-byte references (compressed object pointers on). 114.5 MiB is dominated by the comparator's long[num] (76.3 MiB) and the queue's Object[num + 1] (38.1 MiB, PriorityQueue.java:129, :134), plus small per-call overhead (27,000 bytes in all at num = 1000). These are measurements on OpenJDK 25.0.3, Linux aarch64, not portable constants. With 8 entries (first call at each num, no warm-up) the figures are within 0.1 MiB (114.5, 38.2, 38.2 MiB), and BlendedInfixSuggester, which asks its parent for num x 10, allocates 1144.4 MiB. How far below the limit a lookup still works depends on the heap: with -Xmx1g a probe got OutOfMemoryError: Java heap space from AnalyzingInfixSuggester at num = 10^8 and from all three at 429,496,730.

Exposure and likelihood197 words
  • Who is hit. Callers of lookup on the three suggesters, and on BlendedInfixSuggester, which inherits the AnalyzingInfixSuggester collector, multiplied by numFactor (default 10).
  • Default vs opt-in. Default constructors and settings; nothing to opt into.
  • What goes wrong. On a built suggester, Integer.MAX_VALUE fails on every call to AnalyzingInfixSuggester and FreeTextSuggester, even when nothing matches, and on every call to WFSTCompletionLookup whose prefix matches an entry. The error is an OutOfMemoryError, which callers often do not catch, or, for AnalyzingInfixSuggester on 7.5.0 to 10.6.0, an IllegalArgumentException that blames an argument the javadoc allows. A num in the millions returns the right answer but allocates num slots on every call; concurrent calls multiply that.
  • Scale. None needed: one entry is enough.
  • Workaround. Pass a finite count. Capping num at getCount() is safe for AnalyzingInfixSuggester and WFSTCompletionLookup (at most one suggestion per entry) but not for FreeTextSuggester: the single entry lucene lucid lucky gives getCount() = 1, while lookup("luc", false, 1000) returns 3 suggestions and lookup("luc", false, getCount()) returns 1 (executed).
  • How likely. Rare: Integer.MAX_VALUE fails on the first call, so developers meet it in testing and pick a finite count; no public report was found.

Minimal repro

One JUnit test, public API: build each of the three suggesters from the single entry lucene and call lookup("luc", false, Integer.MAX_VALUE). Fixed input, one thread; it fails with every seed tried, with assertions on and off. With the fix it passes unchanged, so it doubles as the regression test.

TestLookupMaxNum.java (lucene/suggest, package org.apache.lucene.search.suggest, 1 test)47 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.suggest;

import java.io.IOException;
import java.io.StringReader;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.search.spell.PlainTextDictionary;
import org.apache.lucene.search.suggest.analyzing.AnalyzingInfixSuggester;
import org.apache.lucene.search.suggest.analyzing.FreeTextSuggester;
import org.apache.lucene.search.suggest.fst.WFSTCompletionLookup;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestLookupMaxNum extends LuceneTestCase {

  public void testLookupWithMaxNum() throws IOException {
    // expected the 1 suggestion, actual OutOfMemoryError: each lookup allocates num result slots
    try (Directory dir1 = new ByteBuffersDirectory();
        Directory dir2 = new ByteBuffersDirectory();
        AnalyzingInfixSuggester infix = new AnalyzingInfixSuggester(dir1, new StandardAnalyzer())) {
      assertEquals(1, lookupAll(infix));
      assertEquals(1, lookupAll(new WFSTCompletionLookup(dir2, "wfst")));
      assertEquals(1, lookupAll(new FreeTextSuggester(new StandardAnalyzer())));
    }
  }

  private static int lookupAll(Lookup lookup) throws IOException {
    lookup.build(new PlainTextDictionary(new StringReader("lucene")));
    return lookup.lookup("luc", false, Integer.MAX_VALUE).size();
  }
}
Command
cp $REPRO/TestLookupMaxNum.java $LUCENE/lucene/suggest/src/test/org/apache/lucene/search/suggest/
cd $LUCENE && ./gradlew :lucene:suggest:test --tests org.apache.lucene.search.suggest.TestLookupMaxNum -Ptests.seed=DEADBEEF
Expected
Each lookup returns the 1 suggestion, lucene.
Observed
OutOfMemoryError: Requested array size exceeds VM limit from AnalyzingInfixSuggester.lookup (10.5.2 and 10.6.0: IllegalArgumentException: maxSize must be >= 0 and < 2147483631).

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

More files, environment, full output217 words

Copy the test into lucene/suggest/src/test/org/apache/lucene/search/suggest/ of a Lucene checkout and run the command below. On main the first assertion (AnalyzingInfixSuggester) already throws, so the other two lines are not reached; copies holding only the WFSTCompletionLookup line or only the FreeTextSuggester line fail the same way, at WFSTCompletionLookup.java:183 and FreeTextSuggester.java:519.

LuceneTestCase still randomizes codec, locale and the like; seeds DEADBEEF, 5EED1004 and 1004FACE all fail. The error is the JVM's array-length limit, not heap exhaustion: it is the same at -Xmx64m, 2g and 40g.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.5.2 and 10.6.0 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (the only platform it ran on)
Command (environment)
cp $REPRO/TestLookupMaxNum.java $LUCENE/lucene/suggest/src/test/org/apache/lucene/search/suggest/
cd $LUCENE && ./gradlew :lucene:suggest:test --tests org.apache.lucene.search.suggest.TestLookupMaxNum -Ptests.seed=DEADBEEF

Observed output

# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF (module prefixes and two overload frames trimmed)
TestLookupMaxNum > testLookupWithMaxNum FAILED
    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.TopFieldCollectorManager.newCollector(TopFieldCollectorManager.java:29)
        at org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:752)
        at org.apache.lucene.search.suggest.analyzing.AnalyzingInfixSuggester.lookup(AnalyzingInfixSuggester.java:741)
        ...
        at org.apache.lucene.search.suggest.Lookup.lookup(Lookup.java:195)
        at org.apache.lucene.search.suggest.TestLookupMaxNum.lookupAll(TestLookupMaxNum.java:45)
        at org.apache.lucene.search.suggest.TestLookupMaxNum.testLookupWithMaxNum(TestLookupMaxNum.java:37)

# standalone runner (tests_run=1 failures=1 each)
main 645d4fe14a20  -ea seed DEADBEEF, -da seed 5EED1004   OutOfMemoryError (as above)
10.5.2             -da seed DEADBEEF                      IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647
10.6.0             -ea seed DEADBEEF                      the same IllegalArgumentException

Why it looks unintended

  • num is documented only as the "maximum number of results to return", and TSTLookup and FSTCompletionLookup return every match for the same call.
  • Lucene fixed this pattern once already: for LUCENE-2119, Michael McCandless capped IndexSearcher's count at maxDoc, calling users' assumption that the queue is not pre-allocated "a reasonable assumption".
  • AnalyzingInfixSuggester builds the same collector itself and skips that cap.
The full argument251 words
  • Contract. Lookup.java:190: num is a maximum; nothing calls it a buffer size or says it must stay near the number of entries.
  • Siblings. TSTLookup (with onlyMorePopular = false), FSTCompletionLookup, AnalyzingSuggester and FuzzySuggester return the matches for the same call (executed).
  • Tests. TestFSTCompletion.testFullMatchList passes Integer.MAX_VALUE to FSTCompletion.lookup and expects every match (TestFSTCompletion.java:157, :165). That is the API under FSTCompletionLookup, not Lookup.lookup itself.
  • LUCENE-2119 (#3195, 2009), opened by Michael McCandless: "Note that this is a nonsense value to pass in, since our PQ impl allocates the array up front. ... Better, maybe: in IndexSearcher, if that n is ever > maxDoc(), set it to maxDoc(). This trips users up fairly often because they assume our PQ doesn't statically pre-allocate (a reasonable assumption...)." Michael McCandless's commit ae72952fa41 added the cap still in IndexSearcher. CHANGES credits the report to Paul Taylor.
  • SOLR-5661 (2014), reported with a patch by Raintung Li and committed by Michael McCandless, added only the PriorityQueue size guard. On it Michael McCandless noted that "Lucene's IndexSearcher tries to prevent this, when you ask for Integer.MAX_VALUE as the topN hits to the search method, it drops that to the maxDoc for the reader", and asked of the Solr caller: "I'm not sure how to fix Solr here: somewhere it should limit the size it passes to the PQ constructor?" AnalyzingInfixSuggester is such a caller: it builds the manager itself.
  • SOLR-2378 (2011): the original FSTLookup capped its pre-size with Math.min(10, num) (Dawid Weiss).
The devil's-advocate check129 words

The strongest case for "works as designed" (raised by reviewers from another model family and by an intended-behaviour devil's advocate): Lucene's top-N structures pre-allocate and TopFieldCollectorManager documents it; Lookup and the three classes are @lucene.experimental; a comment in the document suggester assumes "num is smallish" (TopSuggestDocsCollector.java:149); and LUCENE-2119 itself calls Integer.MAX_VALUE "a nonsense value to pass in".

Why it fails: the manager's note binds whoever builds the manager, here the suggester, and the caller of lookup never sees it; the same LUCENE-2119 text calls the user's assumption reasonable and fixed the caller, not the user; @lucene.experimental is about API stability, not about failing on allowed input; the sibling suggesters and the module's own test treat Integer.MAX_VALUE as "all". The argument bears on priority, not on correctness.

Suggested fix

Recommended Cap the collector at maxDoc and stop pre-sizing the two lists

Size by what can match, not by num: in AnalyzingInfixSuggester.lookup cap the collector at Math.min(num, Math.max(1, maxDoc)), as IndexSearcher does, and create the two result lists with new ArrayList<>(). With it the test and the suggest module's 237 tests pass, and a lookup at num = 10,000,000 allocates 0.0 MiB.

// AnalyzingInfixSuggester.lookup
// The collector pre-allocates numHits slots: cap it at maxDoc, as IndexSearcher.search does
int numHits = Math.min(num, Math.max(1, searcher.getIndexReader().maxDoc()));
TopFieldCollectorManager c = new TopFieldCollectorManager(SORT, numHits, null, 1);

// WFSTCompletionLookup.lookup and FreeTextSuggester.lookup
List<LookupResult> results = new ArrayList<>(); // was: new ArrayList<>(num)
All options and trade-offs3 options · 637 words

Three options; A was validated with gradle in a fresh scratch worktree at 645d4fe14a20, B and C were not executed. None fixes LFV-998 (BlendedInfixSuggester computes num * numFactor in int), which needs its own fix for Integer.MAX_VALUE to work through BlendedInfixSuggester. Same pattern, not part of this report: TSTLookup with onlyMorePopular = true sizes a PriorityQueue by num (TSTLookup.java:195-196) although maxCnt = Math.min(num, list.size()) is computed on :193.

Cap the collector at maxDoc and stop pre-sizing the two lists (validated by the checks) (recommended)

In AnalyzingInfixSuggester.lookup (:739) pass Math.min(num, Math.max(1, searcher.getIndexReader().maxDoc())) to TopFieldCollectorManager, and keep passing the caller's num to createResults (:745). In WFSTCompletionLookup.lookup (:183) and FreeTextSuggester.lookup (:519) create the list with new ArrayList<>(). +5/-3 lines in three files, one added line a comment.

// AnalyzingInfixSuggester.lookup
// The collector pre-allocates numHits slots: cap it at maxDoc, as IndexSearcher.search does
int numHits = Math.min(num, Math.max(1, searcher.getIndexReader().maxDoc()));
TopFieldCollectorManager c = new TopFieldCollectorManager(SORT, numHits, null, 1);

// WFSTCompletionLookup.lookup and FreeTextSuggester.lookup
List<LookupResult> results = new ArrayList<>(); // was: new ArrayList<>(num)

Trade-offs

  • No change in results: the cap only bounds the collector by what the index can return, and createResults still gets the caller's num, which BlendedInfixSuggester relies on.
  • A result list for many matches grows as it fills instead of being allocated once; the runtime effect was not measured.
  • Does not fix LFV-998: BlendedInfixSuggester at Integer.MAX_VALUE still throws IllegalArgumentException: numHits must be > 0; at 10,000,000 its allocation drops from 1144 MiB to 0 MiB.
  • Optional hardening (the finder's extra hunk): with the pre-size gone, FreeTextSuggester reaches num + seen.size() (:608) with num = Integer.MAX_VALUE, which wraps in int once seen is not empty; the finder saturates it with (int) Math.min((long) num + seen.size(), Integer.MAX_VALUE). One scratch two-token lookup gave the same results without it (gradle seed DEADBEEF, so assertions off), and so did an assertions-on run of the key lucene se (three bigrams, 3 results at 1000 and Integer.MAX_VALUE); that covers only these cases, so keep the saturation or justify leaving it out.

What the checks validated

  • Fresh scratch worktree at 645d4fe14a20, JDK 25.0.3, gradle with -Ptests.seed=DEADBEEF (2026-10-30; this seed runs with assertions off, and a re-run of both steps with -Ptests.asserts=true gave the same results): without the fix the minimal test fails with OutOfMemoryError at AnalyzingInfixSuggester.java:741; with fix.diff applied, :lucene:suggest:checkGoogleJavaFormat and :lucene:suggest:test pass: 32 suites, 237 tests, 7 skipped, 0 failures, including TestLookupMaxNum, TestAnalyzingInfixSuggester (23), TestBlendedInfixSuggester (9), TestFreeTextSuggester (11, 1 skipped) and TestWFSTCompletion (7).
  • With the three fixed files shadowed, the standalone runner passes the test with -ea and with -da, and allocation probes on 8 entries and on 1 entry return all matches at num = 10,000,000 and Integer.MAX_VALUE with 0.0 MiB allocated (BlendedInfixSuggester at Integer.MAX_VALUE still throws: LFV-998).
  • The finder's version, which also has the FreeTextSuggester saturation hunk, passed the finder's JUnit 4 of 4 and the independent reproducer's 8 of 8.

Replace the hand-built manager with searcher.search(finalQuery, num, SORT), the call the comment at :744 names, which caps num at maxDoc; plus the two list changes of option A. Not executed.

Trade-offs

Reuses IndexSearcher's cap, but that path counts hits up to TOTAL_HITS_THRESHOLD (1000) instead of the suggester's 1 before it can skip non-competitive documents, so it may be slower on large suggesters (not measured), for no gain here.

Document a limit, or reject a large num

Add a javadoc note to Lookup.lookup that implementations may allocate num slots up front, or throw IllegalArgumentException above some bound. Not executed.

Trade-offs

Turns the "all suggestions" idiom that TSTLookup and FSTCompletionLookup support into a documented error and keeps the memory cost of a large num. Not recommended.

Downstream note

Solr's SuggestComponent passes the request parameter suggest.count unchanged to lookup as num, so a Solr deployment with one of these suggesters has to bound suggest.count itself.

How it was found

A differential probe ran the same lookup on every Lookup class with growing num (e.g. 1000, 10^8, Integer.MAX_VALUE) and compared each answer with the small-num one. These three failed or allocated in proportion to num (BlendedInfixSuggester failed through a separate overflow); the other four classes passed. Reading the code found the three allocations.

The method in full177 words

Method: executed differential testing (no formal model), in a hunt round on numeric limits of public API arguments, on the stock main jars.

  • First signal. A planning probe called lookup("luc", false, num) on 8-entry suggesters of the eight Lookup classes of the module in nine configurations, -Xmx1g: at Integer.MAX_VALUE the three threw OutOfMemoryError and BlendedInfixSuggester an IllegalArgumentException (its own int overflow, LFV-998); at num = 10^8 they allocated 381 MiB (WFST, FreeText) or ran out of heap after 762 MiB (AnalyzingInfix), where num = 1000 returns 8; TSTLookup, FSTCompletionLookup, AnalyzingSuggester and FuzzySuggester passed.
  • Sweep. The finder widened it to 11 configurations, each with 28 num values chosen with an SMT model of the arithmetic on num plus 7 prefix checks: 385 checks, against the small-num result and a 64 MiB allocation bound. Failures came only from these three sites, from BlendedInfixSuggester (LFV-998) and from one AnalyzingSuggester value (a separate overflow, LFV-1003).
  • Reduction. Code reading located the allocations; the finding was reduced to a public-API JUnit test, later to the single test shown here.

Prior reports, history and checks

Prior reports

No earlier report found (searched 2026-10-29, re-checked 2026-10-30). Related only: LUCENE-2119 capped the same pattern in IndexSearcher; SOLR-5661 added PriorityQueue's size check.

Searches4 searches

Related, not this bug: #3195 (LUCENE-2119, IndexSearcher with Integer.MAX_VALUE, fixed by the maxDoc cap), SOLR-5661 (the PriorityQueue size guard), #9588 (LUCENE-8542, open: core top-hits collectors pre-allocate per slice), #6465 (LUCENE-5402, open: an Integer.MAX_VALUE TopDocs at suggester build time, not lookup), #1581 (LUCENE-504: a FuzzyQuery queue pre-sized by a user maximum). Open PRs touching the files: #16875 (javadoc typos in AnalyzingInfixSuggester.java) and the draft #16847 (long global doc ids, doc-id handling in createResults); neither touches the sizing. Limits: web search was throttled, GitHub search returns at most 50 items per query, inline PR review comments are not searchable. No maintainer has commented on this bug.

What was searched, when, and what turned up
WhereWhenHowFound
Search tool (GitHub issues/PRs/commits incl. the JIRA archive, dev@, java-user@, issues@, commits@, downstream, web)2026-10-29three runs, 153 queries: class and method names, symptoms and error texts, the Solr suggest.count angle, the general pre-size patternno hit about these sites
GitHub (apache/lucene, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch, all of GitHub), JIRA, mailing lists2026-10-2940 manual GitHub queries, 14 JQL queries, 14 list queries, web queriesLUCENE-2119, SOLR-5661, LUCENE-8542, LUCENE-5402, LUCENE-504: related, none the same bug
Open PRs2026-10-28/29changed files of all 414 open apache/lucene PRs, then of every PR updated sinceno PR changes the sizing
GitHub, re-check2026-10-30issue/PR search for the class names with num, Integer.MAX_VALUE, OutOfMemoryError, the error text and maxDoc/numHits; the open PRs that touch lucene/suggest; the newest commits on the three files; the tracking issue0 results; of the 60 newest open PRs, #16875, #16952 and the draft #16847 touch the module but not the sizing; the files unchanged on upstream main 2f424fd428da; no comment on this bug

History

No regression: each allocation came with its class, WFSTCompletionLookup in 3.6.0 (2012), AnalyzingInfixSuggester in 4.4.0 and FreeTextSuggester in 4.6.0 (both 2013). Every release since 3.6.0 has at least one affected class, and main still does.

Versions tested5 rows

Introducing commits: a519b630eea and the branch_3x backport d9a6ee8b125 (2012-02-19, LUCENE-3714, #4788, Robert Muir); 63fa645aa80, shipped through 39f54258327 (2013-07-15, LUCENE-4845, #5910, Michael McCandless); b50a2edbb23 and 9494be7ec2a (2013-10-02, LUCENE-5214, #6278, Michael McCandless). The AnalyzingInfixSuggester collector changed form (new int[topN] in 4.4-4.7, TopFieldCollector.create(SORT, num, ...) from 4.8.0, TopFieldCollectorManager since LUCENE-10002 (#240, 2023), today's four-argument call since #13977 (2024)) but was never capped. The error at Integer.MAX_VALUE changed with PriorityQueue's guard: OutOfMemoryError in 4.4.0-4.7.2, IllegalArgumentException in 4.8.0-4.9.0, NegativeArraySizeException in 4.9.1-7.4.0, IllegalArgumentException in 7.5.0-10.6.0 and OutOfMemoryError on main (#14873). Old releases ran on JDK 25; 10.6.0 was run from its checksum-verified release jars (commit 4ae946e5a9b9).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (gradle; standalone -ea and -da): OutOfMemoryError
10.6.0, 10.5.2 (release jars) affectedthe minimal test fails: IllegalArgumentException from AnalyzingInfixSuggester
3.6.0 to 10.5.2 and the 10.6 RC 21 versionsa portable probe fails on 3.6.0, 3.6.2, 4.0.0, 4.3.1, 4.4.0, 4.5.1, 4.6.0, 4.7.2, 4.8.0, 4.10.4, 5.5.5, 6.6.6, 7.4.0, 7.5.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and the RC for every class present
3.5.0 not affectednone of the three classes exists
fix A (scratch worktree at 645d4fe14a20) not affectedthe minimal test and the suggest module's tests pass

Independent checks

An independent reproducer, an intended-behaviour devil's advocate, an adjudication, a fresh-seed re-run and a framework-free audit confirm it; reading-only reviews by another model family agree on the root cause (one gave a procedural "not real": it could not run Java). This doc re-ran the test and validated the fix with gradle.

Checks and verdicts381 words

The cross-model reviews ran reading only and could not run Java; the refute lens therefore returned a procedural "not real" while agreeing on the root cause and that the inputs are in contract. No executing cross-model review exists.

Each independent check and its verdict
CheckVerdictWhat it found
finder (differential sweep) candidate385 checks fail only at the three sites, at BlendedInfixSuggester (LFV-998) and at one AnalyzingSuggester value (LFV-1003); its JUnit fails 3 of 4 on main (-ea, -da), 10.5.2, 9.12.3 and the 10.6 RC; its fix passes 4 of 4.
cross-model review, refute lens (OpenAI GPT-5.6, reading only) procedural "not real" (medium)Agreed on the root cause; raised "pre-allocation is documented" (moderate). A separate run re-ran the repro with -da: 3 of 4 fail.
cross-model review, intended lens (OpenAI GPT-5.6, reading only) real (medium)Flagged the BlendedInfixSuggester scope limit, the "num is smallish" comment and the TestFSTCompletion idiom; re-run with -ea: 3 of 4 fail.
independent reproducer real, LowOwn 8-test JUnit fails 6 of 8 on main (-ea, -da), 10.5.2 and 9.12.3; first to report the release-vs-main error split (IllegalArgumentException from AnalyzingInfixSuggester on the releases); measured 38 / 38 / 114 MiB at 10^7; with the fix 8 of 8 pass and 0 MiB.
intended-behaviour devil's advocate realArgued the documented pre-allocation, @lucene.experimental and the LUCENE-2119 "nonsense value" remark, and rejected each; traced the release-vs-main error split to its cause (#14873) and found the TSTLookup onlyMorePopular = true pre-size.
adjudication confirmed (high)Re-ran the independent reproducer's test with a new seed, -ea and -da: 6 of 8 fail, controls pass.
Registration check and credibility audit keepFresh seeds on a clean checkout: 3 of 4 fail (-ea, -da); a framework-free main() probe reproduces the three failures and the allocation numbers.
minimal-repro audit verifiedThe one test fails on main 645d4fe14a20 (-ea, -da, two seeds) and passes with the fix.
this doc reproduced, fix validatedThe test fails on main (-ea, -da), 10.5.2 and 10.6.0; gradle in a fresh 645d4fe14a20 worktree fails without the fix and passes with fix A, together with the suggest module's 237 tests and the format check.

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

Finding record

LFV-1004, snapshot of 2026-11-06

Registry title
AnalyzingInfixSuggester, WFSTCompletionLookup and FreeTextSuggester allocate storage for num results before they know how many suggestions match, so lookup(key, false, Integer.MAX_VALUE) on an 8-entry suggester throws instead of returning the 8 suggestions, and a large num allocates memory in proportion to num
Area
suggest / Lookup.lookup result sizing (AnalyzingInfixSuggester, WFSTCompletionLookup, FreeTextSuggester)
Priority
Low, rank 641: Passing Integer.MAX_VALUE to mean 'all suggestions' makes widely used suggesters such as AnalyzingInfixSuggester (and Solr's suggest.count through it) throw an OOM or IAE on every lookup, but the failure is loud and results are never wrong.
Impact class
crash
Impact note
Applications that call lookup on an AnalyzingInfixSuggester (and through it BlendedInfixSuggester), WFSTCompletionLookup or FreeTextSuggester, with default constructors, and pass Integer.MAX_VALUE as 'all suggestions' get an OutOfMemoryError (on releases an IllegalArgumentException from AnalyzingInfixSuggester) on every such call at any suggester size, and a num in the millions succeeds but allocates memory in proportion to num (38 MiB per lookup for WFSTCompletionLookup and FreeTextSuggester and 114 MiB for AnalyzingInfixSuggester at num = 10^7 on 8 entries); ordinary num values (tens to thousands) work as expected.
Summary
You build an AnalyzingInfixSuggester, WFSTCompletionLookup or FreeTextSuggester from 8 entries that start with 'luc' and call lookup("luc", false, Integer.MAX_VALUE) to get every suggestion; num is documented as the 'maximum number of results to return', so you expect the 8 suggestions that num = 1000 returns, which is what TSTLookup and FSTCompletionLookup give for the same call. Instead each of the three sizes its result storage by num before it knows how many entries match (AnalyzingInfixSuggester builds a TopFieldCollectorManager of num slots that, unlike IndexSearcher.search, it never caps at maxDoc; the other two call new ArrayList<>(num)), so the call throws OutOfMemoryError 'Requested array size exceeds VM limit' (on 9.12.3, 10.5.2 and the 10.6 RC, AnalyzingInfixSuggester throws IllegalArgumentException 'maxSize must be >= 0 and < 2147483631' instead). A smaller but still large num succeeds and allocates memory in proportion to num rather than to the matches: 114 MiB per lookup for AnalyzingInfixSuggester and 38 MiB for the other two at num = 10^7.
Introduced
No regression point: each site arrived with the class that contains it [read: git log -S / blame in the full-history clone; exec: the release before each class lacks it]. WFSTCompletionLookup: a519b630eea (trunk) and its same-day branch_3x backport d9a6ee8b125, LUCENE-3714 'add weighted FST suggester impl' (#4788; Robert Muir, CHANGES credit Mike McCandless, Dawid Weiss, Robert Muir; JIRA patch era, no pull request), with new ArrayList<LookupResult>(num). AnalyzingInfixSuggester: 63fa645aa80 (trunk), 39f54258327 (lucene_solr_4_4) and d0fe1baf093 (branch_4x), LUCENE-4845 'add AnalyzingInfixSuggester' (#5910; Michael McCandless), whose collector was sized by num from the start (FirstNDocsCollector, new int[topN], in 4.4.0-4.7.2; TopFieldCollector.create(SORT, num, ...) from LUCENE-5477, 02e3fd09eb5, 4.8.0; new TopFieldCollectorManager(SORT, num, null, 1) since #13977, bc18828fccc, 2024). FreeTextSuggester: b50a2edbb23 (trunk) and 9494be7ec2a (branch_4x), LUCENE-5214 'add FreeTextSuggester' (#6278; Michael McCandless), with new ArrayList<>(num) (2012-02-19 (WFSTCompletionLookup); 2013-07-15 (AnalyzingInfixSuggester); 2013-10-02 (FreeTextSuggester))
First affected version
3.6.0 [exec] (WFSTCompletionLookup, shipped in the contrib spellchecker jar; 3.5.0 has none of the three classes [exec]); AnalyzingInfixSuggester since 4.4.0 [exec] (4.3.1 lacks it [exec]); FreeTextSuggester since 4.6.0 [exec] (4.5.1 lacks it [exec])
Affected versions
Every release from 3.6.0 to 10.5.2, 10.6.0 (released 2026-10-29; run as its RC) and main [exec on 3.6.0, 3.6.2, 4.0.0, 4.3.1, 4.4.0, 4.5.1, 4.6.0, 4.7.2, 4.8.0, 4.10.4, 5.5.5, 6.6.6, 7.4.0, 7.5.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6 RC 4ae946e5a9b and main a9779ef3c193 with the follow-up's version-portable public-API probe: every site present in a release fails there; the finder's JUnit fails 3 of 4 on main (-ea and -da), 10.5.2, 9.12.3 and the RC; the independent reproduction's fails 6 of 8 on main, 10.5.2 and 9.12.3]; every release tag that contains one of the classes has its num-sized allocation without a cap [read]. Error at num = Integer.MAX_VALUE: WFSTCompletionLookup and FreeTextSuggester throw OutOfMemoryError 'Requested array size exceeds VM limit' on every version; AnalyzingInfixSuggester throws OutOfMemoryError in 4.4.0-4.7.2 (int[num]), IllegalArgumentException 'maxSize must be <= 2147483391' in 4.8.0-4.9.0 (the SOLR-5661 PriorityQueue guard), NegativeArraySizeException in 4.9.1-7.4.0, IllegalArgumentException 'maxSize must be >= 0 and < 2147483631' in 7.5.0 to 10.6.0, and OutOfMemoryError on main, where #14873 builds the sort comparators before PriorityQueue's size guard (LFV-993) [exec, except 4.9.0 and 4.9.1: read]. Below Integer.MAX_VALUE all three allocate in proportion to num on every version (num = 10^7 on 8 matches: 38 MiB each, 114 MiB for AnalyzingInfixSuggester since 4.8.0). Not fixed: the newest commits on the three files on upstream main are 54283150a81 (AnalyzingInfixSuggester, FreeTextSuggester; build tooling, author and committer date 2026-06-14) and 644feeb02a9 (WFSTCompletionLookup, 2024-09-17), neither touching the sizing [read, recheck 2026-10-29]. Caveat: all runs used JDK 25.0.3 on Linux aarch64; the allocation sizes assume compressed object pointers; 10.6.0 was checked only as RC jars
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
Upstream and prior reports
none found (searched 2026-10-29 by the follow-up's prior-report leg, plus the finder's searches of 2026-10-28 and the independent reproduction's, the intended-behaviour check's and the adjudication's, all read-only; the prior-report record, verdict new): three scripted prior-report searches of 51 queries each (the pre-size, the Solr suggest.count angle and the general pattern) over GitHub issues and PRs, commits, downstream trackers, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; 40 manual GitHub searches over apache/lucene, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch and all of GitHub, including the exact error texts; 14 JQL queries over LUCENE, SOLR and LUCENENET; 14 manual mailing-list queries; 6 manual web searches (plus the finder's 6); the 100 newest PRs and 26 newest issues, the changed files of the 50 open PRs updated since 2026-10-25 and the finder's scan of the changed files of all 414 open PRs. Recheck 2026-10-29 (6 searches, the newest commits on the three files on main, and the changed files of the 11 open PRs created since 2026-10-27, up to #16938): nothing new. Related, not this bug: #3195 (LUCENE-2119, fixed 2009) is the same pattern at IndexSearcher, fixed by capping nDocs at maxDoc (ae72952fa41); SOLR-5661 (fixed 2014) added the PriorityQueue size guard that gives the released IllegalArgumentException; #9588 (LUCENE-8542, open) is about core top-hits collectors pre-allocating numHits per slice; #6465 (LUCENE-5402, open) is an Integer.MAX_VALUE TopDocs at suggester dictionary build time; #1581 (LUCENE-504, fixed) is a FuzzyQuery queue pre-sized by a user maximum. Open PR #16847 touches AnalyzingInfixSuggester.java but only doc-id types; #16857 touches TopFieldCollectorManager.java and FieldValueHitQueue.java but not numHits sizing. Gaps: GitHub search returns at most 50 items per query, inline PR review comments are not searchable, and web search was throttled
Lucene commit tested
main 645d4fe14a20 (11.0.0-SNAPSHOT): the minimal test with gradle in a fresh scratch worktree, 2026-10-30 (fails without the fix; with fix A it passes together with the suggest module's 237 tests and the format check), and the standalone runner with -ea and -da; an allocation probe without and with the fix; the 10.5.2 and 10.6.0 release jars (the minimal test fails). The hunt ran on main a9779ef3c193, where the three suggester files and the core files involved are identical; the history sweep ran a portable probe on 23 version labels from 3.5.0 to main. The three files are unchanged on upstream main 2f424fd428da (2026-10-30).

Lucene and upstream sources

Finding record
  • a Lucene main checkout lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java:38 (@lucene.experimental), :184-196 ('@param num maximum number of results to return'); analyzing/AnalyzingInfixSuggester.java:95 (@lucene.experimental), :739-741 (new TopFieldCollectorManager(SORT, num, null, 1)), :744 (comment 'hits = searcher.search(query, null, num, SORT)'); fst/WFSTCompletionLookup.java:60, :179-183 (empty list without a prefix match, then new ArrayList<>(num)); analyzing/FreeTextSuggester.java:103, :519 (new ArrayList<>(num)), :604-608 (TopNSearcher depth num + seen.size()); tst/TSTLookup.java:193-196 (PriorityQueue sized by num on the onlyMorePopular path); identical in Lucene main ea57360912ed
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45 (NOTE: 'pre-allocate a full array of length numHits'); IndexSearcher.java:719-733 (searchAfter caps numHits at Math.max(1, reader.maxDoc())); lucene/suggest/src/test/org/apache/lucene/search/suggest/fst/TestFSTCompletion.java:157, :165 (lookup(..., Integer.MAX_VALUE) for the full match list)
The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java:38, :184-196 (:190), :206, :219
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingInfixSuggester.java:95, :739, :741, :743-744, :745
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/WFSTCompletionLookup.java:60, :162-164, :179-183, :198; lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/FreeTextSuggester.java:103, :519
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/tst/TSTLookup.java:193; lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletion.java:204-208
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45, :113-114; lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:146; lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:38; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:677, :721, :729, :733; lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:122-124, :129, :134; lucene/core/src/java/org/apache/lucene/util/fst/Util.java:205, :215, :247-248, :296, :467
Who is affected
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/WFSTCompletionLookup.java:179-181; lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/FreeTextSuggester.java:410 (getCount)
Why it looks unintended
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java:190
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/test/org/apache/lucene/search/suggest/fst/TestFSTCompletion.java:154-171 (:157, :165)
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/document/TopSuggestDocsCollector.java:147-150; lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45
Suggested fix
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:100 (TOTAL_HITS_THRESHOLD = 1000), :677-678, :721-733
  • a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingInfixSuggester.java:739, :745 (createResults gets num); lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/FreeTextSuggester.java:605-608

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro (one entry, lucene) and an executed probe on the stock main jars.

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.