All findings · Lucene finding · LFV-695 · indexrearranger-ignores-writer-config

IndexRearranger ignores the IndexWriterConfig it is given, except the analyzer, since 10.0.0

Since 10.0.0, IndexRearranger uses only the analyzer of the IndexWriterConfig you pass: the rearranged copy silently gets the default codec and no index sort, and a rerun with OpenMode.CREATE or an input with a soft-deletes or parent field makes it throw. A regression from #11815, in every release from 10.0.0 to 10.6.0 and on main.

Summary

Priority
Low, rank 667Since 10.0 the experimental misc IndexRearranger drops the given config's codec and index sort and fails loudly on CREATE, soft-deletes and parent-field inputs while query results stay correct, a slightly broader released sibling of LFV-618.
Impact
minor
Since
10.0.0 (#11815, merged 2022-11-22)introduced 2022-11-22 · about 3 years
Affected
every release from 10.0.0 to 10.6.0 (13) and main; all 29 releases from 8.9.0 to 9.12.3 pass
Status
documented
Found by
code reading of the misc index tools, then a public-API JUnit

AI-assisted page: this finding was first signalled by code reading or testing 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

IndexRearranger copies an index into a new segment layout and takes an IndexWriterConfig. Since 10.0.0 it writes the copy with fresh configs that keep only your analyzer, so the copy gets the defaults: default codec, no index sort, CREATE_OR_APPEND, no soft-deletes or parent field, the current creation major version.

Background

  1. The tool. IndexRearranger copies an index into a new directory with a chosen segment layout: each DocumentSelector picks the documents of one output segment. Its javadoc's use case is benchmarking a change to indexing: index the same content twice and rearrange one copy to the shape of the other.
  2. How it writes. It adds each segment with IndexWriter.addIndexes(CodecReader...), which writes the new segment with the writer's codec and index sort. Since 10.0.0 a second writer then applies requested deletes.
  3. The config. Both constructors take an IndexWriterConfig. A config can serve only one writer, so two writers need two configs.
IndexRearranger is given an IndexWriterConfig with index sort <long: "ord">, but execute() builds both of its writers with new IndexWriterConfig(config.getAnalyzer()), so the rearranged segment is written with the default codec and indexSort=null, and a writer configured with the ord sort then refuses to open it. In 9.x execute() opened its writer with the caller's config; the fix does that again for the first writer.
The config you pass, the two fresh configs execute() builds from its analyzer alone, and what the rearranged copy gets; then the fix. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You have 4 documents sorted by ord, in two segments holding ord {1, 3} and {0, 2}. You rearrange them into one segment: new IndexRearranger(in, out, new IndexWriterConfig().setIndexSort(ordSort), selectors).execute().
  2. You expect the copy to follow your config: doc order [0, 1, 2, 3] and index sort <long: "ord">, as on 8.9.0 to 9.12.3.
  3. Instead the copy has doc order [1, 3, 0, 2] and index sort null, and opening it with a writer configured with the ord sort throws IllegalArgumentException: cannot change previous indexSort=null ....
  4. Why: since 10.0.0 execute() builds both of its writers with new IndexWriterConfig(config.getAnalyzer()). Only the analyzer survives, so a configured SimpleText codec becomes the default codec, a second run with OpenMode.CREATE appends and throws, and soft-deletes or parent-field indexes are rejected.
  5. Search results on the copy stay correct and the input is only read. Using your config for the first writer again, as 9.x did, fixes it.

Who is affected

Callers of the experimental misc IndexRearranger on 10.0.0 to 10.6.0 and main: a configured codec or index sort is lost on every run; OpenMode.CREATE, soft-deletes, parent-field and creation-version settings fail in their own scenarios. The javadoc's own use case, benchmarking an indexing change, then measures the default codec. Search results stay correct; a default config is unaffected.

Mechanism, terms and numbers790 words

IndexRearranger (lucene/misc, @lucene.experimental) copies an index into a new directory with a chosen segment layout, and both public constructors take an IndexWriterConfig ("@param config index writer config"). Up to 9.12.3, execute() forced NoMergePolicy on that config and opened its writer with it. Since #11815 ("Support deletions in rearrange", first released in 10.0.0), execute() builds its two writers with new IndexWriterConfig(config.getAnalyzer()) and sets only NoMergePolicy, so every other setting is dropped, and the analyzer it keeps is never used by addIndexes(CodecReader...). The copy is silently written with the default codec and without the configured index sort; a second run with OpenMode.CREATE appends and fails, and an index with a soft-deletes or parent field, or one created with the previous major version, cannot be rearranged. Search results on the copy are correct and the input is only read. Executed on every 10.x release up to 10.6.0 and on main; all 29 releases from 8.9.0 to 9.12.3 (8.9.0 to 8.11.4 and the 21 9.x releases) pass the same assertions.

The code (main 645d4fe14a20, lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java)

  1. The configs. execute() builds createSegmentsConfig and applyDeletesConfig as new IndexWriterConfig(config.getAnalyzer()) (:119-120) and sets only NoMergePolicy on each (:122-124). Nothing else reads the caller's config, which the class keeps in a protected final field (:70).
  2. The defaults they get. Codec.getDefault(), no index sort, OpenMode.CREATE_OR_APPEND, no soft-deletes or parent field (LiveIndexWriterConfig.java:135, :138), creation major Version.LATEST.major (:58).
  3. First writer (:126): addIndexes (:209) writes each segment with the writer's codec and index sort (IndexWriter.java:3525 config.getCodec(), :3540 config.getIndexSort()), so with the default codec and indexSort=null. With no index sort the merge only maps around deletions instead of sorting (MergeState.java:214-226), so two sorted input segments are concatenated: [1, 3] then [0, 2].
  4. Field checks. addIndexes validates every incoming field against the writer's config up front (IndexWriter.java:3288-3293); an input soft-deletes field or parent field is rejected because the fresh config names none (FieldInfos.java:459-467, :484-490), and so is an input segment created with another major version than the new output (IndexWriter.java:3228-3236). These checks run in the executor task that calls addIndexes, so execute() throws them as the cause of an ExecutionException (:189-198).
  5. Open mode. A second run into an existing output appends instead of replacing, so a selector then matches two segments and finalizeRearrange throws (:154-161).
  6. Opening the copy later. A writer configured with the original sort checks every segment's sort and rejects indexSort=null (IndexWriter.java:1213-1229).

In 9.x

execute() did config.setMergePolicy(NoMergePolicy.INSTANCE) and new IndexWriter(output, config) with the caller's config (9.12.3 IndexRearranger.java:80-82).

What happens, per setting (stock main; assertions on and off unless noted)

setting in the config you passyou get
none (control)a rearranged copy (passes)
setIndexSort(ord), input sorted by ordindexSort=null, doc order [1, 3, 0, 2]; then new IndexWriter(out, new IndexWriterConfig().setIndexSort(ord)) throws IllegalArgumentException: cannot change previous indexSort=null ... to new indexSort=<long: "ord">
setCodec(SimpleText)written with Codec.getDefault() (Asserting under the test framework, the release's default codec outside it)
OpenMode.CREATE, second run into the same outputappends, then IllegalStateException: Document selector ... has matched more than 1 segments. Matched segments order: 0, 1
setSoftDeletesField("soft"), input has soft deletesExecutionException caused by IllegalArgumentException: this index has [soft] as soft-deletes already but soft-deletes field is not configured in IWC
setParentField("parent"), input has a parent fieldExecutionException caused by IllegalArgumentException: can't add field [parent] as parent document field; this IndexWriter has no parent document field configured
setIndexCreatedVersionMajor(10), input created with major 10ExecutionException caused by IllegalArgumentException: Cannot merge a segment that has been created with major version 10 into this index which has been created by major version 11 (on 9.12.3 a major-8 input with setIndexCreatedVersionMajor(8) is rearranged and keeps major 8)

The first two are silent; the other four fail loudly. Search results on the copy are correct, requested deletes are applied, and the input index is only read (:127). Similarity is dropped too, by code reading only. setIndexCreatedVersionMajor is the setting IndexWriterConfig documents for exactly this addIndexes case (IndexWriterConfig.java:175-177).

Where each row was run: the index sort by the minimal test on main, 10.0.0, 10.5.2 and 10.6.0 and by the hunt's tests on main and 10.0.0 to 10.5.2 (the reopen exception in the hunt's probe and in a fact-check probe on main); the codec, OpenMode.CREATE and soft deletes by the hunt's tests on main and 10.0.0 to 10.5.2; the parent field by the independent reproducer on main, 10.5.2 and 10.0.0; the creation major by a fact-check probe on main and on the 10.6.0 jars (assertions on). IndexRearranger.java is identical on 10.6.0 and main.

Exposure and likelihood283 words
  • Who. Callers of IndexRearranger who put a non-default codec, an index sort, OpenMode.CREATE, a soft-deletes field, a parent field or an older creation major in the config they pass. A configured codec or index sort is ignored on every run, with assertions on and off, at any size and with no timing condition. The other settings show only in their scenario: OpenMode.CREATE when the output already holds an index, the soft-deletes and parent fields when the input has such a field, the creation major when the input was created with an older major. A default config is not affected. The tool is experimental and in lucene/misc, so it is opt-in.
  • Silent. The copy's codec and index sort differ from the configured ones, and so does its doc order within segments. The class's own use case, testing search performance after a change to indexing, then measures the default codec instead of the configured one when the change is chosen through the config. A writer configured with the original sort cannot open the copy.
  • Loud. OpenMode.CREATE reruns, soft-deletes indexes, parent-field indexes and indexes created with the previous major fail with a clean exception. For the soft-deletes and parent fields there is no workaround through the API short of reimplementing execute() in a subclass (its helper methods are private); for the codec only the global, static Codec.setDefault.
  • Not affected. Search results on the copy, requested deletes, the input index (only read).
  • How likely: plausible. Upstream's own TestIndexRearranger and TestBinaryDocValueSelector pass a config with an index sort and OpenMode.CREATE, but the tool is little used, the codec and sort loss is invisible unless someone inspects the output, and nobody has reported it since 10.0.0.

Minimal repro

One JUnit test, deterministic, public API, no threads of its own: index one document sorted by ord, rearrange it with a config that sets the same sort, and check the output segment's index sort.

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

import org.apache.lucene.document.BinaryDocValuesField;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;

public class TestIndexRearrangerKeepsIndexSort extends LuceneTestCase {

  public void testIndexSortOfConfigIsKept() throws Exception {
    Sort sort = new Sort(new SortField("ord", SortField.Type.LONG));
    try (Directory in = newDirectory();
        Directory out = newDirectory()) {
      try (IndexWriter w = new IndexWriter(in, new IndexWriterConfig().setIndexSort(sort))) {
        Document doc = new Document();
        doc.add(new BinaryDocValuesField("id", new BytesRef("0")));
        doc.add(new NumericDocValuesField("ord", 0));
        w.addDocument(doc);
      }
      new IndexRearranger(
              in,
              out,
              new IndexWriterConfig().setIndexSort(sort),
              BinaryDocValueSelector.createLiveSelectorsFromIndex("id", in))
          .execute();
      // expected: the rearranged segment has the config's index sort; actual: null
      assertEquals(sort, SegmentInfos.readLatestCommit(out).info(0).info.getIndexSort());
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestIndexRearrangerKeepsIndexSort -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
The rearranged segment has the config's index sort, <long: "ord">.
Observed
AssertionError: expected:<<long: "ord">> but was:<null>, with assertions on and off.

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

More files, environment, full output237 words

One document, one input segment written with the same sort (the realistic case of rearranging a sorted index), and one live selector from BinaryDocValueSelector.createLiveSelectorsFromIndex. The test sits in org.apache.lucene.misc.index for locality and uses no package-private API. It starts no threads of its own; execute() runs the selector on its internal pool. newDirectory() is randomized; the test failed the same way with seeds DEADBEEF and C0FFEE.

The other symptoms (codec, OpenMode.CREATE, soft-deletes field, parent field) share the root cause and were executed in the hunt's longer tests (the creation major in a fact-check probe); they are listed in the table above and are not part of this repro.

Regression test: this same test; it passes with the recommended fix.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Also fails on the 10.6.0, 10.5.2 and 10.0.0 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is pure Java)
Command (environment)
cp $REPRO/TestIndexRearrangerKeepsIndexSort.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/index/
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestIndexRearrangerKeepsIndexSort -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed)
> Task :lucene:misc:test FAILED

TestIndexRearrangerKeepsIndexSort > testIndexSortOfConfigIsKept FAILED
    java.lang.AssertionError: expected:<<long: "ord">> but was:<null>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:3151E5276FE88533]:0)
        at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
        at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:120)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:146)
        at org.apache.lucene.misc.index.TestIndexRearrangerKeepsIndexSort.testIndexSortOfConfigIsKept(TestIndexRearrangerKeepsIndexSort.java:50)

