The constructor takes the taxonomy snapshot before the index snapshot. A document with a new category that is indexed in between is in the searcher but not in the taxonomy reader.
- Two indexes. Taxonomy facets keep a second index, the taxonomy, that gives each category a number, its ordinal (0 is the root). A document in the main index stores the ordinals of its categories.
- Append-only. Categories are only ever added. So a taxonomy reader that is newer than the index reader is safe, and an older one may miss ordinals the documents use. That is why
TaxonomyReader's javadoc says to open the taxonomy after the main index. - The manager.
SearcherTaxonomyManagerhands outSearcherAndTaxonomypairs, anIndexSearcherplus aDirectoryTaxonomyReader, and refreshes them near-real-time from the two writers.
Author/Bob; inside the constructor, the taxonomy snapshot first and the index snapshot second; counting running off the end of a 3-slot counts array; maybeRefresh() keeping the pair; the index-first fix. Select the still to open it at full size. Generated with AI, checked against the code.- You index one document tagged
Author/Alice, so the taxonomy holds 3 ordinals: 0 (root), 1 (Author), 2 (Author/Alice). Then you createnew SearcherTaxonomyManager(writer, true, null, taxoWriter)while another thread indexes a document taggedAuthor/Bob, which becomes ordinal 3. - You expect the first pair, documented as "a matched pair", to count
Alice=1 Bob=1: its taxonomy reader should know every category its searcher's documents use. - Instead the constructor opens the taxonomy reader first and the index reader second, and Bob's document lands in between: the searcher sees 2 documents, the taxonomy reader knows only ordinals 0-2, and
getOrdinal(Author/Bob)returns -1. FastTaxonomyFacetCountsover all documents of that pair throwsArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3, with assertions on and off.maybeRefresh()keeps this pair until the index changes again (it returns true: there is nothing new to refresh). The manager's own refresh opens the index first, asTaxonomyReader's javadoc requires; doing the same in the constructor fixes it.
Applications that use taxonomy facets with SearcherTaxonomyManager's IndexWriter constructors and build a manager while another thread indexes new categories: indexing that starts first, a lazily built manager, or one re-created after replaceTaxonomy, which the javadoc requires. Rare: a manager built before indexing starts cannot hit it. Loud and transient; no index damage.
Mechanism, terms and numbers575 words
SearcherTaxonomyManager's IndexWriter (near-real-time) constructors open new DirectoryTaxonomyReader(taxoWriter) before DirectoryReader.open(writer, applyAllDeletes, false). A document indexed by another thread in between, together with a new category, is visible to the first searcher while the paired taxonomy reader does not know its category. getOrdinal returns -1 for it, FastTaxonomyFacetCounts over the pair throws ArrayIndexOutOfBoundsException when the counted hits include that document, and maybeRefresh() keeps the pair until the index changes again. TaxonomyReader's javadoc and the manager's own refreshIfNeeded both open the index first for exactly this reason. Query-time only, no index damage; it needs the manager to be built while new categories are being indexed. Present since the class was added in 4.3.0; executed failures on 15 releases from 4.3.0 to 10.6.0 and on main. Swapping the two opens fixes it.
- Indexing a faceted document adds its new categories to the taxonomy writer (
FacetsConfig.build) and then the document to the mainIndexWriter, from any thread;IndexWriteris "completely thread safe" (IndexWriter.java:167-168). - The constructor body, shared by all three IndexWriter constructors (
SearcherTaxonomyManager.java:57-103):new DirectoryTaxonomyReader(taxoWriter)takes the taxonomy snapshot first (:93); thenDirectoryReader.open(writer, applyAllDeletes, false)(:96-97), which callswriter.getReader(...)(DirectoryReader.java:141-144), takes the index snapshot. - The window. A document whose
addDocumentcompletes between the two snapshots is in the searcher, but its new categories were added after the taxonomy snapshot, sogetOrdinalreturnsINVALID_ORDINAL(-1) for them. - Counting fails when the counted hits include such a document. With dense counting (always used for a taxonomy of fewer than 1024 ordinals,
TaxonomyFacets.java:104)FastTaxonomyFacetCountscounts in its constructor into an array sizedtaxoReader.getSize()(TaxonomyFacets.java:134), andcounts[(int) ordinal]++(FastTaxonomyFacetCounts.java:93) throws for the unknown ordinal. With sparse counting (1024 or more ordinals and hits under a tenth ofmaxDoc,TaxonomyFacets.java:103-122) the counting itself succeeds (IntIntHashMap.addTo,FastTaxonomyFacetCounts.java:97,:110), andgetTopChildrenthrows when it looks up the parent of the unknown ordinal (TaxonomyFacets.java:482;Index 1104 out of bounds for length 1104with 1,104 categories). By code reading,getPathof an unknown ordinal throwsIllegalArgumentException. A facet count whose hits exclude the document succeeds on the same pair. - Refresh keeps it.
refreshIfNeededopens the index first (SearcherTaxonomyManager.java:195) and returnsnullwhen nothing changed (:196-197), somaybeRefresh()keeps the bad pair. The first refresh after the next index change opens the index, then the taxonomy (:204), and gives a matched pair.
For ordinals the stale taxonomy reader does know, the labels are right, because categories are append-only; drill-down terms are built without a taxonomy reader. This is an argument from the code, not an exhaustive test. Searches that do not count facets do not read the taxonomy reader, and a DrillDownQuery builds its terms without one (DrillDownQuery.java:50-51, :116-119); taxonomy-backed DrillSideways does take the taxonomy reader and counts with it through FastTaxonomyFacetCounts (DrillSideways.java:89-90, :157-166), so its facet counts fail the same way (code reading, not run). Only readers are opened, so the index is not damaged.
Claimed for the IndexWriter constructors only. The Directory constructor has the same order, but its javadoc NOTE already says it can give an "unsync'd" pair unless commit and maybeRefresh() run in the same thread (SearcherTaxonomyManager.java:105-111). The constructors that take already opened readers use the caller's pair.
Exposure and likelihood196 words
- Who. Taxonomy facets (an opt-in module) with
SearcherTaxonomyManager(IndexWriter, ...), otherwise defaultIndexWriterConfig, codec and searcher. The manager must be built while another thread indexes documents that add new categories: indexing that starts before the search side, a manager created lazily, or one re-created afterDirectoryTaxonomyWriter.replaceTaxonomy, which the class javadoc requires ("you must open a new SearcherTaxonomyManager afterwards",SearcherTaxonomyManager.java:37-38). - Effect. Facet counts on the first pair whose hits include such a document fail with
ArrayIndexOutOfBoundsException(counts that exclude it succeed), and a new category reads as absent (getOrdinal= -1). Under continuous indexing it lasts about one refresh interval; if the index then stays unchanged, it stays. - How likely: rare. Applications that build the manager before indexing starts, as upstream's
testNRTdoes, cannot hit it. When construction does overlap indexing of new categories, the window is hit often: 62 to 178 of 300 managers in hook-free stress runs where every document adds a new category (a worst case). No public report of this constructor-order defect was found. - Downstream. A read-only code search found no use of
SearcherTaxonomyManagerorDirectoryTaxonomyReaderin Elasticsearch, OpenSearch or Solr. The Lucene.NET port has the same constructor order.
One JUnit test, one thread, deterministic, public API. An InfoStream on the main writer indexes one document tagged Author/Bob when the constructor's getReader starts, which is after the taxonomy reader was opened. The test checks that the first pair's taxonomy reader knows Author/Bob.
/*
* 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.facet.taxonomy;
import java.io.IOException;
import java.io.UncheckedIOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.facet.FacetField;
import org.apache.lucene.facet.FacetsConfig;
import org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager.SearcherAndTaxonomy;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.InfoStream;
public class TestSearcherTaxonomyManagerCtorOrder extends LuceneTestCase {
private static class AddDocumentAtGetReader extends InfoStream {
IndexWriter writer;
DirectoryTaxonomyWriter taxoWriter;
@Override
public void message(String component, String message) {
if (writer != null && message.equals("flush at getReader")) {
IndexWriter w = writer;
writer = null;
Document doc = new Document();
doc.add(new FacetField("Author", "Bob"));
try {
w.addDocument(new FacetsConfig().build(taxoWriter, doc));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@Override
public boolean isEnabled(String component) {
return "IW".equals(component);
}
@Override
public void close() {}
}
public void testFirstPairKnowsTheCategoryOfItsDocument() throws Exception {
AddDocumentAtGetReader hook = new AddDocumentAtGetReader();
try (Directory indexDir = newDirectory();
Directory taxoDir = newDirectory();
IndexWriter writer = new IndexWriter(indexDir, new IndexWriterConfig().setInfoStream(hook));
DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir)) {
hook.taxoWriter = taxoWriter;
hook.writer = writer;
try (SearcherTaxonomyManager mgr = new SearcherTaxonomyManager(writer, null, taxoWriter)) {
SearcherAndTaxonomy pair = mgr.acquire();
try {
assertEquals(1, pair.searcher().getIndexReader().numDocs());
// expected: the pair's taxonomy knows Author/Bob, its document's category; actual: -1
assertNotEquals(
"taxonomy ordinal of Author/Bob",
TaxonomyReader.INVALID_ORDINAL,
pair.taxonomyReader().getOrdinal("Author", "Bob"));
} finally {
mgr.release(pair);
}
}
}
}
}
cd $LUCENE && ./gradlew :lucene:facet:test --tests TestSearcherTaxonomyManagerCtorOrder -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
- The first pair's searcher sees the document (
numDocs1) and its taxonomy reader returns a valid ordinal forAuthor/Bob. - Observed
AssertionError: taxonomy ordinal of Author/Bob. Actual: -1, with assertions on and off; facet counting over such a pair throwsArrayIndexOutOfBoundsException.
More files, environment, full output392 words
The hook fires on "flush at getReader", which IndexWriter.getReader logs right after ensureOpen() and before it takes any lock or flushes (IndexWriter.java:516-526); the constructor reaches it through DirectoryReader.open(writer, ...) (SearcherTaxonomyManager.java:97), after it opened the taxonomy reader (:93). Indexing there, on the constructing thread, once, creates the ordering a second indexing thread creates when its addDocument completes at that moment; it is a re-entrant call, not ordinary scheduling. Hook-free runs (one thread indexing, managers built in a loop, not part of this repro) show that ordinary scheduling reaches it too: 62 to 178 of 300 managers got a mismatched first pair.
The first assertion (the searcher sees the document, numDocs 1) passes; the second (its category is known) fails. The facet-counting exception follows from the -1 ordinal and is not asserted; the hunt's longer test shows it (Index 3 out of bounds for length 3, below).
Gradle chooses tests.asserts from the seed when the flag is not given (DEADBEEF chooses off); the test fails either way. Run ./gradlew tidy before putting the test in a PR: checkGoogleJavaFormat wants the IndexWriter writer = ... line split over two lines.
Regression test: this same test; it passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the index-first fix: passes. Also fails on the 10.6.0 and 10.5.2 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
cp $REPRO/TestSearcherTaxonomyManagerCtorOrder.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE && ./gradlew :lucene:facet:test --tests TestSearcherTaxonomyManagerCtorOrder -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, scratch worktree of 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true
TestSearcherTaxonomyManagerCtorOrder > testFirstPairKnowsTheCategoryOfItsDocument FAILED
java.lang.AssertionError: taxonomy ordinal of Author/Bob. Actual: -1
at __randomizedtesting.SeedInfo.seed([DEADBEEF:35F52105B867264B]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failEquals(Assert.java:187)
at junit@4.13.2/org.junit.Assert.assertNotEquals(Assert.java:201)
at org.apache.lucene.facet.taxonomy.TestSearcherTaxonomyManagerCtorOrder.testFirstPairKnowsTheCategoryOfItsDocument(TestSearcherTaxonomyManagerCtorOrder.java:75)
:lucene:facet:test (FAILURE): 1 test, 1 failure
# the same with -Ptests.asserts=false; the same on the 10.6.0 and 10.5.2 release jars
# the hunt's longer test (the scenario of the steps above), main, seed DEADBEEF; the pair is acquire()d again after maybeRefresh()
first pair: searcher numDocs=2, taxonomy size=3, taxonomy ordinal of Author/Bob=-1;
counts: [facet counting threw java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3];
after maybeRefresh() (returned true): [facet counting threw java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3]
expected:<[Alice=1 Bob=1]>
# with the fix: :lucene:facet:test (SUCCESS): 1 test, assertions on and offTaxonomyReader's javadoc: open the taxonomy "after opening the main index"; the pair is "a matched pair".- The manager's own refresh opens the index first, "otherwise we may get a new reader that references ords not yet known to the taxonomy reader".
- Only the Directory constructor warns of an "unsync'd" pair; the IndexWriter constructors state no precondition.
The full argument329 words
- Contracts.
TaxonomyReader: "it is important to open the taxonomy after opening the main index ... This order is important, otherwise it would be possible for the main index to refer to a category which is not yet visible in the old snapshot of the taxonomy" (TaxonomyReader.java:46-55).SearcherAndTaxonomy: "Holds a matched pair of IndexSearcher and TaxonomyReader", "Matching point-in-time DirectoryTaxonomyReader" (SearcherTaxonomyManager.java:43-50). - No caveat on these constructors. "Creates near-real-time searcher and taxonomy reader from the corresponding writers" (
:57); the 3-argument one is not marked Expert. The only "unsync'd pair" NOTE is on the Directory constructor (:105-111).SearcherTaxonomyManageris not@lucene.experimental(TaxonomyReaderis). - The code's own intent.
refreshIfNeeded: "Must re-open searcher first, otherwise we may get a new reader that references ords not yet known to the taxonomy reader" (:189-191) and "taxonomy should always be ahead of searchers. Otherwise, searchers might reference ordinals that taxonomy doesn't know of" (:200-202). The first comment (:189-191) came in the commit that created the class (03694e76930, 2013), together with the constructor's opposite order; the second (:200-202) was added byf3796ecf22b6(#14443, 2025). - Maintainers on record. In #6203 (LUCENE-5139, Won't Fix) an application refreshed its two readers separately (
sm.maybeRefresh(), then the taxonomy), and its search threads acquired them separately. A maintainer (shaie) suggested reopening the taxonomy beforesm.maybeRefresh()there, because "It's usually ok if taxoReader sees more ordinals than IndexSearcher, not vice versa" ("usually": not afterreplaceTaxonomywithdeleteAll()), and recommendedSearcherTaxonomyManager. That advice is about publishing two readers separately, not about the snapshot order inside one pair, whereTaxonomyReader's javadoc requires the index first. In a 2014 java-user thread they said the manager returns "a sync'd IndexSearcher and DirectoryTaxonomyReader". - Tests. None asserts or relies on the taxonomy-first order;
TestSearcherTaxonomyManager.testNRTbuilds the manager (:160) before it starts its indexer thread (:192), so it never exercises the window.
The devil's-advocate check123 words
The strongest case: applications normally build the manager once before indexing starts, as testNRT does; the Directory constructor already warns about unsync'd pairs; the taxonomy APIs are experimental; and LUCENE-5139 put pairing on the application.
Why it fails: no javadoc states a "build before indexing" precondition, IndexWriter is thread safe, and the class javadoc itself requires building a new manager at runtime after replaceTaxonomy; the NOTE is attached only to the Directory constructor; in LUCENE-5139 the application refreshed and published its two readers separately, while here the manager builds one pair itself, and the module's own javadoc and refresh code prescribe the opposite order; no comment, test or issue defends the constructor's order. Cross-model reviews (OpenAI GPT-5.6) rated the works-as-designed case weak.
Recommended Open the index reader first, then the taxonomy reader (validated)
Open the index reader first and the taxonomy reader second in the constructors, as refreshIfNeeded already does, and close the index reader if the taxonomy open fails. The minimal test then passes, and so does the facet module's test suite.
DirectoryReader reader = DirectoryReader.open(writer, applyAllDeletes, false);
DirectoryTaxonomyReader taxoReader;
try {
taxoReader = new DirectoryTaxonomyReader(taxoWriter);
} catch (Throwable t) {
IOUtils.closeWhileSuppressingExceptions(t, reader);
throw t;
}
current =
new SearcherAndTaxonomy(
SearcherManager.getSearcher(searcherFactory, reader, null), taxoReader);All options and trade-offs3 options ยท 508 words
Option A was built and run; B is a workaround for users today; C is not recommended.
In the IndexWriter constructor body (SearcherTaxonomyManager.java:93-98) open DirectoryReader.open(writer, applyAllDeletes, false) first and new DirectoryTaxonomyReader(taxoWriter) second; the same swap in the Directory constructor (:118-122) is optional, a consistency change: it does not remove that constructor's documented commit/refresh limitation (:105-111). A taxonomy snapshot taken after the index snapshot contains every category the index snapshot's documents use, because categories are added before their document and never removed.
DirectoryReader reader = DirectoryReader.open(writer, applyAllDeletes, false);
DirectoryTaxonomyReader taxoReader;
try {
taxoReader = new DirectoryTaxonomyReader(taxoWriter);
} catch (Throwable t) {
IOUtils.closeWhileSuppressingExceptions(t, reader);
throw t;
}
current =
new SearcherAndTaxonomy(
SearcherManager.getSearcher(searcherFactory, reader, null), taxoReader);Trade-offs
- Same cost as today: the same two readers are opened, only in the other order.
- It gives the one-sided invariant the javadoc asks for (the taxonomy reader may be ahead of the index reader, never behind), not an atomic snapshot of both indexes; the class still requires a new manager after
replaceTaxonomy. - Like the current code, it still leaks the taxonomy reader if the
SearcherFactorythrows (SearcherManager.getSearcherreleases the index reader,SearcherManager.java:262-264); that is a separate bug (registry LFV-309), not part of this fix. - The diff is the hunt's fix with two lines wrapped for google-java-format.
What the checks validated
- This doc's runs: scratch worktree of
645d4fe14a20with the minimal test copied in, gradle, seed DEADBEEF. Before the fix the test failed with assertions on and off; with the diff,:lucene:facet:test (SUCCESS): 1 testwith assertions on and off. - Re-run on 2026-10-30 in a fresh scratch worktree: the stock test fails (ordinal -1); with the diff,
TestSearcherTaxonomyManager*(the minimal test plusTestSearcherTaxonomyManager's 6 tests) givesSUCCESS: 7 tests, 1 skipped(the@NightlytestNRT) with assertions on and off. - The whole facet module with the fix, assertions on: 40 suites, 307 tests, 7 skipped, 0 failures.
TestSearcherTaxonomyManagerwith-Ptests.nightly=true, which adds the@NightlytestNRT(concurrent indexing and refresh through the IndexWriter constructor): 6 of 6 pass. ecjLintandforbiddenApis(main and test) pass;checkGoogleJavaFormatflags only one line of the test file, not the fix.- Earlier (hunt, the same swap): every repro passes, the hook-free plain-threads program gives 0 of 300 mismatched pairs, upstream
TestSearcherTaxonomyManager5 of 5, and the same swap on the 4.3.0 source passes there. - Not run: the full Lucene suite, benchmarks (the fix opens the same two readers, in the other order).
Open DirectoryReader.open(writer) first, then new DirectoryTaxonomyReader(taxoWriter), and pass both to the constructor that takes already opened readers (SearcherTaxonomyManager.java:127-135).
Trade-offs
Loses the replaceTaxonomy epoch check, since that constructor keeps no taxonomy writer (:154-155). Not run as a separate test.
Add a NOTE like the Directory constructor's to the IndexWriter constructors.
Trade-offs
Contradicts the class javadoc's instruction to re-create the manager after replaceTaxonomy, and leaves the exception in place.
A read-only code search of the default branches found no use of SearcherTaxonomyManager or DirectoryTaxonomyReader in Elasticsearch, OpenSearch or Solr (2026-10-26, repeated 2026-10-30); use outside that search (plugins, forks, other branches) was not checked.
The constructor's open order stood out while writing a TLA+ model of how the manager pairs its two readers. The model checker found a 5-state schedule that breaks the pair, and none with the index-first order. A public-API JUnit then reproduced it on the stock jars.
The method in full190 words
Method: TLA+ model checking (TLC), then an executed public-API test (the hunt classes it as semi-formal: the order was noticed while modelling, and the tool then produced the witness).
- Model.
TaxoPair.tlamodels the two writers, indexing (add a category, then a document), the constructor's two opens and the refresh. With the constructor as coded, TLC violatesPairSafe(every ordinal the searcher's documents use is known to the paired taxonomy reader) in 5 states: Ctor1 (taxonomy snapshot), AddCat, AddDoc, Ctor2 (index snapshot). With the index-first order it is clean (28,053 and 3,172,539 distinct states in two sizes); with no indexing during construction it is clean too; a refresh-order mutant violates it, so the property is not vacuous. - Execution. A JUnit on the public API paused the constructor at
getReader's firstInfoStreammessage while another thread added a document with a new category; it failed on stock main with assertions on and off, 20 of 20 iterations, and passed with the swap. The minimal repro above is that test cut down to one thread and one claim, checked by two assertions (the searcher sees the document, and its category is known).
None found: no issue, PR, list thread or downstream report describes the constructor's order (197 queries, 2026-10-26; refreshed 2026-10-30). The same exception is known only from readers that applications refreshed separately themselves (#6203, Won't Fix), where maintainers recommended this manager.
Searches4 searches
- #6384 (LUCENE-5320) added the Directory constructor and its "unsync'd pair" NOTE, which covers only that constructor.
- #6203 (LUCENE-5139, Won't Fix) and the java-user thread "Exception from FastTaxonomyFacetCounts" (2014-10): the same exception from readers the application opened and refreshed itself, separately rather than as one pair; maintainers recommended
SearcherTaxonomyManagerbecause it keeps the two in sync. - #4859 (LUCENE-3786) created the class; its 15 comments cover commit order and epochs, never the constructor's open order. #5951 (LUCENE-4886) states the reopen-index-first rule. #9505, #9291, #6499 and #14219 / PR #14443 are other changes to the class.
- Not fixed on upstream main
716d6f216f52; no open PR touches the file (all 408 open PRs as of 2026-10-26, where only #16911 did, and the 52 updated since, checked on 2026-10-30). #16911 ("Decommission branch 10x"), which deleted every file onbranch_10x, was merged there on 2026-10-29. Tracking issue #16937 does not list it. Registry rows LFV-250, LFV-264 and LFV-309 (same class) and LFV-312 (in its superclass,ReferenceManager) are other root causes. - Limits: keyword search; GitHub search reads the first results per query; private reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene and downstream) | 2026-10-26 | 16 issue/PR, 3 commit and 2 downstream queries plus 64 GraphQL searches; the changed files of all 408 open PRs; 29 items read in full | related items only (#6384, #6203, #4859, #5951); none the same bug |
| Mailing lists (dev@, java-user@, issues@, commits@, builds@), JIRA archive | 2026-10-26 | 80 list queries, 12 JIRA-archive and 5 JQL queries | same exception only from application-built pairs (2014); commit-order discussions (2016) |
| Web | 2026-10-26 | 10 searches | no report |
| GitHub refresh | 2026-10-30 | issue and PR searches for SearcherTaxonomyManager sorted by creation; upstream main file history; tracking issue #16937 | nothing new; last change to the file 2025-05-18; not fixed |
Present since the class was added (LUCENE-3786, 2013, first release 4.3.0) with the same order in every version of the file (17 commits on main; 16 distinct versions on all branches). Fails on 15 releases from 4.3.0 to 10.6.0 and on main; 4.3.0 with only the two opens swapped passes.
Versions tested7 rows
Introduced with the class by LUCENE-3786 (#4859), 03694e76930 on trunk and e9fb220401e on branch_4x, 2013-04-10 (SVN era, no pull request). The same commit gave refreshIfNeeded the index-first order and its first comment. The 3-argument constructor (LUCENE-6980, 5.5.0) and the RefreshCommitSupplier constructor (#14443, 10.3.0) inherit the body.
The history runs used the hunt's test or ports of it with the 4.x facet API; on 4.3.0 to 4.6.1 with -ea the counting code's own assertion fires (ord=3 vs maxOrd=3), elsewhere the ArrayIndexOutOfBoundsException. The minimal test itself was run on main, 10.6.0 and 10.5.2.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off; passes with the fix |
| 10.6.0 (released 2026-10-29) | affected | minimal test fails on the release jars, assertions on and off |
| 10.5.2 | affected | minimal test and the hunt's test fail, assertions on and off |
| 10.0.0, 9.12.3, 9.0.0, 8.11.4, 8.11.2 | affected | the hunt's test or a port fails (ArrayIndexOutOfBoundsException) |
| 7.7.3, 6.6.6, 5.5.5, 5.0.0, 4.10.4, 4.7.0 | affected | port fails (ArrayIndexOutOfBoundsException) |
| 4.6.1, 4.3.0 | affected | port fails (with -ea the assertion ord=3 vs maxOrd=3; 4.3.0 with -da the exception); 4.3.0 with the two opens swapped passes |
| 4.2.1 | no such class | SearcherTaxonomyManager does not exist (compile error) |
Reproduced on stock main by an independent reproducer (its own test, the finder's seed), a devil's-advocate verifier, an adjudication and a credibility audit (each with new seeds); cross-model reviews (OpenAI GPT-5.6, reading only) rated it real; the fix passed every time.
Checks and verdicts305 words
Each check ran its own test or re-ran an earlier one on stock main; the controls (no indexing during construction, the index-first order) pass. The cross-model reviews were reading only.
| Check | Verdict | What it found |
|---|---|---|
| TLA+ model (TLC) | counterexample | PairSafe violated in 5 states; index-first constructor clean (28,053 and 3,172,539 distinct states). |
| Finder | candidate | Longer test fails -ea and -da, 20 of 20 iterations, and on 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.2; plain threads 129 of 300 managers; fix passes, upstream TestSearcherTaxonomyManager 5 of 5. |
| Cross-model review (OpenAI GPT-5.6, reading only) | real (high) | Works-as-designed case weak; its one flaw (tied labels compared in tie order): the finder reports that it was fixed and the runs repeated. The refute-lens review gave no verdict. |
| Independent reproducer | real | Own test with another pause point (the finder's seed DEADBEEF) fails on main, 10.5.2 and 9.12.3; hook-free 96 of 300 (main), 152 of 300 (10.5.2); no-indexing control passes; own fix passes. |
| Works-as-designed lens (OpenAI GPT-5.6, reading only) | real (medium) | Intended argument weak; the stage's own hook-free program: 62 and 77 of 300 on main, 71 of 300 on 10.5.2; control and fix 0 of 300. |
| Intended-behaviour devil's advocate | real | No intended, misuse or reachability argument survives; re-ran with seed 1A2B3C4D on main and 10.5.2 (fail). |
| Adjudication | confirmed (high) | Re-ran the independent reproduction's and the finder's tests with seed 5EED1234 (fail; stress 178 and 164 of 300); the independent reproduction's fix check passes. |
| Registration check and credibility audit | keep | Fresh seeds fail on main and 10.5.2 and pass with the fix; hook-free 78 of 300. |
| 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 and 10.5.2; passes with the fix. |
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.
- PNG 544 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,377 words
- Registry title
- SearcherTaxonomyManager's IndexWriter (near-real-time) constructors open the taxonomy reader before the index reader, so a manager created while another thread indexes can hand out a first pair whose searcher holds documents with categories the paired taxonomy reader does not know (facet counting throws ArrayIndexOutOfBoundsException until the index changes and the manager refreshes)
- Area
- facet / taxonomy (SearcherTaxonomyManager NRT constructors: index and taxonomy reader open order)
- Priority
- Low, rank 610: A loud facet-counting ArrayIndexOutOfBoundsException from a fairly likely construction-time race, but it is transient and module-only.
- Impact class
- crash
- Impact note
- Applications on Lucene 4.3.0 through 10.6.0 or main that use taxonomy facets with SearcherTaxonomyManager's IndexWriter constructors (an opt-in facet module; default IndexWriterConfig, codec and searcher otherwise) and create the manager while another thread indexes documents that add new categories (for example indexing that starts before the search side, a manager re-created after replaceTaxonomy as the class javadoc requires, or one created lazily) can get a first pair on which facet counting throws ArrayIndexOutOfBoundsException and getOrdinal returns -1 for a visible document's category; it is query-time only, damages no index, and lasts until the first maybeRefresh() after the next index change (it persists while the index stays unchanged).
- Summary
- You index documents with taxonomy facets in one thread and meanwhile create a SearcherTaxonomyManager from the IndexWriter and DirectoryTaxonomyWriter. You expect its first SearcherAndTaxonomy, documented as 'a matched pair', to know every category its searcher's documents use, because TaxonomyReader's javadoc says to open the taxonomy after the main index and the manager's own refresh does exactly that. Instead the constructor opens the taxonomy reader first, so a document indexed in between is visible to the searcher while its category is missing from the taxonomy reader: getOrdinal returns -1, FastTaxonomyFacetCounts throws ArrayIndexOutOfBoundsException, and maybeRefresh() keeps the broken pair until the index changes again.
- Introduced
- 03694e7693092432701be0186607e25e4633ea2b (trunk, svn r1466548; backported to branch_4x as e9fb220401ef, svn r1466550, the same day), 'LUCENE-3786: add SearcherTaxonomyManager' by mikemccand; JIRA LUCENE-3786, now #4859; no pull request (SVN era) [read: git blame and every revision of the file in the full-history clone; the class was added with the NRT constructor already opening new DirectoryTaxonomyReader(taxoWriter) before DirectoryReader.open(writer, applyAllDeletes), while the same commit gave refreshIfNeeded the index-first order with the comment 'Must re-open searcher first'; exec: 4.2.1 lacks the class, 4.3.0 fails]. Present since the class was created, not a regression (2013-04-10)
- First affected version
- 4.3.0 [exec] (first release with SearcherTaxonomyManager; 4.2.1 does not have the class [exec: compile error, class absent from the 4.2.1 facet jar])
- Affected versions
- every release from 4.3.0 through 10.6.0, and main ea57360912ed and 645d4fe14a20 [exec: the same scenario fails on 4.3.0, 4.6.1, 4.7.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.2, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da on 4.3.0, 9.12.3, 10.5.2 and main); 4.3.0 with only the two opens swapped passes; read: all 27 revisions of the file in the history clone, at both of its paths, open the taxonomy reader first]; the constructor taking a RefreshCommitSupplier (10.3.0, #14443) and the 3-argument constructor (5.5.0, LUCENE-6980) share the same body; not fixed upstream on 2026-10-26 (main, branch_10x, branch_10_5 and branch_10_6 read through the read-only contents API, last change to the file f3796ecf22b6, 2025-05-18); the minimal test fails on the 10.6.0 release jars (4ae946e5a9b9, released 2026-10-29) and on main 645d4fe14a20, -ea and -da [exec 2026-10-30], and the file is unchanged on upstream main through 2f424fd428da (2026-10-30) [read]. The Directory constructor has the same order but a javadoc NOTE warns that it can give an unsync'd pair, so it is not claimed
- 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
- semi-formal: TLA+/TLC
- Upstream and prior reports
- none found (2026-10-26, read-only; the prior-report record, 197 recorded queries: three scripted prior-report searches (16 GitHub issue and PR queries, 3 commit queries, 2 downstream queries, 60 queries over the dev@, java-user@, issues@ and commits@ lists, 12 JIRA-archive queries), 64 GitHub GraphQL searches (48 on apache/lucene, 16 GitHub-wide or on downstream repositories), 20 more list queries over java-user@, dev@, issues@ and builds@, 5 JQL queries and 10 web searches; plus the changed-file lists of all 408 open apache/lucene PRs, every historical version of the file, the upstream branches, and full reads of 29 related issues and PRs; 29 hits assessed, none the same bug; the finder's scripted prior-report search and the independent reproduction's and the intended-behaviour check's searches found none either). Related, not this bug: #4859 (LUCENE-3786, created the class with this order; its 15 comments cover commit order and epochs, never the constructor's open order), #6203 (LUCENE-5139, Won't Fix: the same ArrayIndexOutOfBoundsException from an application's own SearcherManager and TaxonomyReader reopened in the wrong order; maintainers recommended SearcherTaxonomyManager), #6384 (LUCENE-5320, added the Directory constructor with its 'unsync'd pair' NOTE), the java-user threads 'Exception from FastTaxonomyFacetCounts' (2014-10, a maintainer: SearcherTaxonomyManager returns 'a sync'd IndexSearcher and DirectoryTaxonomyReader') and 'IndexWriter, DirectoryTaxonomyWriter & SearcherTaxonomyManager synchronization' (2016-09, commit order), #5951 (LUCENE-4886, states the reopen-index-first rule), #9505, #9291, #6499 and #14219 / PR #14443 (other changes to the class). No open PR changes SearcherTaxonomyManager.java (#16911 deletes every file from branch_10x). The Lucene.NET port has the same constructor order and no report. Update 2026-11-02: #16911 (open when searched) merged 2026-10-29 as 48cf3f515d6, which emptied branch_10x to a README tombstone (10.6 is the last 10.x minor; 10.x fixes go to branch_10_6); it fixes nothing here.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28; upstream main716d6f216f52, 2026-10-29, has no change toSearcherTaxonomyManager.javasincef3796ecf22b6, 2025-05-18): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner; with the index-first fix it passes, and so does the facet module's test suite. The minimal test also fails on the released 10.6.0 and 10.5.2 jars. Earlier hunt runs (the hunt onea57360912ed, whoselucene/facetis identical): the hunt's longer test or ports of it fail on 14 releases from 4.3.0 to 10.5.2; 4.2.1 has no such class.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:34-38 (at ea57360912ed) (class javadoc: manages NRT reopen of both readers; a new manager is required after replaceTaxonomy), :44-50 (SearcherAndTaxonomy: 'a matched pair', 'Matching point-in-time DirectoryTaxonomyReader'), :57-62 (3-argument IndexWriter constructor, not Expert, no precondition), :81-103 (constructor body: :93 new DirectoryTaxonomyReader(taxoWriter), :96-97 DirectoryReader.open(writer, applyAllDeletes, false)), :105-125 (Directory constructor, NOTE :108-110 'unsync'd' pair), :127-158 (constructors taking already opened readers), :188-204 (refreshIfNeeded opens the index first; comments :189-191 and :200-202)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyReader.java:46-55 (at ea57360912ed) ('it is important to open the taxonomy *after* opening the main index ... otherwise it would be possible for the main index to refer to a category which is not yet visible in the old snapshot of the taxonomy'), :57 (@lucene.experimental on TaxonomyReader)
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:43-50, (at 645d4fe14a20) :57-103 (:93, :96-97), :105-111, :188-225 (:195-197, :204)lucene/core/src/java/org/apache/lucene/index/DirectoryReader.java:141-144 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:167-168 (at 645d4fe14a20)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:103-122, (at 645d4fe14a20) :134, :482; FastTaxonomyFacetCounts.java:57, :93, :97, :104, :110; directory/DirectoryTaxonomyReader.java:485-488lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyReader.java:33-35, (at 645d4fe14a20) :46-55lucene/facet/src/java/org/apache/lucene/facet/DrillDownQuery.java:50-51, (at 645d4fe14a20) :116-119 (terms built without a taxonomy reader); DrillSideways.java:89-90, :157-166 (takes a TaxonomyReader and counts with FastTaxonomyFacetCounts over it)
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:34-39 (at 645d4fe14a20)lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java:160, (at 645d4fe14a20) :192
- Minimal repro
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:516-526 (at 645d4fe14a20)
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyReader.java:46-55, (at 645d4fe14a20) :57lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:34-39, (at 645d4fe14a20) :43-50, :57, :64-67, :105-111, :189-191, :200-202lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java:137-192 (at 645d4fe14a20)lucene-history: git show 03694e76930 (SearcherTaxonomyManager.java: the :189-191 comment and the taxonomy-first constructor), git show f3796ecf22b6 (adds the :200-202 comment)
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:93-98, (at 645d4fe14a20) :118-122, :127-135, :154-155lucene/core/src/java/org/apache/lucene/search/SearcherManager.java:243-266 (at 645d4fe14a20) (getSearcher decRefs the reader on throw, :262-264)
- History
lucene-history: git log --follow (17 commits) and git log --all over both paths (16 distinct contents, every one opens the taxonomy first)
- TestSearcherTaxonomyManagerCtorOrder.java (one test, public API):
repro/LFV-489/src/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManagerCtorOrder.java(85 lines)
Made in parallel with this doc from the code at 645d4fe14a20, the fix diff, the minimal repro and an executed probe of the steps' scenario.
assets/LFV-489/bug.png(544 KB)
