- The flexible
StandardQueryParserworks in two stages:StandardSyntaxParserturns query text into a tree of query nodes, then processors and builders turn the tree into a LuceneQuery. - Every node can print itself back as query text with
toQueryString; the package docs say that text "can be parsed back by the original text parser". An application can use this to rewrite a parsed tree and pass it on as text. - Since 9.1.0 the syntax has interval functions, for example
fn:ordered(a b),fn:wildcard(ab* 5)(a glob and an optional maxExpansions) andfn:fuzzyTerm(abc 1 5)(a term, then optional maxEdits and maxExpansions). An interval node printsfield:followed by its function'stoString().
fn:fuzzyTerm(abc) parses to maxEdits 2 and maxExpansions 128; toQueryString prints them as one number, abc 2128; parsing that back reads maxEdits 2128 and throws. Select the still to open it at full size. Generated with AI, checked against the code.- Parse
fn:fuzzyTerm(abc)with the flexibleStandardSyntaxParser. The node gets the defaults maxEdits 2 and maxExpansions 128. Print the tree withtoQueryString. - You expect text that
StandardQueryParserparses back to the same query, such asf:fn:fuzzyTerm(abc 2 128): the parser's docs promise "a query string that can be parsed back by the original text parser". - Instead you get
f:fn:fuzzyTerm(abc 2128).FuzzyTerm.toStringuses the format"fn:fuzzyTerm(%s %d%s)", with no space before the second number. Parsing it reads 2128 as maxEdits and throwsIllegalArgumentException: max edits must be 0..2, inclusive; got: 2128. - Two sibling printers fail the same way:
fn:wildcard(ab* 5)prints asfn:wildcard(ab* maxExpansions:5)(syntax error),fn:or("a:b" c)loses its quotes (syntax error), and a quoted literal containing the ideographic space U+3000 loses its quotes and silently parses back as two words instead of a phrase; a literal with a closing parenthesis, such as"a)(b", also comes back as a different query with no error. - The query that
parse()builds is not affected (it is built from the tree, not from this text); the wrong text reaches applications that print node trees and parse that text again, and Lucene's own error messages (fn:fuzzyTerm(abc 12)for maxEdits 1 and maxExpansions 2). Every release since 9.1.0.
Applications that turn flexible-parser node trees with fn: interval functions back into text with toQueryString and parse, store or forward that text: every fn:fuzzyTerm fails, as do wildcards with maxExpansions and some quoted literals; others (with U+3000 or a )) change meaning with no error. The Query that parse() builds is unaffected; Lucene's error messages show the misprint.
Mechanism, terms and numbers719 words
The flexible query parser documents that a query node tree "can also generate a query string that can be parsed back by the original text parser", and QueryNode#toQueryString is "convert to a query string understood by the query parser". For interval functions this fails. IntervalQueryNode#toQueryString prints the field, a colon and the function's toString(), and three of those methods print text the parser rejects or reads differently: FuzzyTerm runs maxEdits and maxExpansions together (fn:fuzzyTerm(abc) prints as fn:fuzzyTerm(abc 2128), which re-parses as maxEdits 2128 and throws), Wildcard prints maxExpansions:5 where the grammar takes a bare number, and AnalyzedText quotes a literal only when Java's \s matches, so literals with : or a keyword print bare (syntax error) and some re-parse with no error as a different query (U+3000 splits a phrase into two words; a ) inside a literal closes the function early). The Query that StandardQueryParser.parse builds is unaffected; applications that round-trip node text and Lucene's own exception messages get the wrong text. Every release from 9.1.0 to 10.6.0 and main; a fix of +12/-4 lines in the three printers passes the repro and the whole queryparser module.
IntervalQueryNode#toQueryStringisString.format(Locale.ROOT, "%s:%s", field, source)(IntervalQueryNode.java:47-50): the field, a colon and the function'stoString(). It ignores itsEscapeQuerySyntaxargument, andIntervalQueryNode#toStringdelegates to it (:52-55), so there is no separate display form.FuzzyTerm.toString(FuzzyTerm.java:52-60) formats"fn:fuzzyTerm(%s %d%s)"with the term,maxEditsand theIntegermaxExpansions: no space before the second number, so 2 and 128 print as2128and 1 and 5 as15. The grammar readsfn:fuzzyTerm(abc 2128)as termabcwith maxEdits 2128 (StandardSyntaxParser.jj:659-679), and building the query throws "max edits must be 0..2, inclusive" (FuzzyAutomatonBuilder.java:38-39, the throw at:39).Wildcard.toString(Wildcard.java:44-51) prints the raw glob and" maxExpansions:" + maxExpansions. The grammar takes the glob as a term, number or quoted token followed by an optional bare number (StandardSyntaxParser.jj:641-657), somaxExpansions:5is a syntax error, and a quoted glob such as"a b*"loses its quotes.AnalyzedText.requiresQuotes(AnalyzedText.java:53-55) quotes a literal only ifPattern.compile("[\\s]")finds a match. The lexer is stricter: a term cannot start with whitespace (including U+3000) or any of+ - ! ( ) : ^ @ < > = [ ] " { } ~ \ /(StandardSyntaxParser.jj:131-133) and ends at any of them except+and-(:134); it readsAND/&&,OR/||andNOT/!as operators (:144-146) and treats U+3000 as whitespace (:135), which Java's\sdoes not match by default. So most such literals print bare and fail to parse, some come back as a different query, and a literal such as"x-y"still round-trips.FuzzyTermreuses this check for its term.
Field body, executed on stock builds with assertions on and off:
| input | toQueryString | parsing that back |
|---|---|---|
fn:ordered(a fn:extend(b 1 2)) (control) | body:fn:ordered(a fn:extend(b 1 2)) | same query |
fn:fuzzyTerm(abc) | body:fn:fuzzyTerm(abc 2128) | IllegalArgumentException "... got: 2128" |
fn:fuzzyTerm(abc 1 5) | body:fn:fuzzyTerm(abc 15) | IllegalArgumentException "... got: 15" |
fn:wildcard(ab* 5) | body:fn:wildcard(ab* maxExpansions:5) | syntax error |
fn:or("a:b" c) | body:fn:or(a:b c) | syntax error |
fn:or("x<U+3000>y" z) | body:fn:or(x<U+3000>y z) | no error, but or(x,y,z) instead of or(BLOCK(x,y),z) |
Also executed, with assertions off: fn:or("a)(b" c) prints as body:fn:or(a)(b c) and parses back with no error as body:a (body:b body:c) instead of body:or(BLOCK(a,b),c); fn:or("x-y" z) round-trips; and the upstream test's own input fn:fuzzyTerm(dfe 2 128) (TestStandardQPEnhancements.java:161) prints as body:fn:fuzzyTerm(dfe 2128) and fails to re-parse.
Over a grid of 8,352 interval expressions (18 leaves, including quoted literals with :, AND, &&, || and U+3000, and every depth-1 function over them), 6,100 do not round-trip on main, 121 of them as a silently different query; with the recommended fix, none. The grid is a test design, not a usage frequency.
StandardQueryParser.parse builds the Query from the tree and never re-parses this text. In-tree, toQueryString output appears only in exception messages (QueryTreeBuilder.java:161, BooleanQueryNodeBuilder.java:72, MatchAllDocsQueryNodeBuilder.java:42, MatchNoDocsQueryNodeBuilder.java:42, AllowLeadingWildcardProcessor.java:74). For example, with a clause limit of 2, parse("a b fn:fuzzyTerm(abc 1 2)", "body") throws "Too many boolean clauses, the maximum supported is 2: body:a body:b body:fn:fuzzyTerm(abc 12)".
Exposure and likelihood178 words
- Who. Applications that use the flexible parser's node tree as an intermediate form: parse user text with
StandardSyntaxParser, inspect or rewrite the tree, then print it withtoQueryStringand parse, store, cache or forward the text. Also anyone reading logs or exception messages that print interval nodes:fn:fuzzyTerm(abc 12)hides the real maxEdits 1 and maxExpansions 2. - Default vs opt-in. Default parser configuration and public API; the trigger needs the
fn:interval syntax and an application that re-parses node text (no in-tree caller does). - Effect. An exception on the second parse, or, for some quoted literals, a different query with no error: with U+3000 a two-word phrase becomes two separate words, and
fn:or("a)(b" c)comes back asbody:a (body:b body:c). The results of the originalparse()are unaffected. - Scale. Any index; deterministic string formatting.
- How likely: rare. It needs both the niche
fn:syntax and a re-parse oftoQueryStringtext; the documented searches found no prior public report since 9.1.0 (2022-03). Two outside ports of the parser copyFuzzyTerm.toStringunchanged; no report was found there either.
One JUnit class on Lucene's test framework, one test method, public API only: parse fn:fuzzyTerm(abc), print the tree with toQueryString, parse the printed text with StandardQueryParser and compare with the direct parse. Deterministic, single thread, StandardAnalyzer from core.
/*
* 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.standard;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryparser.flexible.standard.parser.EscapeQuerySyntaxImpl;
import org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestFuzzyTermToQueryString extends LuceneTestCase {
public void testToQueryStringParsesBack() throws Exception {
StandardQueryParser parser = new StandardQueryParser(new StandardAnalyzer());
String printed =
new StandardSyntaxParser()
.parse("fn:fuzzyTerm(abc)", "f")
.toQueryString(new EscapeQuerySyntaxImpl())
.toString();
Query reparsed;
try {
reparsed = parser.parse(printed, "f");
} catch (Exception e) {
throw new AssertionError(
"expected toQueryString output that parses back, but '" + printed + "' fails: " + e, e);
}
assertEquals(parser.parse("fn:fuzzyTerm(abc)", "f"), reparsed);
}
}
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestFuzzyTermToQueryString -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
- The printed text parses back to the same query as
fn:fuzzyTerm(abc). - Observed
'f:fn:fuzzyTerm(abc 2128)' fails: java.lang.IllegalArgumentException: max edits must be 0..2, inclusive; got: 2128, with assertions on and off.
More files, environment, full output229 words
The test prints the node tree of fn:fuzzyTerm(abc) on field f and parses the printed text. The try/catch only puts the printed string into the failure message (the cause is kept); with a fix, the final assertEquals checks that the re-parsed query equals the direct parse. It covers the fuzzyTerm part, the least arguable one; the wildcard and literal cases are shown in the table above and were executed by the hunt's and a verifier's tests.
Regression test: this same class; it passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; releases 10.6.0 and 9.1.0: fail; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; plain Java string formatting and parsing) |
cp $REPRO/TestFuzzyTermToQueryString.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestFuzzyTermToQueryString -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false fails the same way in the standalone runner)
> Task :lucene:queryparser:test FAILED
TestFuzzyTermToQueryString > testToQueryStringParsesBack FAILED
java.lang.AssertionError: expected toQueryString output that parses back, but 'f:fn:fuzzyTerm(abc 2128)' fails: java.lang.IllegalArgumentException: max edits must be 0..2, inclusive; got: 2128
at __randomizedtesting.SeedInfo.seed([DEADBEEF:D6B50425F7113193]:0)
at org.apache.lucene.queryparser.flexible.standard.TestFuzzyTermToQueryString.testToQueryStringParsesBack(TestFuzzyTermToQueryString.java:38)
Caused by:
java.lang.IllegalArgumentException: max edits must be 0..2, inclusive; got: 2128
at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.FuzzyAutomatonBuilder.<init>(FuzzyAutomatonBuilder.java:39)
at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.FuzzyQuery.getFuzzyAutomaton(FuzzyQuery.java:191)
# standalone runner, seed DEADBEEF: main 645d4fe14a20 and 10.6.0 fail with -ea and -da, 9.1.0 fails with -eatoQueryStringis documented as "convert to a query string understood by the query parser"; the package docs promise text that "can be parsed back".abc 2128is not a display form under any reading, and, on inspection, every other interval function prints its grammar syntax.- 11.0 fixed the same round-trip failure for range nodes as a bug (GITHUB#7865).
The full argument211 words
- The contract:
QueryNode.java:26("convert to a query string understood by the query parser") andcore/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");EscapeQuerySyntaxexists "to allow the QueryNode to escape the queries, when the toQueryString method is called" (EscapeQuerySyntax.java:22-23). - The inputs are accepted, tested syntax: CHANGES 9.1.0 lists LUCENE-10223 (interval functions) and LUCENE-10414 ("Add fn:fuzzyTerm interval function to flexible query parser",
lucene/CHANGES.txt:4415-4417,:4452-4453); upstream tests parsefn:wildcard(foo* 128)("Explicit maxExpansions") andfn:fuzzyTerm(dfe 2 128)(TestStandardQPEnhancements.java:97-102,:156-162); quoted literals are documented ("Quoted or unquoted character sequences ...",intervalfn/package-info.java:149). The package docs list onlyfn:wildcard(glob)and notfn:fuzzyTerm, so "accepted, tested syntax" is the accurate wording. - The code aims for parseable output: on inspection, the other interval functions print their grammar syntax (the controls round-trip and the 8,352-expression grid shows no failure outside the three printers; there is no dedicated round-trip suite over all functions) and
AnalyzedTextalready quotes when it thinks it must. - Precedent: "GITHUB#7865 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; PR #14697 with regression tests).
The devil's-advocate check172 words
The strongest case for "display only": IntervalFunction#toString has no javadoc; the only upstream test that touches the output prints it "for visual diagnostics" and asserts nothing (TestStandardQPEnhancements.java:180-184); maxExpansions:N looks like a deliberate human-readable label; a 2017 java-user reply said a round trip between a parsed query and its string form was never guaranteed; open #8667 calls the toQueryString design flawed; and no in-tree caller re-parses the text.
Why it fails: IntervalQueryNode#toQueryString, the method whose javadoc promises parser syntax, returns exactly these strings, and IntervalQueryNode#toString delegates to it, so even if IntervalFunction#toString were diagnostic only, toQueryString would still need the fix. The 2017 reply was answered in the same thread with "Why don't we fix this in Lucene?", and the project later fixed the range-node case as a bug (GITHUB#7865). #8667 is about nodes built by custom syntax parsers, while the package docs promise the round trip for "the standard lucene syntax". And abc 2128 cannot be a deliberate display choice. All verifiers and the adjudication rejected the intended-behaviour case.
Recommended Make the three printers emit grammar syntax (+12/-4, validated)
Make the three printers emit the grammar's own syntax: "fn:fuzzyTerm(%s %d %d)" in FuzzyTerm, the bare number (" " + maxExpansions) and a quoted glob when needed in Wildcard, and quote every literal the lexer would not read back as one term in AnalyzedText.requiresQuotes. +12/-4 lines; it passes the repro and the whole queryparser module.
// FuzzyTerm.toString()
"fn:fuzzyTerm(%s %d %d)",
// Wildcard.toString()
AnalyzedText.requiresQuotes(wildcard) ? '"' + wildcard + '"' : wildcard,
maxExpansions == 0 ? "" : " " + maxExpansions);
// AnalyzedText.requiresQuotes(String term)
return term.isEmpty()
|| Pattern.compile("[\\s\\u3000+\\-!():^@<>=\\[\\]\"{}~\\\\/]").matcher(term).find()
|| term.equals("AND") || term.equals("OR") || term.equals("NOT")
|| term.equals("&&") || term.equals("||") || term.startsWith("fn:");All options and trade-offs3 options · 655 words
Two fixes were written and validated in a scratch worktree of 645d4fe14a20: the full fix (recommended) and a smaller numbers-only fix; a dedicated serializer is described but not written.
The change touches FuzzyTerm.java, Wildcard.java and AnalyzedText.java (the hunt's fix sketch, reformatted by applyGoogleJavaFormat). fn:fuzzyTerm prints both numbers separated (fn:fuzzyTerm(abc 2 128)); fn:wildcard prints the bare number and quotes a glob that needs it; a literal is quoted when it is empty, contains whitespace including U+3000, contains any character a term cannot start with, equals AND, OR, NOT, && or ||, or starts with fn: (redundant, since : is already covered). That covers every literal the lexer would not read back as one term, and a few it would: + and - may follow the first character of a term, so fn:or(a-b c) now prints as fn:or("a-b" c). Quoting more than needed changes the text but not its meaning: a quoted and an unquoted literal build the same node. Apart from such extra quotes, every other printed form is unchanged.
// FuzzyTerm.toString()
"fn:fuzzyTerm(%s %d %d)",
// Wildcard.toString()
AnalyzedText.requiresQuotes(wildcard) ? '"' + wildcard + '"' : wildcard,
maxExpansions == 0 ? "" : " " + maxExpansions);
// AnalyzedText.requiresQuotes(String term)
return term.isEmpty()
|| Pattern.compile("[\\s\\u3000+\\-!():^@<>=\\[\\]\"{}~\\\\/]").matcher(term).find()
|| term.equals("AND") || term.equals("OR") || term.equals("NOT")
|| term.equals("&&") || term.equals("||") || term.startsWith("fn:");Trade-offs
- Changes
toStringonly; nothing on a search path. The pattern is compiled per call, as today; astatic final Patternwould be a natural cleanup. - A hand-built node whose literal holds a raw double quote still prints unparsably (the inner quote is not escaped). The parser itself keeps backslashes inside interval literals (a separate grammar bug); when that is fixed, the printer must also escape
"and\inside quotes, so the two fixes should agree. More generally, the public constructors can build nodes the current syntax cannot represent, so the fix is validated for parser-originated input and the tested grid, not as a general serializer. - Not a complete serializer fix:
IntervalQueryNode#toQueryStringstill ignores itsEscapeQuerySyntaxargument, and thefield:prefix is still printed unescaped (LFV-356, a separate fix site).
What the checks validated
Gradle in a scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31). Stock: the minimal test fails, a verifier's 8-test round-trip class fails 5 of 8. With the fix:
- the minimal test passes with assertions on and off;
- the verifier's 8-test class (
fuzzyTermdefaults and explicit arguments, wildcard with maxExpansions, the"a:b"and U+3000 literals, 3 controls) passes 8 of 8; - the whole
:lucene:queryparser:testpasses: 30 classes, 533 tests, 0 failures, 1 skipped; checkGoogleJavaFormat,ecjLintMainandforbiddenApisMainfor queryparser pass.
Earlier, on an identical copy of the four files: the finder's 5 tests, 132 upstream queryparser tests, 7 extra edge cases and the 8,352-expression round trip (0 failures) all pass with it.
the FuzzyTerm format "fn:fuzzyTerm(%s %d %d)" and the Wildcard suffix " " + maxExpansions; literal quoting unchanged, to be fixed together with the grammar-side unescaping.
Trade-offs
- Removes the least arguable part with no interaction with the grammar bug.
- Leaves the quoted-literal cases (
"a:b","AND", U+3000) broken until then.
What the checks validated
Same run: the minimal test passes with assertions on; the verifier's 8-test class has exactly its 2 literal tests still failing. Module suite and lint not run for this subset.
Give IntervalFunction an explicit toQueryString(EscapeQuerySyntax) and call it from IntervalQueryNode#toQueryString, leaving toString free to be a display form.
Trade-offs
- Makes the contract explicit and lets the printer use the escaper once the grammar unescapes interval literals.
- More code and a new method on every interval function class, while, on inspection, the existing
toStringmethods already print grammar syntax for every other function.
An application that checks or rewrites a parsed node tree and then re-parses its toQueryString text should not rely on the text meaning the same as the tree, and should add restrictions as separate Query clauses (for example a FILTER clause) instead.
A hunt that checked the flexible parser against its documented syntax ran a bounded-exhaustive round trip: 8,352 interval expressions were parsed, printed with toQueryString and parsed again on the stock build. 6,100 did not come back the same (121 as a silently different query); with the fix sketch, none failed.
The method in full131 words
Method: semi-formal (a bounded-exhaustive check of the stock jars, confirmed by JUnit tests).
- Signal. While writing a reference model of the interval syntax, an exploratory probe printed
fn:fuzzyTerm(abc 2128)forfn:fuzzyTerm(abc). - Exhaustive check. Every expression of the unit's grid (18 leaves, including quoted literals with
:,AND,&&,||and U+3000, and every depth-1 function over them: 8,352 expressions) was parsed withStandardSyntaxParser, printed withtoQueryString, parsed again withStandardQueryParser, and the two queries compared withequals. - Confirmation. A public-API JUnit failed 4 of 5 on main with assertions on and off; then an independent reproducer, a devil's-advocate verifier, read-only cross-model reviews (OpenAI GPT-5.6) and an adjudication checked it, and a follow-up ran the version history, a prior-report search and a security screen.
No prior report found (two search-tool runs with 96 queries, 17 manual GitHub searches, all 408 open PRs, the lists, JIRA and the web on 2026-10-27; rechecked 2026-10-31). Related: #8667 (toQueryString design) and #7865 / PR #14697 (range-node round trip, fixed in 11.0).
Searches3 searches
Verdict new: no issue, PR, review comment, list thread, JIRA entry, commit or downstream report says that IntervalQueryNode#toQueryString or the interval-function toString methods print text that does not parse back. No open PR touches nodes/intervalfn or IntervalQueryNode.
Related, not this bug: #8667 / LUCENE-7616 (open since 2017: toQueryString always emits standard syntax, whatever syntax parser built the node); #7865 / PR #14697 (range nodes, fixed in 11.0, the precedent); open PRs #16853 and #16892 (the escaper, which IntervalQueryNode ignores); #16820 (open proposal to deprecate the flexible parser in Lucene 11); the 2017 java-user thread on range queries parsed twice.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene and three downstream search projects) | 2026-10-27 | two runs of a scripted prior-report search (51 and 45 queries, incl. class and method names), a re-read of the finder's run, 17 manual GitHub searches, the changed files of all 408 open PRs, the #429 and #668 review threads read in full | 137 and 344 distinct hits; the only high hit is the introducing PR #668; none is this bug |
| Mailing lists, JIRA and the web | 2026-10-27 | dev@, java-user@, issues@, commits@; the JIRA archive; 11 web searches | only the 2017 range-query thread and older toQueryString threads; nothing on interval functions |
| GitHub (recheck) | 2026-10-31 | 8 issue/PR searches (fuzzyTerm toString, IntervalQueryNode, toQueryString interval, fn:fuzzyTerm, requiresQuotes, ...), toQueryString newest first, the 25 newest issues and 25 newest PRs, tracking issue #16937 | nothing new |
Introduced in 9.1.0 by LUCENE-10223 (#429: the interval syntax, toQueryString and the literal quoting) and LUCENE-10414 (#668: fn:fuzzyTerm and the wildcard maxExpansions format); the four files are unchanged since, through 10.6.0 and main.
Versions tested4 rows
Introduced in two steps by Dawid Weiss: f725b27e12a "LUCENE-10223: interval support in standard syntax parser (#429)" (2021-11-11) added IntervalQueryNode#toQueryString, the whitespace-only quoting and the unquoted wildcard glob; f6cebac3337 "LUCENE-10414: Add fn:fuzzyTerm interval function to flexible query parser (#668)" (2022-02-10) added the fn:fuzzyTerm(%s %d%s) format and the maxExpansions: suffix. Their branch_9x backports both first shipped in 9.1.0. Not a regression: each defect is in the code that added its feature; the round-trip promise dates from the flexible parser itself (LUCENE-1567, 2009).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0 | affected | minimal repro fails, assertions on and off |
9.1.0 to 10.5.2, main a9779ef3c193 | affected | the same 7 misprints on 18 releases and main (portable probe); the minimal repro fails on 9.1.0; the other 14 releases have byte-identical defect files |
| 9.1.0 without LUCENE-10414 (emulation) | quoting only | only the literal and glob quoting misprints; no fn:fuzzyTerm or wildcard maxExpansions yet |
| 9.0.0 | not affected | no interval functions in the flexible parser (fn:... parses as a field named fn) |
An independent reproducer, a root-cause review by OpenAI GPT-5.6, an intended-behaviour devil's advocate and an adjudication found it real (adjudication: confirmed, high confidence); the only dissent was a procedural "not real" from a read-only reviewer that could not run Java. This doc's gradle run reproduced it and validated the fix.
Checks and verdicts363 words
Every executed check ran on stock builds; the cross-model reviewers could only read code, and separate runs re-ran the repro.
| Check | Verdict | What it found |
|---|---|---|
| Finder (bounded-exhaustive round trip, then a JUnit) | candidate | 5-test class fails 4/5 on main with assertions on and off, and on 10.5.2, 9.12.3 and 9.1.0; the control passes. Round trip: 6,100 of 8,352 fail, 0 with the fix sketch; the sketch passes 5/5 and 132 upstream tests. |
| Refute lens (OpenAI GPT-5.6, read-only) | not real (procedural, low) | Could not run Java, so its "not real" is procedural; agreed on the root cause ("Static inspection strongly supports a real defect"). A separate run re-ran the repro: 4/5 fail. |
| Intended lens (OpenAI GPT-5.6, read-only) | real (high) | The display-only argument rated moderate and failing; found that the fix sketch's quoting missed &&, || and U+3000 (widened and re-validated). |
| Independent reproducer | real | Own 8-test class from the claim: 5 fail on main with assertions on and off and on 10.5.2, 9.12.3, 9.1.0, including the silent U+3000 case; 3 controls pass; 8/8 with the fix. |
| Root-cause lens (OpenAI GPT-5.6, read-only) | real (medium) | First wrong state in the three printers, not the grammar. Separate runs: repro 4/5 fail, fix 5/5, 7 edge cases (4 fail on main, 7/7 with the fix), U+3000 fails on main and passes with the fix; caveat: a hand-built raw double quote still misprints. |
| Intended-behaviour devil's advocate | real | Re-ran the repro (4/5 fail, assertions on and off), built and rejected the display-only case, and showed the misprint in the parser's too-many-clauses message. |
| Adjudication | confirmed (high), minor | Re-ran the independent reproduction's class on a clean checkout with seed C0FFEE02: 5 of 8 fail with assertions on and off. |
| Minimal repro | still reproduces | One test on 645d4fe14a20: fails with assertions on and off, passes with the fix sketch. |
| This doc | real | Standalone runner: fails on main 645d4fe14a20, 10.6.0 and 9.1.0. Gradle in a scratch worktree: stock fails; the recommended fix passes the repro, the verifier's 8 tests, the whole queryparser module (533 tests) and lint; the numbers-only fix leaves only the 2 literal tests failing. |
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 488 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,306 words
- Registry title
- Flexible query parser: IntervalQueryNode#toQueryString prints some fn: interval functions in a form the parser rejects or reads as a different query (fn:fuzzyTerm(abc) prints as fn:fuzzyTerm(abc 2128))
- Area
- queryparser / flexible StandardQueryParser interval function nodes (IntervalQueryNode#toQueryString; intervalfn FuzzyTerm, Wildcard and AnalyzedText toString)
- Priority
- Low, rank 772: A minor toQueryString round-trip break for fn: interval nodes that matters only to applications that re-parse node text, which no in-tree caller does.
- 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 turn flexible-parser node trees with fn: interval functions back into text with toQueryString (or IntervalQueryNode#toString) and parse or forward that text get a string that does not parse for every fn:fuzzyTerm, for fn:wildcard with a maxExpansions argument and for quoted literals that hold a query-syntax character such as ':' or a keyword such as AND, and a silently different query for some quoted literals (one that holds U+3000, or a closing parenthesis as in "a)(b"); inside Lucene the output only feeds exception messages (for example the parser's own too-many-clauses message); default parser configuration, any scale, every release from 9.1.0 to 10.6.0 and main.
- Summary
- You parse fn:fuzzyTerm(abc), fn:wildcard(ab* 5) or fn:or("a:b" c) with the flexible syntax parser, print the node tree with toQueryString, and expect a string that StandardQueryParser reads back as the same query, as the flexible parser's package documentation promises ('a query string that can be parsed back by the original text parser'). Instead FuzzyTerm prints maxEdits and maxExpansions with no space between them (fn:fuzzyTerm(abc 2128), which re-parses as maxEdits 2128 and throws 'max edits must be 0..2'), Wildcard prints 'maxExpansions:5', a syntax error, and AnalyzedText quotes a literal only when it contains ASCII whitespace, so fn:or("a:b" c) prints the unparsable fn:or(a:b c). A quoted literal with the ideographic space U+3000 prints unquoted and re-parses, with no error, as two separate literals.
- Introduced
- f725b27e12a992f12aab12a8e8bfb9b31125bdae (main, 2021-11-11, by dweiss): 'LUCENE-10223: interval support in standard syntax parser (#429)' (issue LUCENE-10223 = GitHub #11259, PR apache/lucene#429), which added IntervalQueryNode#toQueryString (field + ':' + the function's toString, the EscapeQuerySyntax argument unused), the whitespace-only quoting rule AnalyzedText.requiresQuotes and the unquoted wildcard glob; and f6cebac3337926ca871b922241976a4ba4799c70 (main, 2022-02-10, by dweiss): 'LUCENE-10414: Add fn:fuzzyTerm interval function to flexible query parser (#668)' (GitHub #11450, PR #668), which added FuzzyTerm.toString with the format 'fn:fuzzyTerm(%s %d%s)' and the Wildcard ' maxExpansions:' suffix; the branch_9x backports ff9ee28c603 and 9a293da5967 are the commits that shipped in 9.1.0 [read: git blame -w on main and git log -S on the format strings and requiresQuotes find only these two commits; the split between them was also executed, on 9.1.0 jars with the five queryparser files of LUCENE-10414 reverted (an emulation, not a release build)] (2021-11-11 (LUCENE-10223: literal quoting, unquoted glob, IntervalQueryNode#toQueryString); 2022-02-10 (LUCENE-10414: fuzzyTerm numbers run together, 'maxExpansions:N' suffix))
- First affected version
- 9.1.0 (tagged 2022-03-21) [exec]: both introducing changes first shipped there; 9.0.0 has no interval functions in the flexible parser at all (fn:... parses as a field named 'fn') [exec], so no release that has the feature is unaffected
- Affected versions
- every release with fn: interval functions in the flexible StandardQueryParser, 9.1.0 through 9.12.3 and 10.0.0 through 10.6.0, and main a9779ef3c193 and 645d4fe14a20 (unreleased 11.0); never fixed. [exec 2026-10-31] the minimal repro fails 1 of 1 on 10.6.0 with -ea and -da and on main 645d4fe14a20; the four defect files have the same blobs at releases/lucene/10.6.0 as at 645d4fe14a20. [exec] a portable main() probe reports the same 7 misprints, with its control OK, on 18 releases (9.1.0, 9.2.0, 9.3.0, 9.4.2, 9.5.0, 9.6.0, 9.7.0, 9.8.0, 9.9.2, 9.10.0, 9.11.1, 9.12.3, 10.0.0, 10.1.0, 10.2.2, 10.3.2, 10.4.0, 10.5.2) and on main (-ea and -da), byte-identical per-case output; the finder's JUnit fails 4 of 5 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit fails 5 of 8 on 9.1.0, 9.12.3 and 10.5.2 (-ea) and on main (-ea and -da). [read] the other 14 releases of the range (9.4.0, 9.4.1, 9.9.0, 9.9.1, 9.11.0, 9.12.0-9.12.2, 10.2.0, 10.2.1, 10.3.0, 10.3.1, 10.5.0, 10.5.1): FuzzyTerm.java, Wildcard.java, AnalyzedText.java and IntervalQueryNode.java have identical blob hashes at all 32 release tags 9.1.0..10.5.2 and on main.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal; also reached by exhaustive check
- Upstream and prior reports
- none found (2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (A: 51 queries, 137 distinct hits, 0 high, 7 medium; B: 45 queries on the class and method names, 344 distinct hits, 1 high = the introducing PR #668, 11 medium) over GitHub issues, PRs and commits, downstream repositories (Elasticsearch, OpenSearch, Solr), the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; a re-read of the finder's run (51 queries, 101 hits); 17 manual GitHub searches; the file lists of all 408 open apache/lucene PRs (none touches nodes/intervalfn or IntervalQueryNode); 11 general web searches; the review threads of #429 and #668 read in full. Upstream main has the code unchanged (nodes/intervalfn last changed by #668 in 2022, IntervalQueryNode.java only by #429 in 2021). Related, not this bug: #8667 / LUCENE-7616 (open since 2017, by others: toQueryString always emits standard syntax whatever syntax parser built the node; a design issue about custom syntax parsers that predates interval functions), #7865 / PR #14697 (the range-node round trip, fixed as a bug in 11.0; the precedent), open outside PRs #16853 and #16892 (escaper and escape helper; IntervalQueryNode#toQueryString ignores the escaper, so neither changes interval output), #16820 (open proposal to deprecate the flexible parser in Lucene 11) and the 2017 java-user thread 'Range queries get misinterpreted when parsed twice via the Standard parsers' (range nodes). Two outside ports (Kotlin, Go) copy FuzzyTerm.toString unchanged, with no report.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails with assertions on and off through a standalone runner, and with assertions on through gradle in a scratch worktree; with the recommended fix it passes, as do a verifier's 8-test round-trip class and the whole queryparser module. Releases 10.6.0 (assertions on and off) and 9.1.0 fail with the standalone runner. Earlier, the hunt ran a portable probe on 18 releases from 9.1.0 to 10.5.2 and on maina9779ef3c193. Upstream main2f424fd428da(2026-10-30) andbranch_11x603af8703753(2026-10-29) carry byte-identical defect files [read].
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/FuzzyTerm.java:52-60, Wildcard.java:45-51, AnalyzedText.java:45-55, standard/nodes/IntervalQueryNode.java:47-55, core/nodes/package-info.java:42-43, core/nodes/QueryNode.java:26-27 (read during the follow-up)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/IntervalQueryNode.java:47-55a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/FuzzyTerm.java:34-60; Wildcard.java:44-51; AnalyzedText.java:44-55a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:130-168 (lexer), :641-686 (IntervalWildcard, IntervalFuzzyTerm, IntervalText)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/FuzzyAutomatonBuilder.java:38-39; FuzzyQuery.java:54; lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:64a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/builders/QueryTreeBuilder.java:161; standard/builders/BooleanQueryNodeBuilder.java:72; MatchAllDocsQueryNodeBuilder.java:42; MatchNoDocsQueryNodeBuilder.java:42; standard/processors/AllowLeadingWildcardProcessor.java:74
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/QueryNode.java:26; core/nodes/package-info.java:42-43; core/parser/EscapeQuerySyntax.java:22-23a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:296-298 (11.0.0 Bug Fixes), :4415-4417 and :4452-4453 (9.1.0)a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestStandardQPEnhancements.java:97-102, :156-162, :180-184a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:149, :183
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:131-136, :649-650, :668-669, :684-685
- History
a Lucene clone git ls-tree at releases/lucene/9.1.0, releases/lucene/10.5.2, releases/lucene/10.6.0, origin/main 2f424fd428da and origin/branch_11x 603af8703753 (same blob hashes as 645d4fe14a20 for the four defect files)
- TestFuzzyTermToQueryString.java (one test class):
repro/LFV-748/src/org/apache/lucene/queryparser/flexible/standard/TestFuzzyTermToQueryString.java(43 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the minimal repro.
assets/LFV-748/bug.png(488 KB)
