All findings · Lucene finding · LFV-006 · bkd-selective-indexing

Set queries miss matches on points fields with more dimensions than index dimensions

On custom points fields with one index dimension and more in total, set queries silently skip matches: the writer may sort a leaf by non-indexed values, but the query expects increasing indexed values. A 200,000-document run missed 91% of hits, CheckIndex stays clean and every release since 7.6.0 is affected.

Summary

Priority
Medium, rank 79Raised from the bottom of Medium: PointInSetQuery deterministically and silently misses about 91% of matches on selectively indexed points in every release since 7.6, but only custom field types have that shape (no Lucene, Elasticsearch, OpenSearch or Solr field), so it stays mid-Medium.
Impact
silent wrong results
Since
7.6.0introduced 2018-10-08 · about 8 years
Affected
all 65 releases 7.6.0-10.5.2, branch_10_6 and main (7.7.x and 8.0.0: flush case only)
Status
documented
Found by
semi-formal: a Dafny proof aimed a bounded-exhaustive check of the real BKD writer

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

Summary card of the diagram: one leaf block, sorted by the non-indexed value d, drawn as four document cards (30, 5), (10, 5), (20, 5), (40, 9); doc0 is marked match, doc1 missed, doc2 and doc3 no match; newSetQuery(f, 10, 30) = 1, expected 2, because the indexed values arrive as 30, 10, 20, 40. The card also states the field shape setDimensions(2, 1, 4), that every release from 7.6.0 to 10.5.2 and main is affected, and that nothing fails loudly.
One leaf block as four document cards, the bytes of each value, and the sorted query values with a forward-only pointer, plus the merge case. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Give a points field 2 dimensions, only the first indexed (setDimensions(2, 1, 4)), and add 4 documents (indexed, non-indexed): (30, 5), (10, 5), (20, 5), (40, 9). They flush as one leaf.
  2. To compress the leaf, the writer sorts it by the dimension with fewer distinct values in the first differing byte, non-indexed ones included: here the non-indexed one, 2 against 3. So the indexed values are stored as 30, 10, 20, 40.
  3. You expect newSetQuery(f, 10, 30) to find 2 documents. On the same field newExactQuery(f, 10) finds 1 and newRangeQuery(f, 10, 30) finds 3, both correct.
  4. Instead it finds 1. The set query's pointer only moves forward: seeing 30 first, it moves from 10 to 30, so doc 1 (value 10) is missed. newSetQuery(f, 10) finds 0. In a separate 200,000-document run (random values, 200 random set queries), 189,088 of 207,884 expected hits (91.0%) were missed.
  5. With one index dimension there is no error with or without -ea, and CheckIndex reports the index clean. Two segments answer 2; after forceMerge(1) the same query finds 1. With 2 of 3 dimensions indexed, a multi-dimensional set query (core has no public factory for one) found 0 instead of 2; under -ea an assertion fires instead.

Who is affected

Narrow: it needs a custom field type, setDimensions(n > 1, 1, bytes) (one index dimension, more in total), a set query (newSetQuery, or JoinUtil with ScoreMode.None) and data that makes a leaf sort by a non-indexed dimension; the default codec and IndexWriterConfig suffice. No Lucene-provided field has that shape; a partial code search of Elasticsearch, OpenSearch and Solr found none.

Mechanism, terms and numbers716 words

A points field can store more dimensions than it indexes ("selective indexing", Lucene 7.6.0). For such a field with one index dimension, the BKD writer may order a leaf block by a non-indexed dimension, while PointInSetQuery assumes the indexed values arrive in increasing order. The set query then silently skips matches: in a 200,000-document run it missed 91% of the hits, with no error, and CheckIndex stays clean. Every release since 7.6.0 is affected; it needs a custom field type.

