- Java floats and doubles have two zeros, -0.0 and +0.0.
-0.0 == 0.0is true, butFloat.compare(-0f, 0f)is -1: it puts -0.0 just below +0.0. FloatPointandDoublePointindex and compare values in theFloat.compare/Double.compareorder, so -0.0 and +0.0 are two different, adjacent values in a point range.- Point range queries take inclusive bounds only. To run
f < 0the parser must usef <= v, where v is the largest value below +0.0 in that order, which is -0.0.
f < 0 inclusive with Math.nextDown(0) = -1.4E-45, which ends the range just before -0.0, so the document holding -0.0 is missed. Select the still to open it at full size. Generated with AI, checked against the code.- Index one document with
FloatPoint("f", -0f)and giveStandardQueryParsera FloatPointsConfigforf. Ordinary arithmetic produces -0.0:-1 * 0.0andMath.rint(-0.4)both give it. - Parse
f:[* TO 0}(orf<0). Points order values likeFloat.compare, where -0.0 sits just below 0, so you expect 1 hit. - You get 0 hits. Point ranges take inclusive bounds, so the parser rewrites
< 0as<= Math.nextDown(0), which is -1.4E-45 and skips -0.0: the query becomesf:[-Infinity TO -1.4E-45]. f:[0 TO *]andf>=0exclude the document too, so it is in neither half of a split at zero. Doubles behave the same, and{-0.0 TO *]misses +0.0 the same way.
Code that parses query text with the flexible StandardQueryParser and a Float or Double PointsConfig (opt-in; Luke's search tab does this; the classic QueryParser builds no point queries), when it asks the most natural signed question, x < 0, of an index where some values are exactly -0.0. Any index size.
Mechanism, terms and numbers432 words
With the flexible StandardQueryParser and a PointsConfig of type Float or Double, an exclusive bound at zero skips the opposite-signed zero. PointRangeQueryNodeBuilder makes an exclusive bound inclusive with Math.nextUp / Math.nextDown, which step in IEEE order, where -0.0 equals +0.0; FloatPoint and DoublePoint order values like Float.compare / Double.compare, where -0.0 is a separate value just below +0.0. So the natural f:[* TO 0} ("f < 0") is built as f:[-Infinity TO -1.4E-45] and misses documents holding -0.0, which f:[0 TO *] excludes as well. LUCENE-7594 fixed the same pitfall in the core point classes in 2016 and added the FloatPoint/DoublePoint.nextUp/nextDown helpers, but the parser still calls Math.*. Silent, deterministic, any index size, in every release checked since 6.0.0.
- The syntax parser records
{/}and</>as exclusive flags;f<0becomes[* TO 0}(StandardSyntaxParser.jj:751-756). PointRangeQueryNodeProcessorparses each bound with the configuredNumberFormatand converts it withfloatValue()(:118-120) ordoubleValue()(:115-117). The literal0becomes +0.0; an open bound*staysnull.PointRangeQueryNodeBuilder.build, Float branch (:97-113): an open bound becomes -Infinity or +Infinity (:99-101,:107-109); thenif (minInclusive == false) { lower = Math.nextUp(lower); }(:102-104) andif (maxInclusive == false) { upper = Math.nextDown(upper); }(:110-112); thenFloatPoint.newRangeQuery(field, lower, upper)(:113), whose bounds are inclusive. The Double branch repeats it (:119-121,:127-129,:130).Math.nextDown(+0.0)is-Float.MIN_VALUE(-1.4E-45) andMath.nextUp(-0.0)is+Float.MIN_VALUE(1.4E-45): in IEEE order -0.0 counts as zero, so each step jumps over the other zero.
FloatPoint.nextUp/nextDown (FloatPoint.java:48-70) and the DoublePoint ones exist for exactly this: "Return the greatest float that compares less than f consistently with Float#compare. The only difference with Math#nextDown(float) is that this method returns -0f when the argument is +0f."
One document with f = -0f and d = -0d: f:[* TO 0} and f<0 parse to f:[-Infinity TO -1.4E-45], 0 hits; d:[* TO 0} and d<0 to d:[-Infinity TO -4.9E-324], 0 hits; f:[0 TO *], f>=0 and d:[0 TO *] also 0 hits. On four documents (-1, -0.0, +0.0, 1), f:{-0.0 TO *] parses to f:[1.4E-45 TO Infinity] and returns only 1, missing +0.0, and f:[* TO 0} OR f:[0 TO *] misses the -0.0 document. Only two of the four exclusive cases at zero are wrong: an upper bound at +0.0 and a lower bound at -0.0; {0 TO *] and [* TO -0.0} are right.
Exposure and likelihood160 words
- Who.
setPointsConfigMapwith a Float or Double type. Opt-in, but it is how Lucene's ownTestPointQueryParsersets up float fields and the path Luke uses when a user assigns a float or double type to a field (SearchImpl.java:221-240). - Trigger.
x:[* TO 0}orx<0on an index holding exactly -0.0, which ordinary arithmetic produces:-1 * 0.0,Math.rint(-0.4),Math.ceil(-0.5),Float.parseFloat("-0")and(float) -1e-50all give -0.0. The mirrored case,{-0.0 TO *]missing +0.0, needs -0.0 typed in the query and is rarer. - Effect. Silent wrong results: the documents holding exactly the opposite-signed zero are missing; no error; one document shows it; the same with assertions on and off. One numeric value per query, but every document holding it; every other finite bound is exact.
- Seen before, elsewhere. The same symptom was user-visible in another code base: elastic/elasticsearch#22167 (2016,
lt 0.0missing -0.0, in Elasticsearch's own range code, not this parser) is what led to LUCENE-7594.
One JUnit class, one test method, public API only: one document with FloatPoint("f", -0f), a StandardQueryParser with a Float PointsConfig, and the count of f:[* TO 0} must be 1. The failure message prints the parsed query. Single thread, ByteBuffersDirectory, deterministic.
/*
* 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 java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FloatPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestPointRangeExclusiveZeroBound extends LuceneTestCase {
public void testLessThanZeroMatchesNegativeZero() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new FloatPoint("f", -0f));
w.addDocument(doc);
}
StandardQueryParser parser = new StandardQueryParser();
parser.setPointsConfigMap(
Map.of("f", new PointsConfig(NumberFormat.getInstance(Locale.ROOT), Float.class)));
Query query = parser.parse("f:[* TO 0}", "f");
try (DirectoryReader reader = DirectoryReader.open(dir)) {
// expected: -0f < 0f in FloatPoint's Float.compare order, so 1 hit; actual: 0 hits
assertEquals("hits of " + query, 1, new IndexSearcher(reader).count(query));
}
}
}
}
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestPointRangeExclusiveZeroBound -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
- 1 hit: in the
Float.compareorder that points use, -0.0 is below 0. - Observed
hits of f:[-Infinity TO -1.4E-45] expected:<1> but was:<0>, with assertions on and off.
More files, environment, full output240 words
Oracle: the Float#compareTo order that FloatPoint.newRangeQuery documents, in which -0f < 0f. LuceneTestCase is only the base class; plain IndexWriterConfig and IndexSearcher, so nothing depends on the seed (DEADBEEF and 1234ABCD fail the same way). The query text holds no negative zero.
Regression test: this same class (or its one method added to TestPointQueryParser); it passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; 10.6.0, 10.5.2, 9.12.3, 9.1.0: fail; an equivalent main() probe fails on 14 releases 6.0.0 to 10.6.0, and the tagged releases in between carry the same builder code; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; plain float arithmetic) |
cp $REPRO/TestPointRangeExclusiveZeroBound.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestPointRangeExclusiveZeroBound -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)
> java.lang.AssertionError: hits of f:[-Infinity TO -1.4E-45] expected:<1> but was:<0>
> at __randomizedtesting.SeedInfo.seed([DEADBEEF:5CC102E284EE967E]:0)
> at org.apache.lucene.queryparser.flexible.standard.TestPointRangeExclusiveZeroBound.testLessThanZeroMatchesNegativeZero(TestPointRangeExclusiveZeroBound.java:49)
:lucene:queryparser:test (FAILURE): 1 test, 1 failure
# extra context, not the repro: one document with f = -0f and d = -0d, stock main jars
f:[* TO 0} -> f:[-Infinity TO -1.4E-45] hits=0
f<0 -> f:[-Infinity TO -1.4E-45] hits=0
f:[0 TO *] -> f:[0.0 TO Infinity] hits=0
f>=0 -> f:[0.0 TO Infinity] hits=0
d:[* TO 0} -> d:[-Infinity TO -4.9E-324] hits=0
d<0 -> d:[-Infinity TO -4.9E-324] hits=0
d:[0 TO *] -> d:[0.0 TO Infinity] hits=0- The
FloatPoint.newRangeQuerythe parser calls: "For exclusive ranges, pass nextUp(lowerValue) or nextDown(upperValue)", linkingFloatPoint's own helpers, and "Range comparisons are consistent with Float#compareTo". - LUCENE-7594 (2016, a bug fix): "Float/DoublePoint should not recommend using Math.nextUp/nextDown for exclusive ranges"; it fixed only the core classes.
- The -0.0 document matches neither
f<0norf>=0: wrong under either order of the zeros.
The full argument218 words
- The API the builder calls says how.
FloatPoint.java:210-213(DoublePoint.java:210-213the same), on the 1-DnewRangeQuerythat the builder calls at:113and:130: "Ranges are inclusive. For exclusive ranges, passnextUp(lowerValue)ornextDown(upperValue)" (links toFloatPoint.nextUp/nextDown), and "Range comparisons are consistent withFloat#compareTo(Float)". - Lucene decided this in 2016. #8645 (LUCENE-7594, Adrien Grand, reviewed by Dawid Weiss): "Float/Double points are supposed to be consistent with Double/Float.compare, so +0 is supposed to compare greater than -0. However Math.nextUp/nextDown is not consistent with Double/Float.compare". Listed under 6.4.0 Bug Fixes (
lucene/CHANGES.txt:8708-8710). Its commit18d53a43f7bchanged onlyFloatPoint,DoublePoint, CHANGES and two tests (coreTestPointQueries, sandboxTestHalfFloatPoint); nobody mentioned the parser. - Regression against the replaced path. LUCENE-7089 (#8145) added points to the flexible parser "the exact same way legacy numerics works". On 5.5.5 and on 6.0.0's own legacy path,
f:[* TO 0}matches the -0.0 document; on 6.0.0's points path it does not. - The parser's inclusive forms use the points order.
f:[0 TO *]excludes -0.0 andf:[* TO -0.0]includes it; only the exclusive step switches to IEEE order. - Tests. Core asserts
FloatPoint.nextDown(0f)is -0f andnextUp(-0f)is 0f (TestPointQueries.java:2396-2431).TestPointQueryParser.java:57-84tests only inclusive float/double ranges and single values; no queryparser test uses an exclusive float/double bound.
The devil's-advocate check192 words
The strongest case for "intended" (the read-only OpenAI GPT-5.6 review rated it moderate): in Java -0.0 < 0 is false, so leaving -0.0 out of x < 0 follows IEEE arithmetic; the parser docs say only that an exclusive range selects values "excluding boundaries" (StandardQueryParser.java:156) and promise nothing about signed zero; the Math.* calls may encode that choice. That much is true: the parser's docs leave the order of the zeros unstated.
Why it fails: under IEEE order -0.0 equals the boundary 0, so it would have to match f:[0 TO *] / f>=0, and it does not (0 hits). No order of the two zeros explains the parser's results: 4 of 12 probe queries are wrong under either. The code shipped in 6.0.0 together with a FloatPoint javadoc that itself recommended Math#nextUp (added 2016-03-17, a week after the parser code of 2016-03-09), advice withdrawn as a bug in 6.4.0; nothing records a decision to keep IEEE order in the parser. The package docs call the standard package "an example Lucene query parser implementation", but the same file calls StandardQueryParser "the default implementation of a fully fledged query parser".
Recommended Use the FloatPoint/DoublePoint helpers for exclusive bounds
Call FloatPoint.nextUp/nextDown and DoublePoint.nextUp/nextDown instead of Math.* (4 changed lines in PointRangeQueryNodeBuilder.java); they differ from Math.* only at signed zero. The repro passes with assertions on and off, as do TestPointQueryParser and the whole queryparser module (534 tests, two seeds).
- lower = Math.nextUp(lower);
+ lower = FloatPoint.nextUp(lower);
- upper = Math.nextDown(upper);
+ upper = FloatPoint.nextDown(upper);
- lower = Math.nextUp(lower);
+ lower = DoublePoint.nextUp(lower);
- upper = Math.nextDown(upper);
+ upper = DoublePoint.nextDown(upper);All options and trade-offs2 options · 344 words
One fix was validated with gradle in a scratch worktree of 645d4fe14a20; an alternative is described so the issue can answer it up front.
4 changed lines in PointRangeQueryNodeBuilder.java (:103, :111, :120, :128); both classes are already imported. The change the study verifier first wrote.
- lower = Math.nextUp(lower);
+ lower = FloatPoint.nextUp(lower);
- upper = Math.nextDown(upper);
+ upper = FloatPoint.nextDown(upper);
- lower = Math.nextUp(lower);
+ lower = DoublePoint.nextUp(lower);
- upper = Math.nextDown(upper);
+ upper = DoublePoint.nextDown(upper);Trade-offs
- Changes results only for an exclusive upper bound at +0.0 and an exclusive lower bound at -0.0, which are wrong today; for every other value the helpers return what
Math.*returns. - Results then match
FloatPoint.newRangeQueryused as its javadoc says, and the pre-6.0 numeric path. - Backport-friendly: the file is the same blob on 9.x, 10.x and main.
- An application that relied on
x:[* TO 0}leaving out -0.0 would see those documents appear, which is the documented order.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20: the repro passes with-Ptests.asserts=trueand=false;TestPointQueryParserpasses 4 of 4. :lucene:queryparser:test, whole module, seeds DEADBEEF and CAFEBABE: SUCCESS, 534 tests (533 + the repro), 1 skipped, 0 failed;checkGoogleJavaFormat,forbiddenApisMainandecjLintMainpass.- With the rebuilt builder,
f:[* TO 0},f<0,d:[* TO 0}andd<0parse to[-Infinity TO -0.0]and find the -0.0 document;f:[0 TO *]is unchanged. Earlier shadow-class runs: the study's six-test class passes 6 of 6, and the tie-break's 12-query probe returns exactly the 5.5.5 results.
Treat -0.0 and +0.0 as one value in every parsed float/double range: widen an inclusive bound at zero to cover both zeros and step an exclusive bound at zero past both.
Trade-offs
Changes inclusive queries that are correct today (f:[0 TO *] would start matching -0.0), makes the parser disagree with FloatPoint.newRangeQuery and newExactQuery, and reverses LUCENE-7594's decision.
An application that turns query text into numeric filters with this parser silently leaves documents holding exactly -0.0 out of an x < 0 filter.
A study of Lucene's points query code transcribed Float.compare, Math.nextUp/nextDown and the FloatPoint helpers into Z3's floating-point theory and asked whether x > f equals x >= Math.nextUp(f) for every float. Z3 found the counterexample f = -0.0, x = +0.0 in under 0.1 s. An independent verifier then reproduced it with its own test.
The method in full162 words
Method: formal (an SMT query on the real arithmetic, then executed tests).
- Finder (study experiment on points and BKD query evaluation, 2026-10-22, main
7cecf79523).Float.compare, the JDK'sMath.nextUp/nextDownandFloatPoint/DoublePoint.nextUp/nextDownwere transcribed statement by statement. TheMath.nextUpexclusive-bound equivalence holds for every f except -0 (and +0 fornextDown) and fails exactly there; theFloatPointhelpers satisfy it for every f other than NaN and +Inf (-Inf fornextDown). The parser'sMath.*calls were then confirmed with a JUnit. The pitfall itself is the one LUCENE-7594 fixed in core in 2016; what is new is that the parser still has it. - Verification. An independent study verifier wrote its own six-test JUnit (float and double, both bounds, a partition test and a control) and a 4-line counterfactual patch; the hunt added a one-test minimal repro, a reproducer, a refute review, a second adjudication, a history and a prior-report follow-up (see the checks below).
No prior report found: 157 queries and reads over GitHub issues and PRs, the JIRA archive, dev@, java-user@, issues@, downstream trackers and the web (2026-10-31). Closest: LUCENE-7594 (#8645), the same pitfall fixed in 2016 in the core point classes only.
Searches4 searches
Verdict new. Not fixed upstream: the builder's only commits are 89cc676f2bf (2016) and 2cbf261032d (2021 reformat), and no open PR touched it on 2026-10-31. #8645 states the intended order and supplies the helpers the fix calls.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-31 | the scripted prior-report search (16 issue/PR, 3 commit queries) plus manual searches (class names, StandardQueryParser with exclusive/float/double, "negative zero" and "signed zero" phrasings), the 10 closest issues read with all comments, and the changed-file lists of all 409 open PRs | context only: #8645 / LUCENE-7594 (same pitfall, core only, 2016), #8148 / LUCENE-7092 and #8117 / LUCENE-7061 (where the Math.nextUp pattern was advised), #8145 / LUCENE-7089 (introduced the code), #14697 (unrelated range-node toQueryString fix) |
| Downstream (Elasticsearch, OpenSearch, Solr) | 2026-10-31 | 2 tool queries, plus manual GitHub searches of those trackers (counted among the 56 manual GitHub searches) | elastic/elasticsearch#22167 (2016): the same symptom in Elasticsearch's own code, fixed there; it prompted LUCENE-7594 |
| Mailing lists, JIRA and the web | 2026-10-31 | 50 list queries (dev@, java-user@, issues@), 15 JIRA JQL queries, 8 web queries, plus 1 list-thread read and 1 JIRA REST read | a 2023 java-user thread on setPointsConfigMap usage; no bound problem raised |
| Earlier searches | 2026-10-22, 2026-10-31 | the study verifier's GitHub and history searches; a first scripted prior-report search run (45 queries) | nothing |
Introduced by 89cc676f2bf (LUCENE-7089, 2016-03-09), which added point support to the flexible parser to replace legacy numerics; first released in 6.0.0. 15 releases from 6.0.0 to 10.6.0 (14 by main() probe, 9.1.0 by JUnit) and main fail; 5.5.x and the 6.0.0 legacy path are correct.
Versions tested6 rows
Introduced by 89cc676f2bf "LUCENE-7089, LUCENE-7075: add points to flexible queryparser to replace legacy numerics support" (Robert Muir, 2016-03-09; a JIRA patch, migrated as #8145). It reached 6.0.0 through the branch_6_0 copy 306d3fe8524. git blame of the method gives it for every code line; the only later commit, 2cbf261032d (LUCENE-9570), is formatting. It shipped in 6.0.0 together with a FloatPoint javadoc that itself recommended Math#nextUp (added 2016-03-17 by c1e95d7b4da, a week after the parser code); LUCENE-7594 (6.4.0) withdrew that advice and added the helpers, but did not touch the parser. Behaviour is the same on 6.3.0 and 6.4.0.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (gradle and standalone) |
| 10.6.0, 10.5.2, 9.12.3, 9.1.0 | affected | the JUnit repro fails (SHA-1-checked jars; 9.1.0 is the oldest it compiles on) |
| 6.0.0 to 10.6.0 (14 releases) | affected | main() probe on 6.0.0, 6.0.1, 6.3.0, 6.4.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0: f:[* TO 0} parsed to f:[-Infinity TO -1.4E-45], the -0.0 document missed; the -1.0 control found; the tagged releases in between carry the same builder blob [read] |
| 6.0.0, legacy numeric path | same release | LegacyFloatField + LegacyNumericConfig: f:[* TO 0} finds the -0.0 document; only the new points path misses it |
| 5.5.0, 5.5.5 | not affected | no points path; the numeric path counts -0.0 below 0 correctly |
main 645d4fe14a20 with the fix | not affected | repro, TestPointQueryParser and the queryparser module pass |
The study's verifier, a reproducer, a tie-break adjudication, the version-history runs and this doc's gradle run all reproduced it; the only dissent was a procedural "not real" from a read-only OpenAI GPT-5.6 review that could not run code itself and agreed on the root cause.
Checks and verdicts261 words
Every executed check ran on stock builds or SHA-1-checked release jars; the cross-model reviewer could only read code.
| Check | Verdict | What it found |
|---|---|---|
| Study finder (Z3) | candidate | The Math.nextUp exclusive-bound equivalence fails exactly at f = -0.0 (x = +0.0) and holds for every other f; a JUnit. |
| Study verifier | new bug | Own six-test JUnit on main (3 seeds): 5 fail, the control passes; a 4-line patch makes all 6 pass. |
| Minimal repro | verified | Fails on 645d4fe14a20 with -ea and -da; the fix shadow passes. |
| Reproducer | real | Main and 10.5.2 with -ea and -da, 9.12.3 with -da; the study's six-test class on main: 5 of 6 fail. |
| Refute lens (OpenAI GPT-5.6, read-only) | not real (procedural) | Could not run code itself (a separate run of the repro on 9.12.3 failed 1 of 1); agreed on the root cause and that the inputs are in contract; rated the IEEE-intent argument moderate. |
| Tie-break adjudication | real | A 12-query probe: correct on 5.5.5, wrong on 6.0.0 to 10.5.2 and main (4 of 12 under either order); the fix shadow returns the 5.5.5 results. |
| History follow-up | 6.0.0, 89cc676f2bf | 15 releases (14 by main() probe, 9.1.0 by JUnit) and main fail; 5.5.0, 5.5.5 and the 6.0.0 legacy path are correct. |
| Prior-report follow-up | new | 157 queries and reads; closest is LUCENE-7594 (#8645), core only. |
| This doc (gradle) | real; fix validated | Stock fails with assertions on and off; the fix passes the repro, TestPointQueryParser and the queryparser module (534 tests, two seeds). |
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 319 KB
Finding record, Lucene and upstream sources, repro files and diagram notes635 words
- Registry title
- Flexible StandardQueryParser misses -0.0 documents for exclusive float/double bounds
- Area
- queryparser / flexible
- Priority
- Low, rank 770: The flexible parser's exclusive float/double bounds miss -0.0 documents, which ordinary arithmetic can produce, but only on a niche parser config and only for that one value.
- Impact class
- silent wrong results
- Impact note
- Users of the opt-in flexible parser with a Float/Double PointsConfig whose index holds the opposite-signed zero; a one-value miss; released since 6.0.0 (2016; 15 releases from 6.0.0 to 10.6.0 executed).
- Summary
- With the flexible StandardQueryParser and a Float/Double PointsConfig, you query x:[* TO 0} on an index that holds -0.0. You expect every document to match exactly one of x < 0 and x >= 0. Instead the parser uses Math.nextUp/nextDown for exclusive bounds, so x:[* TO 0} misses -0.0 documents and such a document matches neither x < 0 nor x >= 0.
- Introduced
- 89cc676f2bf (LUCENE-7089; LUCENE-7594 came later, in 6.4.0) [read: git blame] (2016-03-09)
- First affected version
- 6.0.0 [exec] (89cc676f2bf, LUCENE-7089, reached 6.0.0 through the branch_6_0 cherry-pick 306d3fe8524); 5.5.0 and 5.5.5 are not affected: they have no points path and their numeric path counts -0.0 below 0 correctly [exec]
- Affected versions
- 6.0.0 to 10.6.0 and main 645d4fe14a20 [exec: a main() probe fails on 14 releases (6.0.0, 6.0.1, 6.3.0, 6.4.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0) and the minimal JUnit test on 9.1.0, 9.12.3, 10.5.2 and 10.6.0, 15 releases in all; main with -ea and -da]; the tagged releases in between carry the same builder blob [read]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that could only read the code (its “not real” is procedural); not yet triaged by a person.
- Found by
- formal: Z3
- Upstream and prior reports
- none found (study searches 2026-10-22); #8645 fixed the helpers, not the parser
- 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 doTestPointQueryParserand the whole queryparser module (534 tests, seeds DEADBEEF and CAFEBABE). The same test fails on 10.6.0, 10.5.2, 9.12.3 and 9.1.0; a main() probe fails on 14 releases from 6.0.0 to 10.6.0 (15 releases executed in all, with 9.1.0; the tagged releases in between carry the same builder code [read]) and is correct on 5.5.0, 5.5.5 and the 6.0.0 legacy numeric path (SHA-1-checked jars). Upstream main2f424fd428da(2026-10-30) has the same code [read].
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/PointRangeQueryNodeBuilder.java:97-130a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:751-756a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/PointRangeQueryNodeProcessor.java:76-120a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/FloatPoint.java:48-70, :210-213; DoublePoint.java:48-70, :210-213
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/luke/src/java/org/apache/lucene/luke/models/search/SearchImpl.java:221-240a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestPointQueryParser.java:57-84
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/FloatPoint.java:48-70, :210-213; DoublePoint.java:48-70, :210-213a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:8677, :8708-8710 (6.4.0 Bug Fixes, LUCENE-7594)a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:2396-2431a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestPointQueryParser.java:57-84a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/package-info.java:31-32, :41-42a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:154-158
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/FloatPoint.java:53-58, :65-70
- TestPointRangeExclusiveZeroBound.java (one test class):
repro/LFV-059/src/org/apache/lucene/queryparser/flexible/standard/TestPointRangeExclusiveZeroBound.java(53 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs (the minimal repro and the probes on stock main).
assets/LFV-059/bug.png(319 KB)
![Diagram of the bug: in the order points use, -0.0 sits between -1.4E-45 and +0.0. The query f:[* TO 0} (f < 0) is built as f:[-Infinity TO -1.4E-45] with Math.nextDown(0), which ends before -0.0, so the document holding -0.0 is missed: 0 hits instead of 1. FloatPoint.nextDown(0) = -0.0 would include it.](../assets/LFV-059/bug.png)