All findings · Lucene finding · LFV-655 · termvectorleafreader-getmetadata-null-minversion

TermVectorLeafReader.getMetaData() always throws IllegalArgumentException, so a sorted search on the reader fails

TermVectorLeafReader, the public helper that turns one document's term vectors into a one-document reader, builds metadata that LeafMetaData's constructor rejects, so getMetaData() throws on every call. Any search with a Sort, ParallelLeafReader or addIndexes on it fails, in every release since 7.0.0.

Summary

Priority
Low, rank 662TermVectorLeafReader.getMetaData always throws a clear IllegalArgumentException, but only when this rarely used helper is passed to sorted search, ParallelLeafReader or addIndexes.
Impact
crash
Since
7.0.0 (LUCENE-7756, 2017); sorted search since 7.2.0introduced 2017-03-30 · about 9 years
Affected
7.0.0 to 10.6.0 and main; never fixed
Status
documented
Found by
a sweep that put every public LeafReader type under a ParallelLeafReader

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

Every LeafReader describes itself through getMetaData(): the Lucene version that created it, the oldest version that wrote to it, and its index sort. TermVectorLeafReader reports "created by 11, oldest writer unknown". LeafMetaData forbids an unknown oldest writer for anything created by 7 or later, so the method throws instead of returning.

Background: three pieces

  1. TermVectorLeafReader is a public highlighter class that "Wraps a Terms with a LeafReader, typically from term vectors": it turns one document's term vectors into a tiny searchable reader with maxDoc() = 1.
  2. LeafMetaData is what LeafReader.getMetaData() returns: the major version that created the index, the minimum version that wrote to it, the index sort and a block flag. Its constructor checks these values and throws IllegalArgumentException on an invalid combination.
  3. Some APIs read every leaf's metadata before they start: a search with a Sort (to see whether it can stop early), the ParallelLeafReader constructor, and IndexWriter.addIndexes (to check versions and sort).
Summary card for the TermVectorLeafReader metadata bug: one document with body = "foo" and term vectors, wrapped in a one-document TermVectorLeafReader; a search without a Sort returns doc 0, but a search sorted by doc id calls getMetaData(), which builds LeafMetaData(11, null, null, false); the constructor throws "minVersion must be set when createdVersionMajor is >= 7"; with Version.LATEST as minVersion the sorted search returns 1 hit.
The repro step by step: one document with term vectors, a TermVectorLeafReader over them, a search sorted by doc id that asks the reader for its metadata, the LeafMetaData check that rejects "created by 11, oldest writer unknown", and the one-line fix. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document whose text field body = "foo" stores term vectors, and wrap them in new TermVectorLeafReader("body", terms): a reader with exactly 1 document.
  2. Search it sorted by doc id: searcher.search(new MatchAllDocsQuery(), 1, new Sort(SortField.FIELD_DOC)). You expect 1 hit, doc 0; the same search without a Sort returns it.
  3. Instead it throws IllegalArgumentException: minVersion must be set when createdVersionMajor is >= 7. A sorted search asks each reader for its metadata, and this reader answers "created by version 11, oldest writer unknown", a pair LeafMetaData rejects for anything created by 7 or later.
  4. The arguments are constants, so the call fails every time, for any document, with assertions on or off. new ParallelLeafReader(reader) and addIndexes(SlowCodecReaderWrapper.wrap(reader)) fail the same way; MemoryIndex's reader passes Version.LATEST and works.

Who is affected

Only applications that build a TermVectorLeafReader themselves and hand it to an API that reads leaf metadata: a search with any Sort, ParallelLeafReader or addIndexes. Lucene's own highlighters are not affected. The failure is a clean exception and nothing from the reader is written. Every release since 7.0.0; rare: no public report found in nine years.

Mechanism, terms and numbers450 words

TermVectorLeafReader (highlighter module, public, @lucene.experimental) wraps one document's term vectors as a one-document LeafReader. Its getMetaData() returns new LeafMetaData(Version.LATEST.major, null, null, false) (TermVectorLeafReader.java:237-240 on main 645d4fe14a20), and the LeafMetaData constructor throws IllegalArgumentException("minVersion must be set when createdVersionMajor is >= 7") for exactly that pair (LeafMetaData.java:58-60). The arguments are constants, so the method can never return. Every API that reads leaf metadata fails on the reader: a search with any Sort, Sort.RELEVANCE included (TopFieldCollector.java:58), new ParallelLeafReader(reader) (ParallelLeafReader.java:131) and IndexWriter.addIndexes(SlowCodecReaderWrapper.wrap(reader)) (IndexWriter.java:3229). Lucene's own highlighters do not ask for the reader's metadata (code reading of both in-tree call sites; their 21 term-vector tests pass), so the code that is hit is code that uses the class directly, and it gets a clean exception. The commit that added the check (LUCENE-7756, first released in 7.0.0) gave MemoryIndex's reader the valid Version.LATEST; passing the same here fixes it, and with that one-line change the repro and the whole highlighter test suite pass.

