All findings · Lucene finding · LFV-923 · function-queries-visit-hides-values-source-query

IndexSearcher's clause limit never counts the query inside a function query's values source

FunctionScoreQuery.boostByQuery(in, boostMatch, 2f) with two 600-term disjunctions runs 1,200 term clauses although the default maxClauseCount is 1,024: the cumulative check never looks inside a values source, so the boost query is only checked on its own. Hits are correct; every release since 9.0.0.

Summary

Priority
Low, rank 733Function queries hide their inner query from the cumulative maxClauseCount check, so the limit can be bypassed without bound while hits stay correct, and it is the strongest of the new QueryVisitor-hiding rows, so it leads them after LFV-402 and above the 10.6.0 regression LFV-910.
Impact
minor
Since
9.0.0 (2021), when LUCENE-8811 made the clause check cumulativeintroduced 2019-06-26 · about 7 years
Affected
9.0.0 to 10.6.0 and main (34 releases run)
Status
documented
Found by
bounded-exhaustive differential check of the clause limit, then a public-API JUnit

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

The clause limit is checked once, by walking the query with a QueryVisitor. Function queries do not pass the visitor into their values source, so a query held there, such as boostByQuery's boost query, is not counted. It is checked later, on its own, when the search builds its weight.

Background

  1. The clause limit. IndexSearcher throws TooManyNestedClauses for a query with more than maxClauseCount clauses (1,024 by default) counted over all its nested sub-queries. Since 9.0 it counts them once, after rewrite, by walking the query tree with a QueryVisitor.
  2. Values sources. A function query computes a value per document from a values source (DoubleValuesSource or ValueSource). Some sources hold a whole query and use its score: DoubleValuesSource.fromQuery(q), QueryValueSource(q), and the boost query of FunctionScoreQuery.boostByQuery.
