All findings · Lucene finding · LFV-475 · pointinset-multidim-lexicographic-bounds-early-exit

Multi-dimensional PointInSetQuery skips segments that contain matching points

Since 10.3.0, a multi-dimensional PointInSetQuery treats the first and last points of its sorted set as the set's minimum and maximum in every dimension, and skips segments that hold a match. Against one document at (1,0), the set {(0,1), (1,0)} counts 0; the set {(1,0)} counts 1.

Summary

Priority
Medium, rank 80Since 10.3 the PointInSetQuery early exit silently and deterministically skips whole segments for ordinary multi-point sets, but only multi-dimensional subclasses are hit and none exist in Lucene or major downstreams, the same custom-subclass profile as LFV-006.
Impact
silent wrong results
Since
10.3.0 (GITHUB#14268, 2025)introduced 2025-04-01 · about 1 year
Affected
10.3.0 to 10.6.0 (10.6.0 by reading) and main
Status
documented
Found by
bounded-exhaustive comparison with a brute-force reference, then a z3 model

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

A PointInSetQuery matches documents whose point equals one of a set of points. Since 10.3.0 it skips a segment early when it decides the set cannot match there, by comparing the set's "lowest" and "highest" point with the segment's minimum and maximum in each dimension.

  1. Write a 2-D PointInSetQuery subclass (Lucene's own set queries are all 1-D) and index one document at the point (1,0) with IntPoint, default settings.
  2. Search for the set {(1,0)}: count 1. Search for {(0,1), (1,0)}: you expect 1 again, because (1,0) is still in the set.
  3. You get 0, with no error. The constructor sorts the set point by point, dimension 0 first, so (0,1) comes first and (1,0) last, and the query keeps those two points as the set's "lowest" and "highest" point.
  4. It then checks each dimension against the segment. In dimension 1 the "lowest" value is 1, but the segment's largest value there is 0, so the query decides the segment cannot match and skips it. The real range of the set in dimension 1 is 0 to 1.
  5. So adding a point to a set can remove hits. Ordinary data can trigger it when segments are narrow in a later dimension: 43 of 200 random sets were wrong on day-ordered (entity, day) data, 0 of 200 on uniform data.

Who is affected

Only applications that write their own PointInSetQuery subclass with two or more dimensions; every Lucene factory is 1-D, and the recorded code search of Elasticsearch, OpenSearch and Solr found none. There no setting or scale is needed: segments that are narrow in a later dimension, such as small or time-ordered ones, can trigger it with ordinary sets.

Mechanism, terms and numbers631 words

PointInSetQuery finds documents whose single or multi-dimensional point is in a set, and is meant for subclasses. Its constructor requires the packed points in sorted order, which it enforces with BytesRef.compareTo: an unsigned lexicographic order over the whole packed value, in which dimension 0 decides first. Since 10.3.0 (GITHUB#14268, an optimization) the constructor keeps the first and the last point of that order as lowerPoint and upperPoint (PointInSetQuery.java:110-114, :129-134), and scorerSupplier returns null for a segment as soon as, in any dimension, lowerPoint is above the segment's maximum or upperPoint below its minimum (:187-201). Those two points bound only dimension 0. For the set {(0,1), (1,0)} lowerPoint is (0,1); a segment whose only document is at (1,0) has maximum 0 in dimension 1, and 1 > 0, so the whole segment is skipped and count() is 0 instead of 1. Only application-written subclasses with two or more dimensions are hit; every production in-tree factory is 1-D. Used as a MUST_NOT clause or in IndexWriter#deleteDocuments(Query), the skipped segment fails open. Affects 10.3.0 through 10.6.0 (10.6.0 by reading) and main.

Terms

  • Point field: a field indexed with IntPoint, LongPoint and so on; a point has one value per dimension, for example new IntPoint("p", 1, 0) is the 2-D point (1,0). Each segment records, per dimension, the smallest and largest value of the field (PointValues#getMinPackedValue/getMaxPackedValue: "minimum value for each dimension, packed", PointValues.java:473-481).
  • PointInSetQuery: "Abstract query class to find all documents whose single or multi-dimensional point values ... is contained in the specified set"; "This is for subclasses" (PointInSetQuery.java:43-48). The set arrives as a stream of packed points that "must be in sorted order" (:74).
  • Lexicographic order: points compared byte by byte over the whole packed value, so dimension 0 decides first and dimension 1 only breaks ties. (0,1) sorts before (1,0) because 0 < 1 in dimension 0.

The mechanism (main ea57360912ed)

  1. The constructor copies the first point into lowerPoint (PointInSetQuery.java:110-114), checks every further point with BytesRef.compareTo against the previous one (equal: skipped; smaller: "values are out of order", :116-121) and copies the last point into upperPoint (:129-134).
  2. scorerSupplier, after the dimension and byte-width checks (:168-185): if the segment has documents, then for every dimension i it returns null when lowerPoint[i] > max[i] or upperPoint[i] < min[i] (:187-201).
  3. In lexicographic order the first point has the set's smallest dimension-0 value and the last point the largest, so dimension 0 is checked correctly. In a later dimension the first point's value can be above the set's minimum and the last point's value below its maximum. The check then rejects a segment that holds a set point.
  4. A null ScorerSupplier means "nothing matches in this segment": count() and search() get no hits from it; BooleanWeight drops a null clause that is not required (BooleanWeight.java:299-306), so a MUST_NOT clause excludes nothing there; delete-by-query runs only when the weight returns a scorer (FrozenBufferedUpdates.java:400-402), so deleteDocuments(Query) deletes nothing there.
  5. The same per-dimension loop in PointRangeQuery (PointRangeQuery.java:263-275), which #14268 followed, is correct: a range's lowerPoint and upperPoint are real per-dimension bounds.

The repro's numbers

One segment, one document at (1,0); segment minimum = maximum = (1,0).

setsortedlowerPoint / upperPointthe check that failsexpectedactual
{(1,0)}(1,0)(1,0) / (1,0)none11
{(0,1), (1,0)}(0,1), (1,0)(0,1) / (1,0)dimension 1: lowerPoint 1 > segment max 010
{(1,0), (2,-1)}(1,0), (2,-1)(1,0) / (2,-1)dimension 1: upperPoint -1 < segment min 010

Shadow builds of main that remove the early exit, or keep it for dimension 0 only, make every test pass, which isolates the cause.

Exposure and likelihood368 words
  • Who is exposed. Applications that subclass PointInSetQuery through its protected constructor with numDims >= 2, the extension point the javadoc names ("This is for subclasses"), for example a lookup of a set of (entity, day), (x, y) cell or (category, label) pairs on a multi-dimensional IntPoint or LongPoint field.
  • Not exposed. Every production in-tree factory passes numDims = 1: IntPoint, LongPoint, FloatPoint, DoublePoint, BinaryPoint and InetAddressPoint.newSetQuery, JoinUtil, and the sandbox BigIntegerPoint and HalfFloatPoint. The only in-tree multi-dimensional factory is the private test helper TestPointQueries#newMultiDimIntSetQuery. A read-only code search of the default branches of Elasticsearch, OpenSearch and Solr found no multi-dimensional subclass (only that summary was kept, not the search output); plugins and private code were not checked.
  • Default or opt-in. Default IndexWriterConfig, codec and searcher; one document is enough. The opt-in is the application-written subclass of a @lucene.experimental class.
  • When it fires. On a segment, when the set's lexicographically first point has a value in some dimension k >= 1 above the segment's maximum in k, or its last point a value below the segment's minimum in k, while the segment holds a set point. That needs segments that are narrow in a later dimension: small segments (near-real-time readers, frequent flushes), ingestion or index sorting by a later dimension (time series), or merges after deletes.
  • How often (measured on small synthetic indexes, not in the field). 2,000 (entity, day) documents indexed in day order and committed every 200: 43 of 200 random sets of 1-8 indexed points return wrong counts. The same with uniform random (x, y): 0 of 200. Deliberately tiny segments: 109 of 300 random 2-D and 150 of 300 random 3-D set queries wrong with one seed (109-154 and 125-159 of 300 over the five seeds run). For a given index and set it is deterministic (20 of 20 iterations).
  • What you see. No error. Whole segments contribute no hits, and adding a point to a set can remove hits that the smaller set returned. Positive use only loses hits; negated use fails open (Downstream note). No hang, resource growth, crash or index damage was observed, and the early-return path suggests none.

Minimal repro

The test shows that a 2-D PointInSetQuery for the set {(0,1), (1,0)} skips a segment whose only point is (1,0), because the segment early exit uses the lexicographically first/last set points as per-dimension bounds.

TestPointInSetQueryMultiDim.java (JUnit test)46 lines
package org.apache.lucene.search;

import java.util.Iterator;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;

public class TestPointInSetQueryMultiDim extends LuceneTestCase {

  public void testSetContainingIndexedPoint() throws Exception {
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
      Document doc = new Document();
      doc.add(new IntPoint("p", 1, 0));
      w.addDocument(doc);
      try (DirectoryReader r = DirectoryReader.open(w)) {
        Iterator<BytesRef> sorted = List.of(IntPoint.pack(0, 1), IntPoint.pack(1, 0)).iterator();
        Query q =
            new PointInSetQuery(
                "p",
                2,
                Integer.BYTES,
                new PointInSetQuery.Stream() {
                  @Override
                  public BytesRef next() {
                    return sorted.hasNext() ? sorted.next() : null;
                  }
                }) {
              @Override
              protected String toString(byte[] value) {
                return new BytesRef(value).toString();
              }
            };
        // the set {(0,1), (1,0)} contains the only indexed point (1,0): expected 1 hit, actual 0
        assertEquals("hits for the set {(0,1), (1,0)}", 1, new IndexSearcher(r).count(q));
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-475/, 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: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.search.TestPointInSetQueryMultiDim
Expected vs actual
Expected count() == 1 (the indexed point is in the set); actual 0 (expected:<1> but was:<0>), with -ea and -da.

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

More files, environment, full output39 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: hits for the set {(0,1), (1,0)} 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.apache.lucene.search.TestPointInSetQueryMultiDim.testSetContainingIndexedPoint(TestPointInSetQueryMultiDim.java:42)

Why it looks unintended

  • The javadoc promises all documents whose "single or multi-dimensional" point is in the set; its sorted-order precondition is enforced and met.
  • #14268 is an Optimizations entry; its 2-D tests use one-point sets only.
  • No order of {(0,1), (1,0)} is sorted in both dimensions, yet the constructor accepts it; 10.2.2 counts 1.
The full argument279 words
  • Contract. Class javadoc: "Abstract query class to find all documents whose single or multi-dimensional point values, previously indexed with e.g. {@link IntPoint}, is contained in the specified set." and "This is for subclasses and works on the underlying binary encoding" (PointInSetQuery.java:43-48). Besides the field shape (bytesPerDim, numDims and each packed point's length, checked at :76-108), the only precondition is the one the constructor javadoc states, "The {@code packedPoints} iterator must be in sorted order" (:74); it is enforced (:116-121) and met by the repro.
  • An optimization. "GITHUB#14268: PointInSetQuery early exit on non-matching segments. (hanbj)" is listed under 10.3.0 "Optimizations" (lucene/CHANGES.txt:1597, :1605); an optimization must not change results.
  • Upstream tests. TestPointQueries asserts exact multi-dimensional set results (testBasicMultiDimPointInSetQuery, testBasicMultiValueMultiDimPointInSetQuery, testManyEqualValuesMultiDimPointInSetQuery, :1560, :1582, :1610). The first two use multi-point sets, such as {(-7,-7), (17,42)}, which are ordered alike in every dimension, where the first and last points happen to be per-dimension bounds; the third uses one-point sets only. #14268's own test testPointInSetQuerySkipsNonMatchingSegments (:2566) uses 2-D sets of one point only. With the fix every test in lucene/core passes, so no test pins the current behaviour.
  • History. #14268's description gives the premise: "the data in the packedPoints parameter is returned in order, so the maximum and minimum values can be determined". That holds for dimension 0 only. Its review covered tests, asserts, the docCount == 0 check and the CHANGES entry, not sets of several multi-dimensional points. It followed the PointRangeQuery early exit (#13033), where the bounds are real per-dimension bounds. 10.2.2 and older, without the early exit, return the right count.
The devil's-advocate check173 words

The strongest case for "works as designed or misuse" (built by the devil's advocate and checked by the adjudication): the class is @lucene.experimental; every in-tree factory is 1-D; the test helper's comment calls it a "test-only query factory" that could be "graduated" "if there is demand for real usage" (TestPointQueries.java:1491-1493); and "sorted order" could be read as sorted in every dimension.

Why it fails: @lucene.experimental covers API stability, not correctness, and nothing documents approximate results. The class javadoc promises multi-dimensional membership and invites subclasses, the scorer has its own numDims > 1 branch, and upstream tests assert exact multi-dimensional results; the comment calls the factory test-only, not multi-dimensional support. The constructor enforces whole-value lexicographic order and accepts {(0,1), (1,0)}, which no order sorts in both dimensions at once, so a per-dimension reading would make ordinary sets illegal, which the constructor does not do. A reviewer from another model family (OpenAI GPT-5.6, reading only) called the intended-behaviour case weak. What remains is a reach argument (few users), not a correctness one.

Suggested fix

Recommended A per-dimension bounding box of the set (+28/-2)

Track the set's real minimum and maximum in every dimension after the first while the constructor reads the sorted points; keep the first and last points for dimension 0 (+28/-2 in PointInSetQuery.java). The repro passes, and so do TestPointQueries, the whole lucene/core suite (8,866 tests) and :lucene:core:check.

All options and trade-offs5 options · 967 words

Option A was validated in a scratch worktree at ea57360912ed (removed afterwards). B and C were run only as shadow classes against the reproducers' tests; D was not built.

A per-dimension bounding box of the set (+28/-2) (validated by the checks) (recommended)

While the constructor streams the sorted points, update lowerPoint and upperPoint for every dimension >= 1 with the unsigned minimum and maximum over all set points (Arrays.compareUnsigned, already imported); dimension 0 keeps the first and the last sorted point. scorerSupplier is unchanged. The patch is the finder's, with relative paths; fix-with-test.diff adds the TestPointQueries method. Add a CHANGES entry under Bug Fixes.

diff --git a/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java b/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
index 6efe5a9f5d0..90b1b979044 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
@@ -110,8 +110,10 @@ public abstract class PointInSetQuery extends Query implements Accountable {
       if (previous == null) {
         previous = new BytesRefBuilder();
         lowerPoint = new byte[bytesPerDim * numDims];
+        upperPoint = new byte[bytesPerDim * numDims];
         assert lowerPoint.length == current.length;
         System.arraycopy(current.bytes, current.offset, lowerPoint, 0, current.length);
+        System.arraycopy(current.bytes, current.offset, upperPoint, 0, current.length);
       } else {
         int cmp = previous.get().compareTo(current);
         if (cmp == 0) {
@@ -123,14 +125,38 @@ public abstract class PointInSetQuery extends Query implements Accountable {
       }
       builder.add(field, current);
       previous.copyBytes(current);
+      // track the per-dimension bounding box: the sorted order only bounds the first dimension
+      for (int dim = 1; dim < numDims; dim++) {
+        int offset = dim * bytesPerDim;
+        if (Arrays.compareUnsigned(
+                current.bytes,
+                current.offset + offset,
+                current.offset + offset + bytesPerDim,
+                lowerPoint,
+                offset,
+                offset + bytesPerDim)
+            < 0) {
+          System.arraycopy(current.bytes, current.offset + offset, lowerPoint, offset, bytesPerDim);
+        }
+        if (Arrays.compareUnsigned(
+                current.bytes,
+                current.offset + offset,
+                current.offset + offset + bytesPerDim,
+                upperPoint,
+                offset,
+                offset + bytesPerDim)
+            > 0) {
+          System.arraycopy(current.bytes, current.offset + offset, upperPoint, offset, bytesPerDim);
+        }
+      }
     }
     sortedPackedPoints = builder.finish();
     sortedPackedPointsHashCode = sortedPackedPoints.hashCode();
     if (previous != null) {
+      // the first dimension's maximum is the last point in sorted order
       BytesRef max = previous.get();
-      upperPoint = new byte[bytesPerDim * numDims];
       assert upperPoint.length == max.length;
-      System.arraycopy(max.bytes, max.offset, upperPoint, 0, max.length);
+      System.arraycopy(max.bytes, max.offset, upperPoint, 0, bytesPerDim);
     }
     ramBytesUsed =
         BASE_RAM_BYTES

Trade-offs

Sound for every set the constructor accepts (sorted, with any duplicates and any BytesRef offset): a segment is skipped only when the set's bounding box misses the segment's box, and a set point inside the segment lies in both. Keeps the early exit in every dimension. Cost, once at construction: per distinct set point 2 x (numDims - 1) unsigned comparisons of bytesPerDim-byte values and at most as many bytesPerDim-byte copies (the loop does not run for 1-D queries), plus one copy of the first point into upperPoint, also for 1-D; nothing per segment. Memory unchanged (the same two arrays). The two arrays become box corners instead of set points; only the constructor, ramBytesUsed, scorerSupplier and a ramBytesUsed test read them, and that test passes. No benchmark was run.

What the checks validated

  • Through gradle at ea57360912ed (main only; the patch was not built against the release branches): the repro fails on stock with assertions on and off and passes with the fix with both.
  • With the fix: TestPointQueries 56 tests, 0 failures (2 skipped), assertions on and off; 57 with the new method, which fails on stock; 46 point-related test classes, 344 tests, 0 failures; the whole lucene/core suite, 748 classes, 8,866 tests with assertions on and 8,867 (with the new method) with assertions off, 0 failures (the class counts come from the JUnit XML of the fact check's re-run of the same patch; the gradle logs print totals only); :lucene:core:check -x test (google-java-format, ecj, forbidden APIs) successful.
  • Earlier steps: the finder's, the independent reproducer's (6 of 6), the refute reviewer's witness and the security probe's (5 of 5) tests pass with the same patch; the bounded-exhaustive harness finds 0 mismatches in 1,282,176 cases (11,160 on stock).

Check only dimension 0 (1 line) (validated by the checks)

Limit the loop in scorerSupplier to dimension 0 (i < numDims becomes i < 1).

Trade-offs

Tiny and sound by the same argument as option A's dimension 0 (the first and last sorted points do bound dimension 0), but it drops the pruning on later dimensions that option A keeps, and only the independent reproducer's 6 tests were run with it.

What the checks validated

As a shadow class on main: the independent reproducer's 6 tests pass. The module suite was not run.

Remove the early exit (validated by the checks)

Revert the bounds check of #14268.

Trade-offs

Also removes the optimization for 1-D queries, where it is correct. Not recommended.

What the checks validated

As a shadow class on main: the independent reproducer's 6 tests and the finder's test pass.

Check each set point against the segment's box (not built)

Skip a segment only if no set point lies inside the segment's per-dimension box.

Trade-offs

Prunes more than a bounding box, but costs up to one box test per set point per segment. Not built or run.

Workaround until a fix ships

A subclass cannot override createWeight, which builds the check and is final, so the check runs whenever the query itself is executed; a subclass could override rewrite(IndexSearcher), which is not final and which PointInSetQuery does not override, to return the query below instead (not run). For small sets, use a BooleanQuery of SHOULD clauses IntPoint.newRangeQuery(field, p, p), one exact box per point, within the clause limit; or ship a patched copy of the class.

Trade-offs

Slower than a set query for large sets and bounded by the clause limit. Not run as a workaround; the independent reproducer used exactly this OR of exact boxes only as a cross-check of its brute-force counts on main, where it agreed with them.

Downstream note

Why: a skipped segment returns no scorer, which Lucene reads as "nothing matches here". BooleanQuery then drops the MUST_NOT clause for that segment and delete-by-query skips it, so negated use fails open. Positive use only loses hits. This class documents no untrusted-input guarantee (SECURITY.md lines 28, 50); no concrete exploit is known.

How it was found

An AI finder checked PointInSetQuery against a brute-force reference on every tiny 2-D and 3-D index with 0/1 coordinates (11,088 indexes, 1,282,176 queries): 11,160 mismatches, all multi-dimensional set queries, none with the fix. Code reading found the early exit; in a z3 model of four small cases the shipped check can skip a matching segment, a per-dimension box cannot.

The method in full236 words

Method: bounded-exhaustive translation validation, a hunt unit (bulk-overload precondition matrix), 2026-10-26 (the hunt's "semi-formal" tier).

  • The harness built every 2-D index of 1-4 documents over {0,1}^2 and every 3-D index of 1-3 documents over {0,1}^3, with maxPointsInLeafNode 1-3 and 4 layouts (11,088 indexes, 1,282,176 query cases), on the stock main jars, and compared each result with a brute-force reference over the stored values. The unit's own bulk-versus-per-document oracle found nothing; the brute-force reference flagged 11,160 mismatches, all multi-dimensional PointInSetQuery set queries. With the fix: 0.
  • A z3 model of the check: for (dimensions, set points) = (2,2), (2,3), (3,2) and (3,4), with one-byte dimensions, the shipped check can skip a segment whose box holds a set point (satisfiable, witnesses logged), a per-dimension min/max check cannot (unsatisfiable); a vacuity control is satisfiable and a 1-D control (3 set points) unsatisfiable. Other sizes were not run.
  • Then a public-API JUnit, a review by an independent reviewer from another model family (OpenAI GPT-5.6, reading only: real, high), an independent reproducer, a refute-lens review by the same model family (reading only; its "not real, low" was procedural because it could not run code), a devil's advocate, an adjudication, a history sweep, a prior-report search, a security assessment, a registry re-run and an audit. Neither reviewer from the other model family wrote or ran a test of its own; the refute review's wrapper did run the finder's test, which failed.

Prior reports, history and checks

Prior reports

No prior report: two scripted prior-report search runs, manual GitHub, JIRA, mailing-list, downstream and web searches, and the changed files of all 409 open PRs (2026-10-26). Closest: a deleted #14268 review comment that questioned the sorted-input premise without naming dimensions or wrong results.

Searches5 searches

Limits: GitHub search does not index review comments or diffs (the #14268 review was read directly, on GitHub and in the issues@ archive), and private security@ reports are invisible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-26two runs of a scripted prior-report search (the first ran 45 queries and left its 6 web queries pending, the second ran 53; both also cover downstream repositories, lists, JIRA and the web) and 19 manual GitHub queries (17 in apache/lucene, 2 GitHub-wide), for example PointInSetQuery early exit, multi-dimensional, numDims, lowerPoint upperPoint, getMinPackedValue, lexicographic, skip segment, wrong resultsno report; #14268 is the introducing PR (2-D tests with one-point sets only); #16405 / #16406 changed only the RAM accounting of the same two arrays; #13033, #14161, #10624 are other defects
Deleted review comment on #14268 (issues@ archive)2026-10-26the full issues@ mirror of the #14268 review (11 mails)a comment by iverase (2025-03-13), deleted on GitHub, no reply: it asked whether the sorted-input premise is a contract or an implementation detail; no dimensions, no wrong result
Open PRs2026-10-26changed files of all 409 open apache/lucene PRs#709, #13988 and #14352 touch PointInSetQuery.java; none changes the bounds code
Mailing lists and JIRA2026-10-26dev@, java-user@, issues@, commits@ through the tool and manual archive queries; JQL queriesonly the #14268 review traffic; JIRA items from 2016 to 2021, which predate the code
Downstream and web2026-10-26Elasticsearch, OpenSearch and Solr (4 manual queries plus the tool's); 8 web queriesOpenSearch #18422 is a different 1-D defect; the web returned generic pages only

History

Introduced in 10.3.0 by #14268 (merged 2025-04-01), an optimization that added the segment early exit. The repro fails on every release from 10.3.0 to 10.5.2 and on main and passes on 10.2.2 and older; swapping only PointInSetQuery.java flips the result both ways.

Versions tested7 rows

Introduced by GITHUB#14268, "PointInSetQuery early exit on non-matching segments" (@hanbj, merged by @gsmiller on 2025-04-01): main commit dba6c2c2fd98 (+27 lines in PointInSetQuery.java, +41 in TestPointQueries.java) and the branch_10x backport 0b1e2225f69a (parent 7f4b775d7e4a), which is in the release tags 10.3.0 to 10.6.0. Found with git blame, git log -S getMinPackedValue and git tag --contains. Later commits on the file (#14965, #16406, #16519) do not touch the check. The multi-dimensional protected constructor exists since 6.0.0; only the early exit is new.

Pinned by swapping one file: the backport's PointInSetQuery.java on the 10.2.2 jars fails, and its parent's version on the 10.3.0 jars passes. 10.6.0, released on 2026-10-29 from branch_10_6 4ae946e5a9b9 without a fix, is affected too (read).

Versions and branches executed, with results
Version or commitVerdictResult
main ea57360912ed affectedfails with -ea and -da, through gradle and the runner
upstream main a95c754ee003, branch_10x 657014b3c579, branch_10_6 4ae946e5a9b9, branch_10_5 e85bb7b2c86d readsame code on 2026-10-26, not fixed (the branches move)
10.5.2 affectedfails with -ea and -da
10.5.1, 10.5.0, 10.4.0, 10.3.2, 10.3.1 affectedfail (-ea)
10.3.0 first affectedfails with -ea and -da; with the parent's PointInSetQuery.java swapped in it passes
10.2.2 last unaffectedpasses with -ea and -da; with the backport's PointInSetQuery.java swapped in it fails
10.2.1, 10.2.0, 10.1.0, 10.0.0, 9.12.3 not affectedpass; older releases have no early exit

Independent checks

An independent reproducer, a devil's advocate, an adjudication and a reviewer from another model family (reading only) say real; a second cross-model review said "not real" only because it could not run code. The adjudication, a registry re-run and an audit re-ran the repro with fresh seeds; every executed check fails on stock and passes with the fix.

Checks and verdicts378 words

Neither cross-model reviewer ran code of its own; every executed check, including the finder's test that the refute review's wrapper ran (it failed), was run by the hunt's own AI tools and runner.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (a hunt unit) candidateBounded-exhaustive mismatches only in multi-dimensional set queries, 0 with the fix; z3 model; the JUnit fails on main (-ea, -da, 20 of 20), 10.3.0 and 10.5.2 and passes on 10.2.2 and with the fix.
review (OpenAI GPT-5.6, reading only) real, highRefuted five objections (illegal sort, encoding mismatch, unsupported use, intended early exit, misread failure) and judged the fix correct.
Independent reproducer realOwn 6-test JUnit outside org.apache.lucene: 5 fail on main (three seeds, -ea and -da), 10.3.0 and 10.5.2; pass on 10.2.2, 9.12.3 and with the fix; shadow builds isolate the cause.
Refute review (OpenAI GPT-5.6, reading only) not real (low), proceduralIt could not run Java; on substance it agreed with the root cause and the legality of the trigger and called the intended case weak. Its witness {(2,9),(7,-3)} as a JUnit fails on main, 10.3.0 and 10.5.2 and passes on 10.2.2 and with the fix.
Devil's advocate real (high that it is not intended; reach narrow)Built and refuted the works-as-designed case; two segments both skipped; 43 of 200 day-ordered sets wrong.
Adjudication confirmed, highRe-ran the finder's test (fresh seed 5EED1234: fails) and the independent reproduction's (-da: 5 of 6 fail; 10.2.2 and the fix: pass).
Security assessment out of scope of SECURITY.mdFails open only when negated (MUST_NOT, deleteDocuments(Query)), executed on main, 10.3.0 and 10.5.2; fails closed otherwise; no denial of service.
Registry re-run and audit keepRegistry re-run (fresh seeds 5EED1491-5EED1494) and an independent audit (A0D17401-A0D17405): the finder's test fails on main with -ea and -da and on 10.5.2 with -da; the independent reproduction's fails 5 of 6 with -da; they pass on 10.2.2 or with the fix.
This doc's fix validation reproduced; fix validatedThrough gradle in a scratch worktree: the repro and a new TestPointQueries method fail on stock; with the fix they, TestPointQueries, the whole core suite (assertions on and off) and :lucene:core:check -x test pass.

Sources and provenance

Finding record, Lucene and upstream sources and repro files1,276 words

Finding record

LFV-475, snapshot of 2026-11-06

Registry title
Multi-dimensional PointInSetQuery skips segments that contain matching points, because its segment early exit uses the lexicographically first and last set points as per-dimension bounds
Area
search / points queries (PointInSetQuery segment early exit)
Priority
Medium, rank 80: Since 10.3 the PointInSetQuery early exit silently and deterministically skips whole segments for ordinary multi-point sets, but only multi-dimensional subclasses are hit and none exist in Lucene or major downstreams, the same custom-subclass profile as LFV-006.
Impact class
silent wrong results
Impact note
Only applications that subclass PointInSetQuery with numDims >= 2 are hit (every in-tree builder, IntPoint/LongPoint/FloatPoint/DoublePoint/BinaryPoint/InetAddressPoint.newSetQuery and JoinUtil, is 1-D and unaffected, and no multi-dimensional subclass was found in Elasticsearch, OpenSearch or Solr), on the default IndexWriterConfig, codec and searcher with no scale needed: a whole segment silently contributes no hits whenever the set's lexicographically first point has a value in some dimension k >= 1 above the segment's maximum in k, or its last point a value below the segment's minimum in k, which happens with small (near-real-time, frequently flushed) segments or data ingested in the order of a non-first dimension (43 of 200 random sets wrong on day-ordered (entity, day) data, 0 of 200 on uniform data).
Summary
You subclass PointInSetQuery for a 2-D point field, as Lucene's own tests do, and search for the set {(0,1), (1,0)} in an index whose segment holds one document at (1,0). You expect that document to match, as it does for the one-point set {(1,0)}. Instead, since 10.3.0 the query keeps the first and last set points in sorted order, (0,1) and (1,0), as if they were the set's per-dimension minimum and maximum, concludes from dimension 1 (1 is above the segment's maximum 0) that the segment cannot match, and skips it, so count() is 0 with no error; adding a point to a set can thus remove hits.
Introduced
dba6c2c2fd98dce4a1348109ecad09cd3ba44e5f (main) and its branch_10x backport 0b1e2225f69a3ecc106259112d37d48980a1f74b (parent 7f4b775d7e4a), GITHUB#14268 'PointInSetQuery early exit on non-matching segments' (author hanbj, merged by gsmiller; CHANGES 10.3.0, Optimizations) [read: git log -S 'getMinPackedValue' and -G 'fieldPackedUpper' on PointInSetQuery.java return only dba6c2c2fd9, and git blame attributes the lowerPoint/upperPoint capture and the per-dimension segment check to it; later commits on the file (#14965, #16406, #16519) do not touch the check] [exec: PointInSetQuery.java of the backport shadowed onto the 10.2.2 jars fails, the parent's version shadowed onto the 10.3.0 jars passes, and the file differs between 10.2.2 and 10.3.0 only by this commit, so the regression is pinned to it]. The multi-dimensional protected constructor itself exists since 6.0.0 (013abeaa5b7, 2016); only the early exit is new (2025-04-01 (author date 2025-04-01T22:51:34+08:00, merged 2025-04-01T14:51:35Z; PR opened 2025-02-21))
First affected version
10.3.0 [exec, -ea and -da]; 10.2.2 and earlier are not affected [exec: 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.1 and 10.2.2 pass; the early exit does not exist there]
Affected versions
10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1, 10.5.2 and main ea57360912ed [exec: the finder's JUnit fails on every one with -ea, and with -da on 10.3.0, 10.5.2 and main; the independent reproduction's JUnit fails on 10.3.0, 10.5.2 and main; the security probe fails on 10.3.0, 10.5.2 and main]; branch_10_6 (2763db11b0a, contained in the 10.6.0 tag commit 4ae946e5a9b, released 2026-10-29) and the unreleased branch_10x (1309534296e) carry the same constructor and early-exit lines [read], so 10.6.0 has them. Not fixed upstream: main f4cc6ac2097c, branch_10x 657014b3c579, branch_10_6 940768fb0cda and branch_10_5 still compare the first and last sorted points in every dimension [read, 2026-10-26]. Not affected [exec]: 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.1, 10.2.2; older releases by construction (no early exit). Executed only on aarch64 Linux with JDK 25
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; also reached by code reading, Z3
Upstream and prior reports
none found (2026-10-26, read-only; the prior-report record, plus the finder's scripted prior-report search): two scripted prior-report search query sets (GitHub issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web), manual GitHub searches over apache/lucene and downstream repositories, Pony Mail and JQL queries, and the changed files of all 409 open apache/lucene PRs (only #709, #13988 and #14352 touch PointInSetQuery.java; none changes the bounds code). Related, not this bug: #14268 (the introducing PR; its 2-D tests use one-point sets only, and the review never discussed several multi-dimensional points), a deleted #14268 review comment by iverase (2025-03-13, kept only in the issues@ archive, no reply) that asked whether the sorted-input premise is a contract or an implementation detail, without naming dimensions or any wrong result, #16405 and #16406 (ramBytesUsed of the same lowerPoint/upperPoint arrays, merged 2026-08-04), #13033 (the PointRangeQuery early exit it followed, correct there), #14161, #10624.
Lucene commit tested
main ea57360912ed (commit of 2026-10-24; tested 2026-10-26): the repro through gradle in a scratch worktree (stock fails with assertions on and off; with the per-dimension fix it passes, and so do TestPointQueries with a new test method, 46 point-related test classes, the whole lucene/core suite with assertions on and off and :lucene:core:check -x test; the worktree was removed afterwards) and through the standalone JUnit runner on main (-ea, -da, 20 iterations, fresh seeds in the registry re-run and the audit) and on the Maven Central jars of 10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1 and 10.5.2 (fail) and 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.1 and 10.2.2 (pass); upstream main a95c754ee003, branch_10x 657014b3c579, branch_10_6 4ae946e5a9b9 and branch_10_5 e85bb7b2c86d read on 2026-10-26 (same code, not fixed). The fix was validated on main only.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java:42-52 (at ea57360912ed) (class javadoc: 'Abstract query class to find all documents whose single or multi-dimensional point values ... is contained in the specified set', 'This is for subclasses', @lucene.experimental), :64-65 (lowerPoint, upperPoint), :74-75 (constructor javadoc: 'The packedPoints iterator must be in sorted order'), :110-114 (lowerPoint = copy of the first point), :116-121 (order enforced with BytesRef.compareTo over the whole packed value), :129-134 (upperPoint = copy of the last point), :187-201 (scorerSupplier: for every dimension i, return null if lowerPoint[i] > segment max[i] or upperPoint[i] < segment min[i])
  • lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:1491-1494 (at ea57360912ed) (newMultiDimIntSetQuery, the in-tree multi-dimensional subclass, called test-only), :1572-1598 (multi-dimensional set assertions with sets correlated in every dimension), :2566 (testPointInSetQuerySkipsNonMatchingSegments, added by #14268)
  • lucene/CHANGES.txt:1605 (at ea57360912ed) ('GITHUB#14268: PointInSetQuery early exit on non-matching segments. (hanbj)', 10.3.0 Optimizations)
The finding in 30 seconds
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java:43-52 (class javadoc), 74 (sorted order), 110-114, 116-121, 129-134 (constructor), 168-185, 187-201 (scorerSupplier early exit)
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/PointValues.java:473-481
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/search/BooleanWeight.java:299-306; lucene/core/src/java/org/apache/lucene/index/FrozenBufferedUpdates.java:400-402; lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java:263-275
Who is affected
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java:46, 51
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/document/IntPoint.java:241, LongPoint.java:260, FloatPoint.java:270, DoublePoint.java:270, BinaryPoint.java:244, InetAddressPoint.java:294; lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java:415; lucene/sandbox BigIntegerPoint.java:247, HalfFloatPoint.java:399 (all numDims 1)
  • Lucene main ea57360912ed: lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:1491-1494 (newMultiDimIntSetQuery, test-only)
Why it looks unintended
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java:43-48, 74, 116-121
  • Lucene main ea57360912ed: lucene/CHANGES.txt:1522 (Lucene 10.3.0), 1597 (Optimizations), 1605
  • Lucene main ea57360912ed: lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:1491-1494, 1514-1522 (helper sorts with Arrays.compareUnsigned over the whole array), 1560-1580, 1582-1608, 1610-1636, 2566-2604
Suggested fix
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java:151 (final createWeight), 135-140 (ramBytesUsed); Lucene main ea57360912ed: lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:2636-2651 (testRamBytesUsedIncludesBoundsArrays); lucene/core/src/java/org/apache/lucene/search/Query.java:84-86 (rewrite, not final)
History
  • a Lucene clone: git tag --contains 0b1e2225f69a (releases/lucene/10.3.0 to 10.6.0)

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

Repro files

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.