The bug (main 645d4fe14a20)

  1. The reader. TermVectorLeafReader.java:237-240: return new LeafMetaData(Version.LATEST.major, null, null, false);. Version.LATEST is LUCENE_11_0_0 on main (Version.java:259), so this is created major 11 with a null minVersion.
  2. The invariant. LeafMetaData is a record (LeafMetaData.java:45-46) whose compact constructor, "Expert: Sole constructor. Public for use by custom LeafReader impls." (:48-49), rejects a created major in the future (:50-53) or below 6 (:54-57), and then createdVersionMajor >= 7 && minVersion == null (:58-60). The third check always fires for this reader, whatever its document.
  3. The consumers. TopFieldLeafCollector's constructor reads context.reader().getMetaData().sort() for every leaf (TopFieldCollector.java:53-59), and every IndexSearcher.search(query, n, sort) goes through it, with Sort.RELEVANCE and Sort.INDEXORDER too. ParallelLeafReader's constructor reads every sub-reader's metadata (ParallelLeafReader.java:130-131). SlowCodecReaderWrapper forwards getMetaData() (SlowCodecReaderWrapper.java:140-143), and addIndexes(CodecReader...) reads it in its up-front validateMergeReader (IndexWriter.java:3228-3229, :3288-3290).
  4. The sibling that works. MemoryIndex's reader returns new LeafMetaData(Version.LATEST.major, Version.LATEST, null, false) (MemoryIndex.java:2291-2294), as does the test framework's fake reader (QueryUtils.java:312). Of the seven new LeafMetaData( calls in the non-test sources, this is the only one that passes a literal null.

What runs and what fails

call on the 1-document readerexpectedactual
searcher.search(new MatchAllDocsQuery(), 1)1 hit1 hit (doc 0)
searcher.search(new MatchAllDocsQuery(), 1, new Sort(SortField.FIELD_DOC))1 hitIllegalArgumentException
the same with Sort.RELEVANCE or Sort.INDEXORDER1 hitIllegalArgumentException
reader.getMetaData()metadata, no sortIllegalArgumentException
new ParallelLeafReader(reader)a readerIllegalArgumentException
writer.addIndexes(SlowCodecReaderWrapper.wrap(reader))1 document copiedIllegalArgumentException; nothing from the reader added

The message is always "minVersion must be set when createdVersionMajor is >= 7", from LeafMetaData.<init>(LeafMetaData.java:59) via TermVectorLeafReader.getMetaData(TermVectorLeafReader.java:239).

Exposure and likelihood283 words
  • Who is hit. Code that constructs the reader itself (public class, public constructor) and passes it, or a FilterLeafReader that delegates metadata to it, to a metadata-reading API: a search with any Sort, new ParallelLeafReader(...), addIndexes through SlowCodecReaderWrapper, and other getMetaData() callers such as Sort.getPrimarySortField and CheckIndex (listed from code, not run). Default codec and settings; one document and one term are enough.
  • Not hit: Lucene's own highlighters. The unified highlighter builds the reader and wraps it in OverlaySingleDocTermsLeafReader, a FilterLeafReader over the real segment reader whose metadata it returns (TermVectorOffsetStrategy.java:48-50, FilterLeafReader.java:477-481). The classic highlighter builds it only for term vectors with positions and offsets (WeightedSpanTermExtractor.java:393-401) and only rewrites queries and builds span weights on it. The 21 tests of TestTokenSources, TestHighlighterPhrase and TestUnifiedHighlighterTermVec pass on unfixed main; since the method throws on every call, a reached call would fail them. This rests on code reading of the two in-tree call sites plus these tests; it is not a proof for every query type or extension.
  • Downstream. A code search found no use of the class in Solr, Elasticsearch or OpenSearch (not exhaustive).
  • Effect. An immediate IllegalArgumentException at the call. Nothing from the reader is written: addIndexes first flushes the writer's own buffered documents, as it always does (IndexWriter.java:3286), and then its up-front validation throws before anything from the reader is added (:3288-3290). Only an Error is treated as tragic there (:3390-3392), so the writer stays open. No wrong results, no hang.
  • How likely: rare. The class is a highlighter helper few applications build directly, and the documented searches found no public report since 7.0.0 (September 2017).

Minimal repro

One JUnit test in the highlighter module, public API only: one document with term vectors, a TermVectorLeafReader over them, one search sorted by doc id. The test body makes no random calls; in-memory directory, single thread. It fails the same way with assertions on and off and with every seed tried.

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

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestTermVectorLeafReaderSortedSearch extends LuceneTestCase {

  public void testSortedSearch() throws Exception {
    FieldType withVectors = new FieldType(TextField.TYPE_NOT_STORED);
    withVectors.setStoreTermVectors(true);
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
      Document doc = new Document();
      doc.add(new Field("body", "foo", withVectors));
      w.addDocument(doc);
      try (DirectoryReader r = DirectoryReader.open(w)) {
        TermVectorLeafReader reader =
            new TermVectorLeafReader("body", r.termVectors().get(0, "body"));
        IndexSearcher searcher = new IndexSearcher(reader);
        int hits;
        try {
          hits =
              searcher.search(new MatchAllDocsQuery(), 1, new Sort(SortField.FIELD_DOC))
                  .scoreDocs
                  .length;
        } catch (IllegalArgumentException e) {
          throw new AssertionError("expected 1 hit from a doc-sorted search, got " + e, e);
        }
        assertEquals(1, hits);
      }
    }
  }
}
Command
cp $REPRO/TestTermVectorLeafReaderSortedSearch.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/highlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests TestTermVectorLeafReaderSortedSearch -Ptests.seed=DEADBEEF
# also with -Ptests.asserts=false: the same failure
Expected
1 hit (doc 0), the same as the search without a Sort.
Observed
IllegalArgumentException: minVersion must be set when createdVersionMajor is >= 7 from TermVectorLeafReader.getMetaData (main, 10.6.0, 10.5.2 and 9.12.3; assertions on and off).

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

