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.
- A suggester (a
Lookup) is built from a dictionary of entries, each a text with a weight, and answerslookup(key, onlyMorePopular, num)with up tonumcompletions ofkey, best first. - Lucene ships several.
TSTLookup,FSTCompletionLookupandWFSTCompletionLookupcomplete prefixes from in-memory trees or FSTs;AnalyzingInfixSuggester(and its subclassBlendedInfixSuggester) keeps a small Lucene index and runs a top-N search sorted by weight;FreeTextSuggesterpredicts the next word from n-grams.
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.- Build an
AnalyzingInfixSuggesterwith its default constructor from one entry,lucene, and ask for every completion ofluc:lookup("luc", false, Integer.MAX_VALUE). - You expect
[lucene]:numis documented as the "maximum number of results to return", andTSTLookupandFSTCompletionLookupreturn[lucene]for the same call. - You get
OutOfMemoryError: Requested array size exceeds VM limit(on 7.5.0 to 10.6.0 anIllegalArgumentException).WFSTCompletionLookupandFreeTextSuggesterthrow the sameOutOfMemoryError. - 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)capsnat the number of documents (here 1); the suggester builds the same top-N collector without that cap, and the other two callnew ArrayList<>(num). - Below that limit the call works, but memory follows
num, not the matches: withnum= 10,000,000, one lookup for the one match allocates 114.5 MiB (AnalyzingInfix) or 38.1 MiB (WFST, FreeText);TSTLookupallocates 0.0 MiB.
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.
- Contract.
Lookup.java:190: "@param num maximum number of results to return" (also:206,:219). No bound, no validation.Lookupand the three classes are@lucene.experimental. - AnalyzingInfixSuggester.
TopFieldCollectorManager c = new TopFieldCollectorManager(SORT, num, null, 1);(AnalyzingInfixSuggester.java:739), thensearcher.search(finalQuery, c)(:741). The manager's javadoc: its collectors "pre-allocate a full array of length numHits" (TopFieldCollectorManager.java:44-45). On mainFieldValueHitQueue.createbuilds the weight sort's comparator first (FieldValueHitQueue.java:146), andLongComparatorallocatesnew long[numHits](LongComparator.java:38), which the JVM refuses forInteger.MAX_VALUEelements. On 7.5.0 to 10.6.0 thePriorityQueueconstructor ran first and rejected the size ("Throw exception to prevent confusing OOME"), hence theIllegalArgumentException; the order changed with #14873 (LFV-993). - The cap the suggester skips.
IndexSearcher.search(Query query, int n, Sort sort)(IndexSearcher.java:677) reachesfinal int limit = Math.max(1, reader.maxDoc());andfinal 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, whilesearcher.search(q, new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1))throws the sameOutOfMemoryError(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; otherwiseList<LookupResult> results = new ArrayList<>(num);(WFSTCompletionLookup.java:183).numalso goes toUtil.shortestPaths(:198), which pre-sizes nothing by it: itsTopNSearcherkeeps candidate paths in aTreeSetand results in an unsized list (Util.java:205,:296), which grow with the paths found, up tonum(: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(withonlyMorePopular = false) loops tomaxCnt = Math.min(num, list.size())(TSTLookup.java:193);FSTCompletionstreamslookup(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 entry | num = 1000 | num = 10,000,000 | num = Integer.MAX_VALUE |
|---|---|---|---|
AnalyzingInfixSuggester | 1 result, 0.0 MiB | 1 result, 114.5 MiB | OutOfMemoryError |
WFSTCompletionLookup | 1 result, 0.0 MiB | 1 result, 38.1 MiB | OutOfMemoryError |
FreeTextSuggester | 1 result, 0.0 MiB | 1 result, 38.1 MiB | OutOfMemoryError |
TSTLookup, FSTCompletionLookup | 1 result, 0.0 MiB | 1 result, 0.0 MiB | 1 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
lookupon the three suggesters, and onBlendedInfixSuggester, which inherits theAnalyzingInfixSuggestercollector, multiplied bynumFactor(default 10). - Default vs opt-in. Default constructors and settings; nothing to opt into.
- What goes wrong. On a built suggester,
Integer.MAX_VALUEfails on every call toAnalyzingInfixSuggesterandFreeTextSuggester, even when nothing matches, and on every call toWFSTCompletionLookupwhose prefix matches an entry. The error is anOutOfMemoryError, which callers often do not catch, or, forAnalyzingInfixSuggesteron 7.5.0 to 10.6.0, anIllegalArgumentExceptionthat blames an argument the javadoc allows. Anumin the millions returns the right answer but allocatesnumslots on every call; concurrent calls multiply that. - Scale. None needed: one entry is enough.
- Workaround. Pass a finite count. Capping
numatgetCount()is safe forAnalyzingInfixSuggesterandWFSTCompletionLookup(at most one suggestion per entry) but not forFreeTextSuggester: the single entrylucene lucid luckygivesgetCount()= 1, whilelookup("luc", false, 1000)returns 3 suggestions andlookup("luc", false, getCount())returns 1 (executed). - How likely. Rare:
Integer.MAX_VALUEfails on the first call, so developers meet it in testing and pick a finite count; no public report was found.
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.
/*
* 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();
}
}
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 limitfromAnalyzingInfixSuggester.lookup(10.5.2 and 10.6.0:IllegalArgumentException: maxSize must be >= 0 and < 2147483631).
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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.5.2 and 10.6.0 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (the only platform it ran on) |
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
# 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 IllegalArgumentExceptionnumis documented only as the "maximum number of results to return", andTSTLookupandFSTCompletionLookupreturn every match for the same call.- Lucene fixed this pattern once already: for LUCENE-2119, Michael McCandless capped
IndexSearcher's count atmaxDoc, calling users' assumption that the queue is not pre-allocated "a reasonable assumption". AnalyzingInfixSuggesterbuilds the same collector itself and skips that cap.
The full argument251 words
- Contract.
Lookup.java:190:numis a maximum; nothing calls it a buffer size or says it must stay near the number of entries. - Siblings.
TSTLookup(withonlyMorePopular = false),FSTCompletionLookup,AnalyzingSuggesterandFuzzySuggesterreturn the matches for the same call (executed). - Tests.
TestFSTCompletion.testFullMatchListpassesInteger.MAX_VALUEtoFSTCompletion.lookupand expects every match (TestFSTCompletion.java:157,:165). That is the API underFSTCompletionLookup, notLookup.lookupitself. - 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
ae72952fa41added the cap still inIndexSearcher. 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
PriorityQueuesize 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?"AnalyzingInfixSuggesteris such a caller: it builds the manager itself. - SOLR-2378 (2011): the original
FSTLookupcapped its pre-size withMath.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.
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
createResultsstill gets the caller'snum, whichBlendedInfixSuggesterrelies 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:
BlendedInfixSuggesteratInteger.MAX_VALUEstill throwsIllegalArgumentException: 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,
FreeTextSuggesterreachesnum + seen.size()(:608) withnum = Integer.MAX_VALUE, which wraps inintonceseenis 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 seedDEADBEEF, so assertions off), and so did an assertions-on run of the keylucene se(three bigrams, 3 results at 1000 andInteger.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=truegave the same results): without the fix the minimal test fails withOutOfMemoryErroratAnalyzingInfixSuggester.java:741; withfix.diffapplied,:lucene:suggest:checkGoogleJavaFormatand:lucene:suggest:testpass: 32 suites, 237 tests, 7 skipped, 0 failures, includingTestLookupMaxNum,TestAnalyzingInfixSuggester(23),TestBlendedInfixSuggester(9),TestFreeTextSuggester(11, 1 skipped) andTestWFSTCompletion(7). - With the three fixed files shadowed, the standalone runner passes the test with
-eaand with-da, and allocation probes on 8 entries and on 1 entry return all matches atnum= 10,000,000 andInteger.MAX_VALUEwith 0.0 MiB allocated (BlendedInfixSuggesteratInteger.MAX_VALUEstill throws: LFV-998). - The finder's version, which also has the
FreeTextSuggestersaturation 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.
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.
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.
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 eightLookupclasses of the module in nine configurations,-Xmx1g: atInteger.MAX_VALUEthe three threwOutOfMemoryErrorandBlendedInfixSuggesteranIllegalArgumentException(its ownintoverflow, LFV-998); atnum= 10^8 they allocated 381 MiB (WFST, FreeText) or ran out of heap after 762 MiB (AnalyzingInfix), wherenum= 1000 returns 8;TSTLookup,FSTCompletionLookup,AnalyzingSuggesterandFuzzySuggesterpassed. - Sweep. The finder widened it to 11 configurations, each with 28
numvalues chosen with an SMT model of the arithmetic onnumplus 7 prefix checks: 385 checks, against the small-numresult and a 64 MiB allocation bound. Failures came only from these three sites, fromBlendedInfixSuggester(LFV-998) and from oneAnalyzingSuggestervalue (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.
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.
| Where | When | How | Found |
|---|---|---|---|
| Search tool (GitHub issues/PRs/commits incl. the JIRA archive, dev@, java-user@, issues@, commits@, downstream, web) | 2026-10-29 | three runs, 153 queries: class and method names, symptoms and error texts, the Solr suggest.count angle, the general pre-size pattern | no hit about these sites |
| GitHub (apache/lucene, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch, all of GitHub), JIRA, mailing lists | 2026-10-29 | 40 manual GitHub queries, 14 JQL queries, 14 list queries, web queries | LUCENE-2119, SOLR-5661, LUCENE-8542, LUCENE-5402, LUCENE-504: related, none the same bug |
| Open PRs | 2026-10-28/29 | changed files of all 414 open apache/lucene PRs, then of every PR updated since | no PR changes the sizing |
| GitHub, re-check | 2026-10-30 | issue/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 issue | 0 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 |
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).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle; standalone -ea and -da): OutOfMemoryError |
| 10.6.0, 10.5.2 (release jars) | affected | the minimal test fails: IllegalArgumentException from AnalyzingInfixSuggester |
| 3.6.0 to 10.5.2 and the 10.6 RC | 21 versions | a 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 affected | none of the three classes exists |
fix A (scratch worktree at 645d4fe14a20) | not affected | the minimal test and the suggest module's tests pass |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder (differential sweep) | candidate | 385 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, Low | Own 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 | real | Argued 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 | keep | Fresh 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 | verified | The one test fails on main 645d4fe14a20 (-ea, -da, two seeds) and passes with the fix. |
| this doc | reproduced, fix validated | The 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 476 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,542 words
- 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-eaand-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 maina9779ef3c193, 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 main2f424fd428da(2026-10-30).
- 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 ea57360912eda 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, :219a Lucene main checkout (645d4fe14a20) lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingInfixSuggester.java:95, :739, :741, :743-744, :745a 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, :519a 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-208a 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:190a 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-733a 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
- TestLookupMaxNum.java (lucene/suggest, package org.apache.lucene.search.suggest, 1 test):
repro/LFV-1004/src/org/apache/lucene/search/suggest/TestLookupMaxNum.java(47 lines)
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.
assets/LFV-1004/bug.png(476 KB)
