Skip to content

Commit 2d8f0cb

Browse files
authored
Relax Lucene Index Upgrade Policy to Allow Safe Upgrades Across Multiple Major Versions #13797 (#15012)
1 parent af9d4e5 commit 2d8f0cb

File tree

8 files changed

+380
-11
lines changed

8 files changed

+380
-11
lines changed

dev-tools/scripts/releaseWizard.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,42 @@ groups:
300300
user_input: !UserInput
301301
prompt: Enter end date of feature freeze (YYYY-MM-DD)
302302
name: feature_freeze_date
303+
- !Todo
304+
id: evaluate_min_supported_major
305+
title: Evaluate MIN_SUPPORTED_MAJOR policy
306+
types:
307+
- major
308+
description: |
309+
For major releases, evaluate whether MIN_SUPPORTED_MAJOR in Version.java should be bumped.
310+
This constant controls the relaxed index upgrade policy - when format-breaking changes occur,
311+
it should be updated to the new minimum supported version.
312+
313+
**Evaluation Checklist:**
314+
315+
1. **Review index format changes**: Have any format-breaking changes been introduced since the current MIN_SUPPORTED_MAJOR?
316+
- Codec version changes that break backward compatibility
317+
- SegmentInfo format changes
318+
- New mandatory index structures
319+
- Changes to core file formats
320+
321+
2. **Check compatibility span**:
322+
- Current MIN_SUPPORTED_MAJOR: Review Version.java
323+
- Proposed new value (if changes occurred): Should match the oldest version that can still be opened
324+
325+
3. **Update if needed**:
326+
- Modify Version.MIN_SUPPORTED_MAJOR constant in lucene/core/src/java/org/apache/lucene/util/Version.java
327+
- Update test expectations in backward compatibility tests
328+
- Document the change in CHANGES.txt with clear reasoning
329+
330+
4. **Validate the change**:
331+
- Run backward compatibility tests: `./gradlew -p lucene/backward-codecs test`
332+
- Ensure error messages are clear and actionable
333+
- Verify no unintended version restrictions
334+
**Note**: If no format-breaking changes occurred, leave MIN_SUPPORTED_MAJOR unchanged to
335+
maintain the widest possible upgrade span for users.
336+
links:
337+
- https://github.com/apache/lucene/blob/main/lucene/core/src/java/org/apache/lucene/util/Version.java
338+
- https://github.com/apache/lucene/issues/13797
303339
- !TodoGroup
304340
id: branching_versions
305341
title: Create branch (if needed) and update versions

lucene/CHANGES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ http://s.apache.org/luceneversions
77

88
API Changes
99
---------------------
10+
* GITHUB#13797: Relaxed Lucene Index Upgrade Policy to Allow Safe Upgrades Across Multiple Major Versions.
11+
The MIN_SUPPORTED_MAJOR constant in Version.java is now manually maintained instead of auto-computed as
12+
LATEST.major-1. This enables users to perform their one index upgrade across multiple major version numbers
13+
when no format breaks occur (previously limited to exactly one major version). The constant is set to 10 for
14+
Lucene 11.0.0 and will only be bumped when actual incompatible format changes are introduced. This changes
15+
reindexing frequency from "every major release" to "only when format breaks occur" while maintaining the
16+
fundamental "one upgrade per index lifetime" limitation. See MIGRATE.md for upgrade instructions. (Mark Miller)
17+
1018
* GITHUB#11023: Removing deprecated parameters from CheckIndex. (Jakub Slowinski)
1119

1220
* GITHUB#14165: TieredMergePolicy's maxMergeAtOnce parameter was removed. (Adrien Grand)

lucene/MIGRATE.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,62 @@
1919

2020
## Migration from Lucene 10.x to Lucene 11.0
2121

