All findings · Lucene finding · LFV-059 · flexible-parser-signed-zero-bounds

Flexible StandardQueryParser misses -0.0 documents for exclusive float/double bounds

With the flexible StandardQueryParser and a Float or Double PointsConfig, f:[* TO 0} (f < 0) misses documents holding -0.0, which points sort below 0: the parser builds < 0 as <= Math.nextDown(0) = -1.4E-45, skipping -0.0. Such a document matches neither f<0 nor f>=0, since 6.0.0.

Summary

Priority
Low, rank 770The 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
silent wrong results
Since
6.0.0 (LUCENE-7089, 2016)introduced 2016-03-09 · over 10 years
Affected
6.0.0 to 10.6.0 (15 releases executed) and main
Status
documented
Found by
formal: Z3 floating-point query (< 0.1 s)

AI-assisted page: this finding was first signalled by a formal tool's own output and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Two zeros and inclusive point ranges

  1. Java floats and doubles have two zeros, -0.0 and +0.0. -0.0 == 0.0 is true, but Float.compare(-0f, 0f) is -1: it puts -0.0 just below +0.0.
  2. FloatPoint and DoublePoint index and compare values in the Float.compare / Double.compare order, so -0.0 and +0.0 are two different, adjacent values in a point range.
  3. Point range queries take inclusive bounds only. To run f < 0 the parser must use f <= v, where v is the largest value below +0.0 in that order, which is -0.0.
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.
The parser makes 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.

The finding, step by step

  1. Index one document with FloatPoint("f", -0f) and give StandardQueryParser a Float PointsConfig for f. Ordinary arithmetic produces -0.0: -1 * 0.0 and Math.rint(-0.4) both give it.
  2. Parse f:[* TO 0} (or f<0). Points order values like Float.compare, where -0.0 sits just below 0, so you expect 1 hit.
  3. You get 0 hits. Point ranges take inclusive bounds, so the parser rewrites < 0 as <= Math.nextDown(0), which is -1.4E-45 and skips -0.0: the query becomes f:[-Infinity TO -1.4E-45].
  4. f:[0 TO *] and f>=0 exclude 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.

Who is affected

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.

What the code does

  1. The syntax parser records {/} and </> as exclusive flags; f<0 becomes [* TO 0} (StandardSyntaxParser.jj:751-756).
  2. PointRangeQueryNodeProcessor parses each bound with the configured NumberFormat and converts it with floatValue() (:118-120) or doubleValue() (:115-117). The literal 0 becomes +0.0; an open bound * stays null.
  3. PointRangeQueryNodeBuilder.build, Float branch (:97-113): an open bound becomes -Infinity or +Infinity (:99-101, :107-109); then if (minInclusive == false) { lower = Math.nextUp(lower); } (:102-104) and if (maxInclusive == false) { upper = Math.nextDown(upper); } (:110-112); then FloatPoint.newRangeQuery(field, lower, upper) (:113), whose bounds are inclusive. The Double branch repeats it (:119-121, :127-129, :130).
  4. Math.nextDown(+0.0) is -Float.MIN_VALUE (-1.4E-45) and Math.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."

The numbers

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. setPointsConfigMap with a Float or Double type. Opt-in, but it is how Lucene's own TestPointQueryParser sets 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} or x<0 on 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-50 all 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.0 missing -0.0, in Elasticsearch's own range code, not this parser) is what led to LUCENE-7594.

Minimal repro

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.

TestPointRangeExclusiveZeroBound.java (one test class)53 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.queryparser.flexible.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));
      }
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestPointRangeExclusiveZeroBound -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
1 hit: in the Float.compare order 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.

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

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.

Environment

lucenemain 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
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; plain float arithmetic)
Command (environment)
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

Observed output

# 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

