CachingCollectorManager records the first pass's hits per slice so a second pass can replay them. Its replay state belongs to the instance, not to one search.
IndexSearcher.search(query, manager)asks aCollectorManagerfor one collector per slice, collects, then callsreducewith those collectors to build the result.CachingCollectorManagerwraps a first-pass manager: each slice's collector also records the doc ids (and optionally scores) it sees, up to a per-slice limit.- After the search,
isCached()says whether every slice's cache is intact. If so,replay(secondPassManager)feeds the recorded hits to a second pass; if not, you run the query again.
- Index 10 documents:
k:aon docs 0-2,k:bon docs 3-9. Wrap a manager that keeps no state between searches (it collects doc ids) innew CachingCollectorManager<>(ids, false, null, 100)and searchk:a:[0, 1, 2]. - Search
k:bwith the same instance:[3..9], correct, andisCached()is true. You expectreplay(new IdsManager())to replay this search:[3, 4, 5, 6, 7, 8, 9]. - Instead it returns
[0, 1, ..., 9]. The manager appends every search's per-slice caches to one list, never clears it, and replays the whole list, so the second pass also gets docs 0-2, whichk:bdoes not match. - One overflow sticks: with a 5-doc budget, search
k:b(7 hits) overflows, then searchk:a(3 hits) fits, yetisCached()stays false, so the caller re-runs the query every time. A fresh manager says true. No error anywhere. - Nothing in the javadoc says an instance is single-use, while
TopScoreDocCollectorManagerandTopFieldCollectorManagersay "a new collectorManager should be created for each search".GroupingSearchcreates a new manager per search and is not affected.
Direct users of the new, experimental CachingCollectorManager who keep one instance for several searches with a reusable delegate, such as their own stateless manager or the grouping module's FirstPassGroupingCollectorManager. The second pass silently gets earlier searches' hits, and after one overflow the query is always re-run. GroupingSearch is not affected. 10.6.0 (released 2026-10-29) and main only.
Mechanism, terms and numbers474 words
CachingCollectorManager is new in 10.6.0 (#16247, @lucene.experimental). It wraps a first-pass CollectorManager, caches each slice's hits and lets a second pass replay them instead of re-running the query. The caches live in one list per instance that newCollector() appends to and nothing clears, while isCached() and replay() read the whole list. So an instance used for a second search replays the hits of both searches, and one earlier overflow keeps isCached() false for every later search, silently. With a delegate that is itself reusable, each search's own result is correct. The javadoc describes one search and never says an instance is single-use, although Lucene says so for TopScoreDocCollectorManager and TopFieldCollectorManager, whose state affects results, and a code comment in TotalHitCountCollectorManager calls reuse generally supported. This is a documentation gap, not a correctness bug: no contract promises reuse. GroupingSearch, the only in-tree non-test caller, creates a new manager per search and is not affected.
- Class javadoc: the manager "caches all collected documents (and optionally scores) per slice, so they can be replayed to a second-pass CollectorManager without re-running the query" (
CachingCollectorManager.java:25-27). The example runs one search, thenisCached(), thenreplay()or a re-run (:35-46).@lucene.experimental(:48). isCached(): "Returns true if the search has been run and all per-slice caches are intact ... false if the search has not yet been run or any cache overflowed" (:103-107).- Nothing says what happens on a second search, either way.
CollectorManageritself says nothing about reusing a manager (CollectorManager.java:23-46).
- One list per instance:
private final List<CachingCollector> cachingCollectors = new ArrayList<>();, commented "One CachingCollector per slice" (CachingCollectorManager.java:58-59). newCollector()wraps a new delegate collector in aCachingCollectorand callscachingCollectors.add(cache)(:82-91). No method ever removes an entry.IndexSearcher.search(Query, CollectorManager)callsnewCollector()once per slice and thenreducewith exactly those collectors (IndexSearcher.java:750-791).reduce()unwraps them and calls the delegate (CachingCollectorManager.java:95-101), so with a reusable delegate each search's own result is right.isCached()is "list not empty and every cache intact" (:108-111), andreplay()replays every cache in the list into a fresh second-pass collector (:118-130). After search N, both cover searches 1..N.
10 documents in one segment, no executor, so one slice per search. Search k:a adds cache 1 (docs 0-2); search k:b on the same instance adds cache 2 (docs 3-9). isCached() is true (both intact) and replay() replays both caches: [0..9]. With a 5-document budget, cache 1 of a k:b search (7 hits) overflows; cache 2 of a later k:a search (3 hits) is intact, but isCached() checks both and stays false. A reused instance also keeps every earlier cache (4 or 8 bytes per hit, CachingCollector.java:32) reachable (read, not measured).
Exposure and likelihood203 words
- Opt-in. The class is used only when an application builds it itself (or through
GroupingSearch, which creates a new one per search,GroupingSearch.java:167-176). No setting or scale is needed: 10 documents suffice. - Which delegates. A reusable first-pass manager: a user manager that keeps no state, or
FirstPassGroupingCollectorManager, which the grouping package docs suggest wrapping inCachingCollectorManager(package-info.java:49-58); the intended-behaviour check reproduced the replay of both searches with it. WithTopScoreDocCollectorManagerorTopFieldCollectorManageras delegate, reuse already breaks their own documented single-use rule. - Effect. Silent. The second pass receives documents the latest query does not match, and duplicates where searches overlap (the independent reproduction: 21 entries with 5, 15 and 25 twice instead of 15 odd ids, with one slice and with 3 slices on a 2-thread pool). After one overflow the manager never reports a usable cache again, so the query is re-run every time. Earlier caches stay in memory. Each first-pass result is correct (with a reusable delegate).
- Releases. 10.6.0 (released 2026-10-29 from RC1) is the first release with the class; main has it too.
- Likelihood: rare. Few direct users of a new experimental class, and reuse with the common stateful delegates is already documented as wrong.
The test shows (LFV-907, documentation defect) that a CachingCollectorManager used for two searches replays the hits of both searches, because it never clears its per-slice cache list; its javadoc does not say an instance serves one search.
/*
* 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;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.search.DummyTotalHitCountCollector;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestCachingCollectorManagerReuse extends LuceneTestCase {
public void testReplayAfterSecondSearch() throws IOException {
try (Directory dir = newDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
w.addDocument(new Document());
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
CachingCollectorManager<DummyTotalHitCountCollector, Integer> caching =
new CachingCollectorManager<>(
DummyTotalHitCountCollector.createManager(), false, null, 10);
searcher.search(MatchAllDocsQuery.INSTANCE, caching);
searcher.search(MatchAllDocsQuery.INSTANCE, caching);
// expected: replay() replays the 1 hit of the last search; actual: 2 (both searches)
assertEquals(1, (int) caching.replay(DummyTotalHitCountCollector.createManager()));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-907/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.search.TestCachingCollectorManagerReuse
- Expected vs actual
- Expected (after searching MatchAllDocsQuery twice over one document with a stateless delegate):
replay()returns the last search's 1 hit; actual (-ea and -da): 2. The intended fix is a javadoc sentence, so this test documents the gap rather than a future green test.
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<1> but was:<2>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.search.TestCachingCollectorManagerReuse.testReplayAfterSecondSearch(TestCachingCollectorManagerReuse.java:43)TopScoreDocCollectorManagerandTopFieldCollectorManagersay "a new collectorManager should be created for each search due to its internal states"; this class says nothing.- A
TotalHitCountCollectorManagercode comment calls reuse "generally supported" (not a strict requirement). - In #13542 javanna, who approved #16247, wrote that a stateful manager must be cleaned between searches or declared non-reusable; this one does neither, silently.
The full argument307 words
- Lucene documents single use where it applies.
TopScoreDocCollectorManager.java:26andTopFieldCollectorManager.java:27: "Note that a new collectorManager should be created for each search due to its internal states." (added by #240, LUCENE-10002, 2023). - A code comment states a reuse norm.
TotalHitCountCollectorManager.java:87-89(an implementation comment inreduce(), not javadoc; #13542, 2024): "Make the same collector manager instance reusable across multiple searches. It isn't a strict requirement but it is generally supported as collector managers normally don't hold state, as opposed to collectors." It is qualified and is no interface contract. - The rule a reviewer stated (as javanna wrote in the #13542 review, 2024-07-22). "Because the collector manager holds state, either we clean it in-between searches ... or we declare that collector managers cannot be re-used across multiple searches (potential breaking change)." The same comment goes on: "I am unsure whether it is necessary to make a breaking change for this, perhaps clearing state in reduce is not that bad." So javanna leaned toward cleaning state over a breaking declaration (said of an already released manager, as this one now is in 10.6.0), which supports the code option below at least as much as the javadoc one. javanna also reviewed and approved #16247 (2026-07-08).
CachingCollectorManagerholds state, does not clean it and does not declare it non-reusable. - Never discussed. The #16247 review (39 inline comments) covered the list's concurrency ("looks like this list is needed for the replay functionality";
newCollectoris called from one thread), thevolatilecached flag andisCached()before any search, never a second search. - No test either way. Upstream
TestCachingCollectorManager(testCacheOverflow,testNotCachedBeforeSearch,testBasic,testConstructor) uses each instance for at most one search. - What is not claimed. No contract promises that a reused manager replays only its latest search, so this is filed as a javadoc gap, not a correctness bug.
The devil's-advocate check214 words
The strongest case (the intended-behaviour check and the refute-lens cross-model review): the class is a per-search result holder by design. Its javadoc speaks of "the search" (CachingCollectorManager.java:104) and shows a single search (:35-46); "caches all collected documents" (:25) is literally what a reused instance does; it is @lucene.experimental; CollectorManager promises nothing about reuse; TotalHitCountCollectorManager calls reuse "not a strict requirement"; and GroupingSearch uses one instance per search. The refute-lens reviewer (OpenAI GPT-5.6) rated it not real, with high confidence, as a correctness bug.
Why it fails against the documentation claim: it defeats a correctness framing, which is not made here, but not the gap. No text says one instance must not serve two searches; Lucene writes that sentence for TopScoreDocCollectorManager and TopFieldCollectorManager, whose state affects results; a code comment calls reuse generally supported; and the failure is silent. The refute-lens reviewer itself agreed that a sibling-style sentence would reduce surprise. Remaining risk: maintainers may find the singular "the search" sufficient (judge confidence medium). The hunt's own noise audit first triaged it as a noise candidate (documented/defensible) before a challenger kept it as a Low documentation claim, and the credibility audit expects a one-line javadoc PR to be welcome but push-back on any bug or regression wording.
Recommended One javadoc sentence, like the sibling managers' (validated)
Add one sentence to the class javadoc, as the sibling managers have: "a new CachingCollectorManager should be created for each search", saying that isCached() and replay cover every search run on the instance (+4 lines). Format, lint and javadoc checks pass. A code alternative, replaying only the most recent search, was also validated.
* </pre>
*
+ * <p>Note that a new CachingCollectorManager should be created for each search: it keeps the
+ * per-slice caches of every search it is used for, and {@link #isCached()} and {@link #replay}
+ * cover all of them.
+ *
* @lucene.experimentalAll options and trade-offs3 options ยท 561 words
Both options were built in a scratch worktree of 645d4fe14a20 and run with gradle. The javadoc sentence matches the advice of the adjudication and the verifiers; upstream may prefer the small code change, whose regression test is the repro.
In the class javadoc of CachingCollectorManager, after the usage example (1 file, +4).
* </pre>
*
+ * <p>Note that a new CachingCollectorManager should be created for each search: it keeps the
+ * per-slice caches of every search it is used for, and {@link #isCached()} and {@link #replay}
+ * cover all of them.
+ *
* @lucene.experimentalTrade-offs
Smallest change, mirrors existing wording, no behaviour change. Reuse stays a silent misuse, and a reused instance still keeps earlier caches in memory.
What the checks validated
:lucene:core:check -x test(checkGoogleJavaFormat, ecjLint, forbiddenApis) passes.:lucene:core:renderJavadocpasses (its log records only BUILD SUCCESSFUL); the independent fact check rebuilt the javadoc and found the note in the rendered class page.- The 8 upstream
TestCachingCollector*tests pass; the repro still fails, as it must for a javadoc-only change.
newCollector() empties the replayable set when a search starts, and reduce() publishes exactly the collectors it receives, which are this search's (1 file, +10/-4, including one class-javadoc line: "isCached() and replay refer to the most recent search run with this manager"). The excerpt below has verbatim diff lines with shortened hunk headers and leaves out the javadoc hunk.
- // One CachingCollector per slice
- private final List<CachingCollector> cachingCollectors = new ArrayList<>();
+ // One CachingCollector per slice of the most recent search: set by reduce, emptied when a new
+ // search starts
+ private volatile List<CachingCollector> cachingCollectors = List.of();
@@ newCollector()
+ // a new search starts: the caches of an earlier search must not be replayed
+ cachingCollectors = List.of();
@@
- cachingCollectors.add(cache);
@@ reduce()
- return delegate.reduce(originals);
+ R result = delegate.reduce(originals);
+ cachingCollectors = List.copyOf(collectors);
+ return result;Trade-offs
- With a reusable delegate, reuse just works: a reused instance replays only its latest search and no longer keeps earlier caches alive. It does not make a stateful delegate (such as
TopScoreDocCollectorManager) reusable.GroupingSearchand every single-use caller behave as before. - A slightly different edge: after a search that throws before
reduce(),isCached()is false (today it may report earlier caches). - Two concurrent searches through one instance stay unsupported, as today.
- Touches the same method as the LFV-910 fix (independent logic, a trivial rebase).
What the checks validated
- Stock + test: fails with assertions on and off.
- + fix: the test passes with assertions on and off; core
TestCachingCollector,TestCachingCollectorManager, the new test,TestIndexSearcher,TestMultiCollector,TestMultiCollectorManager(39 tests, 1 skipped) and the wholelucene/groupingmodule (56 tests), 0 failures, assertions on and off. :lucene:core:check -x testpasses after one re-wrap of the javadoc line.- The independent reproduction's 3 tests (one slice, 3 slices on a 2-thread pool, overflow then fit) fail 3 of 3 on stock
645d4fe14a20and pass 3 of 3 with the patched class. - Not run: the full test suite, nightly tests, a benchmark.
newCollector() throws IllegalStateException once the instance has been reduced.
Trade-offs
Turns silent misuse into an error, but is stricter than the sibling managers, which only document it, and goes against the "generally reusable" norm.
Only application code decides to reuse an instance; an application that shared one across requests would feed earlier requests' hits, including documents the latest query or filter excludes, into a later second pass.
Code reading: while modelling GroupingSearch's replay-or-re-run decision on main, a hunt unit noticed that the manager's caches belong to the instance and are never cleared. It reduced the effect to a public-API JUnit with a stateless delegate and framed it as a documentation gap.
The method in full115 words
- Earlier sightings (this hunt). A unit on concurrent collector managers saw the effect in a scratch probe (3 + 7 = 10 replayed hits instead of 7) and left it unreported as a grey zone. In planning round 12, an ideation pack by an independent model from another family (OpenAI GPT-5.6) and a release-review probe proposed it again on main
a9779ef3c193. - Hunt unit. Read the per-search state while modelling the replay decision, reduced it to the JUnit in this doc (a delegate that keeps no state, so only the
CachingCollectorManagercarries state across searches), and classified it as a documentation gap, since no contract promises reuse. - Method tier. Non-formal: code reading and executed tests.
No prior report found (2026-10-28: two automated searches of 55 queries over GitHub, commits, the Lucene lists, JIRA, downstream trackers and the web; 23 manual GitHub searches; all 411 open PRs' changed files), refreshed 2026-10-29. Only #16247 (introducing) and #16379 name the class.
Searches4 searches
Nobody has reported that a reused CachingCollectorManager replays earlier searches or keeps isCached() false, or asked for a single-use note. Related context, not reports: #240 (the sibling managers' single-use sentence), #13542 (the reuse norm and javanna's clean-or-declare remark), #15605 / #15608 (a similar per-instance list removed from TopFieldCollectorManager for thread safety).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene; Solr, Elasticsearch, OpenSearch) | 2026-10-28 | two scripted prior-report searches (55 queries each: class and method names, reuse and single-use wording, the sibling sentence) and 23 manual searches | no report; only #16247 and #16379 name the class; context: #240, #13542, #15605, #15608, #5438, #4175, LUCENE-10002 |
| Open apache/lucene PRs | 2026-10-28 | changed files of all 411 open PRs; the file's commit history on main, branch_10_6 and branch_10x | none touches CachingCollectorManager.java; its only commit is #16247 |
| dev@, java-user@, issues@, commits@ lists and JIRA | 2026-10-28 | list searches over the last year; LUCENE-10002 with its comments | no mention of CachingCollectorManager on dev@ or java-user@; issues@ only the #16247 and #16379 notifications |
| GitHub refresh | 2026-10-29 | git ls-remote; issue and PR searches for CachingCollectorManager, "... reuse", "... single" and the sibling sentence; changed files of each of the 24 open PRs updated since 2026-10-28 (file lists kept) | nothing new; no open PR touches a CachingCollector* or GroupingSearch file; main still 645d4fe14a20, branch_10_6 still 4ae946e5a9b9, no 10.6 tag |
Introduced with the class by #16247 (gaobinlong, approved by javanna, merged 2026-07-08; main 3afffeef5a8d, branch_10x d60e550b47f), which is in 10.6.0 (released 2026-10-29). No earlier release has the class; the file is unchanged since.
Versions tested6 rows
The never-cleared list and the javadoc without a single-use note come from the introducing commit; its parent 6f3c1d7ad69 has no such file. CachingCollectorManager.java is byte-identical at 3afffeef5a8d, d60e550b47f, branch_10_6 4ae946e5a9b9 and main a9779ef3c193, and unchanged through ea57360912ed to 645d4fe14a20. Before 10.6, GroupingSearch used one CachingCollector once, so there was nothing to reuse. Listed under 10.6.0 in CHANGES.txt:543. A new API, not a regression.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the one-method test fails, [0..9] instead of [3..9], assertions on and off (gradle and standalone) |
branch_10_6 4ae946e5a9b9 (the 10.6.0 RC1 revision; jars built from source, not the RC1 release artifacts) | affected | this doc's and the finder's test fail; the independent reproduction and the intended-behaviour check reproduce with their own tests; a separate test, which asserts the accumulation, passes |
main a9779ef3c193 | affected | the finder's, the independent reproduction's and the intended-behaviour check's tests fail (judge and re-runs too), assertions on and off; a separate test, which asserts the accumulation, passes (verifier-step re-run) |
main ea57360912ed | affected | the cross-model reviews' own tests, assertions on and off: the refute-lens test fails (replay counts 5 instead of 3), the cross-model review's accumulation test passes |
3afffeef5a8d (#16247) sources over the 10.5.2 jars | introducing commit | the test fails (assertions on) |
| 10.5.2, 10.5.0, 10.4.0, 9.12.3 | no such class | compile error: CachingCollectorManager does not exist |
Two executed cross-model reviews (the second is verifier), the independent reproduction and the intended-behaviour check and an adjudication agree on the mechanism; all rate it a documentation gap, and the refute-lens review rejects a correctness framing. This doc's test fails on main and the release candidate, and passes with the code fix.
Checks and verdicts314 words
Every check ran code. The cross-model reviews used an independent reviewer from another model family (OpenAI GPT-5.6) on main ea57360912ed (2026-10-24), where the relevant classes are identical to 645d4fe14a20.
| Check | Verdict | What it found |
|---|---|---|
| finder (a hunt unit) | candidate (category doc) | Public-API JUnit with a stateless delegate: fails 1 of 1 on main (-ea, -da) and branch_10_6 (-ea; the follow-up step added the -da run); 10.5.2 compile error. |
| cross-model review, refute lens | not real as a correctness bug (high); agreed a single-use sentence would help | Own JUnit on main ea57360912ed (-ea, -da): replay counted 5 instead of 3; a fresh-manager control passed. |
| Independent reproducer | real, documentation only | Own JUnit (30 docs, 3 segments, overlapping queries, one slice and 3 slices, overflow then fit): 3 of 3 fail on main and branch_10_6 (-ea, -da). |
| Cross-model review, intended lens | real (high); intended argument moderate | Own JUnit asserting the accumulation and the sticky overflow passes on main ea57360912ed (-ea, -da); the verifier step re-ran it on main a9779ef3c193 (-ea, -da) and branch_10_6 (-ea): passes. |
| Devil's advocate | real, documentation only | Works-as-designed case strong against a correctness framing, not against the gap; probe with FirstPassGroupingCollectorManager as delegate replays [0..9] instead of [3..9]. |
| adjudication | confirmed, medium (minor, Low) | Re-ran the independent reproduction's test (3 of 3 fail, -ea, -da) and the finder's (1 of 1 fail). |
| credibility audit | keep | Re-ran the finder's test on main (-da): fails; prior art refreshed. |
| this doc | as above | The test on 645d4fe14a20 (gradle and standalone, -ea, -da), jars built from branch_10_6 at the RC1 revision and 10.5.2; both fixes in a scratch worktree; the independent reproduction's test with and without the code 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 598 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,283 words
- Registry title
- CachingCollectorManager (new in 10.6.0) does not document that it is single-use: reused for a second search, replay() replays the hits of both searches and one earlier overflow keeps isCached() false
- Area
- search / CachingCollectorManager lifecycle (documentation; new public API in 10.6.0)
- Priority
- Low, rank 1261: A documentation gap in the new experimental 10.6.0 CachingCollectorManager that only affects callers who reuse one instance, like LFV-797's reuse-after-call row but with a worse consequence of replayed hits and a stuck isCached().
- Impact class
- minor
- Impact note
- Documentation gap only, Low: direct users of the new @lucene.experimental CachingCollectorManager (10.6.0, released 2026-10-29, and main) who keep one instance for more than one search get a replay() that delivers the hits of every search run on that instance so far and, after any earlier overflow, an isCached() that stays false, so the query is always re-run; every search's own first-pass result is correct, no scale or setting is needed, and GroupingSearch, the only in-tree user, creates a new manager per search and is not affected.
- Summary
- You wrap a reusable CollectorManager in a CachingCollectorManager, run a search with it, then run a second search with the same instance and call isCached() and replay(secondPassManager), the workflow the class javadoc shows for one search. Nothing in its javadoc says an instance serves only one search, unlike TopScoreDocCollectorManager and TopFieldCollectorManager, which say 'a new collectorManager should be created for each search due to its internal states', while Lucene's stated norm (TotalHitCountCollectorManager) is that managers are generally reusable. Instead the manager keeps the per-slice caches of every search in one list that is never cleared, so replay() returns the hits of both searches (the repro: [0..9] instead of the second search's [3..9]) and one earlier overflow makes isCached() false for every later search, with no error.
- Introduced
- 3afffeef5a8d3bcbdcff9f2376e1c5a357764197 on main (squash merge of apache/lucene#16247 'Introduce CachingCollectorManager to parallelize search when using CachingCollector', by gaobinlong) and its backport d60e550b47f (branch_10x, contained in branch_10_6). The class, its never-cleared cachingCollectors list and its javadoc without a single-use note all come from that commit; the parent 6f3c1d7ad69 has no such file [read: git log, git log --all -S CachingCollectorManager in the full-history clone, upstream commits API; exec: the introducing commit's CachingCollectorManager.java and CachingCollector.java shadowed over the 10.5.2 jars fail the repro]. A new class, not a regression. (2026-07-08 (author date 2026-07-08T09:27:53Z on both commits; PR merged 2026-07-08T09:27:54Z; backport committed 2026-07-08T09:29:13Z))
- First affected version
- 10.6.0 (released 2026-10-29; branch_10_6 head 4ae946e5a9b [exec]); no earlier release has the class (compile error on 9.12.3, 10.4.0, 10.5.0 and 10.5.2 [exec])
- Affected versions
- main a9779ef3c193 [exec: finder JUnit fails with -ea and -da; the independent reproduction JUnit fails 3 of 3 with -ea and -da; the reviewer's and the intended-behaviour check's own tests reproduce] and the branch_10_6 head 4ae946e5a9b, the 10.6.0 release line [exec: finder JUnit -ea and -da, the independent reproduction JUnit 3 of 3 -ea and -da, reviewer test, the intended-behaviour check probe]; CachingCollectorManager.java is byte-identical at 3afffeef5a8d, d60e550b47f, 4ae946e5a9b and a9779ef3c193 [read]. The 10.6.0 RC artifacts themselves were not run (the branch_10_6 head build was). Not affected: every release up to and including 10.5.2, where the class does not exist [exec on 9.12.3, 10.4.0, 10.5.0, 10.5.2]; before 10.6 GroupingSearch wrapped a single CachingCollector used once, with no manager to reuse [read]. Not fixed upstream: the only commit to the file on main (head c5ab3e57b9ba) and on branch_10_6 (head 4ae946e5a9b9) is #16247, and no open PR touches it (checked 2026-10-28) [read]. All runs with JDK 25 on aarch64 Linux.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), 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 (the prior-report record, verdict new; searched 2026-10-28, read-only; refreshed at the registration check 2026-10-28): two scripted prior-report searches (55 queries each over GitHub issues, PRs and commits, downstream trackers, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web), 23 manual GitHub issue/PR searches (including elasticsearch, OpenSearch and solr), a GitHub code search, the changed files of all 411 open apache/lucene PRs, the file's upstream commit history on main, branch_10_6 and branch_10x, list-archive statistics for the last year, LUCENE-10002 with its comments, and full reads of the #16247 review and of #240, #13542, #15605, #15608 and related items. Only #16247 (the introducing PR) and #16379 (TestGrouping) name the class; neither discusses reuse, and the #16247 review covered the list's concurrency, the volatile cached flag and isCached() before any search, not reuse. Context, not reports: #240 added the sibling managers' 'a new collectorManager should be created for each search' note; the #13542 review (2024) states that a stateful manager must be cleaned between searches or declared non-reusable; #15605/#15608 removed a similar per-instance collector list from TopFieldCollectorManager for thread safety. Refresh: the class search still returns only #16247 and #16379; 'CachingCollectorManager reuse', 'CachingCollectorManager javadoc' and the sibling sentence return nothing; of the 65 PRs updated since 2026-10-26 only #16857 (Sort pruning) touches a collector manager, TopFieldCollectorManager.java, and its diff touches neither the single-use note nor CachingCollector*.
- Lucene commit tested
- main
645d4fe14a20(commit of 2026-10-28, upstream main on 2026-10-29): the one-method repro test through gradle in a scratch worktree and through the standalone runner, assertions on and off, stock, with the javadoc fix (checks, javadoc build) and with the keep-the-last-search code fix (repro, core caching tests, grouping module, checks); the independent reproduction's three-method test stock and with the code fix; jars built from branch_10_64ae946e5a9b9, the 10.6.0 RC1 revision (the RC1 release artifacts themselves were not run); the 10.5.2 release jars (compile error: no such class). Earlier hunt runs used maina9779ef3c193, except the cross-model reviews, which ran on mainea57360912ed;CachingCollectorManager.java,CachingCollector.java,IndexSearcher.java,CollectorManager.javaandlucene/groupingare identical at all three commits.
- Finding record
lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java:24-49 (at a9779ef3c193) (class javadoc: single-search example at :35-46, no reuse statement, @lucene.experimental at :48), :58-59 (per-instance cachingCollectors list), :82-91 (newCollector appends at :89, nothing clears the list), :93-101 (reduce uses only the current search's collectors), :103-111 (isCached javadoc 'the search has been run', allMatch over the whole list), :113-130 (replay over the whole list)lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollectorManager.java:26 (at a9779ef3c193) and TopFieldCollectorManager.java:27 ('Note that a new collectorManager should be created for each search due to its internal states'); TotalHitCountCollectorManager.java:87-89 (reuse 'isn't a strict requirement but it is generally supported as collector managers normally don't hold state'); CollectorManager.java:23-46 (no reuse statement); lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:167-176 (a new manager per search)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java:25-27, :35-46, :48, :58-59, :82-91, :95-101, :103-111, :118-130a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:750-791; CollectorManager.java:23-46; CachingCollector.java:32
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:167-176; lucene/grouping/src/java/org/apache/lucene/search/grouping/package-info.java:49-58
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollectorManager.java:26; TopFieldCollectorManager.java:27; TotalHitCountCollectorManager.java:87-89; CollectorManager.java:23-46a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java:25, :35-46, :48, :104a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java (each instance used for at most one search)
- History
a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:543 (10.6.0 section from :456); git log of CachingCollectorManager.java (only 3afffeef5a8)
- TestCachingCollectorManagerReuse.java (JUnit test):
repro/LFV-907/src/org/apache/lucene/search/TestCachingCollectorManagerReuse.java(47 lines)
It is made in parallel with this doc; every on-screen value comes from the code at 645d4fe14a20 or the diagram's executed probe on that commit (assertions on and off).
assets/LFV-907/bug.png(598 KB)
![Summary card for CachingCollectorManager reuse: one instance used for two searches over 10 documents keeps both searches' per-slice caches in one list; after the second search replay() returns [0..9] instead of that search's [3..9], and after one overflow isCached() stays false.](../assets/LFV-907/bug.png)