All findings · Lucene finding · LFV-584 · querybuilder-multiphrase-drops-token-boosts

QueryBuilder drops every token boost from a quoted phrase when one of its words has a single-word synonym

Since 8.5, QueryBuilder applies token boosts (typed word|0.5, weighted synonyms) to phrases, except when a word of the phrase has a single-word synonym and none has a multi-word one: then "fast|0.5 car" scores exactly like "fast car". The same documents match; only the ranking is wrong, with no error.

Summary

Priority
Low, rank 374Ranking-only, and only behind an opt-in boost-emitting query analyzer.
Impact
silent wrong results
Since
8.5.0 (LUCENE-9171 added token boosts to every QueryBuilder path except this one)introduced 2020-02-24 · about 6 years
Affected
8.5.0 to 10.6.0 and main (all 52 releases to 10.5.2 run; 10.6.0 by reading)
Status
documented
Found by
a bounded-exhaustive check of QueryBuilder against a reference model, then JUnit tests

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

When a word of a quoted phrase has a single-word synonym, QueryBuilder builds a MultiPhraseQuery through a method that never reads token boosts, so every boost of the phrase is lost. Plain phrases and phrases with multi-word synonyms keep them.

Diagram of the bug. Query analyzer: WhitespaceTokenizer, DelimitedBoostTokenFilter('|'), SynonymGraphFilter with car, automobile and guinea pig, cavy; BM25. createPhraseQuery of "fast|0.5 tiger" goes to analyzePhrase and builds (body:"fast tiger")^0.5, ratio 0.5, correct. "fast|0.5 guinea pig" goes to analyzeGraphPhrase and builds (body:"fast cavy")^0.5 (body:"fast guinea pig")^0.5, ratio 0.5, correct. "fast|0.5 car" goes to analyzeMultiPhrase and builds body:"fast (automobile car)", ratio 1.0, wrong. For the document "fast car" the expected score is 0.1308 and the actual 0.2615: the 0.5 is lost. The same documents match and there is no error; weighted synonyms (cat, kitty|0.5) are lost in quotes too. Reached by createPhraseQuery, the classic QueryParser, MultiFieldQueryParser and SimpleQueryParser (all executed). Why: createFieldQuery sends a quoted, non-graph phrase with a stacked token to analyzeMultiPhrase, which never reads BoostAttribute; analyzePhrase multiplies the token boosts into a BoostQuery and analyzeGraphPhrase builds each path the same way. Fix sketch, for equal boosts per position: wrap the phrase in a BoostQuery of the product and keep the parser's explicit slop. Affected: every release from 8.5.0 through 10.5.2 and main.
The same boost 0.5 on fast in three quoted phrases: the plain phrase and the multi-word-synonym phrase keep it, the phrase where car has the single-word synonym automobile loses it and scores 0.2615 instead of 0.1308. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Your query analyzer turns word|weight into a token boost (the stock DelimitedBoostTokenFilter) and knows the synonym car, automobile. You search the quoted phrase "fast|0.5 car" with QueryBuilder.createPhraseQuery or the classic QueryParser.
  2. You expect half the score of "fast car", as without a synonym: "fast|0.5 tiger" becomes (body:"fast tiger")^0.5 and scores 0.1308 instead of 0.2615.
  3. Instead car has a stacked synonym, so QueryBuilder builds a MultiPhraseQuery in analyzeMultiPhrase, which never reads the boost: body:"fast (automobile car)" scores 0.2615, as if you had typed no boost.
  4. Weighted synonyms are lost the same way: with cat, kitty|0.5 the unquoted cat food ranks the document kitty food lower (0.2867 vs 0.3979), but the quoted "cat food" scores both 0.7130.
  5. No error, and the matching documents do not change. Every release from 8.5.0 to 10.6.0 (10.6.0 by reading) and main is affected.

Who is affected

Applications whose query analyzer emits token boosts through the stock DelimitedBoostTokenFilter (weighted synonyms, or typed word|weight) and that build phrases with QueryBuilder or the classic, multi-field or simple query parser. Every quoted phrase in which a word has a single-word synonym is hit, deterministically. Ranking only; the boosting analyzer is opt-in.

Mechanism, terms and numbers739 words

