A spatial4j Rectangle can be reused: reset(minX, maxX, minY, maxY) overwrites it in place. The Geo3d rectangle keeps its coordinates in four fields, and its reset replaces only the inner spatial3d box. BBoxStrategy reuses one rectangle each time it reads a segment's values, so every document reads back as that rectangle's first value, (0, 0, 0, 0).
reset is missing, and the executed results next to the expected ones: wrong getters, shape values and overlap scores; matching and distances unaffected. Select the still to open it at full size. Generated with AI, checked against the code.- You use the Geo3d spatial context (
Geo3dSpatialContextFactory) withBBoxStrategyand index two boxes: P =rect(10, 20, 30, 40)and Q =rect(15, 25, 35, 45). - You read each document's box with
makeShapeValueSource()and expect P and Q back. You get (0, 0, 0, 0) for both. - You rank by
makeOverlapRatioValueSource(rect(10, 20, 30, 40), 0.5)and expect P = 1.0 (it equals the query) and Q = 0.25 (it covers a quarter of it). You get 0.0 and 0.0, andexplainsays "No intersection". - Why:
BBoxStrategycreates onerect(0, 0, 0, 0)each time it reads a segment's values (getValues) and callsreset(...)for each document, andGeo3dRectangleShape.resetrebuilds its inner spatial3d box but never assigns the four fields the getters return. Directly: afterrect(-50, -40, -30, -20).reset(10, 20, 30, 40)the rectangle equalsrect(10, 20, 30, 40), yetgetMinX()is still -50. - No error, with assertions on or off. Lucene never resets the rectangles it queries with or indexes, so matching and the index are fine; distances use the rebuilt inner box. The default spatial4j context returns P, Q, 1.0 and 0.25. Every release since 7.1.0 (2017) has it.
Applications that use the opt-in Geo3d spatial context with BBoxStrategy and read its shape values or rank by its overlap ratio, and code that calls reset on a Geo3d rectangle. There every stored box reads back as (0, 0, 0, 0). Matching with freshly built query boxes, distances, the default spatial4j context and Lucene's core geo types are not affected.
Mechanism, terms and numbers878 words
In the opt-in Geo3d spatial context (Geo3dSpatialContextFactory, @lucene.experimental), Geo3dRectangleShape.reset(minX, maxX, minY, maxY) rebuilds the wrapped spatial3d GeoBBox but never assigns the four fields that getMinX()..getMaxY(), getWidth(), getHeight() and getCrossesDateLine() return. The reset rectangle equals the new box but reports the old coordinates. BBoxStrategy's shape-value source reuses one rect(0, 0, 0, 0) per getValues call and resets it for every document, so makeShapeValueSource() reports (0, 0, 0, 0) for every document and makeOverlapRatioValueSource() scores against that point: a box equal to the query scores 0.0 instead of 1.0. No exception. Lucene's own matching and indexing never reset a rectangle, so with freshly built rectangles they are unaffected, and distances use the rebuilt inner box; only a rectangle that application code resets itself and then queries with or indexes carries the stale bounds there. It came in with LUCENE-7951 in 7.1.0 (2017) and is in every release since and on main; a 4-line fix (assign the four fields in reset) passes every repro and the module's tests, and should land together with, or after, the fix for the class's separate getCrossesDateLine bug.
- Spatial context: spatial4j's
SpatialContextbuilds and relates shapes.Geo3dSpatialContextFactory(opt-in,@lucene.experimental) makes one whose shapes wrap spatial3d (Geo3d) shapes; its rectangles areGeo3dRectangleShape. Rectangle.reset: spatial4j's "Expert: Resets the state of this shape given the arguments. This is a performance feature to avoid excessive Shape object allocation". A caller keeps one rectangle and overwrites it instead of allocating a new one.BBoxStrategy(@lucene.experimental): indexes a box as four numbers (and a dateline flag).makeShapeValueSource()reads each document's box back from doc values;makeOverlapRatioValueSource(queryBox, proportion)scores each document by how much its box and the query box overlap (1.0 when they are equal).
Geo3dRectangleShapekeepsminX,maxX,minY,maxYas fields (Geo3dRectangleShape.java:39-42);Geo3dShapeFactory.rectpasses the caller's raw bounds into them (Geo3dShapeFactory.java:163-173).getMinX()..getMaxY()return them (:132-150), andgetWidth(),getHeight()andgetCrossesDateLine()are computed from them (:118-130,:152-155).reset(:100-111) builds a newGeoBBoxinto the wrappedshape, clears thecenterandboundingBoxcaches, and returns. It never writes the four fields.- So the object contradicts itself:
equalsand the rectangle's ownrelateuse the newGeoBBox(Geo3dShape.java:55-79,:160-164), and so doesgetCenter(), but the getters return the old box, and so does any code that reads them (spatial4j'sRectangleImpl.relate(Rectangle)reads the other rectangle's getters). BBoxValueSourcecreates onerect(0, 0, 0, 0)in eachgetValues(LeafReaderContext)call (BBoxValueSource.java:55-56, "// reused"; each call reads one segment, and a segment can get several calls) and resets it to each document's four doc values (:74). In the Geo3d context every document read through it therefore reports (0, 0, 0, 0).BBoxOverlapRatioValueSource.scorecomputes the overlap height from the target'sgetMinY()/getMaxY()(BBoxOverlapRatioValueSource.java:148-156): for the query (10, 20, 30, 40) and the target (0, 0, 0, 0) it ismin(40, 0) - max(30, 0) = -30, so the score is 0 with "No intersection".
| call, Geo3d context | expected | main | fix |
|---|---|---|---|
rect(-50, -40, -30, -20).reset(10, 20, 30, 40), then getMinX()..getMaxY() | 10, 20, 30, 40 | -50, -40, -30, -20 (but it equals rect(10, 20, 30, 40)) | 10, 20, 30, 40 |
| shape values of P = (10, 20, 30, 40) and Q = (15, 25, 35, 45) | P, Q | (0, 0, 0, 0), (0, 0, 0, 0) | P, Q |
| overlap scores of P, Q for the query (10, 20, 30, 40), proportion 0.5 | 1.0, 0.25 | 0.0, 0.0 | 1.0, 0.25 |
| self-overlap of the box (-10, 10, -10, 10), which contains the point (0, 0) | 1.0 | 0.5 | 1.0 (with the getCrossesDateLine fix too; see Suggested fix) |
| distance from the point (15, 35) to P and Q | 0.0, 6.379961 | 0.0, 6.379961 | 0.0, 6.379961 |
The default spatial4j context (SpatialContext.GEO) gives the "expected" column on main.
- Matching:
BBoxStrategy.makeQueryreads the query rectangle's getters but never callsreset, so a freshly built query box matches correctly (code reading). In a 300-document probe with the class's separategetCrossesDateLinefix applied, the 125 queries return exactly the arithmetic truth with and without this fix. Without that fix, stock main already mismatches 62 of the 125 queries because of it, with identical aggregate counts with and without this fix (hit sets not compared query by query). - Distances:
makeDistanceValueSourcemeasures togetCenter(), which comes from the newGeoBBox(the table's last row). The Geo3d context measures withGeo3dDistanceCalculator, the default context with spatial4j's haversine calculator; both give degrees on the default sphere. - The index: documents are indexed from freshly built rectangles, so the stored numbers are right and need no reindex after a fix. Only a rectangle that the application has reset itself would be indexed with stale bounds and a stale dateline flag, and those documents would need a reindex; used as a query box, such a rectangle would also match by its stale bounds (code reading).
RecursivePrefixTreeStrategy, the default spatial4j contexts and Lucene's core geo types do not use this method.
Exposure and likelihood381 words
- Who is hit. Applications that pair the Geo3d spatial context (
Geo3dSpatialContextFactory,@lucene.experimental) withBBoxStrategy(@lucene.experimental) and readmakeShapeValueSource()or rank bymakeOverlapRatioValueSource(); and any application code that callsreseton a Geo3d rectangle. Downstream, by reading Solr's code only (not run): Solr mapsspatialContextFactory="Geo3D"to this context, and itsBBoxFieldbuilds theoverlapRatioscore on these shape values; one public downstream configuration uses such a field. - Default vs opt-in. Opt-in: two experimental pieces together, plus the value-source path. Within that pairing it is deterministic, assertions on or off: every document reads back as (0, 0, 0, 0), so every stored box other than (0, 0, 0, 0) itself is wrong (269 of 269 generated boxes in the probe).
- What goes wrong. Every shape value is (0, 0, 0, 0). Overlap relevance is computed against the point (0, 0): 0.0 for every document when the query box does not contain that point (all ties, so ranking by overlap does nothing), and wrong in other ways when it does (the box -10..10 x -10..10 scores itself 0.5). Scores are wrong in general; a few can be right by coincidence.
- At scale (follow-up probe: 300 boxes in 3 segments, 31 deleted, two tenants, 25 query boxes scored by overlap ratio): all 269 live documents report (0, 0, 0, 0). With the class's separate
getCrossesDateLinebug fixed to isolate this one, 141 of 141 returned scores and the top-10 order of 16 of 25 queries differ from the default spatial4j context; with both fixes 0 of 141 and 0 of 25. - Silent, but contained. No exception or hang;
explainsays "No intersection". This bug does not change matching with freshly built query boxes, filter clauses or deletions (0 other-tenant and 0 deleted documents in the probe, all scores in [0, 1]), and an index built from fresh rectangles holds the right numbers. On stock main the same probe's matching was already wrong for 62 of 125 queries, and 77 of 218 scored documents did not satisfy the query, because of the class's separategetCrossesDateLinebug; with that fix both are 0. - How likely. Rare: the pairing appears little used (no upstream test combines the Geo3d context with
BBoxStrategy), and nobody reported it in the nine years since 7.1.0.
The test shows that in the Geo3d spatial context, Rectangle.reset(minX, maxX, minY, maxY) does not update the bounds that getMinX()..getMaxY() return.
/*
* 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.spatial.spatial4j;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Rectangle;
public class TestGeo3dRectangleResetBounds extends LuceneTestCase {
public void testResetReplacesMinX() {
SpatialContext ctx = new Geo3dSpatialContextFactory().newSpatialContext();
Rectangle r = ctx.getShapeFactory().rect(-50, -40, -30, -20);
r.reset(10, 20, 30, 40);
// expected: getMinX() is the reset value 10; actual: the old value -50
assertEquals(10, r.getMinX(), 0);
}
}
# in a copy of this finding's repro folder (repro/LFV-675/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/spatial-extras/build/libs/lucene-spatial-extras-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.spatial.spatial4j.TestGeo3dRectangleResetBounds
# the module(s) spatial-extras also need their third-party runtime jars
- Expected vs actual
- Expected
rect(-50, -40, -30, -20).reset(10, 20, 30, 40).getMinX() == 10; actual-50(expected:<10.0> but was:<-50.0>), with-eaand-da.
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<10.0> but was:<-50.0>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:555)
org.junit.Assert.assertEquals(Assert.java:685)
org.apache.lucene.spatial.spatial4j.TestGeo3dRectangleResetBounds.testResetReplacesMinX(TestGeo3dRectangleResetBounds.java:30)- spatial4j's
Rectangle.reset: "Resets the state of this shape given the arguments"; spatial4j's own rectangle assigns all four bounds. - After
resetthe Geo3d rectangle contradicts itself:equalssees the new box,getMinX()..getMaxY()the old one. - In the introducing PR,
resetcleared a bounds cache; a refactor turned the cache into four fields and dropped that line.
The full argument274 words
- The contract. spatial4j 0.8
Rectangle.reset: "Expert: Resets the state of this shape given the arguments. This is a performance feature to avoid excessive Shape object allocation as well as some argument error checking. Mutable shapes is error-prone so use with care."getMinX(): "The left edge of the X coordinate." spatial4j'sRectangleImpl.resetassignsminX,maxX,minYandmaxY(fourputfields in its bytecode).BBoxValueSourceusesresetfor exactly the documented purpose, andShapeValues.value()"Returns a Shape for the current document". - The class contradicts itself. After
reset,equalsand the rectangle's ownrelatesee the new box and the getters the old one. - The fields are the coordinates, not a constructor cache. In LUCENE-7951 (#9000) the author explained that the 6-argument constructor exists because wide Geo3d rectangles (more than 180 degrees of longitude) cannot compute their longitude bounds from the
GeoBBox, and the reviewer accepted it. - History. In the PR's first version of the class (lucene-solr#246,
ea31e5ce0f, 2017-09-07) the bounds were computed lazily from a cachedLatLonBounds, andresetcleared the cache (bounds = null;). PR commitcfbac2fb00(2017-09-12) replaced the cache with the four stored fields and removedbounds = null;fromresetwithout assigning them. No review comment on #246 and none of the 43 comments on #9000 mentionreset. - The sibling resets are complete.
Geo3dPointShape.resetandGeo3dCircleShape.resetrebuild the wrapped shape, and their getters read it; only the rectangle caches coordinates, and only it forgets them. - Tests. No test under
lucene/spatial-extras/src/testcallsreset(, andTestBBoxStrategyuses only spatial4j's own contexts. No test, CHANGES or MIGRATE entry accepts the current behaviour.
The devil's-advocate check169 words
The strongest case for "intended" (an intended-behaviour checker): the public Lucene classes involved (Geo3dSpatialContextFactory, Geo3dRectangleShape, BBoxStrategy) are @lucene.experimental, BBoxValueSource is @lucene.internal and reuses the rectangle on purpose, spatial4j calls mutable shapes "expert" and "error-prone", the Geo3d context with BBoxStrategy is untested and partly unsupported (Geo3dShape.getArea throws UnsupportedOperationException), and nobody has reported it since 2017.
Why it fails: the "expert" warning is about callers sharing a mutable object, not a licence for reset to update half of it; experimental status warns about API changes, not wrong values; the wrong values are read right away, for the current document, with ordinary boxes; nothing documents the pairing as unsupported, and where something really is unsupported (getArea) it throws instead of returning wrong numbers; and the direct trigger, reset on a rectangle from the Geo3d factory, needs no BBoxStrategy at all. Verdict: not intended, a defect (the adjudication and independent checks; the cross-model reviewer rated the intended-behaviour argument weak under both of its lenses).
Recommended A: assign the four fields in reset
Assign the four fields in Geo3dRectangleShape.reset (4 lines), as a freshly built rectangle has them. The new test, every earlier repro and the spatial-extras module check pass. Land it together with, or after, the fix for the class's separate getCrossesDateLine bug: alone, it exposes that bug and a self-overlap score becomes 19.0. Indexes built from fresh rectangles need no reindex.
@@ -106,6 +106,10 @@ public class Geo3dRectangleShape extends Geo3dShape<GeoBBox> implements Rectangl
minY * DistanceUtils.DEGREES_TO_RADIANS,
minX * DistanceUtils.DEGREES_TO_RADIANS,
maxX * DistanceUtils.DEGREES_TO_RADIANS);
+ this.minX = minX;
+ this.maxX = maxX;
+ this.minY = minY;
+ this.maxY = maxY;
center = null;
boundingBox = null;
}All options and trade-offs4 options ยท 785 words
One option was built and validated in a scratch worktree at a9779ef3c193 with gradle (JDK 25), alone and together with the getCrossesDateLine fix, and with the standalone runner against every repro (patched class shadowed). The other three were weighed and not built. Any fix needs a regression test (this doc's test is one).
Add four assignments after the new GeoBBox is built in Geo3dRectangleShape.reset (after line 108). A reset rectangle then matches the one Geo3dShapeFactory.rect builds from the same arguments (the factory passes the raw arguments to the 6-argument constructor). Because the assignments come after makeGeoBBox, an invalid argument that makes it throw leaves the object unchanged. It restores what bounds = null; did before the bounds became fields.
@@ -106,6 +106,10 @@ public class Geo3dRectangleShape extends Geo3dShape<GeoBBox> implements Rectangl
minY * DistanceUtils.DEGREES_TO_RADIANS,
minX * DistanceUtils.DEGREES_TO_RADIANS,
maxX * DistanceUtils.DEGREES_TO_RADIANS);
+ this.minX = minX;
+ this.maxX = maxX;
+ this.minY = minY;
+ this.maxY = maxY;
center = null;
boundingBox = null;
}Trade-offs
- Smallest change, at the defect; the same hunk as the finder's fix. It also makes
getWidth()andgetHeight()right after a reset, since they read the fields, and givesgetCrossesDateLine()the new bounds as input; that getter's formula stays wrong for boxes that span longitude 0 until the class's separategetCrossesDateLinefix lands (with this fix alone:crossesDateLine=trueand 19.0 for -10..10 x -10..10; with both:falseand 1.0). - Fix ordering. On main the stale (0, 0, 0, 0) happens to hide the class's separate
getCrossesDateLinebug (it returnsmaxX > 0 && minX < 0) from the overlap scorer. With only this fix, boxes that span longitude 0 reach the scorer with the wrong flag: the self-overlap of -10..10 x -10..10 is 19.0 with assertions off and anAssertionError: 19.0from thequeryRatioassertion with assertions on; in the 300-document probe 28 of 218 scores fell outside [0, 1] with assertions off and 14 of 25 queries threw with assertions on. With both fixes: 1.0, and 0 and 0. So land it together with that fix (one PR with both lines and both tests is simplest) or after it. - No reindex for indexes built from fresh rectangles. Lucene's indexing does not call
reset, so existingBBoxStrategyindexes built from factory rectangles hold the right numbers and read back correctly after the fix. An application that indexed rectangles it had reset itself stored stale bounds and needs to reindex those documents (code reading).
What the checks validated
- Gradle, scratch worktree at
a9779ef3c193, seed DEADBEEF, this doc's test added: it fails 3 of 3 on stock main (assertions on and off) and passes with the fix. ./gradlew :lucene:spatial-extras:checkwith the fix passes: 27 suites, 120 tests (the module's 117 plus the 3 new), 5 skipped, 0 failures, and the format, ecjLint and forbiddenApis checks. The suites includeTestBBoxStrategy,TestGeo3d,TestGeo3dRpt, both Geo3d RectRelation tests,TestDistanceStrategyandTestPointVectorStrategy.TestBBoxStrategy,TestDistanceStrategyandTestGeo3d*10 times with a random root seed and assertions off: 480 tests, 50 skipped, 0 failures.- With the
getCrossesDateLinefix added:./gradlew :lucene:spatial-extras:checkpasses (120 tests, 5 skipped, 0 failures); the fix-order probe gives 1.0 for both boxes, assertions on and off. - Standalone, fix shadowed: this doc's test 3/3 (assertions on and off, alone and with both fixes); earlier, by the hunt: the finder's 3/3, the independent reproducer's 7/7, the cross-model reviewer's and the devil's advocate's own tests, the history test on 7.1.0 and main, the bridge model clean (0 of 10,000 resets, with the hunt's first five fixes applied together, not this fix alone), and the 300-document probe with both fixes (0 of 141 scores differ from the default context).
Call the existing setBoundsFromshape() at the end of reset.
Trade-offs
A reset rectangle would then differ from a freshly built one: bounds derived from a GeoBBox do not round-trip exactly (-50 comes back as -49.999999999999986) and cannot be computed for boxes wider than 180 degrees of longitude, which is why the 6-argument constructor exists. Not recommended.
Build a fresh ctx.getShapeFactory().rect(...) per document instead of rect.reset(...).
Trade-offs
Fixes only the in-tree path: Rectangle.reset stays broken for application code, and the class keeps contradicting itself. By code reading it costs one extra object per document (reset already builds a new GeoBBox per document). Not sufficient on its own.
Compute the bounds lazily from a cached LatLonBounds and clear the cache in reset.
Trade-offs
Reintroduces the wide-rectangle problem that the stored fields solved. Not recommended.
Solr maps spatialContextFactory="Geo3D" to this context and builds its BBoxField overlap-ratio score on these shape values (read in Solr's code, not run), so such a field ranks every document against (0, 0, 0, 0).
Model-based differential testing of the spatial4j-to-spatial3d bridge (not formal verification). A harness checked that a shape changed with reset reports the same coordinates as one freshly built: 10,000 of 10,000 random resets kept the old ones while equals said the shapes matched; 0 with the hunt's combined fixes applied. A public-API JUnit then showed the BBoxStrategy effects.
The method in full185 words
Method: model-based differential testing (non-formal; the finder tagged it semi-formal), in a hunt unit on the spatial4j bridge to spatial3d.
- While modelling the shapes' mutators, the finder wrote the rule G6 of the bridge model: a shape changed with
resetmust equal, and report the same values as, a freshly built shape with the same arguments. - The harness
Geo3dBridgeDiffchecked G6a on 10,000 random rectangle resets (5,000 on the SPHERE and 5,000 on the WGS84 planet model): every reset rectangle kept its oldgetMinX()..getMaxY()whileequalssaid it matched the fresh one; with the hunt unit's first five fixes applied together (this one among them), 0 of 10,000. - The finder then looked for in-tree callers of
Rectangle.reset, foundBBoxValueSource, and wrote a public-API JUnit on the stock main jar: a direct reset,BBoxStrategyshape values, and the overlap-ratio score compared with the default spatial4j context. It fails 3 of 3. - The same rule's circle family (G6d) also flagged
Geo3dCircleShape.reseton WGS84 (a reset circle never equals a fresh one); it has no in-tree caller and is recorded as a lead only.
No prior report found (2026-10-27): two runs of the prior-report tool (45 and 51 queries), 45 manual GitHub searches, the changed files of the 409 open PRs (only #16911, a branch_10x decommission, removes the two files), 8 JIRA queries, the Lucene and Solr lists and the web. Closest: #9000 (LUCENE-7951), the introducing change, whose review never mentions reset.
Searches4 searches
None of the hits is this bug. Limits: keyword search only; private security reports and unindexed chat cannot be searched; the changed-file list of #16911 (7,630 files) could not be enumerated in full.
| Where | When | How | Found |
|---|---|---|---|
| the prior-report tool: GitHub issues, PRs and commits (apache/lucene), downstream repositories, the lists, JIRA and the web | 2026-10-27 | two runs: 45 queries (the finder's; its 6 web queries left pending), then 51 queries in all, 45 with expanded class, method and symptom terms (Geo3dRectangleShape, BBoxValueSource, BBoxOverlapRatioValueSource, makeOverlapRatioValueSource, Geo3dSpatialContextFactory, reset, Geo3D BBoxField, overlapRatio) and 6 web queries; 201 distinct hits, 0 high, 6 medium, all read | no report; related: #9000, lucene-solr#246, #9134, #6776, SOLR-11382 |
| GitHub, manual search | 2026-10-27 | 45 searches across apache/lucene, apache/lucene-solr, apache/solr, locationtech/spatial4j, Elasticsearch, OpenSearch and all of GitHub | nothing matching; one downstream configuration uses a solr.BBoxField with the Geo3D context (not a report) |
| Open PRs | 2026-10-27 | the changed files of the 409 open apache/lucene PRs (the list of #16911, 7,630 files, could not be enumerated in full) | none changes Geo3dRectangleShape.java or BBoxValueSource.java, except #16911 "Decommission branch 10x", which removes every file from branch_10x (a branch decommission, not a fix); #12295 changes only a cast in BBoxStrategy.makeQuery |
| JIRA and mailing lists | 2026-10-27 | 8 JQL queries (LUCENE, SOLR); dev@, java-user@ and issues@ lucene, users@ and dev@ solr, the old solr-user@; 7 web queries | SOLR-11382 (added the Geo3D alias, no reset discussion); a 2013 fix of the same bug class in another class (LUCENE-4914, Cell.reset) |
Introduced in 7.1.0 by LUCENE-7951 (035523ba7a86 on main, 85491401d54c on branch_7x, 2017-09-20). In its PR, reset first cleared a bounds cache; a refactor replaced the cache with four fields and dropped the line. 7.0.x has no Geo3d context; 7.1.0 to 10.5.2 and main fail. Never fixed.
Versions tested5 rows
Introduced by the LUCENE-7951 commit "Spatial4j implementations using Geo3d" (issue #9000, "New wrapper classes for Geo3d"): code from lucene-solr#246 by @iverase (closed without a GitHub merge), committed by @dsmiley on 2017-09-20 as 035523ba7a86 on main (first released in 8.0.0) and the same-day cherry-pick 85491401d54c on branch_7x, which shipped in 7.1.0 (it is in the 7.1.0 tag, not in 7.0.1). Geo3dRectangleShape.java is new in it, and reset has never assigned the four fields; the only later commit on the method is the LUCENE-9570 reformat 2cbf261032d (2021). Inside the PR, the first version ea31e5ce0f cleared a lazy bounds cache in reset (bounds = null;); cfbac2fb00 (2017-09-12) replaced the cache with four stored fields and dropped that line. A source scan of the 63 local release tags from 7.1.0 to 10.5.2 finds the same reset body; 8.8.2 to 8.11.3 are not tagged locally; in their release jars javap shows reset writing only shape, center and boundingBox (8.9.0 and 8.11.4 were also run). Before 7.1.0 the class and the Geo3d context did not exist, and the BBoxStrategy usage that existed then (Geo3d shapes under the default context) is still correct, so this is not an upgrade regression. Upstream main and branch_10x still have the unchanged reset.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test fails 3 of 3, assertions on and off (gradle and standalone); the finder's 3 of 3 |
| 10.5.2, 9.12.3 | affected | this doc's test fails 3 of 3; the finder's 3 of 3; the history test 1 of 3 (controls pass) |
| 10.0.0, 9.0.0, 8.11.4, 8.9.0, 8.0.0, 7.7.3, 7.1.0 | affected | the finder's test fails 3 of 3; the history test 1 of 3 (not run on 8.9.0) |
| 7.0.1, 7.0.0 | not affected | the history test passes (the bounding box is a spatial4j RectangleImpl); the Geo3d spatial context does not exist yet |
| fix on 7.1.0 and main | not affected | every repro passes; on main the spatial-extras module check passes, alone and with the getCrossesDateLine fix |
Reproduced by the finder, an independent reproducer, two executed reviews by another model family (OpenAI GPT-5.6), a devil's advocate, the adjudication, the follow-up and a credibility audit, each with its own runs. All rated it real and not intended; the audit lowered the impact because the pairing is rare.
Checks and verdicts296 words
Each check ran its own tests on main a9779ef3c193 (the cross-model reviews on ea57360912ed, whose spatial-extras and spatial3d code is the same).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Bridge model G6a (10,000 of 10,000 resets stale, 0 with the hunt's combined fixes); a 3-test JUnit fails 3 of 3 with assertions on and off and passes with the fix. |
| Independent reproducer | real | Own test from a non-zero rectangle with spatial4j controls: 3 of 7 fail on main, 10.5.2 and 9.12.3, the controls pass; 7 of 7 with the fix. |
| refute review (OpenAI GPT-5.6, executed) | real, high | Own tests: a direct reset kept -120.0 where 20.0 was due, a shape value reported minX 0 instead of 65, a self-overlap scored 0 instead of 1; all pass with the fix, and so do focused existing tests. |
| intended review (OpenAI GPT-5.6, executed) | real, high | Intended-behaviour argument weak; own tests: 5 run, 3 failures, 2 controls pass; 5 of 5 with the fix. |
| Devil's advocate | real, high | Built the strongest intended case and showed why it fails; showed from the LUCENE-7951 review that the fields are the coordinates; found that getArea throws, so area scores are not claimed. |
| adjudication | confirmed, high | Re-ran the independent reproduction's test (3 of 7 fail with assertions on and off, 7 of 7 with the fix) and the finder's (3 of 3 fail). |
| Follow-up | since 7.1.0 | Release boundary with a version-portable test; the 300-document probe (matching unaffected, 269 of 269 documents stale, the fix-ordering effect); prior-report search. |
| credibility audit | real; impact downgraded | Re-ran the finder's test with a fresh seed: 3 of 3 fail with assertions on and off. Kept the bug, lowered the impact (rare pairing, scoring only). |
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 449 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,589 words
- Registry title
- Geo3dRectangleShape.reset(minX, maxX, minY, maxY) replaces the wrapped spatial3d box but keeps the old minX/maxX/minY/maxY, so with the Geo3d spatial context BBoxStrategy shape values report a stale box for every document and BBoxStrategy overlap-ratio relevance scores are wrong
- Area
- spatial-extras / spatial4j bridge to spatial3d (Geo3dRectangleShape.reset), read by BBoxStrategy's value sources (BBoxValueSource: makeShapeValueSource, makeOverlapRatioValueSource)
- Priority
- Low, rank 435: It only affects scores and shape values, never matching, when two rarely combined experimental pieces are used together, the same scoring-only BBoxStrategy class as the Low row LFV-369, so it moves to Low just ahead of LFV-369 because every document is affected.
- Impact class
- silent wrong results
- Impact note
- Applications on the opt-in, @lucene.experimental Geo3d spatial context (Geo3dSpatialContextFactory; Solr exposes it as spatialContextFactory="Geo3D" and its BBoxField can score by overlap ratio, read, not run) that read BBoxStrategy value sources get wrong values with no error for every document: makeShapeValueSource reports the per-segment initial box (0, 0, 0, 0) instead of each document's indexed box (executed: 269 of 269 live documents in 3 segments), and makeOverlapRatioValueSource ranks by overlap with that constant (a document box equal to the query scores 0.0 instead of 1.0; in an executed 300-document probe, with the separate getCrossesDateLine sign bug fixed to isolate this one, 141 of 141 returned scores and the top-10 order of 16 of 25 queries differed from the default spatial4j context); application code that calls Rectangle.reset on a Geo3d rectangle itself also reads the old bounds; it is deterministic with -ea and -da, needs no scale and is in every release since 7.1.0; BBoxStrategy matching (makeQuery, executed), distance value sources (code reading), RecursivePrefixTreeStrategy, the default spatial4j contexts and Lucene's core geo types are not affected, and the index is not corrupted.
- Summary
- You use the Geo3d spatial context (Geo3dSpatialContextFactory) with BBoxStrategy and read each document's box with makeShapeValueSource() or rank by makeOverlapRatioValueSource(queryBox, ...), or you call reset(10, 20, 30, 40) on a Geo3d rectangle yourself. You expect reset to replace the whole rectangle (the spatial4j Rectangle.reset javadoc: 'Resets the state of this shape given the arguments'; spatial4j's own RectangleImpl.reset assigns all four bounds), so getMinX()..getMaxY() return 10, 20, 30, 40, the shape values return each document's indexed box, and a document box equal to the query scores 1.0, as it does with the default SpatialContext.GEO. Instead Geo3dRectangleShape.reset rebuilds only the wrapped GeoBBox and clears the center and bounding-box caches, never assigning the four fields that getMinX()..getMaxY(), getWidth(), getHeight() and getCrossesDateLine() read: the rectangle equals rect(10, 20, 30, 40) but reports its previous bounds, BBoxValueSource (which creates one rect(0, 0, 0, 0) per segment and resets it for each document) reports (0, 0, 0, 0) for every document, and the self-overlap score is 0.0.
- Introduced
- 035523ba7a86773500c053129f65c9a6dc4fd384 (master) and its branch_7x cherry-pick 85491401d54c59e1593a5c9f13dca7bfcdda31a2 (the commit that shipped in 7.1.0): LUCENE-7951 'Spatial4j implementations using Geo3d' (migrated as apache/lucene#9000), from PR apache/lucene-solr#246 by GitHub iverase, committed directly by GitHub dsmiley (the PR was closed without a GitHub merge). Geo3dRectangleShape is a new file in it and its reset has never assigned the four bound fields [read: git log -L, git blame -w -M -C and git log -S over the full history agree; the only later commit on the method is the LUCENE-9570 reformat 2cbf261032d]. Inside the PR, the first version ea31e5ce0f derived the bounds lazily from a cached LatLonBounds and reset cleared it ('bounds = null'); commit cfbac2fb00 (iverase, 2017-09-12) replaced the cache with four stored fields and the 6-argument constructor and removed the cache clear from reset without assigning the new fields [read: GitHub API GETs, snapshots in the follow-up]. No review comment mentions reset. (2017-09-20 (commit date 23:09:46 -0400, i.e. 2017-09-21T03:09:46Z); the omission entered the PR in cfbac2fb00 on 2017-09-12)
- First affected version
- 7.1.0 [exec: the finder's JUnit fails 3 of 3 and the version-portable bisection test fails 1 of 3 with both controls passing]; 7.0.0 and 7.0.1 are not affected [exec: the bisection test passes 3 of 3, because Geo3dShape.getBoundingBox() returns a spatial4j RectangleImpl there; Geo3dRectangleShape and Geo3dSpatialContextFactory do not exist before 7.1.0, so the finder's JUnit does not compile on 7.0.1]
- Affected versions
- Every release from 7.1.0 through 10.5.2 (the last release before 10.6.0) and main a9779ef3c193; not fixed upstream. [exec, JDK 25, SHA-1-checked release jars] the finder's JUnit (the 7.x and 8.x runs use a source variant that differs only in the LuceneTestCase import) fails 3 of 3 on 7.1.0, 7.7.3, 8.0.0, 8.9.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the bisection test fails 1 of 3 (both controls pass) on 7.1.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da) and passes on 7.0.0 and 7.0.1; with the 4-line fix both tests pass on 7.1.0 and main (follow-up); the independent reproduction's JUnit fails 3 of 7 (controls pass) on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check ran the finder's JUnit on main (-ea and -da) and 10.5.2 (3 of 3 fail). [read] a source scan of the 63 local release tags 7.1.0-8.8.1 and 9.0.0-10.5.2 finds that reset never assigns the fields; the 8.8.2-8.11.x tags are not in the local clones (8.9.0 and 8.11.4 executed, the rest inferred from branch lineage); upstream main (a95c754ee003, 2026-10-26) and branch_10x still have the unchanged reset (read 2026-10-27).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal
- Upstream and prior reports
- none found (searched 2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (45 queries; then 51 queries with an expanded spec plus 6 web queries, 201 distinct hits, 0 high, 6 medium, all triaged), 45 manual GitHub searches (apache/lucene, apache/lucene-solr, apache/solr, locationtech/spatial4j, Elasticsearch, OpenSearch and all of GitHub), a scan of the changed files of all 409 open apache/lucene PRs, a full read of PR apache/lucene-solr#246 and issue #9000, 8 JIRA queries over LUCENE and SOLR, list searches over the Lucene dev, java-user and issues lists and the Solr user and dev lists, and 7 web queries; the intended-behaviour check's own scripted prior-report search (40 queries) agrees. Nothing reports or discusses Geo3dRectangleShape.reset keeping stale bounds, or wrong BBoxStrategy shape values or overlap-ratio scores with the Geo3d context. Not fixed upstream: on upstream main (a95c754ee003, 2026-10-26) and branch_10x the last commit on Geo3dRectangleShape.java is the 2021 reformat 2cbf261032d and reset still assigns only shape, center and boundingBox; no open PR touches Geo3dRectangleShape.java or BBoxValueSource.java (the nearest, #12295, changes only a cast in BBoxStrategy.makeQuery). Related items, not reports of this bug: #9000 / LUCENE-7951 and apache/lucene-solr#246 (the introducing issue and PR; nobody discusses reset), #9134 / LUCENE-8086 (a later bridge change), #6776 / LUCENE-5714 (origin of BBoxValueSource and its rectangle reuse, before Geo3d existed), SOLR-11382 (added Solr's Geo3D context alias) and commit a22e571189c (LUCENE-4914, 2013: the same bug class, a reset that forgets state, in SpatialPrefixTree Cell).
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-27 and 2026-10-28) with gradle in a scratch worktree and with the standalone JUnit runner, assertions on and off; the 10.5.2 (on and off) and 9.12.3 (on) release jars. The follow-up ran the finder's test on 7.1.0, 7.7.3, 8.0.0, 8.9.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main, and its version-portable test also on 7.0.0 and 7.0.1 (both clean). The cross-model reviews ran onea57360912ed, whose spatial-extras and spatial3d sources are the same. The fix was validated with gradle (the new test, the whole spatial-extras check alone and together with the getCrossesDateLine fix, the closest suites 10 times) and with the standalone runner against every repro.
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:35 (at a9779ef3c193) (@lucene.experimental), :39-42 (the four bound fields), :44-56 (6-argument constructor stores the caller's bounds), :100-111 (reset assigns shape, center and boundingBox only), :118-130 (getWidth, getHeight), :132-150 (getMinX..getMaxY return the fields), :152-155 (getCrossesDateLine reads the fields)lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:164-173 (at a9779ef3c193) (rect builds the GeoBBox and passes the raw minX..maxY to the 6-argument constructor) and lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28 (@lucene.experimental)lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java:32 (at a9779ef3c193) (@lucene.internal), :55-56 ('// reused' rect(0, 0, 0, 0) per getValues call, i.e. per segment), :74 (rect.reset per document); lucene/spatial-extras/src/java/org/apache/lucene/spatial/ShapeValues.java:35 ('Returns a Shape for the current document')lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:71 (at a9779ef3c193) (@lucene.experimental), :218-219 (makeShapeValueSource returns BBoxValueSource), :223-225 (makeDistanceValueSource via getCenter), :232-235 (makeOverlapRatioValueSource over makeShapeValueSource); lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:67 (@lucene.experimental), :148-149, :219-232 (score from target.getMinX()..getMaxY(), getWidth(), getHeight(); asserts at 231-232)
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:35, (at a9779ef3c193) :39-42, :44-56, :85-98, :100-111, :113-116, :118-155; Geo3dDistanceCalculator.java:43-71lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java:32, (at a9779ef3c193) :55-56, :74; lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:146-156, :228-229; lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:179-182, :206, :218-236, :254-276; BBoxSimilarityValueSource.java:113-124 (explain makes its own getValues call)
- Who is affected
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28; (at a9779ef3c193) lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:71
- Why it looks unintended
lucene/spatial-extras/src/java/org/apache/lucene/spatial/ShapeValues.java:34-35; (at a9779ef3c193) lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dPointShape.java:40-59; lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dCircleShape.java:39-70; lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java:126-128; lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:85-90, :178, :214
- Suggested fix
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:63-83 (at a9779ef3c193) (setBoundsFromshape), :152-155; lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:231 (the queryRatio assertion); lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:179-182, :206
- History
a Lucene clone: git merge-base --is-ancestor 85491401d54c releases/lucene-solr/7.1.0 (yes) and releases/lucene-solr/7.0.1 (no)
- TestGeo3dRectangleResetBounds.java (JUnit test):
repro/LFV-675/src/org/apache/lucene/spatial/spatial4j/TestGeo3dRectangleResetBounds.java(32 lines)
A step-by-step picture made in parallel with this doc; the PNG is the final result frame. Every value on screen is parsed from its own executed probe (main a9779ef3c193 with assertions on and off, and with the fix shadowed) and checked before it is drawn, with the same boxes as this doc.
assets/LFV-675/bug.png(449 KB)