1 test completed, 1 failed

# -Ptests.asserts=false: the same AssertionError
# with the fix applied: BUILD SUCCESSFUL (assertions on and off)

Why it looks unintended

  • Both constructors take and document the config ("@param config index writer config"); on main only its analyzer is read, and addIndexes never analyzes, so the parameter has no effect.
  • Up to 9.12.3 the writer used the caller's config; #11815, CHANGES.txt and MIGRATE.md never mention dropping it.
  • Upstream's tests still pass a config with an index sort and OpenMode.CREATE.
The full argument264 words
  • Contract. "@param config index writer config" (IndexRearranger.java:85), kept in a protected final field (:70). The class javadoc's use case is testing search performance "after a change to indexing" (:57-60).
  • The tool's author on the original contract. Before 10.0, when the tool was new, Patrick Zhai, who wrote it (LUCENE-9694), told a user on java-user (2021-05-24, "Multiple merge-runs from same set of segments") that "the IndexRearranger let you pass in an IndexWriterConfig so that you could set it there", about setIndexSort.
  • Tests. TestIndexRearranger.java:209-214 and TestBinaryDocValueSelector.java:123-128 still build the config they pass with OpenMode.CREATE, NoMergePolicy and setIndexSort(ord). TestIndexRearranger's helper is unchanged from 9.x, where those settings were applied (9.12.3 TestIndexRearranger.java:123-128); TestBinaryDocValueSelector was added by #11815 itself, with the same helper. They check segment contents and live docs, never the output's sort or codec.
  • History. #11815 needed a second writer to apply deletes, and one config cannot serve two writers (IndexWriterConfig.java:125-131). Its issue (#11814), body, comments and review comments never mention the config, codec, index sort or open mode; its only compatibility remark ("This change is technically not backwards compatible") names "changes to the rearrange API" and deleted documents no longer disappearing from the rearranged index ("They become live instead"). CHANGES.txt:2210 says only "GITHUB#11814: Support deletions in IndexRearranger."; MIGRATE.md has no entry. So the loss reads as an unintended side effect; that is an inference, the authors did not say so.
  • Precedent. PKIndexSplitter in the same package solves the same two-writer problem by taking one caller config per output (PKIndexSplitter.java:62-68, LUCENE-3296, #4369).
The devil's-advocate check133 words

The strongest case: the class is @lucene.experimental; "index writer config" promises nothing about which settings apply; even 9.x overrode the merge policy; the tool's purpose is geometry (segment membership and order), which the upstream tests check; and one config cannot serve two writers, so fresh configs were a pragmatic choice.

Why it fails: the merge-policy override is required by the algorithm and commented, while dropping everything but the one setting addIndexes never uses is a choice nobody stated; #11815 lists its incompatible changes and this is not among them; the upstream tests still pass a sort and an open mode; the javadoc's own use case needs the codec kept; and PKIndexSplitter shows the two-writer constraint has a known solution. Cross-model reviews (OpenAI GPT-5.6) and an intended-behaviour verifier rated the works-as-designed case weak.

Suggested fix

Recommended Use the caller's config for the first writer, copy the output settings into the second (validated for the listed tests)

Open the rearrange writer with the caller's config, as 9.x did (still forcing NoMergePolicy), and give the apply-deletes writer a fresh config copying codec, similarity, index sort, soft-deletes and parent field, with OpenMode.APPEND (+18/-2). The repro and all 317 misc tests pass. Validated for the listed tests only. Caution: the caller's merge scheduler then reaches addIndexes, and commitOnClose(false) needs handling.

All options and trade-offs4 options ยท 781 words

Option A was built and run; B avoids its merge-scheduler coupling; C changes the API; D is not recommended.

Use the caller's config for the first writer, copy the output settings into the second (validated for the listed tests) (recommended)

In execute() (IndexRearranger.java:119-120) replace the two fresh configs: the rearrange writer uses config itself, and the deletes writer gets a fresh config that carries over the output's settings. Of those, only the soft-deletes and parent fields must match when that writer opens the output (IndexWriter.java:1267-1278, FieldInfos.java:417-418), and an index sort, when one is set (IndexWriter.java:1213-1229); the codec and similarity are copied for consistency, as that writer only marks deletes. The rest of execute(), including setMergePolicy(NoMergePolicy.INSTANCE) on both configs, is unchanged.

// The caller's config writes the rearranged segments (as before the deletions support).
IndexWriterConfig createSegmentsConfig = config;
// A writer config can only be used once, so the deletes pass gets a fresh config that
// carries over the output's settings.
IndexWriterConfig applyDeletesConfig =
    new IndexWriterConfig(config.getAnalyzer())
        .setCodec(config.getCodec())
        .setSimilarity(config.getSimilarity())
        .setOpenMode(IndexWriterConfig.OpenMode.APPEND);
if (config.getIndexSort() != null) {
  applyDeletesConfig.setIndexSort(config.getIndexSort());
}
if (config.getSoftDeletesField() != null) {
  applyDeletesConfig.setSoftDeletesField(config.getSoftDeletesField());
}
if (config.getParentField() != null) {
  applyDeletesConfig.setParentField(config.getParentField());
}

Trade-offs

  • Merge scheduler. The first writer also takes the caller's merge scheduler. With NoMergeScheduler, addIndexes(CodecReader...) blocks since 9.4.0 (a separate bug in the registry, LFV-586); today's fresh configs avoid that only because they use the default ConcurrentMergeScheduler (LiveIndexWriterConfig.java:137). On 9.4.0 to 9.12.3 the tool had the same exposure (code reading).
  • Single use. As in 9.x, the caller's config is attached to the first writer and changed (setMergePolicy), so it, and the IndexRearranger holding it, can run execute() once (IndexWriterConfig.java:125-131; code reading, not executed). Today a second execute() into the same output fails anyway (the OpenMode.CREATE row above).
  • Other settings come back too. As in 9.x, the first writer also applies the caller's commitOnClose, deletion policy, setIndexCommit and setIndexCreatedVersionMajor (the last one lets an input created with the previous major be rearranged, as the 9.12.3 probe shows for 9.x; not run with the fix). With commitOnClose(false) the first writer rolls back on close (IndexWriter.java:1363-1368), so the rearranged segments are not committed before finalizeRearrange reads the output (code reading, not executed); the PR should handle it (for example reject it or commit explicitly) or document it.
  • Maintenance. The settings copied into the second config must be kept in step with new IndexWriterConfig settings that must match an existing index.

What the checks validated

Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-30):

  • stock: the minimal test fails with assertions on and off;
  • with the fix: the minimal test passes with assertions on and off; the closest suites TestIndexRearranger (3 tests) and TestBinaryDocValueSelector (2 tests) pass; the whole :lucene:misc:test passes (24 classes, 317 tests, 0 failures, 9 skipped); checkGoogleJavaFormat, ecjLintMain and forbiddenApisMain for misc pass.
  • Earlier, with the same change shadowed into the stock jars on 645d4fe14a20, the hunt's longer tests for the codec, OpenMode.CREATE, soft-deletes and parent-field symptoms passed (4 of 4 and 6 of 6).
  • After these runs the diff's second code comment was reworded (comment only, same +18/-2): it applies cleanly to 645d4fe14a20, and the minimal test passes with it, assertions on and off.
  • Scope: validated for these tests and settings only; it is not a general config-preservation patch.

