All findings · Lucene finding · LFV-975 · flexible-boost-node-toquerystring-e-notation

Flexible query parser prints small boosts in E-notation: ab^0.0009 comes back as ab^9.0E-4, which it cannot parse

BoostQueryNode.toQueryString() prints every nonzero boost below 0.001 with Float.toString, so ab^0.0009 comes back as ab^9.0E-4, text the same parser rejects. The query that actually runs is correct; only code that prints node trees and reads the text back is hit.

Summary

Priority
Low, rank 773The fuzzy-node leg merged into this finding re-parses silently as a different query; the boost leg has a slightly more plausible trigger, but on 9.0 and later its re-parse fails loudly.
Impact
minor
Since
2.9.0 (2009), the flexible parser's first releaseintroduced 2009-08-03 · over 17 years
Affected
every release 2.9.0 to 10.6.0, and main
Status
documented
Found by
non-formal: a parse, print, re-parse probe; then every float checked

AI-assisted page: this finding was first signalled by code reading or testing 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

How Java prints a float, and what the query syntax accepts

  1. Float.toString writes plain decimals only from 0.001 up to just below 10,000,000 (0.5, 2.5). Outside that range it uses scientific notation: 0.0009f prints 9.0E-4.
  2. The query syntax after ^ accepts only digits with an optional .digits (2, 0.5, 0.0009). It has no exponent, so 9.0E-4 is not a number to it.
Diagram of the bug: the query ab^0.0009 is parsed by StandardSyntaxParser into a BoostQueryNode whose float value is 0.0009. toQueryString prints the value with Float.toString, which writes numbers below 0.001 in scientific notation, giving f:ab^9.0E-4. Parsing the printed text again fails with a ParseException, because the number after ^ may not have an exponent. The BoostQuery built from the original text keeps boost 0.0009.
The query ab^0.0009 becomes a boost node holding 0.0009; toQueryString prints it as ab^9.0E-4, and the same parser rejects that text when it reads it back. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Parse ab^0.0009 with the flexible StandardSyntaxParser, the syntax parser behind StandardQueryParser. The tree is a BoostQueryNode with boost 0.0009.
  2. Print the tree with toQueryString(new EscapeQuerySyntaxImpl()). You expect text the parser reads back: the method is documented to produce "a query string understood by the query parser".
  3. Instead you get f:ab^9.0E-4. The node prints the boost with Java's Float.toString, which switches to scientific notation below 0.001. Boosts above 2^63 are hit too: 1e19 prints 1.0E19.
  4. Parsing f:ab^9.0E-4 again throws ParseException: after ^ the grammar accepts only digits with an optional .digits. Before 9.0 the same text silently became a different query, f:ab^9 f:E-4.
  5. Search is unaffected: StandardQueryParser.parse builds the right boost. The fuzzy node has the same pattern in a different method, and there it is silent today: ab~0.0001 prints ab~1.0E-4, which re-parses as ab~2 plus an extra term.

Who is affected

Applications that print a flexible-parser syntax tree, a hand-built boost node or a tree with a configured field boost via toQueryString, then parse or forward the text, when a boost is below 0.001 (usually a computed one) or above 2^63. Default configuration. Searches built by StandardQueryParser.parse are unaffected.

Mechanism, terms and numbers696 words

The flexible query parser reads a boost ^N into a BoostQueryNode holding a float. To print it, toQueryString() and toString() write a whole-number boost as a long and every other boost with Float.toString, which uses scientific notation below 0.001. So ab^0.0009 prints f:ab^9.0E-4. The grammar's number token has no exponent, so the same parser rejects that text with ParseException; before 9.0.0 it silently read it as a different query (f:ab^9 f:E-4). Above 2^63 the whole-number check overflows and 1e19 prints 1.0E19, with the same result. An exhaustive check ran the real printer on all 2,139,095,040 non-negative finite floats and classified its output with a model of the grammar's number token (validated against the real parser in 761,082 checks): exactly the boosts in (0, 0.001) and above 2^63 print text that does not parse back; every other boost round-trips. That breaks the documented promise that a node tree prints "a query string that can be parsed back by the original text parser". Original parsing and search are unaffected: the Query built from the original text is correct, and the impact appears only when printed node text is reused as query syntax. FuzzyQueryNode has the same pattern in a different method (ab~0.0001 prints ab~1.0E-4), and there the re-parse silently builds a different query. The fix prints a plain decimal (BigDecimal.toPlainString) in both nodes.

The code

  • Parse side. The grammar's Boost rule reads '^' <NUMBER> and builds new BoostQueryNode(node, Float.parseFloat(...)) (StandardSyntaxParser.jj:695-702, :115-116). The node stores a float (BoostQueryNode.java:33).
  • Print side (BoostQueryNode.java:80-84):
