One clause limit, two top-terms rewrites: the plain FuzzyQuery rewrite caps its terms at the limit, the span rewrite that SpanMultiTermQueryWrapper picks for it does not.
IndexSearcherhas one clause limit,getMaxClauseCount(), 1,024 by default. Since 9.0,IndexSearcher.rewritecounts every clause of the rewritten query, span terms included, and throwsTooManyNestedClausesabove it.- A
FuzzyQueryexpands to the indexed terms close to its term. Its default rewrite keeps only the bestmin(maxExpansions, clause limit)terms, so it never trips the limit on its own. SpanMultiTermQueryWrapperlets a multi-term query such asFuzzyQuerybe used inside span queries (SpanNearQuery,SpanFirstQuery, ...). For a top-terms rewrite it copies the size into its own span rewrite,TopTermsSpanBooleanQueryRewrite(size).
FuzzyQuery rewritten two ways under one clause limit: the plain rewrite keeps min(size, limit) terms and runs, the span rewrite keeps min(size, Integer.MAX_VALUE) terms and the searcher's clause check throws. Select the still to open it at full size. Generated with AI, checked against the code.- Index 676 documents whose field
fholds 1,326 different terms, all within two edits ofabcde. Buildnew FuzzyQuery(new Term("f", "abcde"), 2, 0, 2000, true): up to 2,000 expansions. - Search with it as is.
FuzzyQuerydocuments that amaxExpansionsabove the clause limit (1,024 by default) is replaced by the limit, so it keeps the best 1,024 terms and counts 676 documents. - Wrap the same query in
SpanMultiTermQueryWrapper, for example to use it inside aSpanNearQuery. You expect the same 1,024 terms: the span rewrite's javadoc says it "will not overflow the boolean max clause count". - Instead it keeps all 1,326 terms, and the search throws
IndexSearcher$TooManyNestedClauses("maxClauseCount is set to 1024"). The span rewrite'sTopTermsRewritedelegate caps its size withgetMaxSize(), which returnsInteger.MAX_VALUE; the core top-terms rewrites return the clause limit there. - Same with the default
FuzzyQuery(50 expansions) once the application lowers the limit below 50: at limit 32 the plain query keeps 32 terms and runs, the wrapped one keeps 50 and throws.
Applications that use a span-wrapped FuzzyQuery (or another top-terms query) with maxExpansions above the clause limit, that lower the limit below the expansion size (below 50 even the default FuzzyQuery fails), or that set an explicit span top-N size above it. That request throws; results are never wrong. Opt-in shapes, rare.
Mechanism, terms and numbers501 words
FuzzyQuery documents that a maxExpansions above IndexSearcher#getMaxClauseCount is replaced by the limit, and its default top-terms rewrite does exactly that (TopTermsRewrite.rewrite keeps min(size, getMaxSize()) terms, and the core rewrites' getMaxSize() is the clause limit). SpanMultiTermQueryWrapper turns a top-terms rewrite into TopTermsSpanBooleanQueryRewrite(size) with the same size, documented as "will not overflow the boolean max clause count", but its TopTermsRewrite delegate returns Integer.MAX_VALUE from getMaxSize(), so it keeps up to maxExpansions terms. Since 9.0 (LUCENE-8811) IndexSearcher.rewrite counts every SpanTermQuery of the resulting SpanOrQuery and throws IndexSearcher$TooManyNestedClauses. So a FuzzyQuery(abcde, 2, 0, 2000, true) that keeps 1,024 of 1,326 matching terms and counts 676 documents on its own throws once span-wrapped; under a lowered limit (32) even the default FuzzyQuery (50 expansions) fails once wrapped. FrequentTermsSpanBooleanQueryRewrite (new in 10.6.0) has the same gap. The cap dates from 2010, when SpanOrQuery had no clause limit; the exception from 9.0.0. No wrong results, no index effect. The fix is two lines.
- Clause limit:
IndexSearcher.getMaxClauseCount(), 1,024 by default (IndexSearcher.java:80,:251-261).IndexSearcher.rewritevisits the rewritten query and throwsTooManyNestedClauseswhen the clause count goes over it (:878,:916-920). - Top-terms rewrite:
TopTermsRewritekeeps the bestmaxSize = Math.min(size, getMaxSize())terms (TopTermsRewrite.java:64). The three core subclasses return the clause limit fromgetMaxSize()(MultiTermQuery.java:167-168,:212-213,:260-261).FuzzyQuery's default rewrite is one of them,TopTermsBlendedFreqScoringRewrite(maxExpansions)(FuzzyQuery.java:60-62; defaultmaxExpansions50,:56). - Span rewrites:
SpanMultiTermQueryWrappermaps anyTopTermsRewritetonew TopTermsSpanBooleanQueryRewrite(getSize())(SpanMultiTermQueryWrapper.java:70-78).FrequentTermsSpanBooleanQueryRewrite(n)(#16484, 10.6.0) can be set explicitly.
TopTermsSpanBooleanQueryRewrite's delegate overridesgetMaxSize()withreturn Integer.MAX_VALUE;(SpanMultiTermQueryWrapper.java:208-211).TopTermsRewrite.rewritethen computesmin(maxExpansions, Integer.MAX_VALUE): the clause limit is never applied, and the rewrite returns aSpanOrQueryof up tomaxExpansionsSpanTermQuerys.FrequentTermsSpanBooleanQueryRewritekeeps the best terms inPriorityQueue.usingComparator(maxSize, order)(:312-314) and turns its own clause check off (:326-329). No clause cap either.IndexSearcher.rewritevisits theSpanOrQuery(SpanOrQuery.java:87-93); eachSpanTermQuerycallsconsumeTerms(SpanTermQuery.java:98-101), which counts one clause and throwsTooManyNestedClausesonce the count is over the limit (IndexSearcher.java:916-920). The check runs before the increment, so limit + 1 clauses pass and limit + 2 throw (open #10339, not part of this bug).- The sibling default rewrite still states the 2010 premise: "we accept all terms as SpanOrQuery has no limits" (
:169-171). Since 9.0 that is no longer true.
Default limit 1,024; 676 documents with 1,326 distinct terms within two edits of abcde; FuzzyQuery(abcde, 2, 0, 2000, true): plain, 1,024 terms and 676 hits; span-wrapped, 1,326 span terms and TooManyNestedClauses. Limit 32, default FuzzyQuery: plain 32 terms; wrapped 50 terms, throws. maxExpansions 1024, 1025, 1026, 2000: plain always 676 hits; wrapped 676, 676, throws, throws. Explicit TopTermsSpanBooleanQueryRewrite(2000) and FrequentTermsSpanBooleanQueryRewrite(2000) on a 1,352-term prefix throw; a size equal to the limit works. Minimal repro: limit 2, four terms one edit from abcd: plain 2 hits, wrapped throws.
Exposure and likelihood187 words
- Who is hit. Code that uses
SpanMultiTermQueryWrapperaround aFuzzyQueryor another top-terms multi-term query and either setsmaxExpansionsabove the clause limit (relying onFuzzyQuery's documented cap), lowers the limit withIndexSearcher.setMaxClauseCountbelow the expansion size, or setsTopTermsSpanBooleanQueryRewrite(n)/FrequentTermsSpanBooleanQueryRewrite(n)withnabove the limit. - What it takes. The expansion must really match more terms than the limit (+1, because of #10339). Ordinary span-wrapped fuzzy queries (50 expansions, default limit 1,024) are not affected.
- What happens. Search, count and any other entry point that rewrites the query throw
IndexSearcher$TooManyNestedClausesfor that request, where the same query unwrapped runs with the capped expansion. - What does not happen. No wrong results; nothing in the index or the searcher is changed; the clause limit still holds.
- Default vs opt-in. Default writer, codec and searcher settings, but every trigger needs a non-default size or a lowered limit on the spans API. No in-tree query parser builds this shape: no main source under
lucene/queryparsernamesSpanMultiTermQueryWrapperor either span top-N rewrite (grep at645d4fe14a20). Likelihood: rare; no user report was found in the nearly five years since 9.0.0.
One JUnit test on the public API: four one-term documents one edit from abcd, clause limit 2, the default FuzzyQuery(abcd). The plain query is capped and counts 2; the test expects the same count from the span-wrapped query and fails because it throws. Deterministic, single thread; the same with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.queries.spans;
import org.apache.lucene.document.Document;
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.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestSpanWrappedFuzzyQueryClauseLimit extends LuceneTestCase {
public void testSpanWrappedFuzzyQueryIsCappedAtMaxClauseCount() throws Exception {
int saved = IndexSearcher.getMaxClauseCount();
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
for (String t : new String[] {"abca", "abcb", "abce", "abcf"}) {
Document doc = new Document();
doc.add(new TextField("f", t, Field.Store.NO));
w.addDocument(doc);
}
}
IndexSearcher.setMaxClauseCount(2);
try (DirectoryReader r = DirectoryReader.open(dir)) {
IndexSearcher s = new IndexSearcher(r);
FuzzyQuery fuzzy = new FuzzyQuery(new Term("f", "abcd"));
int plain = s.count(fuzzy);
try {
assertEquals(plain, s.count(new SpanMultiTermQueryWrapper<>(fuzzy)));
} catch (IndexSearcher.TooManyClauses e) {
// expected: capped at maxClauseCount like the plain FuzzyQuery; actual: TooManyNestedClauses
throw new AssertionError("expected " + plain + " hits like the plain FuzzyQuery, got " + e, e);
}
}
} finally {
IndexSearcher.setMaxClauseCount(saved);
}
}
}
cp TestSpanWrappedFuzzyQueryClauseLimit.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/spans/
cd $LUCENE && ./gradlew :lucene:queries:test --tests org.apache.lucene.queries.spans.TestSpanWrappedFuzzyQueryClauseLimit -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
- The span-wrapped
FuzzyQueryis capped at the clause limit like the plain one and counts the same 2 hits. - Observed
IndexSearcher$TooManyNestedClauses: Query contains too many nested clauses; maxClauseCount is set to 2, with assertions on and off.
More files, environment, full output297 words
Public API only: IndexWriter with the default IndexWriterConfig, ByteBuffersDirectory, TextField, IndexSearcher.setMaxClauseCount, FuzzyQuery, SpanMultiTermQueryWrapper, IndexSearcher.count; the class sits in the spans package by Lucene test convention but uses no package-private API, and it restores the static clause limit in finally. The catch only turns the exception into an expected-vs-actual message.
Why these numbers: the plain default FuzzyQuery (50 expansions) keeps 2 terms at limit 2 and counts 2 hits; wrapped, it keeps all 4, and 4 is the smallest clause count the visitor rejects at limit 2 (it lets limit + 1 through, #10339).
It fails unchanged on 10.6.0 and 10.5.2 with assertions on and off, and 5 of 5 iterations with another seed fail the same way on main (assertions on).
As the regression test: this same test, for example as a method in TestSpanMultiTermQueryWrapper. Either way (new file or new method), ./gradlew tidy (google-java-format) re-wraps two long lines (the comment and the AssertionError), which stay over 100 columns at the same indentation inside an existing class; nothing else changes.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the same test also fails on 10.6.0 and 10.5.2; every release from 9.0.0 throws |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestSpanWrappedFuzzyQueryClauseLimit.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/spans/
cd $LUCENE && ./gradlew :lucene:queries:test --tests org.apache.lucene.queries.spans.TestSpanWrappedFuzzyQueryClauseLimit -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
TestSpanWrappedFuzzyQueryClauseLimit > testSpanWrappedFuzzyQueryIsCappedAtMaxClauseCount FAILED
java.lang.AssertionError: expected 2 hits like the plain FuzzyQuery, got org.apache.lucene.search.IndexSearcher$TooManyNestedClauses: Query contains too many nested clauses; maxClauseCount is set to 2
at __randomizedtesting.SeedInfo.seed([DEADBEEF:40644820E53FDBCA]:0)
at org.apache.lucene.queries.spans.TestSpanWrappedFuzzyQueryClauseLimit.testSpanWrappedFuzzyQueryIsCappedAtMaxClauseCount(TestSpanWrappedFuzzyQueryClauseLimit.java:53)
Caused by:
org.apache.lucene.search.IndexSearcher$TooManyNestedClauses: Query contains too many nested clauses; maxClauseCount is set to 2
at org.apache.lucene.search.IndexSearcher$1.consumeTerms(IndexSearcher.java:918)
at org.apache.lucene.queries.spans.SpanTermQuery.visit(SpanTermQuery.java:100)
at org.apache.lucene.queries.spans.SpanOrQuery.visit(SpanOrQuery.java:93)
at org.apache.lucene.search.ConstantScoreQuery.visit(ConstantScoreQuery.java:78)
at org.apache.lucene.search.IndexSearcher.rewrite(IndexSearcher.java:878)
at org.apache.lucene.search.IndexSearcher.count(IndexSearcher.java:497)
at org.apache.lucene.queries.spans.TestSpanWrappedFuzzyQueryClauseLimit.testSpanWrappedFuzzyQueryIsCappedAtMaxClauseCount(TestSpanWrappedFuzzyQueryClauseLimit.java:50)
# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=false; identical with assertions onFuzzyQuerydocuments the cap ("the maxClauseCount will be used instead"); both span top-N rewrites say they "will not overflow the boolean max clause count".Integer.MAX_VALUEdates from 2010, whenSpanOrQueryhad "no limits"; LUCENE-8811 began counting span clauses in 9.0 without touching spans.- The upstream test
testFuzzy2asserts that the wrapper honoursmaxExpansions.
The full argument306 words
- Contract, in four javadocs.
FuzzyQuery,@param maxExpansions: "the maximum number of terms to match. If this number is greater than IndexSearcher#getMaxClauseCount when the query is rewritten, then the maxClauseCount will be used instead." (FuzzyQuery.java:79-81).TopTermsRewriteconstructor, which the span rewrite's delegate extends (the class is@lucene.internal,TopTermsRewrite.java:34): "NOTE: if IndexSearcher#getMaxClauseCount is smaller than size, then it will be used instead." (TopTermsRewrite.java:43-44).TopTermsSpanBooleanQueryRewrite: "only uses the top scoring terms so it will not overflow the boolean max clause count" (SpanMultiTermQueryWrapper.java:196-197).FrequentTermsSpanBooleanQueryRewrite, written in 2026: "retains only the most frequent terms so it will not overflow the boolean max clause count" (:258-259; alsolucene/CHANGES.txt:519-521). On mainBooleanQuery.Builder.addchecksIndexSearcher.maxClauseCount(BooleanQuery.java:82): the only clause limit isIndexSearcher's. - Long-standing intent. Lucene 1.9's CHANGES: "FuzzyQuery can no longer throw a TooManyClauses exception. If a FuzzyQuery expands to more than BooleanQuery.maxClauseCount terms only the BooleanQuery.maxClauseCount most similar terms go into the rewritten query" (
lucene/CHANGES.txt:21672-21675). - Upstream test.
TestSpanMultiTermQueryWrapper.testFuzzy2("maximum of 1 term expansion") asserts that the wrapper honours aFuzzyQuery'smaxExpansions(TestSpanMultiTermQueryWrapper.java:96-103). - History.
Integer.MAX_VALUEcomes from the wrapper's introduction (ac71ebc2371, LUCENE-2754, 2010), whenSpanOrQueryhad no clause limit ("we accept all terms as SpanOrQuery has no limits",1d0515124e2, LUCENE-2757, still at:169-171). LUCENE-8811 (53f56fb7ad4, 9.0) madeIndexSearchercount every clause, span terms included, and touched no span file; spans are not mentioned in #9855. That the check reaches span expansions is by design: on #9855 Alan Woodward asked to run the count after rewriting because "some multi-term queries can end up expanding into very large boolean combinations, and we should try and catch that". What was not revisited is the span top-N rewrites' own cap, which the core top-terms rewrites apply before the check.
The devil's-advocate check270 words
The strongest case for "intended" (rated moderate by the cross-model reviewers, partial by the study's intended-behaviour verifier): IndexSearcher.rewrite documents TooManyClauses for any query over the limit (IndexSearcher.java:868-869); LUCENE-8811 meant that check to catch large multi-term expansions; the span constructors promise only "at most size terms" (SpanMultiTermQueryWrapper.java:204, :305); the wrapper deliberately replaces FuzzyQuery's own rewrite with a span rewrite, so FuzzyQuery's cap describes a rewrite that does not run; TopTermsRewrite is @lucene.internal (TopTermsRewrite.java:34), so its NOTE is not part of the public span rewrite's contract, and its getMaxSize() javadoc says "for boolean rewrites" (:55-58); Integer.MAX_VALUE was a deliberate 2010 choice; and "boolean max clause count" could be read as BooleanQuery's only.
Why it fails: for the lead trigger the user never asks for more than the limit: they pass a maxExpansions that FuzzyQuery documents as capped, and the wrapper copies that size into its span rewrite (:72-74), which testFuzzy2 relies on. The two public span javadocs, not only the internal NOTE, say the rewrite "will not overflow", and on main there is no other "boolean max clause count" than IndexSearcher's. The check catching the expansion is intended; the span rewrite not capping before it is what differs from the core rewrites. The 2010 choice rested on "no limits", which 9.0 removed without revisiting spans. The argument keeps real weight for explicit span sizes above the limit, where a javadoc fix is a fair alternative (option C): a separate decision, not an equal defect. Verdict: not intended, a defect. Not security: the limit still holds and fails closed.
Recommended A. Cap both span top-N rewrites at the clause limit, at rewrite time (two lines)
Make the span top-terms rewrite's getMaxSize() return IndexSearcher.getMaxClauseCount(), as the three core top-terms rewrites do, and cap the FrequentTermsSpanBooleanQueryRewrite queue the same way: two lines. With it the repro passes with assertions on and off, the 233 span tests run with 0 failures (2 skipped by the suite's own assumeTrue), and the queries module's static checks pass.
All options and trade-offs3 options ยท 518 words
Fix A was validated by the focused gradle runs listed below in a scratch worktree of 645d4fe14a20; the full Lucene test suite was not run. B and C are alternatives that were not run.
A. Cap both span top-N rewrites at the clause limit, at rewrite time (two lines) (validated by the checks) (recommended)
Return the clause limit from TopTermsSpanBooleanQueryRewrite's getMaxSize() (SpanMultiTermQueryWrapper.java:210), so TopTermsRewrite.rewrite keeps min(size, limit) terms, and size the FrequentTerms queue min(maxSize, limit) (:313). Both read the limit when the query is rewritten, as FuzzyQuery documents.
--- a/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java
@@ -207,7 +207,7 @@
new TopTermsRewrite<>(size) {
@Override
protected int getMaxSize() {
- return Integer.MAX_VALUE;
+ return IndexSearcher.getMaxClauseCount();
}
@Override
@@ -310,7 +310,8 @@
new ScoringRewrite<>() {
@Override
protected PriorityQueue<ScoreTerm> getTopLevelBuilder() {
- return PriorityQueue.usingComparator(maxSize, order);
+ return PriorityQueue.usingComparator(
+ Math.min(maxSize, IndexSearcher.getMaxClauseCount()), order);
}
@OverrideTrade-offs
- Matches the three core top-terms rewrites exactly; a span-wrapped
FuzzyQuerykeeps the same topmaxClauseCountterms as the plain one. - Explicit
TopTermsSpanBooleanQueryRewrite(n)/FrequentTermsSpanBooleanQueryRewrite(n)withnabove the limit now keepmaxClauseCountterms instead of throwing: what their "will not overflow" javadoc promises, but still a behaviour change. - Parity with core, no more: a span-wrapped
FuzzyQueryinside aSpanNearQuerywith sibling clauses can still exceed the limit, exactly asBooleanQuery(a, b, fuzzy)does in core. - Backport to branch_10_6: the same
getMaxSize()line (:210); its FrequentTerms queue isnew PriorityQueue<>(maxSize) {...}(:313), so cap that argument.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seedDEADBEEF, 2026-11-01: without the fix the repro fails with assertions on and off; with it, it passes with assertions on and off. ./gradlew :lucene:queries:test --tests 'org.apache.lucene.queries.spans.*': 23 classes, 233 tests, 0 failures (2 skipped by the suite's ownassumeTrue), includingTestSpanMultiTermQueryWrapper,TestSpanSearchEquivalence,TestBasicsand the repro../gradlew :lucene:queries:check -x testwith the fix and the repro as a new test file: passes (google-java-format, ecj lint, forbidden APIs) once./gradlew tidyre-wraps two long lines of the test; the fix itself needed no reformatting. The full Lucene test suite was not run.- Earlier: with the fix shadowed into the stock jars, the finder's, the independent reproduction's and the adjudication's tests and 73 upstream span tests pass, and the study's boundary checker finds no span top-N mismatch (its 8 record-only rows for the unbounded default rewrite remain).
new TopTermsSpanBooleanQueryRewrite(Math.min(pqsize, IndexSearcher.getMaxClauseCount())) at SpanMultiTermQueryWrapper.java:74. Fixes the wrapped-FuzzyQuery case without touching explicit sizes.
Trade-offs
- Reads the limit when the wrapper is built, not when the query is rewritten, so a later
setMaxClauseCountis ignored (FuzzyQuerysays "when the query is rewritten"). - The two "will not overflow" javadocs stay false for explicit sizes.
Keep explicit sizes uncapped and replace "will not overflow the boolean max clause count" (:196-197, :258-259) with "throws TooManyNestedClauses at search time if more than IndexSearcher#getMaxClauseCount terms match".
Trade-offs
Honest about today's behaviour for explicit sizes, but does not fix the FuzzyQuery case on its own (combine with B).
Search servers that let end users send span queries with a fuzzy or top_terms_N multi-term clause return an error instead of results when the expansion size is above the clause limit.
Semi-formal: an AI tool listed the documented clause limits and the paths that reach them, then built a bounded-exhaustive checker comparing 18 entry points with their javadoc at clause limits 1 to 8. 48 mismatches, all in the three span top-N entry points, none in 14 others; a record-only check of the default span rewrite logged 8 (known off-by-one, #10339).
The method in full278 words
Method: bounded-exhaustive boundary translation validation of the stock jar against each entry point's javadoc, public API only (study unit on documented resource limits, main a9779ef3c193, 2026-10-27).
- Inventory: a grep of javadoc sentences that name a numeric limit with a consequence (TooManyClauses, maxExpansions, "at most", ...) over core, queries, suggest, analysis-common and queryparser, then a reading of every call site that reaches the limited computation; the span rewrites'
Integer.MAX_VALUEwas flagged there. - Checker: 915 checks: clause limit L = 1..8, n in {L - 1, L, L + 1} matching terms, sizes {L - 1, L, L + 1, 2L}, 18 entry points (
BooleanQuery.Builder,SynonymQuery, the core top-terms and scoring rewrites,FuzzyQuery, the span rewrites,QueryBuilder, the classic parser, ...), expected outcome per entry point from its javadoc. - Result: 48 mismatches, all in the span-wrapped
FuzzyQuery,TopTermsSpanBooleanQueryRewriteandFrequentTermsSpanBooleanQueryRewrite(they keep L + 1 terms at n = L + 1 where the core rewrites keep L), 0 in 14 others. The 18th entry, a record-only check of the unbounded default span rewrite (SCORING_SPAN_QUERY_REWRITE, out of scope: #7571, #10339), logged 8 more: at n = L + 1 it lets L + 1 terms through where the javadoc model expects a throw, the known clause-visitor off-by-one. A mutant that adds one to the core rewrites'getMaxSize()gives 16 mismatches per core entry point, so the checker is not vacuous; the fix removes every span top-N mismatch and leaves the 8 record-only rows. - A scale probe at the default limit 1,024 gave the lead trigger; the finder then wrote the public-API JUnit, and the one-test minimal repro was reduced from it.
No prior report found in GitHub issues, PRs and commits, the JIRA archive, the Lucene mailing lists, downstream projects or the web (247 recorded queries, 2026-10-27; refreshed 2026-11-01). Closest: open #7571 and a 2024 java-user thread, both about the unbounded default span rewrite, not the capped top-N ones. Not fixed upstream as of 2026-10-30.
Searches4 searches
Closest items, none a report of this bug: #7571 (LUCENE-6513, open: limits for the unbounded default span rewrite; a 2017 comment asks whether that default should use the max boolean clause limit; nothing on the top-N rewrites or FuzzyQuery's cap); java-user 2024-03-28 "SpanMultiTermQueryWrapper with PrefixQuery hitting num clause limit" (the default rewrite; answered as expected: "using spans and wildcards together is asking for trouble, you will hit limits"); #16484 (added FrequentTermsSpanBooleanQueryRewrite, sizes above the limit not discussed); #9855 (LUCENE-8811, where the exception starts; spans never mentioned); #10339 (open, the visitor off-by-one, a different fix site). The default rewrite is out of scope here.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits | 2026-10-27 | two scripted prior-report searches with different term sets (class and identifier names; symptom and usage terms), 33 manual searches over apache/lucene, downstream projects and all of GitHub (for example "SpanMultiTermQueryWrapper" + "TooManyNestedClauses", "TopTermsSpanBooleanQueryRewrite" + maxClauseCount), 3 code searches, the changed-file lists of all 410 open PRs | no report; no open PR touches the fix site |
| Mailing lists, JIRA archive, downstream, web | 2026-10-27 | dev@, java-user@, issues@ and commits@ (40 scripted queries and 14 direct searches); JIRA LUCENE and SOLR; Solr, Elasticsearch and OpenSearch issues; 13 web queries | only default-rewrite reports (java-user 2024-03-28, Elasticsearch #37241 and others) |
| GitHub refresh | 2026-11-01 | 6 read-only searches, 4 of them limited to items updated since 2026-10-27 (span maxClauseCount, TooManyNestedClauses, SpanMultiTermQueryWrapper PRs, fuzzy span clause) | nothing new |
| Upstream branches | 2026-11-01 | file contents on main 2f424fd428d, branch_11x 603af870375, branch_10_6 7e5aec4370e and the 10.6.0 tag | not fixed: getMaxSize() still returns Integer.MAX_VALUE |
The exception starts in 9.0.0 with LUCENE-8811 (53f56fb7ad4, 2019), which made IndexSearcher count span clauses; the missing cap is from 2010 (ac71ebc2371, LUCENE-2754). Every release from 9.0.0 to 10.6.0 throws; 3.1.0 to 8.11.4 over-expanded silently. Not fixed upstream as of 2026-10-30.
Versions tested6 rows
Exception introduced by LUCENE-8811 "Move max clause checks to IndexSearcher" (apache/lucene#9855; committed by romseygeek 2019-06-26 as 53f56fb7ad4, patches by atris; first released in 9.0.0). Its 8.x port fa3bf88783a was reverted on branch_8x and branch_8_2 (2885a115782, 193c77fe1a0, 2019-07-15) before any 8.x release. Missing cap since ac71ebc2371 (LUCENE-2754, apache/lucene#3828, uschindler, 2010-11-14, on trunk; the 3.x port 7d137513808, 2010-11-15, first released it in 3.1.0). FrequentTerms half: #16484 (156fb97fd33 on main, 3572e2c03f0 on the 10.x branches, 2026-08-18), first released in 10.6.0 (2026-10-29).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (standalone and gradle); passes with fix A |
| 10.6.0, 10.5.2 | affected | minimal repro fails unchanged with assertions on and off (SHA-1-checked release jars) |
| 9.0.0 to 10.5.2 (33 releases) | affected | the study's probe: span-wrapped FuzzyQuery (2000 expansions at limit 1024; 50 at limit 32) and TopTermsSpanBooleanQueryRewrite(2000) throw TooManyNestedClauses; plain queries capped |
| 3.1.0 to 8.11.4 (105 releases) | silent over-expansion | no exception, but the span rewrite already keeps more terms than the limit (1,326 instead of 1,024); history only, not claimed |
| 3.0.3 | API absent | no SpanMultiTermQueryWrapper (compile error) |
| main with fix A (shadow and gradle) | fix | span rewrites keep 1,024 / 32 / 1,024 terms like core; the probe and every test pass |
Confirmed by the study's independent verifiers and its adjudication, reviewers from another model family (OpenAI GPT-5.6) that ran their own tests and judged it real with high confidence, a credibility audit and this doc's gradle run. The two-line fix passed every check it was given.
Checks and verdicts393 words
The study's checks: finder, independent reproducer, two executed cross-model reviews, intended-behaviour devil's advocate, adjudication; then the registry re-run and audit, the minimal repro on 645d4fe14a20, and this doc's release and gradle runs.
| Check | Verdict | What it found |
|---|---|---|
| finder | fails on main, 10.5.2, 9.12.3, 9.0.0 | Public-API JUnit, four scale tests; the two FuzzyQuery tests carry passing plain-query controls (1,024 terms and 676 hits; 32 terms at limit 32): 4 of 4 fail on main with -ea and -da; 8.11.4 passes (no exception before 9.0); all pass with the fix. |
| Independent reproducer | real | Own tests from the claim alone, other data (limits 1,024 and 8): 4 of 4 fail on main, 20 of 20 over 5 iterations; 10.5.2, 9.12.3 and 9.0.0 throw; its own fix and the finder's pass. |
| cross-model review, refute lens (OpenAI GPT-5.6, ran code) | real, high confidence | Own tests at clause limit 3 fail with -ea and -da; the at-limit and plain controls pass; with the fix its tests and TestSpanMultiTermQueryWrapper (10 of 10) pass. Intended-behaviour argument: moderate. |
| cross-model review, intended lens (OpenAI GPT-5.6, ran code) | real, high confidence | Own test: plain FuzzyQuery capped at 3 terms while the three span forms throw; same on 10.5.2, no exception on 8.11.4; passes with the fix. |
| Intended-behaviour devil's advocate | real (high for FuzzyQuery, medium for explicit sizes) | Built the strongest "intended" case; boundary probe (wrapped throws from maxExpansions 1026 where the plain query runs); found that the fix restores parity with core but not nested-tree safety. |
| adjudication | confirmed, high | Own test in a package outside spans (clause limit 10) with the independent reproduction's: 6 of 7 fail on main with -ea and -da; 7 of 7 pass with the fix. |
| registry re-run and credibility audit | reproduces; impact rated opt-in | 4 of 4 fail on a clean checkout with -ea and -da; 4 of 4 pass with the fix; the audit confirmed the claim and lowered its priority because every trigger needs a non-default size or limit. |
| this doc's runs | fails stock, passes fixed | The minimal repro fails on 10.6.0 and 10.5.2 with assertions on and off; gradle in a scratch worktree of 645d4fe14a20: fails stock with -ea and -da, passes with fix A, as do all span tests and the module's static checks. |
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 441 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,222 words
- Registry title
- SpanMultiTermQueryWrapper's top-N span rewrites (TopTermsSpanBooleanQueryRewrite, FrequentTermsSpanBooleanQueryRewrite) do not cap their size at IndexSearcher#getMaxClauseCount, so a span-wrapped FuzzyQuery that works unwrapped throws TooManyNestedClauses
- Area
- queries / spans: SpanMultiTermQueryWrapper top-N rewrite methods vs the core IndexSearcher clause limit
- Priority
- Low, rank 1126: A span-wrapped FuzzyQuery that works unwrapped fails loudly with TooManyNestedClauses, but only when maxExpansions or an explicit span top-N size is above the clause limit.
- Impact class
- crash
- Impact note
- Applications that wrap a FuzzyQuery (or another top-terms multi-term query) in SpanMultiTermQueryWrapper with maxExpansions above IndexSearcher#getMaxClauseCount (1024 by default), that lower the clause limit below the expansion size (below 50 even the default FuzzyQuery fails once wrapped), or that pass an explicit TopTermsSpanBooleanQueryRewrite / FrequentTermsSpanBooleanQueryRewrite size above the limit get IndexSearcher$TooManyNestedClauses from search or count as soon as the expansion matches more terms than the limit, where the unwrapped query is capped and runs; a clean per-request exception with no wrong results and no index effect, default writer, codec and searcher settings, no scale needed, every release since 9.0.0.
- Summary
- You wrap new FuzzyQuery(term, 2, 0, 2000, true) in SpanMultiTermQueryWrapper (for example to use it inside a SpanNearQuery) and search an index where more than 1024 terms are within two edits. FuzzyQuery documents that a maxExpansions above IndexSearcher#getMaxClauseCount is replaced by the limit, and the span top-N rewrites say they 'will not overflow the boolean max clause count', so you expect the same capped expansion the plain FuzzyQuery gets (it runs with 1024 terms). Instead the span rewrite keeps up to maxExpansions terms (TopTermsSpanBooleanQueryRewrite.getMaxSize() returns Integer.MAX_VALUE, and FrequentTermsSpanBooleanQueryRewrite sizes its queue with the caller's size only), and IndexSearcher.rewrite throws IndexSearcher$TooManyNestedClauses ('maxClauseCount is set to 1024').
- Introduced
- 53f56fb7ad44afab9c78fab2e89737438093247f (LUCENE-8811 'Move max clause checks to IndexSearcher', apache/lucene#9855, committed by romseygeek with patches by atris, 2019-06-26, first released in 9.0.0) is where the failure starts: IndexSearcher.rewrite began to count every clause of the rewritten query with a QueryVisitor, span terms included, and the commit did not touch SpanMultiTermQueryWrapper. The missing cap it exposes is older: TopTermsSpanBooleanQueryRewrite.getMaxSize() returning Integer.MAX_VALUE and the mapping of a TopTermsRewrite (FuzzyQuery's) to new TopTermsSpanBooleanQueryRewrite(size) come from ac71ebc23716 (LUCENE-2754, apache/lucene#3828, uschindler, 2010-11-14; 3.x backport 7d137513808), written when SpanOrQuery had no clause limit (sibling comment 'we accept all terms as SpanOrQuery has no limits', 1d0515124e2, LUCENE-2757). The FrequentTermsSpanBooleanQueryRewrite half is 156fb97fd33 on main and 3572e2c03f0 on branch_10x / branch_10_6 (apache/lucene#16484, 2026-08-18, first released in 10.6.0). [read: git blame -w -C -C -M and git log; exec: release sweep, exception from 9.0.0 on] (2019-06-26 (53f56fb7ad4; first released in 9.0.0, 2021-12); latent cap omission 2010-11-14 (ac71ebc2371); FrequentTerms half 2026-08-18 (#16484, first released in 10.6.0))
- First affected version
- 9.0.0 [exec] (first release that throws; the 8.x port of LUCENE-8811, fa3bf88783a, was reverted before any 8.x release, so 8.2.0 behaves like 8.1.1 [exec]; from 3.1.0, the first release with the wrapper, to 8.11.4 the span rewrite already kept more terms than the clause limit, but silently and without an exception [exec]; the FrequentTerms half from 10.6.0, released 2026-10-29)
- Affected versions
- Exception (TooManyNestedClauses): every 9.x and 10.x release, 9.0.0 through 10.5.2 (33 releases) [exec: a version-portable probe on each], 10.6.0 [exec: the one-test minimal repro fails on the release jars, SHA-1 checked against Maven Central, -ea and -da, 2026-11-01], and main a9779ef3c193 [exec, -ea and -da]; never fixed: origin/main a95c754ee00, branch_10x 1309534296e and branch_10_6 2763db11b0a still return Integer.MAX_VALUE [read]. The finder's JUnit fails on 9.0.0, 9.1.0, 9.12.3, 10.0.0 and 10.5.2 (3 of 3) and on main (4 of 4, -ea and -da); the independent reproduction's tests fail on 9.0.0, 9.12.3, 10.5.2 and main. FrequentTermsSpanBooleanQueryRewrite: main, branch_10x and branch_10_6 only (10.6.0, released 2026-10-29) [exec on main]. Silent over-expansion without an exception, recorded as history and not claimed: 3.1.0 through 8.11.4 (105 releases) [exec]. 3.0.3 and earlier: API absent [exec 3.0.3]. All runs with JDK 25 on aarch64.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check, code reading
- Upstream and prior reports
- none found (searched 2026-10-27, read-only, 247 recorded queries: two scripted prior-report searches with 60 GitHub issue/PR, 6 commit, 4 downstream-repository, 40 mailing-list (dev@, java-user@, issues@, commits@), 8 JIRA-archive and 6 web queries, plus the finder's earlier run (51 queries, re-read); 33 manual GitHub issue/PR searches over apache/lucene, downstream projects and all of GitHub; 3 GitHub code searches; 14 direct mailing-list searches; 3 JIRA queries; 7 web searches; the changed-file lists of all 410 open apache/lucene PRs; the upstream history of the fix-site files on 5 branches). Nearest items, not this bug: apache/lucene#7571 (LUCENE-6513, open enhancement to bound the unbounded DEFAULT span rewrite; a 2017 comment asks whether that default should use the clause limit), the java-user thread of 2024-03-28 'SpanMultiTermQueryWrapper with PrefixQuery hitting num clause limit' (default rewrite, answered as expected), #16484 (added FrequentTermsSpanBooleanQueryRewrite; sizes above the limit not discussed), #9855 (LUCENE-8811, the origin of the failure; spans never mentioned), #10339 (open, the visitor off-by-one at a different fix site). Not fixed upstream: SpanMultiTermQueryWrapper.java last changed on main by 156fb97fd33 (#16484), and no open PR touches it.
- Lucene commit tested
- main
645d4fe14a20: the minimal repro with assertions on and off, standalone and through gradle; the recommended fix in a scratch worktree with everyorg.apache.lucene.queries.spans.*test and:lucene:queries:check -x test. SHA-1-checked release jars: the minimal repro on 10.6.0 and 10.5.2 (assertions on and off); the study's version-portable probe on every release from 3.0.3 to 10.5.2. The study ran on maina9779ef3c193;SpanMultiTermQueryWrapper.java,TopTermsRewrite.java,FuzzyQuery.java,MultiTermQuery.javaandIndexSearcher.javaare identical there, in645d4fe14a20and in upstream main2f424fd428d(2026-10-30, read).
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:70-77 (selectRewriteMethod copies the TopTermsRewrite size), :170 ('we accept all terms as SpanOrQuery has no limits'), :196-197 and :259 ('will not overflow the boolean max clause count'), :208-211 (getMaxSize returns Integer.MAX_VALUE), :310-314 (FrequentTerms queue sized maxSize)lucene/core/src/java/org/apache/lucene/search/TopTermsRewrite.java:39-44 (constructor NOTE: the clause limit is used if smaller than size) and :64 (maxSize = Math.min(size, getMaxSize())); FuzzyQuery.java:79-81 (maxExpansions capped at getMaxClauseCount); IndexSearcher.java:865-879 (rewrite runs the clause-count visitor); MultiTermQuery.java:167-168, 212-213, 260-261 (core getMaxSize returns getMaxClauseCount)lucene/queries/src/test/org/apache/lucene/queries/spans/TestSpanMultiTermQueryWrapper.java:96-103 (testFuzzy2: the wrapped FuzzyQuery honours maxExpansions)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:65-78, :169-171, :196-197, :208-211, :258-259, :312-314, :326-329 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/TopTermsRewrite.java:43-44, :64; MultiTermQuery.java:167-168, :212-213, :260-261; FuzzyQuery.java:56, :60-62, :79-81; IndexSearcher.java:80, :251-261, :878, :916-920 (at 645d4fe14a20)lucene/queries/src/java/org/apache/lucene/queries/spans/SpanOrQuery.java:87-93; SpanTermQuery.java:98-101 (at 645d4fe14a20)
- Who is affected
grep -rl -E 'SpanMultiTermQueryWrapper|TopTermsSpanBooleanQueryRewrite|FrequentTermsSpanBooleanQueryRewrite' lucene/*/src/java at 645d4fe14a20: only queries/spans, highlighter PhraseHelper and sandbox PhraseWildcardQuery
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java:79-81; TopTermsRewrite.java:43-44; IndexSearcher.java:868-869; BooleanQuery.java:82 (at 645d4fe14a20)lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java:169-171, :196-197, :204, :258-259, :305 (at 645d4fe14a20)lucene/CHANGES.txt:519-521, :21672-21675 (at 645d4fe14a20)lucene/queries/src/test/org/apache/lucene/queries/spans/TestSpanMultiTermQueryWrapper.java:96-103 (at 645d4fe14a20)
- Suggested fix
a Lucene clone: git show origin/branch_10_6:lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java (:210, :313)
- History
a Lucene clone: git show releases/lucene/10.6.0, origin/main, origin/branch_11x, origin/branch_10_6 :lucene/queries/src/java/org/apache/lucene/queries/spans/SpanMultiTermQueryWrapper.java; 2f424fd428d (DOAP: 10.6.0 created 2026-10-29)
- TestSpanWrappedFuzzyQueryClauseLimit.java (JUnit, one test):
repro/LFV-733/src/org/apache/lucene/queries/spans/TestSpanWrappedFuzzyQueryClauseLimit.java(60 lines)
Made in parallel with this doc from the mechanism in the brief and the minimal repro; the PNG is the summary frame.
assets/LFV-733/bug.png(441 KB)