Copy the output settings into both fresh configs

Keep two fresh configs and copy codec, index sort, soft-deletes field and parent field into both (similarity too, for consistency); the first gets the caller's open mode and creation major version, the second APPEND.

Trade-offs

Leaves the caller's config untouched and reusable and keeps the default merge scheduler (no LFV-586 exposure). Copying only these settings is a deliberate choice, not just a list to maintain: the first writer then does not do everything the caller's config would (for example its commitOnClose, deletion policy or merge scheduler), which may be what maintainers want; settings that are not copied stay silently ignored. Not run as a separate test; option A's second config is this copy (without the creation major, which only matters when the output is created).

API change: one caller config per writer, or a Supplier<IndexWriterConfig>

Like PKIndexSplitter (PKIndexSplitter.java:62-68), let the caller supply a config for each writer.

Trade-offs

Fully explicit, but changes a public (experimental) API, and callers must make the two configs agree. Not run.

Document that only the analyzer is used

Javadoc change only.

Trade-offs

Leaves soft-deletes and parent-field indexes impossible to rearrange and the codec impossible to choose, and contradicts the upstream tests and the 9.x behaviour.

Downstream note

No use of IndexRearranger was found in Elasticsearch, OpenSearch or Solr; the luceneutil benchmark's BenchRearranger passes it its full writer config, codec and index sort included (code reading only).