Diagram of the bug: FunctionScoreQuery.boostByQuery(in, boostMatch, 2f) with in = 600 term clauses and boostMatch = 600 term clauses, maxClauseCount 1,024. The clause check after rewrite walks the query with a QueryVisitor, enters in and counts 600, but stops at the values source that holds boostMatch. When the search builds its weight, boostMatch is rewritten and checked on its own, from 0: 600. Both checks pass, the search returns the hit (score doubled by the boost), although 1,200 term clauses ran. A BooleanQuery with the same 1,200 clauses is counted in one walk and rejected with TooManyNestedClauses.
The repro's example: the clause check walks in (600 clauses) but stops at the values source; the boost query (600) is checked later on its own, so no check sees 1,200, while the same clauses in a BooleanQuery are rejected. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document (f:a0, f:b0) and search FunctionScoreQuery.boostByQuery(in, boostMatch, 2f), where in is 600 SHOULD term clauses f:a0..f:a599 and boostMatch is 600 more, f:b0..f:b599. The limit is the default maxClauseCount of 1,024.
  2. You expect TooManyNestedClauses: both parts run (the boost query doubles the hit's score, 0.164 to 0.329), so 1,200 term clauses run, and a BooleanQuery holding the same 1,200 clauses (MUST in, SHOULD boostMatch) is rejected.
  3. Instead the search runs and returns the hit. The limit is checked by walking the query with a QueryVisitor, and FunctionScoreQuery.visit stops at the values source: the check counts 600. The boost query is checked later, alone, from 0: 600 again.
  4. The same happens for FunctionScoreQuery and FunctionMatchQuery over DoubleValuesSource.fromQuery, and for FunctionQuery and FunctionRangeQuery over QueryValueSource. Repetition and nesting multiply it: 100 boostByQuery clauses with 1,000-clause boost queries run 100,100 term clauses, accepted.

Who is affected

Applications that put a large or repeated query inside a values source (boostByQuery, fromQuery, QueryValueSource) get no TooManyNestedClauses for it, with default settings. Hits and scores are not affected; only the guard rail is missing: about 2x the limit for one boostByQuery, a fresh full budget for every embedded query, and no bound when such queries repeat or nest.

Mechanism, terms and numbers514 words

IndexSearcher is documented to reject a query with more than maxClauseCount clauses (1,024 by default) "cumulatively in all of its children". It counts them once, after rewrite, by walking the query with a QueryVisitor. Some function queries hold a second query inside their values source: the boost query of FunctionScoreQuery.boostByQuery (the replacement for the removed BoostingQuery), any DoubleValuesSource.fromQuery, and QueryValueSource. Their visit() stops at the values source, which has no visit method, so those clauses are never added to the total. The embedded query still runs: it is rewritten when the weight is created, through searcher.rewrite, which checks it alone with a fresh count. So boostByQuery over two 600-term disjunctions runs 1,200 term clauses at the default limit, while the same clauses in a BooleanQuery are rejected, and repeating or nesting such queries multiplies the total without bound. Hits and scores are not affected; every release since 9.0.0 (LUCENE-8811) is affected. A counting-only visitor channel fixes it without changing what term extraction, highlighting or Monitor see.

The code (main 645d4fe14a20)

  1. One check, after rewrite. IndexSearcher.search(Query, int) (IndexSearcher.java:623) goes through searchAfter (:587, :601) to search(Query, CollectorManager) (:750), which rewrites the query (:753) through rewrite(Query) (:871-880, reached via :884); it ends with query.visit(getNumClausesCheckVisitor()) (:878). The visitor (:896-932) returns itself for every occur, "even for MUST_NOT" (:902-905), and counts every visitLeaf, consumeTerms and consumeTermsMatching call. Each call tests numClauses > maxClauseCount before it increments, so one walk accepts 1,025 clauses at the default limit and throws at the 1,026th.
  2. The walk stops at the values source. FunctionScoreQuery.visit is in.visit(visitor.getSubVisitor(MUST, this)) (FunctionScoreQuery.java:133-136); FunctionMatchQuery.visit (:80-83), FunctionRangeQuery.visit (:132-135) and FunctionQuery.visit (:92-95) are visitor.visitLeaf(this). DoubleValuesSource and ValueSource have no visit method.
  3. Where the boost query lives. boostByQuery (FunctionScoreQuery.java:101-106) builds the source MultiplicativeBoostValuesSource(QueryBoostValuesSource(DoubleValuesSource.fromQuery(boostMatch), boost)).
  4. It is checked later, alone. For a scoring search FunctionScoreQuery.createWeight calls source.rewrite(searcher) (:121), which reaches QueryDoubleValuesSource.rewrite (DoubleValuesSource.java:776-780) and searcher.rewrite(query) (:779): step 1 again, on the boost query alone, with a new count that starts at 0. FunctionMatchQuery.createWeight does the same (:88), and QueryValueSource.createWeight calls searcher.rewrite(q) (QueryValueSource.java:83) for FunctionRangeQuery (:149) and FunctionQuery (:71). A non-scoring search skips the boost query (FunctionScoreQuery.java:118-120).

The numbers

boostByQuery(600, 600): the outer check counts 600, the boost query's own check 600; both pass and the hit scores 0.329 instead of 0.164, so the boost query ran. BooleanQuery(MUST 600, SHOULD 600): one check, 1,200, rejected. 32 and 100 boostByQuery clauses with 1,000-clause boost queries run 32,032 and 100,100 term clauses. With maxClauseCount 4, nesting boostByQuery 1, 2 and 3 levels deep builds 20, 84 and 340 leaf queries, and on main and 10.5.2 pulls a scorer for each (16, 64 and 256 on 9.12.3); each level gets a fresh budget, so the total grows exponentially with depth. An embedded query that is over the limit by itself is still rejected.

Exposure and likelihood146 words
  • Who. Users of FunctionScoreQuery.boostByQuery (the documented replacement for BoostingQuery, MIGRATE.md), of FunctionScoreQuery or FunctionMatchQuery over DoubleValuesSource.fromQuery, and of the @lucene.experimental FunctionQuery and FunctionRangeQuery over QueryValueSource, with the default IndexSearcher and limit.
  • What goes wrong. A query the documented cumulative check should reject is accepted. Nothing else: hits and scores are not affected, each embedded query is still checked on its own, and each BooleanQuery is still capped when it is built.
  • Matching role. FunctionMatchQuery and FunctionRangeQuery always run the embedded query, and it decides which documents match. FunctionScoreQuery runs its boost query only on scoring searches.
  • Reach. No Lucene query parser builds these queries. Downstream function-query syntaxes can (for example a query() function that builds a QueryValueSource).
  • Likelihood. Rare: it needs a large embedded query, and the only visible effect is a missing exception. No earlier report was found (see Prior reports).

Minimal repro

One JUnit test on the public API: one document, a plain IndexSearcher, the default maxClauseCount of 1,024, and boostByQuery over two 600-term disjunctions, 1,200 term clauses. It must throw TooManyNestedClauses; nothing depends on the seed.

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

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
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.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestBoostByQueryClauseLimit extends LuceneTestCase {

  private static Query disjunction(String prefix) {
    BooleanQuery.Builder b = new BooleanQuery.Builder();
    for (int i = 0; i < 600; i++) {
      b.add(new TermQuery(new Term("f", prefix + i)), Occur.SHOULD);
    }
    return b.build();
  }

  public void testBoostQueryCountsTowardsMaxClauseCount() throws Exception {
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
      Document doc = new Document();
      doc.add(new StringField("f", "a0", Field.Store.NO));
      doc.add(new StringField("f", "b0", Field.Store.NO));
      w.addDocument(doc);
      try (DirectoryReader reader = DirectoryReader.open(w)) {
        IndexSearcher searcher = new IndexSearcher(reader);
        Query query = FunctionScoreQuery.boostByQuery(disjunction("a"), disjunction("b"), 2f);
        // 1200 term clauses > default maxClauseCount 1024: expected TooManyNestedClauses, actual none
        expectThrows(IndexSearcher.TooManyNestedClauses.class, () -> searcher.search(query, 10));
      }
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestBoostByQueryClauseLimit -Ptests.seed=DEADBEEF -Ptests.asserts=false
Expected
searcher.search(query, 10) throws IndexSearcher.TooManyNestedClauses (1,200 term clauses > 1,024).
Observed
No exception, the search returns the hit: AssertionFailedError: Expected exception TooManyNestedClauses but no exception was thrown, with assertions on and off.

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

More files, environment, full output233 words

ByteBuffersDirectory, default IndexWriterConfig, one document with f:a0 and f:b0 so that both disjunctions match and the boost query really scores. Each part alone (600) is under the limit and the total (1,200) is far above it, so the test does not sit at the limit's boundary. It sits in org.apache.lucene.queries.function, like Lucene's own function-query tests; it fails with seeds DEADBEEF and 1234ABCD.

Release jars: the test fails on 9.12.3, 10.5.2 and 10.6.0 (assertions off). It needs org.apache.lucene.tests.util.LuceneTestCase, which exists from 9.1.0; on 9.0.0 a variant with the old import fails the same way.

Regression test: this same test; it passes with the recommended fix and fits as one more method of the existing TestFunctionScoreQuery.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails, assertions on and off; with the fix: passes. Releases 9.12.3, 10.5.2 and 10.6.0: fail
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is pure Java)
Command (environment)
cp $REPRO/TestBoostByQueryClauseLimit.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestBoostByQueryClauseLimit -Ptests.seed=DEADBEEF -Ptests.asserts=false
# the same with -Ptests.asserts=true

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed)
TestBoostByQueryClauseLimit > testBoostQueryCountsTowardsMaxClauseCount FAILED
    junit.framework.AssertionFailedError: Expected exception TooManyNestedClauses but no exception was thrown
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:9AEB3E5629C00982]:0)
        at org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1432)
        at org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
        at org.apache.lucene.queries.function.TestBoostByQueryClauseLimit.testBoostQueryCountsTowardsMaxClauseCount(TestBoostByQueryClauseLimit.java:56)
