Under setPointsOnly(true), an Intersects query on one date unit coarser than full precision becomes a single term lookup for a term that points-only indexing at full precision, the normal path, never writes.
- Index one document at
2014-10-23T21:22:33.159with aNumberRangePrefixTreeStrategyoverDateRangePrefixTree, after callingsetPointsOnly(true)because you only index exact timestamps. - Search
Intersectswith the month2014-10. You expect 1 hit: the timestamp is in October 2014, andIsWithinwith the same shape, or the same query with the defaultpointsOnly=false, returns 1. - You get 0 hits and no error. The same for
2014,2014-10-23,2014-10-23T21:22:33and the range[2014-10-01 TO 2014-10-31], which the tree turns into the month. - Why: under pointsOnly the query becomes one term lookup for "2014-10, marked as a leaf". Points-only indexing stores October 2014 only as a prefix term of each timestamp and writes leaf terms only at full millisecond precision, so that term is not there.
Direct Lucene users of NumberRangePrefixTreeStrategy (with DateRangePrefixTree or a custom tree) who opt in to setPointsOnly(true) and run Intersects on a year, month, day, hour, minute or second, or a one-unit range: every such query silently returns 0 hits. The default pointsOnly=false, spans, exact points, IsWithin and the index itself are fine.
Mechanism, terms and numbers609 words
NumberRangePrefixTreeStrategy over DateRangePrefixTree (or a custom NumberRangePrefixTree) with the opt-in setPointsOnly(true) returns no documents for an Intersects query whose shape is one unit coarser than full precision: a year, month, day, hour, minute or second, or a range such as [2014-10-01 TO 2014-10-31] that the tree normalizes to one unit. One document at 2014-10-23T21:22:33.159 is found by IsWithin with 2014-10 (1 hit) and by Intersects with pointsOnly=false (1 hit), but Intersects with pointsOnly=true gives 0. RecursivePrefixTreeStrategy.makeGridShapeIntersectsQuery builds a single TermQuery on the unit's term plus the leaf marker byte, and points-only indexing writes leaf terms only at full precision; every coarser level of a point is stored as a prefix (non-leaf) term. The 7.0.0 CHANGES entry of LUCENE-7845 (d4f87b4a36c) introduced this shortcut as an optimization for "a simple date interval (e.g. 1 month)", exactly the failing input. No error is raised; the index is unaffected. Every release from 7.0.0 to 10.5.2 and main fails the same way, and 10.6.0 has the same code (read); a one-statement fix that queries the non-leaf term for a coarse cell passes the repro and the module's whole test suite.
- Number/date range prefix tree:
DateRangePrefixTreesplits time into levels: 1 million years, 1,000 years, year (level 3), month (4), day (5), hour (6), minute (7), second (8) and millisecond (9, the maximum). A shape such as2014-10is a unit: one cell of the tree. - Indexing: a shape is indexed as one term per cell. A cell that the shape covers completely is a leaf and its term gets a trailing leaf byte (
NumberRangePrefixTree.java:690-698); a cell the shape only partly covers is a prefix term without it. A full-precision instant therefore gives 8 prefix terms (levels 1-8) and 1 leaf term (level 9). setPointsOnly(true): tells the strategy that only points (full-precision instants) are indexed; any other shape is rejected at indexing time (PrefixTreeStrategy.java:176-182). Its javadoc: "There are no \"leaves\" in such a case, except those at maximum precision" (:119-125).
NumberRangePrefixTreeStrategy.isGridAlignedShapeis true for any unit with level > 0 (NumberRangePrefixTreeStrategy.java:68-76), soRecursivePrefixTreeStrategy.makeQuerysendsIntersectson a unit tomakeGridShapeIntersectsQuery(RecursivePrefixTreeStrategy.java:185-188).- Under pointsOnly that method walks the query shape's cells to the last one, the unit's own cell, which the iterator marks as a leaf because the unit covers it (
NumberRangePrefixTree.java:858-864), and returnsTermQuery(cell.getTokenBytesWithLeaf(null)): the unit's term plus the leaf byte (RecursivePrefixTreeStrategy.java:218-229). - Points-only indexing at full precision never writes a leaf term above full precision (this tree always indexes at the maximum level,
NumberRangePrefixTree.java:479-487), so the query matches nothing. The term that is there, the same bytes without the leaf byte, is held by every point inside the unit. Only the public overloadcreateIndexableFields(Shape, int detailLevel)with a coarser level can write a coarse leaf, against thesetPointsOnlyjavadoc; such a document is the only kind the shortcut finds (executed). - A range that covers exactly one unit is normalized to that unit (
NumberRangePrefixTree.java:107-117) and fails too. Spans such as[2014 TO 2015]useIntersectsPrefixTreeQueryand are correct. In the 2D trees the query cell is always at the maximum level, so they are not affected.
One document at 2014-10-23T21:22:33.159: IsWithin = 1 and Intersects = 0 for 2014, 2014-10, [2014-10-01 TO 2014-10-31], 2014-10-23 and 2014-10-23T21:22:33; with pointsOnly=false all are 1. Five documents (an independent test): 2014 expected 3 got 0, 2014-10 expected 2 got 0, the day to second units expected 1 got 0. The queried term 2014-10 + leaf byte has docFreq 0; 2014-10 without it has docFreq 1.
Exposure and likelihood234 words
- Who is exposed. Applications that use
NumberRangePrefixTreeStrategydirectly and call the non-defaultsetPointsOnly(true)(the constructor sets false,NumberRangePrefixTreeStrategy.java:50). For them everyIntersectsquery on one coarse unit, or on a range that normalizes to one unit, returns 0 hits. That is the optimization's headline query, so a user of the setting meets it on the first month or day query (a judgement, not measured). - Not affected.
pointsOnly=false, full-precision points, spans,IsWithinandContains(other query classes), and the 2D strategies. - Silent, any scale. No exception or warning; one document reproduces it; assertions on or off.
- The index is fine. For the same full-precision dates the flag does not change the indexed terms (executed), so no reindex is needed after a fix. Workaround until then:
IsWithin, or a second strategy instance withpointsOnly=falseat query time; both work on the existing index (executed).IsWithinmeans the same only while the field really holds points and runsWithinPrefixTreeQuerywith the setting the code calls "slower but ensures correct results" (RecursivePrefixTreeStrategy.java:191-198); the second strategy runs the generalIntersectsPrefixTreeQuery. Neither was benchmarked. - Downstream (code reading only, not executed). In their default branches as read on 2026-10-26, Solr's
DateRangeFieldbuilds this strategy but never callssetPointsOnly, and Elasticsearch and OpenSearch do not use it; older downstream releases and third-party plugins were not checked. So none of these servers exposes the setting today.
The test shows that NumberRangePrefixTreeStrategy with setPointsOnly(true) returns no hits for an Intersects query on a date unit coarser than full precision (here the month 2014-10).
/*
* 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.prefix;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.spatial.prefix.tree.DateRangePrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestNumberRangePointsOnlyUnitIntersects extends LuceneTestCase {
public void testPointsOnlyIntersectsMonth() throws Exception {
DateRangePrefixTree tree = new DateRangePrefixTree(DateRangePrefixTree.DEFAULT_CAL);
NumberRangePrefixTreeStrategy strategy = new NumberRangePrefixTreeStrategy(tree, "date");
strategy.setPointsOnly(true);
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
for (Field f : strategy.createIndexableFields(tree.parseShape("2014-10-23T21:22:33.159"))) {
doc.add(f);
}
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects, tree.parseShape("2014-10"));
// expected: the one document (its date lies in October 2014) matches; actual: 0 hits
assertEquals(1, new IndexSearcher(reader).count(strategy.makeQuery(args)));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-612/, 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.prefix.TestNumberRangePointsOnlyUnitIntersects
# the module(s) spatial-extras also need their third-party runtime jars
- Expected vs actual
- One document indexed at 2014-10-23T21:22:33.159; Intersects('2014-10'): expected 1 hit, actual 0 (
expected:<1> but was:<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:<1> but was:<0>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.spatial.prefix.TestNumberRangePointsOnlyUnitIntersects.testPointsOnlyIntersectsMonth(TestNumberRangePointsOnlyUnitIntersects.java:49)- 7.0.0 CHANGES: the pointsOnly
TermQueryshortcut is an optimization for "a simple date interval (e.g. 1 month)". setPointsOnlyjavadoc: "There are no \"leaves\" in such a case, except those at maximum precision"; the shortcut looks for a coarser leaf.- Class javadoc: with only points indexed, Intersects "is effectively equivalent to IsWithin"; here IsWithin finds 1, Intersects 0.
The full argument234 words
- The CHANGES entry names the failing input.
CHANGES.txt:8140-8142(7.0.0): "LUCENE-7845: Enhance spatial-extras RecursivePrefixTreeStrategy queries when the query is a point (for 2D) or a is a simple date interval (e.g. 1 month). When the strategy is marked as pointsOnly, the results is a TermQuery." The issue (#8896): "if the query is a grid cell (a common date range unit like some particular day) ... If the data is pointsOnly the strategy can return a TermQuery". The code comment: "this will be equivalent to a TermQuery" (RecursivePrefixTreeStrategy.java:219). - The setter states the invariant the shortcut breaks.
PrefixTreeStrategy.java:119-125: "True if only indexed points shall be supported. There are no \"leaves\" in such a case, except those at maximum precision." - The class javadoc promises the equivalence that fails.
PrefixTreeStrategy.java:51-53: "Can query with any shape." and "If only points are indexed then this is effectively equivalent to IsWithin." - The combination is meant to work. The introducing commit
d4f87b4a36cmadesetPointsOnly(true)usable with this strategy and madeTestDateNRStrategyrandomize it (TestDateNRStrategy.java:41). That test's points-only documents are random instants over the wholelongrange (:94-102) while queries stay near 1970 (:43-45,:129-131), so a point almost never falls inside a coarse query unit and 0 hits is also the expected answer there. - Precedent. Two other wrong results from the same optimization were fixed as bugs: LUCENE-7859 (#8910) and
6b022c98f9a.
The devil's-advocate check229 words
The strongest case for "intended": a leaf term on a coarse cell could mean "a document indexed exactly as that unit", so the TermQuery would be a precise exact-unit match; the strategy is @lucene.experimental and pointsOnly is opt-in; the class javadoc still says "Only Intersects is supported"; and it calls the strategy "most effective as a fast approximate spatial search filter" (PrefixTreeStrategy.java:37-40).
Why it fails: under pointsOnly, indexing 2014-10 throws, and a point indexed the normal way never gets a coarse leaf term (the tree always indexes at full precision). The only documents the exact-unit reading could match are points indexed through the createIndexableFields(Shape, int detailLevel) overload at a coarser level, against the setPointsOnly javadoc; for every normally indexed point the query never matches, against the CHANGES entry that calls it an optimization for one month. The "approximate" wording is about the grid precision of shape boundaries; this strategy sets setDistErrPct(0) and its tree says "We don't do approximations in this tree/strategy", and losing every hit of a unit is not an approximation. @lucene.experimental is about API stability, not about returning nothing. "Only Intersects is supported" is older wording (the strategy also supports IsWithin and Contains), and the same bullet promises that points-only Intersects equals IsWithin, which is what fails. The read-only cross-model reviewers and the devil's advocate rated the intended case weak. Verdict: a defect.
Recommended Query the non-leaf term for a coarse cell (validated)
Under pointsOnly, look up the unit's prefix (non-leaf) term when its cell is coarser than the tree's maximum level, and the leaf term only at the maximum level. One statement in makeGridShapeIntersectsQuery; the query stays a single TermQuery. With it the repro passes and the whole spatial-extras test suite passes (119 tests, 0 failures).
assert cell.isLeaf();
// pointsOnly: only cells at the maximum level are indexed as leaves; a coarser grid-aligned
// shape (e.g. a date unit coarser than full precision) is indexed as a prefix (non-leaf) term
// of every point beneath it.
return new TermQuery(
new Term(
getFieldName(),
cell.getLevel() == grid.getMaxLevels()
? cell.getTokenBytesWithLeaf(null)
: cell.getTokenBytesNoLeaf(null)));All options and trade-offs3 options · 596 words
One fix was validated through gradle and the standalone runner; an alternative was validated through the standalone runner only; documenting the behaviour is not recommended.
In RecursivePrefixTreeStrategy.makeGridShapeIntersectsQuery, under pointsOnly, use cell.getTokenBytesNoLeaf when cell.getLevel() < grid.getMaxLevels() and keep getTokenBytesWithLeaf at the maximum level. 1 file, +9/-1. Exact for points indexed the normal way (at full precision): each such point stores the non-leaf term of each ancestor cell, so the unit's non-leaf term is held by exactly those points inside it.
assert cell.isLeaf();
// pointsOnly: only cells at the maximum level are indexed as leaves; a coarser grid-aligned
// shape (e.g. a date unit coarser than full precision) is indexed as a prefix (non-leaf) term
// of every point beneath it.
return new TermQuery(
new Term(
getFieldName(),
cell.getLevel() == grid.getMaxLevels()
? cell.getTokenBytesWithLeaf(null)
: cell.getTokenBytesNoLeaf(null)));Trade-offs
Keeps the single TermQuery LUCENE-7845 wanted and needs no prefix-tree traversal. Today's query looks up a term with no postings; the fixed one reads the unit's postings, one per point inside the unit, so its cost grows with the matches (no benchmark was run). The maximum-level case and the 2D strategies (always at the maximum level) are unchanged. It relies on the pointsOnly contract: on an index that broke it, results could be incomplete, as with any pointsOnly shortcut. Two concrete cases: an index written by a pointsOnly=false strategy with ranges, and points indexed through the createIndexableFields(Shape, int detailLevel) overload at a coarser level. The latter are found today, but fix A no longer finds them; option B finds both kinds (fact check's probe, one point indexed each way: stock 1, fix A 1, option B 2, IsWithin 2 of 2). It changes the base class RecursivePrefixTreeStrategy, which external code can subclass; in Lucene only NumberRangePrefixTreeStrategy widens isGridAlignedShape.
What the checks validated
In a scratch worktree through gradle: checkGoogleJavaFormat passes; the module's whole test suite (all 27 test classes, including the new test) runs 119 tests with 0 failures and 5 skips (BBox and Geo3d suites); the new test passes with assertions on and off. Through the standalone runner: the new test passes with assertions on and off. Earlier, the same change made the finder's, the independent reproducer's and the adjudication's tests, 100 upstream test runs and the security probe pass, and removed every pointsOnly mismatch of the exhaustive harnesses (with-fix runs with assertions on). An independent fact check re-ran the module's suite with the fix through gradle: 119 tests, 0 failures.
In NumberRangePrefixTreeStrategy.isGridAlignedShape, return isPointShape(shape) when isPointsOnly(), so a coarse unit takes IntersectsPrefixTreeQuery, as spans already do.
Trade-offs
Correct for both kinds of indexed points (normal and coarse-level overload) and confined to the number/date strategy, leaving the base class alone, but it drops the optimization for the "1 month" case LUCENE-7845 was written for: the general query walks the unit's cells and scans terms, which costs more than one term lookup, more so for a year. Not benchmarked.
What the checks validated
The finder's copy of the repro (the same test before formatting), the independent reproducer's test and the security screen's probe pass with it (assertions on); in the fact check's overload probe it counts 2 of 2. Not run through gradle or the module's suite.
Declare coarse units unsupported under pointsOnly.
Trade-offs
Contradicts the CHANGES entry, makes the shortcut dead code for coarse units and leaves silent 0-hit answers in place.
An application that writes an exclusion rule as MUST_NOT Intersects(<month>) on such a points-only field excludes nothing, so documents it should hide stay visible; in their default branches as read on 2026-10-26, Solr's DateRangeField, Elasticsearch and OpenSearch do not set pointsOnly on this strategy (older releases not checked).
A bounded-exhaustive check compared the real number and date range trees with independent interval and calendar references, over every unit, range, operation and setting at small scale (about 303 million checks). All its points-only Intersects mismatches were these false negatives (1,880 and 410), and they vanish with the fix (with-fix runs with assertions on).
The method in full150 words
Method: bounded-exhaustive translation validation of the real jar against independent references (the hunt's semi-formal tier).
- Six small
NumberRangePrefixTreetest subclasses, including variable fan-out, were compared with a 1-D interval reference over every unit, every[a TO b]range and every operation, prefix grid scan level and pointsOnly setting: 292,244,615 checks. In pointsOnly mode the end-to-end stage reported 1,880Intersectsfalse negatives, all on a unit query coarser than the maximum level. DateRangePrefixTreewas compared with ajava.timereference: 10,679,369 checks, 410 pointsOnlyIntersectsfalse negatives.- Both groups drop to 0 with the fix. The stock runs used assertions on and off; the with-fix runs used assertions on only, and the date harness's with-fix run also applied a second fix for a separately tracked mismatch class. The root cause was then read from the code and reduced to a one-document public-API JUnit. (The same harnesses also found other, separately tracked mismatches.)
No prior report found (searched 2026-10-26, re-checked 2026-10-27): three scripted prior-report search runs (each a 51-query plan extended with web searches: 62, 65 and 64 query records), manual GitHub, JIRA, mailing-list and downstream searches, and the changed files of all 407 open Lucene PRs. Nearest: LUCENE-7845 (#8896), the issue that introduced the shortcut, whose discussion covers only 2D failures.
Searches4 searches
No issue, PR, commit, JIRA ticket, list thread, downstream report or web page describes a points-only NumberRangePrefixTreeStrategy answering Intersects on a coarse unit with a leaf TermQuery that matches nothing.
Closest, not this bug: #8896 (LUCENE-7845, the introducing issue; its text states the intent this bug breaks), #8910 (LUCENE-7859) and commit 6b022c98f9a (other wrong results from the same optimization, fixed in 7.0), #7252, #7243 and #7401 (earlier pointsOnly items from 2015, before the TermQuery path), and #12295 (open PR; an instanceof refactor of these files that does not touch this code).
Gaps: 11 planned web queries (2, 5 and 4 in the three runs) were not run (rate limits); 15 web queries ran in each run, including the most specific ones; one Stack Overflow page was read only through an archived copy, one only by title.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits, migrated JIRA, dev@/java-user@/issues@/commits@ lists, downstream repositories, web | 2026-10-26 | three scripted prior-report search runs (identifiers; user-level symptoms; the optimization's history), each a 51-query plan extended with web searches (62, 65 and 64 query records; 15 web queries ran in each) | only the related items above |
| GitHub (apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch), Lucene and Solr JIRA, Lucene and Solr lists | 2026-10-26 | manual GraphQL, JIRA and list searches by class, method and symptom | no report |
| Open PRs | 2026-10-26 | the changed files of all 407 open apache/lucene PRs | 9 touch spatial-extras; only #12295 touches these files (refactor only) |
| GitHub, re-check for this doc | 2026-10-27 | 5 read-only searches (pointsOnly, NumberRangePrefixTreeStrategy, DateRangePrefixTree, makeGridShapeIntersectsQuery, isGridAlignedShape); spatial-extras issues and PRs created since 2026-10-26; upstream history of the two files | only previously triaged items (15 numbers); none describes this bug; both files unchanged on upstream main |
Introduced in 7.0.0 by LUCENE-7845 (d4f87b4a36c, 2017-05-26), which in one commit made pointsOnly usable with the date/number strategy and added the TermQuery shortcut. A version-portable main() fails on 8 releases from 7.0.0 to 10.5.2 and on main; on 6.6.x the trigger cannot be built.
Versions tested6 rows
Introduced by d4f87b4a36c "LUCENE-7845: RPT query by point (or simple date interval) optimization" (@dsmiley, 2017-05-26; LUCENE-7845, now #8896; a JIRA patch, no GitHub PR). It made setPointsOnly(true) usable with this strategy (it added the isPointShape override and changed the existing pointsOnly indexing guard from !(shape instanceof Point) to !isPointShape(shape)), declared every unit with level > 0 grid aligned in isGridAlignedShape, and added the leaf TermQuery. The assert cell.isLeaf() came with the follow-up 6b022c98f9a (2017-06-05, also in 7.0.0). Never backported to 6.x. Later commits to these files are refactors, formatting, pruning and date-parsing changes; none changes the token the shortcut queries. Not fixed on upstream main (both files unchanged as of 2026-10-27), branch_10x or branch_10_6. No commit-level bisect: the boundary is executed at release level.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test fails 1 of 2 (gradle and the standalone runner, assertions on and off); the main() port fails (-ea and -da) |
| 10.5.2, 9.12.3 | affected | the test and the main() port fail (-ea and -da), same counts |
| 10.0.0, 9.0.0, 8.11.4, 8.0.0, 7.7.3 | affected | main() port: Intersects = 0 for all five units, IsWithin = 1, point and span controls = 1 |
| 7.0.0 | first release | main() port fails with -ea and -da, same counts |
| 6.6.0, 6.6.6 | trigger unavailable | points-only indexing of any date throws IllegalArgumentException; controls correct |
| main + fix | not affected | this doc's test passes 2 of 2; the module's whole test suite passes (119 tests, 0 failures) |
An independent reproducer, an intended-behaviour devil's advocate and an adjudication each re-ran it and call it real (adjudication: confirmed, high). Read-only reviews by another model family agree on the root cause; one says "not real" only because it could not run code. A registry re-run and an audit reproduced it with new seeds.
Checks and verdicts525 words
The reproductions ran on the stock jars with assertions on and off (the cross-model review's variant, option B and the with-fix harness runs with assertions on only). The cross-model reviews were read-only, so no review by another model family executed code; the executed evidence comes from the finder, the cross-model review's own variant, the independent reproduction, the intended-behaviour check, the adjudication, the audit and this doc's runs on a9779ef3c193.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive harnesses (1,880 and 410 pointsOnly Intersects false negatives; 0 with the fix, assertions on), then a public-API JUnit that fails on main (-ea, -da), 10.5.2 and 9.12.3 and passes with the fix; 100 upstream test runs pass with it. |
| cross-model review, refute lens | not real (low, procedural only) | OpenAI GPT-5.6, read-only, could not run Java: agrees with the root cause, finds the inputs legal and the intended case weak, the fix "appears narrowly correct by inspection"; added the class-javadoc contract. A separate run re-ran the JUnit: fails as claimed. |
| Cross-model review, intended lens | real (medium); intended argument weak | Read-only review; the cross-model review's executed variant (3 documents, 6 queries) shows the TermQuery returning 0 where a directly built IntersectsPrefixTreeQuery returns the right counts; passes with the fix. |
| Independent reproducer | real | Own JUnit from the claim only, own package, oracle from ISO string prefixes: fails on main (-ea, -da), 10.5.2 (-ea, -da) and 9.12.3; 20 of 20 iterations; mechanism test (docFreq 0 with the leaf byte, 1 without); passes with the fix. |
| Intended-behaviour devil's advocate | real (high that it is not intended) | Probe: indexing a coarse unit under pointsOnly throws, so normal points-only indexing never writes the queried term; a pointsOnly=false strategy finds the document on the same index; re-ran the finder's JUnit on main (-ea, -da) and 10.5.2. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test with new seeds: fails with -ea and -da, passes with the fix. |
| registry re-run and audit | keep | The finder's JUnit re-run with seeds DEADBEEF and C0FFEE12 fails (-ea, -da) and passes with the fix; root cause re-read in the code. |
| this doc | reproduced; fix validated | The test fails 1 of 2 on main (gradle and standalone, -ea and -da) and on 10.5.2; passes with the fix; the module's whole test suite passes with the fix (119 tests, 0 failures) and checkGoogleJavaFormat is clean. |
| independent fact check | 3 wording errors, now fixed; bug, numbers and fix hold | Re-ran the gradle command on a clean extraction of a9779ef3c193 (fails 1 of 2, assertions on and off), the standalone runner on main, 10.5.2 and 9.12.3 with new seeds, and fix A through gradle (119 tests, 0 failures). Its overload probe found the one way around the points-only contract; the doc now says so. |
| cross-model fact check (read-only) | core report sound; intended case weak | OpenAI GPT-5.6, read-only, reading only: 7 factual errors (0 high, 2 medium, 5 low), 5 overclaims and 4 missing caveats; each is fixed in this doc or answered. |
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 532 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,473 words
- Registry title
- NumberRangePrefixTreeStrategy with setPointsOnly(true): an Intersects query for a date or number unit coarser than full precision (a year, month, day, ...) matches no documents
- Area
- spatial-extras / prefix-tree strategies (NumberRangePrefixTreeStrategy over DateRangePrefixTree or a custom NumberRangePrefixTree; the pointsOnly TermQuery shortcut in RecursivePrefixTreeStrategy.makeGridShapeIntersectsQuery)
- Priority
- Medium, rank 200: Since 7.0, every ordinary year, month or day Intersects query with opt-in setPointsOnly(true) on NumberRangePrefixTreeStrategy has silently returned nothing, which defeats the optimization's main use case, but few users opt in.
- Impact class
- silent wrong results
- Impact note
- Applications that use NumberRangePrefixTreeStrategy directly (DateRangePrefixTree or a custom NumberRangePrefixTree) with the non-default setPointsOnly(true) and run SpatialOperation.Intersects on a single unit coarser than full precision (a year, month, day, hour, minute or second, or a range such as [2014-10-01 TO 2014-10-31] that the tree normalizes to one unit) get 0 hits and no error for every such query; spans such as [2014 TO 2015], full-precision points, IsWithin, Contains and the default pointsOnly=false are correct; default codec, writer and searcher otherwise, one document suffices, and the index is unaffected; no known server exposes the setting (in their default branches, read 2026-10-26: Solr's DateRangeField never sets it, and Elasticsearch and OpenSearch do not use this strategy, by reading).
- Summary
- You index full-precision dates such as 2014-10-23T21:22:33.159 with a NumberRangePrefixTreeStrategy over DateRangePrefixTree on which you called setPointsOnly(true), and search makeQuery(new SpatialArgs(SpatialOperation.Intersects, tree.parseShape("2014-10"))). You expect the documents from October 2014, because each point lies inside that month, IsWithin with the same shape and the same query with pointsOnly=false find them, and the LUCENE-7845 CHANGES entry describes this pointsOnly TermQuery shortcut as an optimization for 'a simple date interval (e.g. 1 month)'. Instead RecursivePrefixTreeStrategy.makeGridShapeIntersectsQuery builds a TermQuery on the month's token plus the leaf byte, a term that pointsOnly indexing at full precision, the normal path, never writes (each point stores every coarser level only as a non-leaf prefix term), so the query matches nothing; the same happens for a year, day, hour, minute or second and for a range that normalizes to one unit.
- Introduced
- d4f87b4a36ca50c4361d7ec4e0858b18d9eaebe8 'LUCENE-7845: RPT query by point (or simple date interval) optimization' (by GitHub dsmiley; a JIRA patch, no GitHub PR; migrated issue apache/lucene#8896). In one commit it made setPointsOnly(true) usable with NumberRangePrefixTreeStrategy (isPointShape override accepting a unit at the maximum level), declared every UnitNRShape with level > 0 grid-aligned (isGridAlignedShape) and routed grid-aligned Intersects queries under pointsOnly to TermQuery(cell.getTokenBytesWithLeaf); the 'assert cell.isLeaf()' line came with the follow-up 6b022c98f9a (2017-06-05, also in 7.0.0) [read: git blame and git log in the history clone; exec at release level only: 6.6.6 cannot build the trigger, 7.0.0 fails; no commit-level bisect] (2017-05-26)
- First affected version
- 7.0.0 [exec, -ea and -da: Intersects = 0 for 2014, 2014-10, [2014-10-01 TO 2014-10-31], 2014-10-23 and 2014-10-23T21:22:33 while IsWithin = 1 and the point and span controls are correct]; 6.6.0 and 6.6.6 [exec] cannot build the trigger, because pointsOnly indexing of any date shape throws IllegalArgumentException there, and LUCENE-7845 was never backported to 6.x [read]
- Affected versions
- every release from 7.0.0 through 10.6.0 (10.6.0 by reading) and main a9779ef3c193 [exec: official release jars, JDK 25.0.3, the follow-up's main() repro NrPointsOnlyUnitIntersectsHistory fails with the same counts on 7.0.0 (-ea, -da), 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3 (-ea, -da), 10.0.0, 10.5.2 (-ea, -da) and main (-ea, -da); the finder's JUnit fails on main (-ea, -da), 10.5.2 and 9.12.3; the independent reproduction's JUnit fails on main (-ea, -da), 10.5.2 (-ea, -da) and 9.12.3]; the releases in between by code history [read: no later commit changes the token the shortcut queries]. The brief's test also fails on 10.5.2 (-ea, -da) and 9.12.3 (-da) [exec: brief and fact-check runs]. Not fixed upstream: upstream main head 20ced71c1e6e (2026-10-27; RecursivePrefixTreeStrategy.java last changed 2025-06-22, NumberRangePrefixTreeStrategy.java 2021-01-05) [read, gh api, 2026-10-27]; branch_10x and branch_10_6 have the same code [read, 2026-10-26].
- 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: exhaustive check
- Upstream and prior reports
- none found (2026-10-26, the prior-report record verdict new, read-only: three scripted prior-report searches (each a 51-query plan extended with web searches: 62, 65 and 64 query records) over apache/lucene issues and PRs, commit messages, the migrated JIRA, the dev@, java-user@, issues@ and commits@ lists, downstream repositories and the web, plus manual GitHub GraphQL, Lucene and Solr JIRA, mailing-list and downstream code searches and a scan of the changed files of all 407 open apache/lucene PRs; the independent reproduction's and the intended-behaviour check's searches agree): no issue, PR, commit, JIRA entry, list thread or downstream report describes a pointsOnly NumberRangePrefixTreeStrategy answering Intersects on a coarser unit with a leaf TermQuery that matches nothing. The only item that names the code path is apache/lucene#8896 (LUCENE-7845), the issue that introduced it; its discussion covers 2D failures (#8910 / LUCENE-7859 PackedQuadCell token bytes, and 6b022c98f9a for degenerate rectangles and circles) and states the intent this bug breaks (an equivalent TermQuery for 'a common date range unit like some particular day'). Not fixed: no open PR touches makeGridShapeIntersectsQuery or isGridAlignedShape (#12295, the only open PR on these files, is an instanceof refactor). Gaps: 11 planned web queries (2, 5 and 4 in the three runs) were not executed because of rate limits (15 web queries ran in each run, including the most specific ones), one Stack Overflow page was read only through an archived copy and one only by title. Re-checked 2026-10-27 (read-only gh): only previously triaged items, nothing new; upstream main head 20ced71c1e6e, both files unchanged.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 release jars (assertions off). An independent fact check (2026-10-27) re-ran the test through gradle on a clean extraction ofa9779ef3c193(assertions on and off) and through the standalone runner on main, 10.5.2 (assertions on) and 9.12.3 (assertions off) with new seeds. Earlier checks: the finder's and the independent reproduction's tests on main, 10.5.2 and 9.12.3; a version-portablemain()on 6.6.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 and main (follow-up). The fix was validated in the scratch worktree: the repro with assertions on and off, the wholelucene/spatial-extrastest suite andcheckGoogleJavaFormat.
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:179-203 (at a9779ef3c193) (makeQuery: Intersects on a grid-aligned shape goes to makeGridShapeIntersectsQuery at :185-188), :216-229 (pointsOnly: last cell of getTreeCellIterator, 'assert cell.isLeaf()', TermQuery(cell.getTokenBytesWithLeaf); comment 'this will be equivalent to a TermQuery' at :219)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/NumberRangePrefixTreeStrategy.java:46-52 (at a9779ef3c193) (constructor: setPointsOnly(false)), :59-66 (isPointShape: UnitNRShape at maxLevels), :68-76 (isGridAlignedShape: any UnitNRShape with level > 0)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/PrefixTreeStrategy.java:51-53 (at a9779ef3c193) (class javadoc: 'Can query with any shape'; 'If only points are indexed then this is effectively equivalent to IsWithin'), :119-125 (setPointsOnly javadoc: 'There are no "leaves" in such a case, except those at maximum precision'), :176-182 (createCellIteratorToIndex rejects non-point shapes under pointsOnly)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:105-115 (at a9779ef3c193) (toRangeShape: ranges are normalized, sometimes to a UnitNRShape), :849-873 (NRCell.hasNext marks the query's own cell as a leaf)lucene/CHANGES.txt:8140-8142 (at a9779ef3c193) (7.0.0, LUCENE-7845: 'when the query is a point (for 2D) or a is a simple date interval (e.g. 1 month). When the strategy is marked as pointsOnly, the results is a TermQuery.')
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/NumberRangePrefixTreeStrategy.java:59-66 (isPointShape), :68-76 (isGridAlignedShape)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:185-188, :211-213, :216-237 (makeGridShapeIntersectsQuery; :219 comment, :228 assert, :229 TermQuery)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/PrefixTreeStrategy.java:119-125 (setPointsOnly javadoc), :176-182 (pointsOnly indexing guard)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/CellToBytesRefIterator.java:41-46 (indexing emits getTokenBytesWithLeaf per cell)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:107-117 (toRangeShape normalization), :680-698 (token bytes, leaf byte 0), :849-873 (NRCell.hasNext sets the leaf flag)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:479-487 (getLevelForDistance always returns maxLevels: 'thus always use full precision')lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/PrefixTreeStrategy.java:127-142 (createIndexableFields(Shape) -> getLevelForDistance; the public createIndexableFields(Shape, int detailLevel) overload), tree/TreeCellIterator.java:67-69 (the bottom cell is marked a leaf)
- Who is affected
lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/NumberRangePrefixTreeStrategy.java:46-52 (constructor: setPointsOnly(false))lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:189-202 (spans, IsWithin and Contains use other query classes)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:191-198 (IsWithin: WithinPrefixTreeQuery with the -1 flag)
- Why it looks unintended
lucene/CHANGES.txt:8140-8142 (7.0.0, LUCENE-7845)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/PrefixTreeStrategy.java:51-53, :119-125lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:191-202, :219lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/NumberRangePrefixTreeStrategy.java:42 (@lucene.experimental)lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/TestDateNRStrategy.java:41, :43-45, :94-102, :109-114, :129-131lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/PrefixTreeStrategy.java:37-40, :50-51 (class javadoc: approximate filter; boundaries approximated to a grid precision), :142 (the detailLevel overload)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/NumberRangePrefixTreeStrategy.java:51 (setDistErrPct(0)); tree/NumberRangePrefixTree.java:484 ("We don't do approximations in this tree/strategy.")
- Suggested fix
lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:189-190, :211-213, :216-237
- History
lucene/CHANGES.txt:8140-8142 (7.0.0)
- TestNumberRangePointsOnlyUnitIntersects.java (JUnit test):
repro/LFV-612/src/org/apache/lucene/spatial/prefix/TestNumberRangePointsOnlyUnitIntersects.java(53 lines)
A picture of the repro's example, made in parallel with this doc from the executed repro on main a9779ef3c193 and the code; the PNG is the summary card. The alt text describes the intended content.
assets/LFV-612/bug.png(532 KB)