How it was found

While building a round-trip harness for BinaryDocValueSelector and IndexRearranger, the finder noticed that execute() reads only the analyzer from its config. A probe of the configured settings against the output segments, and a 9.x versus 10.x run of the same tests, confirmed it on the stock jars.

The method in full117 words

Method: code reading, then executed public-API tests (the hunt classes it as non-formal).

  • Probe. A small program printed the input and output segments' codec and index sort: IN _0 ... sort=<long: "ord">, OUT _0 ... sort=null; reopening with the sort, a second run and a soft-deletes input threw.
  • Release differential. Two JUnit classes on the public API (index sort and codec; OpenMode.CREATE and soft deletes) fail 4 of 4 on main and every 10.x release, and their 9.x-API variants pass on every release from 8.9.0 to 9.12.3. A commit-level A/B on identical 9.4.2 jars pins the change to #11815.
  • The minimal repro above is the index-sort test cut down to one document and one assertion.

Prior reports, history and checks

Prior reports

None found: no issue, PR, list thread or downstream report says IndexRearranger drops its config (three prior-art runs of 45 to 51 queries and every apache/lucene item naming the class, 2026-10-27; refreshed 2026-10-30). Closest: the introducing #11815 and a 2021 java-user thread on the 9.x contract.

Searches3 searches
  • #11815 / #11814 (the introducing change): the body, comments, review comments and reviews never mention the config, codec, index sort or getAnalyzer.
  • java-user, 2021-05-24, "Multiple merge-runs from same set of segments": the tool's author says the passed config is where to set the index sort; evidence of the contract before 10.0, not a report.
  • #4369 (LUCENE-3296): PKIndexSplitter takes one caller config per writer, a precedent for the fix. #6770 (LUCENE-5708): IndexWriterConfig.clone removed, which is why the second writer cannot get a clone.
  • Same bug class elsewhere (separate registry row): LFV-618, MultiPassIndexSplitter builds its parts with a bare config. Same method, other root cause: the executor is not shut down when execute() throws (separate candidates).
  • Not fixed on upstream main 2f424fd428da (2026-10-30); open PRs #14996 and #16847 touch the file but not its config handling (open PRs as of 2026-10-27).
  • Limits: keyword search; web search partly throttled; list and JIRA coverage depends on the archive search APIs; private reports cannot be searched.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene and downstream)2026-10-27three scripted prior-report searches (51, 51 and 45 queries: identifiers and error messages, symptoms and use case, bug class) plus the finder's run; the 10 apache/lucene items that ever mention IndexRearranger, read with comments; all 409 open PRs with their files; a global GitHub search; 13 rearrange items in luceneutilrelated items only (#11815, #11814, #4369, #6770); none the same bug
