A query cache asks each cached query how much heap it holds. PointInGeo3DShapeQuery answers with its own small fields plus a fixed 256-byte guess for the shape and another for the shape's bounds, so a polygon or path of any size reports 592 bytes.
- ramBytesUsed(). Queries that implement
Accountablereport an estimate of the heap they hold.RamUsageEstimator.sizeOfObjectsizes known types (strings, arrays, collections,Accountableobjects) and gives any other object a flat 256 bytes, documented as "a few primitive fields and a couple short String-s". - LRUQueryCache. It caches the matching documents of filters, keyed by query, and evicts the least recently used entries once it holds more than
maxSizeentries or more thanmaxRamBytesUsedbytes. Each cachedAccountablequery is charged itsramBytesUsed(). - Geo3DPoint shapes. The spatial3d polygon, large-polygon and path factories turn the input into a
GeoShapemade of planes, edges and search trees: about 280 bytes per edge fornewPolygonQuery, 680 fornewLargePolygonQueryand 1,450 per point fornewPathQuery.
- Build
Geo3DPoint.newLargePolygonQueryfor a polygon with 8 edges and one with 2,000 edges, and ask each forramBytesUsed(). The two queries hold about 6.4 KB and 1.36 MB of heap. - You expect the 2,000-edge query to report far more:
ramBytesUsed()is an estimate of the object's heap, andLRUQueryCachecharges it against itsmaxRamBytesUsedbudget. - Instead both report 592 bytes (with compressed oops; 624 without).
PointInGeo3DShapeQuerysizes its shape and the shape's bounds withRamUsageEstimator.sizeOfObject, and since neither aGeoShapenor anXYZBoundsisAccountable, each of the two counts as a fixed 256-byte unknown object. - The cache believes it: with a 1 MiB budget it keeps 8 cached 2,000-edge filters that hold 10.9 MB, reports 6,080 bytes and evicts nothing. Hits stay correct.
- Releases 8.2 to 10.6 give every
IndexSearchersuch a cache by default (for segments of 10,000+ documents); on main only applications that install one are hit.
Users of Geo3DPoint polygon, large-polygon or path filters with a query cache: by default in 8.2 to 10.6 (one JVM-wide cache, segments of 10,000+ documents, after 2 uses), on main only with an installed cache. The RAM budget stops bounding these keys; only the entry limit does. Hits stay correct.
Mechanism, terms and numbers534 words
Geo3DPoint.newPolygonQuery, newLargePolygonQuery and newPathQuery build a PointInGeo3DShapeQuery, which implements Accountable. Its ramBytesUsed() adds RamUsageEstimator.sizeOfObject(shape) and sizeOfObject(shapeBounds); no GeoShape or XYZBounds is Accountable, so each falls back to the fixed 256 bytes that RamUsageEstimator assigns to unknown objects, and every such query reports the same size whatever its shape, 592 bytes on a field named f with compressed oops (624 without): 4.9x too low for an 8-edge polygon, 47x for 99 edges, about 2,300x for a 2,000-edge large polygon (1,360,960 bytes retained). LRUQueryCache charges a cached Accountable query exactly this value, so its maxRamBytesUsed budget stops bounding these keys and only its entry limit does. Search hits are always correct. The code dates from LUCENE-8855 (8.2.0) and is unchanged on main and in 10.6.0.
- The factories.
Geo3DPoint.java:168-172(newPolygonQuery, documented for "a limited number of edges (less than 100)",:157-159),:186-190(newLargePolygonQuery, "optimized for very large numbers of polygon edges",:175-177) and:203-212(newPathQuery, no documented limit) build aGeoShapeand callnewShapeQuery(:241-243):new PointInGeo3DShapeQuery(field, shape).newDistanceQueryandnewBoxQuerygo the same way, but their shapes do not grow with the input. - The query.
PointInGeo3DShapeQuery.java:44implements Accountable; it keeps the shape and anXYZBounds(:48-58), andisCacheablealways returnstrue(:113-116). - The estimate.
:160-166:BASE_RAM_BYTES + sizeOfObject(field) + sizeOfObject(shape) + sizeOfObject(shapeBounds). - The fallback.
RamUsageEstimator.java:393-395:sizeOfObject(Object)passesUNKNOWN_DEFAULT_RAM_BYTES_USED(256,:82-86) as the default; the type switch (:407-444) knowsAccountable, strings, primitive arrays, queries, maps and collections, and any other object gets the default (:435-441). AGeoComplexPolygon,GeoConvexPolygonorGeoStandardPathand anXYZBoundsare none of those, so 512 of the 592 bytes are the two fixed guesses. - The cache.
LRUQueryCache.java:465-471chargesRamUsageEstimator.sizeOf(query, 32)("Deep RAM cost of the query object itself"), which returnsramBytesUsed()for anAccountablequery (RamUsageEstimator.java:378-386); the charge is added when a query is first cached in a partition (LRUQueryCache.java:1121-1124, throughonQueryCache,:1037-1039), and eviction runs only whilesize > maxSize || this.ramBytesUsed > maxRamBytesUsed(:1214-1220). 10.5.2 and 10.6.0 chargeLINKED_HASHTABLE_RAM_BYTES_PER_ENTRY + ramBytesUsed()(10.6.0LRUQueryCache.java:428-433). - Why these queries get cached.
UsageTrackingQueryCachingPolicy.java:33-54: any class namedPoint*Queryis "costly" and is cached after 2 uses (:122-125).
Retained heap measured with the test framework's RamUsageTester, not counting the shared PlanetModel (compressed oops; on 645d4fe14a20):
| query | ramBytesUsed() | retained |
|---|---|---|
newPolygonQuery, 8 edges | 592 | 2,920 |
newPolygonQuery, 99 edges | 592 | 27,696 |
newLargePolygonQuery, 8 edges | 592 | 6,400 |
newLargePolygonQuery, 2,000 edges | 592 | 1,360,960 |
newLargePolygonQuery, 5,000 edges | 592 | 3,400,960 |
newPathQuery, 200 points | 592 | 290,168 |
One segment of 100 points, new LRUQueryCache(1000, 1 MiB, ctx -> true, Float.POSITIVE_INFINITY, partitions, null) with an always-cache policy, 8 different 2,000-edge newLargePolygonQuery filters each run once as a ConstantScoreQuery: 8 entries, 0 evictions, cache.ramBytesUsed() = 6,080 bytes (8 x 760: 592 for each query key, 168 for the entry and its cached doc id set), while the 8 query keys retain 10,888,352 bytes (with 1 and with 16 partitions); 296 hits in total, the same as with the fix.
Exposure and likelihood318 words
- Who. Applications that filter or count with
Geo3DPointpolygon, large-polygon or path queries (the spatial3d module,@lucene.experimental) on an index searched through a query cache. - 8.2.0 to 10.6.0: by default.
IndexSearcherinstalls one staticLRUQueryCache(1000, min(32 MB, heap / 20))for every searcher in the JVM (10.6.0IndexSearcher.java:95-100), caching only on segments of at least 10,000 documents that also hold more than half the average documents per segment (10.6.0LRUQueryCache.java:175-177,:188-196), withUsageTrackingQueryCachingPolicy, which caches these queries after 2 uses. - main and
branch_11x: "Caching is disabled by default" (IndexSearcher.java:82-84); only applications that install a cache are hit. - How much. The under-count grows by about 280 bytes per edge for
newPolygonQuery, 680 fornewLargePolygonQueryand 1,450 per path point (310, 810 and 1,770 without compressed oops). The study's heap probe, on maina9779ef3c19and 10.5.2 (the files involved are unchanged since): with a 256 MB heap andLRUQueryCache(1000, 13.4 MB)plus the stock caching policy, distinct 3,000-edgenewLargePolygonQueryfilters, each sent twice, ran the JVM out of heap after 128 polygons while the cache reported 415,744 bytes and had evicted nothing; without a cache all 400 ran. With a 3 GB heap only the entry limits held: on main the cache (16 partitions of at most 62 entries) ended at 964 entries after 136 evictions, all driven by the entry count (it counted 3.1 MB of its 32 MiB budget), holding 1,986,237,856 bytes; the 10.5.2 default cache ended at itsmaxSizeof 1,000 entries, holding 2,057,955,792 bytes. - What it does not do. Hits were identical with and without a cache in every run; nothing is bypassed, exposed or corrupted. Small polygons are off by little in absolute terms (8 edges: about 2.3 KB).
- How likely: rare. It takes shapes with hundreds to thousands of vertices, repeated often enough to be cached, in enough distinct copies to matter for the heap.
One JUnit test, no index and no randomness: build newLargePolygonQuery for an 8-edge and a 2,000-edge polygon and assert that the larger one reports more heap. Public API only; the cast is to the public Accountable interface.
/*
* 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.spatial3d;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.Accountable;
public class TestGeo3DShapeQueryRamBytesUsed extends LuceneTestCase {
private static long ramBytesUsed(int edges) {
double[] lats = new double[edges + 1];
double[] lons = new double[edges + 1];
for (int i = 0; i < edges; i++) {
lats[i] = 30 * Math.sin(-2 * Math.PI * i / edges);
lons[i] = 40 * Math.cos(-2 * Math.PI * i / edges);
}
lats[edges] = lats[0];
lons[edges] = lons[0];
Polygon polygon = new Polygon(lats, lons);
return ((Accountable) Geo3DPoint.newLargePolygonQuery("f", PlanetModel.WGS84, polygon))
.ramBytesUsed();
}
public void testRamBytesUsedGrowsWithPolygonSize() {
long small = ramBytesUsed(8);
long large = ramBytesUsed(2000);
// expected: the 2000-edge query reports more heap than the 8-edge one; actual: both report 592
assertTrue("8 edges: " + small + " bytes, 2000 edges: " + large + " bytes", large > small);
}
}
cd $LUCENE && ./gradlew :lucene:spatial3d:test --tests TestGeo3DShapeQueryRamBytesUsed -Ptests.seed=DEADBEEF
- Expected
- The 2,000-edge query (about 1.36 MB retained) reports more
ramBytesUsed()than the 8-edge one (about 6.4 KB). - Observed
AssertionError: 8 edges: 592 bytes, 2000 edges: 592 bytes, with assertions on and off.
More files, environment, full output231 words
The two queries differ only in the number of edges and come from the same factory, the one documented for very large polygons. The assertion is the weakest one any heap estimate must pass, so it needs no tolerance. The retained sizes quoted on this page come from a separate diagnostic with RamUsageTester, not from the repro.
Release jars: the test fails on 10.6.0 with assertions on and off; the finder's earlier tests failed the same way on 8.2.0 to 10.5.2.
Regression test: this same test; it passes with the recommended fix and fits as one more method of spatial3d's TestGeo3DPoint.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Release 10.6.0: fails |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
cp $REPRO/TestGeo3DShapeQueryRamBytesUsed.java $LUCENE/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/
cd $LUCENE && ./gradlew :lucene:spatial3d:test --tests TestGeo3DShapeQueryRamBytesUsed -Ptests.seed=DEADBEEF
# the same result with -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF (assertions on; the same with -Ptests.asserts=false)
TestGeo3DShapeQueryRamBytesUsed > testRamBytesUsedGrowsWithPolygonSize FAILED
java.lang.AssertionError: 8 edges: 592 bytes, 2000 edges: 592 bytes
at __randomizedtesting.SeedInfo.seed([DEADBEEF:F9C51CF1D77A9822]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.assertTrue(Assert.java:42)
at org.apache.lucene.spatial3d.TestGeo3DShapeQueryRamBytesUsed.testRamBytesUsedGrowsWithPolygonSize(TestGeo3DShapeQueryRamBytesUsed.java:44)
# the numbers behind it (a diagnostic test on the same build, RamUsageTester, PlanetModel excluded; not the repro)
newLargePolygonQuery 8 edges: reported 592, retained 6,400
newLargePolygonQuery 2000 edges: reported 592, retained 1,360,960
# with the fix: reported 9,560 and 2,049,368; the test passesramBytesUsed()is "an estimate of the JVM heap memory used by this object"; one that ignores the shape cannot track it.- The 256-byte fallback is documented for "a few primitive fields and a couple short String-s".
- LUCENE-8855 added
Accountablefor queries "much larger than the default estimate (10x or more)"; #16405 fixed a far smaller under-count as a bug.
The full argument206 words
- The contract.
Accountable.java:34-40: "Returns an estimate of the JVM heap memory used by this object in bytes."LRUQueryCache.java:137-140: the cache holds "at mostmaxSizecache entries ... with at mostmaxRamBytesUsedbytes of memory"; the class javadoc says it evicts "in order to remain under a given maximum size and number of bytes used" (:60-62). - The fallback's own definition.
RamUsageEstimator.java:82-86: "Approximate memory usage that we assign to all unknown objects - this maps roughly to a few primitive fields and a couple short String-s." - The introducing change's goal. LUCENE-8855 (#9898), the 2019-06-19 patch note that introduced the design that was committed: it "adds Accountable to Queries that may be much larger than the default estimate (10x or more)". On the same issue Adrien Grand wrote: "For the caching use-case I suspect it's better to overestimate memory usage a bit?"
- Upstream tests.
TestPointQueries.java:2653-2669andTestTermInSetQuery.java:377-389holdramBytesUsed()within 5% ofRamUsageTester.ramUsed(query);TestLRUQueryCache.testCachingAccountableQuery(:702-726) checks that the cache reflects bigAccountablequeries. No spatial3d test callsramBytesUsed. - Precedent. #16405 ("PointInSetQuery#ramBytesUsed under-reports memory",
type:bug, fixed by #16406); #15097 ("Incorrect size estimation of BooleanQuery in LRUQueryCache causing heap exhaustion", fixed by #15124, a fix that does not reachAccountablequeries).
The devil's-advocate check259 words
The strongest case: ramBytesUsed() is only an estimate and sizeOfObject documents its 256-byte fallback; on LUCENE-8855 its author wrote "It's not a strict calculation anyway"; Accountable is @lucene.internal and Geo3DPoint, the query and LRUQueryCache are @lucene.experimental; GeoShape is an open interface, so no estimate fits every shape; caching is off by default on main; and on the same issue Adrien Grand first argued against making queries Accountable at all, naming Weight#isCacheable (keep large queries out of the cache) or a rough visitor-based estimate as alternatives. Open issue #16113 also proposes counting constructor inputs "only to the extent they are owned".
Why it fails: neither Accountable nor the cache promises an accuracy, but the value is not rough, it is constant; it does not move when the edge count grows 250-fold (the retained heap about 213-fold). The constant is documented for a few primitive fields, and the change that wrote this method aimed at queries 10x or more above the default. Adrien Grand then proposed the design that was merged themselves (ramBytesUsed() for Accountable queries, a constant for the rest), and this class was made Accountable under it. The isCacheable route was not taken either: isCacheable is always true, so large shapes neither opt out nor get sized. An open GeoShape explains the fallback for user shapes, not for the shapes Geo3DPoint builds itself; under #16113 those still count, since the factory builds each shape for that query alone and the cache keeps it alive. The verifier panel rated the case moderate and convincing to none.
Recommended Size the shape from its vertex count in the Geo3DPoint factories
Let the Geo3DPoint polygon, large-polygon and path factories pass a size to the query: vertices times 512, 1,024 or 2,048 bytes. Every measured polygon and path of 3+ vertices then reports 1.16x to 1.86x its real heap. The repro passes with assertions on and off, and so do the spatial3d module's 181 tests.
// Geo3DPoint: per-vertex heap estimates for the shapes built below (comment shortened)
private static final long POLYGON_BYTES_PER_VERTEX = 512;
private static final long LARGE_POLYGON_BYTES_PER_VERTEX = 1024;
private static final long PATH_BYTES_PER_POINT = 2048;
...
return new PointInGeo3DShapeQuery(
field, shape, numVertices(polygons) * LARGE_POLYGON_BYTES_PER_VERTEX);
// PointInGeo3DShapeQuery.ramBytesUsed()
return BASE_RAM_BYTES
+ RamUsageEstimator.sizeOfObject(field)
+ shapeRamBytesUsed
+ RamUsageEstimator.sizeOfObject(shapeBounds);All options and trade-offs4 options · 939 words
Option A was built and run through gradle. B (make the GeoShape classes Accountable) is potentially the most accurate but a much larger change, not built; C (return false from isCacheable for large shapes, one of the two alternatives Adrien Grand named on LUCENE-8855 before proposing the merged design) stops the cache overrun but leaves the value wrong and can complement A; D (document it) leaves the budget broken. Workarounds that need no fix: cap the vertices per shape for request-built queries, install a QueryCachingPolicy that declines these queries, or run without a query cache (the default on main).
Size the shape from its vertex count in the Geo3DPoint factories (validated by the checks) (recommended)
The polygon, large-polygon and path factories know how many vertices they were given (holes included). They pass vertices x bytes per vertex to a new package-private PointInGeo3DShapeQuery constructor, which uses it instead of sizeOfObject(shape), never less than the old 256. newShapeQuery(field, shape) and the existing two-argument constructor keep sizeOfObject(shape), which already uses a caller's shape's ramBytesUsed() when that shape is Accountable. 2 files, +35/-4.
// Geo3DPoint: per-vertex heap estimates for the shapes built below (comment shortened)
private static final long POLYGON_BYTES_PER_VERTEX = 512;
private static final long LARGE_POLYGON_BYTES_PER_VERTEX = 1024;
private static final long PATH_BYTES_PER_POINT = 2048;
...
return new PointInGeo3DShapeQuery(
field, shape, numVertices(polygons) * LARGE_POLYGON_BYTES_PER_VERTEX);
// PointInGeo3DShapeQuery.ramBytesUsed()
return BASE_RAM_BYTES
+ RamUsageEstimator.sizeOfObject(field)
+ shapeRamBytesUsed
+ RamUsageEstimator.sizeOfObject(shapeBounds);Trade-offs
- How close. Each constant is the next power of two above the measured heap per edge or point for shapes of 8 or more edges or 20 or more path points (about 280-430, 680-960 and 1,400-1,770 bytes, with and without compressed oops). Smaller shapes cost more per edge (3 edges: up to 643 and 1,232 bytes) but are still over-reported. With the fix every measured polygon and path of 3 or more vertices reports 1.16x to 1.86x its retained heap on both layouts (8 edges: 9,560 vs 6,408 bytes; 2,000 edges: 2,049,368 vs 1,360,968); the larger gaps are short paths, 1 point (2,392 vs 624) and 2 points (4,440 vs 1,888). No measured polygon or path is under-reported, in line with Adrien Grand's remark on LUCENE-8855, made about the constant for unknown queries, that for caching it is "better to overestimate".
- Constants. They follow today's
spatial3d.geominternals and would need re-measuring if those change; aRamUsageTestertest with a tolerance, likeTestPointQueries.testRamBytesUsedMatchesActualWithinTolerance, could guard them. - Tiling fallback.
newPolygonQueryfalls back toGeoComplexPolygonwhen it cannot tile the polygon (GeoPolygonFactory.java:352-362), about 680 bytes per vertex, so 512 under-counts that case by about 1.3x. - Unchanged. Shapes that callers pass to
newShapeQuery, and circles and boxes, are priced as before. - Effect on the cache. Large shapes now count against the budget: in the cache diagnostic each 2,000-edge key is charged about 2 MB, more than the whole 1 MiB budget, so the cache evicts all 8 (0 entries, 8 evictions, the same 296 hits). By arithmetic (not run), the 10.x default cache (one budget of at most 32 MiB) has room for about 16 such keys before other overhead, instead of up to 1,000; on main the budget is split across partitions, so a key larger than one partition's share is not kept. Re-running an evicted filter costs one BKD intersection per segment.
- Cost. One pass over the input vertex arrays when the query is built.
What the checks validated
Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-31; the worktree was removed afterwards):
- stock: the minimal test fails with assertions on and off;
- with the fix: the minimal test passes with assertions on and off;
- the finder's original two tests: the size test (reported within 2x of retained for 8- and 99-edge polygons, a 2,000-edge large polygon and a 200-point path) passes; the cache test now fails at its first assertion, which expects all 8 keys to stay cached (
cache entries=0 evictions=8): the cache evicts them, the intended effect, so that test is not a regression test for the fix; - the closest suite
TestGeo3DPoint: 17 tests, 0 failures, 2 skipped; - the whole
:lucene:spatial3d:test: 19 classes, 181 tests, 0 failures, 3 skipped; checkGoogleJavaFormat,ecjLintMain,forbiddenApisMainandrenderJavadocfor spatial3d pass.
Implement ramBytesUsed() in GeoComplexPolygon, GeoConvexPolygon, GeoConcavePolygon, the composite shapes and GeoStandardPath (and other shapes as needed) from their arrays, edges, planes and trees; sizeOfObject(shape) then picks it up with no change to the query.
Trade-offs
Potentially the most accurate option (not built or measured), with no constants, and it also covers newShapeQuery with built-in shapes. A much larger change across spatial3d.geom (the large polygon alone holds edges, planes and three search trees), with a RamUsageTester test per shape type, and an ownership rule so that shared objects (the PlanetModel, parts that composite shapes share) are not counted twice. Not run.
Return false from isCacheable (PointInGeo3DShapeQuery.java:113-116) for large shapes, for example above a vertex count the factories pass. Adrien Grand named this route on LUCENE-8855 ("rely on the existing Weight#isCacheable logic, which disables caching on large queries") as one of two alternatives to making queries Accountable, before proposing the compromise that was merged.
Trade-offs
Stops the cache overrun without constants, but ramBytesUsed() stays wrong, the minimal test still fails, and repeated large filters are re-evaluated every time. Can be combined with the recommended option. Not run.
Say in the javadoc that the estimate leaves out the shape.
Trade-offs
The cache budget stays broken. Not run.
Services that build Geo3DPoint polygon or path filters from request input should cap the vertices per shape or keep these queries out of the query cache until a fix ships, because the cache can otherwise hold up to maxSize large shapes, far beyond its RAM budget.
A hunt unit compared each Accountable query's ramBytesUsed() with the heap it really retains, for 26 public-API constructions of 12 Accountable query classes in core, join and spatial3d. It flagged 9 Geo3DPoint polygon and path constructions (4.9x to 5,745x too low); reading PointInGeo3DShapeQuery.ramBytesUsed() showed why, and a JUnit on the stock jars confirmed it.
The method in full191 words
Method: semi-formal (the hunt's tier for a systematic differential against an independent measurement). The unit hunted identity and accounting defects outside query semantics.
- Differential. A harness built 26 constructions of 12
Accountablequery classes through public APIs (the join module'sGlobalOrdinalsQueryandGlobalOrdinalsWithScoreQuerywere not covered) and comparedAccountable#ramBytesUsed()with the heap each query object retains (the test framework'sRamUsageTester, not following readers, directories or thePlanetModelsingleton), before and after a search through an always-cachingLRUQueryCache. TheAutomatonQueryNFA case of LFV-192 served as a positive control. - Signal. 11 of the 26 were flagged (more than 2x under-reported): all 9
Geo3DPointpolygon and path constructions (4.9x to 5,745x), theAutomatonQueryNFA positive control (3.2x) and the numeric score join (2.4x, the separate LFV-402). The others were at most 1.9x under (theGeo3DPointbox), most within 1.1x. - Confirmation. Reading
PointInGeo3DShapeQuery.ramBytesUsed()showed thesizeOfObject(shape)fallback, and a JUnit on the stock jars failed on main, 10.5.2 and 9.12.3. A cross-model review's objection (a 2,000-edgenewPolygonQueryexceeds its documented "less than 100" edges) led to usingnewLargePolygonQueryfor large polygons. A 2026-10-29 minimization cut the repro to the one test above.
No prior report found: 129 logged queries on 2026-10-26 (GitHub issues, PRs and commits, the JIRA archive, four mailing lists, downstream repositories, the web) plus the changed files of all 411 open PRs, and a GitHub re-check on 2026-10-31. Closest: LUCENE-8855 (#9898), which introduced the code, and #16405, a fixed under-count elsewhere.
Searches4 searches
- #9898 / LUCENE-8855 introduced the code; it never mentions geo3d. #16113 / #16114 (open) discuss
Accountableownership semantics, with no geo3d. #16405 / #16406 (PointInSetQueryunder-count, fixed) and #15097 / #15124 (non-AccountableBooleanQuerycache overrun, fixed; it does not reachAccountablequeries) are the same symptom class elsewhere. #9053 / LUCENE-8005 (open) is about thePoint*Queryname match in the caching policy. #12614, #16657, #16743 and #15026 are other accounting paths. - Open PRs: the only one touching
PointInGeo3DShapeQuery.java, #709, changes aDocIdSetBuildercall. - Related, different root cause: on main a non-
AccountableLatLon polygon query is charged a flat 32 bytes by the cache's query visitor (LRUQueryCache.java:468-470,RamUsageEstimator.java:314-357; read, not executed). Maintainers may raise it. - Limits: GitHub search does not index PR diffs or review comments; the list search matches words, not phrases; private security reports are invisible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; Elasticsearch, OpenSearch and Solr repositories) | 2026-10-26 | the scripted prior-report search (51 queries over GitHub, commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and JIRA; 1,329 and 1,155 distinct hits screened), 39 manual GitHub searches (class, factory and constant names, symptom words), the changed files of all 411 open apache/lucene PRs | 18 items recorded, none the same bug |
| Mailing lists and JIRA archive | 2026-10-26 | 24 queries over dev@, java-user@ and issues@; 9 JQL queries over LUCENE and SOLR | no report |
| Web | 2026-10-26 | 6 queries | 10 hits, none relevant |
| GitHub issues and PRs (apache/lucene) | 2026-10-31 | PointInGeo3DShapeQuery; Geo3DPoint ramBytesUsed; geo3d ramBytesUsed; spatial3d Accountable; GeoShape ramBytesUsed; geo3d query cache memory; UNKNOWN_DEFAULT_RAM_BYTES_USED; and items updated since 2026-10-20 for three symptom phrases | 14 old geo3d items (test failures, integration history, #9053) and #16743, #16113, #9898; none about this bug (re-run by the independent fact check the same day, same hit counts) |
Introduced in 8.2.0 by LUCENE-8855 (a76c962ee61, 2019), which made the query Accountable but sized its shape with the 256-byte fallback. The cache charges that value since 8.11.0 (LUCENE-10103). Unchanged through 10.6.0, branch_11x and main.
Versions tested5 rows
Introduced by a76c962ee61f5185d30ff5bd94a23c16d21a5a98 (main) and the branch_8x backport 81208427d25f4541957eeeb03d971e1c45226428, LUCENE-8855 "Add Accountable to some Query implementations" (Andrzej Bialecki, 2019-06-26; a JIRA patch with no pull request, now GitHub issue #9898). The only later change to the method is the LUCENE-9570 reformatting 2cbf261032d; no GeoShape class has ever been Accountable (git log -S Accountable on the geom package).
The cache consequence through this method starts in 8.11.0 with LUCENE-10103 (#346, 6a41bc6310e, Patrick Zhai, 2021-10-13), which made LRUQueryCache charge Accountable queries their ramBytesUsed(). From 8.2.0 to 8.10.1 the cache charged every query a flat 1,024 bytes plus its per-entry overhead, which under-counted large shapes for a different reason.
Default cache: 8.2.0 to 10.6.0 install a JVM-wide default cache; main and branch_11x disable caching by default.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off (gradle and the standalone runner); passes with the fix |
| 10.6.0 | affected | minimal test fails, assertions on and off (release jars) |
| 8.2.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.5.2 | affected | ramBytesUsed() = 592 for all four test shapes; the cache keeps 8 large keys within its count but far over its 1 MiB budget |
| 8.1.1 | API absent | the query is not Accountable; the cache's flat 1,024-byte charge under-counts for a different reason |
upstream main 2f424fd428da, branch_11x 603af8703753, branch_10_6 7e5aec4370e0 | read | same method |
Confirmed by an independent reproducer, a devil's-advocate verifier and an adjudication. Of read-only reviews from another model family (OpenAI GPT-5.6), two said not real on procedure only, since they could not run code; all agreed on the root cause. A credibility audit, the minimization and this doc's gradle run reproduced it again.
Checks and verdicts262 words
Every executed check agrees; the only dissents are procedural, from reviewers that could not execute anything.
| Check | Verdict | What it found |
|---|---|---|
| Finder (Accountable-query RAM differential, then JUnit) | candidate | 9 Geo3DPoint constructions flagged (4.9x to 5,745x); its 2-test JUnit fails 2/2 on main (assertions on and off), 10.5.2 and 9.12.3. |
| reviews (OpenAI GPT-5.6, read-only) | refute: not real (procedural, low); intended: real (medium) | Could not run code; the one real objection (a 2,000-edge newPolygonQuery exceeds its documented limit) was fixed by switching to newLargePolygonQuery. |
| Independent reproducer | real | Own 4-test class fails 2/4 on main, 10.5.2 and 9.12.3 (up to 3,448x; 16.3 MB cached under 1 MiB); a LongPoint.newSetQuery control is at 1.02x. |
| Refute lens (OpenAI GPT-5.6, read-only) | not real (procedural, low) | Root cause agrees, inputs in contract; a separate GC heap-delta check (no RamUsageTester) measured 1,369,074 bytes per 2,000-edge large polygon against 592 reported. |
| Intended-behaviour devil's advocate | real (high) | Re-ran on main with assertions on and off; no intended, documented or misuse argument survives. |
| Adjudication | confirmed (high), class minor | Re-ran the independent reproduction's tests on main; overruled the candidate's "silent wrong results": hits are correct. |
| Credibility audit | keep, minor | Re-ran the finder's tests on main with a fresh seed; the recorded numbers matched. |
| Minimal repro | verified | The one-test class fails on 645d4fe14a20 with assertions on and off and two seeds. |
| This doc | real; fix validated | Gradle in a scratch worktree of 645d4fe14a20: stock fails, the fix passes the test and the spatial3d module; 10.6.0 jars fail. |
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 599 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,445 words
- Registry title
- Geo3DPoint polygon and path queries report a constant 592-byte ramBytesUsed() regardless of shape size, because PointInGeo3DShapeQuery prices its GeoShape as a 256-byte unknown object, so LRUQueryCache can hold many times its maxRamBytesUsed (since 8.2.0)
- Area
- spatial3d / PointInGeo3DShapeQuery RAM accounting (Accountable#ramBytesUsed; core LRUQueryCache RAM budget)
- Priority
- Low, rank 731: A constant ramBytesUsed() for large Geo3D shape queries lets LRUQueryCache go far over its RAM budget, bounded only by maxSize, with correct hits.
- Impact class
- minor
- Impact note
- Applications that filter with Geo3DPoint polygon, large-polygon or path queries (the spatial3d module) and have a query cache are hit: the IndexSearcher default cache in 8.2-10.5.x (segments of at least 10,000 docs, after 2 uses of an equal query, because the default policy treats Point*Query classes as costly) or an application-installed cache on main, where caching is off by default; every such query is charged 592 bytes while a 2,000-edge newLargePolygonQuery retains about 1.36 MB and a 200-point newPathQuery about 290 KB, so maxRamBytesUsed no longer bounds the heap of the cached keys (only maxSize, 1,000 entries by default, does); hits are always correct, small polygons are off by only a few KB, and no extreme scale is needed (executed: 8 keys retain 10.9 MB under a 1 MiB budget).
- Summary
- You filter with a Geo3DPoint polygon or path query, for example newLargePolygonQuery with 2,000 edges or newPathQuery with 200 points, and expect its ramBytesUsed() to estimate the heap it holds, which is what Accountable promises and what LRUQueryCache charges against its maxRamBytesUsed budget. Instead PointInGeo3DShapeQuery.ramBytesUsed() passes the shape and its bounds to RamUsageEstimator.sizeOfObject, which prices every object that is not Accountable (no GeoShape is) at a fixed 256 bytes, so every such query reports 592 bytes whatever its size: 4.9x too low for an 8-edge polygon, 47x for 99 edges, about 2,300x for the 2,000-edge large polygon (1,360,960 bytes retained). An LRUQueryCache with a 1 MiB budget therefore keeps 8 such filters that retain 10.9 MB, accounts about 6 KB for them and evicts nothing; the hits are correct.
- Introduced
- a76c962ee61f5185d30ff5bd94a23c16d21a5a98 (main; branch_8x backport 81208427d25f4541957eeeb03d971e1c45226428), LUCENE-8855 'Add Accountable to some Query implementations' (GitHub issue apache/lucene#9898; a JIRA patch by GitHub sigram, reviewed on the issue by jpountz and atris; no pull request): it made PointInGeo3DShapeQuery Accountable with ramBytesUsed() = BASE_RAM_BYTES + sizeOfObject(field) + sizeOfObject(shape) + sizeOfObject(shapeBounds) [read: git log -L on the method lists only this commit and the LUCENE-9570 reformatting 2cbf261032d; git blame -w ignoring the reformatting attributes every line to it; git log -S Accountable on the file finds only LUCENE-8855] [exec: 8.1.1 has no Accountable on the class, the value test is skipped; 8.2.0 fails]. Not a regression of later work: no GeoShape class has ever implemented Accountable (2019-06-26)
- First affected version
- 8.2.0 [exec] (8.1.1 and earlier: PointInGeo3DShapeQuery is not Accountable and has no ramBytesUsed() [exec: test skipped]). The cache-budget consequence through this method starts in 8.11.0 (LUCENE-10103, apache/lucene#346: LRUQueryCache charges Accountable queries their ramBytesUsed()); in 8.1.1-8.10.1 the cache charged a flat 1,024 bytes per query, which under-counted the same keys for a different reason [exec]
- Affected versions
- 8.2.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.5.2 and main ea57360912ed. [exec, all fail]: 8.2.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.5.2 and main (-ea and -da); ramBytesUsed() is 592 for every shape in every executed version. [read]: the other releases in those ranges (method body unchanged since 8.2.0); upstream main cf398bfb142 (2026-10-26) has the same method. Cache charge for 8 cached 2,000-edge keys [exec]: 9,168 bytes in 8.1.1-8.10.1 (flat 1,024 per key), 5,712 in 8.11.x, 5,904 in 9.0.0-10.5.2, 6,080 on main, against 10.9-11.0 MB retained. Not executed: 8.3.0-8.9.0, 9.2.0-9.11.1, 10.0.0-10.5.1, maintenance branches, x86
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (2026-10-26, the prior-report record verdict new, high confidence on absence of evidence; all read-only): scripted prior-report searches with 51 queries over apache/lucene issues, PRs and commits, the Elasticsearch, OpenSearch and Solr repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive (1,329 and 1,155 distinct hits screened), plus 6 web queries; 39 manual GitHub searches; 24 mailing-list and 9 JIRA queries; the changed files of all 411 open apache/lucene PRs; the file's commit history; 18 issue, PR and JIRA reads; 18 items recorded, all same_bug false. Not fixed: upstream main cf398bfb142 (2026-10-26) has the same ramBytesUsed(), and the only open PR touching PointInGeo3DShapeQuery.java (#709) changes only a DocIdSetBuilder call. Closest items, not this bug: #9898 / LUCENE-8855 (introduced the code; stated goal: account queries 'much larger than the default estimate (10x or more)'; never mentions geo3d), #16113 / #16114 (open, Accountable ownership semantics, no geo3d), #16405 / #16406 (PointInSetQuery under-count, fixed: precedent), #15097 / #15124 (non-Accountable BooleanQuery cache overrun, fixed; the fix does not reach Accountable queries), #15026, #12614, #16657 and #16743 (other accounting paths). Gaps: GitHub search does not index PR diffs or review comments, the list API matches words rather than phrases, and private security@ reports are invisible
- 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, and so do the spatial3d module's tests. Release jars 10.6.0: the minimal test fails. Earlier checks used the finder's tests (ported where the API differs) on mainea57360912edand on 9 releases (8.1.1, 8.2.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.5.2), and the study's heap probe on maina9779ef3c19and 10.5.2; the files involved are unchanged between those commits and645d4fe14a20. Upstream main2f424fd428da,branch_11x603af8703753andbranch_10_67e5aec4370e0carry the same method (read, 2026-10-30 refs).
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java:44 (implements Accountable), :113-116 (isCacheable returns true), :160-166 (ramBytesUsed: sizeOfObject(shape) and sizeOfObject(shapeBounds)) @ ea57360912edlucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:81-86 (UNKNOWN_DEFAULT_RAM_BYTES_USED = 256, 'a few primitive fields and a couple short String-s'), :378-386 (sizeOf(Query, long) returns ramBytesUsed() for an Accountable), :388-395 (sizeOfObject(Object) uses the 256 default), :434-441 (default branch) @ ea57360912edlucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:62 ('remain under a given maximum size and number of bytes used'), :137-140 ('at most maxRamBytesUsed bytes of memory'), :465-471 (queryRamBytesUsed = RamUsageEstimator.sizeOf(query, 32)) @ ea57360912edlucene/core/src/java/org/apache/lucene/util/Accountable.java:34-40 ('Returns an estimate of the JVM heap memory used by this object in bytes') @ ea57360912edlucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:157-172 (newPolygonQuery: 'less than 100' edges), :174-189 (newLargePolygonQuery: 'optimized for very large numbers of polygon edges'), :191-203 (newPathQuery) @ ea57360912edlucene/core/src/java/org/apache/lucene/search/UsageTrackingQueryCachingPolicy.java:33-53 (isPointQuery 'Point*Query', isCostly) and lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:82-84 ('Caching is disabled by default', DEFAULT_QUERY_CACHE = null) @ ea57360912edSECURITY.md:28, :38, :44-46 @ ea57360912ed
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java:44, (at 645d4fe14a20) :48-58, :113-116, :160-166lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:157-159, (at 645d4fe14a20) :168-172, :175-177, :186-190, :203-212, :241-243lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:82-86, (at 645d4fe14a20) :378-386, :393-395, :407-444 (:435-441)lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:465-471, (at 645d4fe14a20) :1037-1039, :1096-1124, :1214-1220; UsageTrackingQueryCachingPolicy.java:33-54, :122-125a Lucene clone releases/lucene/10.6.0: lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:428-433 (read)
- Who is affected
a Lucene clone releases/lucene/10.6.0: lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:95-100, LRUQueryCache.java:175-177, :188-196 (read)lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:82-84 (at 645d4fe14a20)a Lucene clone: git diff a9779ef3c19 645d4fe14a20 for lucene/spatial3d, LRUQueryCache.java, RamUsageEstimator.java, UsageTrackingQueryCachingPolicy.java, IndexSearcher.java, Accountable.java: emptylucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:150, (at 645d4fe14a20) :191-196 (16 partitions, maxSize / 16 entries each)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/Accountable.java:30, (at 645d4fe14a20) :34-40; RamUsageEstimator.java:82-86; lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:60-62, :117, :137-140lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:2653-2669; (at 645d4fe14a20) TestTermInSetQuery.java:377-389; TestLRUQueryCache.java:702-726; lucene/spatial3d/src/test (grep ramBytesUsed: none)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java:42, (at 645d4fe14a20) :113-116; Geo3DPoint.java:42github apache/lucene#9898 (LUCENE-8855), read 2026-10-31 with gh issue view --comments: Andrzej Bialecki's 2019-06-19 patch note ('10x or more'; two more patch versions followed on 2019-06-24 and 2019-06-25), 'It's not a strict calculation anyway', Adrien Grand's comments of 2019-06-14 (Weight#isCacheable or a rough visitor-based estimate), 2019-06-18 (the visitor-plus-Accountable compromise), 2019-06-20 ('a better trade-off', 'better to overestimate')
- Suggested fix
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPolygonFactory.java:352-362 (at 645d4fe14a20) (tiling fallback); lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:150, :191-196 (partitions)a Lucene clone releases/lucene/10.6.0: LRUQueryCache.java:283-290 (one budget, no partitions); IndexSearcher.java:95-100 (32 MB cap)github apache/lucene#9898 (LUCENE-8855): Adrien Grand's comments of 2019-06-14 (Weight#isCacheable or a visitor-based estimate), 2019-06-18 (the merged compromise) and 2019-06-20 ('better to overestimate') (read 2026-10-31)
- Prior reports
gh search issues --repo apache/lucene --include-prs, read-only, 2026-10-31 (this doc)lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:314-357 (at 645d4fe14a20) (RamUsageQueryVisitor); LRUQueryCache.java:468-470
- History
a Lucene clone refs of 2026-10-30: origin/main, origin/branch_11x, origin/branch_10_6, releases/lucene/10.6.0 (PointInGeo3DShapeQuery.java:163-165 unchanged; IndexSearcher default cache)
- TestGeo3DShapeQueryRamBytesUsed.java (one test):
repro/LFV-400/src/org/apache/lucene/spatial3d/TestGeo3DShapeQueryRamBytesUsed.java(46 lines)
Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro and the diagnostic runs of this doc (reported and retained sizes; the cache numbers).
assets/LFV-400/bug.png(599 KB)