More files, environment, full output242 words

TestTermVectorLeafReaderSortedSearch (62 lines with the license header, one test method, package org.apache.lucene.search.highlight). ByteBuffersDirectory, a plain IndexWriterConfig(new StandardAnalyzer()), one document whose field body = "foo" stores term vectors (no positions or offsets needed). The test body makes no random calls. LuceneTestCase still randomizes the default codec, locale, time zone and similarity from the seed (SetupAndRestoreStaticEnv.java:144-230); the failure depends on none of them, and it failed with every seed tried. The catch only turns the exception into a message that states expected versus actual; a plain assertEquals(1, searcher.search(...).scoreDocs.length) that lets the exception propagate works as well.

The suggested regression test is this same test; it passes with the recommended fix.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also the 10.6.0, 10.5.2 and 9.12.3 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is plain Java)
Command (environment)
cp $REPRO/TestTermVectorLeafReaderSortedSearch.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/highlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests TestTermVectorLeafReaderSortedSearch -Ptests.seed=DEADBEEF

Observed output

# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF (-Ptests.asserts=true; =false prints the same)
TestTermVectorLeafReaderSortedSearch > testSortedSearch FAILED
    java.lang.AssertionError: expected 1 hit from a doc-sorted search, got java.lang.IllegalArgumentException: minVersion must be set when createdVersionMajor is >= 7
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:FFA37021B6AA4628]:0)
        at org.apache.lucene.search.highlight.TestTermVectorLeafReaderSortedSearch.testSortedSearch(TestTermVectorLeafReaderSortedSearch.java:56)

        Caused by:
        java.lang.IllegalArgumentException: minVersion must be set when createdVersionMajor is >= 7
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.index.LeafMetaData.<init>(LeafMetaData.java:59)
            at org.apache.lucene.search.highlight.TermVectorLeafReader.getMetaData(TermVectorLeafReader.java:239)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.TopFieldCollector$TopFieldLeafCollector.<init>(TopFieldCollector.java:58)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.TopFieldCollector$SimpleFieldCollector$1.<init>(TopFieldCollector.java:200)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.TopFieldCollector$SimpleFieldCollector.getLeafCollector(TopFieldCollector.java:199)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.IndexSearcher.searchLeaf(IndexSearcher.java:836)
            ...
            at org.apache.lucene.search.highlight.TestTermVectorLeafReaderSortedSearch.testSortedSearch(TestTermVectorLeafReaderSortedSearch.java:52)

# with the recommended fix: the test passes (assertions on and off), and the whole highlighter suite passes

Why it looks unintended

  • LeafReader.getMetaData() is an abstract method, "Return metadata about this leaf", with no throws clause; the class and its constructor are public.
  • The method can never succeed: its arguments are constants that the LeafMetaData constructor rejects.
  • The 2017 commit that added the check gave the sibling MemoryIndex reader valid metadata (Version.LATEST); this reader got null.
