- Since 8.5.0 an analyzer can attach a weight to a query token. The stock
DelimitedBoostTokenFilterreads it fromword|weight; afterSynonymGraphFilterit gives weighted synonyms. - A word alone at its position becomes a
BoostQuery:lion => simba|1.5gives(body:simba)^1.5, which multiplies the score by 1.5. - Words that share a position (a synonym stacked on the query word) become one
SynonymQuery, which scores them as one term. Its per-term weight scales the within-document term frequency and must be in (0, 1].
QueryBuilder turns a weighted synonym into a query: alone at its position the weight becomes a BoostQuery; stacked with the query word it goes to SynonymQuery, which accepts only (0, 1] and throws for 1.5. Select the still to open it at full size. Generated with AI, checked against the code.- Your query analyzer has the weighted synonym
leopard, panthera|1.5. The query wordleopardbecomes two tokens at the same position:pantherawith weight 1.5 andleopardwith weight 1. - You expect a query that applies the weight, as other rules do:
lion => simba|1.5gives(body:simba)^1.5, andleopard, panthera|0.5givesSynonym(body:leopard body:panthera^0.5). - Instead
QueryBuilderpasses 1.5 unchanged toSynonymQuery, which throwsIllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive). The message names neither the word nor the rule, and no query is built. - So
createBooleanQuery,QueryParser.parse("spotted leopard")and the other unquoted entry points fail for text withleopard, and so does the one-word phrase; a quoted phrase over several words does not throw but drops the weight (LFV-584). A weight of 0 fails the same way; 0.5, 0.9 and 1 work. Affected: 8.5.0 to 10.6.0 and main (7 releases run, the others by tag containment or inference).
Applications whose query analyzer emits token weights (weighted synonyms or typed word|weight) and give a weight above 1, or 0, to a word stacked with another token, as in leopard, panthera|1.5. Unquoted queries with that word, and the one-word phrase, fail; a quoted phrase over several words drops the weight (LFV-584). Opt-in and rare.
Mechanism, terms and numbers674 words
Since 8.5.0 (LUCENE-9171) QueryBuilder reads the BoostAttribute of query tokens, which the stock DelimitedBoostTokenFilter (SPI name delimitedBoost) sets from word|weight; placed after SynonymGraphFilter it gives weighted synonyms. For a word alone at its position any finite weight >= 0 works: lion => simba|1.5 gives (body:simba)^1.5. But when the weighted word shares its position with another token, as in the equivalence rule leopard, panthera|1.5, QueryBuilder.newSynonymQuery passes the weight unchanged to SynonymQuery.Builder.addTerm, which accepts only (0, 1] (LUCENE-8652: a per-term weight on the term's within-document frequency). So createBooleanQuery("body", "leopard") and QueryParser.parse("spotted leopard") throw IllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive) instead of returning a query, with a message that names neither the word nor the rule. A weight of 0 fails the same way; weights in (0, 1] work on both paths. A quoted phrase over several words does not throw; it drops the weight (the separate bug LFV-584). None of the javadocs and CHANGES entries checked states a limit for token weights. The failure is loud and fail-closed: no wrong results, no index effect. Affected: 8.5.0 through 10.6.0 and main (7 releases run, the others by tag containment or inference). A small change in newSynonymQuery (+10/-2 lines: divide by the largest weight above 1 and boost the whole SynonymQuery by it) makes the repro pass; it is one policy among several.
- Token weight (boost): a float on a query token, carried by
BoostAttribute.DelimitedBoostTokenFilter(SPI namedelimitedBoost) splitsword|weightinto the tokenwordand the weight, withFloat.parseFloatand no range check (DelimitedBoostTokenFilter.java:26-33, 53-54). It is the only producer of the attribute on token streams in Lucene's non-test sources. - Stacked tokens: tokens that share a position: the first has a position increment of 1 or more, each one after it 0. The equivalence rule
leopard, panthera|1.5turns the query wordleopardintopanthera(weight 1.5, increment 1) andleopard(weight 1, increment 0) at one position; a replacement with one output,lion => simba|1.5, does not stack. SynonymQuery: scores several terms as if they were one term. Since LUCENE-8652 (8.1.0) each term can carry a weight that multiplies its within-document frequency (the javadoc says "document frequencies");addTermrejects NaN, values <= 0 and values > 1 (SynonymQuery.java:90-94).
createFieldQuerymarkshasSynonymswhen a token has position increment 0. For text that is not a token graph it sends one position with several tokens (quoted or not) toanalyzeBoolean, unquoted text over several positions toanalyzeMultiBoolean, and quoted text over several positions toanalyzeMultiPhrasewhen a token is stacked (QueryBuilder.java:308-365).- A position with one token becomes
newTermQuery(term, boost), aBoostQueryfor any weight other than 1 (:638-644);BoostQueryaccepts any finite value >= 0 (BoostQuery.java:38-44). - A position with several tokens becomes
newSynonymQuery, which callsbuilder.addTerm(t.term, t.boost)with the raw weight (:603-609, the call at:606). It is reached fromanalyzeBoolean(:385-396),addforanalyzeMultiBoolean(:408-409) andanalyzeGraphBoolean(:554-555). SynonymQuery.Builder.addTermthrows for 1.5 or 0 (SynonymQuery.java:90-94). Nothing catches it:QueryParserBase.parseconverts onlyParseException,TokenMgrErrorandTooManyClauses(QueryParserBase.java:129-146).
So one attribute means two things: a score multiplier (any finite value >= 0) for a word alone at its position, and a frequency weight in (0, 1] for words that share one. Values legal on the first path throw on the second.
Index of four one-word documents (leopard, panthera, simba, cat), BM25, main 645d4fe14a20: leopard, panthera|0.5 gives Synonym(body:leopard body:panthera^0.5) (doc leopard 0.5473, doc panthera 0.3541); |0.9 and |1 work too; lion => simba|1.5 gives (body:simba)^1.5 (0.8209); lion => simba|0 gives (body:simba)^0.0 (match, score 0.0). leopard, panthera|1.5, leopard, panthera|0 and lion => lion, simba|1.5 throw, for leopard, spotted leopard and QueryParser.parse("spotted leopard"). A quoted "spotted leopard" does not throw: it becomes body:"spotted (panthera leopard)" and loses the weight (the separate bug LFV-584).
Exposure and likelihood234 words
- Who is hit. Applications whose query-time analyzer emits token weights: weighted synonyms (
synonymGraphthendelimitedBoost, the chain the Solr Reference Guide documents) or user-typedword|weight(delimitedBoostbefore the synonym filter, wheretiger|2works andleopard|2throws). The trigger is a weight above 1, or exactly 0, on a word the analyzer stacks with another token: an equivalence rule such asleopard, panthera|1.5, or a replacement with several outputs such aslion => lion, simba|1.5. - Entry points (all executed):
createBooleanQuery,createMinShouldMatchQuery, a single-positioncreatePhraseQuery, the classicQueryParserandSimpleQueryParser. - Effect. Building a query from unquoted text that contains the word fails, through each entry point above (the single-position
createPhraseQueryis the one-word phrase); a quoted phrase over several words does not throw but drops the weight (LFV-584). Fail-closed: no query object is returned, so no restriction or exclusion clause can be lost (4 of 4 property tests); no index effect. The message names neither the word nor the rule. - Default vs opt-in. The weight-emitting query analyzer is opt-in; writer, codec, similarity, searcher and parsers are defaults. No scale is needed: one word and one rule.
- How likely. Rare: every weighted-synonym example found in the recorded searches uses weights of at most 1, the failure shows at the first unquoted query that contains the word, and the recorded searches found no report of it since 8.5.0 (2020).
One JUnit test with stock filters only: a whitespace tokenizer, SynonymGraphFilter with the rule leopard, panthera|1.5, DelimitedBoostTokenFilter, then QueryBuilder.createBooleanQuery("body", "leopard"). No index and no query parser (SolrSynonymParser only parses the rule). Deterministic and single-threaded; the same failure with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.analysis.boost;
import java.io.StringReader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.analysis.synonym.SolrSynonymParser;
import org.apache.lucene.analysis.synonym.SynonymGraphFilter;
import org.apache.lucene.analysis.synonym.SynonymMap;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.QueryBuilder;
public class TestStackedTokenBoostRange extends LuceneTestCase {
public void testSynonymWeightAboveOne() throws Exception {
SolrSynonymParser parser = new SolrSynonymParser(true, true, new WhitespaceAnalyzer());
parser.parse(new StringReader("leopard, panthera|1.5"));
SynonymMap synonyms = parser.build();
Analyzer analyzer =
new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer tokenizer = new WhitespaceTokenizer();
return new TokenStreamComponents(
tokenizer,
new DelimitedBoostTokenFilter(
new SynonymGraphFilter(tokenizer, synonyms, true), '|'));
}
};
// expected: a query, as weight 1.5 on an unstacked token gives a BoostQuery; actual: IAE
try {
new QueryBuilder(analyzer).createBooleanQuery("body", "leopard");
} catch (IllegalArgumentException e) {
throw new AssertionError("expected a query for synonym weight 1.5, but got " + e, e);
}
}
}
cp TestStackedTokenBoostRange.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/boost/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.boost.TestStackedTokenBoostRange -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
createBooleanQuery("body", "leopard")returns a query that applies the synonym weight 1.5.- Observed
- It throws
IllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive)fromSynonymQuery.Builder.addTerm; the same with assertions off.
More files, environment, full output252 words
The test asserts only the claim: building the query throws instead of returning one. It does not assert the query's shape, because upstream could fix this in more than one way (normalize the weights, reject them early with a clear message, or document a limit). The test uses no randomness of its own, and QueryBuilder reads none of the settings LuceneTestCase randomizes; it fails the same with seeds DEADBEEF, 5EED5881 and 5EED5882. It needs the analysis-common module, so it lives in org.apache.lucene.analysis.boost.
As the regression test: the same test (for example next to TestDelimitedBoostTokenFilter). It fails on stock and passes with the recommended fix; with the reject-early option it would become an expectThrows on the new message.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0, 10.5.2 and 9.12.3; the hunt's tests fail on every release run from 8.5.0; 8.4.1 not affected |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestStackedTokenBoostRange.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/boost/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.boost.TestStackedTokenBoostRange -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
TestStackedTokenBoostRange > testSynonymWeightAboveOne FAILED
java.lang.AssertionError: expected a query for synonym weight 1.5, but got java.lang.IllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive)
at __randomizedtesting.SeedInfo.seed([DEADBEEF:6BCD06C43690A2DD]:0)
at org.apache.lucene.analysis.boost.TestStackedTokenBoostRange.testSynonymWeightAboveOne(TestStackedTokenBoostRange.java:51)
Caused by:
java.lang.IllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive)
at org.apache.lucene.search.SynonymQuery$Builder.addTerm(SynonymQuery.java:92)
at org.apache.lucene.util.QueryBuilder.newSynonymQuery(QueryBuilder.java:606)
at org.apache.lucene.util.QueryBuilder.analyzeBoolean(QueryBuilder.java:395)
at org.apache.lucene.util.QueryBuilder.createFieldQuery(QueryBuilder.java:360)
at org.apache.lucene.util.QueryBuilder.createFieldQuery(QueryBuilder.java:256)
at org.apache.lucene.util.QueryBuilder.createBooleanQuery(QueryBuilder.java:108)
at org.apache.lucene.util.QueryBuilder.createBooleanQuery(QueryBuilder.java:93)
# gradle, main 645d4fe14a20, seed DEADBEEF; the same with -Ptests.asserts=false
# (module prefixes org.apache.lucene.core@11.0.0-SNAPSHOT/ trimmed)- LUCENE-9171 (8.5.0) says
QueryBuildercan use token boosts "to selectively boost particular terms or synonyms", andDelimitedBoostTokenFilteraccepts any float; none of the javadocs and CHANGES entries checked states a limit. - The same weight works or fails only by stacking:
lion => simba|1.5gives(body:simba)^1.5,leopard, panthera|1.5throws. - No test covers it:
TestQueryBuilder#testTokenStreamBoostsuses only 0.5.
The full argument203 words
- The feature's 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:5965-5966), and "QueryBuilder.newTermQuery() and .newSynonymQuery() now take boost parameters" (:5925-5926). No range is stated. - The producer states no range.
DelimitedBoostTokenFilter: "Characters before the delimiter are the "token", those after are the boost", parsed withFloat.parseFloat; its factory is SPI-registered asdelimitedBoost. - Stacking decides.
lion => simba|1.5gives(body:simba)^1.5;leopard, panthera|1.5andlion => lion, simba|1.5throw. A user cannot tell from the configuration which rules are safe. SynonymQuerydocuments no range either. TheaddTermjavadoc says only that "document frequencies of this term will be boosted by boost" (SynonymQuery.java:86-89); the scorer actually scales the within-document frequency (:649-685).- No test.
TestQueryBuilder#testTokenStreamBoosts(TestQueryBuilder.java:572-605) uses 0.5 only; no test asserts the exception at theQueryBuilderlevel. - History. The (0, 1] check (LUCENE-8652, 2019; in 8.1.0 through the branch_8x backport
24427f9f5cd) predates token boosts and was harmless whileQueryBuilderpassed none; LUCENE-9171 (2020; in 8.5.0 through the backport2752d50dd1d) passed them through without range handling. The reviews of LUCENE-9171, SOLR-12238 and apache/lucene-solr#357 never discuss a range.
The devil's-advocate check293 words
The strongest case for "intended" (the refute-lens review, the works-as-designed review and the intended-behaviour devil's advocate): SynonymQuery's range is deliberate. LUCENE-8652 added it as a "deboost" (CHANGES.txt:6549-6550); its author (jimczi) wrote "The boost must be between 0+ and 1 in order to ensure that document term frequencies do not go beyond the total term frequency" (#9698); TestSynonymQuery#testBogusParams expects 1.3f to throw. 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" (BoostAttribute.java:24-35); in #12169 a committer (rmuir) objected to any use of it in token streams. Every example in the recorded searches uses weights of at most 1, and newSynonymQuery is an overridable extension point (an override accepts 1.5).
Why it fails: the range is SynonymQuery's own invariant and stays valid; the defect is one layer up. LUCENE-9171 made token weights documented QueryBuilder input, shipped a public, SPI-registered producer that parses any float, and applies the same attribute without a limit when the word is alone at its position. The BoostAttribute javadoc predates that feature, and the #12169 objection argues against the feature as a whole, not for a limit that depends on stacking. No API promises either that a weight above 1 on a stacked token must be accepted, so the claim is narrower: an undocumented limit that depends on stacking, which surfaces as an unchecked exception. Even if upstream rules that synonym weights must be in (0, 1], the current failure is the wrong error: an internal exception that names neither the word nor the rule. Hence the class "wrong error", not "crash". Verdict: not intended; the open question is which layer should handle the range.
Recommended Normalize in newSynonymQuery (validated)
Normalize in QueryBuilder.newSynonymQuery: divide the weights by the largest one above 1 and boost the whole SynonymQuery by it; a weight of 0 becomes the smallest positive weight. Weights in (0, 1] give today's queries. With it the repro passes with assertions on and off, and the closest suites pass.
float max = 1f;
for (TermAndBoost t : terms) {
max = Math.max(max, t.boost);
}
SynonymQuery.Builder builder = new SynonymQuery.Builder(field);
for (TermAndBoost t : terms) {
builder.addTerm(t.term, t.boost == 0f ? Float.MIN_VALUE : t.boost / max);
}
Query query = builder.build();
return max == 1f ? query : new BoostQuery(query, max);All options and trade-offs4 options ยท 637 words
Upstream should choose the policy: normalize, reject early with a clear message, or fall back to boosted term clauses. The first one was run through gradle in a scratch worktree of 645d4fe14a20.
+10/-2 in QueryBuilder.java. Divide every weight by the largest one above 1 and wrap the SynonymQuery in a BoostQuery of that value; a weight of 0 becomes Float.MIN_VALUE, so the term still matches, as (term)^0 does.
Result: leopard, panthera|1.5 gives (Synonym(body:leopard^0.6666667 body:panthera))^1.5; doc panthera scores 0.8209 (the score of (body:simba)^1.5), doc leopard 0.6450. QueryParser.parse("spotted leopard") gives body:spotted (Synonym(body:leopard^0.6666667 body:panthera))^1.5. leopard, panthera|0 gives Synonym(body:leopard body:panthera^1.4E-45): doc panthera matches with score 0.0. Negative, NaN and infinite weights still throw today's exception.
float max = 1f;
for (TermAndBoost t : terms) {
max = Math.max(max, t.boost);
}
SynonymQuery.Builder builder = new SynonymQuery.Builder(field);
for (TermAndBoost t : terms) {
builder.addTerm(t.term, t.boost == 0f ? Float.MIN_VALUE : t.boost / max);
}
Query query = builder.build();
return max == 1f ? query : new BoostQuery(query, max);Trade-offs
- Not score-equivalent to "panthera counts 1.5 times":
SynonymQueryblends frequencies, which BM25 saturates, so the outer 1.5 also raises a document that contains onlyleopard(0.5473 with weight 1, 0.6450 with the fix). It is one policy; dividing without the outer boost, or clamping to 1, are others. - Gives weight 0 a meaning (match, score about 0) where it throws today.
- Changes the query (shape,
toString,equals) only for weights that throw today, so no working configuration changes. It also needs a CHANGES entry; no benchmark was run. - The hunt's first sketch did the same scaling but also turned negative and NaN weights into
Float.MIN_VALUEsilently; this version keeps them throwing.
What the checks validated
- Gradle in a scratch worktree of
645d4fe14a20(removed afterwards), seedDEADBEEF: without the fix the repro fails with assertions on and off; with the fix it passes with assertions on and off (1 of 1 each). - Closest suites with the fix:
TestQueryBuilder,TestSynonymQuery, the analysis-commonboostandsynonympackages (the repro included) and the wholelucene/queryparsermodule: 45 classes, 692 tests, 0 failures, 0 errors, 1 skipped. :lucene:core:check -x test(google-java-format, ecj lint, forbidden APIs) and the analysis-common format check pass with the fix and the repro in place.- A probe on the main jars with the fixed
QueryBuildershadowed gives the queries and scores listed above; weights in (0, 1] give exactly today's queries and scores. Other modules' suites, Solr and benchmarks were not run.
In newSynonymQuery, check each weight and throw an IllegalArgumentException that names the term, its weight and the (0, 1] limit for words that share a position; state the limit in the DelimitedBoostTokenFilter and QueryBuilder javadocs.
Trade-offs
No scoring policy to choose and no score change, but the query still fails (now with a message an operator can act on), and it still escapes QueryParser.parse unchecked unless the parser converts it. The repro would become an expectThrows on the message. Not run.
When a weight at a position is outside (0, 1], build a BooleanQuery of SHOULD (term)^weight clauses for that position, as Solr's as_distinct_terms synonym style does (read in Solr's source, not run).
Trade-offs
Keeps each weight exactly as a score multiplier, like the unstacked path, but loses SynonymQuery's blended statistics for that position, so the query's shape and scoring change with the weight's value. Not run.
State in the javadocs that weights on stacked tokens must be in (0, 1].
Trade-offs
Leaves the internal exception, whose message names neither the word nor the rule. Still a credible upstream outcome: BoostAttribute is @lucene.internal, and SynonymQuery's bound is deliberate and tested.
Downstream, Solr's default synonym query style (as_same_term) delegates to this Lucene method, so a synonym weight above 1 would fail there, while its pick_best and as_distinct_terms styles wrap weights in BoostQuery (read in Solr's source, not run).
A bounded-exhaustive check ran the real QueryBuilder against an independent reference model on every small token stream (up to 3 tokens, stacked tokens, weights from 0 to 2): 864,680 checks. 197,228 threw this exception, each from a stream with a stacked token weighted outside (0, 1]. JUnit tests with stock filters confirmed it.
The method in full145 words
Method: bounded-exhaustive translation validation on the real jar (semi-formal tier), in a hunt unit on QueryBuilder reference semantics (2026-10-26, main a9779ef3c193).
- Scope C: every well-formed token stream of at most 3 tokens over {a, b, c} (19,458 of 106,956 enumerated; the rest are ill-formed graphs) with position increments 0-2, position lengths 1-2 and weights {0.5, 1, 2}, through 10
QueryBuilderentry points, compared with an independent reference model: 739,080 checks, 155,052 of them threw thisIllegalArgumentException. - Scope C0: over {a, b} with weights {0, 0.25, 1, 1.5}: 125,600 checks, 42,176 threw it.
- Every throwing check came from a stream in which a stacked token carries a weight outside (0, 1]; no other exception class occurred.
- Then public-API JUnit tests on stock analysis chains (
SynonymGraphFilterandDelimitedBoostTokenFilterin either order) and throughQueryParserandSimpleQueryParser; later cut to the one-test repro above.
No prior report found (2026-10-26) in GitHub issues, PRs and commits, the JIRA archive, the Lucene and Solr lists, downstream trackers or the web; the exact message has no hit in a synonym context. Closest: LUCENE-8652, which added SynonymQuery's range, and LUCENE-9171, which added token weights without a limit.
Searches4 searches
Verdict new. Closest items, none a report of this bug: LUCENE-8652 (#9698), the rationale for the range; LUCENE-9171 (#10211), SOLR-12238 and apache/lucene-solr#357, the feature (weights of at most 1 only, no limit stated); a 2020-04 java-user@ thread on in-range weights having no effect under BooleanSimilarity (no exception); #12169 (a committer's objection to BoostAttribute in token streams); open #12433 (renames the attribute, leaves newSynonymQuery unchanged); open #14262 (a new multi-field path that rejects synonym weights other than 1); #11864 / #11941 and elasticsearch#54434 (a different empty-array crash in the same method). Limits: Stack Overflow was read through an archived copy; chat channels were not searched.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; downstream repositories; dev@, java-user@, issues@, commits@, solr-user@; the JIRA archive; the web | 2026-10-26 | a scripted prior-report search in three runs: 48 issue/PR, 9 commit, 6 downstream, 65 list, 12 JIRA and 12 web queries | 0 same-bug hits |
| GitHub issues and PRs (apache/lucene, apache/solr, apache/lucene-solr, Elasticsearch, OpenSearch, Lucene.NET) | 2026-10-26 | 27 manual searches and 4 code searches, including the exact exception message | 0 same-bug hits; every hit for the message family is BoostQuery's related message "boost must be a positive float, got -1.0", from an unrelated negative fuzzy boost (#10608) |
| Open apache/lucene PRs | 2026-10-26 | the changed files of all 406 open PRs | 8 touch the files; none changes the boost handling (#12433, #14262 the closest) |
| JIRA (LUCENE, SOLR), lists.apache.org, the web | 2026-10-26 | 11 JIRA queries, 30 list queries, 7 more web searches | only the related items listed above |
Introduced in 8.5.0 by 663611c99c7 (LUCENE-9171 / SOLR-12238, 2020-02-24), which started passing token weights to SynonymQuery, whose (0, 1] check dates from LUCENE-8652 (8.1.0). 8.4.1 is not affected; every release run from 8.5.0 to 10.6.0, and main, fails.
Versions tested5 rows
Introduced by 663611c99c7 "[SOLR-12238] Synonym Queries boost (#357)" (apache/lucene-solr#357; SOLR-12238 and LUCENE-9171 = #10211), merged 2020-02-24; branch_8x backport 2752d50dd1d the same day. It made QueryBuilder read BoostAttribute, wrap an unstacked token's weight in BoostQuery, pass a stacked token's weight unchanged to SynonymQuery.Builder.addTerm, and added DelimitedBoostTokenFilter; git blame puts the call at QueryBuilder.java:606 on it. Its backport 2752d50dd1d is the only commit between the 8.4.1 and 8.5.0 tags that touches QueryBuilder.java, SynonymQuery.java or BoostAttribute.java (history reading plus release runs, not a commit-level bisect). 663611c99c7 itself is the master commit (first released in 9.0.0). The (0, 1] check itself is from LUCENE-8652 (d8cfeba912ea on master, 2019-03-11; in 8.1.0 through its branch_8x backport 24427f9f5cd). Later commits only refactored the code (#11941, #12169, #12366). 8.9.0-8.11.1 and 8.11.3 are affected by tag containment and inference.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off (standalone and gradle) |
| 10.6.0, 10.5.2, 9.12.3 | affected | minimal test fails (assertions off) |
main a9779ef3c193, 10.5.2, 9.12.3, 9.0.0, 8.11.4, 8.11.2, 8.5.0 | affected | the hunt's tests or history probe fail |
| 8.4.1 | not affected | not affected: QueryBuilder ignored token weights, DelimitedBoostTokenFilter did not exist |
fix on 645d4fe14a20 | fix | minimal test passes, assertions on and off |
An independent reproducer, an intended-behaviour devil's advocate and a works-as-designed review found it real; an adjudication confirmed it (medium confidence, wrong error). A reading-only refute review from another model family said not real (medium): it could not run code, so its verdict is procedural, and it rated the intended-behaviour case strong; it agreed with the root cause.
Checks and verdicts273 words
Every executing check wrote its own test or re-ran an earlier one on stock main and saw it fail; every control passed.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive check (864,680 cases); 3-test JUnit fails on main (assertions on and off), 10.5.2, 9.12.3, 9.0.0, 8.11.2 and 8.5.0; the first fix sketch passes, with upstream TestQueryBuilder 26 of 26. |
| cross-model review (refute lens) | not real (medium) | An independent reviewer from another model family (OpenAI GPT-5.6) reviewed by reading only and could not run code, so its verdict is procedural; it rated the intended-behaviour case strong (section above) and agreed with the root cause. A separate re-run failed 3 of 3. |
| independent reproducer | real | Own test (CustomAnalyzer with the SPI names): 4 of 8 fail on main (assertions on and off), 10.5.2 and 9.12.3; all 4 controls pass; a breadth test adds createMinShouldMatchQuery, single-position createPhraseQuery and SimpleQueryParser. |
| works-as-designed review | real (medium) | OpenAI GPT-5.6, reading only: rated the intended case moderate, outweighed by the range-free public filter, the LUCENE-9171 entry and the unstacked path; a separate re-run failed 3 of 3. |
| intended-behaviour devil's advocate | real (medium) | Built the strongest intended case and answered it; re-ran on main and 10.5.2; an intent probe showed 0.9 and 1 work and an override accepts 1.5. |
| adjudication | confirmed (medium, wrong error) | Re-ran the independent reproduction's test with assertions off: 4 of 8 fail, controls pass. |
| this doc's runs | reproduced; fix validated | The minimal test on main (standalone and gradle, assertions on and off), 10.6.0, 10.5.2 and 9.12.3; the recommended fix through gradle with the closest suites. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 526 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,290 words
- Registry title
- QueryBuilder hands a token boost (BoostAttribute) above 1 or equal to 0 on a word that shares its position with a synonym straight to SynonymQuery, which throws IllegalArgumentException, so every query containing that word fails, while the same weight on a word without a synonym works
- Area
- core / util QueryBuilder token boosts on stacked tokens (newSynonymQuery; BoostAttribute from DelimitedBoostTokenFilter and weighted synonyms; reached through the classic and simple query parsers)
- Priority
- Low, rank 1056: A weighted synonym above 1 or equal to 0 on a stacked token makes every query with that word throw IllegalArgumentException, which fails closed and is rare because published weights are below 1.
- Impact class
- wrong error
- Impact note
- Applications that opted in to token boosts in the query analyzer (weighted synonyms through SynonymGraphFilter plus DelimitedBoostTokenFilter, the recipe in the Solr Reference Guide, or user-typed word|weight boosts) and give a weight above 1 or exactly 0 to a word that shares its position with another token (a single-word equivalence synonym such as 'leopard, panthera|1.5') get, for every query containing that word, an IllegalArgumentException from SynonymQuery out of createBooleanQuery, createMinShouldMatchQuery, single-position createPhraseQuery, QueryParser.parse and SimpleQueryParser.parse [exec], with a message that names neither the word nor the synonym rule; fail-closed (no query is returned, no wrong hits, no index effect); no scale needed; every release from 8.5.0 through 10.6.0 and main.
- Summary
- You configure weighted synonyms ('leopard, panthera|1.5') or let users type boosts ('leopard|2'), and expect QueryBuilder to apply the weight, as it does for a word without a synonym: 'lion => simba|1.5' gives (body:simba)^1.5 and 'tiger|2' gives (body:tiger)^2.0. But when the boosted token shares its position with another token, QueryBuilder.newSynonymQuery passes the raw boost to SynonymQuery.Builder.addTerm, which accepts only (0, 1], so building the query throws IllegalArgumentException 'boost must be a positive float between 0 (exclusive) and 1 (inclusive)' and QueryParser.parse('spotted leopard') fails with it instead of returning a query or a ParseException. Weights in (0, 1] work on both paths; whether 1.5 or 0 works depends only on whether the analyzer stacked the token.
- Introduced
- 663611c99c7d48dd31d53ea17644fcecd5e0fad7 '[SOLR-12238] Synonym Queries boost (#357)' (apache/lucene-solr#357; SOLR-12238 and LUCENE-9171 = apache/lucene#10211; branch_8x backport 2752d50dd1dc, same day) [read: the only commit between releases 8.4.1 and 8.5.0 that touches QueryBuilder.java, SynonymQuery.java or BoostAttribute.java; consistent with the executed release runs (8.4.1 not affected, 8.5.0 affected); no commit-level bisect]. It made QueryBuilder read BoostAttribute, wrap an unstacked token's boost in a BoostQuery and pass a stacked token's boost unchanged to SynonymQuery.Builder.addTerm, whose (0, 1] check comes from LUCENE-8652 (d8cfeba912ea, 2019-03-11, first released in 8.1.0; not a defect on its own, because QueryBuilder passed no boosts before 8.5.0) (2020-02-24)
- First affected version
- 8.5.0 [exec] (8.4.1 not affected [exec]: QueryBuilder ignored BoostAttribute and DelimitedBoostTokenFilter did not exist; the weighted-synonym feature and this failure arrived together)
- Affected versions
- 8.5.0 through 10.6.0 and main [exec: 8.5.0, 8.11.2, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main a9779ef3c193 (-ea and -da) fail the finder's JUnit or the same-source history probe; the independent reproduction's JUnit fails on main, 10.5.2 and 9.12.3, the intended-behaviour check's re-run on main and 10.5.2; read: git tag containment of the introducing commit for 8.5.0-8.8.1, 9.0.0-9.12.3 and 10.0.0-10.5.2; 8.9.0-8.11.1 and 8.11.3 by inference (not in the local history clone); the minimal repro fails on the 10.6.0 release jars (released 2026-10-29; SHA-1 checked against Maven Central; -da, 2026-11-01) and on main 645d4fe14a20 (-ea and -da)]. Not affected: 8.4.1 and earlier [exec on 8.4.1]. 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 disputed it (the adjudication ruled it real), 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): a scripted prior-report search in three runs (48 GitHub issue and PR queries on apache/lucene, 9 commit, 6 downstream, 65 mailing-list over dev@, java-user@, issues@, commits@ and solr-user@, 12 JIRA-archive, 12 web), 27 manual GitHub issue and PR searches (apache/lucene, apache/solr, apache/lucene-solr, elastic/elasticsearch, opensearch-project/OpenSearch, apache/lucenenet), 4 GitHub code searches, the changed-file lists of all 406 open apache/lucene PRs, 11 manual JIRA queries (LUCENE, SOLR), 30 manual list queries, 7 more web searches and the git history of QueryBuilder.java, SynonymQuery.java and analysis/boost; plus the independent reproduction's, the intended-behaviour check's and the adjudication's own searches (0 hits). The exact exception message has no hit in any synonym context. Related, none a report of this bug: LUCENE-8652 (#9698), which added SynonymQuery's (0, 1] per-term boost with its rationale; LUCENE-9171 (#10211), SOLR-12238 and apache/lucene-solr#357, the feature that created the path (all examples use weights below 1, no limit stated); a 2020-04 java-user@ thread 'DelimitedBoostTokenFilterFactory Issue - Boosting and BooleanSimilarity' (in-range weights without effect under BooleanSimilarity, no exception); #12169 (a committer's objection to BoostAttribute in token streams); open #12433 (renames the attribute, adds a producer, leaves newSynonymQuery unchanged); open #14262 (a new multi-field path that rejects synonym boosts other than 1; the single-field path is unchanged); #11864 / #11941 and elasticsearch#54434 (a different empty-array crash in the same method). Gaps: Stack Overflow was read through an archived copy; Solr chat channels were not searched
- Lucene commit tested
- main
645d4fe14a20(2026-11-01: the minimal repro standalone with assertions on and off and through gradle in a scratch worktree; the fix through gradle with the closest suites; an evidence probe of queries and scores, stock and fixed); the SHA-1-checked release jars 10.6.0, 10.5.2 and 9.12.3 (minimal repro, assertions off). The hunt (2026-10-26, maina9779ef3c193, where the relevant files are identical) also ran 8.4.1 (not affected), 8.5.0, 8.11.2, 8.11.4, 9.0.0, 9.12.3 and 10.5.2 (affected), all on JDK 25.
- Finding record
lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:385-396 (at a9779ef3c193) (analyzeBoolean: reads BoostAttribute of every stacked token and calls newSynonymQuery)lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:398-411 (at a9779ef3c193) and :425-432 (add, used by analyzeMultiBoolean: one token -> newTermQuery, several tokens at one position -> newSynonymQuery)lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:541-556 (at a9779ef3c193) (analyzeGraphBoolean: a segment without side paths with several tokens -> newSynonymQuery)lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:603-609 (at a9779ef3c193) (newSynonymQuery: passes every TermAndBoost.boost unchanged to SynonymQuery.Builder.addTerm)lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:638-644 (at a9779ef3c193) (newTermQuery: the same boost on an unstacked token becomes a BoostQuery)lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java:86-94 (at a9779ef3c193) (addTerm: javadoc 'document frequencies of this term will be boosted by boost', states no range; throws for NaN, <= 0 and > 1)lucene/core/src/java/org/apache/lucene/search/BoostQuery.java:38-44 (at a9779ef3c193) (accepts any finite boost >= 0)lucene/core/src/java/org/apache/lucene/search/BoostAttribute.java:24-35 (at a9779ef3c193) (@lucene.internal; javadoc speaks only of TermsEnum and MultiTermQuery rewrites, unchanged by LUCENE-9171)lucene/analysis/common/src/java/org/apache/lucene/analysis/boost/DelimitedBoostTokenFilter.java:26-58 (at a9779ef3c193) ('Characters before the delimiter are the token, those after are the boost'; Float.parseFloat, no range check)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:122-145 (at a9779ef3c193) (parse declares ParseException and converts only ParseException, TokenMgrError and TooManyClauses)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'), :6532 (LUCENE-8652: 'SynonymQuery can now deboost the document frequency of each term')upstream, read-only: apache/lucene#9698 (LUCENE-8652, the SynonymQuery range rationale), apache/lucene#10211 (LUCENE-9171), apache/lucene-solr#357, apache/lucene#12169, apache/lucene#12433, apache/lucene#14262
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:308-363, (at 645d4fe14a20) 372-434, 541-556, 603-609, 638-644lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java:86-94, (at 645d4fe14a20) 649-685lucene/core/src/java/org/apache/lucene/search/BoostQuery.java:38-44 (at 645d4fe14a20)lucene/analysis/common/src/java/org/apache/lucene/analysis/boost/DelimitedBoostTokenFilter.java:26-58 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:129-146 (at 645d4fe14a20)
- Why it looks unintended
lucene/CHANGES.txt:5925-5926, (at 645d4fe14a20) 5965-5966, 6549-6550lucene/analysis/common/src/java/org/apache/lucene/analysis/boost/DelimitedBoostTokenFilter.java:26-58; (at 645d4fe14a20) DelimitedBoostTokenFilterFactory.java:34-39lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java:86-94 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/BoostAttribute.java:24-35 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/util/TestQueryBuilder.java:572-605; (at 645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestSynonymQuery.java:107
- TestStackedTokenBoostRange.java (JUnit, one test):
repro/LFV-588/src/org/apache/lucene/analysis/boost/TestStackedTokenBoostRange.java(54 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and probes on the main jars.
assets/LFV-588/bug.png(526 KB)
![Diagram of the bug. A query analyzer with SynonymGraphFilter and DelimitedBoostTokenFilter. The rule lion => simba|1.5 gives one token, simba with weight 1.5, alone at its position: QueryBuilder builds (body:simba)^1.5. The rule leopard, panthera|1.5 gives two tokens at one position, panthera with weight 1.5 and leopard with weight 1: QueryBuilder.newSynonymQuery passes 1.5 to SynonymQuery.Builder.addTerm, which accepts only weights in (0, 1] and throws IllegalArgumentException: boost must be a positive float between 0 (exclusive) and 1 (inclusive). The weight 0.5 works on both paths. The query fails closed: no query, no wrong hits, no index effect. A quoted phrase over two positions does not throw; it drops the boost instead (a separate bug). Affected: 8.5.0 through 10.6.0, and main.](../assets/LFV-588/bug.png)