All findings · Lucene finding · LFV-454 · knn-query-per-leaf-heap-sized-by-unbounded-k

kNN queries without a filter size each segment's heap from k: on 3 documents in one segment, k = Integer.MAX_VALUE throws and k = 2^28 runs out of memory

Since 9.10.0, unfiltered KnnFloatVectorQuery and KnnByteVectorQuery size each segment's heap from k before looking at the segment. On a 3-document, one-segment index, k = Integer.MAX_VALUE throws IllegalArgumentException and k = 2^28 exhausts a 2 GiB heap; with a non-match-all filter the same k returns all 3 (main).

Summary

Priority
Low, rank 390Core default kNN queries throw for the k=Integer.MAX_VALUE 'all' idiom and allocate memory in proportion to k, a loud MAX_VALUE-idiom core failure that is more reachable than LFV-394, so it heads that group.
Impact
crash
Since
9.10.0 (#12962); join queries since 9.8.0introduced 2024-02-06 · about 2 years
Affected
9.10.0 to 10.6.0 (10.6.0 by reading) and main; join queries 9.8.0 to main
Status
documented
Found by
a boundary scan of the accepted parameters of public APIs

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 kNN query asks every segment for its k nearest vectors. Since 9.10.0 the unfiltered query builds each segment's collector first, with a heap of k + 1 longs allocated up front, and only then looks at how many vectors the segment holds. Nothing caps k at the segment's size.

Diagram of the bug: on a 3-document, one-segment index, an unfiltered KnnFloatVectorQuery builds each segment's collector from k before it reads the segment, and its heap allocates k + 1 longs at once; k = 2^24 allocates 128 MiB to return 3 hits, k = 2^28 runs out of a 2 GiB heap and k = Integer.MAX_VALUE throws IllegalArgumentException, while the filtered path and LeafReader's searchNearestVectors cap k first and return the 3 hits.
Select the still to open it at full size. Generated with AI, checked against the code.
  1. Index 3 documents with a float vector (one segment, default codec) and search new KnnFloatVectorQuery("v", target, k) with no filter, on a plain IndexSearcher.
  2. k = 1000 returns all 3 documents, as the javadoc ("the number of documents to find") and the upstream testFindAll promise. You expect the same for k = Integer.MAX_VALUE, the usual "all" value; 9.9.2 gives exactly that for the unfiltered query.
  3. Instead the query builds the segment's collector first, with a heap of k + 1 longs, and only then reads the segment's vector count. k = Integer.MAX_VALUE throws IllegalArgumentException: initialCapacity must be > 0 and < 2147483630; got: 2147483647.
  4. Smaller huge k values allocate instead: k = 2^24 reserves 128 MiB to return 3 hits, and k = 2^28 throws OutOfMemoryError with a 2 GiB heap. Four segments cost about 9 times one.
  5. With a filter that matches every document, the same k = Integer.MAX_VALUE returns the 3 hits: that path caps its queue at min(k, matches) (since 9.11.0). Lucene capped k at the segment's vector count until 9.9.2; #12962 dropped the cap. The join DiversifyingChildren*KnnVectorQuery has the same flaw since 9.8.0, and SeededKnnVectorQuery also sizes its seed collector by k.

Who is affected

Anyone on 9.10.0 to 10.5.2 or main who runs KnnFloatVectorQuery or KnnByteVectorQuery (also seeded or patience variants) without a filter with a very large k, such as Integer.MAX_VALUE for "all" or a client's k forwarded unchecked; the join DiversifyingChildren*KnnVectorQuery since 9.8.0. Default codec and searcher. Ordinary k (thousands) wastes only kilobytes; no wrong hits were seen.

Mechanism, terms and numbers858 words

KnnFloatVectorQuery accepts any k >= 1 ("the number of documents to find"), and a k larger than the index returns every vector: k = 1000 over a 3-document index gives 3 hits. Since 9.10.0 the unfiltered (approximate) path builds each segment's collector with a heap sized from k (k + 1 longs on a single segment) before it looks at how many vectors the segment has. So on a single-segment index k = Integer.MAX_VALUE throws IllegalArgumentException: initialCapacity must be > 0 and < 2147483630; got: 2147483647 from an internal heap (with several segments it runs out of memory instead), k = 2^28 over the same 3 documents throws OutOfMemoryError with a 2 GiB heap, and k = 2^24 allocates about 128 MiB to return 3 hits. On main the same k works on that index with a filter that is not match-all (the exact path caps its queue at Math.min(k, cost), since 9.11.0) and through LeafReader#searchNearestVectors(..., int k, ...), which still caps k at the vector count. Up to 9.9.2 the query went through that method, and the readers before it carried the comment "bound k by total number of vectors to prevent oversizing data structures"; #12962 (9.10.0) made the query build its own collector and the cap was lost. The join DiversifyingChildren{Float,Byte}KnnVectorQuery has the same shape and has been unbounded since it was added in 9.8.0, and SeededKnnVectorQuery also sizes its seed collector by k. No wrong hits were seen: every search either returned the expected hits or threw.

Terms

  • Segment collector: for each segment, the kNN query creates a KnnCollector through a KnnCollectorManager. The default one, TopKnnCollector, keeps the best hits in a NeighborQueue, which is a TernaryLongHeap.
  • Approximate vs exact path: without a filter, every segment takes the approximate path (approximateSearch). With a filter, a segment whose matching documents are no more than its share of k takes the exact path (exactSearch), which scores the matches directly.

The bug

  1. The constructor rejects only k < 1 (AbstractKnnVectorQuery.java:67-68). Without a filter (or with one that rewrites to MatchAllDocsQuery, which is dropped, :87-103), getLeafResults calls approximateSearch (:252-254).
  2. KnnFloatVectorQuery.approximateSearch calls knnCollectorManager.newCollector(...) first (KnnFloatVectorQuery.java:107-108), then reads the vector values (:110) and checks Math.min(knnCollector.k(), floatVectorValues.size()) == 0 (:115): the remnant of the old cap, after the allocation. KnnByteVectorQuery is the same (:106-107, :114).
  3. OptimisticKnnCollectorManager turns k into perLeafTopK from the segment's share of the index (AbstractKnnVectorQuery.java:319-347); for one segment that is k itself (computed in float, so a k above 2^24 is rounded to the nearest float). TopKnnCollectorManager passes it on unchanged: new TopKnnCollector(k, visitedLimit, searchStrategy) (knn/TopKnnCollectorManager.java:46, :52).
  4. TopKnnCollector builds new NeighborQueue(k, false) (TopKnnCollector.java:45), and TernaryLongHeap(k) throws IllegalArgumentException for k >= ArrayUtil.MAX_ARRAY_LENGTH ("Throw exception to prevent confusing OOME") or allocates new long[k + 1] at once (util/TernaryLongHeap.java:56-69).
  5. With several segments, the optimistic re-entry pass builds two more full-k collectors per re-entered segment (ReentrantKnnCollectorManager, AbstractKnnVectorQuery.java:128-130, :433-476).
  6. Join: DiversifyingNearestChildrenKnnCollectorManager builds new DiversifyingNearestChildrenKnnCollector(k, ...) (:65-66, :77-78), whose NodeIdCachingHeap(k) allocates three arrays of k + 1 entries and a k-sized hash map (DiversifyingNearestChildrenKnnCollector.java:61, :140-153).
  7. Seeded: SeededKnnVectorQuery first builds a seed collector, new TopScoreDocCollectorManager(k, null, Integer.MAX_VALUE).newCollector() (SeededKnnVectorQuery.java:315-316), with the query's full k on every segment; its TopScoreDocCollector allocates new TernaryLongHeap(numHits, ...) at once (TopScoreDocCollector.java:44).

Where Lucene does cap the same request: the exact path, Math.min(k, Math.toIntExact(acceptIterator.cost())) (AbstractKnnVectorQuery.java:373, #13184, from 9.11.0); LeafReader#searchNearestVectors(..., int k, ...), k = Math.min(k, ...size()) (index/LeafReader.java:288, :332, :376); regular top-N search, IndexSearcher#searchAfter caps numHits at max(1, maxDoc) (IndexSearcher.java:587-597).

The numbers

3 documents, 1 segment, default codecexpectedmain a9779ef3c193
k = 1000, no filter3 hits3 hits
k = Integer.MAX_VALUE, TermQuery filter matching all 33 hits3 hits
k = Integer.MAX_VALUE, no filter3 hitsIllegalArgumentException from TernaryLongHeap
k = 2^28, no filter3 hitsOutOfMemoryError with a 2 GiB heap
k = 2^24, no filter3 hits3 hits, 134,224,064 bytes allocated (14,336 for k = 1000)
the unfiltered rows on 9.9.23 hits3 hits (5,000 bytes for k = 2^24)
the filtered row on 9.9.2 and 9.10.03 hitsIllegalArgumentException from the exact path's HitQueue(k) (capped from 9.11.0 by #13184)
join query, 2 parents, k = Integer.MAX_VALUE2 parentsIllegalArgumentException from NodeIdCachingHeap (also on 9.8.0, 9.9.2, 9.12.3 and 10.5.2)

The k = 2^24 rows come from runs on main ea57360912ed, whose query path is the same as a9779ef3c193's. On 12 vectors, k = 2^22 allocates 33,561,432 bytes in 1 segment and 302,491,608 bytes in 4 segments (9.01x); one k = 2^26 search over 3 vectors runs out of a 512 MiB heap. On a9779ef3c193, k = Integer.MAX_VALUE over 4 segments of 3 documents runs out of a 2 GiB heap instead of throwing IllegalArgumentException, and the same k also fails for KnnByteVectorQuery, KnnFloat16VectorQuery, PatienceKnnVectorQuery, SeededKnnVectorQuery and the byte join query. The filtered vs unfiltered contrast exists only from 9.11.0.

Exposure and likelihood268 words
  • Who is hit. Unfiltered KnnFloatVectorQuery / KnnByteVectorQuery (or with a filter that rewrites to MatchAllDocsQuery) on 9.10.0 through 10.5.2 and main; DiversifyingChildrenFloatKnnVectorQuery / DiversifyingChildrenByteKnnVectorQuery on 9.8.0 and later. Default codec, default IndexSearcher, no setting. SeededKnnVectorQuery and PatienceKnnVectorQuery (since 10.2.0) and KnnFloat16VectorQuery (main only) fail the same way on main (executed on main only).
  • Trigger. k = Integer.MAX_VALUE to mean "all" (an idiom IndexSearcher supports: TestIndexSearcher.testHugeN), a k derived from the index size, or a server that forwards a client's k unchecked.
  • Effect. On a single-segment index, k at or above ArrayUtil.MAX_ARRAY_LENGTH always throws IllegalArgumentException on the first search; with several segments the first pass sizes each heap by the segment's share of k, so it runs out of memory instead (4 segments, 2 GiB heap). Below that, a single-segment search reserves about 8 bytes per unit of k before it collects anything: 128 MiB for k = 2^24, a 512 MiB heap exhausted at k = 2^26, a 2 GiB heap at k = 2^28. More segments (about 9x for 4: first-pass heaps by each segment's share, plus two full-k collectors per re-entered segment) and concurrent searches add up. After a caught OutOfMemoryError a follow-up search on the same searcher worked; with -XX:+CrashOnOutOfMemoryError the JVM aborted.
  • Not affected. No wrong hits were seen; on the 3-document index a filter that is not match-all takes the bounded exact path; ordinary k values cost kilobytes.
  • Scale. None: 3 documents in one segment.
  • How likely. Rare: only a very large k fails, and the Integer.MAX_VALUE case is loud on the first search; nothing was reported since 9.10.0 (2024-02).

Minimal repro

The test shows that KnnFloatVectorQuery without a filter, k = Integer.MAX_VALUE, over a 3-vector single-segment index sizes the per-segment collector heap from k (TestKnnQueryKMaxValue#testKMaxValue).

TestKnnQueryKMaxValue.java (JUnit test)50 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.search;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.KnnFloatVectorField;
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;

public class TestKnnQueryKMaxValue extends LuceneTestCase {

  public void testKMaxValue() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
        for (int i = 0; i < 3; i++) {
          Document doc = new Document();
          doc.add(new KnnFloatVectorField("v", new float[] {i, 1}));
          w.addDocument(doc);
        }
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        Query query = new KnnFloatVectorQuery("v", new float[] {1, 1}, Integer.MAX_VALUE);
        try {
          assertEquals(3, new IndexSearcher(reader).search(query, 10).scoreDocs.length);
        } catch (IllegalArgumentException e) {
          // expected: all 3 vectors (k is "the number of documents to find"); actual: this exception
          throw new AssertionError("expected 3 hits for k = Integer.MAX_VALUE, but got " + e, e);
        }
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-454/, 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 5EED0454
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=5EED0454 \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.search.TestKnnQueryKMaxValue
Expected vs actual
Expected 3 hits (k is "the number of documents to find"); actual IllegalArgumentException 'initialCapacity must be > 0 and < 2147483630; got: 2147483647' from TernaryLongHeap, with -ea and -da.

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

More files, environment, full output66 words

Environment

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

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed 5EED0454: 1 test(s) run, 1 failed
    => java.lang.AssertionError: expected 3 hits for k = Integer.MAX_VALUE, but got java.lang.IllegalArgumentException: initialCapacity must be > 0 and < 2147483630; got: 2147483647
       org.apache.lucene.search.TestKnnQueryKMaxValue.testKMaxValue(TestKnnQueryKMaxValue.java:45)
     Caused by: java.lang.IllegalArgumentException: initialCapacity must be > 0 and < 2147483630; got: 2147483647
       org.apache.lucene.util.TernaryLongHeap.<init>(TernaryLongHeap.java:59)
       org.apache.lucene.util.hnsw.NeighborQueue.<init>(NeighborQueue.java:60)
       org.apache.lucene.search.TopKnnCollector.<init>(TopKnnCollector.java:45)
       org.apache.lucene.search.knn.TopKnnCollectorManager.newOptimisticCollector(TopKnnCollectorManager.java:52)
       org.apache.lucene.search.AbstractKnnVectorQuery$OptimisticKnnCollectorManager.newCollector(AbstractKnnVectorQuery.java:330)
       [...]

Why it looks unintended

  • The javadoc accepts any k >= 1 ("the number of documents to find"), and testFindAll expects a k above the document count to return every document.
  • Until 9.9.2 Lucene capped k at the segment's vector count "to prevent oversizing data structures"; #12962 dropped it, and no review comment mentions it.
  • Since 9.11.0 a non-match-all filter makes the same k work.
The full argument297 words
  • Contract. KnnFloatVectorQuery: "@param k the number of documents to find", "@throws IllegalArgumentException if <code>k</code> is less than 1" (KnnFloatVectorQuery.java:60-61, :73-75; KnnByteVectorQuery.java:59-60, :72-74). These constructors are not @lucene.experimental. The heap's "initialCapacity must be > 0 and < 2147483630" for an accepted k is not the documented failure.
  • Upstream test. BaseKnnVectorQueryTestCase.testFindAll: "Tests that a AbstractKnnVectorQuery whose topK &gt;= numDocs returns all the documents in score order" (k = 10 over 3 documents, :178-194).
  • A deliberate cap, lost silently. 9.0-9.7 readers: "// bound k by total number of vectors to prevent oversizing data structures" / k = Math.min(k, fieldEntry.size()); (9.7.0 Lucene95HnswVectorsReader.java:288-289, from LUCENE-9614 / #413). 9.8-9.9: the cap lived in LeafReader#searchNearestVectors(..., int k, ...) (9.9.2 LeafReader.java:253), which the query called (9.9.2 KnnFloatVectorQuery.java:82); main still has it (LeafReader.java:288). #12962 (9.10.0) made the query build its own collector; reviewers asked to keep the int-k LeafReader method unchanged, and none of the 22 conversation or 40 review comments discusses the cap.
  • Siblings are capped. #13184 (9.11.0) capped the exact path of the same class (AbstractKnnVectorQuery.java:373) because "the number of candidates is actually lower than k". Until then that path failed for a huge k too (9.9.2, 9.10.0), so the project has bounded this kind of k-sized preallocation before. LUCENE-4938 extended IndexSearcher's min(maxDoc, n) cap to the sorted searches and added TestIndexSearcher.testHugeN ("should not throw exception"), which runs search(query, Integer.MAX_VALUE).
  • Inconsistent (from 9.11.0). On one index, k = Integer.MAX_VALUE and 2^28 return all hits with a filter matching every document and through LeafReader#searchNearestVectors(int k), but throw without the filter (this doc's test; a verifier's contrast probe).
  • The manager API speaks of per-leaf sizing. KnnCollectorManager#newOptimisticCollector: "generally with a specific k value, scaled per leaf statistics".
The devil's-advocate check209 words

The strongest case for "intended": top-k naturally needs O(k) storage; TopKnnCollector is @lucene.experimental; the heap's IllegalArgumentException is deliberate ("to prevent confusing OOME"); maintainers have said Lucene targets "some reasonable value of k" (#9588), that k = Integer.MAX_VALUE "will not only run in O(maxDoc) time but also use O(maxDoc) memory" (#12679), and, on dev@ (2022-11), of a very large k, "something that I would like to be documented as not recommended"; downstream engines bound k; SECURITY.md leaves bounding resource-consuming input to the application.

Why it fails: here the storage does not grow with the index at all, because it is reserved per segment before the segment's size is known; 3 documents need 2 GiB. Lucene capped exactly this on purpose until 9.9.2 and still caps it on the filtered path and in LeafReader, and the remarks above were made while the cap existed, so they assume O(maxDoc). The heap's guard only turns an OOME into an exception; it does not make k = Integer.MAX_VALUE a documented failure of the query. Every reviewer rejected the intended-behaviour argument; the latest cross-model fact check rated it moderate, mainly as an argument about which fix to choose (a documented high upper bound would not contradict testFindAll, which uses k = 10).

Suggested fix

Recommended Cap the per-segment k at the segment's maxDoc in both collector managers and the seeded query's seed collector

Cap each segment's collector at min(k, maxDoc) in TopKnnCollectorManager, in the join DiversifyingNearestChildrenKnnCollectorManager and in SeededKnnVectorQuery's seed collector (+22/-5 lines in three files). Validated with gradle on main: the repro tests and 8 large-k variant tests pass with assertions on and off, the 13 closest kNN suites (323 tests) pass, and format and lint pass.

All options and trade-offs4 options · 867 words

One fix was built and validated, first in two files and then completed with the seeded query; the alternatives are untested.

Cap the per-segment k at the segment's maxDoc in both collector managers and the seeded query's seed collector (validated by the checks) (recommended)

A segment cannot return more hits than it has documents. TopKnnCollectorManager.newCollector and newOptimisticCollector pass perLeafK(k, context) instead of k, DiversifyingNearestChildrenKnnCollectorManager does the same in both methods, and SeededKnnVectorQuery's collector manager sizes its seed collector the same way. The TopKnnCollectorManager change covers KnnFloatVectorQuery, KnnByteVectorQuery, KnnFloat16VectorQuery, the optimistic re-entry pass and PatienceKnnVectorQuery; SeededKnnVectorQuery builds its seed collector from the full k on every segment, so it needs the third change (executed).

// TopKnnCollectorManager: newCollector and newOptimisticCollector
return new TopKnnCollector(perLeafK(k, context), visitedLimit, searchStrategy);

/** A leaf cannot return more than {@code maxDoc} hits, so do not size its collector by a larger k. */
private static int perLeafK(int k, LeafReaderContext context) {
  return Math.max(1, Math.min(k, context.reader().maxDoc()));
}

// DiversifyingNearestChildrenKnnCollectorManager: the same helper, in both methods
return new DiversifyingNearestChildrenKnnCollector(
    perLeafK(k, context), visitedLimit, searchStrategy, parentBitSet);

// SeededKnnVectorQuery.SeededCollectorManager.newCollector: the seed collector
// a leaf cannot return more than maxDoc seeds, so do not size the collector by a larger k
int seedK = Math.max(1, Math.min(k, ctx.reader().maxDoc()));
TopScoreDocCollector seedCollector =
    new TopScoreDocCollectorManager(seedK, null, Integer.MAX_VALUE).newCollector();

Trade-offs

  • Same hits. A segment has at most maxDoc vectors, so min(k, maxDoc) slots hold every hit. The reader's choice between graph search and an exhaustive scan (knnCollector.k() < numVectors, Lucene99HnswVectorsReader.java:373) does not change: if k >= numVectors, then min(k, maxDoc) >= numVectors too. A segment cannot hold more than maxDoc seed hits either. Math.max(1, ...) keeps k >= 1 for an empty segment.
  • Bound is maxDoc, not the vector count as before 9.10.0 (the managers do not know the field), so a large segment can still reserve 8 bytes per document for a huge k: O(maxDoc), the cost maintainers described in #12679.
  • Custom KnnCollectorManager implementations are not covered. No benchmark was run; the change adds one Math.min per segment. Add a CHANGES entry under Bug Fixes.
  • Backports (read): branch_10x and branch_10_6 have the same manager code as main. Older lines also allocate a global BlockingFloatHeap(k) per multi-segment query (TopKnnCollectorManager from 9.10.0, the join manager in 9.12.3, both up to 10.2.2), which a backport must bound too; 9.8.x and 9.9.x build the join collector directly in the query; SeededKnnVectorQuery exists from 10.2.0.

What the checks validated

  • Two-manager fix (fix.diff), clean scratch worktree at a9779ef3c193, gradle, seed DEADBEEF, -Ptests.heapsize=2g. Stock: TestKnnQueryLargeK 2 of 4 fail, TestDiversifyingKnnLargeK 2 of 3 fail. With the fix: both pass (4 of 4, 3 of 3), with assertions on and with -Ptests.asserts=false; the 13 closest suites pass; format and lint pass.
  • The seeded gap (standalone runner, a9779ef3c193): the 8 variant tests fail 6 of 6 and 2 of 2 on stock; with fix.diff 4 of 6 pass and both seeded tests still throw IllegalArgumentException from TopScoreDocCollector; with fix-complete.diff all pass, assertions on and off.
  • Complete fix (fix-complete.diff), gradle on a source extraction of a9779ef3c193 with only the patch and the four test files added: with assertions on 338 tests, 0 failures (the 15 repro and variant tests plus TestKnnFloatVectorQuery (43), TestKnnByteVectorQuery (40), TestKnnByteVectorQueryMMap (40), TestSeededKnnFloatVectorQuery (39), TestSeededKnnByteVectorQuery (39), TestPatienceFloatVectorQuery (38), TestPatienceByteVectorQuery (38), TestKnnGraph (6), TestTopKnnResults (1), join TestParentBlockJoinFloatKnnVectorQuery (12), TestParentBlockJoinFloat16KnnVectorQuery (12), TestParentBlockJoinByteKnnVectorQuery (11), TestToParentJoinKnnResults (4)); with -Ptests.asserts=false the 15 tests pass; checkGoogleJavaFormat, ecjLintMain, ecjLintTest, forbiddenApisMain and forbiddenApisTest pass for core and join.
  • Not validated with the fix: byte and float16 queries on several segments, seeded queries with a filter, executor-backed searchers.

Cap at the field's vector count, as before 9.10.0 (untested)

In approximateSearch, read the vector values first and size the collector by min(k, values.size()), in the core and the join queries.

Trade-offs

The pre-9.10 behaviour of the core queries (the join queries were unbounded before 9.10 too) and the tightest bound, but KnnCollectorManager#newCollector has no k parameter, so it needs an API change (or routing a bounded k through newOptimisticCollector).

Grow the heap lazily up to k (untested)

Start NeighborQueue / TernaryLongHeap small and grow it as hits arrive, up to k.

Trade-offs

Also helps large segments with a huge k, but the heap's capacity is its overflow bound today (TernaryLongHeap.java:93-103), so it needs a separate maximum, and it touches the hot path of every kNN search; needs benchmarks. The heap's growth has its own limit (TernaryLongHeap stops growing at 715,827,883 elements, registry LFV-069).

Reject or document a maximum k (not recommended)

Validate k against an upper bound in the constructor, or document one.

Trade-offs

The query does not know the index when it is built, and a bound would not fit the filtered path, LeafReader's int-k method and IndexSearcher (testHugeN), which all accept any k (testFindAll uses only k = 10, so a high documented bound would not contradict it). Until a fix ships, applications can cap k themselves, for example at reader.maxDoc() (read, not executed); that caps by the whole index, so on several segments each segment's collectors can still be sized by the index's maxDoc.

Downstream note

Downstream: Elasticsearch and OpenSearch cap k at 10,000, while Solr's kNN parser reads topK with no upper bound (source reading only), so an application that forwards a client's k should cap it, for example at the index size.

How it was found

A hunt for accepted-parameter boundaries scanned the argument checks of Lucene's public APIs and probed the ranges they accept: the kNN query constructors reject only k < 1. End-to-end runs with very large k then hit the failure, and a JUnit test reproduced it on the stock jars.

The method in full152 words

Method: a static and dynamic boundary scan (the finder's label: semi-formal; no model checker, solver or prover found it).

  • A static guard inventory of the real jars: a JDK ClassFile API pass over 27 core and non-sandbox module jars listed 1,231 public entry points whose bytecode reaches an IllegalArgumentException, with each guard's parameters and constants.
  • The kNN query constructors reject only k < 1 (AbstractKnnVectorQuery.java:67-68), so every k up to Integer.MAX_VALUE is accepted.
  • A scratch driver (its output was not saved) then ran KnnFloatVectorQuery, KnnByteVectorQuery and the two DiversifyingChildren* queries end to end on the stock jars with k in {5, 1000, 2^20, 2^27, 2^28, Integer.MAX_VALUE - 9, Integer.MAX_VALUE}, and a public-API JUnit test witnessed the failure (2026-10-26).
  • The same scan found the pattern at LatLonPoint.nearest(..., n = Integer.MAX_VALUE) and RescoreTopNQuery(..., n = Integer.MAX_VALUE) (an IllegalArgumentException from PriorityQueue); those are separate sites and were not filed.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-26, re-checked 2026-10-28) in Lucene's GitHub issues, PRs and commits, the mailing lists, JIRA, four downstream projects or the web. Nearest: #13184 (capped only the exact path) and #11387 (the k = 0 case on all-deleted segments).

Searches4 searches

Verdict new: 29 hits were classified and none is this bug by anyone. Context, not reports: #12962 / #13086 introduced it; #413 (LUCENE-9614) added the cap; #9588 (LUCENE-8542, open) asks to size regular per-slice collectors by the slice; #12679, #11946 and a 2022-11 dev@ thread call a very large k costly or not recommended, while the cap still existed. No open PR bounds k by the segment. Gaps: GitHub search is token-based and ran near the rate limit; JIRA search covers the LUCENE archive only; private security reports cannot be searched.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; dev@, java-user@, issues@ and commits@; JIRA archive; the web2026-10-26the hunt's prior-report tool: 20 GitHub issue/PR, 3 commit, 2 downstream, 20 list, 4 JIRA and 6 web queries (TopKnnCollectorManager, TernaryLongHeap, "maxSize must be", "initialCapacity must be", "bound k", "oversizing data structures", perLeafTopK, ...)no report; nearest #13184, #11387 / LUCENE-10351, #9588
GitHub: apache/lucene and four downstream projects (Elasticsearch, OpenSearch, OpenSearch k-NN, Solr)2026-10-2623 manual searches (exception texts, large-k OOM, NodeIdCachingHeap, BufferAllocationException), plus the finder's 45 and a verifier's 43 queriesno report; downstream hits unrelated
Open PRs, upstream history, CHANGES, PR comments2026-10-26the changed files of 129 of the 408 open apache/lucene PRs; history of the 9 affected files; the comments of #12962, #13086 and #12794; one dev@ thread; 3 JIRA entries10 open PRs touch the files, none caps k; #12962's review never discusses the cap
GitHub issues and PRs (apache/lucene); upstream commit lists2026-10-28"initialCapacity must be"; TopKnnCollector updated since 2026-10-26; commits of the affected filesnothing new; no affected file changed after a9779ef3c193 (head 20ced71c1e6e)

History

Introduced in 9.10.0 by #12962 (main d095ed02a2a; backport #13086, a6c45ebe432), which stopped routing the query through LeafReader#searchNearestVectors and its cap. 9.9.2 passes; 9.10.0 through 10.5.2 and main fail. The join queries fail in every release since they were added in 9.8.0.

Versions tested11 rows

Introduced by #12962, "Speedup concurrent multi-segment HNWS graph search" (main d095ed02a2a, merged 2024-02-06), and its branch_9x backport #13086 (a6c45ebe432, 2024-02-07), first released in 9.10.0. It made approximateSearch build the collector through the new TopKnnCollectorManager instead of calling LeafReader#searchNearestVectors(field, target, int k, ...), which caps k. Attribution rests on the diff, the release boundary (9.9.2 passes, 9.10.0 fails) and the only other query-path commit in 9.9.2..9.10.0 (#13058, unrelated); no commit-level bisect build was run.

The cap itself came from LUCENE-9614 (#413, 2021, before 9.0.0) in the readers, and #12434 (9.8.0) moved it into LeafReader. The join queries were added by #12434 (1f25c68cf03, 9.8.0) without it. #15163 (10.4.0) switched NeighborQueue to TernaryLongHeap, which changed the message from maxSize must be ... to initialCapacity must be .... #13184 (9.11.0) capped the filtered/exact path, which also failed on 9.9.2 and 9.10.0. Not fixed upstream as of 20ced71c1e6e (2026-10-27).

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis doc's test fails 2 of 4 and the join test 2 of 3 (gradle and standalone, assertions on and off)
main a9779ef3c193, variants affectedk = Integer.MAX_VALUE fails for byte, float16, patience and seeded queries, on 4 segments (OutOfMemoryError), and for the byte and 2-segment join queries: 8 of 8 tests
main ea57360912ed affectedoriginal repro 2 of 3, a verifier's tests 4 of 6; k = 2^24 allocates 134,224,064 bytes
10.5.2, 10.0.0, 9.12.3 affectedfail 4 of 5 (TernaryLongHeap on 10.5.2, LongHeap on 10.0.0 and 9.12.3)
9.11.0, 9.12.0 affectedfail (a reviewer's 9.x port of the repro)
9.10.0 first releasefails 4 of 5; the filtered path fails too there (capped from 9.11.0 by #13184)
9.9.2 last before (core, unfiltered)unfiltered: every k returns 3 hits, 5,000 bytes for k = 2^24; the filtered k = Integer.MAX_VALUE throws from HitQueue(k) (capped from 9.11.0) and the join query fails
9.5.0, 9.7.0, 9.8.0, 9.9.0 not affectedcore queries pass
join: 9.8.0, 9.9.2, 9.12.3, 10.5.2, main join queriesk = Integer.MAX_VALUE and 2^28 fail since the queries were added
join: 9.7.0 API absentno DiversifyingChildrenFloatKnnVectorQuery (compile error)
fix, a9779ef3c193 not affectedtwo-manager fix: both repro tests and the 13 closest suites pass, but the seeded variant still fails; complete three-file fix: the repro tests, 8 variant tests and the 13 closest suites (338 tests) pass, format and lint pass

Independent checks

The finder's repro, an independent reproducer's own tests, a cross-model review (OpenAI GPT-5.6), a devil's-advocate check, an adjudication, a credibility audit and fact checks found it real; a second cross-model review said not real only because it could not run code. The fact checks corrected the text and found that the fix missed the seeded query.

Checks and verdicts458 words

The checks below ran on main ea57360912ed unless stated; the kNN query path is the same on a9779ef3c193.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateA public-API JUnit fails 2 of 3 on main, 10.5.2, 10.0.0 and 9.12.3, assertions on and off; a core-only cap passes it and TestKnnFloatVectorQuery.
cross-model review (refute lens) not real (low), on procedure onlyOpenAI GPT-5.6, reading only: it could not run Java, so it voted not real on procedure only; it agreed with the root cause and rated the intended-behaviour argument weak. A separate run re-ran the repro: fails 2 of 3.
independent reproducer realOwn tests fail 4 of 6 on main, 10.5.2 and 9.12.3, 5 of 6 on 9.10.0 and 1 of 6 on 9.9.2 (on the last two also the filtered control, before #13184); the unfiltered tests pass on 9.9.2. Found the join variant (failing since 9.8.0) and measured 134,224,064 bytes for k = 2^24.
cross-model review (intended lens) real (high)OpenAI GPT-5.6, reading only (no Java run): the intended-behaviour argument is weak; the core-only fix sketch is incomplete without the join manager. (The release runs that show 9.7.0-9.9.0 passing and 9.10.0-9.12.0 failing were run separately)
devil's advocate real (high)Built the works-as-designed case and showed why it fails; its contrast probe shows the same k working with a filter and through LeafReader#searchNearestVectors.
adjudication confirmed (high)Re-ran the reproducer's tests: 4 of 6 fail, both controls pass. Corrected the versions (core 9.10.0, join 9.8.0) and the framing (an exception for an accepted k and a lost cap, not a memory contract).
credibility audit keepRe-ran the original repro with a fresh seed: 2 of 3 fail, assertions on and off.
independent fact check real; 4 text errors, fixedRe-ran the gradle repro (assertions on and off), the release jars and a multi-segment probe on a9779ef3c193: the bug, root cause and fix hold. Corrected the 9.9.2 rows (only unfiltered queries pass there), the single-segment scope of the IllegalArgumentException, a commit-range description and a release number.
cross-model fact check real; fix incomplete (seeded), fixedOpenAI GPT-5.6, reading only: found that SeededKnnVectorQuery's seed collector is still sized by k with the two-manager fix (then executed and fixed here), plus over-broad filter, per-segment and backport claims (corrected).
diagram check one wrong line range, fixed
this doc reproduced; fix validatedGradle on a9779ef3c193: stock, the new test fails 2 of 4 and the join test 2 of 3; with the two-manager fix both pass and the 13 closest suites pass; with the complete three-file fix also the 8 variant tests (seeded included) pass, 338 tests in all, and lint passes.

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,634 words

Finding record

LFV-454, snapshot of 2026-11-06

Registry title
kNN vector queries size each segment's result heap from the requested k, not from what the segment can return, so k = Integer.MAX_VALUE throws IllegalArgumentException and k = 2^28 runs out of a 2 GiB heap on a 3-document index (core KnnFloat/KnnByteVectorQuery without a filter: regression in 9.10.0; join DiversifyingChildren*KnnVectorQuery: since 9.8.0)
Area
search / kNN vector queries (per-segment collector sizing: AbstractKnnVectorQuery, TopKnnCollectorManager, TopKnnCollector; join DiversifyingNearestChildrenKnnCollectorManager)
Priority
Low, rank 390: Core default kNN queries throw for the k=Integer.MAX_VALUE 'all' idiom and allocate memory in proportion to k, a loud MAX_VALUE-idiom core failure that is more reachable than LFV-394, so it heads that group.
Impact class
crash
Impact note
Applications on 9.10.0 through 10.6.0 (10.6.0 by reading) or main that run a KnnFloatVectorQuery or KnnByteVectorQuery without a filter (default codec, IndexSearcher and query, no setting needed), or on 9.8.0 and later a DiversifyingChildrenFloat/ByteKnnVectorQuery, with a very large k get an IllegalArgumentException from an internal heap for k = Integer.MAX_VALUE (the usual Java 'all' value), and each search reserves heap in proportion to k before any segment's size is consulted (about 8 bytes per unit of k on a single-segment index; four segments cost about 9 times as much), so one k = 2^24 search over 3 vectors allocates 128 MiB and k = 2^28 runs out of a 2 GiB heap; ordinary k values (up to the thousands) waste only kilobytes, results are never wrong, no index size is needed, Elasticsearch and OpenSearch cap k at 10,000, and Solr's kNN parser does not bound topK (read in source only, not run).
Summary
You run a KnnFloatVectorQuery with a k larger than the index, for example k = Integer.MAX_VALUE to mean 'all vectors', and expect every vector back, as k = 1000 over 3 vectors gives, and as 9.9.2 gives for any k. Instead each segment's collector is built with a heap sized from k (k + 1 longs on a single-segment index) before the segment's vector count is looked at: k = Integer.MAX_VALUE throws IllegalArgumentException('initialCapacity must be > 0 and < 2147483630; got: 2147483647') from TernaryLongHeap, and k = 2^28 over a 3-document index throws OutOfMemoryError with a 2 GiB heap. Lucene used to cap k at the segment's vector count ('bound k by total number of vectors to prevent oversizing data structures'); a search speed-up change dropped that cap in 9.10.0, and the join DiversifyingChildren*KnnVectorQuery never had it.
Introduced
d095ed02a2a7742235e73320fa992eabfeecbecc on main (GITHUB#12962 'Speedup concurrent multi-segment HNWS graph search' by mayya-sharipova, merged 2024-02-06) and its branch_9x backport a6c45ebe4321cacfac27380d41125ed6ae7a8dee (PR #13086, merged 2024-02-07), which first shipped in 9.10.0 [read: the diff plus the executed release boundary (9.9.2 passes, 9.10.0 fails); the only other query-path commit in 9.9.2..9.10.0 is c059819a59a (#13058, filtered-path visit limit, unrelated); no commit-level bisect build]: approximateSearch stopped calling LeafReader.searchNearestVectors(field, target, int k, ...), which caps k at the vector count, and instead builds the per-segment collector from k through the new TopKnnCollectorManager; the old cap survives only as the emptiness check 'Math.min(knnCollector.k(), size()) == 0', evaluated after the heap is allocated. Join DiversifyingChildren{Float,Byte}KnnVectorQuery: unbounded since the API was added (#12434, branch_9x 1f25c68cf03, 2023-08-07, first released in 9.8.0) [read + exec] (2024-02-06 (main, d095ed02a2a); 2024-02-07 (branch_9x backport a6c45ebe432); join path 2023-08-07 (1f25c68cf03))
First affected version
9.10.0 [exec] for the core KnnFloat/KnnByteVectorQuery (tag 2024-02-11, from the branch_9x backport; 9.5.0, 9.8.0 and 9.9.2 pass [exec]; the first main-line release containing d095ed02a2a is 10.0.0); 9.8.0 [exec] for the join DiversifyingChildren*KnnVectorQuery (since the API was added; 9.7.0 has no such class [exec: compile error])
Affected versions
core KnnFloatVectorQuery/KnnByteVectorQuery without a filter (the approximate path): 9.10.0, 9.11.0, 9.11.1, 9.12.0-9.12.3, 10.0.0, 10.1.0, 10.2.0-10.2.2, 10.3.0-10.3.2, 10.4.0, 10.5.0-10.5.2, 10.6.0 (by reading: same collector-manager code as main) and main ea57360912ed [exec: 9.10.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da) in the follow-up history runs; 9.11.0 and 9.12.0 by the cross-model review; the rest by source check at each tag]; join DiversifyingChildren{Float,Byte}KnnVectorQuery: 9.8.0 through main [exec: 9.8.0, 9.9.2, 9.12.3, 10.5.2, main]; not affected: core 9.0.0-9.9.2 [exec: 9.5.0, 9.8.0, 9.9.2, and 9.7.0 and 9.9.0 by the cross-model review; read for the rest]; before 9.0.0 there is no kNN query; KnnFloat16VectorQuery (main only, unreleased) has the same approximateSearch shape [read, not executed]; a filtered search that routes to exact search is bounded by Math.min(k, cost) from 9.11.0 (#13184); up to 9.10.0 that exact path also failed (HitQueue(k)) [exec on 9.9.2 and 9.10.0], a separate history already fixed; not fixed upstream (no change to the 9 affected files on upstream main 49483e82b357, 2026-10-26, and no open PR bounds k) [read]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal
Upstream and prior reports
none found (2026-10-26, the prior-report record: the scripted prior-report search (20 GitHub issue and PR, 3 commit, 2 downstream, 20 mailing-list queries over dev@, java-user@, issues@ and commits@, 4 JIRA-archive and 6 web queries), 23 manual read-only GitHub searches over apache/lucene, elastic/elasticsearch, opensearch-project/k-NN, opensearch-project/OpenSearch and apache/solr, the changed files of 129 of the 408 open apache/lucene PRs (kNN, vector, collector, heap, join or query in the title; 10 touch the affected files, none caps k), the upstream commit history of the 9 affected files, local git history and CHANGES.txt, the comments of #12962, #13086 and #12794, one dev@ thread, 3 JIRA entries and 8 more web queries, plus the finder's run (45 queries) and the intended-behaviour check's (43 queries); 29 hits classified, none the same bug); not fixed upstream (no change to the affected files on upstream main 49483e82b357, 2026-10-26; no open PR fixes it); closest items, not reports: #12962 / #13086 (the introducing change; no review comment discusses the dropped cap), #413 / LUCENE-9614 (added the per-segment cap 'to prevent oversizing data structures', 2021), #13184 (capped the same oversizing at the sibling exact-search site only, 9.11), #11387 / LUCENE-10351 with PR #580 (the k = 0 'maxSize must be' case on all-deleted segments, fixed in 9.0), #9588 / LUCENE-8542 (open enhancement: size regular per-slice collectors by the slice), and #12679, #11946 and a 2022-11 dev@ thread in which maintainers call a very large k costly or not recommended, all written while k was still capped per segment
Lucene commit tested
main a9779ef3c193 (2026-10-26): the doc's repro through gradle in a clean scratch worktree (stock, and with the fix plus the closest kNN suites and lint) and through a standalone JUnit runner on the built jars, assertions on and off; on 2026-10-28, 8 more large-k variant tests (stock and fixed, standalone runner) and the complete three-file fix through gradle (the repro and variant tests, the 13 closest kNN suites, lint). Earlier runs of the same code: main ea57360912ed and the release jars 9.5.0, 9.7.0, 9.8.0, 9.9.0, 9.9.2, 9.10.0, 9.11.0, 9.12.0, 9.12.3, 10.0.0 and 10.5.2. Upstream main 20ced71c1e6e (2026-10-27) has the same code (read).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java:60-61 (at ea57360912ed) ('@param k the number of documents to find', '@throws IllegalArgumentException if <code>k</code> is less than 1'; same at :73-75), :106-108 (approximateSearch builds the collector via knnCollectorManager.newCollector first), :115 ('if (Math.min(knnCollector.k(), floatVectorValues.size()) == 0)', after the allocation)
  • lucene/core/src/java/org/apache/lucene/search/KnnByteVectorQuery.java:107, (at ea57360912ed) :114 (same shape); lucene/core/src/java/org/apache/lucene/search/KnnFloat16VectorQuery.java:108, :115 (same shape, main only)
  • lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java:64-69 (at ea57360912ed) (constructor rejects only k < 1), :272-286 (filtered path: perLeafTopK, exact search when cost <= perLeafTopK), :319-333 (OptimisticKnnCollectorManager.newCollector: perLeafTopK from k and the leaf proportion, not the leaf size), :343-346 (perLeafTopKCalculation; equals k for a single leaf), :373 (exactSearch: 'Math.min(k, Math.toIntExact(acceptIterator.cost()))'), :444-470 (ReentrantKnnCollectorManager builds two collectors per segment on re-entry)
  • lucene/core/src/java/org/apache/lucene/search/knn/TopKnnCollectorManager.java:43-53 (at ea57360912ed) ('new TopKnnCollector(k, visitedLimit, searchStrategy)' in newCollector and newOptimisticCollector)
  • lucene/core/src/java/org/apache/lucene/search/TopKnnCollector.java:43-46 (at ea57360912ed) ('this.queue = new NeighborQueue(k, false)')
  • lucene/core/src/java/org/apache/lucene/util/TernaryLongHeap.java:56-69 (at ea57360912ed) (IllegalArgumentException for initialCapacity >= ArrayUtil.MAX_ARRAY_LENGTH 'to prevent confusing OOME'; eager 'new long[initialCapacity + 1]')
  • lucene/core/src/java/org/apache/lucene/index/LeafReader.java:273, (at ea57360912ed) :317, :361 ('k = Math.min(k, ...size())' before 'new TopKnnCollector(k, visitedLimit)' in the int-k searchNearestVectors methods)
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:588-597 (at ea57360912ed) (searchAfter caps numHits at max(1, maxDoc))
  • lucene/join/src/java/org/apache/lucene/search/join/DiversifyingNearestChildrenKnnCollectorManager.java:65, (at ea57360912ed) :77 (new DiversifyingNearestChildrenKnnCollector(k, ...)); DiversifyingNearestChildrenKnnCollector.java:61 ('new NodeIdCachingHeap(k)'), :140 (NodeIdCachingHeap(int maxSize))
  • Lucene main ea57360912ed SECURITY.md lines 28, 38 (resource exhaustion from unbounded, unfiltered input is not a Lucene vulnerability), 50
The finding in 30 seconds
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java:67-68, 87-103, 106-110, 128-130, 252-254, 319-347, 373, 433-476
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java:101-118; KnnByteVectorQuery.java:106-107, 114
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/knn/TopKnnCollectorManager.java:43-53; TopKnnCollector.java:43-46
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/util/TernaryLongHeap.java:56-69; util/hnsw/NeighborQueue.java:59-60
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/LeafReader.java:288, 292, 332, 336, 376, 380; IndexSearcher.java:587-597
  • a Lucene main checkout lucene/join/src/java/org/apache/lucene/search/join/DiversifyingNearestChildrenKnnCollectorManager.java:57-79; DiversifyingNearestChildrenKnnCollector.java:61, 140-153
  • a Lucene clone: git diff --stat ea57360912ed a9779ef3c193 (no change to the kNN query path)
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/SeededKnnVectorQuery.java:315-316; TopScoreDocCollector.java:44; TopScoreDocCollectorManager.java:62
Who is affected
  • a Lucene main checkout lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java:78-110 (testHugeN)
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java:87-103 (match-all filter dropped)
Why it looks unintended
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java:60-61, 73-75; KnnByteVectorQuery.java:59-60, 72-74; AbstractKnnVectorQuery.java:67-68, 373
  • a Lucene main checkout lucene/core/src/test/org/apache/lucene/search/BaseKnnVectorQueryTestCase.java:178-194; TestIndexSearcher.java:78-110
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/LeafReader.java:288; search/knn/KnnCollectorManager.java (newOptimisticCollector javadoc); util/TernaryLongHeap.java:57-63; search/TopKnnCollector.java:27
  • a Lucene clone releases/lucene/9.9.2 LeafReader.java:253, KnnFloatVectorQuery.java:82; releases/lucene/9.7.0 lucene95/Lucene95HnswVectorsReader.java:288-289
Suggested fix
  • a Lucene main checkout lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsReader.java:373; util/TernaryLongHeap.java:93-103; search/knn/KnnCollectorManager.java
  • a Lucene clone release tags: BlockingFloatHeap(k) in TopKnnCollectorManager at 9.10.0-10.2.2 and in the join manager at 9.12.3-10.2.2; join manager absent in 9.8.0 and 9.9.2; SeededKnnVectorQuery from 10.2.0
History
  • a Lucene clone: git tag --contains a6c45ebe432 (first releases/lucene/9.10.0); lucene/CHANGES.txt:2843, 3028 (#13184, #12962)

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

Repro files

About the diagrams

Made in parallel from the code at main a9779ef3c193 and executed probe runs; the PNG is the summary still.

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.