All findings · Lucene finding · LFV-612 · numberrange-pointsonly-unit-intersects-leaf-termquery

Points-only date range strategy: Intersects on a year, month or day matches nothing

With setPointsOnly(true), NumberRangePrefixTreeStrategy answers Intersects on a year, month, day, hour, minute or second by looking up a term that points-only indexing at full precision never writes: one document at 2014-10-23T21:22:33.159 and the query 2014-10 give 0 hits, while IsWithin gives 1. Every release since 7.0.0 is affected.

Summary

Priority
Medium, rank 200Since 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
silent wrong results
Since
7.0.0 (LUCENE-7845, 2017)introduced 2017-05-26 · about 9 years
Affected
7.0.0 to 10.5.2, 10.6.0 (by reading) and main
Status
documented
Found by
bounded-exhaustive check of the range trees against independent references

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Diagram of the repro: the document 2014-10-23T21:22:33.159 indexed with pointsOnly=true is stored as eight prefix terms, one per coarser level (the million-year block, the thousand-year block, year 2014, month 2014-10, day 2014-10-23, hour, minute and second), and one leaf term at full millisecond precision. The query Intersects(2014-10) is turned into a TermQuery for 2014-10 plus the leaf marker, a term that is not in the index, so it returns 0 hits; IsWithin(2014-10) returns 1 hit, and the fix looks up the prefix term 2014-10, which every timestamp in October 2014 indexed at full precision has.
The terms one timestamp leaves in a points-only index, prefix terms for every level and a leaf term only at full precision, next to the term the month query looks up. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Index one document at 2014-10-23T21:22:33.159 with a NumberRangePrefixTreeStrategy over DateRangePrefixTree, after calling setPointsOnly(true) because you only index exact timestamps.
  2. Search Intersects with the month 2014-10. You expect 1 hit: the timestamp is in October 2014, and IsWithin with the same shape, or the same query with the default pointsOnly=false, returns 1.
  3. You get 0 hits and no error. The same for 2014, 2014-10-23, 2014-10-23T21:22:33 and the range [2014-10-01 TO 2014-10-31], which the tree turns into the month.
  4. 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.

Who is affected

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.

Terms

  • Number/date range prefix tree: DateRangePrefixTree splits 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 as 2014-10 is 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).

The bug

  1. NumberRangePrefixTreeStrategy.isGridAlignedShape is true for any unit with level > 0 (NumberRangePrefixTreeStrategy.java:68-76), so RecursivePrefixTreeStrategy.makeQuery sends Intersects on a unit to makeGridShapeIntersectsQuery (RecursivePrefixTreeStrategy.java:185-188).
  2. 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 returns TermQuery(cell.getTokenBytesWithLeaf(null)): the unit's term plus the leaf byte (RecursivePrefixTreeStrategy.java:218-229).
  3. 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 overload createIndexableFields(Shape, int detailLevel) with a coarser level can write a coarse leaf, against the setPointsOnly javadoc; such a document is the only kind the shortcut finds (executed).
  4. 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] use IntersectsPrefixTreeQuery and are correct. In the 2D trees the query cell is always at the maximum level, so they are not affected.

The numbers

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 NumberRangePrefixTreeStrategy directly and call the non-default setPointsOnly(true) (the constructor sets false, NumberRangePrefixTreeStrategy.java:50). For them every Intersects query 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, IsWithin and Contains (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 with pointsOnly=false at query time; both work on the existing index (executed). IsWithin means the same only while the field really holds points and runs WithinPrefixTreeQuery with the setting the code calls "slower but ensures correct results" (RecursivePrefixTreeStrategy.java:191-198); the second strategy runs the general IntersectsPrefixTreeQuery. Neither was benchmarked.
  • Downstream (code reading only, not executed). In their default branches as read on 2026-10-26, Solr's DateRangeField builds this strategy but never calls setPointsOnly, 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.

Minimal repro

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).

TestNumberRangePointsOnlyUnitIntersects.java (JUnit test)53 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.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)));
      }
    }
  }
}
Command
# 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 -ea and -da.

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

More files, environment, full output34 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# 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)

Why it looks unintended

  • 7.0.0 CHANGES: the pointsOnly TermQuery shortcut is an optimization for "a simple date interval (e.g. 1 month)".
  • setPointsOnly javadoc: "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 d4f87b4a36c made setPointsOnly(true) usable with this strategy and made TestDateNRStrategy randomize it (TestDateNRStrategy.java:41). That test's points-only documents are random instants over the whole long range (: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.

Suggested fix

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.

Query the non-leaf term for a coarse cell (validated) (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.

Route coarse units under pointsOnly to the general query (validated by the standalone runner only)

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.

Document that pointsOnly supports only full-precision Intersects (not recommended)

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.

Downstream note

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).

How it was found

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 NumberRangePrefixTree test 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,880 Intersects false negatives, all on a unit query coarser than the maximum level.
  • DateRangePrefixTree was compared with a java.time reference: 10,679,369 checks, 410 pointsOnly Intersects false 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.)

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits, migrated JIRA, dev@/java-user@/issues@/commits@ lists, downstream repositories, web2026-10-26three 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 lists2026-10-26manual GraphQL, JIRA and list searches by class, method and symptomno report
Open PRs2026-10-26the changed files of all 407 open apache/lucene PRs9 touch spatial-extras; only #12295 touches these files (refactor only)
GitHub, re-check for this doc2026-10-275 read-only searches (pointsOnly, NumberRangePrefixTreeStrategy, DateRangePrefixTree, makeGridShapeIntersectsQuery, isGridAlignedShape); spatial-extras issues and PRs created since 2026-10-26; upstream history of the two filesonly previously triaged items (15 numbers); none describes this bug; both files unchanged on upstream main

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis 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 affectedthe 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 affectedmain() port: Intersects = 0 for all five units, IsWithin = 1, point and span controls = 1
7.0.0 first releasemain() port fails with -ea and -da, same counts
6.6.0, 6.6.6 trigger unavailablepoints-only indexing of any date throws IllegalArgumentException; controls correct
main + fix not affectedthis doc's test passes 2 of 2; the module's whole test suite passes (119 tests, 0 failures)

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateExhaustive 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 weakRead-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 realOwn 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 keepThe 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 validatedThe 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 holdRe-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 weakOpenAI 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.

Diagram file

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

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,473 words

Finding record

LFV-612, snapshot of 2026-11-06

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 of a9779ef3c193 (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-portable main() 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 whole lucene/spatial-extras test suite and checkGoogleJavaFormat.

Lucene and upstream sources

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-125
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:191-202, :219
  • lucene/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-131
  • lucene/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)

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

Repro files

About the diagrams

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.

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