Mailing lists (dev@, java-user@, issues@, commits@), JIRA archive, web2026-10-2756 list queries plus manual all-time queries; JIRA-archive text queries; 4 web queries (2 more throttled)the 2021 java-user thread on the 9.x contract; no report
GitHub refresh2026-10-30apache/lucene issue and PR search for IndexRearranger sorted by creation, and for 'IndexRearranger config'; upstream main file history (GitHub API)nothing newer than #11815 (2022); 0 hits for 'IndexRearranger config'; no commit to the file after #11815 on upstream main 2f424fd428da

History

Introduced by #11815 (369a70f2894, merged 2022-11-22, main only), first released in 10.0.0. Fails on every 10.x release from 10.0.0 to 10.6.0 and on main; passes on all 8 releases from 8.9.0 to 8.11.4 and all 21 releases from 9.0.0 to 9.12.3; a commit-level A/B on identical 9.4.2 jars pins it to that commit.

Versions tested6 rows

Introduced by 369a70f2894, "Support deletions in rearrange" (#11815, closes #11814; by Stefan Vodita, reviewed by zhaih and mikemccand, merged by zhaih on 2022-11-22, main only; a reviewer wrote it was "going to only put it under Lucene 10"). git blame gives it for IndexRearranger.java:119-124, :126 and :132. The class dates from LUCENE-9694 (e4cede0e8c5, 8.9.0), which opened the writer with the caller's config.

The release sweep ran the hunt's four tests (9.x-API variants before 10.0.0, same assertions). The commit-level A/B swapped only IndexRearranger.java and BinaryDocValueSelector.java into identical 9.4.2 jars; a 9.x IndexRearranger changed only in its config handling also fails, so the tests are not vacuous. 8.10.0, 8.10.1 and 8.11.0 to 8.11.3 were run by the fact check with the same tests (4 of 4 pass each). The parent-field case was executed on main, 10.5.2 and 10.0.0 by the independent reproducer (setParentField exists since 9.10.0).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, assertions on and off (gradle and standalone); passes with the fix
10.6.0 affectedminimal test fails on the release jars, assertions on and off
10.0.0 to 10.5.2 (12 releases) affectedthe hunt's four tests fail 4 of 4; the minimal test fails on 10.0.0 and 10.5.2
369a70f2894 vs its parent, on 9.4.2 jars bisectthe commit fails 4 of 4 (assertions on and off); its parent passes
8.9.0 to 8.11.4 (8 releases) and 9.0.0 to 9.12.3 (21) not affectedthe 9.x-API variants pass, including OpenMode.CREATE and soft deletes (8.10.0 to 8.11.3 run by the fact check)
8.8.2 and older no such classno IndexRearranger (compile error)

Independent checks

Reproduced on stock main by an independent reproducer (its own six tests), cross-model reviews (OpenAI GPT-5.6, executed), a devil's-advocate verifier, an adjudication and a credibility audit with new seeds; controls with a default config pass, and the fix passed every time.

Checks and verdicts372 words

Each check ran its own tests or re-ran an earlier one on stock main; the default-config controls pass, and the same assertions pass on 9.12.3.

Each independent check and its verdict
CheckVerdictWhat it found
Finder candidateProbe: input sort=<long: "ord">, output sort=null. Two public-API classes fail 4 of 4 on main with assertions on and off; the 9.x variant passes on 9.4.2 and 9.12.3; the fix passes, and so do the upstream TestIndexRearranger and TestBinaryDocValueSelector.
Cross-model review (OpenAI GPT-5.6, refute lens, executed) real (high)Its own codec and sort tests fail on every run, with assertions on and off; the default-codec and no-sort controls and an adapted 9.12.3 test pass; the fix passes.
Independent reproducer realOwn six tests: 5 fail on main, 10.5.2 and 10.0.0 (the control passes), 50 of 50 over 10 iterations; executed the parent-field case; doc order [1, 3, 0, 2]; 9.12.3 passes 6 of 6.
Works-as-designed lens (OpenAI GPT-5.6, executed) real (high)Intended argument weak; its own tests fail 2 of 3 and 2 of 2 on main (the first also on 10.0.0) and pass with the fix; 9.12.3 passes.
Intended-behaviour devil's advocate realHigh confidence for codec and sort, medium for open mode and soft deletes; read #11814, #11815, CHANGES and MIGRATE; found the PKIndexSplitter precedent.
Adjudication confirmed (high)Re-ran the independent reproduction's tests with seed 5EEDF00D: 5 of 6 fail with assertions on and off; the control passes.
Registration check, audit and main refresh keepFresh seeds fail on main; re-verified on 645d4fe14a20; the fix passes, including the upstream misc tests (5 of 5).
Fact checks of this doc (one by OpenAI GPT-5.6) bug real; corrections madeRe-ran the gradle repro on main (fails with assertions on and off, passes with the fix), ran 8.10.0, 8.10.1 and 8.11.0 to 8.11.3 (pass), a reopen probe on main and a creation-major probe (fails on main and 10.6.0, passes on 9.12.3).
Minimal repro and this doc's runs still fails (2026-10-30)One-method test fails on main with assertions on and off (standalone and gradle), and on 10.6.0, 10.5.2 and 10.0.0; with the fix it passes and the misc module passes (317 tests, 0 failures).

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

Finding record

LFV-695, snapshot of 2026-11-06

Registry title
Since 10.0.0 IndexRearranger keeps only the analyzer from the IndexWriterConfig it is given, so the rearranged index loses the configured codec and index sort, OpenMode.CREATE is ignored, and an index with a soft-deletes or parent field cannot be rearranged
Area
misc / index tools (IndexRearranger.execute: writer configs of the rearrange pass and the apply-deletes pass)
Priority
Low, rank 667: Since 10.0 the experimental misc IndexRearranger drops the given config's codec and index sort and fails loudly on CREATE, soft-deletes and parent-field inputs while query results stay correct, a slightly broader released sibling of LFV-618.
Impact class
minor
Impact note
Hits callers of the experimental lucene/misc IndexRearranger on 10.0.0 through 10.6.0 (10.6.0 executed) and main who put a non-default setting in the IndexWriterConfig they pass, on every run, with -ea and -da and no scale or timing condition: the rearranged copy is silently written with the default codec and without the configured index sort (so its documents are not in sort order and a writer configured with that sort refuses to open it), a second run into the same output with OpenMode.CREATE appends and throws IllegalStateException, and an input with a soft-deletes or parent field is rejected with IllegalArgumentException with no workaround through the API; query results on the copy are correct and the input index is only read.
Summary
You rearrange an index with IndexRearranger and pass an IndexWriterConfig that sets, for example, a codec, an index sort, OpenMode.CREATE or a soft-deletes field, and expect the rearranged index to be written and opened with those settings, as it was from 8.9.0 through 9.12.3. Instead, since 10.0.0 execute() builds both of its writers with new IndexWriterConfig(config.getAnalyzer()) and sets only NoMergePolicy, so the output silently uses the default codec and has no index sort, a second run with OpenMode.CREATE appends and fails with IllegalStateException ('Document selector ... has matched more than 1 segments'), and an input with a soft-deletes or parent field fails with IllegalArgumentException ('soft-deletes field is not configured in IWC', 'this IndexWriter has no parent document field configured').
Introduced
369a70f28948e748ed157c0731f36c367c4a03fa ('Support deletions in rearrange', apache/lucene#11815, closes issue #11814; author @stefanvodita, reviewed by @zhaih and @mikemccand, merged by @zhaih; main only, never backported to branch_9x) [exec: commit-level A/B on identical 9.4.2 jars: IndexRearranger.java and BinaryDocValueSelector.java from 369a70f2894^ pass 4 of 4, from 369a70f2894 fail 4 of 4 with -ea and -da; a 9.x IndexRearranger with only its config handling changed the way 369a70f2894 does also fails 4 of 4 (vacuity check). read: git blame of IndexRearranger.java:114-136 on main gives 369a70f2894 for the config lines 119-124 and the writer lines 126 and 132; git log -S 'new IndexWriterConfig(config.getAnalyzer())' finds only this commit; git log -G 'new IndexWriter\(output, config\)' finds e4cede0e8c5 (added, LUCENE-9694) and 369a70f2894 (removed)] (2022-11-22)
First affected version
10.0.0 [exec: the finder's two classes fail 4 of 4 with -ea and -da; CHANGES.txt lists 'GITHUB#11814: Support deletions in IndexRearranger.' under 'Lucene 10.0.0 / API Changes']; the class itself dates from 8.9.0 (LUCENE-9694, e4cede0e8c5), and from 8.9.0 through 9.12.3 execute() opened its writer with the caller's config
Affected versions
every release from 10.0.0 through 10.5.2 and main a9779ef3c193 through 645d4fe14a2 (unreleased 11.0) [exec: the finder's two classes fail 4 of 4 on 10.0.0, 10.1.0, 10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1 and 10.5.2 (-ea; 10.0.0 and 10.5.2 also -da) and on main (-ea, -da); the independent reproduction's tests fail 5 of 6 on 10.0.0, 10.5.2 and main]; branch_10x 657014b3c57 and branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) have the same IndexRearranger.java (:119-120) [read 2026-10-29, not executed]; the bug doc's minimal test fails on the 10.6.0 release jars with -ea and -da [exec]; main 645d4fe14a2 re-run 2026-10-29: 4 of 4 fail with -ea and -da [exec]. Not affected [exec]: 8.9.0, 8.11.4 and all 21 releases 9.0.0 to 9.12.3 pass the 9.x-API variants with the same assertions (9.4.2 and 9.12.3 also -da); 8.8.2 and older have no IndexRearranger (compile error)
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-27, read-only; the prior-report record verdict new): three scripted prior-report searches (51, 51 and 45 queries: identifiers and error messages, symptoms and use case, bug class; over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web) plus the finder's earlier run (45 queries); manual GitHub searches, in which only 10 apache/lucene items ever mention IndexRearranger (#10750, #10004, #11814, #11815, #1019, #11407, #630, #633, #683, #10733), all read with comments and review comments; all 409 open apache/lucene PRs listed with their files (#14996 and #16847 touch IndexRearranger.java but not its config handling); a global GitHub search for IndexRearranger (no mention in Elasticsearch, OpenSearch or Solr; 13 rearrange items in mikemccand/luceneutil, none about codec, sort or config); the independent reproduction and the intended-behaviour check ran their own read-only GitHub searches and found nothing either. Nearest items, none the same bug: #11815 / #11814 (the introducing change; its body, comments, review comments and reviews never mention the config, codec, index sort or getAnalyzer; its only compatibility remark is about the DocumentSelector API and deletes staying in the output), a java-user thread of 2021-05-24 ('Multiple merge-runs from same set of segments') in which the tool's author tells a user that the IndexWriterConfig passed to IndexRearranger is where to set the index sort (evidence of the 9.x contract, not a report), #4369 (LUCENE-3296, PKIndexSplitter takes one caller config per writer: a precedent for the fix), #6770 (LUCENE-5708, IndexWriterConfig.clone removed: why the second config cannot be a clone). Not fixed upstream: the last commit to IndexRearranger.java on main, branch_10x and branch_10_6 is #11815. Gaps: web search partly throttled; list and JIRA coverage depends on the archive search APIs; the luceneutil observation is code reading only. Still not fixed on main 645d4fe14a2 [exec 2026-10-29], branch_10x 657014b3c57 or branch_10_6 4ae946e5a9b (IndexRearranger.java unchanged) [read].
Lucene commit tested
main 645d4fe14a20 (2026-10-28; upstream main 2f424fd428da, 2026-10-30, has no later commit to IndexRearranger.java than #11815): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner; with the fix applied it passes, and the whole misc module passes (24 classes, 317 tests, 0 failures). The minimal test also fails on the released 10.6.0, 10.5.2 and 10.0.0 jars. Earlier hunt runs (the hunt on a9779ef3c193, same IndexRearranger.java): the hunt's four tests fail on all 12 releases from 10.0.0 to 10.5.2 and pass on 8.9.0, 8.11.4 and all 21 releases from 9.0.0 to 9.12.3; the fact check ran them on 8.10.0, 8.10.1 and 8.11.0 to 8.11.3 too (all pass).

Lucene and upstream sources

Finding record
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:45-67 (at a9779ef3c193) (class javadoc: 'Copy and rearrange index according to document selectors'; example use case 'You are testing search performance after a change to indexing ... rearrange one of them to the shape of the other'; @lucene.experimental), :70 ('protected final IndexWriterConfig config'), :80-111 (both public constructors take the config; '@param config index writer config', :85)
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:113-137 (at a9779ef3c193) (execute(): :119-120 'new IndexWriterConfig(config.getAnalyzer())' for both writers, :122-124 only NoMergePolicy set, :126 rearrange writer, :127 input opened read-only with DirectoryReader.open(input), :130 finalizeRearrange, :132 apply-deletes writer)
  • a Lucene clone releases/lucene/9.12.3 lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:56 (config field), :64 ('@param config index writer config'), :80-82 (config.setMergePolicy(...); new IndexWriter(output, config): the caller's config used directly)
  • lucene/misc/src/test/org/apache/lucene/misc/index/TestIndexRearranger.java:209-214 (at a9779ef3c193) and TestBinaryDocValueSelector.java:95-100 (the upstream tests pass a config with OpenMode.CREATE, NoMergePolicy and setIndexSort(ord) and never check the output's sort or codec)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3288-3293 (at a9779ef3c193) (addIndexes up-front field-info validation) and FieldInfos.java:465, :490 (soft-deletes and parent-field checks that throw the observed IllegalArgumentExceptions) [from follow-up the security record sources]
The finding in 30 seconds
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:45-67, (at 645d4fe14a20) :70, :80-111, :113-137 (:119-124, :126, :127, :130, :132), :143-174 (:154-161), :202-210
  • lucene/core/src/java/org/apache/lucene/index/LiveIndexWriterConfig.java:135, (at 645d4fe14a20) :137, :138; IndexWriterConfig.java:125-131
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1133, (at 645d4fe14a20) :1213-1229, :3275, :3288-3293, :3491, :3525, :3540; MergeState.java:214-226; FieldInfos.java:459-467, :484-490
  • a Lucene clone releases/lucene/9.12.3 lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:56, :64, :79-83
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1009, (at 645d4fe14a20) :3228-3236, :3290; IndexWriterConfig.java:175-177; LiveIndexWriterConfig.java:58; lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:189-198
Who is affected
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:57-60, (at 645d4fe14a20) :66
  • lucene/misc/src/test/org/apache/lucene/misc/index/TestIndexRearranger.java:209-214; (at 645d4fe14a20) TestBinaryDocValueSelector.java:123-128
Why it looks unintended
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:57-60, (at 645d4fe14a20) :66, :70, :85, :119-124
  • lucene/misc/src/test/org/apache/lucene/misc/index/TestIndexRearranger.java:209-214; (at 645d4fe14a20) TestBinaryDocValueSelector.java:123-128
  • lucene/misc/src/java/org/apache/lucene/misc/index/PKIndexSplitter.java:62-68; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:125-131; lucene/CHANGES.txt:2172, :2174, :2210
  • a Lucene clone releases/lucene/9.12.3 lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:80-82
  • a Lucene clone releases/lucene/9.12.3 lucene/misc/src/test/org/apache/lucene/misc/index/TestIndexRearranger.java:123-128 (TestBinaryDocValueSelector.java absent at 9.12.3; added by 369a70f2894)
Suggested fix
  • lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:119-124; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/LiveIndexWriterConfig.java:137; IndexWriterConfig.java:126-131; lucene/misc/src/java/org/apache/lucene/misc/index/PKIndexSplitter.java:62-68
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1133, (at 645d4fe14a20) :1213-1229, :1267-1278, :1363-1368; FieldInfos.java:417-418

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 with this doc from the code at 645d4fe14a20, the 9.12.3 source, the fix diff, the minimal repro and an executed probe of the settings.

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.