A block is children followed by their parent, and a block join finds a child's parent as the first parent after it. IndexWriter lets a merge reorder documents only when a parent field identifies the blocks, then applies the merge policy's map without checking it.
- Set
setParentField("_parent"), no index sort, and the merge policynew BPReorderingMergePolicy(new LogDocMergePolicy(), new BpVectorReorderer("vec"))with default settings. Add 4 blocks withaddDocuments, each two children with a vector and a parent without one, in two segments:[b0c0 b0c1 b0p b1c0 b1c1 b1p]and[b2c0 b2c1 b2p b3c0 b3c1 b3p]. - Run
forceMerge(1). You expect every block to stay together, parent last: the parent field exists so that IndexWriter never alters the order of a block, and IndexWriter lets a merge reorder documents only because the parent field lets it keep blocks. - Instead the merged order is
b0c0 b0c1 b1c0 b1c1 b2c0 b2c1 b3c0 b3c1 b0p b1p b2p b3p.BpVectorReorderernever looks at the parent field, and it puts every document without a vector after all documents that have one. - So
ToParentBlockJoinQueryreturnsb0pfor all 8 children (6 wrong), and a nested kNN query for the best child of 3 parents returns 1 hit instead of 3. With the vectors on the parents instead, all 8 children land after the last parent and have no parent at all. - No exception, with assertions on or off, and
CheckIndexpasses. The scramble is written into the merged segment; reindexing the affected blocks repairs it.
Applications on main that opt into BPReorderingMergePolicy with BpVectorReorderer and index document blocks with a parent field and no index sort. With vectors on the children or the parents, every merge the policy reorders (forced, and at default settings natural too) breaks blocks, from 4 blocks on; with vectors on every document, only merges of 64+ vectors.
Mechanism, terms and numbers818 words
IndexWriter lets a merge renumber documents on an index with document blocks only when a parent field identifies the blocks, because then the reorder can keep each block together; it applies whatever doc map the merge policy returns without checking it. BPReorderingMergePolicy with BpVectorReorderer returns a map that moves every document on its own: the vector bisection never reads the parent field, and documents without a vector are moved after all documents that have one. So a reordered merge silently scrambles the merged segment's blocks: with vectors on the children only or on the parents only, at default settings and at any scale (4 blocks suffice); with vectors on every document, only merges with at least 64 vectors (twice the default minPartitionSize), where the bisection runs, can break them (66 documents: 23 children wrong). Block joins then return another block's parent or none, nested kNN returns fewer hits, and deletes that locate children through a block join miss them; CheckIndex passes, and only reindexing the affected blocks reliably repairs it. The defect came with #14097 (2025-01-07) and exists only on main, so a fix before 11.0.0 keeps it out of every release.
- Document block: documents added together with
IndexWriter#addDocuments, children first, parent last. Block joins (ToParentBlockJoinQuery,DiversifyingChildrenFloatKnnVectorQuery, ...) find a child's parent as the first parent document after it, so the order inside a segment is the only link between them. - Parent field (
IndexWriterConfig#setParentField): IndexWriter marks every parent with it. Its javadoc: "In order to maintain the API guarantee that the document order of a block is not altered by the IndexWriter a marker for parent documents is required." - Merge-time reordering: a merge policy can return a doc map from
OneMerge#reorder, and IndexWriter writes the merged segment in that order.BPReorderingMergePolicydoes this with anIndexReorderer:BPIndexReorderer(by postings) orBpVectorReorderer(by vector similarity, recursive graph bisection).
IndexWriter.mergeMiddleskips the reorder only if an index sort is set, or if some merging segment has blocks while some merging segment (not necessarily the same one) has no parent field (IndexWriter.java:5544-5555, added by #13128 "MakeOneMerge#reorderpreserve blocks"). Otherwise it callsmerge.reorder(...)(:5558) and applies the map withSortingCodecReader.wrap(mergedView, docMap, null)(:5576). Nothing checks the map against the blocks.BPReorderingMergePolicyforwards the call toreorderer.computeDocMap(...)(BPReorderingMergePolicy.java:133-145).BpVectorReorderer.computeDocMap(BpVectorReorderer.java:578-593) permutes the vector ordinals and never reads the parent field; the file contains neither "parent" nor "block". The bisection splits a range only when each half would hold at leastminPartitionSizevectors (default 32,:208-211); when it runs it may move any vector document anywhere in the range, so blocks can interleave, depending on the vectors.valueMapToDocMap(:735-789, "sort docs with no value at the end") gives every document without a vector a new ID after all documents with one. When only the children (or only the parents) carry the vector, all parents end up after all children (or before them), whatever the vector values and even when the bisection does not run. When every document has a vector, nothing is moved to the end, and below 64 vectors (default settings) the bisection does not run either: the map is the identity (:737-738) and the blocks survive.- The sibling
BPIndexReordererreads the parent field and moves whole blocks (BPIndexReorderer.java:376-382,:450-455,:878-882, #13125).BpVectorReorderercame later (#14097) without that.
4 blocks [bNc0, bNc1, bNp], 2-dimensional vectors, two segments, forceMerge(1), default reorderer settings, vectors on the children or on the parents, main ea57360912ed, assertions on and off:
| vectors on | merged order | parent a block join finds |
|---|---|---|
| children only | b0c0 b0c1 b1c0 b1c1 b2c0 b2c1 b3c0 b3c1 b0p b1p b2p b3p | b0p for all 8 children: 6 wrong |
| parents only | b0p b1p b2p b3p b0c0 b0c1 b1c0 b1c1 b2c0 b2c1 b3c0 b3c1 | none for all 8 children |
On the first index, ToParentBlockJoinQuery returns b0p for every child and DiversifyingChildrenFloatKnnVectorQuery with k=3 returns 1 hit (b2c0); with the fix, every child gets its own parent and the kNN query returns 3 hits. CheckIndex passes on the scrambled index.
- 30 blocks of 1-3 children (
minPartitionSize2, three segments):ToParentBlockJoinQuerywrong for 59 of 60 children with child-only vectors and 56 of 60 with vectors on every document. - 16 blocks, default settings, child-only vectors: 15 of 16 tested children joined to
b0p; diversified kNN with k=3 returned 1 hit. - A variant with
minPartitionSize2 broke 64 of 64 blocks with vectors on children only, on parents only and on every document. - Vectors on every document, default settings (fact check): 4, 16 and 21 blocks (up to 63 documents) keep every child with its parent; 22 blocks (66 documents) give 23 children the wrong parent, 32 blocks (96 documents) 24.
Exposure and likelihood376 words
- Who is exposed. Applications that combine
BPReorderingMergePolicywith aBpVectorReorderer(lucene/misc, expert), document blocks withIndexWriterConfig#setParentField, and no index sort (with an index sort IndexWriter never reorders). The natural user is a nested-vector index: parents with child "chunk" documents that carry embeddings, searched withDiversifyingChildrenFloatKnnVectorQueryandToParentBlockJoinQuery. That is the layout that breaks deterministically; the join module's own test notes "only child documents are allowed to have a vector!" for its nested kNN queries. - Default or opt-in. Opt-in. Inside that setup, with vectors on the children or on the parents, nothing else is needed: default reorderer settings, no explicit codec (the runs drew randomized test codecs), assertions on or off.
- Scale. With vectors on the children or on the parents: none, 4 blocks (12 documents) in 2 segments, and the result is deterministic. With vectors on every document and default settings, the merge must hold at least 64 vectors (66 documents broke, 63 did not), and where each child lands depends on the vectors (56 of 60 children wrong in a 30-block run with
minPartitionSize2). - Frequency. Whenever the policy applies a doc map to a segment with blocks. Forced merges were executed; by code reading the policy also reorders natural, full-flush and forced-deletes merges that reach
minNaturalMergeNumDocsand the biggest-segment ratio (defaults 1 and 0, so every such merge; the javadoc suggests 262,144 documents for natural merges). No map is applied when the reorderer exceeds its RAM budget, when the merge has no float vector field of that name, or when the IndexWriter gate refuses.addIndexes(CodecReader...)passes the same IndexWriter gate but was not executed. - What you see. Nothing at write time: no exception,
CheckIndexpasses (its only parent-aware order check sits in the index-sort test, and reordering never happens with an index sort). At query time block joins return another block's parent or none, nested kNN returns fewer hits, and a delete that finds a root's children through a block join leaves them live. The scramble is persisted; by code reading later merges do not undo it, and reindexing the affected blocks repairs it; documents deleted before the merge stay deleted. - Versions. Main only, since #14097 (2025-01-07);
CHANGES.txtlists it under 11.0.0. Fixing it before 11.0.0 keeps it out of every release.
The test shows that forceMerge through BPReorderingMergePolicy with BpVectorReorderer breaks document blocks even though IndexWriterConfig#setParentField is set (2 blocks [child with vector, parent], one segment each).
/*
* 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.misc.index;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.KnnFloatVectorField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LogDocMergePolicy;
import org.apache.lucene.index.StoredFields;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestBpVectorMergeKeepsBlocks extends LuceneTestCase {
public void testForceMergeKeepsBlocks() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
IndexWriterConfig config =
new IndexWriterConfig()
.setParentField("_parent")
.setMergePolicy(
new BPReorderingMergePolicy(
new LogDocMergePolicy(), new BpVectorReorderer("vector")));
try (IndexWriter writer = new IndexWriter(dir, config)) {
for (int block = 0; block < 2; block++) {
Document child = new Document();
child.add(new StringField("id", "c" + block, Field.Store.YES));
child.add(new KnnFloatVectorField("vector", new float[] {1, block + 1}));
Document parent = new Document();
parent.add(new StringField("id", "p" + block, Field.Store.YES));
writer.addDocuments(List.of(child, parent));
writer.commit();
}
writer.forceMerge(1);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
StoredFields storedFields = reader.storedFields();
List<String> order = new ArrayList<>();
for (int doc = 0; doc < reader.maxDoc(); doc++) {
order.add(storedFields.document(doc).get("id"));
}
// expected: each child directly before its own parent; actual: [c0, c1, p0, p1]
assertTrue(
"expected blocks [cN, pN] kept together after the merge, got " + order,
order.equals(List.of("c0", "p0", "c1", "p1"))
|| order.equals(List.of("c1", "p1", "c0", "p0")));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-093/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/misc/build/libs/lucene-misc-$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.misc.index.TestBpVectorMergeKeepsBlocks
- Expected vs actual
- Expected each child directly before its own parent, e.g. [c0, p0, c1, p1]. Actual: [c0, c1, p0, p1], so c1 now belongs to p0's block.
More files, environment, full output43 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected blocks [cN, pN] kept together after the merge, got [c0, c1, p0, p1]
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.assertTrue(Assert.java:42)
org.apache.lucene.misc.index.TestBpVectorMergeKeepsBlocks.testForceMergeKeepsBlocks(TestBpVectorMergeKeepsBlocks.java:63)setParentFieldexists "to maintain the API guarantee that the document order of a block is not altered by the IndexWriter";addDocuments: "merging will preserve a block".- IndexWriter reorders a merge only when the parent field lets the reorder keep blocks (#13128), and the sibling
BPIndexReordererkeeps them (#13125). - The introducing PR never discusses blocks.
The full argument316 words
- The contracts.
IndexWriterConfig#setParentField(IndexWriterConfig.java:580-588): the parent marker is required "to maintain the API guarantee that the document order of a block is not altered by the IndexWriter".IndexWriter#addDocuments(IndexWriter.java:1499-1500): "the index does not currently record which documents were added as a block. Today this is fine, because merging will preserve a block." Here IndexWriter itself runs the reorder, inside a merge it schedules. - The gate. #13128 ("Make
OneMerge#reorderpreserve blocks", 2024-03-29) changed IndexWriter "to only callOneMerge#reorderwhen it has a chance to preserve the block structure, ie. either there are no blocks or blocks are identified by a parent field", and made the test framework'sMockRandomMergePolicyreverse whole blocks when a parent field is set (MockRandomMergePolicy.java:268-300). - The sibling and CHANGES. 9.11.0 CHANGES (
lucene/CHANGES.txt:2742-2743): "GITHUB#13125: Recursive graph bisection is now supported on indexes that have blocks, as long as they configure a parent field".BpVectorReorderer's javadoc calls it an "Implementation of recursive graph bisection ... See BPIndexReorderer".TestBPIndexReorderer.testSingleTermWithBlocksasserts complete, parent-last blocks. That entry concernsBPIndexReorderer, the only reorderer at the time, so it shows the design intent rather than a rule for every later reorderer; thesetParentFieldsentence and the #13128 gate are the direct evidence. - Intent of #14097. Its PR body: "refactored to make
BpVectorReorderercompatible withBPIndexReordererso that either can plug intoBPReorderingMergePolicy". None of its 33 texts (body, reviews, comments) mentions parent, child, nested, join,setParentFieldorhasBlocks; the 3 hits for "block" are "should not block merging". Upstream'sTestBPReorderingMergePolicypicksBpVectorReordererat random but never uses blocks or a parent field. - History repeats. On 9.10.0 the same policy with the postings reorderer broke blocks the same way ("child b4c1 ended up in the block of parent b10p"); 9.11.0 fixed that with #13125 and #13128 (executed). #14097 let a second reorderer into the gated path without the same handling.
The devil's-advocate check367 words
The strongest case for "intended" (an OpenAI GPT-5.6 reviewer with an "intended" lens, and the devil's-advocate verifier): neither the OneMerge#reorder javadoc (@lucene.experimental) nor the IndexReorderer interface (no such tag, like BpVectorReorderer and BPReorderingMergePolicy) says a reorderer must keep blocks; the setParentField javadoc calls the field "optional for all indices that don't use document blocks in combination with index sorting", and MIGRATE says it must be configured "only if index sorting is used together with documents blocks"; addDocuments warns that "in the future Lucene may merge more aggressively re-order documents ... in which case you may need to fully re-index" and that offline re-sorting tools "will likely break them up. Use such tools at your own risk!"; the BPReorderingMergePolicy javadoc still names only BPIndexReorderer; and moving documents without a vector to the end is deliberate and tested (TestBpVectorReorderer.testIndexReorderSparse).
Why it fails: the block duty is stated at the IndexWriter level, and the #13128 gate exists only so that a reorder runs when blocks can be kept; a reorderer that ignores the parent field defeats it. The "future" sentence and the offline-tool NOTE come from LUCENE-3112 (2011, the commit that added addDocuments), long before the parent field (2024). The NOTE is about offline tools; the "future" sentence does foresee merges that reorder documents for compression, which is what this policy does, so the javadoc pulls two ways. What settles it: #13128 later made merge-time reordering conditional on the parent field precisely so that blocks are kept. The "optional ... with index sorting" sentence and the MIGRATE note came with #12829 (2024-01-11), before #13128 (2024-03-29) made the same field the condition for reordering an index with blocks at all. (The NOTE does cover BpVectorReorderer's offline reorderIndexDirectory tool.) The policy javadoc is stale, not a restriction: #14097 plugged the vector reorderer into this policy on purpose. Sparse ordering is fine without blocks; with blocks it is exactly what separates parents from children. @lucene.experimental marks an unstable API, not permission to return wrong block-join results. The "intended" lens rated the case moderate in its record and "weak" in its notes; the refute lens, the independent reproducer, the devil's advocate and the adjudication rejected it.
Recommended A: do not reorder segments that have blocks
Stop BpVectorReorderer from reordering segments that have blocks: computeDocMap returns null when the segment has blocks (+8 lines). The test, the closest suites and the whole misc module pass with assertions on and off. The cost: no vector reordering for nested indexes; a block-aware reorderer would keep it, but is untested.
if (vectorScore == null) {
return null;
}
if (reader.getMetaData().hasBlocks()) {
// This reorderer moves documents one by one, which would break document blocks (children
// followed by their parent), so leave segments that have blocks in their current order.
return null;
}All options and trade-offs4 options ยท 800 words
One fix was built and validated in a scratch worktree at ea57360912ed (removed afterwards); the other options come from the adjudication, the verifiers and this doc and were not run.
In BpVectorReorderer.computeDocMap, return null (no reordering) when reader.getMetaData().hasBlocks(), and say so in the class javadoc. +8 lines in BpVectorReorderer.java, no API change.
if (vectorScore == null) {
return null;
}
if (reader.getMetaData().hasBlocks()) {
// This reorderer moves documents one by one, which would break document blocks (children
// followed by their parent), so leave segments that have blocks in their current order.
return null;
}Trade-offs
- Feature loss. Vector reordering is switched off for every segment with blocks, which is the nested-vector layout most likely to want it; those segments keep their merge order, and
bp.reorderedsaysfalse. - Offline tool. For indexes without a parent field,
reorderIndexDirectorythen copies segments with blocks unchanged (SortingCodecReader.wrapaccepts a null map) instead of scrambling them (executed). On an index with a parent field the tool already fails before reordering, with and without the fix (IllegalArgumentException: its writer has no parent field), and the run left an empty commit: a possible separate defect, not triaged and not part of this report. - Blind spot it inherits. It relies on
LeafMetaData#hasBlocks, which segments written before 9.9 do not record. Those indexes cannot have a parent field; that case is a separate bug in the IndexWriter gate (LFV-429) and is not fixed here.
What the checks validated
- Stock: the test fails 2 of 2 through gradle, assertions on and off.
- With the patch: the test passes 2 of 2, assertions on and off;
TestBpVectorReorderer,TestBPReorderingMergePolicyandTestBPIndexReorderer: 19 tests, 0 failures, 2 skipped (@Nightly); the first two with-Ptests.iters=20(20 draws of the policy test's randomized reorderer choice; the log does not record which reorderer each drew): 220 tests, 0 failures, 40 skipped. The policy test's only reorder-on-merge tests are@Nightly; with-Ptests.nightly=true -Ptests.iters=5the three suites give 95 tests, 0 skipped, 0 failures (fact check). :lucene:misc:test: 24 classes, 317 tests, 0 failures, 9 skipped, with assertions on and off;:lucene:misc:check -x testpasses.- Standalone harness with the patched class: the block-join variant (0 of 8 wrong, 3 kNN hits; with this doc's
fix/srcin the fact check, and with the finder's code-identical sketch earlier), the devil's advocate's layout variants (4/4) and block-join symptom test (1/1), and the security probe (7/7) pass. The independent reproducer's test fails 2 of 4 only on its own precondition that the merge was reordered (expected:<[tru]e> but was:<[fals]e>), as expected for a fix that skips reordering. - Not executed with the patch:
addIndexes(CodecReader...). By code reading it is covered too: the merged view ORshasBlocksover its inputs (SlowCompositeCodecReaderWrapper.java:78-94) andaddIndexesReaderMergeskips a null map (IndexWriter.java:3561-3565).
Partition whole blocks instead of documents, as BPIndexReorderer does: give all vectors of a block the block's bias (or use one representative per block, such as the centroid of its vectors), sort with blocks as units, and keep documents without a vector inside their block.
Trade-offs
Keeps vector reordering for nested indexes, but it is real work: both the bisection and the "no value at the end" step must change, blocks without any vector need a rule, and ordering quality would need a benchmark.
In IndexWriter.mergeMiddle and addIndexesReaderMerge, when a parent field is set and the merged view has blocks, verify the returned map against the live documents: every live child must keep the same first live parent after it, and the live documents of a block must stay together and in order; otherwise ignore the map or throw. It also needs a rule for trailing children without a parent, which can occur (LFV-417). The simple predicate "for each non-parent document d, oldToNew(d) + 1 == oldToNew(d + 1)" is enough only for complete blocks without deletions: moving a deleted child breaks it without harm, a block whose parent is deleted can pass it and still leave its live children orphaned, and a trailing child has no d + 1.
Trade-offs
Protects every OneMerge#reorder implementation, custom ones too, for O(maxDoc) per reordered merge. Ignoring the map hides a reorderer's bug; throwing fails the merge. A core change, so it needs core review; combine it with A or B rather than replacing them.
State in the BpVectorReorderer and BPReorderingMergePolicy javadoc that indexes with blocks are not supported.
Trade-offs
Leaves IndexWriter breaking its own setParentField guarantee without any error. Either way, the BPReorderingMergePolicy javadoc should stop naming only BPIndexReorderer.
Once a parent field is set IndexWriter trusts the reorder map, and block joins find a child's root only by position, so children moved by the merge fall under other roots: an application that extends a root's visibility or delete to its children with ToChildBlockJoinQuery then reaches the wrong children. Affected indexes must be reindexed.
Code reading. While a hunt unit modelled the doc maps that index sorting builds with document blocks, the finder, an AI tool, read every place where IndexWriter applies a doc map and saw that the merge-time reorder path has no block check and that BpVectorReorderer ignores the parent field. A JUnit on the stock jar confirmed it.
The method in full139 words
Method: code reading (the hunt's non-formal tier).
- A hunt unit (index sorting, 2026-10-25) was building an Alloy model of the doc maps that index sorting builds with document blocks. Reading every place where IndexWriter applies a doc map, the finder saw that
mergeMiddleappliesSortingCodecReader.wrap(mergedView, docMap, null)with no block check, and thatBpVectorReorderernever reads the parent field. A JUnit on the stock jar confirmed it. The Alloy model has no reorderer and gave no signal for this bug. - Before the candidate was written, an independent reviewer from another model family (OpenAI GPT-5.6, refute lens) reproduced it with its own 48-block test. Then an independent reproducer, a second GPT-5.6 reviewer with an "intended" lens, a devil's advocate and an adjudication confirmed it, and later stages bisected the introducing commit, searched for prior reports and probed the security angle.
No prior report found (2026-10-26): 82 recorded searches and checks over GitHub, commits, downstream code, four mailing lists, the JIRA archive and the web, the changed files of all 412 open PRs and of 1,946 recent closed PRs, and the introducing PR read in full. Three open PRs touch the same files without fixing it.
Searches4 searches
No issue, PR, review comment, list thread, JIRA entry or downstream report describes this bug, and no merged or open change fixes it. JIRA predates the feature, so empty JIRA results are expected; GitHub search is phrase-like and was rate-limited at times; private security reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26 | 12 queries from the prior-report tool plus 16 manual searches (BpVectorReorderer, BPReorderingMergePolicy, reorder blocks parent, hasBlocks reorder, ToParentBlockJoinQuery reorder, DiversifyingChildren reorder, ...); every hit for the two class names read | only related items: #14097 (introduced it), #13123, #13125, #13128 (2024 block work), #12622, #14117 |
| Open and recent PRs | 2026-10-26 | changed-file scans of all 412 open PRs and of 1,946 closed PRs created since 2025-01-01, filtered on the reorderer files and SortingCodecReader; diffs of #15430, #16571 and #15285 grepped for parent or block handling | #15430 (moves BpVectorReorderer to core), #16571 (skips reordering for partitioned merges) and #15285 (Separate Panama and Vector classes: vector-API plumbing in BpVectorReorderer and its test) touch the same files; none fixes it. #16879 and #16847 touch only SortingCodecReader |
| Threads read in full | 2026-10-26 | #14097 (33 texts) and its unmerged predecessor #13683, grepped for block, parent, child, nested and join; 31 other related issues and PRs | no discussion of document blocks |
| Mailing lists, JIRA archive, downstream code, web | 2026-10-26 | 20 tool queries over dev@, java-user@, issues@ and commits@ plus 12 direct archive searches; 4 JIRA queries; 2 tool downstream queries plus class-name searches in Elasticsearch, OpenSearch, the OpenSearch k-NN plugin and Solr; 6 web queries | no report; downstream items name the classes only for other topics |
Introduced by #14097 (merged 2025-01-07), which added BpVectorReorderer and let the policy take it; it has broken blocks ever since. Main only: no release contains the class, and the 10.x branches do not have it. 9.10.0 had the same failure with the postings reorderer, fixed in 9.11.0.
Versions tested7 rows
Introduced by #14097, "HNSW BP reordering" (msokolov; approved by jpountz; merged 2025-01-07 as 02a09f6ab12 on main). It added BpVectorReorderer, IndexReorderer and AbstractBPReorderer and widened the BPReorderingMergePolicy constructor from BPIndexReorderer to IndexReorderer, inside the IndexWriter gate that #13128 (4a3f4cfe974, 2024-03-29) had made conditional on a parent field. Later commits to the file (#14117, a split heuristic; #16255, comment typos) add no block handling. CHANGES.txt lists #14097 under 11.0.0; no release tag contains the commit, and branch_10x, branch_10_6 and branch_10_5 have neither BpVectorReorderer nor IndexReorderer (read). The same failure class existed once before: on 9.10.0 the policy with the postings reorderer broke blocks the same way, and 9.11.0 fixed that with #13125 and #13128 (executed with a release control variant).
| Version or commit | Verdict | Result |
|---|---|---|
| 9.12.3, 10.1.0, 10.2.0, 10.5.2 | not affected | no BpVectorReorderer: the repro does not compile; the policy with BPIndexReorderer keeps blocks |
main 72d93de50c2 (parent of #14097) | bisect | the repro does not compile; the policy keeps blocks |
main 02a09f6ab12 (#14097) | first affected | the finder's test fails with assertions on and off; passes with the conservative fix |
main b7c7fe064ce (#14117) | affected | fails the same way |
main ea57360912ed | affected | this doc's test fails 2 of 2 (gradle and harness, assertions on and off); the join variant 6 of 8 wrong |
main a9779ef3c193 (2026-10-26) | affected | the finder's test still fails (main refresh) |
| main + fix A | not affected | the test, the closest suites and the whole misc module pass |
Every check says real: a refute review and an "intended" review by OpenAI GPT-5.6, an independent reproducer, a devil's advocate and an adjudication, each with its own runs and controls. This doc re-ran a new 4-block test on main through gradle and the harness, and validated the fix on the whole misc module.
Checks and verdicts428 words
All checks ran on main ea57360912ed, with assertions on and off, and with the fixed seeds recorded in their result files.
| Check | Verdict | What it found |
|---|---|---|
| Finder (a hunt unit) | candidate | Its 64-block test fails 1 of 2 with assertions on and off ('child b13c0 ended up in the block of parent b0p'); the BPIndexReorderer control passes; its fix sketch passes 2/2 and 9 upstream tests. |
| Refute review (OpenAI GPT-5.6) | real (high) | Own 48-block test fails 1 of 3 with assertions on and off; the ordinary-merge control and the same policy without a parent field (reorder skipped) pass; the fix passes 3/3 and 9/9 component tests. |
| Independent reproducer | real | Own test from the claim only: 2 of 4 fail; ToParentBlockJoinQuery wrong for 59 of 60 children (child-only vectors) and 56 of 60 (every document); both controls pass; 10.5.2 does not compile it. |
| 'intended' reviewer (OpenAI GPT-5.6) | real (high); intended case moderate, 'weak' in its notes | Own test fails 1 of 3 with assertions on and off and passes with the fix; a verifier variant (minPartitionSize 2) broke 64 of 64 blocks in every vector layout. |
| Devil's advocate | real | No intended, misuse or unreachability argument survives; default settings break 46 of 48 documents (child-only vectors) and 48 of 48 (parent-only); 15 of 16 children joined to b0p; diversified kNN k=3 returns 1 hit. |
| Adjudication | confirmed (high), silent wrong results | Re-ran the independent reproduction's test with fresh seeds (2 of 4 fail each, assertions on and off) and the finder's test with another seed; the fix sketch passes. |
| History, prior reports, security | main only since #14097; new; out of scope of SECURITY.md | Bisect of #14097 and its parent, release controls, 82 recorded prior-report searches and checks with nothing found, and a security probe on main (assertions on and off) with the fix passing 7 of 7. |
| This doc | re-run and fix validated | New 4-block test fails 2 of 2 on main through gradle (assertions on and off) and the harness (4 seeds); the join variant shows 6 of 8 wrong joins and 1 kNN hit; the fix passes it, the closest suites and the whole misc module. |
| Fact checks of this doc | bug holds; precision fixes applied | An independent fact check re-ran the test (gradle, the harness and a new seed), the join variant and the fix (including the @Nightly policy tests: 95 tests, 0 failures), and found that with vectors on every document default settings break blocks only from 64 vectors. |
Finding record, Lucene and upstream sources and repro files1,303 words
- Registry title
- Merges through BPReorderingMergePolicy with BpVectorReorderer break document blocks even when a parent field is configured
- Area
- misc / BP merge-time reordering
- Priority
- Medium, rank 100: Every merge that BPReorderingMergePolicy with BpVectorReorderer reorders silently scrambles document blocks, so block joins return wrong parents, but it is unreleased (main only) and an expert setup with no known user.
- Impact class
- silent wrong results
- Impact note
- Applications that opt into the expert lucene/misc BPReorderingMergePolicy with a BpVectorReorderer on an index that has document blocks and IndexWriterConfig#setParentField (and no index sort): every merge the policy reorders silently scrambles the merged segment's blocks, whichever documents carry the vector (children only, parents only or all) and with default reorderer settings, and block-join queries then return the wrong parents and children; ordinary scale (16 blocks suffice); unreleased main (11.0.0) only.
- Summary
- You index documents in blocks (children followed by their parent) with IndexWriterConfig#setParentField, and use BPReorderingMergePolicy with a BpVectorReorderer over a float vector field. You expect merges to keep every block together with its parent last, because IndexWriter only lets a merge reorder documents when the parent field identifies the blocks, and setParentField promises that IndexWriter does not alter the order of a block. Instead BpVectorReorderer moves each document on its own and puts every document without a vector after all documents with one, so the merged segment's blocks are silently scrambled and ToParentBlockJoinQuery joins children to the wrong parent (59 of 60 children in one run), with no exception and with assertions on or off.
- Introduced
- 02a09f6ab124 (#14097 'HNSW BP reordering', main) [read: git log --follow and git blame on BpVectorReorderer.java; exec: the repro fails at 02a09f6ab12 with -ea and -da, passes there with the conservative fix sketch shadowed, and does not compile at its parent 72d93de50c2, where BPReorderingMergePolicy accepts only the block-aware BPIndexReorderer and keeps blocks]; #14097 added BpVectorReorderer without block handling and widened the BPReorderingMergePolicy constructor from BPIndexReorderer to IndexReorderer, inside the IndexWriter reorder gate that #13128 (4a3f4cfe974, 2024-03-29) had made conditional on a parent field; not a regression of BpVectorReorderer, which has broken blocks since it was added (2025-01-07)
- First affected version
- unreleased (main only, 11.0.0-SNAPSHOT; no release tag contains 02a09f6ab12)
- Affected versions
- main from 02a09f6ab12 (2025-01-07) through ea57360912ed [exec: 02a09f6ab12, b7c7fe064ce (#14117) and ea57360912ed fail the same way, with -ea and -da at 02a09f6ab12 and on main; read: BpVectorReorderer.java is byte-identical at 7cecf795236 and ea57360912ed, and its only later commits, #14117 and #16255, add no block handling]; no release is affected: 9.12.3, 10.1.0, 10.2.0 and 10.5.2 have no BpVectorReorderer or IndexReorderer, so the repro does not compile, and their BPReorderingMergePolicy (BPIndexReorderer only) keeps blocks under the same merge [exec]; branch_10x, branch_10_6 and branch_10_5 have neither class [read]; CHANGES.txt lists GITHUB#14097 under 11.0.0, so 11.0.0 ships it unless it is fixed first [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 agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, the prior-report record, all read-only: a scripted prior-report search with 12 GitHub issue/PR, 2 commit, 2 downstream-repo, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 6 web queries; 16 manual gh issue/PR searches (3 of them over 4 downstream repos); 12 direct mailing-list archive queries; changed-file scans of all 412 open PRs and of the 1,946 PRs created 2025-01-01..2026-10-26 and closed; bodies and comments of 33 related issues and PRs (28 upstream, 5 downstream) grepped for block, parent, nested and child; the finder's earlier run (25 queries) and the intended-behaviour check's 20 list queries found the same); related prior items: #14097 (introduced it, no mention of blocks), #13125 and #13128 (the block-preservation work for BPIndexReorderer and OneMerge#reorder), #13123, #12622, #14117; open #15430 (moves BpVectorReorderer to core) and #16571 (skips reorder only for partitioned merges) touch the same files without a fix
- Lucene commit tested
- main
ea57360912ed(commit of 2026-10-24; tested 2026-10-26) through gradle in a scratch worktree (assertions on and off, seed DEADBEEF) and through a standalone JUnit harness (seeds DEADBEEF, 5EED1234, CAFEBABE and 0FACE123, assertions on and off); the block-join variant with the join module (assertions on and off); the fix in the scratch worktree (the test, the closest suites, the whole misc module with assertions on and off,:lucene:misc:check -x test), removed afterwards; the verifiers' tests and the security probe with the patched class shadowed in; the 10.5.2 release jars (the test does not compile: noBpVectorReorderer). The fact check re-ran the test (gradle and harness, a new seed), the fix (adding the@Nightlypolicy tests), every-document layouts at default settings (4 to 32 blocks) and the offline tool. Earlier stages bisected the introducing commit02a09f6ab12and its parent, and the main refresh re-ran the finder's test on maina9779ef3c193(2026-10-26): still fails.
- Finding record
a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:5544-5577 (mergeMiddle: reorder only without an index sort and when every segment with blocks has a parent field, then SortingCodecReader.wrap(mergedView, docMap, null) with no block check), :3548-3566 (the same gate in addIndexesReaderMerge; not executed), :1495-1516 (addDocuments javadoc: 'merging will preserve a block'; the NOTE on offline re-sorting tools)a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:578-589 (setParentField: 'In order to maintain the API guarantee that the document order of a block is not altered by the IndexWriter a marker for parent documents is required')a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:311-325 (OneMerge#reorder javadoc, @lucene.experimental, no explicit block clause)a Lucene main checkout lucene/misc/src/java/org/apache/lucene/misc/index/BpVectorReorderer.java:578-593 (computeDocMap, never reads the parent field), :735-789 (valueMapToDocMap: 'sort docs with no value at the end')a Lucene main checkout lucene/misc/src/java/org/apache/lucene/misc/index/BPIndexReorderer.java:876-892 (builds a parents BitSet from the parent field and keeps blocks together, #13125)a Lucene main checkout lucene/misc/src/java/org/apache/lucene/misc/index/BPReorderingMergePolicy.java:34-62 (class javadoc names only BPIndexReorderer; constructor takes an IndexReorderer), :46 (minNaturalMergeNumDocs = 1), :164-200 (natural, forced, forced-deletes and full-flush merges are wrapped)a Lucene main checkout lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1227-1278 (the only parent-field-aware order check runs inside the index-sort test)a Lucene main checkout lucene/misc/src/test/org/apache/lucene/misc/index/TestBPReorderingMergePolicy.java:54 (picks BpVectorReorderer; the test never calls setParentField or addDocuments)a Lucene main checkout lucene/CHANGES.txt:158 (GITHUB#14097 under 11.0.0), :2741-2742 (GITHUB#13125: BP supported on indexes with blocks when a parent field is configured)a Lucene main checkout SECURITY.md:28 (downstream application handles untrusted input), :42 (configuration values are trusted as given), :50 (in-scope reports)
- The finding in 30 seconds
Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:5544-5577 (mergeMiddle reorder gate and SortingCodecReader.wrap)Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:580-588 (setParentField javadoc)Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BPReorderingMergePolicy.java:133-145Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BpVectorReorderer.java:208-211, 578-593, 735-789 (no 'parent' or 'block' in the file: grep)Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BPIndexReorderer.java:376-382, 450-455, 878-882
- Who is affected
Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BPReorderingMergePolicy.java:46-47, 54-57, 102-105, 136-141, 163-203 (thresholds and their defaults; javadoc suggestion; forced merges; RAM skip; wrapped merge kinds)Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BpVectorReorderer.java:208-211, 586-589, 699-724, 737-738 (no split below 2 x minPartitionSize; no field, no map; identity map when every document has a vector)Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3548-3566 (the same gate in addIndexesReaderMerge; not executed)Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1227-1278 (parent-aware check inside the index-sort test)Lucene main ea57360912ed: lucene/join/src/test/org/apache/lucene/search/join/ParentBlockJoinKnnVectorQueryTestCase.java:482Lucene main ea57360912ed: lucene/CHANGES.txt:158 (GITHUB#14097 under 11.0.0)
- Why it looks unintended
Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:580-588 (585-587: 'optional ... with index sorting'); IndexWriter.java:1499-1500, 1506-1516; MergePolicy.java:311-324; lucene/MIGRATE.md:879-880Lucene main ea57360912ed: lucene/test-framework/src/java/org/apache/lucene/tests/index/MockRandomMergePolicy.java:268-300Lucene main ea57360912ed: lucene/CHANGES.txt:2742-2743; lucene/misc/src/java/org/apache/lucene/misc/index/BpVectorReorderer.java:51-54Lucene main ea57360912ed: lucene/misc/src/test/org/apache/lucene/misc/index/TestBPReorderingMergePolicy.java:49-55; TestBpVectorReorderer.java:387; TestBPIndexReorderer.java:139-228
- Suggested fix
Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:453-460 (null map); IndexWriter.java:3548-3566, 5544-5577Lucene main ea57360912ed: lucene/misc/src/java/org/apache/lucene/misc/index/BPIndexReorderer.java:376-382, 450-455; BPReorderingMergePolicy.java:34-39, 60Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:78-94 (hasBlocks ORed over the inputs)
- History
Lucene main ea57360912ed: lucene/CHANGES.txt:158
- TestBpVectorMergeKeepsBlocks.java (JUnit test):
repro/LFV-093/src/org/apache/lucene/misc/index/TestBpVectorMergeKeepsBlocks.java(70 lines)