- A Java
floathas 24 bits of precision, so it holds every whole number exactly only up to 2^24 = 16,777,216. - Above that, converting an
inttofloatrounds to the nearest float: 16,777,217 becomes 16,777,216, and every int from 2,147,483,584 to 2,147,483,647 becomes 2,147,483,648 (2^31), one more than the largestint.
"a b"~2147483647 is parsed into a slop node holding the exact int; printing it goes through a float and yields ~2147483648, which the parser rejects when it reads the text back. Select the still to open it at full size. Generated with AI, checked against the code.- Parse
f:"a b"~2147483647with the flexibleStandardSyntaxParser, the syntax parser behindStandardQueryParser. 2147483647 isInteger.MAX_VALUE, a historically suggested "any distance" slop. The tree'sSlopQueryNodekeeps it exactly:getValue()is 2147483647. - Print the tree with
toQueryString(new EscapeQuerySyntaxImpl()). You expect the input back: the method is documented to produce "a query string understood by the query parser". - Instead you get
f:"a b"~2147483648. The node converts itsintslop to aFloatbefore printing it, and a float holds every whole number exactly only up to 2^24 = 16,777,216, so 2147483647 rounds to 2^31. - Parsing that text again throws
NumberFormatException: For input string: "2147483648". Smaller slops change silently:~16777217prints~16777216, which parses back as slop 16777216. - Search is unaffected:
StandardQueryParser.parsebuilds aPhraseQuerywith slop 2147483647 from the original text. Only code that prints node trees (toQueryString,toString) and reuses the text sees it.
Applications that print a flexible-parser syntax tree or a hand-built slop node (toQueryString, toString) and then parse, compare or forward the text, when a phrase slop above 16,777,216 is one a float cannot hold (most likely the "any distance" ~2147483647). Default configuration. Searches built by StandardQueryParser.parse are unaffected.
Mechanism, terms and numbers486 words
The flexible query parser reads the phrase slop of "..."~N with Integer.parseInt into SlopQueryNode, whose value is an int. To print it, toQueryString() and toString() box the int into a Float and print the float's long value. A float is exact only up to 2^24, so 2,071,986,176 of the 2,147,483,648 non-negative slops print a different number: "a b"~16777217 prints ~16777216, and the 64 slops from 2147483584 to 2147483647, including the "any distance" value Integer.MAX_VALUE, print ~2147483648, which the same parser rejects with NumberFormatException. That breaks the documented promise that a node tree prints "a query string that can be parsed back by the original text parser". The Query built from the original text is correct, so searches are unaffected. The printer has not changed since 2.9.0, and the parsed "a b"~2147483647 has printed ~2147483648 in every release. Up to 9.0.0 the grammar rounded the slop itself, so that text still read back as the same slop; since 9.1.0, when the grammar started keeping the exact int, the text is rejected and other parsed slops above 2^24 print a different value. PhraseSlopQueryNode has the same printer. The fix is Integer.toString(this.value) in both classes.
- Parse side, exact int. The grammar builds
new SlopQueryNode(q, parseInt(slop))(StandardSyntaxParser.jj:833), andparseIntisInteger.parseInt(token.image)(:119-121). - The node is int-typed:
private int value;(SlopQueryNode.java:35), the constructorSlopQueryNode(QueryNode query, int value)(:41) andint getValue()(:57-59). - Print side, through a float (
SlopQueryNode.java:61-65;PhraseSlopQueryNode.java:53-57is identical):
private CharSequence getValueString() {
Float f = Float.valueOf(this.value); // int -> float: rounds above 2^24
if (f == f.longValue()) return "" + f.longValue();
else return "" + f; // never taken for an int
}toQueryString returns the child's text + "~" + getValueString() (:73-76), and toString prints <slop value='...'> with the same helper (:68-70). The body is a copy of BoostQueryNode.getValueString (BoostQueryNode.java:80-84), where the value really is a float (:33).
- Exhaustively over every non-negative int: 2,071,986,176 of 2,147,483,648 slops print a different number, exactly the ints in [2^24, 2^31) that are not floats (2^31 - 2^24 - 7 x 2^23). The smallest is 16777217 -> 16777216; the largest error is 64.
- The 64 slops 2147483584..2147483647 print 2147483648, which
Integer.parseIntrejects, so re-parsing throwsNumberFormatException: For input string: "2147483648"fromStandardSyntaxParser.parseInt. - Hand-built nodes:
SlopQueryNode(q, 20000001).toString()shows<slop value='20000000'>;PhraseSlopQueryNode(q, 123456789)prints~123456792. - Only non-negative slops are in scope: the grammar's
NUMBERtoken has no sign (StandardSyntaxParser.jj:160) andPhraseQueryrejects a negative slop (PhraseQuery.java:173-174).
SlopQueryNodeBuilder copies getValue() into the PhraseQuery (SlopQueryNodeBuilder.java:47), so StandardQueryParser.parse("\"a b\"~2147483647", "f") searches with slop 2147483647. Inside Lucene, toQueryString output only goes into exception messages (for example QueryTreeBuilder.java:161). No processor or builder creates PhraseSlopQueryNode, so that half is reachable only through hand-built trees.
Exposure and likelihood246 words
- Who. Code that turns node trees back into text to log, display, cache, forward or rewrite a query and then parses or compares it. The usual source is the syntax tree from
StandardQueryParser.getSyntaxParser().parse(text, field), plus hand-builtSlopQueryNodeandPhraseSlopQueryNodetrees. No option is needed. - Trigger. A phrase slop above 16,777,216 that a float cannot hold. The most likely such value is the "any distance" slop
"..."~2147483647, which a 2004 java-user answer recommends ("red fox"~2147483647; a 2010 java-user thread uses the same value as aSpanNearslop in the XML query parser). How often applications use it was not measured. - Effect. For slops 2147483584..2147483647 the printed query cannot be parsed again (
NumberFormatException). Other slops above 2^24 that a float cannot hold parse into a slightly different query (off by at most 64); whether that changes matching or scoring for a given document was not measured.toString()shows the same wrong value, which can mislead debugging. - Not affected. Search results: the query is built from the exact int. In processed trees a
SlopQueryNodesurvives only above a tokenized or multi-phrase child (PhraseSlopQueryNodeProcessor.java:43-50), which prints in internal form ([TP[...]],[MTP[...]], commented as not re-parseable), so for them only the displayed number differs. The bug concerns raw syntax-parser trees and hand-built nodes. - Likelihood: rare. It needs both the print-and-reuse pattern and a huge slop; it is deterministic when both are present, and the recorded prior-report searches found no report.
One JUnit class with one test method on the public API: parse f:"a b"~2147483647 with StandardSyntaxParser and assert that toQueryString gives the input back. No index, no random input; it fails 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.queryparser.flexible.core.nodes;
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 TestSlopQueryNodeToQueryString extends LuceneTestCase {
public void testMaxIntSlopRoundTrips() throws Exception {
String query = "f:\"a b\"~2147483647";
QueryNode node = new StandardSyntaxParser().parse(query, "f");
// expected the parsed text back; actual ~2147483648 (the int slop is printed through a float)
assertEquals(query, node.toQueryString(new EscapeQuerySyntaxImpl()).toString());
}
}
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestSlopQueryNodeToQueryString -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
toQueryStringreturns the input,f:"a b"~2147483647.- Observed
expected:<f:"a b"~214748364[7]> but was:<f:"a b"~214748364[8]>, with assertions on and off.
More files, environment, full output253 words
The class sits in SlopQueryNode'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 assertion has no random input; LuceneTestCase still randomizes locale, timezone, codec and similarity, none of which affects the comparison, and the seed pins them. The test does not assert the NumberFormatException of re-parsing the printed text: that exception comes from the grammar's own Integer.parseInt, a separate parse-side issue.
Regression test: this same class (or its one method added to the existing TestQueryNode); it passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; releases 10.6.0, 9.1.0 and 9.0.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 int-to-float conversion) |
cp $REPRO/TestSlopQueryNodeToQueryString.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/core/nodes/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestSlopQueryNodeToQueryString -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 identical)
TestSlopQueryNodeToQueryString > testMaxIntSlopRoundTrips FAILED
org.junit.ComparisonFailure: expected:<f:"a b"~214748364[7]> but was:<f:"a b"~214748364[8]>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:BDECA60AB7A6F547]:0)
at app//org.apache.lucene.queryparser.flexible.core.nodes.TestSlopQueryNodeToQueryString.testMaxIntSlopRoundTrips(TestSlopQueryNodeToQueryString.java:29)
# standalone runner, seed DEADBEEF: main 645d4fe14a20 fails with -ea and -da; releases 10.6.0, 9.1.0 and 9.0.0 fail
# re-parsing the printed text (a verifier's test, same queryparser code)
printed string not re-parsable: f:"a b"~2147483648 -> java.lang.NumberFormatException: For input string: "2147483648"
at java.lang.Integer.parseInt(Integer.java:662)
at org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.parseInt(StandardSyntaxParser.java:110)
# a public-API probe on main
flexible-syntax "a b"~16777217 -> node 16777217 -> printed f:"a b"~16777216 -> reparsed 16777216
flexible-StandardQueryParser "a b"~2147483647 -> PhraseQuery slop 2147483647
hand-built SlopQueryNode(16777217) -> printed f:"a b"~16777216- 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".
- The slop is an
intin the grammar, field and getter; the float printer is a copy of the boost node's. - Since 9.1.0 the parser accepts
"a b"~2147483647but prints text it rejects.
The full argument144 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-297, 11.0.0 bug fixes), withTestAbstractRangeQueryNodeparsing the printed string back. - Int end to end:
Integer.parseIntin the grammar, the int field, constructor and getter,StandardQueryParser.setPhraseSlop(int)andPhraseQuery.Builder.setSlop(int). Nothing documents float precision for the slop. - Copy-paste artifact: identical to
BoostQueryNode.getValueString, whose field is afloat; for an int itselsebranch can never run. - Tests: no upstream test prints a slop above 2^24 or asserts the current output.
The devil's-advocate check255 words
The strongest case for "intended": Lucene's standard syntax has always read the slop as a float and truncated it. The classic QueryParser still does on main (QueryParserBase.java:802, (int) Float.parseFloat(...): "a b"~16777217 becomes slop 16777216), and the flexible grammar did the same until 9.0.0, so on 9.0.0 a parsed tree's printed text reads back as the same node and the float printer "matches the parser". toQueryString is also loosely honoured elsewhere (phrases in processed trees print as [TP[...]]), Lucene uses it only for messages, and the open #8667 (LUCENE-7616) calls the contract weak.
Why it fails: since 9.1.0 the flexible grammar reads an exact int, and the node is int-typed; a hand-built SlopQueryNode(q, 16777217) has never printed a value any Lucene parser reads back as 16777217; the documented contract is a re-parsable string, and upstream fixed another node for breaking it; #8667 is about custom parsers getting standard syntax back. Even on 9.0.0 the printer did not match the parser for the headline value: the parser stored 2147483647 (the (int) cast saturates float 2^31) and the printer showed ~2147483648, which read back as 2147483647 only because the cast saturated again; the minimal test fails on 9.0.0 too. The fix is also harmless under the float reading: a slop stored by a float-rounding parser prints the same digits, except the saturated Integer.MAX_VALUE, which then prints 2147483647 instead of 2147483648 and reads back as the same value. Verifiers and the adjudication rejected the intended-behaviour case; a read-only cross-model review rated it weak.
Recommended Print the int as an int (one line per class, validated)
Print the slop as an int: in SlopQueryNode and PhraseSlopQueryNode, getValueString() becomes return Integer.toString(this.value);. Every slop then prints exactly, and nothing changes for slops that already printed correctly. It passes the repro, the finder's tests and the whole queryparser module.
private CharSequence getValueString() {
return Integer.toString(this.value);
}All options and trade-offs2 options · 279 words
One fix was written and validated in a scratch worktree of 645d4fe14a20; the parser-side alternative is described but not run.
Replace the float detour in both getValueString() methods (+2/-6 in SlopQueryNode.java and PhraseSlopQueryNode.java). Covers toQueryString and toString of both nodes.
private CharSequence getValueString() {
return Integer.toString(this.value);
}Trade-offs
- Output is unchanged for every slop that printed correctly before (all of [0, 2^24] and the float-representable values above it); an exhaustive check found 0 mismatches over [0, 2^26) with the fix.
- Formatting only; not benchmarked.
- It does not change parsing: the grammar's
Integer.parseIntstill throws a rawNumberFormatExceptionon a fractional or out-of-range slop, a separate parse-side issue.
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 and the finder's 3-test class fails 3 of 3. With the fix: the minimal test passes with assertions on and off; the finder's class (which also coversPhraseSlopQueryNodeand 16777217) passes 3 of 3;TestQueryNodepasses; the whole queryparser module passes (30 classes, 533 tests, 0 failures);checkGoogleJavaFormat,ecjLintMainandforbiddenApisMainpass for the module. - Earlier, the hunt, a verifier and the registration check validated the same change on
a9779ef3c193(7 of 7 and 3 of 3 pass), and the standalone runner on645d4fe14a20.
Not run.
Trade-offs
Hides the printer bug for parsed trees only, leaves hand-built nodes wrong and changes accepted syntax. How the grammar should treat fractional or out-of-range slops is a separate parse-side question.
Services that log, cache or forward the printed node tree as the canonical query get, for such slops, text their own parser rejects or reads as a slightly different slop; matching and access control are unchanged.
A sweep of rarely discussed public queryparser APIs. While writing the reference for a round-trip check (Integer.toString, the inverse of the grammar's Integer.parseInt), the finder noticed the float conversion, then ran every non-negative int slop through toQueryString: 2,071,986,176 of 2,147,483,648 printed a different number, exactly the ints a float cannot hold.
The method in full116 words
Method: semi-formal (bounded-exhaustive translation validation of the stock jar, then a public-API JUnit).
- Signal. Code reading of
getValueStringwhile writing the reference printer for a round-trip harness. - Exhaustive check. Every int in [0, 2^31) through
SlopQueryNode.toQueryStringand every 64th throughPhraseSlopQueryNode, againstInteger.toString: 2,147,483,648 checks in 118 s on 4 threads, 2,071,986,176 mismatches, equal to the analytic count, smallest 16777217, 64 unparseable. With the fix shadowed: 0 mismatches over [0, 2^26). - Confirmation. A 3-test public-API JUnit failed on main with assertions on and off; 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 (206 logged queries on 2026-10-28 over GitHub, JIRA, the mailing lists and the web; rechecked 2026-10-31). Related: GITHUB#7865, the same round-trip contract fixed for range nodes.
Searches4 searches
Verdict new: no issue, PR, JIRA entry or list thread says that the slop nodes print the slop through a float or that ~2147483647 prints ~2147483648; no open PR touches either node or the grammar. Every upstream item naming SlopQueryNode or PhraseSlopQueryNode is about null checks (#7686, #7685, #7660, closed PR #12935) or is a javadoc/build mention.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits and code (apache/lucene, all of GitHub, Solr and two downstream search engines) | 2026-10-28 | a scripted prior-report search (class, method and symptom terms) plus about 30 manual searches and 4 code searches; the file lists of all 410 open and 620 closed-unmerged apache/lucene PRs | no report; related: #7865 / #14697 (range-node round trip), #8667 (custom-parser design issue), #11259 / #429 (the grammar change that exposed it) |
| Mailing lists and JIRA | 2026-10-28 | dev@, java-user@, issues@, commits@; LUCENE and SOLR JIRA | no report; a 2004 java-user answer recommends "red fox"~2147483647 (a 2010 thread uses 2147483647 as a SpanNear slop in the XML query parser) |
| Web | 2026-10-28 | 8 queries | nothing relevant |
| GitHub (recheck) | 2026-10-31 | the 31 issues and PRs created since 2026-10-28 (#16926-#16956); the file lists of all 409 open PRs; issue searches for the class names (PR searches rate-limited) | nothing new; no open PR touches the slop nodes or the grammar |
The float printer dates from 2.9.0 (2009): hand-built nodes print wrong in every release, and the parsed "a b"~2147483647 has always printed ~2147483648. Up to 9.0.0 the grammar rounded the slop itself, so that text read back as the same slop; since 9.1.0 (LUCENE-10223, exact-int grammar) it is rejected, and other parsed slops above 2^24 can read back changed.
Versions tested7 rows
Printer introduced by 343992fcbb4 "LUCENE-1567: New flexible QueryParser framework." (2009-08-03, the original contribution, GitHub #2641), which added both nodes with an int slop and the float printer copied from the boost node; it has not changed since (835de0b44d4 only swapped new Float for Float.valueOf). Re-parse broken for parsed queries by f725b27e12a on main / ff9ee28c603 on branch_9x, "LUCENE-10223: interval support in standard syntax parser (#429)" (dweiss, 2021-11-11, first released in 9.1.0), which changed the grammar action from (int) Float.parseFloat(slop.image) to parseInt(slop). The PR does not discuss the slop. Before it, the parser rounded the slop at parse time and the (int) cast saturated: on 9.0.0 "a b"~16777217 is stored, printed and read back as 16777216, and "a b"~2147483647 is stored as 2147483647, printed as ~2147483648 and read back as 2147483647. Not a regression of the printer.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0; 9.1.0 | affected | minimal repro fails (prints ~2147483648); the printed text is rejected on re-parse |
9.12.3, 10.0.0, 10.5.2, branch_10_6, main a9779ef3c193 | affected | probe: hand-built nodes 4 of 4 wrong; parsed slops read back wrong 2 of 2 |
| 2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0 | prints wrong, reads back | hand-built nodes 4 of 4 wrong; the parsed ~2147483647 prints ~2147483648 (the minimal repro fails on 9.0.0, assertions on and off), but the printed text reads back as the same slop because the old grammar rounded and saturated the slop at parse time |
9.0.0 with only the slop line switched to Integer.parseInt | swap | parsed slops read back wrong 2 of 2, as in 9.1.0 |
| 9.1.0 with only the slop line switched back to the float action | swap | parsed slops read back as the same slop |
| 2.4.1 and earlier | not affected | no flexible query parser |
| main + fix | not affected | every check passes |
An independent reproducer, a devil's-advocate verifier and an adjudication 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 verdicts317 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 (exhaustive check, JUnit) | candidate | 2,071,986,176 mismatches over every non-negative int, equal to the analytic count; 3-test JUnit fails 3 of 3 on main with assertions on and off, and on 10.5.2, 9.12.3 and branch_10_6; passes with the fix. |
| Reproducer (own JUnit, own package) | real | 7 tests including a control with six slops up to 2^24: 6 of 7 fail on main (assertions on and off), 10.5.2 and 9.12.3; 5 of 7 on 9.0.0; the fix passes 7 of 7. |
| Cross-model review (OpenAI GPT-5.6, intended lens) | real, low confidence | Read-only; rated the "debug rendering" case weak; a separate run re-ran the JUnit: 3 of 3 fail. |
| Refute review (OpenAI GPT-5.6) | not real (procedural, low) | Could not run anything and said its verdict is procedural; rated the intended case weak. Not counted. |
| Devil's advocate | real | Probes on main, 9.1.0 and 9.0.0; built the classic-parser argument and showed why it fails. |
| Adjudication | confirmed, high confidence | Re-ran the independent reproduction's JUnit with a new seed: 7 run, 6 fail, assertions on and off. |
| This doc | reproduced, fix validated | Gradle in a scratch worktree of 645d4fe14a20; minimal test also on 10.6.0 and 9.1.0. |
| Fact check of this doc | bug real; text fixes applied | Re-ran the minimal test on main (assertions on and off, standalone runner and the gradle command), 10.6.0, 9.1.0 and 9.0.0 (fails there too) and the fix (passes); re-derived the exhaustive count. Its five text corrections, including the 9.0.0 version boundary, are applied here. |
| Cross-model fact check (OpenAI GPT-5.6) | bug real; wording fixes applied | Reviewed the brief against the code and the run records; its corrections (the 2^24 wording, the 9.0.0 round trip, the scope caveats) are applied here. |
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 465 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,412 words
- Registry title
- Flexible query parser: SlopQueryNode and PhraseSlopQueryNode print their int slop through a float, so toQueryString changes every slop above 2^24 (~16777217 prints ~16777216, ~2147483647 prints ~2147483648, which the parser rejects)
- Area
- queryparser / flexible parser node serialization (SlopQueryNode and PhraseSlopQueryNode toQueryString/toString)
- Priority
- Low, rank 774: This toQueryString round-trip break needs a slop above 2^24 and only matters to apps that re-parse node text.
- 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 or toString (trees from StandardQueryParser.getSyntaxParser(), processed trees, or hand-built SlopQueryNode/PhraseSlopQueryNode) and parse, compare or forward that text get a different slop for every slop above 2^24 (rounded to the nearest float, at most 64 off below 2^31, e.g. 16777217 -> 16777216), and for the 64 slops 2147483584..2147483647, including the 'any distance' value Integer.MAX_VALUE, the string ~2147483648, which the same parser rejects with a NumberFormatException on re-parse (through the parse-side path of LFV-027); inside Lucene the output only feeds exception messages; default parser configuration, any scale; parser-built trees since 9.1.0, hand-built nodes since 2.9.0.
- Summary
- You parse "a b"~16777217, or the 'any distance' phrase "a b"~2147483647, with the flexible StandardSyntaxParser, print the node tree with toQueryString, and expect the same slop back, 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 SlopQueryNode prints ~16777216, and ~2147483648 for the second query, which the same parser rejects with NumberFormatException, because getValueString boxes the int slop into a Float and prints its long value, so every slop above 2^24 is rounded to the nearest float. PhraseSlopQueryNode has the same helper (123456789 prints ~123456792), and toString() of both nodes uses it too.
- 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 added SlopQueryNode and PhraseSlopQueryNode with an int slop and a getValueString() copied from BoostQueryNode, whose value really is a float ('Float f = new Float(this.value); if (f == f.longValue()) return "" + f.longValue(); else return "" + f;'); 835de0b44d4 (LUCENE-1833, 2009-10-02) only changed new Float to Float.valueOf. [read: git blame of both getValueString bodies, git log -S 'new Float(this.value)' and -S 'Float.valueOf(this.value)', git log --follow on SlopQueryNode.java (16 commits, none changes the printing); exec: 2.9.0 fails, 2.4.1 has no flexible parser]. Exposed for parser-built trees by f725b27e12a992f12aab12a8e8bfb9b31125bdae on main / ff9ee28c60354fdb50337192883082192c8e494c on branch_9x (LUCENE-10223 'interval support in standard syntax parser', PR #429, first released in 9.1.0), which changed the grammar's slop action from '(int) Float.parseFloat(slop.image)' to 'parseInt(slop)', so the parser started to keep the exact int and the old printer became visible for parsed queries; the PR, its comments and the CHANGES entry do not mention the slop [exec: one-line swaps of the release's own generated StandardSyntaxParser.java: 9.0.0 with Integer.parseInt fails like 9.1.0, 9.1.0 with the float action passes]. Not a regression of the printer, which never changed. (2009-08-03 (343992fcbb4, author date = commit date; the printer defect); 2021-11-11 (f725b27e12a / ff9ee28c603 author date; the exposure for parser-built trees))
- First affected version
- 2.9.0 [exec] for hand-built SlopQueryNode/PhraseSlopQueryNode (the first release with the API; 2.4.1 has no flexible query parser and no lucene-queryparser artifact). 9.1.0 [exec] for parser-built trees: a parsed slop above 2^24 prints a different number and ~2147483647 prints the unparseable ~2147483648 (2.9.0 to 9.0.0 round-trip parsed trees [exec 2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0], because their grammar itself rounded the slop through a float before storing it).
- Affected versions
- every release from 2.9.0 to 10.5.2 (the last release before 10.6.0), plus branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193; no fixed release. [exec] a version-portable main() probe (public API only) prints the same 4 wrong hand-built slops in every executed version (16777217 -> 16777216, 20000001 -> 20000000, 2147483647 -> 2147483648, PhraseSlopQueryNode 123456789 -> 123456792), with its 7 controls passing, on 2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6 and main (-ea; 9.1.0 parser group and main also -da); its 2 parser round-trip checks fail from 9.1.0 on (9.1.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6, main) and pass on 2.9.0, 3.6.2, 4.0.0, 8.11.4 and 9.0.0; the finder's JUnit fails 3 of 3 on 9.1.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6 and main (-ea and -da), and on 9.0.0 (one test at its parse-side precondition); the independent reproduction's JUnit fails 6 of 7 on main (-ea and -da), 10.5.2 and 9.12.3 and 5 of 7 on 9.0.0. [read] the other releases (same printer in all of them, same parse action within each era).
- 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: 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, 206 recorded queries: two scripted prior-report searches (class and method terms; symptom terms) over GitHub issues, PRs, commits and code, the Elasticsearch, OpenSearch and Solr trackers, JIRA LUCENE and SOLR, the dev@, java-user@, issues@ and commits@ lists and the web; the file lists of the 410 open apache/lucene PRs and of all 620 closed-unmerged PRs; the finder's scripted prior-report search (117 hits, 0 high), 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: the 22 apache/lucene issues and PRs created since 2026-10-26 (#16907-#16928), the file lists of the 42 open PRs updated since 2026-10-26 (none touches lucene/queryparser/flexible), issue/PR searches for SlopQueryNode, PhraseSlopQueryNode, 'slop toQueryString', 'slop 2147483648' and 'slop 16777216', and the commits on both files: nothing new). Related, not this bug: #8667 / LUCENE-7616 (open design issue: toQueryString always emits standard syntax, even for custom parsers; no slop or numbers), #7865 / LUCENE-6807 fixed by #14697 (2025, range-node toQueryString round trip; precedent for the parse-back contract), #7686 / #7685 / #7660 (LUCENE-6628 / 6627 / 6602, null checks in the same classes) and closed-unmerged PR #12935, #15889 (merged 2026-04-01, the last change to both files, getValueString untouched), #11259 / PR #429 (LUCENE-10223, the grammar change that exposed it; slop not discussed), java-user threads of 2004 and 2010 that recommend the slop 2147483647 (context for the trigger). Not fixed upstream: getValueString unchanged on main c5ab3e57b9ba (SlopQueryNode.java:61-65); a9779ef..c5ab3e57b9ba touches no queryparser file.
- 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 fix it passes, as do the finder's 3-test class,TestQueryNodeand the whole queryparser module (30 classes, 533 tests, 0 failures). Releases 10.6.0 and 9.1.0 fail the minimal test (standalone runner). The hunt ran on maina9779ef3c193(identical queryparser code), and its history probe on 11 refs from 2.9.0 to main. Upstream main2f424fd428da(2026-10-30),branch_11x603af8703753andbranch_10_67e5aec4370ecarry the same code [read].
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/SlopQueryNode.java:57-76 (int getValue, Float-based getValueString, toString and toQueryString), PhraseSlopQueryNode.java:53-57, QueryNode.java:26, core/nodes/package-info.java:42-43, standard/parser/StandardSyntaxParser.jj:833 (parseInt(slop)), 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:119-121, :833a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:160 (NUMBER); lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java:173-174 (negative slop rejected)a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/SlopQueryNode.java:35, :41, :57-59, :61-65, :68-70, :73-76a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/PhraseSlopQueryNode.java:28, :53-57, :60-67, :70-73a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/BoostQueryNode.java:33, :80-84a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/SlopQueryNodeBuilder.java:47; flexible/core/builders/QueryTreeBuilder.java:161
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:172 (getSyntaxParser); flexible/core/nodes/TokenizedPhraseQueryNode.java:49-59a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/PhraseSlopQueryNodeProcessor.java:43-50; flexible/core/nodes/TokenizedPhraseQueryNode.java:47-59; flexible/standard/nodes/MultiPhraseQueryNode.java:52-63
- 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-43a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:296-297 (GITHUB#7865); lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/nodes/TestAbstractRangeQueryNode.java:35-47a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:802; flexible/standard/StandardQueryParser.java:434a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/BoostQueryNode.java:33, :80-84
- History
a Lucene clone: git show f725b27e12a (StandardSyntaxParser.jj slop action); gh api repos/apache/lucene/pulls/429 (author dweiss)
- TestSlopQueryNodeToQueryString.java (one test class):
repro/LFV-892/src/org/apache/lucene/queryparser/flexible/core/nodes/TestSlopQueryNodeToQueryString.java(31 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the minimal repro's output.
assets/LFV-892/bug.png(465 KB)