22+
### Relaxed Index Upgrade Policy (GITHUB#13797)
23+
24+
Starting with Lucene 11.0.0, the index upgrade policy has been relaxed to allow safe upgrades across multiple major version numbers without reindexing when no format breaks occur.
25+
26+
#### Key Changes
27+
28+
- `Version.MIN_SUPPORTED_MAJOR` is now manually maintained instead of auto-computed as `LATEST.major-1`
29+
- Set to 10 for Lucene 11.0.0, allowing indexes created with Lucene 10.x to be opened directly
30+
- Will only be bumped when actual incompatible format changes are introduced
31+
32+
#### Two-Tier Version Policy
33+
34+
1. **Index opening policy**: An index can be opened if its creation version >= `MIN_SUPPORTED_MAJOR`
35+
2. **Codec reader policy**: Segments can only be read directly if written by current or previous major version number
36+
37+
#### Upgrade Scenarios
38+
39+
**Scenario 1: No format breaks (wider upgrade span)**
40+
- Index created with Lucene 10.x can be opened directly in Lucene 11.x, 12.x, 13.x, 14.x (as long as MIN_SUPPORTED_MAJOR stays ≤ 10)
41+
- Simply open the index with the new version; segments will be upgraded gradually through normal merging
42+
- Optional: Call `forceMerge()` or use `UpgradeIndexMergePolicy` to upgrade segment formats immediately
43+
- **Important**: You still only get one upgrade per index lifetime. Once MIN_SUPPORTED_MAJOR is bumped above 10, the index becomes unopenable and must be reindexed.
44+
45+
**Scenario 2: Format breaks occur**
46+
- If a major version introduces incompatible format changes, `MIN_SUPPORTED_MAJOR` will be bumped
47+
- Indexes created before the new minimum will throw `IndexFormatTooOldException`
48+
- Full reindexing is required for such indexes
49+
50+
**Scenario 3: After using your upgrade**
51+
- Index created with Lucene 10.x, successfully opened with Lucene 14.x
52+
- The index's creation version is still 10 (this never changes)
53+
- When Lucene 15+ bumps MIN_SUPPORTED_MAJOR above 10, this index becomes unopenable
54+
- Must reindex to continue using newer Lucene versions
55+
56+
#### Upgrade Example
57+
58+
```java
59+
// Opening an index created with Lucene 10.x in Lucene 11.x+
60+
try (Directory dir = FSDirectory.open(indexPath)) {
61+
// This will now succeed (if MIN_SUPPORTED_MAJOR <= 10)
62+
try (DirectoryReader reader = DirectoryReader.open(dir)) {
63+
// Index can be read normally
64+
}
65+
// Optional: Upgrade segment formats
66+
try (IndexWriter writer = new IndexWriter(dir, config)) {
67+
writer.forceMerge(1); // Rewrites all segments to latest format
68+
}
69+
}
70+
```
71+
72+
#### Error Handling
73+
74+
Enhanced error messages will clearly indicate:
75+
- Whether the index creation version is below `MIN_SUPPORTED_MAJOR` (reindex required)
76+
- Whether segments are too old to read directly (sequential upgrade required)
77+
2278
### TieredMergePolicy#setMaxMergeAtOnce removed
2379

2480
This parameter has no replacement, TieredMergePolicy no longer bounds the