1 test completed, 1 failed

# the same failure with -Ptests.asserts=false

# with the recommended fix (both settings)
1 test, 0 failures (TooManyNestedClauses is thrown)

Why it looks unintended

  • TooManyNestedClauses covers "total clauses cumulatively in all of its children"; since 9.0 the checks "apply to all queries".
  • LUCENE-8811 made the count cumulative so that splitting clauses cannot bypass it; SHOULD and MUST_NOT clauses count too.
  • The by-design history (a matching-only visit()) is about term extraction and highlighting, not the limit; a counting-only fix keeps that view.
The full argument265 words
  • Contract. IndexSearcher.java:1193-1199: "Thrown when a client attempts to execute a Query that has more than getMaxClauseCount() total clauses cumulatively in all of its children". CHANGES.txt:4681-4684 (9.0.0, LUCENE-8811): "The checks are now implemented using a QueryVisitor and apply to all queries, rather than only booleans." MIGRATE.md:1531-1534: "IndexSearcher now performs max clause count checks on all types of queries". BooleanQuery.java:75-76: "We do the final deep check for max clauses count limit during IndexSearcher.rewrite". Query.java:88-93: visit should "Recurse through the query tree, visiting any child queries." The boost query is part of the query's equals, hashCode and toString.
  • Intent. On LUCENE-8811 (#9855) jpountz wrote "I like that it removes ways to by-pass the maximum clause count by splitting boolean queries into multiple sub boolean queries" and that the limit is "now actually the maximum number of clauses across the entire query"; romseygeek: "we also want to handle nested booleans, booleans wrapped in other queries, etc, in which case a QueryVisitor makes most sense".
  • Role does not matter. The check visitor descends into every occur, MUST_NOT included (IndexSearcher.java:903); executed controls BooleanQuery(MUST 768, SHOULD 768) and BooleanQuery(MUST 768, MUST_NOT 768) are both rejected.
  • History. BoostingQuery, which boostByQuery replaced, rewrote its context query as part of the tree; the query became hidden when it moved behind the values source.
  • Precedent. The same kind of gap was fixed as a bug elsewhere: LUCENE-8849 "DocValuesRewriteMethod.visit wasn't visiting its embedded query" (8.6.0) and LUCENE-9050 (8.3.1). We found no javadoc, test, CHANGES entry or thread that excludes values-source queries; TestMaxClauseLimit covers only Boolean, DisjunctionMax and phrase shapes.
The devil's-advocate check224 words

The strongest case (the intended-behaviour verifier): the four visit() bodies are a deliberate matching-only view, ported from Weight#extractTerms by LUCENE-3041, which already skipped the values source; LUCENE-8099 made the highlighters extract only getWrappedQuery(); on #12938 romseygeek wrote "FunctionQuery doesn't actually match things, it just scores, so it shouldn't be used for highlighting"; LUCENE-4261 (#5331, 2012) was closed as not a bug because value sources are opaque to the Scorer API; setMaxClauseCount says "per Query"; each embedded query is still checked alone; and visiting the source as ordinary clauses made Monitor miss two stored queries.

Why it fails: every item concerns term extraction, highlighting or Scorer navigation, and each predates or is independent of the cumulative check; none says values-source queries are outside the clause limit. "Per Query" was written during LUCENE-8811 to mean the entire query. FunctionMatchQuery and FunctionRangeQuery decide matching from the embedded query, so "it just scores" does not cover them. And the matching view need not change: the counting-only fix keeps termCollector and Monitor as they are. Still, the behaviour is inconsistent with the cumulative-limit contract rather than an explicit violation: neither Query.visit nor TooManyNestedClauses says outright that a query reachable only through a values source is a child, and maintainers may regard values sources as a deliberately opaque boundary or prefer a javadoc clarification, so the confidence is medium.

Suggested fix

Recommended A visitor channel that only the clause check follows (validated)

Add a visitor channel that only the clause check follows: QueryVisitor.getValuesSourceVisitor(parent) returns an empty visitor by default and the check's visitor returns itself; values sources get a no-op visit hook that query-holding sources forward; the four function queries visit their source through it. The repro passes, and term extraction, highlighting and Monitor see nothing new.

// QueryVisitor: a channel that only accounting visitors follow
public QueryVisitor getValuesSourceVisitor(Query parent) {
  return EMPTY_VISITOR;
}
// IndexSearcher.getNumClausesCheckVisitor(): count these clauses too
@Override
public QueryVisitor getValuesSourceVisitor(Query parent) {
  return this;
}
// DoubleValuesSource and ValueSource: a no-op hook; query-holding sources forward it
public void visit(QueryVisitor visitor) {}
// FunctionScoreQuery.visit (likewise FunctionMatchQuery, FunctionRangeQuery, FunctionQuery)
in.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
source.visit(visitor.getValuesSourceVisitor(this));
All options and trade-offs4 options · 794 words

One option was built and validated; the others come from the hunt's verifiers. The requirement (adjudication, intended-behaviour verifier, tie-breaker): count the embedded query in the clause check without changing what matching-oriented visitors (termCollector, Monitor's query analyzer, the highlighters) see.

A visitor channel that only the clause check follows (validated) (recommended)