private CharSequence getValueString() {
  Float f = Float.valueOf(this.value);
  if (f == f.longValue()) return "" + f.longValue();   // whole numbers up to 2^63
  else return "" + f;                                  // Float.toString: 9.0E-4, 1.0E19
}

toQueryString returns the child's text + "^" + getValueString() (:91-95), and toString prints <boost value='...'> with the same helper (:86-89).

  • The grammar. NUMBER is (<_NUM_CHAR>)+ ( "." (<_NUM_CHAR>)+ )? with no exponent (StandardSyntaxParser.jj:160), while TERM (:161) accepts digits, ., E and -. By longest match 9.0E-4 lexes as one term, so Boost ::= '^' <NUMBER> (:688-703) fails.

Why exactly two ranges fail

Float.toString is plain only for magnitudes in [10^-3, 10^7). Every non-integral float is below 2^23, and integral floats up to 2^63 take the long branch, so [0.001, 2^63] always prints plainly. Below 0.001 the else branch prints 9.0E-4. Above 2^63 longValue() saturates at Long.MAX_VALUE, the equality fails and 1e19 prints 1.0E19. An exhaustive run printed all 2,139,095,040 non-negative finite floats with the real BoostQueryNode and classified each output with a model of the NUMBER token (the model agreed with the real parser in all 761,082 real-parser checks on a sample around the notation switches and at random). It found 981,668,462 failures in (0, 0.001) and 545,259,519 above 2^63, and none elsewhere. A grid through 15 host constructs (term, phrase, group, ranges, wildcard, regexp, ...) gave 5,700 ParseExceptions in 19,620 checks and no silent difference.

The same pattern in the fuzzy node

FuzzyQueryNode.toQueryString appends "~" + this.similarity (FuzzyQueryNode.java:50-57). The grammar's number after ~ is optional (StandardSyntaxParser.jj:712-729), so f:ab~1.0E-4 re-parses without error as ~ with the default 2 edits plus a separate term 1.0E-4. ab~0.0001 builds f:ab~1; its printed text builds f:ab~2 f:1.0e-4, which matched 1 document instead of 0 on a one-document index. That witness is narrow: the edit count changes only for 1-2 character terms (FuzzyQuery.floatToEdits already gives 2, the cap, for 3 or more, so abcd~0.0001 builds f:abcd~2). The extra clause is there for every term: under the default OR operator it widens the query (f:abcd~2 f:1.0E-4), under AND it narrows it (+f:abcd~2 +f:1.0E-4), and what it matches depends on the analyzer.

What is not affected

BoostQueryNodeBuilder copies getValue() into the BoostQuery (BoostQueryNodeBuilder.java:48), so StandardQueryParser.parse("ab^0.0009", "f") searches with boost 0.0009. Apart from the node printers themselves (composite nodes print their children; IntervalQueryNode.toString() returns its own toQueryString), Lucene puts toQueryString output only into exception messages (for example QueryTreeBuilder.java:161); it never parses it again.

Exposure and likelihood178 words
  • Who. Code that turns node trees back into text to log, display, cache, forward or rewrite a query and then parses it again. The usual source is the syntax tree from StandardQueryParser.getSyntaxParser().parse(text, field), plus hand-built BoostQueryNode trees and processed trees with a configured field boost (setFieldsBoost(Map.of("title", 0.0005f)) prints title:lucene^5.0E-4). No option is needed.
  • Trigger. A boost below 0.001 or above 2^63. Such boosts mostly come from code that computes them; a 2019 java-user post shows a client generating foo:bar^2.77E-4.
  • Effect. Since 9.0.0 the printed query cannot be parsed again (ParseException), a loud failure. From 2.9.0 to 8.11.4 it silently re-parsed as a different query (f:ab^9 f:E-4). The fuzzy leg is silent on current releases.
  • Not affected. Search results: the query is built from the exact float. Lucene's own code never re-parses the text.
  • Likelihood: rare. It needs both the print-and-reuse pattern and an unusual boost; it is deterministic when both are present, and no matching public report was found in the recorded searches, although the code dates from 2.9.0 (2009).

Minimal repro

One JUnit class with one test method on the public API: parse ab^0.0009 with StandardSyntaxParser, print it with toQueryString, and parse the printed text again expecting boost 0.0009. No index, nothing random; it fails with assertions on and off.

TestBoostQueryNodeToQueryString.java (one test class)37 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.queryparser.flexible.core.nodes;