Terms

  • A points field stores one or more fixed-width numbers per document and is indexed in a BKD tree: inner nodes split the values into ranges, and the values end up in leaf blocks of up to 512 points by default (1024 up to 8.5.x).
  • Selective indexing (LUCENE-8496, 7.6.0): FieldType.setDimensions(dimensionCount, indexDimensionCount, dimensionNumBytes). Only the first indexDimensionCount dimensions build the tree; the others are only stored next to each value in the leaf blocks. In the code, numDataDims means all dimensions.
  • A set query matches documents whose value is any of the given values: IntPoint.newSetQuery("f", 10, 30) and the same method on LongPoint, FloatPoint, DoublePoint, BinaryPoint, InetAddressPoint and the sandbox HalfFloatPoint and BigIntegerPoint all build a one-dimensional PointInSetQuery.

What goes wrong, for a field with 1 index dimension and more total dimensions

  1. The writer. To compress a leaf block, BKDWriter sorts the leaf by one dimension: at the first byte where the values differ, it picks the dimension with the fewest distinct values of that byte, among all dimensions, non-indexed ones included (it must be strictly fewer; a tie keeps dimension 0). So a leaf can be sorted by a non-indexed dimension, and then its indexed values are not in increasing order. This is designed: the feature commit widened these loops to all dimensions, and in review the author replied that leaves "require even the non indexed dimensions be sorted for compression".
  2. The query. For any field with one index dimension, PointInSetQuery uses MergePointVisitor. It walks the sorted query values and the indexed values together, like a merge, and its pointer only moves forward: when it sees an indexed value larger than the current query value, it moves past that query value for good. If a smaller indexed value comes later in the same leaf, that document is silently missed (main PointInSetQuery.java:326-340).