QueryVisitor gets getValuesSourceVisitor(Query parent), returning EMPTY_VISITOR by default; the clause-check visitor in IndexSearcher.getNumClausesCheckVisitor() overrides it to return itself. DoubleValuesSource and ValueSource get a no-op visit(QueryVisitor) hook; DoubleValuesSource.fromQuery's source and QueryValueSource forward it to their query, and the two sources boostByQuery builds and the two ValueSource/DoubleValuesSource bridges forward it to the source they wrap. FunctionScoreQuery, FunctionMatchQuery, FunctionRangeQuery and FunctionQuery add source.visit(visitor.getValuesSourceVisitor(this)) to visit (9 files, +74 lines, no deletions). The method name is a placeholder: ask maintainers which channel they prefer.

// QueryVisitor: a channel that only accounting visitors follow
public QueryVisitor getValuesSourceVisitor(Query parent) {
  return EMPTY_VISITOR;
}
// IndexSearcher.getNumClausesCheckVisitor(): count these clauses too
@Override
public QueryVisitor getValuesSourceVisitor(Query parent) {
  return this;
}
// DoubleValuesSource and ValueSource: a no-op hook; query-holding sources forward it
public void visit(QueryVisitor visitor) {}
// FunctionScoreQuery.visit (likewise FunctionMatchQuery, FunctionRangeQuery, FunctionQuery)
in.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
source.visit(visitor.getValuesSourceVisitor(this));

Trade-offs

  • API addition on three public abstract classes (QueryVisitor, DoubleValuesSource, ValueSource); every default is a no-op, so existing subclasses compile and behave as before.
  • Composite sources stay hidden until they forward the hook: ValueSource functions that wrap other sources (sum, product, if, ...), expression bindings in lucene/expressions, custom sources. The patch covers every source that holds a query directly and the wrappers boostByQuery uses; the rest is a follow-up.
  • Non-scoring searches now count the boost query too, although FunctionScoreQuery does not run it there: an over-count in the same direction as IndexOrDocValuesQuery today (#14756).
  • Nothing is relaxed: each embedded query is still also checked on its own when it is rewritten.
  • Behaviour change: queries accepted today whose total, counted with their embedded queries, is over the limit start to throw TooManyNestedClauses. That is the point of the fix, but callers who rely on today's acceptance need a CHANGES/MIGRATE note; with the API addition this makes option A a design and compatibility question for maintainers, not an obvious one-line bug fix.
  • Optional: RamUsageEstimator's query visitor (query-cache sizing) could follow the same channel; not done, not part of this report.

What the checks validated

Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-31; the worktree was removed afterwards):

  • stock: the minimal test fails with assertions on and off; with the fix it passes with assertions on and off;
  • closest suites: org.apache.lucene.queries.function.* (14 classes, 111 tests, 0 failures) and core TestMaxClauseLimit, TestQueryVisitor, TestDoubleValuesSource, TestIndexSearcher, TestBooleanQuery (62 tests, 0 failures, 1 skipped); all of core org.apache.lucene.search.* (214 classes, 2,339 tests, 0 failures, 49 skipped);
  • whole modules: queries (512 tests, 0 failures, 5 skipped), highlighter (722 tests), monitor (181 tests, 1 skipped), expressions (170 tests), all 0 failures;
  • matching views unchanged: the intended-behaviour verifier's Monitor and termCollector probe passes 4 of 4 with the fix, as on stock (Monitor still matches the stored boostByQuery and FunctionQuery(QueryValueSource) queries; termCollector still returns only [f:a]);
  • with the fix shadowed in the standalone runner (assertions off): the finder's 5 tests and the tie-breaker's 7 (including nesting) pass; an independent reproducer's 10 tests have 1 failure, its own termCollector assertion, which this fix deliberately leaves unchanged;
  • checkGoogleJavaFormat, ecjLintMain, forbiddenApisMain and renderJavadoc pass for core and queries.

Visit the source as ordinary clauses (not recommended)

Forward the hook through getSubVisitor(SHOULD or FILTER, this), as the finder's first fix did.

Trade-offs