Why it looks unintended

  • The FloatPoint.newRangeQuery the parser calls: "For exclusive ranges, pass nextUp(lowerValue) or nextDown(upperValue)", linking FloatPoint'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<0 nor f>=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-213 the same), on the 1-D newRangeQuery that the builder calls at :113 and :130: "Ranges are inclusive. For exclusive ranges, pass nextUp(lowerValue) or nextDown(upperValue)" (links to FloatPoint.nextUp/nextDown), and "Range comparisons are consistent with Float#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 commit 18d53a43f7b changed only FloatPoint, DoublePoint, CHANGES and two tests (core TestPointQueries, sandbox TestHalfFloatPoint); 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 and f:[* TO -0.0] includes it; only the exclusive step switches to IEEE order.
  • Tests. Core asserts FloatPoint.nextDown(0f) is -0f and nextUp(-0f) is 0f (TestPointQueries.java:2396-2431). TestPointQueryParser.java:57-84 tests 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".

Suggested fix

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.

Use the FloatPoint/DoublePoint helpers for exclusive bounds (validated by the checks) (recommended)

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.newRangeQuery used 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=true and =false; TestPointQueryParser passes 4 of 4.
  • :lucene:queryparser:test, whole module, seeds DEADBEEF and CAFEBABE: SUCCESS, 534 tests (533 + the repro), 1 skipped, 0 failed; checkGoogleJavaFormat, forbiddenApisMain and ecjLintMain pass.
  • With the rebuilt builder, f:[* TO 0}, f<0, d:[* TO 0} and d<0 parse 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.

Adopt IEEE order in the parser instead (not run, not recommended)

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.

Downstream note

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.

How it was found

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's Math.nextUp/nextDown and FloatPoint/DoublePoint.nextUp/nextDown were transcribed statement by statement. The Math.nextUp exclusive-bound equivalence holds for every f except -0 (and +0 for nextDown) and fails exactly there; the FloatPoint helpers satisfy it for every f other than NaN and +Inf (-Inf for nextDown). The parser's Math.* 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).

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-31the 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 PRscontext 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-312 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 web2026-10-3150 list queries (dev@, java-user@, issues@), 15 JIRA JQL queries, 8 web queries, plus 1 list-thread read and 1 JIRA REST reada 2023 java-user thread on setPointsConfigMap usage; no bound problem raised
Earlier searches2026-10-22, 2026-10-31the study verifier's GitHub and history searches; a first scripted prior-report search run (45 queries)nothing

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (gradle and standalone)
10.6.0, 10.5.2, 9.12.3, 9.1.0 affectedthe 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) affectedmain() 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 releaseLegacyFloatField + LegacyNumericConfig: f:[* TO 0} finds the -0.0 document; only the new points path misses it
5.5.0, 5.5.5 not affectedno points path; the numeric path counts -0.0 below 0 correctly
main 645d4fe14a20 with the fix not affectedrepro, TestPointQueryParser and the queryparser module pass

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Study finder (Z3) candidateThe 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 bugOwn six-test JUnit on main (3 seeds): 5 fail, the control passes; a 4-line patch makes all 6 pass.
Minimal repro verifiedFails on 645d4fe14a20 with -ea and -da; the fix shadow passes.
Reproducer realMain 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 realA 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, 89cc676f2bf15 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 new157 queries and reads; closest is LUCENE-7594 (#8645), core only.
This doc (gradle) real; fix validatedStock fails with assertions on and off; the fix passes the repro, TestPointQueryParser and the queryparser module (534 tests, two seeds).

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes635 words

Finding record

LFV-059, snapshot of 2026-11-06

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 do TestPointQueryParser and 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 main 2f424fd428da (2026-10-30) has the same code [read].

Lucene and upstream sources

The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/PointRangeQueryNodeBuilder.java:97-130
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:751-756
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/PointRangeQueryNodeProcessor.java:76-120
  • a 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-240
  • a 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-213
  • a 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-2431
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestPointQueryParser.java:57-84
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/package-info.java:31-32, :41-42
  • a 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

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs (the minimal repro and the probes on stock main).

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.