Since 8.5.0 (LUCENE-9171) QueryBuilder reads the BoostAttribute of query tokens, which the stock DelimitedBoostTokenFilter sets from word|weight input, so typed boosts and weighted synonyms such as cat, kitty|0.5 reach the parsed query. For a phrase it multiplies the token boosts: "fast|0.5 tiger" becomes (body:"fast tiger")^0.5, and a phrase with a multi-word synonym becomes one boosted query per path. But when a quoted phrase analyzes to more than one position with a stacked token (a word with a single-word synonym) and no token graph (no multi-word synonym), createFieldQuery builds a MultiPhraseQuery in analyzeMultiPhrase, which never reads BoostAttribute, and every boost of the phrase is silently dropped: "fast|0.5 car" with car, automobile becomes the unboosted body:"fast (automobile car)" and scores 0.2615 instead of 0.1308, and with cat, kitty|0.5 the quoted "cat food" scores kitty food the same as cat food (0.7130 both), although the unquoted query ranks it lower. The classic QueryParser shows the same. The bug changes no match set (a boost only scales scores); only scores and ranking are wrong. Every release from 8.5.0 to 10.6.0 (10.6.0 by reading) and main; the boost-emitting query analyzer is an opt-in choice. A 28-line fix (boost the MultiPhraseQuery like analyzePhrase does, and let the classic parser's explicit-slop rewrite look inside the BoostQuery) fixes the lost boost; the closest suites pass, and only the weighted-synonym test still fails, which this fix leaves to an expansion or a javadoc note by design.

Terms

  • Token boost: a weight attached to one query token through BoostAttribute. The stock DelimitedBoostTokenFilter (SPI name delimitedBoost) sets it from word|weight and strips the suffix: fast|0.5 becomes the token fast with boost 0.5. With a synonym file such as cat, kitty|0.5 placed before it in the chain, it gives weighted synonyms.
  • Single-word synonym: car, automobile adds automobile at the same position as car (position increment 0, a stacked token). A multi-word synonym such as guinea pig, cavy makes a token graph instead.
  • QueryBuilder: turns analyzed query text into a Query. The classic QueryParser, MultiFieldQueryParser and SimpleQueryParser build their queries through it. For a quoted phrase it picks one of three builders: analyzePhrase (no stacked token), analyzeGraphPhrase (a token graph) or analyzeMultiPhrase (a stacked token and no graph).

The bug

  1. createFieldQuery marks hasSynonyms when a token has position increment 0 and sends a quoted, non-graph phrase with such a token to analyzeMultiPhrase, any other quoted, non-graph phrase to analyzePhrase (QueryBuilder.java:347-355).
  2. analyzePhrase multiplies the token boosts, phraseBoost *= boostAtt.getBoost(), and returns new BoostQuery(query, phraseBoost) when the product is not 1 (:436-461). analyzeGraphPhrase sends each path of the graph back through createFieldQuery, so a path of several tokens is built by analyzePhrase and keeps its product, and a one-token path is built by analyzeTerm, which reads the boost too (:565-583).
  3. analyzeMultiPhrase collects the terms of each position into a MultiPhraseQuery and returns it as is; it never adds or reads BoostAttribute (:463-497). Every other analyze* method reads it, directly or (for analyzeGraphPhrase) through the builders it calls.
  4. So whether a boost survives depends on the builder: in practice, on whether some word of the phrase has a single-word synonym (and none a multi-word one). Two cases: (a) a boost on a word without a synonym is lost because another word has one, the clear-cut case; (b) weighted single-word synonyms are ignored inside quotes, weaker because MultiPhraseQuery has no per-term weights.
  5. A boost only scales scores, so the matched documents are the same: on 14,376 multi-phrase-path queries (main with and without assertions, 10.5.2) the match sets never differed and 6,461 scores did (executed).

The numbers (main a9779ef3c193, executed)

Stock WhitespaceTokenizer, DelimitedBoostTokenFilter and SynonymGraphFilter, BM25, small indexes:

  • No synonym: "fast tiger" gives body:"fast tiger", score 0.2615; "fast|0.5 tiger" gives (body:"fast tiger")^0.5, score 0.1308.
  • Multi-word synonym guinea pig, cavy: "fast|0.5 guinea pig" gives (body:"fast cavy")^0.5 (body:"fast guinea pig")^0.5, half the score of the unboosted phrase.
  • Single-word synonym car, automobile: "fast|0.5 car" gives body:"fast (automobile car)", score 0.2615 (expected 0.1308).
  • Weighted synonym cat, kitty|0.5, unquoted cat food: Synonym(body:cat body:kitty^0.5) body:food, cat food 0.3979 and kitty food 0.2867.
  • Quoted "cat food": body:"(kitty cat) food", both documents 0.7130.
  • Classic QueryParser with tv => television|0.9: "tv food" gives (body:"television food")^0.9, but "tv car" gives body:"television (automobile car)", without the 0.9.
Exposure and likelihood199 words
  • Who. Users of QueryBuilder.createPhraseQuery, of quoted phrases in the classic QueryParser (executed) and in MultiFieldQueryParser and SimpleQueryParser (executed by the fact check), or of autoGeneratePhraseQueries, whose query-time analyzer sets token boosts. The only stock token-stream producer of BoostAttribute is DelimitedBoostTokenFilter: weighted synonyms from the synonym file, or user-typed word|weight when the tokenizer keeps the delimiter.
  • When. Every quoted (or auto-generated) phrase that analyzes to more than one position with a stacked token and no token graph (in practice: some word has a single-word synonym and none a multi-word one), deterministically. A single-word synonym such as car, automobile is an ordinary synonym-file entry.
  • Effect. Silent wrong scores and ranking: the phrase's typed boosts and synonym weights are ignored. The matched documents are unchanged, so filters, deny rules and counts built from such a query are not affected; an application that drops hits below a score threshold can still show a different result list. No error.
  • Default vs opt-in. Default writer, codec, similarity, searcher and parsers; the boost-emitting query analyzer is an opt-in choice.
  • Scale. None needed: one document and one phrase show it.
  • In practice a low-severity ranking defect; the searches found no report since 8.5.0 (2020).

Minimal repro

The test shows that QueryBuilder.createPhraseQuery drops the token boost of a phrase once one of its words has a single-word synonym: "fast|0.5 car" with the synonym car -> automobile builds an unboosted MultiPhraseQuery, while "fast|0.5 tiger" gives (body:"fast tiger")^0.5.

TestQueryBuilderMultiPhraseBoost.java (JUnit test)49 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.util;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.boost.DelimitedBoostTokenFilter;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.analysis.synonym.SynonymGraphFilter;
import org.apache.lucene.analysis.synonym.SynonymMap;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestQueryBuilderMultiPhraseBoost extends LuceneTestCase {

  public void testPhraseBoostKeptWhenAWordHasASynonym() throws Exception {
    SynonymMap.Builder synonyms = new SynonymMap.Builder();
    synonyms.add(new CharsRef("car"), new CharsRef("automobile"), true);
    SynonymMap map = synonyms.build();
    Analyzer analyzer =
        new Analyzer() {
          @Override
          protected TokenStreamComponents createComponents(String fieldName) {
            Tokenizer tokenizer = new WhitespaceTokenizer();
            return new TokenStreamComponents(
                tokenizer,
                new SynonymGraphFilter(new DelimitedBoostTokenFilter(tokenizer, '|'), map, true));
          }
        };
    Query q = new QueryBuilder(analyzer).createPhraseQuery("body", "fast|0.5 car");
    // "fast|0.5 tiger" gives (body:"fast tiger")^0.5; expected boost 0.5 here too, got 1.0
    assertEquals(q.toString(), 0.5f, q instanceof BoostQuery bq ? bq.getBoost() : 1f, 0f);
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-584/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.util.TestQueryBuilderMultiPhraseBoost
Expected vs actual
Expected boost 0.5; actual 1.0 (body:"fast (automobile car)" expected:<0.5> but was:<1.0>), with -ea and -da.

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

More files, environment, full output36 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: body:"fast (automobile car)" expected:<0.5> but was:<1.0>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:577)
       org.apache.lucene.util.TestQueryBuilderMultiPhraseBoost.testPhraseBoostKeptWhenAWordHasASynonym(TestQueryBuilderMultiPhraseBoost.java:47)

Why it looks unintended

  • LUCENE-9171 (8.5.0) made QueryBuilder apply token boosts to parsed queries; plain and graph phrases multiply them.
  • The boost on fast is kept in "fast tiger" and "fast guinea pig" but dropped in "fast car", only because car has a single-word synonym.
  • The change that added the feature skipped only analyzeMultiPhrase; no test, doc or discussion calls that a choice.
The full argument238 words
  • The contract. CHANGES 8.5.0, LUCENE-9171: "QueryBuilder can now use BoostAttributes on input token streams to selectively boost particular terms or synonyms in parsed queries" (CHANGES.txt:5948-5949).
  • QueryBuilder's own rule. analyzePhrase multiplies the token boosts and graph phrases keep them per path; the same boosted token is honoured or dropped depending only on whether an unrelated word has a single-word synonym ("tv food" keeps ^0.9, "tv car" drops it, executed through the classic parser).
  • The feature author meant phrases. In the review of apache/lucene-solr#357, a reviewer (romseygeek) wrote "we need to combine boosts together somehow; currently your phrase boost is just the boost of the last term in the phrase", and the author (alessandrobenedetti) answered "I implemented a simple multiplicative boost ... it's also compatible in not synonym cases, if the user needs a boost per token in phrase and span queries". MultiPhraseQuery is never mentioned in that PR, in LUCENE-9171 (#10211) or in SOLR-12238.
  • History: an omission. Commit 663611c99c7 added BoostAttribute handling to analyzeTerm, analyzeBoolean, analyzeMultiBoolean, analyzePhrase, analyzeGraphBoolean and createSpanQuery, but not to the older analyzeMultiPhrase (2013/2015), which has had no boost-related change since.
  • Tests. The only upstream test of token-stream boosts through QueryBuilder, TestQueryBuilder#testTokenStreamBoosts, covers createBooleanQuery only (TestDelimitedBoostTokenFilter tests the filter's own output); no test asserts the unboosted MultiPhraseQuery.
  • Legal trigger. Public API, the stock DelimitedBoostTokenFilter and SynonymGraphFilter in the documented order, the classic parser.
The devil's-advocate check481 words

The strongest case for "intended" (the works-as-designed reviewer, the intended-behaviour checker, the adjudication, and a committer's own words): BoostAttribute is @lucene.internal and its javadoc says it is "intended to be added only by the TermsEnum to itself in its constructor and consumed by the MultiTermQuery.RewriteMethod". A committer, rmuir, restated this in 2023 in the review of #12169 (the Word2VecSynonymFilter): "BoostAttribute is not for use in tokenstreams. it is for multitermquery." (review comment 1116841545, 2023-02-24); "This is wrong, wrong wrong. Remove any use of boostattribute outside of MultiTermQuery. It documents not to do this." (review comment 1121546026, 2023-03-01); "It is really important that this only be used by fuzzyquery, it is only for multitermquery. That's why it is documented as such." (issue comment 1467945170, 2023-03-14). The filter merged without setting it, and its author's follow-up, open #12433, proposes a separate token-stream boost attribute next to a renamed MultiTermQuery one. Further, createPhraseQuery's javadoc lists the query types it returns and promises nothing about boosts; the CHANGES entry only says "can now use"; LUCENE-9171 was scoped to term and synonym queries (a reviewer suggested to "leave the graph queries alone for the moment, as boosting there is more complex"); and MultiPhraseQuery cannot weight alternatives. So a maintainer may well answer that token streams should not set this attribute at all. A 2011 proposal, LUCENE-3130 (#4203), already said the multi-term synonym phrase case would need a decision ("possibly just punt for now").

Why it fails: the javadoc conflicts with the shipped feature. LUCENE-9171 made token-stream boosts documented QueryBuilder input (the CHANGES 8.5.0 entry), with a public SPI filter and a reference-guide recipe, and QueryBuilder still reads the attribute on every other path at a9779ef3c193. rmuir's statements argue against the feature as a whole, not for dropping boosts on one path only: while the feature ships, a boost kept on one path and dropped on another is a bug; if the maintainers would rather retire or move token-stream boosts (the direction of #12433), the report can point to that decision. The author stated the multiplicative rule is for "a boost per token in phrase and span queries", and the final commit applied it to plain and graph phrases despite the "leave graph alone" remark. No doc, test or discussion records the multi-phrase omission as a choice. For case (a) a BoostQuery around the MultiPhraseQuery expresses the rule exactly, so "MultiPhraseQuery cannot weight alternatives" does not apply. LUCENE-3130 predates the code by nine years and describes no implemented behaviour. For (b) the case for "intended" is stronger (the cross-model fact check rates it moderate): MultiPhraseQuery cannot weight alternatives, and taking the largest, averaging, expanding or rejecting differing weights are all different policies. Verdict: not intended. High confidence for (a); (b) may be treated upstream as an enhancement or a documentation note, so file (a) as the bug and offer (b) as fix-or-document.

Suggested fix

Recommended Boost the MultiPhraseQuery by the product of the token boosts, and keep the explicit slop (validated)

Boost the MultiPhraseQuery like analyzePhrase does, by the product over positions of each position's largest token boost (+19/-2 in QueryBuilder), and let the classic parser apply an explicit ~N inside the BoostQuery (+9 in QueryParserBase). The lost boost is fixed and the closest suites pass, except the weighted-synonym test: weighted single-word synonyms inside quotes stay unweighted.

BoostAttribute boostAtt = stream.addAttribute(BoostAttribute.class);
float phraseBoost = DEFAULT_BOOST;
float positionBoost = Float.NEGATIVE_INFINITY;
// ... at each new position, after mpqb.add(...):
  phraseBoost *= positionBoost;
  positionBoost = Float.NEGATIVE_INFINITY;
// ... for every token:
  positionBoost = Math.max(positionBoost, boostAtt.getBoost());
// after the loop:
phraseBoost *= positionBoost;
Query query = mpqb.build();
return phraseBoost == DEFAULT_BOOST ? query : new BoostQuery(query, phraseBoost);
All options and trade-offs4 options · 1,121 words

One fix was applied in a scratch worktree at a9779ef3c193 and run through gradle; the hunt's expansion sketch was run by shadowing and through gradle; the other options were not run. Lead the PR with case (a); offer (b) as expansion or documentation.

Boost the MultiPhraseQuery by the product of the token boosts, and keep the explicit slop (validated) (recommended)

Part A, QueryBuilder.analyzeMultiPhrase: read BoostAttribute; each position contributes the largest boost of its stacked terms; the phrase boost is the product over positions; wrap the MultiPhraseQuery in a BoostQuery when it is not 1. When the terms at each position share one boost (typed boosts on words without synonyms, unweighted synonyms), this is exactly analyzePhrase's rule. Part B, QueryParserBase.getFieldQuery(String, String, int): the classic parser re-applies an explicit ~N only to a bare PhraseQuery or MultiPhraseQuery (QueryParserBase.java:420-432), so once part A wraps the phrase, "fast|0.5 car"~3 would lose the ~3 that stock keeps; part B adds a BoostQuery branch that applies the slop to the wrapped phrase, as MultiFieldQueryParser.applySlop has done since 9.6.0 (#12196). Part B also fixes an older, separate gap: a boosted plain phrase such as "fast|0.5 tiger"~3 loses its ~3 on stock (tracked separately in this hunt's list as the classic parser's explicit-slop bug). A maintainer may want part B as its own commit or issue. If that bug's own fix (pass the explicit slop into the analysis) lands first, part B is not needed. A PR also needs a CHANGES entry.

BoostAttribute boostAtt = stream.addAttribute(BoostAttribute.class);
float phraseBoost = DEFAULT_BOOST;
float positionBoost = Float.NEGATIVE_INFINITY;
// ... at each new position, after mpqb.add(...):
  phraseBoost *= positionBoost;
  positionBoost = Float.NEGATIVE_INFINITY;
// ... for every token:
  positionBoost = Math.max(positionBoost, boostAtt.getBoost());
// after the loop:
phraseBoost *= positionBoost;
Query query = mpqb.build();
return phraseBoost == DEFAULT_BOOST ? query : new BoostQuery(query, phraseBoost);

Trade-offs

  • Match-neutral (a BoostQuery only scales scores) and cheap: one float per position, no extra clauses. No benchmark was run. It still changes the query object: its shape, toString, equals (so query-cache identity) and explanations; and a zero product gives a ^0.0 query, which BoostQuery.rewrite turns into a constant-score one (BoostQuery.java:92-94).
  • Leaves (b): when the terms at one position carry different boosts, the largest counts, so "cat food" with kitty|0.5 still scores both documents 0.7130 (executed). The same holds for a typed boost on the word that has the synonym, when the boost filter runs before the synonym filter: the injected synonym gets the default boost 1, so "fast car|0.5" stays unboosted (executed). The chosen boost is at least the boost of every combination of alternatives; taking the largest is one policy among several (averaging, the exact expansion below, rejecting differing weights), and a maintainer may prefer another.
  • Behaviour change by design: boosted phrases with a single-word synonym score differently after the upgrade; part B also makes "fast|0.5 tiger"~3 keep its ~3.
  • Zero, negative and non-finite token boosts on a word without a synonym now behave as on the plain-phrase path (executed): fast|0 gives ^0.0, fast|-1 and fast|NaN throw IllegalArgumentException from BoostQuery, where stock silently drops them. On the word with the synonym (boost filter first) the largest boost of the position counts, so the synonym's default 1 hides a zero or negative boost: "fast car|0" and "fast car|-1" stay unboosted, where "fast tiger|0" gives ^0.0 and "fast tiger|-1" throws; "fast car|NaN" and "fast car|Infinity" still throw (executed).

What the checks validated

  • Stock main a9779ef3c193: core 1 of 3 fail (both controls pass), analysis-common 2 of 2, queryparser 3 of 3, with assertions on and off.
  • With the fix, assertions on and off: core 3 of 3 and queryparser 3 of 3 pass; analysis-common: case (a) passes, case (b) still fails, as designed.
  • Closest suites with the fix (seed DEADBEEF): TestQueryBuilder 26 tests and the repro's 3, 0 failures; the analysis-common boost and synonym packages, 13 classes, 124 tests, 1 failure (case (b)); the whole lucene/queryparser module, 31 classes, 536 tests, 0 failures (1 test skipped by @Ignore upstream); the UnifiedHighlighter tests, 7 classes, 439 tests, 0 failures.
  • tidy changes nothing; ecjLint, checkGoogleJavaFormat and renderJavadoc of lucene/core and lucene/queryparser pass; the diff applies cleanly to a9779ef3c193.
  • Probe on the main jars with the fix shadowed in: "fast|0.5 car" gives (body:"fast (automobile car)")^0.5 and 0.1308; the classic parser gives (body:"fast (automobile car)"~3)^0.5 for "fast|0.5 car"~3.
  • Not run: other modules' suites, Solr, benchmarks.

Exact weights: expand into one boosted phrase per combination when the boosts at a position differ (the hunt's sketch) (validated by the checks)

Uniform positions as above; when the terms at a position carry different boosts, build a BooleanQuery of one boosted PhraseQuery per combination of alternatives, as analyzeGraphPhrase does per path. This also fixes (b): "cat food" with kitty|0.5 becomes (body:"kitty food")^0.5 body:"cat food" and scores cat food 0.3979 and kitty food 0.1990, the order of the unquoted query (executed).

Trade-offs

  • The query grows with the product of the alternatives: 10 weighted words give 1,024 clauses, and 11 or more throw TooManyClauses at the default limit, where the phrase parses today (executed).
  • With slop above 0 it matched a strict superset of today's documents in 14 of 4,809 probe cases; the three printed ones are sloppy and each repeats a term across positions (executed).
  • The classic parser's explicit slop is lost for such phrases even with part B, because the result is a BooleanQuery: "cat food"~3 gives (body:"kitty food")^0.5 body:"cat food", where stock keeps ~3 (executed). The same gap exists today for graph phrases.

What the checks validated

Shadowed onto the main jars: the hunt's repro passes and upstream TestQueryBuilder passes 26 of 26. Through gradle with part B, assertions on: the doc's three tests pass (8 of 8, case (b) included), TestQueryBuilder 26 of 26, the analysis-common boost and synonym packages 124 of 124, and the whole lucene/queryparser module 536 tests with 0 failures.

Document that stacked terms share one weight (not run)

Add to the createPhraseQuery (or analyzeMultiPhrase) javadoc that the terms stacked at one position of a phrase share one weight, so weighted single-word synonyms are not weighted inside quotes.

Trade-offs

Documentation only; pairs with the recommended fix and leaves (b) as a known limitation.

Instead of part B: pass the explicit slop into the analysis (not run here)

Set the parser's phrase slop to the explicit ~N around the analysis in getFieldQuery(String, String, int) and restore it afterwards, so QueryBuilder builds every phrase shape with the right slop. This is the fix proposed for the classic parser's explicit-slop bug tracked separately in this hunt's list.

Trade-offs

Also covers graph phrases and the expansion option; it changes two pinned TestQueryParser assertions, according to that bug's validation.

Downstream note

Downstream, Solr's standard and edismax parsers inherit this method unchanged and the Solr Reference Guide documents the weighted-synonym chain, while Elasticsearch's match query overrides it (both read in their source, not run).

How it was found

A bounded-exhaustive check ran the real QueryBuilder against an independent reference model on every well-formed token stream of up to 3 tokens (position increments 0-2, position lengths 1-2, boosts 0.5, 1 and 2): 739,080 checks. No phrase built as a MultiPhraseQuery from boosted tokens kept its boosts (35,244 flagged checks); JUnit tests with stock filters confirmed it.

The method in full240 words

Method: bounded-exhaustive translation validation on the real jar (semi-formal tier), in a hunt unit on QueryBuilder reference semantics.

  • Scope C: every well-formed token stream of at most 3 tokens over {a, b, c} (19,458 of 106,956 enumerated; the other 87,498 are ill-formed graphs and were skipped) with position increments 0-2, position lengths 1-2 and BoostAttribute values {0.5, 1, 2}, run through every QueryBuilder entry point (Boolean, minimum-should-match and phrase queries at several slops) on the stock main jar and compared, structurally and by the matched documents, with an independent reference model: 739,080 checks in about 10 s.
  • A structural boost check flagged every createPhraseQuery result that is a MultiPhraseQuery while some token carries a boost other than 1: 35,244 checks at slop 0 (26,820 + 8,424), class MULTIPHRASE-BOOST-DROPPED. The check is structural, so it also counts streams whose boosts multiply to 1, where nothing is lost. Plain phrases, and graph phrases without a hole inside a multi-word synonym, kept their boosts. The run's other classes are not this bug: boosts or matches lost around a hole inside a multi-word synonym (the known upstream #9185 family) and an IllegalArgumentException from SynonymQuery, registered separately as LFV-588.
  • Then public-API JUnit tests on the stock jar with SynonymGraphFilter and DelimitedBoostTokenFilter confirmed it by scores. Found 2026-10-26 on main a9779ef3c193; then checked by an independent reproducer, an intended-behaviour devil's advocate, reading-only reviews by a model from another family (OpenAI GPT-5.6) and an adjudication.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-26: 89 tool queries, 39 manual GitHub and 13 JIRA searches, the mailing lists, the web, and all 406 open PRs). Closest: LUCENE-9171 and its PR, which added the feature without discussing this path, and LUCENE-3130 (2011), a proposal that predates the code.

Searches5 searches

Verdict new: 172 distinct tool hits over the two runs (41 and 167), 0 of high overlap, none the same bug. LUCENE-9171 (#10211) and apache/lucene-solr#357 introduced the feature and never mention analyzeMultiPhrase; LUCENE-3130 (#4203, open since 2011) said the multi-term synonym phrase case would need a decision ("possibly just punt for now"), nine years before the code, and does not cover a boost lost on a word without synonyms. Related, not this bug: #12169 (the Word2VecSynonymFilter PR, where a committer objected to any use of BoostAttribute in token streams; no phrase case; see the devil's advocate above), open #12433 (proposes a separate token-stream boost attribute and renames the MultiTermQuery one, QueryBuilder included; does not touch analyzeMultiPhrase), LUCENE-8943 / #9986 (MultiPhraseQuery IDF, not boosts). None of the open PRs that touch the files changes analyzeMultiPhrase. Keyword search cannot prove absence.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch and Solr repositories; dev@, java-user@, issues@ and commits@; the LUCENE JIRA archive2026-10-26the hunt's prior-report tool in two runs: the finder's (35 queries, 41 hits) and the follow-up's (54 queries, 167 hits)172 distinct hits, 0 of high overlap in either run; the introducing feature only
GitHub issues and PRs (apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch)2026-10-2639 manual searches and 4 code searchesno report
Open apache/lucene PRs2026-10-26changed files of all 406 open PRs#12295, #12433, #14262, #14716, #16851 and #16911 touch the files (#16911 only because it deletes branch_10x's files); none changes analyzeMultiPhrase
JIRA (SOLR, LUCENE)2026-10-2613 manual JQL queries; SOLR-12238 (32 comments) and LUCENE-3130, -8652, -9823, -9283 readno report
Mailing lists and the web2026-10-26java-user@, dev@, users@solr, solr-user@; 8 web searches; the Solr Reference Guide's weighted-synonym sectionno report; the guide documents no phrase limitation

History

Introduced in 8.5.0 by 663611c99c7 (LUCENE-9171 / SOLR-12238, merged 2020-02-24), which added token boosts to every QueryBuilder path except analyzeMultiPhrase. Executed: 8.0.0-8.4.1 ignore token boosts everywhere, so they are not affected; all 52 releases from 8.5.0 to 10.5.2 and main are.

Versions tested6 rows

Introduced by 663611c99c7 "[SOLR-12238] Synonym Queries boost (#357)" (apache/lucene-solr#357; SOLR-12238 and LUCENE-9171 = apache/lucene#10211), merged to master on 2020-02-24 and backported to branch_8x as 2752d50dd1d the same day; first released in 8.5.0. It made QueryBuilder read BoostAttribute in analyzeTerm, analyzeBoolean, analyzeMultiBoolean, analyzeGraphBoolean and createSpanQuery, made the analyzePhrase boost the product of the token boosts, and added DelimitedBoostTokenFilter. analyzeMultiPhrase dates from LUCENE-5261 (2013) and LUCENE-6401 (2015); git log -L shows 8 commits on it in total (6 after LUCENE-6401), none about boosts. Found by reading the history (git log -S, git log -L), not a bisect; the release sweep agrees.

On 8.x graph phrases were span queries (LUCENE-9207, which made them one boosted PhraseQuery per path, is 9.0-only), so the hunt's second test stops at its graph-phrase sanity check there; the portable test covers that case. Unfixed: upstream main a95c754ee00, branch_10_6 and branch_10x have the same analyzeMultiPhrase (read, 2026-10-26); re-checked on 2026-10-28 on upstream main 20ced71c1e6 (2026-10-27) and both branches: the method is byte-identical, and neither QueryBuilder.java nor QueryParserBase.java changed since a9779ef3c193.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthe doc's tests via gradle: core 1 of 3, analysis-common 2 of 2, queryparser 3 of 3 fail, assertions on and off; the hunt's and the reproducer's tests fail too
10.0.0 to 10.5.2 (12 releases) affectedportable test: both controls pass, both bug tests fail; the hunt's test fails 2 of 2 on 10.0.0 and 10.5.2
9.0.0 to 9.12.3 (21 releases) affectedportable test: same pattern; the hunt's test fails 2 of 2 on 9.0.0, 9.1.0 and 9.12.3
8.5.0 to 8.11.4 (19 releases) first affectedportable test: same pattern and the same scores (0.2615 for "fast|0.5 car", 0.7130 both for "cat food")
8.0.0 to 8.4.1 not affectedQueryBuilder ignores token boosts on every path (both controls fail), so nothing is inconsistent
recommended fix on a9779ef3c193 fixcase (a) fixed (core 3 of 3, queryparser 3 of 3); case (b) left as designed

Independent checks

An independent reproducer (3 of 5 fail, 2 controls pass), an intended-behaviour devil's advocate and a classic-parser test from the works-as-designed review reproduced it. Reading-only reviews by a model from another family (OpenAI GPT-5.6) agreed on the root cause. An adjudication confirmed it, and this doc's gradle runs reproduce it on a9779ef3c193.

Checks and verdicts431 words

Every executing check wrote or re-ran its own test on stock main; every control passed.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateExhaustive check (739,080 cases); JUnit 2 of 2 fail with assertions on and off, on 10.5.2, 9.12.3, 9.0.0 and 8.5.0 (there test 2 stops at a sanity check); the fix sketch passes, with upstream TestQueryBuilder 26 of 26.
independent reproducer realOwn test from the claim only (score ratio of boosted to unboosted query): 3 of 5 fail on main (assertions on and off), 10.5.2 and 9.12.3; both controls pass; the classic parser too. A diagnostic patch fixes case (a) and, as expected, not (b).
cross-model reviews (refute, intended) not real (low, procedural); real (medium)An independent reviewer from another model family (OpenAI GPT-5.6) reviewed by reading only, since it could not run Java. The refute lens called the report "technically credible" and agreed on the root cause; the intended lens rated the works-as-designed case moderate but outweighed by the sibling paths. A separate classic-parser test fails 1 of 1 ("tiger story" with tiger, tigre|0.9 gives f:"(tigre tiger) story"); a separate run re-ran the repro: 2 of 2 fail.
intended-behaviour devil's advocate real (high for (a), medium overall)Built the strongest intended case (@lucene.internal attribute, javadoc silence, scope of LUCENE-9171) and showed why it fails; read the introducing PR review; its reachability test through the classic parser and the SPI chain fails on main and 10.5.2.
adjudication confirmed (high)Re-ran the reproducer's test without assertions: 3 of 5 fail, controls pass.
Follow-up and registry re-runs still reproducesHistory on 60 releases plus main; a security probe (0 match-set differences in 14,376 queries); registry and audit re-runs on a9779ef3c193: the hunt's test 2 of 2 and the reproducer's 3 of 5 fail.
fact checks of this doc reproduced; wording fixedAn independent fact check re-ran the doc's gradle commands on a clean a9779ef3c193 tree (stock and with the fix, assertions on and off) and its own probe: every query and score matched. It and a reading-only cross-model fact check (OpenAI GPT-5.6) found wording errors (a stale-javadoc claim, a commit count, scope and precision points), all fixed or answered; a diagram check found no errors.
this doc's gradle runs reproduced; fix validatedScratch worktree at a9779ef3c193, gradle: stock, the three tests fail (6 of 8 test methods; the 2 controls pass) with assertions on and off; with the fix, case (a) passes and the closest suites pass, (b) is left as designed.

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

Finding record

LFV-584, snapshot of 2026-11-06

Registry title
QueryBuilder drops every token boost (BoostAttribute) from a quoted phrase as soon as one of its words has a single-word synonym, so typed word|weight boosts and weighted-synonym weights are silently ignored there, while plain phrases, graph phrases and unquoted queries keep them
Area
core / util QueryBuilder phrase building with token boosts (analyzeMultiPhrase; BoostAttribute from DelimitedBoostTokenFilter and weighted synonyms; reached through the classic, multi-field and simple query parsers)
Priority
Low, rank 374: Ranking-only, and only behind an opt-in boost-emitting query analyzer.
Impact class
silent wrong results
Impact note
Applications whose query-time analyzer emits token boosts through DelimitedBoostTokenFilter (weighted synonyms such as 'cat, kitty|0.5', the recipe in the Solr Reference Guide, or user-typed word|weight boosts) and that build phrases through QueryBuilder (createPhraseQuery, or quoted phrases in the classic QueryParser [exec], MultiFieldQueryParser and SimpleQueryParser [read], or autoGeneratePhraseQueries) get, for every phrase in which some word has a single-word synonym, an unboosted MultiPhraseQuery with no error: the same documents match, but the scores ignore all of the phrase's boosts; default writer, codec, similarity and searcher, but the boost-emitting analyzer is an opt-in choice; no scale needed; every release from 8.5.0 through 10.6.0 (10.6.0 by reading) and main.
Summary
You give a phrase a token boost, for example "fast|0.5 car" through DelimitedBoostTokenFilter, or use weighted synonyms such as 'cat, kitty|0.5', and expect the quoted phrase to keep the boost the way QueryBuilder's other paths do: "fast|0.5 tiger" becomes (body:"fast tiger")^0.5, and a multi-word weighted synonym gives one boosted phrase per path. But as soon as one word of the phrase has a single-word synonym ('car, automobile'), QueryBuilder builds a MultiPhraseQuery in analyzeMultiPhrase, which never reads BoostAttribute: "fast|0.5 car" becomes the unboosted body:"fast (automobile car)" (score 0.2615 instead of 0.1308), and the quoted "cat food" scores 'kitty food' the same as 'cat food' (0.7130 both), although the unquoted query ranks it lower. The matching documents are unchanged; only scores and ranking are wrong, and there is no error.
Introduced
663611c99c7d48dd31d53ea17644fcecd5e0fad7 '[SOLR-12238] Synonym Queries boost (#357)' (apache/lucene-solr#357; SOLR-12238 and LUCENE-9171 = apache/lucene#10211; branch_8x backport 2752d50dd1dc) [read: git log -S BoostAttribute and git log -L on analyzeMultiPhrase, not a bisect; consistent with the executed release sweep: 8.4.1 not affected, 8.5.0 affected]. The commit added BoostAttribute handling to analyzeTerm, analyzeBoolean, analyzeMultiBoolean, analyzePhrase (phrase boost = product of the token boosts), analyzeGraphBoolean and createSpanQuery, but not to analyzeMultiPhrase, which dates from LUCENE-5261 (57f38e67d65, 2013) and LUCENE-6401 (3b8ea7b2e78, 2015) and has had no boost-related change since (2020-02-24)
First affected version
8.5.0 [exec] (8.0.0 through 8.4.1 are not affected [exec]: QueryBuilder ignored BoostAttribute on every path then, so nothing was inconsistent; earlier releases [read])
Affected versions
8.5.0 through 10.6.0 (10.6.0 by reading) and main [exec: all 52 releases from 8.5.0 to 10.5.2 (19 on 8.x, 21 on 9.x, 12 on 10.x) and main a9779ef3c193 fail the portable 5-test JUnit with both controls passing; the finder's registered repro fails on 8.5.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da; on 8.x its second test stops at a graph-phrase sanity assertion because 8.x builds span queries for graph phrases, and the portable test covers that manifestation there); the independent reproduction's repro fails on main, 10.5.2 and 9.12.3, the intended-behaviour check's on main and 10.5.2]. Upstream main a95c754ee00, branch_10_6 and branch_10x [read: analyzeMultiPhrase unchanged, reads no BoostAttribute]. Not affected: 8.0.0 to 8.4.1 [exec]. All runs on JDK 25, not each release's own JDK.
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: exhaustive check
Upstream and prior reports
none found (2026-10-26, read-only; follow-up prior-report record verdict new): a scripted prior-report search in two runs (the finder's 35 queries; the follow-up's 54: 16 GitHub issue and PR, 3 commit, 5 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 LUCENE JIRA; 167 distinct hits, 0 high overlap), 39 manual GitHub issue and PR searches (28 in apache/lucene, 11 in elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr and apache/lucene-solr), 4 downstream code searches, 13 manual JQL queries over SOLR and LUCENE (SOLR-12238 read in full, 32 comments), manual searches of the java-user@, dev@, users@solr and solr-user@ archives, 8 web searches, and the changed-file lists of all 406 open apache/lucene PRs (#12295, #12433, #14262, #14716 and #16851 touch the files; none changes analyzeMultiPhrase); plus the independent reproduction's, the intended-behaviour check's and the adjudication's own searches (0 hits). Related, none a report of this bug: LUCENE-9171 (#10211) and apache/lucene-solr#357, the feature that introduced it (analyzeMultiPhrase never discussed); LUCENE-3130 (#4203, open since 2011), a pre-feature proposal whose description says the multi-term synonym phrase case would need a decision, 'possibly just punt for now'; #12169 (a committer's design objection to BoostAttribute outside MultiTermQuery, no phrase case); open #12433 (renames the attribute in QueryBuilder, does not touch analyzeMultiPhrase); LUCENE-8943 (#9986, MultiPhraseQuery IDF, not boosts). Gaps: GitHub search does not index PR diffs (closed unmerged PRs not scanned by file); GitHub's search rate limit was reached and rate-limited queries were re-run; one Stack Overflow how-to question could not be fetched
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26): the doc's three public-API tests through gradle in a scratch worktree, stock and with the fix, assertions on and off, plus TestQueryBuilder, the analysis-common boost and synonym packages, the whole lucene/queryparser module and the UnifiedHighlighter tests with the fix (2026-10-28); probes on the main jars with each fix shadowed in. Hunt runs on main a9779ef3c193, the 10.5.2 and 9.12.3 jars and a portable test on 60 releases from 8.0.0 (8.5.0 to 10.5.2 affected, 8.0.0 to 8.4.1 not).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:347-355 (at a9779ef3c193) (createFieldQuery: a quoted, non-graph stream with a stacked token goes to analyzeMultiPhrase, otherwise to analyzePhrase)
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:436-461 (at a9779ef3c193) (analyzePhrase: reads BoostAttribute, phraseBoost *= each token's boost, returns BoostQuery(PhraseQuery, product) when it is not 1)
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:463-497 (at a9779ef3c193) (analyzeMultiPhrase: no BoostAttribute is added or read; returns the bare MultiPhraseQuery)
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:565-583 (at a9779ef3c193) (analyzeGraphPhrase: one phrase per path through createFieldQuery, so each path keeps its tokens' boosts)
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:100-135 (at a9779ef3c193) (createPhraseQuery javadoc: lists the returned query types only, says nothing about boosts)
  • lucene/core/src/java/org/apache/lucene/search/BoostAttribute.java:31-34 (at a9779ef3c193) (javadoc 'intended to be added only by the TermsEnum'; @lucene.internal; stale since LUCENE-9171 made token-stream boosts QueryBuilder input)
  • lucene/CHANGES.txt:5908 (at a9779ef3c193) and :5948 (8.5.0, LUCENE-9171: 'QueryBuilder can now use BoostAttributes on input token streams to selectively boost particular terms or synonyms in parsed queries')
  • upstream: apache/lucene#10211 (LUCENE-9171), apache/lucene-solr#357 review comments discussion_r376473778 and discussion_r376513280, apache/lucene#4203 (LUCENE-3130) description, read-only
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:311-355 (createFieldQuery: hasSynonyms, routing to analyzeMultiPhrase or analyzePhrase); :436-461 (analyzePhrase); :463-497 (analyzeMultiPhrase); :565-583 (analyzeGraphPhrase); :371-434 (analyzeTerm, analyzeBoolean, analyzeMultiBoolean read BoostAttribute)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/boost/DelimitedBoostTokenFilter.java:27-64
Who is affected
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:61, 405-411 (extends QueryBuilder; newFieldQuery calls createFieldQuery); simple/SimpleQueryParser.java:101 (extends QueryBuilder)
Why it looks unintended
  • lucene/CHANGES.txt:5908, 5948-5949 (LUCENE-9171)
  • lucene/core/src/java/org/apache/lucene/search/BoostAttribute.java:31-34; lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:111-135 (createPhraseQuery javadoc)
  • lucene/core/src/test/org/apache/lucene/util/TestQueryBuilder.java:551-605 (MockBoostTokenFilter, testTokenStreamBoosts: createBooleanQuery only)
  • apache/lucene#12169 review comments 1116841545 (2023-02-24) and 1121546026 (2023-03-01) and issue comment 1467945170 (2023-03-14), all by rmuir; #12433 (open) file list and diff; read-only GitHub API, 2026-10-28
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/word2vec/ (no BoostAttribute; grep)
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/boost/TestDelimitedBoostTokenFilter.java (testBoosts: the filter's own output)
Suggested fix
  • lucene/core/src/java/org/apache/lucene/search/BoostQuery.java:57, 75-94, 110 (equals, rewrite of a zero boost, toString)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:420-432; lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/MultiFieldQueryParser.java:110-130 (applySlop unwraps BoostQuery)
History
  • lucene/CHANGES.txt:5908, 5948-5949

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 a probe on main a9779ef3c193 (assertions on and off, 10.5.2, and with the hunt's fix sketch shadowed in); the values on screen come from the probe's log. Its analyzer puts the boost filter before the synonym filter, like the doc's stock-filter test for case (a). The PNG is the final summary frame.

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.