import org.apache.lucene.queryparser.flexible.core.QueryNodeParseException;
import org.apache.lucene.queryparser.flexible.standard.parser.EscapeQuerySyntaxImpl;
import org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestBoostQueryNodeToQueryString extends LuceneTestCase {

  public void testSmallBoostParsesBack() throws Exception {
    QueryNode node = new StandardSyntaxParser().parse("ab^0.0009", "f");
    String printed = node.toQueryString(new EscapeQuerySyntaxImpl()).toString();
    // expected: text that parses back to boost 0.0009; actual: f:ab^9.0E-4, a ParseException
    try {
      QueryNode back = new StandardSyntaxParser().parse(printed, "f");
      assertEquals(0.0009f, ((BoostQueryNode) back).getValue(), 0f);
    } catch (QueryNodeParseException e) {
      throw new AssertionError("expected text that parses back, got " + printed, e);
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestBoostQueryNodeToQueryString -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
The printed text parses back to a BoostQueryNode with boost 0.0009.
Observed
AssertionError: expected text that parses back, got f:ab^9.0E-4, caused by INVALID_SYNTAX_CANNOT_PARSE, with assertions on and off.

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

More files, environment, full output298 words

The class sits in BoostQueryNode's package (org.apache.lucene.queryparser.flexible.core.nodes) so it can live next to the class upstream; it uses no package-private access. The seed only satisfies LuceneTestCase. It asserts only the documented round trip (the same boost after re-parsing), not an exact printed string.

Regression test: this same class (or its one method added to the existing TestQueryNode); it passes with the recommended fix.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; releases 10.5.2 and 10.6.0: fail; with the fix: passes
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; plain Float.toString, whose notation switch is in the Java SE API)
Command (environment)
cp $REPRO/TestBoostQueryNodeToQueryString.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/core/nodes/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestBoostQueryNodeToQueryString -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false identical)
TestBoostQueryNodeToQueryString > testSmallBoostParsesBack FAILED
    java.lang.AssertionError: expected text that parses back, got f:ab^9.0E-4
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:1EF405E732EBE7EA]:0)
        at org.apache.lucene.queryparser.flexible.core.nodes.TestBoostQueryNodeToQueryString.testSmallBoostParsesBack(TestBoostQueryNodeToQueryString.java:34)

        Caused by:
        INVALID_SYNTAX_CANNOT_PARSE: Syntax Error, cannot parse f:ab^9.0E-4:
            at app//org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.Boost(StandardSyntaxParser.java:1061)

# standalone runner, seed DEADBEEF: main 645d4fe14a20 fails with -ea and -da; 10.5.2 (-ea, -da) and 10.6.0 fail

# a 15-row boost test on stock main (gradle): printed text and whether it parses back
ab^0.001 -> f:ab^0.001 : ok
ab^0.5 -> f:ab^0.5 : ok
ab^0.0009999999 -> f:ab^9.999999E-4 : does not parse back (ParseException)
ab^0.00090 -> f:ab^9.0E-4 : does not parse back (ParseException)
ab^9223373000000000000 -> f:ab^9.223373E18 : does not parse back (ParseException)
ab^10000000000000000000 -> f:ab^1.0E19 : does not parse back (ParseException)
fieldsBoost 5.0E-4 -> title:lucene^5.0E-4 : does not parse back (ParseException)

# the same mistake in the fuzzy node, stock main (gradle)
java.lang.AssertionError: ab~0.0001 -> f:ab~1.0E-4: q1=f:ab~1 q2=f:ab~2 f:1.0e-4 expected:<0> but was:<1>

# before 9.0 (8.11.4, a public-API probe)
ab^0.0009 -> f:ab^9.0E-4 : parsed back as a different query: f:ab^9 f:E-4 (BooleanQueryNode)