The full argument188 words
  • Contract. LeafReader.java:526-531: public abstract LeafMetaData getMetaData(), "Return metadata about this leaf", @lucene.experimental, no throws clause. TermVectorLeafReader is a public class with a public constructor, whose javadoc reads "Wraps a Terms with a LeafReader, typically from term vectors" (TermVectorLeafReader.java:52-61), so a sorted search on it is ordinary LeafReader use. The expected behaviour follows from the abstract method and the LeafMetaData invariant; it is not stated word for word.
  • History. LUCENE-7756 (23b002a0fdf, 2017) added LeafMetaData with the check, wrote this null call, and gave MemoryIndex the valid Version.LATEST in the same commit. The strict check was asked for in review (migrated issue #8807: throw "if minVersion is null but major version is >= 7"); the thread never mentions TermVectorLeafReader. That this caller was overlooked is an inference.
  • No test. No test in the repository uses TermVectorLeafReader directly or expects "minVersion must be set", so no test calls this method directly.
  • Context only. The LeafMetaData javadoc says minVersion may be null "if this information is not available" (LeafMetaData.java:30-31), but the constructor allows that only with created major 6 ("unknown", :27-29).
The devil's-advocate check241 words

The intended-behaviour checker and the cross-model reviews built the strongest case: the class and the method are @lucene.experimental; the class is a minimal highlighter helper with stub methods (getNormValues returns null with the comment "Is this needed?", null cache helpers, empty stored fields); Lucene's own callers avoid its metadata; and DocValuesLeafReader also throws from getMetaData() (DocValuesLeafReader.java:93-95).

Why it fails: the other stubs return values their contracts allow, and none throws; DocValuesLeafReader is package-private (:25), used only inside the indexing chain and throws UnsupportedOperationException by design, whereas this is a public reader whose one metadata method trips a constructor invariant on every call; @lucene.experimental allows API change, not a method that can never succeed; and the same commit gave MemoryIndex valid metadata. The cross-model reviews rated the argument weak, and the intended-behaviour check rejected it. What it does establish is narrow reach, hence low priority.

A dissenting rating: the cross-model fact check of this doc (OpenAI GPT-5.6, read-only) rated the case moderate, not weak. Its reasons: an experimental, minimal helper; term-vector Terms that are meant to be consumed only on the thread that acquired them (TermVectors.java:26-27); little practical value in sorting a one-document reader; and DocValuesLeafReader as a reader that throws by design. It still judged the bug a real, low-priority claim, not noise, and agreed with an ordinary public issue or small PR that leads with the direct call and the sorted search.

Suggested fix

Recommended Pass Version.LATEST as minVersion, as MemoryIndex does

Pass Version.LATEST as the minimum version, as MemoryIndex does: a one-line change in TermVectorLeafReader.getMetaData(). That is the convention Lucene's other synthetic readers already follow. Validated: the repro passes with assertions on and off, and so do all 723 tests of the highlighter module.

   @Override
   public LeafMetaData getMetaData() {
-    return new LeafMetaData(Version.LATEST.major, null, null, false);
+    return new LeafMetaData(Version.LATEST.major, Version.LATEST, null, false);
   }
All options and trade-offs3 options ยท 522 words

One fix was built and validated; two alternatives were considered and not built.

Pass Version.LATEST as minVersion, as MemoryIndex does (validated by the checks) (recommended)

One line in TermVectorLeafReader.getMetaData() (TermVectorLeafReader.java:239, 1 file, +1/-1). The reader then reports "created by and only written by this version" and no index sort, like MemoryIndex's reader (MemoryIndex.java:2293). Sorted searches and ParallelLeafReader accept it, and addIndexes gets past its metadata check (see the trade-offs).

   @Override
   public LeafMetaData getMetaData() {
-    return new LeafMetaData(Version.LATEST.major, null, null, false);
+    return new LeafMetaData(Version.LATEST.major, Version.LATEST, null, false);
   }

Trade-offs

  • Version.LATEST is a convention, not recorded provenance. The constructor gets only a field name and a Terms (TermVectorLeafReader.java:61), so the reader has no version input of its own, and its term vectors may come from a segment an older version wrote. It is the convention MemoryIndex and the test framework's QueryUtils reader already use, and the created major is already Version.LATEST.major. No other trade-off is known.
  • With it, addIndexes gets past validateMergeReader, which checks the created major against the index's and requires a non-null minVersion for major >= 7 (IndexWriter.java:3228-3243). The document was copied in the earlier verifiers' test, which uses SerialMergeScheduler, and in the diagram probe, a plain main() with the default IndexWriterConfig. Term-vector Terms are meant to be consumed only on the thread that acquired them (TermVectors.java:26-27), so addIndexes is a secondary path, not a general promise.
  • An upstream test of the addIndexes path (not part of the repro) should use SerialMergeScheduler: with the fix, the test framework's AssertingTerms rejected the Terms being consumed on a ConcurrentMergeScheduler thread in the first fixed run of the cross-model reviews.

What the checks validated

  • Scratch worktree of 645d4fe14a20, gradle --no-daemon, OpenJDK 25.0.3, seed DEADBEEF: the repro test fails on the stock tree with assertions on and off, and passes with the fix with assertions on and off.
  • With the fix, :lucene:highlighter:test with assertions on: 35 suites (the repro included), 723 tests, 0 failures, 0 errors.
  • :lucene:highlighter:checkGoogleJavaFormat passes with the fix and the test in the tree.
  • Earlier, on main a9779ef3c193: the finder's, the independent reproduction's, the cross-model review's and the intended-behaviour check's own tests all pass with the same change shadowed (the independent reproduction 6 of 6 covering the direct call, ParallelLeafReader, INDEXORDER-sorted search and addIndexes).

Report created major 6 ("unknown") with a null minVersion

The LeafMetaData javadoc treats created major 6 as "unknown" (LeafMetaData.java:27-29), and the constructor accepts it with a null minVersion. Not built.

Trade-offs

addIndexes would then reject the reader: validateMergeReader throws when the index's created major differs from the reader's (IndexWriter.java:3230-3236). So it trades one exception for another on that path.

Relax the LeafMetaData check

Allow a null minVersion for created major >= 7, as the javadoc wording "or null if this information is not available" suggests (LeafMetaData.java:30-31). Not built.

Trade-offs

Broader and against the review that asked for the strict check; IndexWriter.java:3238-3242 separately rejects a null minVersion for created major >= 7 ("hides it"), so addIndexes would still fail.

Downstream note

No use of TermVectorLeafReader was found in Solr, Elasticsearch or OpenSearch; an application that builds one gets a clean exception from its own call, which no document or query can cause.

How it was found

A hunt checking that ParallelLeafReader behaves as if its sub-readers were one index wrapped each public LeafReader type in the source tree in turn. Seven of eight types worked; TermVectorLeafReader failed as soon as the parallel reader was built. The failure was then reduced to a public-API JUnit test.

The method in full112 words

Method: non-formal (code reading plus execution; the hunt's hunt unit for ParallelLeafReader "as if one index" metamorphic checks, 2026-10-27, main a9779ef3c193).

  • Reader sweep. One leg of the plan wrapped each public LeafReader type as a ParallelLeafReader sub-reader: SegmentReader, the soft-deletes and exitable leaves, SlowCodecReaderWrapper, SortingCodecReader, FieldFilterLeafReader, MemoryIndex's reader and TermVectorLeafReader, each with construct, identity-dump and addIndexes-copy checks, with assertions on and off. Only TermVectorLeafReader failed, at construction, with the minVersion exception.
  • Reduction. The lead from the plan's code reading was reduced to a public-API JUnit (direct getMetaData(), then ParallelLeafReader and a sorted search), and later minimized to the one-method sorted-search test shown above.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-27, refreshed 2026-10-30): GitHub issues, PRs, commits and code, the downstream trackers, the mailing lists, the JIRA archive, the web and the changed files of 409 open Lucene PRs. Upstream main and branch_10_6 still have the line.

Searches4 searches

Nearest items, none a report of this bug: #8807 (LUCENE-7756, the introducing issue; it discusses a null minVersion only for pre-7.0 segments); #12685 and #13328 (edited the line or LeafMetaData and kept both the null and the check); SOLR-11406 (a different null-minVersion failure, pre-7.0 segments in SlowCompositeReaderWrapper, fixed in 2017); open PRs #13525, #13342 and #16847 touch TermVectorLeafReader.java for API changes and leave getMetaData as is. These searches show only that no report turned up in them. Gaps: keyword search only; GitHub does not index PR diffs; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits; downstream trackers; the web2026-10-27a scripted prior-report search, two runs, 96 queries (the class, the method, both error strings, sort, exception, ParallelLeafReader, LeafMetaData); manual GitHub searches in all repositories for the class name and both error stringsonly #7096, #8490 and #8007 mention the class, none about metadata; the error strings match no Lucene item
GitHub code search; open PRs2026-10-27TermVectorLeafReader everywhere and inside the Solr, Elasticsearch and OpenSearch repositories; the changed files of 409 open apache/lucene PRsno downstream use; 3 open PRs touch the file without changing getMetaData; none touches LeafMetaData.java
Mailing lists and JIRA archive2026-10-27dev@, java-user@, issues@, commits@ and six more Lucene and Solr lists; the SOLR, LUCENE and LUCENENET JIRA archiveonly the commit mails of the commits that wrote or edited the line; both error strings: 0 hits
GitHub (re-check for this doc)2026-10-30 (three times)apache/lucene: TermVectorLeafReader, "minVersion must be set", TermVectorLeafReader getMetaData, LeafMetaData minVersion; items updated since 2026-10-29; the file on main and branch_10_6nothing new; main 2f424fd428da (line 239) and branch_10_6 91eaab4fb298 (line 228) still pass null

History

Introduced in 7.0.0 by LUCENE-7756 (23b002a0fdf, 2017), the commit that added LeafMetaData with the check and wrote this null call; sorted search became a trigger in 7.2.0 (LUCENE-8059). Unchanged since, apart from an appended hasBlocks=false in 9.9.0. 6.x is not affected: the method did not exist.

Versions tested6 rows

Introduced by LUCENE-7756 "Only record the major Lucene version that created the index, and record the minimum Lucene version that contributed to segments" (23b002a0fdf2, Adrien Grand, authored 2017-03-30, committed 2017-04-04; JIRA patch era, migrated as #8807). It replaced TermVectorLeafReader.getIndexSort() with getMetaData() { return new LeafMetaData(Version.LATEST.major, null, null); }, added LeafMetaData with the check, and gave MemoryIndex Version.LATEST. git tag --contains puts it in 7.0.0 and in no 6.x release.

Sorted search became a trigger in 7.2.0 with LUCENE-8059 "Fold early termination support into TopFieldCollector" (812711416d9 on master, e4e7d12f5d8 on branch_7x, 2017-11-29), which made TopFieldCollector read every leaf's metadata. The only later edit to this call, #12685 (6677109ee60 on main, backported to 9.x as 26043819a5b, first released in 9.9.0), appended hasBlocks=false and kept the null; the 9.8.0 tag still has the three-argument call.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 and a9779ef3c193 affectedthe one-method test fails (gradle and the standalone runner, assertions on and off); direct call, ParallelLeafReader and addIndexes fail too
10.6.0, 10.5.2, 9.12.3 affectedthe one-method test fails (assertions off)
7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.0.0 affecteddirect call, ParallelLeafReader, sorted search and addIndexes all throw (version-independent probe)
7.0.0, 7.1.0 sorted search OKdirect call, ParallelLeafReader and addIndexes throw; a sorted search still works
6.5.0, 6.6.6 method absentno getMetaData() (its predecessor getIndexSort() returned null); all consumers work
main + fix not affectedthe test passes, and so do all 723 highlighter tests

Independent checks

Verifiers (an independent reproducer, a cross-model review by OpenAI GPT-5.6 and an intended-behaviour devil's advocate), an earlier executing GPT-5.6 review, the adjudication and a credibility audit all rate it real. This doc re-ran the repro on 645d4fe14a20 and validated the fix with gradle; fact checks of the doc confirmed it and led to corrections.

Checks and verdicts397 words

The cross-model reviews ran with code execution and wrote their own tests. Versions older than 9.12.3 were run by the finder and the history probe only, not by a verifier. Not run on x86 (plain Java logic).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateReader sweep: of 8 public reader types only TermVectorLeafReader fails. JUnit 2 of 2 fail (-ea, -da), also on 10.5.2; the fix passes.
cross-model review, refute lens real, highOpenAI GPT-5.6, executed: own test fails 4 of 6 (direct, ParallelLeafReader, sorted search, addIndexes), controls pass; the fix passes it and 16 highlighter tests; intended argument weak.
Independent reproducer realOwn 6-test JUnit outside the highlighter package fails 4 of 6 on main (-ea, -da, 6 seeds), 10.5.2 and 9.12.3; controls pass; 6 of 6 with the fix.
Cross-model review, intended lens real, highOpenAI GPT-5.6, executed: own test fails 4 of 6, 0 of 6 with the fix; 21 existing highlighter tests pass with the fix; intended argument weak.
Intended-behaviour devil's advocate real, high (low impact)Found that a Sort.RELEVANCE search fails too; the classic highlighter phrase path and MemoryIndex controls pass; built the strongest works-as-designed case and showed why it fails.
adjudication confirmed, highRe-ran the independent reproduction's JUnit with a new seed (-ea, -da): 4 of 6 fail, both controls pass; 6 of 6 with the fix.
Registration check and credibility audit keepRe-ran the finder's JUnit with -da: 2 of 2 fail. Committer view: a real but small bug with an obvious one-line fix.
this doc's runs reproduced; fix validatedThe one-method test fails on 645d4fe14a20 (gradle and standalone, -ea and -da), 10.6.0, 10.5.2 and 9.12.3; with the fix it passes, and so do all 723 highlighter tests.
fact checks of this doc real as described; corrections appliedAn independent fact checker re-ran the repro (gradle and the standalone runner, -ea and -da, release jars), the fix with the whole highlighter suite, and its own probe of every consumer; a read-only cross-model fact check (OpenAI GPT-5.6) read the evidence. Two dating errors (#12685 first shipped in 9.9.0; 7.0.0 is nine years old) and two wording errors (what addIndexes writes; why Version.LATEST) were corrected, and several claims were scoped more narrowly.

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

Finding record

LFV-655, snapshot of 2026-11-06

Registry title
TermVectorLeafReader.getMetaData() always throws IllegalArgumentException ('minVersion must be set when createdVersionMajor is >= 7'), so a TermVectorLeafReader cannot be searched with a Sort, wrapped in a ParallelLeafReader or copied with addIndexes
Area
highlighter / TermVectorLeafReader (LeafReader over one document's term vectors; LeafMetaData invariant)
Priority
Low, rank 662: TermVectorLeafReader.getMetaData always throws a clear IllegalArgumentException, but only when this rarely used helper is passed to sorted search, ParallelLeafReader or addIndexes.
Impact class
crash
Impact note
Hits only applications that construct a TermVectorLeafReader themselves (public class and constructor, @lucene.experimental; a code search finds no use in Solr, Elasticsearch or OpenSearch) and pass it to an API that reads leaf metadata: IndexSearcher.search(query, n, sort) with any Sort, Sort.RELEVANCE and Sort.INDEXORDER included (since 7.2.0), new ParallelLeafReader(...), or IndexWriter.addIndexes(SlowCodecReaderWrapper.wrap(...)); every such call throws IllegalArgumentException, deterministically with -ea and -da and whatever the document; the exception is clean (nothing is persisted, addIndexes leaves the writer open), relevance search without a Sort works, and Lucene's own highlighters never reach the method.
Summary
You build a TermVectorLeafReader over one document's term vectors (new TermVectorLeafReader(field, terms), the public helper that 'wraps a Terms with a LeafReader') and use it like any other LeafReader, expecting getMetaData() to return metadata with no index sort, as MemoryIndex's in-memory reader does. Instead getMetaData() throws IllegalArgumentException('minVersion must be set when createdVersionMajor is >= 7') on every call, because it passes minVersion=null together with createdVersionMajor=Version.LATEST.major, a combination the LeafMetaData constructor rejects. So a field-sorted search (even with Sort.RELEVANCE), new ParallelLeafReader(reader) and addIndexes(SlowCodecReaderWrapper.wrap(reader)) all fail; a plain relevance search without a Sort works.
Introduced
23b002a0fdf2f6025f1eb026c0afca247fb21ed0 (LUCENE-7756 'Only record the major Lucene version that created the index, and record the minimum Lucene version that contributed to segments', migrated as apache/lucene#8807, by Adrien Grand; JIRA patch era, no PR): the same commit added the LeafMetaData constructor check and rewrote TermVectorLeafReader.getIndexSort() { return null; } as getMetaData() { return new LeafMetaData(Version.LATEST.major, null, null); }, while giving MemoryIndex the valid Version.LATEST [read: git log, -S/-G and blame in the full-history clone; exec: 6.5.0 and 6.6.6 pass, 7.0.0 fails; git tag --contains puts the commit in 7.0.0 and in no 6.x release]. The field-sorted-search trigger came with LUCENE-8059 'Fold early termination support into TopFieldCollector' (812711416d9 on master, e4e7d12f5d8 on branch_7x, first release 7.2.0), which made TopFieldCollector read getMetaData() for every leaf [exec: 7.1.0 sorted search works, 7.2.0 fails]. 6677109ee60 (#12685, first release 10.0.0) only appended hasBlocks=false and kept the null (2017-03-30 (authored), 2017-04-04 (committed to master); sorted-search trigger 2017-11-29 (LUCENE-8059))
First affected version
7.0.0 [exec] (direct getMetaData(), ParallelLeafReader and addIndexes fail); field-sorted search fails from 7.2.0 [exec]. 6.x and older are not affected [exec 6.5.0 and 6.6.6]: getMetaData() did not exist and its predecessor getIndexSort() returned null
Affected versions
Every release from 7.0.0 to 10.6.0 (10.6.0 [exec: the minimal test fails on the release jars, -ea and -da, 2026-10-30]) and main a9779ef3c193; never fixed. [exec] follow-up version-independent main() probe: fails on 7.0.0, 7.1.0, 7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da), passes on 6.5.0 and 6.6.6 and on main with the one-line fix shadowed; the finder's JUnit fails 2 of 2 on main (-ea and -da), 10.5.2 and 10.0.0, and its main() repro fails on 9.12.3, 9.4.2, 8.11.4, 7.7.3 and 7.0.0; the independent reproduction's JUnit fails 4 of 6 on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check's probe fails 2 of 5 on main (-ea and -da) and 10.5.2; the GPT reviewers' own tests fail 4 of 6 on ea57360912ed (identical TermVectorLeafReader and LeafMetaData), and the intended-lens one also on a9779ef3c193. [read] git tag --contains 23b002a0fdf lists every 7.0.0-8.8.1 and 9.0.0-10.5.2 tag and no 6.x tag (8.8.2-8.11.4 are not tagged in this clone; 8.11.4 executed); upstream main a95c754ee003, branch_10x and branch_10_6 still have the line (read through the GitHub contents API, not executed). JDK 25.0.3, aarch64; pure Java logic.
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 (searched 2026-10-27, read-only; the prior-report record, verdict new): two hunt prior-report tool runs (96 queries over apache/lucene GitHub issues, PRs and commits, Solr, Elasticsearch and OpenSearch, the dev@, java-user@, issues@ and commits@ lists, the LUCENE JIRA archive and the web), manual GitHub searches in all repositories for the class name and both error strings (only #7096, #8490 and #8007 mention TermVectorLeafReader, none about metadata; the error strings match no Lucene item), GitHub code search (no downstream use of the class), six more Lucene and Solr lists (0 hits), the SOLR, LUCENE and LUCENENET JIRA archive (error strings: 0 hits), six web searches, the changed files of 409 open apache/lucene PRs (#13525, #13342 and #16847 touch TermVectorLeafReader.java without changing getMetaData; none touches LeafMetaData.java) and the git history (the line is unchanged in substance since 2017). Not fixed upstream: main a95c754ee003, branch_10x and branch_10_6 still have the line. Nearest items, none a report of this bug: LUCENE-7756 / #8807 (the introducing issue; its discussion covers null minVersion only for pre-7.0 segments), #12685 (edited the line to add hasBlocks=false and kept the null), #13328 (made LeafMetaData a record and kept the check), SOLR-11406 (a different null-minVersion failure, pre-7.0 segments in SlowCompositeReaderWrapper, fixed 2017). Gaps: keyword search only; GitHub does not index PR diffs; private security reports are not visible; 'not found' is the best available evidence, not proof.
Lucene commit tested
main 645d4fe14a20 (commit of 2026-10-28; runs of 2026-10-30) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.6.0, 10.5.2 and 9.12.3 release jars. The hunt's earlier runs used main a9779ef3c193 (the files involved are identical) and the releases 6.5.0 to 10.5.2. The recommended fix was validated in the scratch worktree: the repro test with assertions on and off, the whole highlighter test suite (35 suites, 723 tests) and checkGoogleJavaFormat.

Lucene and upstream sources

Finding record
  • lucene/highlighter/src/java/org/apache/lucene/search/highlight/TermVectorLeafReader.java :51-56 (class javadoc 'Wraps a Terms with a LeafReader, typically from term vectors', @lucene.experimental, public class), :237-240 (getMetaData returns new LeafMetaData(Version.LATEST.major, null, null, false))
  • lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java :24-31 (javadoc: created major 6 means unknown; minVersion null 'if this information is not available'), :47-61 (constructor 'Public for use by custom LeafReader impls'; throws 'minVersion must be set when createdVersionMajor is >= 7' at :58-59)
  • lucene/core/src/java/org/apache/lucene/index/LeafReader.java :526-531 (getMetaData 'Return metadata about this leaf', @lucene.experimental, no throws clause)
  • lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java :2293 (new LeafMetaData(Version.LATEST.major, Version.LATEST, null, false))
  • lucene/core/src/java/org/apache/lucene/search/TopFieldCollector.java :58, lucene/core/src/java/org/apache/lucene/index/ParallelLeafReader.java :131, lucene/core/src/java/org/apache/lucene/index/SlowCodecReaderWrapper.java :140-143 (consumers that read getMetaData unconditionally)
The finding in 30 seconds
  • lucene/highlighter/src/java/org/apache/lucene/search/highlight/TermVectorLeafReader.java:52-56 (at 645d4fe14a20) (class javadoc, @lucene.experimental), :61 (public constructor), :225-227 (maxDoc 1), :237-240 (getMetaData)
  • lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java:24-44 (at 645d4fe14a20) (javadoc), :45-61 (record and compact constructor; check at :58-60); lucene/core/src/java/org/apache/lucene/util/Version.java:259
  • lucene/core/src/java/org/apache/lucene/search/TopFieldCollector.java:53-59; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/ParallelLeafReader.java:130-131; lucene/core/src/java/org/apache/lucene/index/SlowCodecReaderWrapper.java:140-143; lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3228-3243, :3288-3290
  • lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:2291-2294; (at 645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/search/QueryUtils.java:312; git grep "new LeafMetaData(" over non-test sources (7 calls)
Who is affected
  • lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/TermVectorOffsetStrategy.java:48-50; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/FilterLeafReader.java:477-481; lucene/highlighter/src/java/org/apache/lucene/search/highlight/WeightedSpanTermExtractor.java:393-401, :423; lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3288-3290, :3390-3392
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3286 (at 645d4fe14a20) (flush before the up-front validations)
Minimal repro
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreStaticEnv.java:144-230 (at 645d4fe14a20) (seeded codec, locale, time zone and similarity)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/index/LeafReader.java:526-531; (at 645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/highlight/TermVectorLeafReader.java:52-61; lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java:27-31; lucene/core/src/java/org/apache/lucene/index/DocValuesLeafReader.java:25, :93-95
Suggested fix
  • lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:2293; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3228-3243; lucene/core/src/java/org/apache/lucene/index/LeafMetaData.java:27-31

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

Repro files

About the diagrams

It ends on the summary card that is also the PNG.

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.