SpanMultiTermQueryWrapper lets a prefix, wildcard or fuzzy query sit inside span queries by rewriting it into a SpanOrQuery of the matching terms. The new rewrite keeps only the maxSize most frequent of them, but it sizes its queue by maxSize instead of by the terms it finds.
SpanMultiTermQueryWrapperwraps a multi-term query such asPrefixQuery(f:aa). At search time a rewrite method expands it into aSpanOrQuerywith oneSpanTermQueryper kept term.FrequentTermsSpanBooleanQueryRewrite(maxSize), new in 10.6.0, keeps themaxSizeterms with the highest document frequency. It first collects every matching term, then keeps the bestmaxSizein a priority queue.- Lucene's internal
PriorityQueueallocates its whole array,maxSize + 1slots (2 for a size of 0), when it is created, and rejects sizes of 2,147,483,631 and above (Lucene'sArrayUtil.MAX_ARRAY_LENGTH,Integer.MAX_VALUEminus an array-header allowance).
- Index 3 documents with the terms
aa1,aa2andaa3. WrapPrefixQuery(f:aa)inSpanMultiTermQueryWrapperand pick the newFrequentTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE), meaning "keep every matching term". The constructor accepts it. - You expect a count of 3: the rewrite "keeps at most
maxSizeterms", and its siblingTopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE)returns 3 on the same index. - Instead every search throws
IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647, even when no term matches. The rewrite creates its priority queue with room formaxSizeterms before it has collected a single term, and Lucene's queue refuses an array that large. - A large cap that is legal pays on every query instead:
maxSize67,108,864 (2^26) allocates about 268 MB to keep 3 terms and runs out of memory with a 256 MB heap. The small caps tried (8 and 1,024) are fine.
Only applications that opt in to the new rewrite (10.6.0, released 2026-10-29, and main) with a huge maxSize: Integer.MAX_VALUE fails every query at once; a cap in the millions costs about 4 MB of heap per million on every rewrite. The small caps tried (8 and 1,024) work, and results are never wrong.
Mechanism, terms and numbers503 words
SpanMultiTermQueryWrapper.FrequentTermsSpanBooleanQueryRewrite, added for 10.6.0 by #16484, turns a span-wrapped multi-term query into a SpanOrQuery of the maxSize matching terms with the highest document frequency. It is built on ScoringRewrite, which collects every matching term first, but its builder is a PriorityQueue of capacity maxSize, and ScoringRewrite.rewrite creates the builder before it collects any term (ScoringRewrite.java:107 vs :109 on main 645d4fe14a20; the queue at SpanMultiTermQueryWrapper.java:313). Lucene's internal PriorityQueue allocates maxSize + 1 slots up front and rejects maxSize >= 2,147,483,631 (PriorityQueue.java:121-134). So Integer.MAX_VALUE, which the constructor accepts and the sibling TopTermsSpanBooleanQueryRewrite handles, makes every search throw IllegalArgumentException, with 3 matching terms or none, and a large legal cap such as 2^26 allocates about 268 MB on every rewrite. 10.6.0 (released 2026-10-29) and main are affected; no earlier release has the class. Sizing the queue by the terms actually collected fixes both and, in every case tested, leaves the kept terms unchanged.
- The constructors store
maxSizeunchecked (SpanMultiTermQueryWrapper.java:301-308) and delegate to an anonymousScoringRewrite<PriorityQueue<ScoreTerm>>whose builder is
@Override
protected PriorityQueue<ScoreTerm> getTopLevelBuilder() {
return PriorityQueue.usingComparator(maxSize, order);
}(:311-314). addClause calls insertWithOverflow for each term (:331-339), build pops the queue into a SpanOrQuery (:316-324), and checkMaxClauseCount is a no-op because "the priority queue already bounds the number of retained terms" (:326-329).
ScoringRewrite.rewritecallsgetTopLevelBuilder()(ScoringRewrite.java:107) beforecollectTerms(:109). The number of matching terms is known only afterwards,col.terms.size()(:111).PriorityQueue.usingComparatorbuildsnew PriorityQueue<>(maxSize, lessThan)(PriorityQueue.java:61-64). The constructor throwsIllegalArgumentExceptionformaxSize < 0ormaxSize >= ArrayUtil.MAX_ARRAY_LENGTH(:121-125;Integer.MAX_VALUE - NUM_BYTES_ARRAY_HEADER, 2,147,483,631 on the test JVM), else allocatesnew Object[maxSize + 1](:129,:134). The class is@lucene.internaland documents the pre-allocation only there (:34-39).
So the queue is sized by the cap, not by the terms found: at maxSize >= 2,147,483,631 every rewrite throws, and below that every rewrite with a positive maxSize allocates maxSize + 1 references (2 for maxSize 0) (4 bytes each with compressed references), for any number of matching terms.
TopTermsRewrite.rewrite takes Math.min(size, getMaxSize()) and collects into a growing java.util.PriorityQueue (TopTermsRewrite.java:64-65). TopTermsSpanBooleanQueryRewrite uses it with getMaxSize() = Integer.MAX_VALUE (SpanMultiTermQueryWrapper.java:205-216), so it accepts Integer.MAX_VALUE and allocates the same small amount at every size tried.
| rewrite | count | bytes allocated by one whole rewrite (thread allocation counter) |
|---|---|---|
default (SCORING_SPAN_QUERY_REWRITE) | 3 | 50,000 |
TopTermsSpanBooleanQueryRewrite, every size tested (8 to Integer.MAX_VALUE) | 3 | 16,384 |
FrequentTermsSpanBooleanQueryRewrite(8) | 3 | 50,080 |
FrequentTermsSpanBooleanQueryRewrite(1,048,576) | 3 | 4,244,352 |
FrequentTermsSpanBooleanQueryRewrite(16,777,216) | 3 | 67,158,912 |
FrequentTermsSpanBooleanQueryRewrite(67,108,864) | 3 | 268,485,504 (OutOfMemoryError with -Xmx256m) |
FrequentTermsSpanBooleanQueryRewrite(2,147,483,631) or (Integer.MAX_VALUE) | IllegalArgumentException | - |
FrequentTermsSpanBooleanQueryRewrite with the lazy fix, every size tested (8 to Integer.MAX_VALUE) | 3 | 50,200 |
The same exception with IndexSearcher.setMaxClauseCount(Integer.MAX_VALUE) and for a prefix that matches no term. Byte counts depend on the JVM; the edge 2,147,483,631 depends on its array header size.
Exposure and likelihood218 words
- Opt-in. The wrapper's default rewrite is
SCORING_SPAN_QUERY_REWRITE, orTopTermsSpanBooleanQueryRewritefor a top-terms query such asFuzzyQuery(SpanMultiTermQueryWrapper.java:65-77); an application must callsetRewriteMethod(new FrequentTermsSpanBooleanQueryRewrite(n)). No Lucene code outside the class and its test builds it. Within that usage the default writer, codec and searcher suffice, and 3 documents are enough. - Loud, not silent. A huge cap fails on the first query with a clean
IllegalArgumentException(the constructor accepted it). A cap in the millions costsmaxSize + 1slots of heap on every rewrite, up toOutOfMemoryErroron a small heap. No wrong results, no index effect. - The clause limit. Under the default
IndexSearcherclause limit (1024), keeping more than 1,025 terms already fails withTooManyNestedClauses(related bug LFV-733): the clause check testsnumClauses > maxClauseCountbefore it counts the clause, so it lets 1,025 clauses through and throws on the 1,026th (IndexSearcher.java:907-921; the off-by-one is tracked upstream as #10339). So caps far above 1024 matter once the limit is raised. TheInteger.MAX_VALUEfailure does not depend on the clause limit. - Releases. The class first shipped in 10.6.0 (released 2026-10-29), which is affected; it can be fixed in 10.6.1.
- Likelihood: rare. The class exists to bound expansion, so most callers pass small caps; a GitHub code search on 2026-10-28 found no downstream user.
One JUnit test method in lucene/queries, public API: 3 documents in one segment, SpanMultiTermQueryWrapper(PrefixQuery f:aa) with FrequentTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE), counted on a plain IndexSearcher. It asserts a count of 3. Deterministic; the same result with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.queries.spans;
import java.util.List;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper.FrequentTermsSpanBooleanQueryRewrite;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestFrequentTermsSpanRewriteMaxSize extends LuceneTestCase {
public void testMaxSizeIntegerMaxValue() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
for (String value : new String[] {"aa1", "aa2", "aa3"}) {
w.addDocument(List.of(new TextField("f", value, Field.Store.NO)));
}
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
SpanMultiTermQueryWrapper<PrefixQuery> query =
new SpanMultiTermQueryWrapper<>(new PrefixQuery(new Term("f", "aa")));
query.setRewriteMethod(new FrequentTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE));
try {
assertEquals(3, new IndexSearcher(reader).count(query));
} catch (IllegalArgumentException e) {
// Expected: the 3 matching terms are kept and count is 3; actual: the rewrite throws.
throw new AssertionError("expected count 3, but the rewrite threw " + e, e);
}
}
}
}
}
cp $REPRO/TestFrequentTermsSpanRewriteMaxSize.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/spans/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestFrequentTermsSpanRewriteMaxSize -Ptests.seed=DEADBEEF -Ptests.asserts=true
# also with -Ptests.asserts=false
- Expected
3: the 3 matching terms are kept, as withTopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE).- Observed
IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647from the rewrite, with assertions on and off.
More files, environment, full output260 words
The test uses ByteBuffersDirectory and the default IndexWriterConfig, so the seed randomizes only test-framework settings (locale, time zone, default codec, similarity and the like), and the failure does not depend on them: it is the same with two seeds and with assertions on and off. The recorded gradle runs used this command in a scratch worktree with the test copied in, plus --no-daemon --max-workers=4 -Ptests.jvms=4. The catch only turns the exception into an expected-vs-actual message.
It asserts the headline (the exception) only. The per-rewrite allocation is a JVM-dependent consequence of the same line; the finder's allocation test measured it (about 268 MB at maxSize 2^26, OutOfMemoryError with -Xmx256m) and passes with the fix, but it is evidence, not a proposed regression test.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the finder's test also fails on branch_10_6 4ae946e5a9b (10.6.0 release candidate) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
cp $REPRO/TestFrequentTermsSpanRewriteMaxSize.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/spans/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestFrequentTermsSpanRewriteMaxSize -Ptests.seed=DEADBEEF -Ptests.asserts=true
# also with -Ptests.asserts=false
TestFrequentTermsSpanRewriteMaxSize > testMaxSizeIntegerMaxValue FAILED
java.lang.AssertionError: expected count 3, but the rewrite threw java.lang.IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647
at org.apache.lucene.queries.spans.TestFrequentTermsSpanRewriteMaxSize.testMaxSizeIntegerMaxValue(TestFrequentTermsSpanRewriteMaxSize.java:50)
Caused by:
java.lang.IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647
at org.apache.lucene.util.PriorityQueue.<init>(PriorityQueue.java:123)
at org.apache.lucene.util.PriorityQueue.<init>(PriorityQueue.java:80)
at org.apache.lucene.util.PriorityQueue.usingComparator(PriorityQueue.java:63)
at org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper$FrequentTermsSpanBooleanQueryRewrite$1.getTopLevelBuilder(SpanMultiTermQueryWrapper.java:313)
at org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper$FrequentTermsSpanBooleanQueryRewrite$1.getTopLevelBuilder(SpanMultiTermQueryWrapper.java:310)
at org.apache.lucene.search.ScoringRewrite.rewrite(ScoringRewrite.java:107)
at org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper$FrequentTermsSpanBooleanQueryRewrite.rewrite(SpanMultiTermQueryWrapper.java:350)
at org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper.rewrite(SpanMultiTermQueryWrapper.java:121)
at org.apache.lucene.search.ConstantScoreQuery.rewrite(ConstantScoreQuery.java:44)
at org.apache.lucene.search.IndexSearcher.rewrite(IndexSearcher.java:873)
at org.apache.lucene.search.IndexSearcher.count(IndexSearcher.java:497)
at org.apache.lucene.queries.spans.TestFrequentTermsSpanRewriteMaxSize.testMaxSizeIntegerMaxValue(TestFrequentTermsSpanRewriteMaxSize.java:47)
:lucene:queries:test (FAILURE): 1 test, 1 failure
# the same with -Ptests.asserts=false; with the fix: :lucene:queries:test (SUCCESS): 1 test- The constructors promise only "keeps at most
maxSizeterms" (SpanMultiTermQueryWrapper.java:300); no bound is documented, and anyintis accepted. - The class's own comment says it collects every term first; its sibling
TopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE)works on the same index. - LUCENE-504 fixed this pattern in 2009, moving FuzzyQuery to a growing queue to support
Integer.MAX_VALUE"without exhausting all memory".
The full argument169 words
- Contract. "Create a rewrite that keeps at most
maxSizeterms" (SpanMultiTermQueryWrapper.java:300,:305). The class javadoc defines the class by its ranking difference toTopTermsSpanBooleanQueryRewrite(:261-263). The bound and the pre-allocation are documented only on the@lucene.internalPriorityQueue(PriorityQueue.java:34-39,:121-125). - The class's design. "ScoringRewrite first collects every matching term ... and this rewrite then retains the maxSize most frequent" (
:268-274): the size of the set is known before the queue is needed. - The sibling.
TopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE)keeps the 3 terms with the default and with a raised clause limit. - Precedent. LUCENE-504 (#1581), fixed by Uwe Schindler in
15743fc1793(2009-11-06), changed FuzzyQuery to usejava.util.PriorityQueue, "which grows dynamically to support BooleanQuery.maxClauseCount(Integer.MAX_VALUE) without exhausting all memory" (the commit subject itself spells the classjava.utilPriorityQueue).TopTermsRewritekeeps that design today.IndexSearcher.searchAfterlikewise capsnumHitsatMath.max(1, reader.maxDoc())before sizing its collector (IndexSearcher.java:587-599). - Why tests missed it. The class's only upstream test uses size 8; #16484's review did not discuss sizes.
The devil's-advocate check186 words
Strongest case: pre-allocating a queue sized by the caller's N is a deliberate Lucene pattern (TopScoreDocCollectorManager and TopFieldCollectorManager document "pre-allocate a full array of length numHits"), and the PriorityQueue exception is a deliberate guard against a confusing OutOfMemoryError (4c646dab3fb, 2018). The class exists to bound expansion, so "no limit" goes against its purpose; SCORING_SPAN_QUERY_REWRITE already keeps every term; and under the default clause limit (1024) more than 1,025 terms fail anyway (the clause check throws on the 1,026th clause).
Why it fails: those collectors document the pre-allocation on their public API; this class does not, and fails late, at search time, from an internal class, after its public constructor accepted the value. The sibling it is defined against accepts Integer.MAX_VALUE; with a raised clause limit, keeping every matching term is a legal result; and LUCENE-504 already settled the question for multi-term rewrites. Even if huge caps were unsupported, the constructor should reject them. The intended-behaviour check: real, high confidence for the exception, medium for the allocation on its own, so the allocation is filed as a consequence, not as a second bug.
Recommended A. Allocate the queue only when its size is known
Allocate the queue lazily (SpanMultiTermQueryWrapper.java, +57/-8): collect terms into a small list until more than maxSize arrive, then switch to a queue of maxSize; otherwise size the queue by the terms collected. Kept terms and their order are unchanged in every case tested. The test and all 513 lucene/queries tests pass.
void add(ScoreTerm st) { // once per matching term
if (queue == null) {
if (pending.size() < maxSize) {
pending.add(st);
return;
}
queue = newQueue(maxSize); // more than maxSize terms: allocate now
}
queue.insertWithOverflow(st);
}
// build(): finish() makes newQueue(pending.size()) if the queue was never neededAll options and trade-offs4 options · 685 words
Three fixes were built and run in a scratch worktree of 645d4fe14a20; capping or rejecting the size alone is not recommended.
A private builder TopFrequentTerms replaces the queue as the ScoringRewrite builder. It keeps terms in a pending list while fewer than maxSize have arrived; on the first term beyond maxSize it allocates a queue of maxSize slots, moves the pending terms in and continues with insertWithOverflow; if that never happens, build() allocates a queue of exactly the collected count. Terms are offered in the same order as before. The constructor also rejects a negative maxSize (optional hardening; today a negative size fails at search time with the same exception).
void add(ScoreTerm st) { // once per matching term
if (queue == null) {
if (pending.size() < maxSize) {
pending.add(st);
return;
}
queue = newQueue(maxSize); // more than maxSize terms: allocate now
}
queue.insertWithOverflow(st);
}
// build(): finish() makes newQueue(pending.size()) if the queue was never neededTrade-offs
About 50 lines and a new private class. When more than maxSize terms match, the peak at the switch is the pending list (maxSize terms, plus the ArrayList's spare capacity) plus the new queue (maxSize + 1 slots); the list is dropped right after the move. The branch_10_6 line reads new PriorityQueue<>(maxSize) { lessThan }, so a backport needs the 10.x queue API.
What the checks validated
- Stock + test: fails with assertions on and off; + fix: passes with assertions on and off (gradle).
- + fix: the whole
lucene/queriesmodule, 513 tests with the new test, 0 failures, assertions on and off;:lucene:queries:check -x test(format, ecjLint, forbiddenApis) passes. - + fix: the finder's 3 tests pass, including the 2^26 allocation test, also with
-Xmx256m(stock:OutOfMemoryError). - The finder's exhaustive harness (0-4 matching terms): 60,960 cases, 0 mismatches with the fix and results identical to stock for every size stock accepts; a Z3 model: the fix cannot throw for
maxSize >= 0and its queue array never has more thanmax(n + 1, 2)slots (the model does not cover the pending list). - Not run: the full Lucene test suite, nightly tests,
branch_10_6.
The builder becomes a List<ScoreTerm>; build() creates PriorityQueue.usingComparator(Math.min(maxSize, builder.size()), order), offers every collected term with insertWithOverflow in the same order, then pops as before (+14/-12).
Trade-offs
Smallest and easiest to review, but it keeps a ScoreTerm and its Term for every matching term until build(), where today the queue drops the overflow at once. ScoringRewrite already holds every matching term's bytes and TermStates for the whole rewrite, so memory stays linear in the matching terms, with a larger constant when far more terms match than maxSize.
What the checks validated
The test passes; the whole lucene/queries module passes (513 tests); check -x test passes after a formatting fix; the finder's 3 tests and the -Xmx256m test pass.
Option A with builder capacity Math.min(maxSize, IndexSearcher.getMaxClauseCount()), as the core top-terms rewrites cap their size (+59/-8). Related bug LFV-733 changes the same line for the clause limit.
Trade-offs
It also changes LFV-733's case: an explicit maxSize above the clause limit then keeps at most the limit instead of failing with TooManyNestedClauses; argue that in LFV-733's issue.
What the checks validated
The whole lucene/queries module passes (513 tests); check -x test passes; the finder's 3 tests pass, including the raised-clause-limit case.
Capping the eager queue at IndexSearcher.getMaxClauseCount() hides the bug only at the default limit: with setMaxClauseCount(Integer.MAX_VALUE) the exception is back, and the allocation becomes min(maxSize, limit) + 1 slots (executed by the finder and the independent reproduction). Rejecting large sizes in the constructor and documenting the pre-allocation fails fast, but keeps the per-rewrite allocation for every cap below the edge and diverges from the sibling, which accepts Integer.MAX_VALUE.
A GitHub code search on 2026-10-28 found no downstream user of the new rewrite; an application that takes maxSize from a request should cap it, as with any expansion limit.
A planning probe compared the sizes the two span top-N rewrites accept and saw only the new one throw at Integer.MAX_VALUE. The finder reduced it to a public-API test, found the cause by reading ScoringRewrite, and checked the rewrite exhaustively on small indexes against a reference written from its javadoc.
The method in full172 words
Method: differential testing, then bounded-exhaustive translation validation and a Z3 model (semi-formal).
- A planning probe on main
a9779ef3c193(2026-10-28) ranFrequentTermsSpanBooleanQueryRewriteandTopTermsSpanBooleanQueryRewriteat the same sizes: only the new rewrite threw, at 2,147,483,631 andInteger.MAX_VALUE, and it took 154 ms at 2^26 against 1 ms. - The finder reduced it to a public-API JUnit (6 documents, 3 terms) and found the cause by reading
ScoringRewrite.rewritewhile modelling it. - A harness compared the rewrite with a reference written from its javadoc on every small case: 60,960 cases, 0-4 matching terms, 1 or 2 segments, sizes -1, 0..n+1, 4096, 2,147,483,631 and
Integer.MAX_VALUE, both built-in orders. The only mismatches were the 12,440 exception cases at the two edge sizes; a deliberately wrong reference is caught. - A Z3 bit-vector model of the queue sizing confirmed that the stock code can throw or allocate over 10^6 slots for at most 3 terms, and that the lazy fix cannot (6 of 6 queries as expected).
- This doc's one-method test is a reduction of the finder's test.
No prior report found: three automated search runs, 23 manual GitHub searches and 21 mailing-list searches on 2026-10-28 (GitHub issues and PRs, commits, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive, downstream projects, the web), refreshed on 2026-10-29. Not fixed upstream.
Searches5 searches
Related, not reports of this bug: #16484 introduced the class (sizes never discussed; its only test uses size 8); #7571 (LUCENE-6513) is the feature request; #1581 (LUCENE-504, fixed 2009) is the same pre-sized-queue pattern in FuzzyQuery, a precedent at another fix site; 4c646dab3fb (2018) made PriorityQueue reject Integer.MAX_VALUE on purpose; #16532 is the 10.x compile fix of the same builder and kept the eager allocation; open #16851, #16277 and #13936 change PriorityQueue but keep the pre-sized heap. In-hunt: LFV-733 changes the same line for the clause limit (different root cause).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene, Elasticsearch, OpenSearch, Solr) | 2026-10-28 | three automated runs (16 issue/PR queries each) and 23 manual searches: the class name, FrequentTerms, SpanMultiTermQueryWrapper limits, PriorityQueue with a huge size, the exception text; #16484, #16532 and #7571 read in full | only #16484 (introduced the class) and unrelated items |
| Open apache/lucene PRs | 2026-10-28 | changed files of the PRs updated since 2026-10-14 or created since 2026-08-18 | none touches the file; three PriorityQueue PRs keep the eager allocation |
| Mailing lists and JIRA archive | 2026-10-28 | 20 list queries per run and 21 direct searches (dev@, java-user@, issues@, commits@), all 31 dev@ mails of October 2026 about 10.6.0; 4 JIRA-archive queries per run | no report; LUCENE-504 is the 2009 precedent |
| Downstream code and the web | 2026-10-28 | a GitHub code search; 15-20 web queries per run | no downstream use of the class; nothing relevant |
| Refresh | 2026-10-29 | upstream branch heads; the file's commit list on main and branch_10_6; five keyword searches; items created since 2026-10-28; release tags and Maven Central | main still 645d4fe14a20, the file unchanged, no new item, no 10.6.0 release |
Introduced with the class by #16484 (156fb97fd33, committed 2026-08-18) and carried to 10.x, where #16532 rewrote the line but kept the eager sizing. Main and 10.6.0 (released 2026-10-29 from the tested release candidate) fail; no earlier release has the class (10.5.2 and 9.12.3 do not compile the test).
Versions tested6 rows
#16484 "LUCENE-6513: Add FrequentTermsSpanBooleanQueryRewrite" (David Smiley) is 156fb97fd33 on main; git log -S finds no earlier commit with the class or the line usingComparator(maxSize, order). On 10.x the cherry-pick 3572e2c03f0 was followed by 2bcbe3d82ed (#16532, Dawid Weiss, "Fix API only available on main"), which wrote new PriorityQueue<>(maxSize) { lessThan } instead. The older pieces are unchanged: ScoringRewrite has created its builder before collecting terms since at least 2015, PriorityQueue has rejected oversized queues since 2014 (3ae719d025d, SOLR-5661), and its current check and message, which also cover Integer.MAX_VALUE and negative sizes, date from 2018 (4c646dab3fb); and TopTermsRewrite has used a growing queue since 2010. The introducing commits themselves were not built; the relevant files are identical to the tested heads (except diamond operators on 10.x).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | this doc's test fails, assertions on and off (gradle and standalone) |
main a9779ef3c193 | affected | the finder's test fails 3 of 3, the independent reproduction's 4 of 6, assertions on and off |
branch_10_6 4ae946e5a9b (10.6.0 RC1, released 2026-10-29) | affected | the finder's test fails 3 of 3; the independent reproduction's 4 of 6; OutOfMemoryError under -Xmx256m |
branch_10x (10.7.0 development) | read, not run | the same code (3572e2c03f0, 2bcbe3d82ed) |
| 10.5.2, 9.12.3 and all earlier releases | not affected | the class does not exist: the test does not compile; the sibling control passes |
main 645d4fe14a20 with fix A | not affected | the test and the 513 lucene/queries tests pass |
The finder, an independent reproducer, a devil's-advocate verifier and an adjudication all rate it real; a cross-model refute review could not run code and agreed with the root cause. This doc's test was run on main through gradle, stock and with three fixes.
Checks and verdicts272 words
Every check except the cross-model reviews re-ran a repro on the stock jars; the reviewer from another model family (OpenAI GPT-5.6) only read code.
| Check | Verdict | What it found |
|---|---|---|
| finder (a hunt unit) | candidate | JUnit fails 3 of 3 on main and branch_10_6 (-ea, -da); OutOfMemoryError under -Xmx256m; harness mismatches only at the two edge sizes; the lazy fix passes everything incl. 73 upstream span tests. |
| cross-model review | partial overlap with LFV-733, not a duplicate | Same line, different invariant; its wording points were addressed. |
| Independent reproducer | real | Own JUnit: 4 of 6 fail on main and branch_10_6, controls pass; FrequentTerms(2^24) about 67 MB vs about 7 to 11 KB for TopTerms(2^24); LFV-733's clamp alone leaves the raised-limit test failing. |
| Cross-model refute review | not real, low, procedural only | Could not run any command, called that "not a substantive refutation", agreed with the root cause; a separate re-run failed 3 of 3. |
| Devil's advocate | real | High confidence for the exception, medium for the allocation alone; the LUCENE-504 precedent; re-runs fail. |
| adjudication | confirmed, high | Re-ran the independent reproduction's test (4 of 6) and the finder's (3 of 3) on main with new seeds; duplicate of none. |
| Registration check and credibility audit | keep | Re-ran the finder's test on main with -da and two seeds: 3 of 3 each. |
| this doc | as above | The one-method test through gradle on 645d4fe14a20 (-ea, -da); fixes A, B and C: the test, the whole lucene/queries module, check -x test, the finder's tests and the -Xmx256m test. |
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 468 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,439 words
- Registry title
- SpanMultiTermQueryWrapper.FrequentTermsSpanBooleanQueryRewrite (new in 10.6.0) sizes its priority queue from maxSize before collecting any term, so maxSize >= 2147483631 (for example Integer.MAX_VALUE as 'no limit') makes every rewrite throw IllegalArgumentException, and a large maxSize allocates a (maxSize + 1)-slot array on every rewrite
- Area
- queries / spans (SpanMultiTermQueryWrapper.FrequentTermsSpanBooleanQueryRewrite queue sizing; core ScoringRewrite creates the top-level builder before collecting terms; util PriorityQueue pre-allocates maxSize + 1 slots)
- Priority
- Low, rank 1127: A loud crash at the Integer.MAX_VALUE boundary plus a large per-rewrite allocation in the new 10.6.0 FrequentTermsSpanBooleanQueryRewrite, a niche sibling of LFV-733's span top-N size problem, so it follows it.
- Impact class
- crash
- Impact note
- Applications that install the new (10.6.0) FrequentTermsSpanBooleanQueryRewrite on a SpanMultiTermQueryWrapper with a very large maxSize are hit: Integer.MAX_VALUE (a natural 'no limit' value that the sibling TopTermsSpanBooleanQueryRewrite accepts) or any value >= 2147483631 makes every search or rewrite of that query throw IllegalArgumentException from the internal PriorityQueue, even on a field with 3 matching terms and with IndexSearcher.setMaxClauseCount raised, and a large legal cap such as 2^24 or 2^26 makes each rewrite allocate a (maxSize + 1)-slot array (about 67 MB or 268 MB on the test JVM, OutOfMemoryError under -Xmx256m) whatever the number of matching terms; default writer, codec and searcher, 6 documents suffice, small caps (8-1024) are unaffected, and only 10.6.0 (released 2026-10-29) and main have it.
- Summary
- You wrap a PrefixQuery in SpanMultiTermQueryWrapper, call setRewriteMethod(new FrequentTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE)), whose javadoc promises only that it 'keeps at most maxSize terms' (ranked by document frequency instead of boost, unlike TopTermsSpanBooleanQueryRewrite), and expect it to keep all 3 matching terms, as TopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE) does on the same index. Instead the constructor accepts the value and every search then throws IllegalArgumentException 'maxSize must be >= 0 and < 2147483631; got: 2147483647', because getTopLevelBuilder() returns PriorityQueue.usingComparator(maxSize, order) and ScoringRewrite.rewrite calls it before it collects any term, so the internal queue pre-allocates maxSize + 1 slots (or rejects a size it cannot allocate) instead of being sized by the terms actually found; for the same reason a large legal cap such as 2^26 allocates about 268 MB per rewrite for 3 terms. Sizing the queue lazily, by min(maxSize, collected terms), as the sibling TopTermsRewrite does with a growing queue, fixes both and leaves every result unchanged.
- Introduced
- 156fb97fd33d8f2039309381ccaca4a6a7e59e14 on main, 'LUCENE-6513: Add FrequentTermsSpanBooleanQueryRewrite' (apache/lucene#16484, author dsmiley; the feature request is #7571): it added the class with getTopLevelBuilder() { return PriorityQueue.usingComparator(maxSize, order); }, so the defect was born with the API [read: git log -S FrequentTermsSpanBooleanQueryRewrite --all and git log -S 'usingComparator(maxSize, order)' return only this commit; git blame of SpanMultiTermQueryWrapper.java:300-315 on a9779ef3c193]. On branch_10x and branch_10_6: the cherry-pick 3572e2c03f0 plus 2bcbe3d82ed (#16532, dweiss, 'Fix API only available on main'), which replaced usingComparator with new PriorityQueue<>(maxSize) { lessThan } and kept the eager sizing. The older ingredients are unchanged: ScoringRewrite has created its builder before collecting terms since at least 2015, PriorityQueue has pre-allocated maxSize + 1 slots and rejected sizes >= ArrayUtil.MAX_ARRAY_LENGTH since at least 2018 (4c646dab3fb), and TopTermsRewrite has used Math.min(size, getMaxSize()) with a growing queue since 2010. The introducing commits were not built (disk rule); git diff 156fb97fd33..a9779ef3c193 is empty for the five relevant files, and 2bcbe3d82ed..4ae946e5a9b differs only by diamond-operator edits (#16592) [read] (2026-08-18 (author date of 156fb97fd33 and of the cherry-pick 3572e2c03f0, 12:02:08-04:00; #16484 merged 16:02:09Z); 2026-08-19 (2bcbe3d82ed, the 10.x API adaptation))
- First affected version
- 10.6.0 (released 2026-10-29; the release revision branch_10_6 4ae946e5a9b fails the repro [exec]). No earlier release is affected: 10.5.2 and 9.12.3 do not have the class [exec: compile error, 6 x 'cannot find symbol FrequentTermsSpanBooleanQueryRewrite']
- Affected versions
- main a9779ef3c193 (11.0.0-SNAPSHOT) [exec: the finder's JUnit fails 3 of 3 with -ea and -da (seed DEADBEEF); the independent reproduction's own JUnit fails 4 of 6 with -ea and -da, its 2 controls pass; the intended-behaviour check re-ran the finder's JUnit with -ea and -da (3 of 3); the adjudication re-ran the independent reproduction's (-ea, seed 5EED378, 4 of 6) and the finder's (-da, seed 5EED379, 3 of 3); the registration check re-ran the finder's with -da (seed 5EED3781, 3 of 3); FrequentTerms(2^26) throws OutOfMemoryError under -Xmx256m on a 3-term field while TopTerms(2^26) passes in the same JVM]. branch_10_6 head 4ae946e5a9b (the 10.6.0 RC1 revision, released as 10.6.0 on 2026-10-29) [exec: finder 3 of 3 with -ea and -da; the independent reproduction 4 of 6 with -ea and -da plus the -Xmx256m OutOfMemoryError; the intended-behaviour check 3 of 3 with -da]. branch_10x (10.7.0 development; contains 3572e2c03f0 and 2bcbe3d82ed) [read, not executed]. Not affected: every release up to 10.5.2, all 9.x included (class absent; 10.5.2 and 9.12.3 executed: compile error, while the sibling control TopTermsSpanBooleanQueryRewrite(Integer.MAX_VALUE) passes 2 of 2 there). Not fixed upstream: SpanMultiTermQueryWrapper.java last changed on main by 156fb97fd33 and on branch_10_6 by f6fce7e42c5 (#16592, build infrastructure) [read: GitHub commits API, 2026-10-28, upstream main head 645d4fe14a20]. 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 low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal; also reached by exhaustive check, code reading, Z3
- Upstream and prior reports
- none found (the prior-report record, verdict new, searched 2026-10-28, read-only): three scripted prior-report searches (each 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 15-20 web queries; 132, 212 and 133 distinct hits) plus the finder's earlier run (51 queries, re-read); 23 manual GitHub issue/PR searches over apache/lucene, Elasticsearch, OpenSearch, Solr and all of GitHub; a GitHub code search (no downstream use of the class); the changed-file lists of the open apache/lucene PRs updated since 2026-10-14 or created since 2026-08-18; PR #16484 (body, review, comments), #16532 and all comments of #7571; 21 direct mailing-list searches and all 31 dev@ mails of October 2026 about the 10.6.0 release (through the RC1 vote). Nearest items, not this bug: #16484 (introduced the class; sizes never discussed; its only test uses size 8), #7571 (LUCENE-6513, the open feature request), #1581 (LUCENE-504, fixed 2009: the same pre-sized PriorityQueue pattern in FuzzyQuery, a precedent at a different fix site), 4c646dab3fb (2018, the deliberate PriorityQueue IllegalArgumentException), #16532 (the 10.x compile fix, which kept the eager allocation), open #16851, #16277 and #13936 (PriorityQueue changes that keep the pre-sized heap). Not fixed upstream: SpanMultiTermQueryWrapper.java unchanged since #16484, #16532 and #16592 on main 645d4fe14a20, branch_10_6 4ae946e5a9b and branch_10x; refresh 2026-10-28 through the GitHub REST API (the search API was rate-limited at that moment): no new commit on the file, and the only issue or PR created after a recent cutoff (#16937) does not mention spans.
- Lucene commit tested
- main
645d4fe14a20(this doc's test through gradle and the standalone runner, assertions on and off; fixes A, B and C in a scratch worktree) anda9779ef3c193(the hunt; the relevant files are identical); the 10.6.0 release-candidate headbranch_10_64ae946e5a9b(the finder's, the independent reproduction's and the intended-behaviour check's tests); the 10.5.2 and 9.12.3 release jars (the class is absent: compile error).
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:257-263 (class javadoc), :300-308 (constructors, maxSize unchecked), :310-314 (getTopLevelBuilder)lucene/core/src/java/org/apache/lucene/search/ScoringRewrite.java:104-111 (getTopLevelBuilder before collectTerms; size known only afterwards)lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:34-39 (@lucene.internal, pre-allocation NOTE), :61-63 (usingComparator), :117-134 (IllegalArgumentException for maxSize >= ArrayUtil.MAX_ARRAY_LENGTH, new Object[maxSize + 1])lucene/core/src/java/org/apache/lucene/search/TopTermsRewrite.java:61-65 (sibling: Math.min(size, getMaxSize()) and a growing java.util.PriorityQueue)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:257-274 (class javadoc and implementation comment), :300-340 (constructors, builder, build, addClause), :205-216 (TopTerms sibling)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/ScoringRewrite.java:104-124a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:34-39, :61-81, :113-134a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopTermsRewrite.java:61-65a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/ArrayUtil.java:33-34
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:65-77 (default rewrite selection)a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:519-521 (10.6.0 section)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:257-274, :300-308a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:34-39, :121-125a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:587-599a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollectorManager.java:45; TopFieldCollectorManager.java:44
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/ScoringRewrite.java:108-111, :126-129 (ScoringRewrite holds every collected term)
- TestFrequentTermsSpanRewriteMaxSize.java (lucene/queries, one test):
repro/LFV-944/src/org/apache/lucene/queries/spans/TestFrequentTermsSpanRewriteMaxSize.java(55 lines)
It is made in parallel with this doc; every on-screen value comes from the code at 645d4fe14a20 or the diagram's executed probe (stock jars and the lazy fix).
assets/LFV-944/bug.png(468 KB)