It makes the repro pass but changes what matching tools see: Monitor stops matching a stored boostByQuery(f:a, f:<rare>) and a stored FunctionQuery(QueryValueSource(...)) for a document with only f:a, and termCollector returns [f:a, f:b] instead of [f:a] (3 of 4 probe tests fail). That undoes the deliberate matching-only view (LUCENE-8099, #12938).

Visit the source through a MUST_NOT sub-visitor (feasibility only)

The tie-breaker's experiment: no new QueryVisitor method, because the clause check already descends into MUST_NOT and matching visitors do not. It passes the repros, the Monitor probe, 88 function-query and 70 highlighter tests.

Trade-offs

It labels a scoring query as prohibited: a visitor that also collects prohibited terms (the highlighter's QueryTermExtractor.getTerms(query, true)) would start returning the boost query's terms as if they came from a MUST_NOT clause. It only shows that a counting-only path exists.

Document the gap instead

Say in setMaxClauseCount and TooManyNestedClauses that queries held by values sources are checked on their own, not in the cumulative total.

Trade-offs

Cheapest, no API change, but it leaves the bypass, including the exponential growth with nesting, and goes against LUCENE-8811's aim of no bypass by splitting clauses.

Downstream note

Products that turn user input into function queries with embedded sub-queries (for example a query() function) and rely on maxClauseCount alone to bound query size should bound each embedded query themselves.

How it was found

A hunt unit on queries that run an embedded query but hide it from QueryVisitor compared the clause limit's count with the clauses that actually run, for 16 wrapper queries (1,440 bounded-exhaustive checks), and scanned the bytecode for query- and values-source-typed fields that visit never reads. The five function-query sites under-counted; a public-API JUnit then confirmed it.

The method in full193 words

Method: semi-formal (the hunt's tier for bounded-exhaustive checks against a reference model). The unit's lens was queries that execute an embedded query but hide it from QueryVisitor.

  • Differential check. On the stock main jars, 16 wrapper sites x primary leaves {1, 2, 4} x hidden leaves {1..10} x copies {1, 2, 3} = 1,440 checks at maxClauseCount 8 compared the limit's decision with a count of the clauses that really execute, with assertions on and off. boostByQuery, FunctionScoreQuery(fromQuery), FunctionMatchQuery(fromQuery), FunctionQuery(QueryValueSource) and FunctionRangeQuery(QueryValueSource) under-counted in 44, 44, 35, 35 and 35 of their 90 checks; the BooleanQuery, ConstantScoreQuery and DisjunctionMaxQuery controls had 0 mismatches in 270, and the oracle caught the known IndexOrDocValuesQuery over-count (52), so it is not vacuous.
  • Bytecode scan. The JDK ClassFile API listed query- and values-source-typed fields that visit() never reads (64 such fields in 142 query classes). The four function-query fields are among them, each read when the weight is built: FunctionScoreQuery.source and FunctionMatchQuery.source by createWeight, FunctionQuery.func and FunctionRangeQuery.valueSource by their weight constructors.
  • JUnit. The mismatch was reduced to a public-API test against a BooleanQuery control, then minimized to the one-test repro above.

Prior reports, history and checks

Prior reports

No prior report: a follow-up search on 2026-10-28 (GitHub, the JIRA archive, mailing lists, downstream code, the web, all 413 open PRs) and a GitHub refresh on 2026-10-31. Related, not this bug: LUCENE-4261 (#5331, value sources opaque to Scorer navigation) and a #15097 remark on query-cache RAM sizing.

Searches3 searches
  • LUCENE-4261 (#5331, 2012). A user asked for FunctionQuery's nested QueryValueSource scorers through Scorer.getChildren(); it was closed as not a bug ("whatever that function does is opaque really"). A different API and consequence, years before QueryVisitor and the cumulative check.
  • #15097 (comment of 2025-08-22). A clause-counting visitor would not see a large DoubleValuesSource held by a FunctionScoreQuery. Its subject is query-cache RAM sizing; it mentions neither an embedded query nor the clause limit.
  • Support: LUCENE-8811 (#9855); #14756 and open PR #16175 (the opposite over-count of IndexOrDocValuesQuery; "for the purpose of visiting, we should visit all variants"); #14759 ("QueryVisitor is supposed to be a query introspection API"); LUCENE-8849 and LUCENE-9050 (the same kind of visit() gap, fixed as bugs).
  • Limits: GitHub search is keyword-based (the open-PR file scan finds fixes whatever their wording); two queries of the 2026-10-31 refresh hit the rate limit in the scripted re-run and are recorded from the interactive run two minutes earlier.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive), downstream Elasticsearch, OpenSearch and Solr2026-10-28the scripted prior-report search, two specs (118 queries: clause-limit terms; term-extraction, highlighting and sizeOf terms), 36 manual GitHub searches, and the changed-file lists of all 413 open PRsno report; 28 open PRs match the file filter, none changes these visit() bodies
JIRA archive (LUCENE, SOLR) and mailing lists (dev@, java-user@, issues@, commits@)2026-10-2814 JQL searches, 18 list searches; threads read in full: #5331, #15097, #15124, #12938, #13165, #9147, #9701, #8854, #10801, #14756, #9855, LUCENE-3041related discussions only
GitHub issues and PRs updated since 2026-10-282026-10-3110 searches (QueryVisitor, DoubleValuesSource, ValueSource visit, TooManyNestedClauses, clause count, maxClauseCount, FunctionMatchQuery, QueryValueSource, FunctionScoreQuery, boostByQuery) and the list of all 72 updated items0 hits; none of the 72 items concerns this (that list is from an interactive run two minutes before the scripted one and was checked by title)

History

Introduced in 9.0.0 by LUCENE-8811 (53f56fb7ad4, 2019), which turned the clause limit into one cumulative QueryVisitor walk; the visit() bodies date from 8.1.0 (LUCENE-3041) and were harmless before. Every release 9.0.0 to 10.6.0 and main is affected; 8.x is not.

Versions tested8 rows

Introduced by 53f56fb7ad4 (LUCENE-8811 "Move max clause checks to IndexSearcher", now #9855; a JIRA patch committed by romseygeek on 2019-06-26). It added query.visit(getNumClausesCheckVisitor()) to IndexSearcher.rewrite (IndexSearcher.java:878 on main, by git blame). Its 8.x backport fa3bf88783a was reverted before 8.2.0 (2885a115782, 193c77fe1a0), so it first shipped in 9.0.0.

The fix site is fbd05167f45 (LUCENE-3041, the QueryVisitor API, 2019-03-14), which wrote the four visit() bodies on main as ports of Weight#extractTerms; they have not changed since. 8.1.0 got the same bodies through the separate branch_8x backport ecf86ecab7a. Adding only the LUCENE-8811 check to 8.11.4 brings the bug back (commit isolation).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions on and off (gradle and standalone runner); passes with the fix
10.6.0 affectedminimal test fails (release jars, assertions off)
9.12.3, 10.5.2 affectedminimal test fails (release jars, assertions off)
9.0.0 to 10.5.2 (33 releases) affectedhistory probe: the nested BooleanQuery controls are rejected, 5 of 5 function-query cases accepted
8.1.0, 8.2.0, 8.11.4 latentno cumulative check: the nested controls are accepted too
8.11.4 + only the LUCENE-8811 check isolationcontrols rejected, 5 of 5 function-query cases accepted, as on 9.0.0
upstream main 2f424fd428d, branch_11x readfix-site files identical to 645d4fe14a20
branch_10_6 7e5aec4370e read4 of the 9 fix-site files differ in unrelated code; the bug's code paths are the same and the fix diff applies (not built)

Independent checks

Confirmed by a tie-breaker after the adjudication called it disputed: an independent reproducer and a reviewer from another model family (OpenAI GPT-5.6) said real, the intended-behaviour verifier said uncertain. A credibility audit, the minimization and this doc's gradle run re-ran it; the only "not real" was procedural, from a read-only review.

Checks and verdicts380 words

The cross-model reviews could only read code; every execution result comes from the finder, the independent reproducer, the intended-behaviour verifier, the adjudication, the tie-breaker, the audit, the minimization and this doc.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (bounded-exhaustive differential check, then JUnit) candidate1,440 checks: the five function-query sites under-count in 44, 44, 35, 35 and 35 of 90; controls 0 of 270. Its 5-test JUnit fails 5 of 5 on main, branch_10_6, 10.5.2 and 9.12.3.
Refute review (OpenAI GPT-5.6, read-only) not real (procedural, low)Could not run commands and voted not real on procedure only then; named no flaw in the repro or the root cause. A separate re-run fails 4 of 4.
Independent reproducer realOwn 10-test JUnit (768 + 768 clauses; 32 x boostByQuery, 32,032 clauses): 7 fail on main and 10.5.2 (assertions on and off) and 9.12.3; 3 controls pass.
Works-as-designed lens (OpenAI GPT-5.6, read-only) real (low)Root cause agrees, inputs in contract; the by-design case is moderate but has no written support. Re-runs fail on main, 10.5.2 and 9.12.3.
Intended-behaviour devil's advocate uncertainBuilt the by-design case (extractTerms port, highlighting history, "per Query") and showed that visiting the source as ordinary clauses breaks Monitor (3 of 4 probe tests).
Adjudication disputed (medium)Re-ran the independent reproduction's JUnit on main (10 run, 7 failed, assertions on and off) and sent three questions to a tie-breaker.
Tie-breaker confirmed (medium)Own probe: 3 of 7 fail on main, 10.5.2 and 9.12.3 with 4 controls passing (SHOULD and MUST_NOT clauses are counted; nesting 20, 84, 340 leaf queries at a limit of 4). Showed by execution that a counting-only path exists.
Credibility audit keep (minor)Re-ran the finder's JUnit with a new seed (5 of 5 fail) and the independent reproduction's (7 of 10 fail) on main.
Minimal repro verifiedOne test; fails on 645d4fe14a20 with assertions on and off and two seeds.
This doc (gradle and release jars) confirmed; fix validatedStock fails through gradle with assertions on and off; the recommended fix passes, with the closest suites, core search.*, four modules and lint; the Monitor probe is unchanged. The test also fails on the 9.12.3, 10.5.2 and 10.6.0 jars.

Diagram file

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

Sources and provenance

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

Finding record

LFV-923, snapshot of 2026-11-06

Registry title
IndexSearcher's cumulative maxClauseCount check never counts a query held by a values source (FunctionScoreQuery.boostByQuery's boost query, DoubleValuesSource.fromQuery, QueryValueSource): the function queries' visit() stops at the source and each embedded query is checked alone with a fresh budget, so the total number of executed clauses is unbounded
Area
queries / function queries (FunctionScoreQuery, FunctionMatchQuery, FunctionRangeQuery and FunctionQuery QueryVisitor traversal of DoubleValuesSource/ValueSource; consumer: IndexSearcher's cumulative clause limit)
Priority
Low, rank 733: Function queries hide their inner query from the cumulative maxClauseCount check, so the limit can be bypassed without bound while hits stay correct, and it is the strongest of the new QueryVisitor-hiding rows, so it leads them after LFV-402 and above the 10.6.0 regression LFV-910.
Impact class
minor
Impact note
Applications that build FunctionScoreQuery.boostByQuery (the documented replacement for the removed BoostingQuery), a FunctionScoreQuery or FunctionMatchQuery over DoubleValuesSource.fromQuery, or the @lucene.experimental FunctionQuery/FunctionRangeQuery over QueryValueSource, with the default searcher and the default maxClauseCount (1024), get no TooManyNestedClauses for the embedded query's share of the clauses: each embedded query is checked only on its own, so one function query can run about twice the limit and repetition or nesting multiplies it (measured: 32 boostByQuery clauses with 1,000-clause boost queries run 32,032 term clauses and 100 of them run 100,100, all accepted; at maxClauseCount 4, nesting boostByQuery 1, 2 and 3 levels deep pulls scorers for 20, 84 and 340 leaf queries, all accepted); FunctionScoreQuery runs its boost query only on scoring searches, while FunctionMatchQuery and FunctionRangeQuery always run the embedded query and it decides which documents match; hits and scores are correct, ordinary scale, every release since 9.0.0.
Summary
You search FunctionScoreQuery.boostByQuery(in, boostMatch, 2f), where in and boostMatch are each a 600-term disjunction (1,200 term clauses that all run), with the default maxClauseCount of 1024. You expect TooManyNestedClauses, which you do get for a BooleanQuery holding the same clauses (even when half of them are scoring-only SHOULD or MUST_NOT clauses), because the check counts 'total clauses cumulatively in all of its children' and LUCENE-8811 made it cumulative so that splitting clauses over sub-queries no longer bypasses it. Instead the search runs with correct hits: FunctionScoreQuery.visit visits only in, FunctionMatchQuery, FunctionRangeQuery and FunctionQuery.visit call visitLeaf(this), DoubleValuesSource and ValueSource have no visit hook, and the embedded query is rewritten later with searcher.rewrite, which checks it alone with a fresh count; FunctionScoreQuery(fromQuery), FunctionMatchQuery(fromQuery) and FunctionQuery/FunctionRangeQuery over QueryValueSource behave the same.
Introduced
53f56fb7ad44afab9c78fab2e89737438093247f (LUCENE-8811 'Move max clause checks to IndexSearcher', now apache/lucene#9855, a JIRA patch committed by GitHub romseygeek, no GitHub PR), first released in 9.0.0: it added the cumulative check query.visit(getNumClausesCheckVisitor()) to IndexSearcher.rewrite (IndexSearcher.java:878 on main), which never reaches a query held by a values source; its branch_8x backport fa3bf88783a was reverted by 2885a115782 (branch_8x) and 193c77fe1a0 (branch_8_2) on 2019-07-15, before 8.2.0. The fix site is older and was harmless before: fbd05167f455e3ce2b2ead50336e2b9c2521cd6c (LUCENE-3041 'QueryVisitor', apache/lucene-solr#581, by GitHub romseygeek; branch_8x ecf86ecab7a, released in 8.1.0) wrote the four visit() bodies as ports of Weight#extractTerms, which already skipped the values source [read: git blame and git log -L list only fbd05167f45 for the four visit() bodies and 53f56fb7ad4 for IndexSearcher.java:878; the 107 review comments of apache/lucene-solr#581 never discuss values sources; exec: commit isolation, 8.11.4 plus only the LUCENE-8811 check in a shadow IndexSearcher rejects the nested BooleanQuery controls and accepts 5 of 5 function-query cases, as 9.0.0 does]; not a regression of a check that once worked, so no bisect (2019-06-26 (author date of 53f56fb7ad4, LUCENE-8811, the cumulative check that made the hidden query matter); 2019-03-14 (author date of fbd05167f45, LUCENE-3041, the visit() bodies))
First affected version
9.0.0 [exec] (the first release with the cumulative check: the history probe's nested BooleanQuery controls are rejected while 5 of 5 function-query cases are accepted, and the finder's JUnit (9.0.0 import variant) fails 5 of 5, -da); the visit() bodies exist since 8.1.0 but are latent in 8.x, which has no cumulative check [exec: on 8.1.0, 8.2.0 and 8.11.4 the nested controls are accepted too]; the JUnit's API (TooManyNestedClauses, IndexSearcher.getMaxClauseCount) is absent in 8.11.4 [exec: compile error]
Affected versions
All 33 releases 9.0.0 to 10.5.2 on Maven Central, plus 10.6.0 (the branch_10_6 head 4ae946e5a9b, the RC1 revision) and main a9779ef3c193 [exec: the follow-up's history probe (public API from 8.1 on) fails, 5 of 5 function cases accepted with both controls rejected, on every release 9.0.0 to 10.5.2 (-da) and on main (-ea); the finder's 5-test JUnit fails 5 of 5 on 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 (-da), branch_10_6 (-da) and main (-ea and -da); the independent reproduction's 10-test JUnit fails 7 of 10, with its 3 controls passing, on main and 10.5.2 (each -ea and -da) and 9.12.3 (-da); the tie-breaker's 7-test probe fails 3 of 7, with its 4 controls passing, on main (-ea and -da), 10.5.2 and 9.12.3 (-da)]; latent, not affected, in 8.1.0 to 8.11.4 (no cumulative check) [exec: 8.1.0, 8.2.0, 8.11.4]; upstream main c5ab3e57b9b (author date 2026-10-28) still has the same visit() bodies: the last upstream commits on the fix-site files are #16697 (DoubleValuesSource.java, author date 2026-10-09), #16431 (FunctionScoreQuery.java, 2026-08-22), #15406 (ValueSource.java, 2025-12-02), #13440 (FunctionMatchQuery.java, FunctionRangeQuery.java, FunctionQuery.java, 2024-06-06) and LUCENE-9570 (QueryValueSource.java, 2020-12-28), none touching visit() [read 2026-10-28]; not fixed anywhere. All runs on aarch64 Linux with JDK 25.
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
Upstream and prior reports
none found (the prior-report record, verdict new, searched 2026-10-28, read-only: a scripted prior-report search with two specs, v1 on clause-limit terms (16 GitHub issue/PR, 3 commit, 2 downstream Elasticsearch/OpenSearch/Solr, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 12 web queries; 240 hits, 0 high) and v2 on term-extraction, highlighting and sizeOf terms (61 queries, 2,834 hits, mostly extractTerms history noise); 36 manual GitHub searches, 14 JQL searches over LUCENE and SOLR and 18 mailing-list searches; threads read in full: #5331, #15097, #15124, #12938, #13165, #9147, #9701, #8854, #10801, #14756, #9855 and the LUCENE-3041 JIRA comments, with the 107 review comments of apache/lucene-solr#581 grepped; a changed-file scan of all 413 open apache/lucene PRs (28 match the file filter; the 20 that change main-source files have no visit change in these classes) and of the 69 PRs updated since 2026-10-26), plus the finder's run, the independent reproduction's, the intended-behaviour check's and the tie-breaker's own searches. Refresh 2026-10-28: GitHub search 'FunctionScoreQuery visit' (only #15097, #10801), 'boostByQuery' (#9404, #10801, #9147), 'DoubleValuesSource QueryVisitor' (0), 'QueryValueSource' (9 old items incl. #5331, none on the clause limit), 'FunctionMatchQuery' (7 old items), 'maxClauseCount' and 'QueryVisitor' updated since 2026-10-26 (0), 'visit' updated since 2026-10-27 (two HNSW PRs); of the 40 PRs updated since 2026-10-27 (newest #16931), a changed-file scan finds only #14673 touching FunctionScoreQuery.java and IndexSearcher.java ('Merge 10.1 into 9.11', a 445-file branch-merge PR, closed unmerged, with no visit or clause-check change in its diff). Related, not this bug: LUCENE-4261 (#5331, 2012, closed as not a bug: a maintainer said FunctionQuery is a leaf for the Scorer API and value sources are 'opaque'; a different API, years before QueryVisitor and the cumulative check, and the strongest design-intent argument); #15097 (a 2025-08-22 comment notes that a clause-counting visitor would not catch a FunctionScoreQuery holding a large DoubleValuesSource, in the context of query-cache RAM sizing, with no mention of an embedded Query or the clause limit); #15124 (LRUQueryCache sizes keys with RamUsageEstimator.sizeOf); #12938/#13165 (FunctionQuery treated as score-only for highlighting); LUCENE-8811 (#9855, origin of the cumulative check, 'removes ways to by-pass the maximum clause count'); #14756 and open PR #16175 (the opposite over-count of IndexOrDocValuesQuery; 'for the purpose of visiting, we should visit all variants'); #14759 ('QueryVisitor is supposed to be a query introspection API'); LUCENE-8849 (#9892), LUCENE-9050 (#10092) and LUCENE-9005, the same kind of visit() omission fixed as bugs in other classes; #9147 (LUCENE-8099, boostByQuery replaced BoostingQuery). Gaps: GitHub search is keyword-based (the open-PR file scan finds fixes regardless of wording) and may not index line-review comments; several web searches were throttled and retried until all completed.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner (seeds DEADBEEF and 1234ABCD); with the recommended fix it passes, together with the closest suites and the queries, highlighter, monitor and expressions modules. Release jars 9.12.3, 10.5.2 and 10.6.0: the minimal test fails (assertions off). Earlier, on main a9779ef3c193 (same fix-site code): every release 9.0.0 to 10.5.2 affected, 8.1.0 to 8.11.4 not. Upstream main 2f424fd428d (12.0.0-SNAPSHOT) and branch_11x 603af870375 have the same fix-site files; on branch_10_6 7e5aec4370e and the 10.6.0 tag four of the nine differ in unrelated code, while the bug's code paths are the same and the fix diff applies (read, refs of 2026-10-29/30).

Lucene and upstream sources

Finding record
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:101-106 (boostByQuery wraps boostMatch in DoubleValuesSource.fromQuery), :121 (createWeight: source.rewrite(searcher), only when scores are needed), :124-131 (rewrite rewrites only `in`), :133-136 (visit: in.visit(visitor.getSubVisitor(MUST, this))); FunctionMatchQuery.java:80-83 (visit: visitLeaf), :88 (source.rewrite); FunctionRangeQuery.java:132-135 (visit: visitLeaf), :42 (@lucene.experimental); FunctionQuery.java:92-95 (visit: visitLeaf), :38 (@lucene.experimental); valuesource/QueryValueSource.java:82-86 (createWeight: searcher.rewrite(q)); lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:741 (fromQuery), :776-780 (QueryDoubleValuesSource.rewrite: searcher.rewrite(query)); IndexSearcher.java:255 (setMaxClauseCount 'per Query'), :871-880 (rewrite runs getNumClausesCheckVisitor at :878), :896-932 (the visitor; :902-903 'Return this instance even for MUST_NOT'), :1193-1199 (TooManyNestedClauses 'total clauses cumulatively in all of its children'); Query.java:88-93 ('Recurse through the query tree, visiting any child queries'); lucene/CHANGES.txt:4664-4667 (LUCENE-8811 'apply to all queries'); lucene/MIGRATE.md:1533
  • https://github.com/apache/lucene/issues/15097#issuecomment-3212567936 (sgup432, 2025-08-22T00:31:39Z; read via gh api 2026-10-28): 'if a BooleanQuery has FunctionScoreQuery as one of the clause, FunctionScoreQuery holds DoubleValuesSource where a user can erroneously a huge sized source ... the number of nested or child clauses may be small, but the overall query size can still be large' (query-cache sizing context)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:587 (at 645d4fe14a20) (:601), :623-624, :750-756 (:753), :871-880 (:878), :882-889 (:884), :891-932 (:902-905; the tests before the increments :909-912, :917-920, :926-929), :1193-1199; call path also (IndexSearcher.rewrite:884 <- IndexSearcher.search:753 <- IndexSearcher.searchAfter:601 <- IndexSearcher.search:624)
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:101-106, (at 645d4fe14a20) :109-122 (:118-121), :124-131, :133-136, :315-317, :394-396
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java:80-83, (at 645d4fe14a20) :88; FunctionRangeQuery.java:132-135, :149; FunctionQuery.java:71, :92-95; valuesource/QueryValueSource.java:82-86
  • lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:741-743, (at 645d4fe14a20) :776-780 (:779); grep: no visit method in DoubleValuesSource.java or lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java
Who is affected
  • lucene/MIGRATE.md:1377-1381 (at 645d4fe14a20) (BoostingQuery replaced by boostByQuery); lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:38, FunctionRangeQuery.java:42 (@lucene.experimental); lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:75-86
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:118-120 (at 645d4fe14a20) (non-scoring searches return the inner weight)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:255, (at 645d4fe14a20) :903, :1193-1199; lucene/CHANGES.txt:4681-4684, :5800 (LUCENE-8849, 8.6.0 Bug Fixes), :6163-6164 (LUCENE-9050, 8.3.1 Bug Fixes); lucene/MIGRATE.md:1531-1534; lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:75-76; lucene/core/src/java/org/apache/lucene/search/Query.java:88-93; lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:753-764, :782-785
  • lucene/core/src/test/org/apache/lucene/search/TestMaxClauseLimit.java (at 645d4fe14a20) (test methods at :30-150)
Suggested fix
  • lucene/highlighter/src/java/org/apache/lucene/search/highlight/QueryTermExtractor.java:144-154 (at 645d4fe14a20) (MUST_NOT sub-visitor kept when includeProhibited)
  • Lucene main 645d4fe14a20: grep finds no getValuesSourceVisitor anywhere in lucene/ (the name is free)

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro and an executed probe on the stock main jars (the two clause checks and their counts, the scores, the BooleanQuery control).

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