Why it looks unintended

  • The javadoc promises "a query string understood by the query parser"; the package doc, one that "can be parsed back by the original text parser".
  • Upstream fixed the same broken round trip for range nodes as a bug (GITHUB#7865, Lucene 11.0.0).
  • ab^0.0009 is ordinary syntax: the parser accepts it and builds the right boost, then prints text it rejects.
The full argument167 words
  • The contract: QueryNode.java:26 ("convert to a query string understood by the query parser"); core/nodes/package-info.java:42-43 ("A query node tree can also generate a query string that can be parsed back by the original text parser, at this point only the standard lucene syntax is supported."). Both date from 2.9.0.
  • Upstream enforces it: GITHUB#7865, fixed by #14697 (merged 2025-05-29): "AbstractRangeQueryNode#toQueryString(EscapeQuerySyntax) now returns a string which can be parsed back into the original node" (lucene/CHANGES.txt:296-298, 11.0.0 bug fixes), with TestAbstractRangeQueryNode parsing the printed string back.
  • Ordinary input: StandardQueryParser's javadoc documents "a floating-point weight boost" (jones^2 OR smith^0.5) with no lower bound, and 0.0009 is even inside the class javadoc's own (stale) range "0.0 to 1.0".
  • The grammar was never meant to take exponents (a 2006 java-user reply about the classic parser: scientific notation would be a new feature; for the flexible grammar this is an inference), so the printer is the place to fix.
  • Tests: no upstream test references BoostQueryNode or asserts its printed form.
The devil's-advocate check272 words

The strongest case for "intended": in a 2017 java-user thread about range queries parsed twice, a committer replied: "There has never been a guarantee that going back and forth between a parsed query and its string representation is idempotent. so this isn't supported." Add that Float.toString is the normal Java rendering, that the grammar has no exponent on purpose, that Lucene uses toQueryString only for messages, that processor-only nodes print unparseable text ([TP[...]]), that the open #8667 (LUCENE-7616) calls the method "a design error" that "could never work as currently designed" (for custom syntaxes), and that BoostQuery.toString prints E-notation too.

Our answer, and its limits: in the same thread a PMC member replied "Why don't we fix this in Lucene? ... Could you open an issue and add a patch?", and the project later fixed exactly that range case as a bug (#7865 / #14697). The written contract says parse-back; Query.toString makes no such promise. #8667 is about custom syntaxes, while this is standard syntax to standard syntax, and StandardSyntaxParser produces BoostQueryNode from input it accepts. A grammar without exponents points at the printer as the fix site. This is a credible case for a bug, not a proof: #7865 concerned range nodes, so it is a precedent rather than a ruling on every node, and the processor-only nodes show the promise is not kept everywhere. Verifiers, the adjudication and a tie-break rejected the intended-behaviour case and a read-only cross-model review rated it weak; a later cross-model fact check rated the intended-behaviour case moderate for the boost leg, strong for boosts above 2^63 only, and weak for the fuzzy leg.

Suggested fix

Recommended Print a plain decimal in both nodes (validated)

Print a plain decimal: in BoostQueryNode.getValueString(), replace the Float.toString fallback with new BigDecimal(f.toString()).toPlainString() (Infinity and NaN unchanged), and do the same in FuzzyQueryNode.toQueryString(). 0.0009 then prints 0.00090, which parses back; boosts that printed correctly before are unchanged. It passes the repro and the whole queryparser module.

private CharSequence getValueString() {
  Float f = Float.valueOf(this.value);
  if (f == f.longValue()) return "" + f.longValue();
  // The grammar's NUMBER token has no exponent: print a plain decimal, never Float.toString's
  // E-notation (9.0E-4, 1.0E19), which does not parse back.
  else if (f.isInfinite() || f.isNaN()) return "" + f;
  else return new BigDecimal(f.toString()).toPlainString();
}
All options and trade-offs3 options · 568 words

Two variants of the printer fix were written and validated in a scratch worktree of 645d4fe14a20; the grammar-side alternative is described but not run.

Print a plain decimal in both nodes (validated) (recommended)

In BoostQueryNode.getValueString() keep the whole-number branch and Float.toString for Infinity and NaN, and otherwise print new BigDecimal(f.toString()).toPlainString(). In FuzzyQueryNode.toQueryString() print new BigDecimal(Float.toString(similarity)).toPlainString() for finite values in both return lines. +14/-3 in the two files, including the imports. The boost half alone fixes this repro; the fuzzy half fixes the merged fuzzy leg.

private CharSequence getValueString() {
  Float f = Float.valueOf(this.value);
  if (f == f.longValue()) return "" + f.longValue();
  // The grammar's NUMBER token has no exponent: print a plain decimal, never Float.toString's
  // E-notation (9.0E-4, 1.0E19), which does not parse back.
  else if (f.isInfinite() || f.isNaN()) return "" + f;
  else return new BigDecimal(f.toString()).toPlainString();
}

Trade-offs

  • BigDecimal(Float.toString(f)) is the exact decimal Float.toString chose, so its plain form parses back to the identical float. Text that was already plain is unchanged (0.5, 2.0).
  • Cosmetic: 0.0009 prints 0.00090 and the fuzzy ~0.0001 prints ~0.00010 (the trailing zero comes from 9.0E-4); 1e19 prints 10000000000000000000.
  • Printing only; one BigDecimal per printed non-integral value. Parsing does not change.
  • Visible output: toString() shares the helper, so <boost value='9.0E-4'> becomes <boost value='0.00090'>; exact-string tests, snapshots or log parsers that captured the old text see the new one (only for values that printed in E-notation). Very small values become long plain decimals: Float.MIN_VALUE prints as 48 characters (it still parses back).
  • Out of scope: negative, NaN and infinite boosts. The grammar cannot produce them (no sign after ^) and BoostQuery rejects them (BoostQuery.java:40-41); a hand-built node prints f:ab^-0.5, f:ab^NaN or f:ab^Infinity, which does not parse back with or without the fix.

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31): stock, the minimal test fails with assertions on and off, the 15-row boost test fails 2 of 2 and the fuzzy leg's test fails 2 of 2. With the fix: the minimal test passes with assertions on and off; the 15-row test passes and every control prints exactly as before; the fuzzy test passes 2 of 2; TestQueryNode passes; the whole queryparser module passes (30 classes, 533 tests, 0 failures); checkGoogleJavaFormat, ecjLintMain and forbiddenApisMain pass for the module.
  • Earlier: a model of the boost fix had 0 failures over all 2,139,095,040 non-negative finite floats and caught a lossy mutant; the boost half passed the hunt's, a verifier's and the registration check's tests, and upstream TestQPHelper, TestAbstractRangeQueryNode and TestQueryNode (43 of 43).

The same, plus stripTrailingZeros() in the boost node (validated, cosmetic)

new BigDecimal(f.toString()).stripTrailingZeros().toPlainString() in getValueString(), so ab^0.0009 prints ab^0.0009, the input back.

Trade-offs

Values already printed plainly are unchanged (a non-integral Float.toString result has no trailing zero). Do not use it in the fuzzy node: it would change today's ~2.0 to ~2.

What the checks validated

Same scratch worktree: the minimal test passes; the 15-row test passes and prints f:ab^0.0009, f:ab^0.0001 and title:lucene^0.0005; the whole queryparser module passes (533 tests, 0 failures).

Teach the grammar exponents (not recommended, not run)

Not run.

Trade-offs

Old printed text would parse, but it changes accepted syntax and how terms such as 1e5 are tokenized, for a bug in the printer. A 2006 list reply calls scientific notation a new feature.

Downstream note

The query built from the original text is unchanged; only services that log, cache or forward printed node trees as the canonical query and parse them again are hit: they get text their own parser rejects for such boosts, and for the fuzzy node a silently different query.

How it was found

While verifying the sibling slop-node bug, reviewers noticed that the boost node uses the same printing helper on a real float. A parse, print and re-parse probe over the node printers then showed a^0.0009 printing a^9.0E-4, and an exhaustive check of every non-negative float found exactly two failing ranges.

The method in full132 words

Method: non-formal first signal (a differential round-trip probe), then bounded-exhaustive translation validation of the stock printer and a public-API JUnit.

  • Signal. Code reading during the verification of LFV-892 (slop nodes print their int through a float) noted the lead; a planning probe (parse, toQueryString, re-parse, compare) executed it on stock main.
  • Exhaustive check. All 2,139,095,040 non-negative finite floats through the real BoostQueryNode printer against a model of the NUMBER token (the model agreed with the real parser on 761,082 checks): exactly (0, 0.001) and (2^63, max] fail; 3,515 s on 4 threads. A host-construct grid through StandardQueryParser (19,620 checks) found only loud failures.
  • Confirmation. An independent reproducer, a devil's-advocate verifier, read-only cross-model reviews (OpenAI GPT-5.6), an adjudication, the version-history runs, a prior-report search, a current-main reproducer and a tie-break checked it.

Prior reports, history and checks

Prior reports

No matching public report found in the recorded searches (416 logged queries on 2026-10-31 over GitHub, JIRA, the mailing lists and the web). Related: GITHUB#7865, the same round-trip contract fixed for range nodes.

Searches4 searches

Verdict new, for both the boost and the merged fuzzy leg: in the recorded searches, no issue, PR, JIRA entry or list thread says that BoostQueryNode or FuzzyQueryNode prints numbers in E-notation or that their printed text does not parse back; no open PR touches either node or the grammar. Closest items: #8667 (open design issue on toQueryString for custom syntaxes), #9064 (open proposal to bound boosts to [2^-10, 2^10]; it would not remove the low leg, since 2^-10 prints 9.765625E-4), and java-user posts of 2006 and 2019 about E-notation boosts typed into the classic parser's input. Open PRs #16853 and #16892 change other round trips in this module.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits and code (apache/lucene, lucene-solr, lucenenet, solr, two downstream search engines, all of GitHub), plus the scripted prior-report search's list, JIRA and downstream rows2026-10-31a scripted prior-report search with 5 query variants (255 rows: GitHub, commit, downstream, list, JIRA and web placeholders) plus 73 manual GitHub searches; the file lists of all 409 open apache/lucene PRs; the 53 items updated since 2026-10-28no report; related: #7865 / #14697 (range-node round trip), #8667, #9064, #16853, #16892
Mailing lists and JIRA2026-10-31manual: dev@, java-user@, issues@, commits@ (52 queries) and LUCENE JIRA (12 queries); the tool's own list and JIRA rows are in the first rowno report; java-user threads of 2006 and 2019 (E-notation boosts as input) and 2017 (range round trip)
Web2026-10-3118 manual queries (they replace the tool's 30 web placeholders, which the tool cannot run)nothing relevant
Earlier search2026-10-28278 recorded queries, plus the finder's, a verifier's and the adjudication's searchesnothing

History

The printer dates from the flexible parser's first release, 2.9.0 (2009), and never changed. Up to 8.11.4 the printed text silently re-parsed as a different query (f:ab^9 f:E-4); since 9.0.0, after a grammar cleanup, it fails with ParseException.

Versions tested5 rows

Introduced by 343992fcbb4 "LUCENE-1567: New flexible QueryParser framework." (2009-08-03, the original contribution, GitHub #2641), which created BoostQueryNode with this printer, the exponent-free NUMBER token and the parse-back contract. Later commits only changed new Float to Float.valueOf (835de0b44d4) and reformatted it (2cbf261032d). Symptom changed by 3a92e1b93eb (LUCENE-9528 grammar cleanup, #10568, first released in 9.0.0), which removed the lexer's Boost state: before it, 9.0 became the boost and E-4 a new term. That commit is neither the cause nor a fix. A bytecode scan of all 148 final release jars from 2.9.0 to 10.6.0 (4.0.0-ALPHA and 4.0.0-BETA not scanned) finds the same printer in every one.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20; 10.6.0; 10.5.2 fails loudlyminimal repro fails (prints ab^9.0E-4, ParseException on re-parse)
9.0.0, 9.1.0, 9.12.3, 10.0.0, branch_10_6 fails loudlyprobe or finder's test: ParseException on re-parse
2.9.0, 3.6.2, 4.0.0, 7.7.3, 8.11.4 silentprobe: prints f:ab^9.0E-4, which re-parses as a different query, f:ab^9 f:E-4
2.4.1 and earlier not affectedno flexible query parser
main + fix not affectedevery check passes

Independent checks

An independent reproducer, a devil's-advocate verifier, an adjudication and a tie-break found it real; a read-only cross-model review (OpenAI GPT-5.6) agreed, and a second returned a procedural "not real" because it could not run code. This doc's gradle run reproduced it and validated the fix.

Checks and verdicts376 words

Every executed check ran on stock builds. The earlier cross-model reviewers could only read code, and separate runs re-ran the repro; the cross-model fact check of this doc ran one public-API probe.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (probe, exhaustive check, grid, JUnit) candidate2-test JUnit fails 2 of 2 on main with assertions on and off, and on branch_10_6, 10.5.2 and 9.12.3; exactly two failing ranges over every non-negative finite float; fix passes.
Reproducer (own JUnit, own package) real5 tests including a plain-decimal control: 4 of 5 fail on main (on and off), 10.5.2 and 9.12.3; the fix passes 5 of 5.
Cross-model review (OpenAI GPT-5.6, intended lens) real, medium confidenceRead-only; rated the works-as-designed case weak; a separate run re-ran the JUnit (2 of 2 fail) and a public-API probe (controls pass, E-notation rows fail).
Refute review (OpenAI GPT-5.6) not real (procedural, low)Could not run anything and said its verdict is procedural; agreed on the root cause; not counted.
Devil's advocate realBuilt the 2017 "isn't supported" argument and showed why it fails; JUnit 2 of 2 fail on main, 10.5.2 and 9.12.3.
Adjudication confirmed, high confidenceRe-ran the independent reproduction's JUnit with a new seed (5 run, 4 fail) and the finder's with assertions off (2 of 2 fail).
Current-main reproducer and tie-break realMinimal test on 645d4fe14a20, 10.5.2 and 10.6.0; a 15-row test hitting both switch points; probes on 9.0.0, 8.11.4 and 4.0.0; upstream flexible-parser tests pass 43 of 43 with the fix.
This doc reproduced, fix validatedGradle in a scratch worktree of 645d4fe14a20: stock fails; both fix variants pass the repro and the whole queryparser module.
Fact check of this doc (independent) passed, no errorsRe-ran the minimal test on main (assertions on and off) and 10.6.0, the documented gradle command verbatim (stock fails, the fix passes), and its own public-API probe on main, 9.0.0 and 8.11.4.
Cross-model fact check (OpenAI GPT-5.6) real; wording fixesRan a public-API probe on the 645d4fe14a20 jars (ab^0.001 round-trips, ab^0.0009 prints f:ab^9.0E-4 and does not parse). Found one low-severity wording error and asked for narrower claims and more caveats; all are addressed in this version.

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

Finding record

LFV-975, snapshot of 2026-11-06

Registry title
Flexible query parser: BoostQueryNode#toQueryString prints a boost below 0.001 (or above 2^63) in E-notation, e.g. ab^0.0009 prints ab^9.0E-4, which StandardSyntaxParser cannot parse back
Area
queryparser / flexible parser node serialization (BoostQueryNode toQueryString/toString)
Priority
Low, rank 773: The fuzzy-node leg merged into this finding re-parses silently as a different query; the boost leg has a slightly more plausible trigger, but on 9.0 and later its re-parse fails loudly.
Impact class
minor
Impact note
A round-trip (contract) defect in a utility method, with no effect on the Query that StandardQueryParser.parse builds: applications that print flexible-parser node trees with toQueryString (trees from StandardSyntaxParser or StandardQueryParser.getSyntaxParser(), processed trees that carry an application-configured setFieldsBoost value, or hand-built BoostQueryNodes) and re-parse or forward that text get a ParseException for every boost below 0.001 or above 2^63 (9.0.0 and later; on 2.9.0 to 8.11.4 the re-parse silently builds a different query, e.g. boost 9 plus an extra term E-4); inside Lucene the output only feeds exception messages; default parser configuration, any scale.
Summary
You parse ab^0.0009 with the flexible StandardSyntaxParser, print the node tree with toQueryString, and expect text that parses back to the same boost, since QueryNode#toQueryString is documented as 'convert to a query string understood by the query parser' and the package documentation says the string 'can be parsed back by the original text parser'. Instead BoostQueryNode prints f:ab^9.0E-4, because its getValueString falls back to Float.toString, which uses E-notation below 0.001 and above 2^63 (ab^10000000000000000000 prints ab^1.0E19). The grammar's NUMBER token has no exponent, so re-parsing the printed text throws ParseException; the same happens to a per-field boost such as setFieldsBoost(Map.of("title", 0.0005f)) in the processed tree (title:lucene^5.0E-4).
Introduced
343992fcbb4b31249f07354014723f18d0508d8a (LUCENE-1567 'New flexible query parser', the original contribution of the flexible query parser, svn trunk r800191, no PR; issue migrated as apache/lucene#2641), which created BoostQueryNode with this getValueString ('if (f == f.longValue()) return "" + f.longValue(); else return "" + f;'), its toQueryString, and the grammar whose NUMBER token has no exponent; the printer is unchanged since (835de0b44d4, LUCENE-1833, 2009-10-02, new Float -> Float.valueOf, same behaviour; 2cbf261032d, LUCENE-9570, 2021, reformatting) [read: git log -L and git blame -C -C -M on getValueString, git log -G on the NUMBER token; bytecode: all 147 release jars; exec: 2.9.0 fails, 2.4.1 has no flexible parser]. The symptom changed in 9.0.0 with 3a92e1b93eb (LUCENE-9528 grammar cleanup, apache/lucene#10568, PR apache/lucene-solr#1879), which removed the grammar's Boost lexical state: the printed text no longer re-parses silently into a different query but throws ParseException. That commit is neither a fix nor a regression of the printer; its discussion does not mention exponents or boost printing. (2009-08-03 (343992fcbb4, author date = commit date; the printer defect); 2020-09-18 (3a92e1b93eb author date; symptom change from silent misparse to ParseException, first released in 9.0.0))
First affected version
2.9.0 [exec] (the first release with the API; 2.4.1 has no flexible query parser and no lucene-queryparser artifact). 2.9.0 to 8.11.4: the re-parse silently builds a different query [exec 2.9.0, 3.6.2, 4.0.0, 7.7.3, 8.11.4]; 9.0.0 and later: ParseException [exec].
Affected versions
every final lucene-queryparser release from 2.9.0 to 10.5.2 (the last release before 10.6.0; 147 releases, 4.0.0-ALPHA/BETA not checked), plus 10.6.0 (branch_10_6 4ae946e5a9b) and main a9779ef3c193; no fixed release. [exec] the finder's JUnit fails 2 of 2 on main (-ea and -da), branch_10_6, 10.5.2, 10.0.0, 9.12.3 and 9.1.0; the independent reproduction's JUnit fails 4 of 5 (its control passes) on main (-ea and -da), 10.5.2 and 9.12.3; the version-portable main() of the version-history runs (public API only, control ab^0.5 round-trips) fails on 2.9.0, 3.6.2, 4.0.0, 7.7.3 and 8.11.4 (silent misparse: f:ab^9.0E-4 re-parses as f:ab^9 f:E-4, f:ab^1.0E19 as f:ab^1 f:E19) and on 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (ParseException). [bytecode] all 147 SHA-1-verified release jars have the same Float.toString fallback in getValueString. [read] the other releases.
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 adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading; also reached by exhaustive check
Upstream and prior reports
none found (the prior-report record verdict new, searched read-only 2026-10-28, 278 recorded queries: three scripted prior-report searches (class and method terms; round-trip and symptom terms; setFieldsBoost and toString terms) over GitHub issues, PRs, commits and code, the Elasticsearch, OpenSearch, Solr and Lucene.NET trackers, the JIRA archive, the dev@, java-user@, issues@ and commits@ lists and the web, plus manual searches; the file lists of all 414 open apache/lucene PRs; every issue and PR created or updated in the last 48 h; the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches found nothing either; refreshed at the registration check 2026-10-28: gh search 'BoostQueryNode' (only #6938 and #7660), issues and PRs created since 2026-10-28 (none), upstream main head 645d4fe14a20 and the commits on BoostQueryNode.java (newest f2493e586b11, #14737, author date 2025-06-04, printing unchanged): nothing new). Related, not this bug: #8667 / LUCENE-7616 (open design issue: toQueryString always emits standard syntax, even for custom parsers; no numbers), #7865 / LUCENE-6807 fixed by PR #14697 (2025, range-node toQueryString round trip fixed as a bug; precedent for the parse-back contract), #6938 / LUCENE-5876 and #7660 / LUCENE-6602 (open null-check reports on BoostQueryNodeBuilder and getChild), #6554 / LUCENE-5491 (BoostQueryNodeProcessor exception on the parse side), #5444 / LUCENE-4378 (the boost NUMBER token has no sign), #10568 / LUCENE-9528 (the grammar cleanup that changed the symptom; exponents not discussed), java-user threads of 2006 ('strange behavior 4 query term boost') and 2019 ('Strange lucene parser result in boosted query with decimal in exponential form') about E-notation boosts typed into or generated for the classic parser's input (grammar side), and the 2017 java-user thread 'Range queries get misinterpreted when parsed twice via the "Standard" parsers' (round trip not guaranteed, then an issue and patch were requested). Not fixed upstream: getValueString unchanged on upstream main 645d4fe14a20, branch_11x, branch_10x and branch_10_6; no open PR touches flexible/core/nodes.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off through gradle in a scratch worktree and through a standalone runner; with the recommended fix it passes, as do a 15-row boost test, the fuzzy leg's test, TestQueryNode, the whole queryparser module (30 classes, 533 tests, 0 failures) and the module's lint. Releases 10.5.2 and 10.6.0 fail the minimal test (standalone runner). The hunt ran on main a9779ef3c193 (identical queryparser code); the history probe ran on releases from 2.9.0 to 10.6.0. Upstream main 2f424fd428da (2026-10-30), branch_11x 603af8703753 and branch_10_6 7e5aec4370e carry the same code [read].

Lucene and upstream sources

Finding record
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/BoostQueryNode.java:25-40 (class and constructor javadoc '0.0 to 1.0'), :80-84 (getValueString), :86-95 (toString, toQueryString); core/nodes/QueryNode.java:26; core/nodes/package-info.java:42-43; standard/parser/StandardSyntaxParser.jj:160 (NUMBER token) and :689-703 (Boost ::= '^' <NUMBER>); standard/processors/BoostQueryNodeProcessor.java:57; lucene/CHANGES.txt:289-291 (GITHUB#7865 parse-back fix for range nodes)
The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:115-116, :128-134, :160-161, :688-703, :712-729
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/BoostQueryNode.java:33, :80-84, :86-89, :91-95
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/FuzzyQueryNode.java:50-57
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/BoostQueryNodeBuilder.java:48; flexible/core/builders/QueryTreeBuilder.java:161
  • Java SE API, Float.toString(float) (plain notation for magnitudes in [10^-3, 10^7))
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java:261-271 (floatToEdits); lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/IntervalQueryNode.java:53-54
Who is affected
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:172 (getSyntaxParser); flexible/standard/processors/BoostQueryNodeProcessor.java:57
Why it looks unintended
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/QueryNode.java:26; flexible/core/nodes/package-info.java:42-43
  • a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:296-298 (GITHUB#7865); lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/nodes/TestAbstractRangeQueryNode.java
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:161-174; flexible/core/nodes/BoostQueryNode.java:29, :39, :67
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test (grep BoostQueryNode: 0 hits)

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the minimal repro's output.

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.