lucene/backward-codecs/src/test/org/apache/lucene/backward_index/BackwardsCompatibilityTestBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public void tearDown() throws Exception {
160160
private static Version getLatestPreviousMajorVersion() {
161161
Version lastPrevMajorVersion = null;
162162
for (Version v : getAllCurrentVersions()) {
163-
if (v.major == Version.LATEST.major - 1
163+
if (v.major == Version.MIN_SUPPORTED_MAJOR
164164
&& (lastPrevMajorVersion == null || v.onOrAfter(lastPrevMajorVersion))) {
165165
lastPrevMajorVersion = v;
166166
}

lucene/backward-codecs/src/test/org/apache/lucene/backward_index/TestBasicBackwardsCompatibility.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,10 @@ public void testFailOpenOldIndex() throws IOException {
863863
() -> StandardDirectoryReader.open(commit, Version.LATEST.major, null));
864864
assertTrue(
865865
ex.getMessage()
866-
.contains("only supports reading from version " + Version.LATEST.major + " upwards."));
866+
.contains(
867+
"This Lucene version only supports indexes created with major version "
868+
+ Version.LATEST.major
869+
+ " or later"));
867870
// now open with allowed min version
868871
StandardDirectoryReader.open(commit, Version.MIN_SUPPORTED_MAJOR, null).close();
869872
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.lucene.backward_index;
18+
19+
import static org.apache.lucene.util.Version.MIN_SUPPORTED_MAJOR;
20+
21+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
22+
import java.io.IOException;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
25+
import org.apache.lucene.document.Document;
26+
import org.apache.lucene.document.Field;
27+
import org.apache.lucene.document.TextField;
28+
import org.apache.lucene.index.DirectoryReader;
29+
import org.apache.lucene.index.IndexFormatTooOldException;
30+
import org.apache.lucene.index.IndexWriter;
31+
import org.apache.lucene.index.IndexWriterConfig;
32+
import org.apache.lucene.index.SegmentInfos;
33+
import org.apache.lucene.index.Term;
34+
import org.apache.lucene.search.IndexSearcher;
35+
import org.apache.lucene.search.TermQuery;
36+
import org.apache.lucene.search.TopDocs;
37+
import org.apache.lucene.store.Directory;
38+
import org.apache.lucene.tests.analysis.MockAnalyzer;
39+
import org.apache.lucene.tests.util.TestUtil;
40+
import org.apache.lucene.util.Version;
41+
42+
/**
43+
* Tests for the MIN_SUPPORTED_MAJOR backwards compatibility policy. Verifies that indexes created
44+
* with major version numbers >= MIN_SUPPORTED_MAJOR can be opened, while older indexes throw
45+
* IndexFormatTooOldException.
46+
*
47+
* <p>This test uses the precreated index archives to validate the policy against real historical
48+
* indexes. The test automatically adapts when MIN_SUPPORTED_MAJOR changes by using the existing
49+
* backwards compatibility testing infrastructure.
50+
*/
51+
public class TestMinSupportedMajorBackwardsCompatibility extends BackwardsCompatibilityTestBase {
52+
static final String INDEX_NAME = "index";
53+
static final String SUFFIX = "-nocfs";
54+
55+
public TestMinSupportedMajorBackwardsCompatibility(Version version, String pattern) {
56+
super(version, pattern);
57+
}
58+
59+
@Override
60+
protected void createIndex(Directory directory) throws IOException {
61+
IndexWriterConfig conf =
62+
new IndexWriterConfig(new MockAnalyzer(random()))
63+
.setUseCompoundFile(false)
64+
.setCodec(TestUtil.getDefaultCodec());
65+
try (IndexWriter writer = new IndexWriter(directory, conf)) {
66+
Document doc = new Document();
67+
doc.add(new TextField("content", "test document for version compatibility", Field.Store.YES));
68+
writer.addDocument(doc);
69+
writer.commit();
70+
}
71+
}
72+
73+
/**
74+
* Uses the standard allVersion() method to dynamically test all available versions. The
75+
* supportsVersion() override controls which versions are actually tested.
76+
*/
77+
@ParametersFactory(argumentFormatting = "Lucene-Version:%1$s; Pattern: %2$s")
78+
public static Iterable<Object[]> testVersionsFactory() {
79+
return allVersion(INDEX_NAME, SUFFIX);
80+
}
81+
82+
@Override
83+
protected boolean supportsVersion(Version version) {
84+
// Test only versions that should be supported according to MIN_SUPPORTED_MAJOR
85+
return version.major >= MIN_SUPPORTED_MAJOR;
86+
}
87+
88+
/**
89+
* Test that indexes created with versions >= MIN_SUPPORTED_MAJOR can be opened and searched. This
90+
* validates that the relaxed policy correctly allows older but supported indexes.
91+
*/
92+
public void testSupportedVersionCanBeOpened() throws IOException {
93+
// The base class setup ensures we only get here for supported versions
94+
assertTrue("Version should be supported", version.major >= MIN_SUPPORTED_MAJOR);
95+
96+
// This should succeed for any version >= MIN_SUPPORTED_MAJOR
97+
try (DirectoryReader reader = DirectoryReader.open(directory)) {
98+
// Basic validation that the index can be read
99+
assertTrue("Index should be readable", reader.numDocs() >= 0);
100+
101+
// Verify we can read the SegmentInfos
102+
SegmentInfos sis = SegmentInfos.readLatestCommit(directory);
103+
assertTrue(
104+
"Creation version should be >= MIN_SUPPORTED_MAJOR",
105+
sis.getIndexCreatedVersionMajor() >= MIN_SUPPORTED_MAJOR);
106+
107+
// Test basic search functionality if there are documents
108+
if (reader.numDocs() > 0) {
109+
IndexSearcher searcher = new IndexSearcher(reader);
110+
TopDocs hits = searcher.search(new TermQuery(new Term("content", "test")), 10);
111+
// Note: we can't guarantee the specific content, but should be able to search
112+
assertNotNull("Search should return results", hits);
113+
}
114+
}
115+
}
116+
117+
/**
118+
* Test unsupported versions by dynamically finding versions below MIN_SUPPORTED_MAJOR. This
119+
* validates that both DirectoryReader and IndexWriter properly throw IndexFormatTooOldException
120+
* for versions below MIN_SUPPORTED_MAJOR.
121+
*/
122+
public void testUnsupportedVersionsThrowException() throws IOException {
123+
// Find all versions that should be unsupported
124+
List<Version> allVersions = getAllCurrentReleasedVersionsAndCurrent();
125+
List<Version> unsupportedVersions =
126+
allVersions.stream()
127+
.filter(v -> v.major < MIN_SUPPORTED_MAJOR)
128+
.collect(Collectors.toList());
129+
130+
if (unsupportedVersions.isEmpty()) {
131+
// If there are no unsupported versions (e.g., MIN_SUPPORTED_MAJOR is very low),
132+
// we can't test this behavior
133+
return;
134+
}
135+
136+
// Test each unsupported version
137+
for (Version unsupportedVersion : unsupportedVersions) {
138+
String indexName =
139+
String.format(
140+
java.util.Locale.ROOT, createPattern(INDEX_NAME, SUFFIX), unsupportedVersion);
141+
java.io.InputStream resource =
142+
TestAncientIndicesCompatibility.class.getResourceAsStream(indexName);
143+
144+
if (resource == null) {
145+
// Skip versions that don't have archives (this is expected for some older versions)
146+
continue;
147+
}
148+
149+
java.nio.file.Path tempDir = createTempDir("unsupported-" + unsupportedVersion);
150+
TestUtil.unzip(resource, tempDir);
151+
152+
try (org.apache.lucene.store.Directory dir = newFSDirectory(tempDir)) {
153+
// Test DirectoryReader fails for unsupported versions
154+
IndexFormatTooOldException readerException =
155+
expectThrows(
156+
IndexFormatTooOldException.class,
157+
() -> {
158+
DirectoryReader.open(dir);
159+
});
160+
161+
// Verify the exception message contains useful information
162+
String readerMessage = readerException.getMessage();
163+
assertNotNull(
164+
"Reader exception message should not be null for version " + unsupportedVersion,
165+
readerMessage);
166+
assertTrue(
167+
"Reader exception message should contain version information for "
168+
+ unsupportedVersion
169+
+ ": "
170+
+ readerMessage,
171+
readerMessage.length() > 0);
172+
173+
// Test IndexWriter creation also fails appropriately
174+
IndexWriterConfig config = new IndexWriterConfig();
175+
IndexFormatTooOldException writerException =
176+
expectThrows(
177+
IndexFormatTooOldException.class,
178+
() -> {
179+
new IndexWriter(dir, config);
180+
});
181+
182+
String writerMessage = writerException.getMessage();
183+
assertNotNull(
184+
"Writer exception message should not be null for version " + unsupportedVersion,
185+
writerMessage);
186+
assertTrue(
187+
"Writer exception message should contain version information for "
188+
+ unsupportedVersion
189+
+ ": "
190+
+ writerMessage,
191+
writerMessage.length() > 0);
192+
}
193+
}
194+
}
195+
196+
/**
197+
* Test that validates the constant MIN_SUPPORTED_MAJOR has the expected value. This test will
198+
* fail if someone accidentally changes the constant without considering the implications.
199+
*/
200+
public void testMinSupportedMajorConstantValue() {
201+
// This test validates that MIN_SUPPORTED_MAJOR is set to the expected value
202+
// If this test fails, it likely means the constant was changed and the
203+
// implications need to be considered (tests updated, documentation updated, etc.)
204+
assertEquals(
205+
"MIN_SUPPORTED_MAJOR should be 10 for the relaxed upgrade policy", 10, MIN_SUPPORTED_MAJOR);
206+
207+
// Additional validation: MIN_SUPPORTED_MAJOR should be <= current major
208+
assertTrue(
209+
"MIN_SUPPORTED_MAJOR should not exceed current major version",
210+
MIN_SUPPORTED_MAJOR <= Version.LATEST.major);
211+
212+
assertTrue("MIN_SUPPORTED_MAJOR should be positive", MIN_SUPPORTED_MAJOR > 0);
213+
}
214+
}

lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,19 @@ public static final SegmentInfos readCommit(
349349
if (indexCreatedVersion < minSupportedMajorVersion) {
350350
throw new IndexFormatTooOldException(
351351
input,
352-
"This index was initially created with Lucene "
352+
"Index created with Lucene "
353353
+ indexCreatedVersion
354-
+ ".x while the current version is "
354+
+ ".x is not supported by Lucene "
355355
+ Version.LATEST
356-
+ " and Lucene only supports reading"
357-
+ (minSupportedMajorVersion == Version.MIN_SUPPORTED_MAJOR
358-
? " the current and previous major versions"
359-
: " from version " + minSupportedMajorVersion + " upwards"));
356+
+ ". This Lucene version only supports indexes created with major version "
357+
+ minSupportedMajorVersion
358+
+ " or later (found: "
359+
+ indexCreatedVersion
360+
+ ", minimum: "
361+
+ minSupportedMajorVersion
362+
+ "). To resolve this issue: (1) Re-index your data using Lucene "
363+
+ Version.LATEST.major
364+
+ ".x, or (2) Use an older Lucene version that supports your index format.");
360365
}
361366

362367
SegmentInfos infos = new SegmentInfos(indexCreatedVersion);

0 commit comments

Comments
 (0)