Worked example (the test's data)

Field f = (indexed value, non-indexed value), four documents in one leaf: doc 0 = (30, 5), doc 1 = (10, 5), doc 2 = (20, 5), doc 3 = (40, 9). Each number is 4 bytes and only the last byte differs. On flush the writer counts that byte from the leaf's second point: indexed {0A, 14, 28} = 3 distinct, non-indexed {05, 09} = 2, so the leaf is sorted by the non-indexed dimension. The visit order is 30, 10, 20, 40.

newSetQuery(f, 10, 30) with query values [10, 30]: it sees 30, moves its pointer from 10 to 30 and matches doc 0; it sees 10, but the pointer is at 30, so doc 1 is missed. Result 1 hit, expected 2. newSetQuery(f, 10) returns 0 (expected 1), while newExactQuery(f, 10) returns 1 and newRangeQuery(f, 10, 30) returns 3, both correct. Three documents are enough; two cannot trigger it.

No exception and no assertion fires, with or without -ea. CheckIndex says the index is clean by design: its order check runs only when the total dimension count is 1, and its comment says leaves with more dimensions "are sorted by the dimension with the lowest cardinality to improve block compression".

The same root cause with two or more index dimensions

For a field like setDimensions(3, 2, 4) queried with a multi-dimensional PointInSetQuery (core has no public factory for it), SinglePointVisitor compares the whole stored value (12 bytes) with the 2-index-dimension query point (8 bytes), and these never compare equal. Leaf blocks whose index values all equal the query point are accepted whole without a compare, so it misses the matches in mixed leaf blocks: 2 x (1,2,i) + 2 x (3,4,i) gives 0 instead of 2, and 600 + 600 gives 512 instead of 600. Under -ea, the length assertion in SinglePointVisitor.visit (main :414) fires instead, but only when such a mixed leaf is visited.

Exposure and likelihood337 words
  • Silently wrong results. Missing hits only, no false positives, and CheckIndex is clean. With one index dimension nothing fails under -ea or -da; the multi-index-dimension case trips an assertion only under -ea. On the same field newSetQuery(f, v) can return 0 while newExactQuery(f, v) returns 1.
  • At scale. 200,000 documents, indexed values 0-4999, a 4-valued non-indexed dimension, one segment, 200 random set queries of 1 to 50 values: all 200 wrong, 18,796 of 207,884 expected hits returned, 189,088 (91.0%) missed. A plain one-dimensional copy of the same values is exact. A fully default IndexWriterConfig with FSDirectory and four commits also fails (100 of 100 queries wrong).
  • A merge can make correct results wrong. Written as two segments of two documents each, both leaves are sorted by the indexed value (in the first the non-indexed values are equal, so that dimension is not counted; in the second the counts tie 1 to 1) and the query answers 2. After forceMerge(1) the merged leaf counts 4 versus 2, is sorted by the non-indexed value, and answers 1 instead of 2 (on 7.6.0 and on 8.1.0 and later; 7.7.x and 8.0.0 answer 2 here).
  • When it triggers. In a 50,000-document sweep: never with unique sequential indexed values; with unique random indexed values and a 2-valued or timestamp-like non-indexed dimension, 97-98% of hits missed; with 1000 distinct indexed values and a 2- or 4-valued or timestamp-like one, 90-92%; with 100 distinct and a 2-valued or timestamp-like one, about 2-2.5%. A 64-valued, random or index-equal non-indexed dimension missed nothing.
  • Who is exposed: narrow. It needs a custom field type, setDimensions(n > 1, 1, bytes), through Field(name, byte[], type), BinaryPoint's Expert constructor or a custom field class, plus a set query (the core and sandbox newSetQuery factories, or JoinUtil with ScoreMode.None). No Lucene-provided field has that shape, and a partial code search of Elasticsearch, OpenSearch and Solr found none. The default codec and a default IndexWriterConfig are enough otherwise.
  • Old. Every release since 7.6.0 (2018).

Minimal repro

The test shows that IntPoint.newSetQuery("f", 10, 30) misses a match on a points field with 2 dimensions of which only the first is indexed (FieldType.setDimensions(2, 1, 4)), 3 docs (30,5) (10,9) (20,9) in one segment (TestPointInSetQuerySelectiveIndexing#testSetQueryOnOneIndexDimensionOfTwo).

TestPointInSetQuerySelectiveIndexing.java (JUnit test)40 lines
package org.apache.lucene.search;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
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.tests.util.TestUtil;
import org.apache.lucene.util.NumericUtils;

public class TestPointInSetQuerySelectiveIndexing extends LuceneTestCase {

  public void testSetQueryOnOneIndexDimensionOfTwo() throws Exception {
    FieldType type = new FieldType();
    type.setDimensions(2, 1, Integer.BYTES);
    type.freeze();
    try (Directory dir = new ByteBuffersDirectory()) {
      IndexWriterConfig iwc = new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec());
      try (IndexWriter w = new IndexWriter(dir, iwc)) {
        for (int[] point : new int[][] {{30, 5}, {10, 9}, {20, 9}}) {
          byte[] packed = new byte[2 * Integer.BYTES];
          NumericUtils.intToSortableBytes(point[0], packed, 0);
          NumericUtils.intToSortableBytes(point[1], packed, Integer.BYTES);
          Document doc = new Document();
          doc.add(new Field("f", packed, type));
          w.addDocument(doc);
        }
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        // expected 2 hits (indexed values 30 and 10), actual 1: the doc with value 10 is missed
        assertEquals(2, new IndexSearcher(reader).count(IntPoint.newSetQuery("f", 10, 30)));
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-006/, 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.TestPointInSetQuerySelectiveIndexing
Expected vs actual
Expected count 2 (indexed values 30 and 10); actual expected:<2> but was:<1>: the leaf is sorted by the non-indexed dimension, so the forward-only MergePointVisitor skips 10. Fails the same way with -ea and -da, and no exception is thrown.

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

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:<2> but was:<1>
       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.search.TestPointInSetQuerySelectiveIndexing.testSetQueryOnOneIndexDimensionOfTwo(TestPointInSetQuerySelectiveIndexing.java:36)

Why it looks unintended

  • PointInSetQuery accepts these fields, yet the feature commit made the join's sibling query reject them (IAE): likely an oversight.
  • setDimensions(int, int, int) is public and not experimental, tested at codec level but not with set queries; BinaryPoint's own set query also finds 1 of 2.
  • Precedent: #8920, the same visitor fed unsorted 1-D values, was labelled a bug and fixed.
The full argument181 words
  • PointInSetQuery accepts these fields. It checks values.getNumIndexDimensions() != numDims (main :168), so a one-index-dimension field passes. The feature commit changed that check as part of a ~70-file split of getNumDimensions; nothing in the issue or the PR discusses set queries.
  • In the same commit the sibling query was given the opposite check. PointInSetIncludingScoreQuery (join), which has its own forward-only merge visitor, throws an IllegalArgumentException for such fields. That points to an oversight, not a design.
  • "Wrong factory" does not hold. BinaryPoint's public Expert constructor BinaryPoint(String, byte[], IndexableFieldType) accepts a (2,1,1) type, and BinaryPoint.newSetQuery on it returns 1 instead of 2 (and 0 instead of 1 for one value), while newExactQuery returns 1.
  • The configuration is supported and tested. FieldType.setDimensions(int, int, int) is public and not experimental; its validation allows 1 <= indexDimensionCount <= dimensionCount, and TestBKD and BasePointsFormatTestCase randomize the index dimensions in 1..numDims. Set queries on it are not tested.
  • Precedent. #8920 (LUCENE-7869, "MemoryIndex should sort 1d points"), an unsorted 1-D source feeding MergePointVisitor, was labelled type:bug and fixed in 6.6.1, 6.7 and 7.0.
The devil's-advocate check163 words

The intended-behaviour checker tried to show it is not a bug. What holds: the writer's leaf order is designed and documented; the IntersectVisitor#visit javadoc's "In the 1D case, values are visited in increasing order" was written in 2016, before selective indexing, and Lucene's other code reads "1D" as total dimensions = 1; JoinUtil documents its "to" field as IntPoint, LongPoint, FloatPoint or DoublePoint, so that route is outside its contract. What is weak: PointInSetQuery is @lucene.experimental, which means the API may change, not that wrong results are allowed; and the configuration is tested at the codec level.

Verdict: not intended; a bug. The writer behaves as designed and the query was never adapted to it. The maintainers may still prefer to reject such queries with an IAE (as the sibling query does) rather than make them work; either way the silent wrong answer is a defect. Not a security issue: wrong results on a Lucene-written index, so a public issue is right.

Suggested fix

Recommended Query side: use the merge visitor only for truly one-dimensional fields, and compare only the index-dimension prefix

+9/-5 lines in PointInSetQuery.java; both parts are needed. It repairs segments already on disk with no format change; plain one-dimensional fields are unchanged, and on these fields a set query walks the tree once per value (not measured). On main 174 points and BKD tests and 2,829 core search and document tests pass; the full core suite was not run.

All options and trade-offs4 options ยท 478 words

The checks validated the two-part query-side fix and re-ran the writer-side branch; the single-walk visitor and the reject option come from the brief and were not run.

Query side: use the merge visitor only for truly one-dimensional fields, and compare only the index-dimension prefix (validated by the checks) (recommended)

Branch 5b-bkd-selective-indexing-fix (f340c1790e; -fix-106 5622253100 for branch_10_6), patch fix-query-side-main.patch, +9/-5 lines in PointInSetQuery.java: (1) use MergePointVisitor only if numDims == 1 && values.getNumDimensions() == 1, otherwise the existing SinglePointVisitor path; (2) SinglePointVisitor.visit compares only the index-dimension prefix, Arrays.equals(packedValue, 0, pointBytes.length, pointBytes, 0, pointBytes.length), and its assertion becomes >=.

Trade-offs

  • Both parts are needed: part 1 alone sends these fields to the unfixed SinglePointVisitor (0 hits under -da, an AssertionError under -ea); part 2 alone fixes only the multi-index-dimension case.
  • On these fields set queries then walk the tree once per query value instead of once in total (not measured). Plain one-dimensional fields are unchanged.
  • It repairs segments already on disk; no index format change. The full core suite was not run.
  • Related, separate root cause: MemoryIndex.getNumIndexDimensions() returns the total dimension count; fix it with or after this fix, because fixing it alone turns an IAE into a silent miss.

What the checks validated

  • main: TestPointQueries, org.apache.lucene.util.bkd.*, TestPointValues and TestLucene90PointsFormat, seed DEADBEEF, -ea and -da: 174 tests, 0 failures; branch_10_6: 175 tests, 0 failures; the 3 new tests pass with two seeds.
  • More on the main fix: :lucene:join:test 104 tests, :lucene:memory:test 51, sandbox document.* 23, core search.* + document.* 2,829, all passing.
  • On an index written and force-merged by the 10.5.2 jar: stock code 77 of 100 queries wrong, with the fix 0 of 100. The demo is correct everywhere, the scale run included.

Writer side: sort by dimension 0 when there is one index dimension

Branch 5b-bkd-selective-indexing-fix-writer (43d7991778), patch fix-writer-side-main.patch: force sortedDim = 0 when numIndexDims == 1 and dimension 0 is not constant in the leaf, close to what LUCENE-8562 did on the merge path in 7.7/8.0.

Trade-offs

The two one-index-dimension tests pass, but the 3-dimension / 2-index-dimension test still fails; it does not repair existing segments; it may cost some leaf compression (not measured); SimpleText has its own BKD writer and stays wrong under it.

A single-walk visitor (sketch, not run)

Keep one tree walk: inner nodes are still split on the index dimension, so compare() can keep advancing past query values below the cell; inside a leaf, look each value up among the remaining query values (binary search) instead of consuming them.

Trade-offs

Keeps the single walk and fixes old segments, but still needs part 2 for multi-index-dimension fields. Not built.

Reject instead of fix

Throw an IAE for set queries on fields with non-indexed dimensions, as PointInSetIncludingScoreQuery already does.

Trade-offs

Simpler, but it changes behaviour for queries that sometimes work today.

How it was found

A Dafny proof (of a hand-written model) that the set query's merge visitor finds exactly the matches needed leaf values to arrive sorted. A bounded-exhaustive check of that order on real BKD code covered 664,200 tiny trees with 2 dimensions, 1 indexed: 19,940 failures, all "1-D values out of order"; other shapes had 0. The task never mentioned selective indexing.

The method in full284 words

Method: a Dafny proof whose precondition aimed a bounded-exhaustive check of the real BKD code (the study's "semi-formal" tier). Run 3, experiment "bkd-points-proof" (2026-10-22, main 7cecf79523).

  • The task text named the area (points and BKD query evaluation, including PointInSetQuery and BKDWriter) and the techniques (Dafny proofs, and bounded-exhaustive checking of the real BKDWriter/BKDReader with tiny configurations, "numDims 1-2"). It did not mention selective indexing or index versus non-indexed dimensions.
  • Dafny, on a hand-written model: MergePointVisitor.dfy proves that the merge visitor plus intersect collect exactly the matching values, given a precondition WF: non-empty nodes, sorted values, values inside the node bounds (17 verified, 0 errors). A mutant that drops "sorted" fails with 4 errors.
  • Bounded-exhaustive check of the real code: a Java harness on the real BKDWriter/BKDReader (-ea) checked WF on every tree it built, reading "1D" as numIndexDims == 1, the same reading PointInSetQuery uses. Family 2d1i (2 dimensions / 1 index dimension, 1 byte per dimension, 3 values per dimension, up to 4 points, leaves of 1-3 points; flush, heap and offline paths; format versions 9 and 10; 5 docID layouts): 664,200 trees, 16,106,500 queries, 25.6 s, 19,940 failures, all "1-D values out of order". Every other family, 3d2i included, had 0. The AI tool added the 2d1i and 3d2i families itself.
  • What makes it a bug is the wrong hit counts: the proof is about a model, and the check on the real code is exhaustive only within tiny bounds. A separate AI verifier, given the candidate, wrote its own tests with different data and found the SinglePointVisitor and MemoryIndex parts while reading the code; a follow-up folded SinglePointVisitor into this bug.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues and PRs (39+ queries), the migrated JIRA issues or the dev@, java-user@ and issues@ lists, searched 2026-10-23. Related, not duplicates: the feature (#9542), the 1-D order contract (#8115) and the precedent #8920.

Searches5 searches

No report of this bug was found. The related items are the feature itself, the 1-D order contract, the MemoryIndex precedent and the merge-path leaf-sort changes.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene)2026-10-23Checkers, 39+ queries, open and closed: MergePointVisitor, SinglePointVisitor, "selective indexing", numIndexDims, indexDimensionCount, sortedDim, "data dimensions" PointInSetQuery, newSetQuery wrong hits, writeField1Dim, and all ~46 plain "PointInSetQuery" hitsno report; the newest issue was #16900 (the hunt's own)
Migrated JIRA issues (in the GitHub search)2026-10-23the same searchesrelated, not duplicates: #9542 (LUCENE-8496, the feature), #8115 (LUCENE-7059, the 1-D order contract), #8920 (LUCENE-7869, the precedent), #9608 (LUCENE-8562) and #9719 (LUCENE-8673), the merge-path leaf sort
Mailing lists (lists.apache.org)2026-10-23dev@, java-user@ and issues@no report
Open PRs2026-10-23PRs touching PointInSetQuery.java and the BKD code#14352 adds another MergePointVisitor-based visitor in the same branch; #16847, #13988 and #709 touch the file; #12528 writes sortedDim into leaves (same blind spot if the sorted dimension is non-indexed)
Downstream code (partial)2026-10-23GitHub code search of Elasticsearch, OpenSearch and Solr default branches for 3-argument setDimensionsno (n, 1) field; one (7, 4) shape field

History

Introduced in 7.6.0 (2018) by LUCENE-8496 (#9542), selective indexing, which kept the leaf sort over all dimensions; the set query's forward-only cursor dates from 2016. All 65 releases from 7.6.0 to 10.5.2, branch_10_6 and main are affected; 7.7.x and 8.0.0 only in the flush case.

Versions tested7 rows

Introduced by LUCENE-8496 "Explore selective dimension indexing in BKDReader/Writer" (#9542, Nick Knize, closed 2018-10-18, fix versions 7.6 and 8.0). On master, 1118299c338 (2018-10-08) is the feature: it changed the PointInSetQuery check to index dimensions, kept the leaf sort over all dimensions and added the CheckIndex comment; 4cfa876d9d2 (2018-10-10) sent flush through the same N-dimensional leaf sort. Both first released in 8.0.0. On branch_7x a first backport was reverted and re-applied as 804afbfd47c (committed 2018-10-18), which shipped in 7.6.0. MergePointVisitor's forward-only cursor dates from PointInSetQuery's creation in 2016 and was correct before selective indexing existed.

Versions and branches executed, with results
Version or commitVerdictResult
main b83101cdd2, branch_10_6 d04ee73177 affectedJUnit 3/3 fail on 4 seeds with -ea and -da; the demo and the 200k run fail the same way
10.5.2 affecteddemo identical; 200k: 200/200 wrong, 189,088 missed
8.1.0 to 10.5.1 (every 9.x and 10.x) affectedflush 1/2, merge 1/2, multi-dimension 0/2
7.7.0-7.7.3, 8.0.0 flush case onlyflush 1/2; the merge example gives 2, because LUCENE-8562 sorted merge-built leaves by index dimensions there; LUCENE-8673 (8.1.0) went back to all dimensions
7.6.0 affectedflush and merge; 200k: 200/200 wrong, 187,265 missed (1024-point leaves then)
7.5.0 not affectedthe feature is absent: setDimensions(int, int, int) does not exist
fix f340c1790e / 5622253100 not affectedevery count correct, the scale run included

Independent checks

Independent AI checkers, a lens each, and a claim re-check that re-ran the disputed claims checked brief v1; their corrections (which versions show the merge case, the multi-index-dimension symptom, a leaf-size claim, one command) are in brief v2. The study's verifier reproduced the bug with its own data.

Checks and verdicts288 words

Independent AI checkers each took a lens on brief v1, and a claim re-check re-ran the disputed claims; brief v2 applies the result.

Each independent check and its verdict
CheckVerdictWhat it found
exec-current confirmed8-run JUnit matrix per branch (3/3 fail each); independent repros with LongPoint and DoublePoint, FSDirectory.open and a separate JVM per step; showed both fix parts are needed and that the fix repairs old segments; checked JoinUtil, the sandbox factories and SimpleText.
exec-versions correct in substance, one errorAll 65 releases 7.6.0-10.5.2 affected; one factual error about which versions show the merge symptom (7.7.x and 8.0.0 get it right) and an overstatement in the AI summary.
code-history holds, with errorsCitations, root cause, PR facts and fix results hold; the multi-index-dimension symptom is layout-dependent, not "matches nothing"; ran the join, memory, sandbox and search suites on the fix.
intended-behavior not intended; a bugThe writer is designed, the query was never adapted; the BinaryPoint Expert constructor defeats the wrong-factory objection; a 50k-document sweep of when it triggers.
dups-policy no existing report or fix39 GitHub queries plus the mailing lists; checked the open PRs and the AI-policy wording.
Claim re-check status readyIts own program on three jars (3 docs suffice, the BinaryPoint case, the multi-dimension layouts). Verdicts: one claim refuted (leaf sizes 1-4, really 1-3), the 8.0.0 row wrong as worded, an order of events not supported by the file times, and one command bug.
study verifier new bugBKD points proof verifier reproduced it with its own data and, reading the code next to it, found the SinglePointVisitor and MemoryIndex defects; follow-up 4 folded the SinglePointVisitor symptom into this bug and kept MemoryIndex separate.

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 notes494 words

Finding record

LFV-006, snapshot of 2026-11-06

Registry title
PointInSetQuery silently misses most matches on selectively indexed points (fewer index dimensions than data dimensions)
Area
util / BKD points
Priority
Medium, rank 79: Raised from the bottom of Medium: PointInSetQuery deterministically and silently misses about 91% of matches on selectively indexed points in every release since 7.6, but only custom field types have that shape (no Lucene, Elasticsearch, OpenSearch or Solr field), so it stays mid-Medium.
Impact class
silent wrong results
Impact note
Anyone using the opt-in selective-indexing FieldType (setDimensions with fewer index than data dimensions) with PointInSetQuery/newSetQuery; default codec and SimpleText; every release since 7.6.0; no exception or assertion.
Summary
You index a point field with more data dimensions than index dimensions (FieldType.setDimensions(d, 1, bytes), selective indexing) and run a PointInSetQuery (newSetQuery) on it. You expect every document whose indexed value is in the set to match. Instead BKDWriter sorts leaves by a data dimension and the query's forward-only visitor skips matches: 189,088 of 207,884 expected hits (91%) were missed in the repro, and a 3-dim/2-index-dim field returned 0 hits instead of 2.
Introduced
LUCENE-8496: master 1118299c338 and 4cfa876d9d2 (first released in 8.0.0); branch_7x re-applied backport 804afbfd47c, which shipped in 7.6.0 (2018-10-08 (1118299c338); 2018-10-10 (4cfa876d9d2); 2018-10-18 (804afbfd47c committed on branch_7x))
First affected version
7.6.0
Affected versions
all 65 lucene-core releases 7.6.0-10.5.2 [exec], branch_10_6 and main [exec]; 7.7.0-8.0.0 only the flush case (merge-built leaves sorted correctly there)
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; not yet triaged by a person.
Found by
semi-formal: Dafny, exhaustive check
Upstream and prior reports
none found (2026-10-23: 39 gh queries plus all 46 PointInSetQuery hits); precedent #8920 (LUCENE-7869); contract origin #8115 (LUCENE-7059)
Lucene commit tested
main b83101cdd2 and branch_10_6 d04ee73177 (2026-10-23; JUnit on 4 seeds, each with -ea and -da), all 65 lucene-core releases on Maven Central from 7.6.0 to 10.5.2 (sha1-checked), 7.5.0 (feature absent), and fix branches f340c1790e and 5622253100. The public-list rerun reproduced both repros on main b83101cdd2; the hunt's main ea57360912ed adds only #16883, which narrows LFV-004 alone, and SelIdxDemo run on a jar built from ea57360912ed (2026-10-25, -da and -ea) prints the same flush, merge and 3-dimension results.

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

Repro files

About the diagrams

A structural picture of one leaf block as four document cards, the bytes of each value, and the sorted query list with a forward-only pointer, plus the merge case. The PNG is the closing card.

Each item was checked against the brief and the code before it was applied.

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.