Skip to content

Commit 3afffee

Browse files
authored
Introduce CachingCollectorManager to parallelize search when using CachingCollector (#16247)
Introduce CachingCollectorManager, switch GroupingSearch to use search concurrency and move away from the deprecated search(Query, Collector) method. In addition, remove experimental GroupingSearch constructor that takes a GroupSelector as argument in favor of providing a GroupSelector supplier. Relates to #12892.
1 parent 6f3c1d7 commit 3afffee

9 files changed

Lines changed: 379 additions & 64 deletions

File tree

lucene/CHANGES.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,12 @@ New Features
312312

313313
Improvements
314314
---------------------
315-
* GITHUB#16170: Alter TopGroups.merge() to not return null if no groups. (Binlong Gao)
315+
316+
* GITHUB#16247: Introduce CachingCollectorManager to parallelize search when using CachingCollector.
317+
Remove experimental GroupingSearch constructor that takes a GroupSelector as argument
318+
in favour of providing a GroupSelector supplier. (Binlong Gao)
319+
320+
* GITHUB#16305: Alter TopGroups.merge() to not return null if no groups. (Binlong Gao)
316321

317322
Optimizations
318323
---------------------

lucene/core/src/java/org/apache/lucene/search/CachingCollector.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ public static CachingCollector create(Collector other, boolean cacheScores, int
329329
: new NoScoreCachingCollector(other, maxDocsToCache);
330330
}
331331

332-
private boolean cached;
332+
// visible for other threads in concurrent search mode
333+
private volatile boolean cached;
333334

334335
private CachingCollector(Collector in) {
335336
super(in);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.lucene.search;
18+
19+
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.Collection;
22+
import java.util.List;
23+
24+
/**
25+
* A {@link CollectorManager} that wraps a delegate {@link CollectorManager} and caches all
26+
* collected documents (and optionally scores) per slice, so they can be replayed to a second-pass
27+
* {@link CollectorManager} without re-running the query.
28+
*
29+
* <p>One {@link CachingCollector} is created per slice. During {@link #replay}, each cached slice
30+
* is replayed into a fresh second-pass collector, and all second-pass collectors are reduced
31+
* together. This works correctly with both sequential and concurrent search.
32+
*
33+
* <p>Example usage:
34+
*
35+
* <pre class="prettyprint">
36+
* CachingCollectorManager&lt;C1, R1&gt; caching = new CachingCollectorManager&lt;&gt;(
37+
* firstPassManager, cacheScores, maxRAMMB, null);
38+
* R1 firstResult = searcher.search(query, caching);
39+
*
40+
* if (caching.isCached()) {
41+
* R2 secondResult = caching.replay(secondPassManager);
42+
* } else {
43+
* // cache overflowed — re-run the query
44+
* R2 secondResult = searcher.search(query, secondPassManager);
45+
* }
46+
* </pre>
47+
*
48+
* @lucene.experimental
49+
*/
50+
public class CachingCollectorManager<C extends Collector, R>
51+
implements CollectorManager<CachingCollector, R> {
52+
53+
private final CollectorManager<C, R> delegate;
54+
private final boolean cacheScores;
55+
private final Double maxRAMMB;
56+
private final Integer maxDocsToCache;
57+
58+
// One CachingCollector per slice
59+
private final List<CachingCollector> cachingCollectors = new ArrayList<>();
60+
61+
/**
62+
* @param delegate the first-pass {@link CollectorManager}
63+
* @param cacheScores whether to cache scores in addition to document IDs
64+
* @param maxRAMMB the maximum RAM in MB to use per slice cache, or null if using maxDocsToCache
65+
* @param maxDocsToCache the maximum number of documents to cache per slice, or null if using
66+
* maxRAMMB
67+
*/
68+
public CachingCollectorManager(
69+
CollectorManager<C, R> delegate,
70+
boolean cacheScores,
71+
Double maxRAMMB,
72+
Integer maxDocsToCache) {
73+
if (maxRAMMB == null && maxDocsToCache == null || maxRAMMB != null && maxDocsToCache != null) {
74+
throw new IllegalArgumentException("Exactly one of maxRAMMB or maxDocsToCache must be set");
75+
}
76+
this.delegate = delegate;
77+
this.cacheScores = cacheScores;
78+
this.maxRAMMB = maxRAMMB;
79+
this.maxDocsToCache = maxDocsToCache;
80+
}
81+
82+
@Override
83+
public CachingCollector newCollector() throws IOException {
84+
C collector = delegate.newCollector();
85+
CachingCollector cache =
86+
maxDocsToCache != null
87+
? CachingCollector.create(collector, cacheScores, maxDocsToCache)
88+
: CachingCollector.create(collector, cacheScores, maxRAMMB);
89+
cachingCollectors.add(cache);
90+
return cache;
91+
}
92+
93+
@Override
94+
@SuppressWarnings("unchecked")
95+
public R reduce(Collection<CachingCollector> collectors) throws IOException {
96+
List<C> originals = new ArrayList<>(collectors.size());
97+
for (CachingCollector cache : collectors) {
98+
originals.add((C) cache.in);
99+
}
100+
return delegate.reduce(originals);
101+
}
102+
103+
/**
104+
* Returns {@code true} if the search has been run and all per-slice caches are intact (none
105+
* overflowed their RAM/doc budget). Returns {@code false} if the search has not yet been run or
106+
* any cache overflowed.
107+
*/
108+
public boolean isCached() {
109+
return !cachingCollectors.isEmpty()
110+
&& cachingCollectors.stream().allMatch(CachingCollector::isCached);
111+
}
112+
113+
/**
114+
* Replays each per-slice cache into a fresh second-pass collector, then reduces all results.
115+
*
116+
* @throws IllegalStateException if {@link #isCached()} returns {@code false}
117+
*/
118+
public <C2 extends Collector, R2> R2 replay(CollectorManager<C2, R2> secondPassManager)
119+
throws IOException {
120+
if (!isCached()) {
121+
throw new IllegalStateException("cache is not available; re-run the query instead");
122+
}
123+
List<C2> secondCollectors = new ArrayList<>(cachingCollectors.size());
124+
for (CachingCollector cache : cachingCollectors) {
125+
C2 secondCollector = secondPassManager.newCollector();
126+
cache.replay(secondCollector);
127+
secondCollectors.add(secondCollector);
128+
}
129+
return secondPassManager.reduce(secondCollectors);
130+
}
131+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.lucene.search;
18+
19+
import java.io.IOException;
20+
import org.apache.lucene.document.Document;
21+
import org.apache.lucene.store.Directory;
22+
import org.apache.lucene.tests.index.RandomIndexWriter;
23+
import org.apache.lucene.tests.util.LuceneTestCase;
24+
25+
public class TestCachingCollectorManager extends LuceneTestCase {
26+
27+
public void testCacheOverflow() throws IOException {
28+
Directory dir = newDirectory();
29+
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
30+
for (int i = 0; i < atLeast(10); i++) {
31+
iw.addDocument(new Document());
32+
}
33+
IndexSearcher searcher = newSearcher(iw.getReader());
34+
iw.close();
35+
36+
CachingCollectorManager<TopScoreDocCollector, TopDocs> caching =
37+
new CachingCollectorManager<>(
38+
new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, 0);
39+
40+
searcher.search(MatchAllDocsQuery.INSTANCE, caching);
41+
assertFalse(caching.isCached());
42+
assertThrows(
43+
IllegalStateException.class,
44+
() -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
45+
46+
searcher.getIndexReader().close();
47+
dir.close();
48+
}
49+
50+
public void testNotCachedBeforeSearch() {
51+
CachingCollectorManager<TopScoreDocCollector, TopDocs> caching =
52+
new CachingCollectorManager<>(
53+
new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, Integer.MAX_VALUE);
54+
assertFalse(caching.isCached());
55+
56+
assertThrows(
57+
IllegalStateException.class,
58+
() -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
59+
}
60+
61+
public void testBasic() throws IOException {
62+
Directory dir = newDirectory();
63+
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
64+
for (int i = 0; i < 10; i++) {
65+
iw.addDocument(new Document());
66+
}
67+
IndexSearcher searcher = newSearcher(iw.getReader());
68+
iw.close();
69+
70+
CachingCollectorManager<TopScoreDocCollector, TopDocs> caching =
71+
new CachingCollectorManager<>(
72+
new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), true, null, Integer.MAX_VALUE);
73+
74+
TopDocs firstResult = searcher.search(MatchAllDocsQuery.INSTANCE, caching);
75+
assertTrue(caching.isCached());
76+
assertEquals(10, firstResult.totalHits.value());
77+
78+
TopDocs replayResult = caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE));
79+
assertEquals(firstResult.totalHits.value(), replayResult.totalHits.value());
80+
assertEquals(firstResult.scoreDocs.length, replayResult.scoreDocs.length);
81+
82+
searcher.getIndexReader().close();
83+
dir.close();
84+
}
85+
86+
public void testConstructor() {
87+
assertThrows(
88+
IllegalArgumentException.class,
89+
() ->
90+
new CachingCollectorManager<>(
91+
new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, null));
92+
93+
assertThrows(
94+
IllegalArgumentException.class,
95+
() ->
96+
new CachingCollectorManager<>(
97+
new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, 1.0, 1));
98+
}
99+
}

lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ protected void doSetNextReader(LeafReaderContext context) throws IOException {
173173
@Override
174174
public void setScorer(Scorable scorer) throws IOException {
175175
this.scorer = scorer;
176+
groupSelector.setScorer(scorer);
176177
for (GroupHead<T> head : heads.values()) {
177178
head.setScorer(scorer);
178179
}

lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public Collection<T> getGroups() {
7070
}
7171

7272
@Override
73-
public void setScorer(Scorable scorer) throws IOException {}
73+
public void setScorer(Scorable scorer) throws IOException {
74+
groupSelector.setScorer(scorer);
75+
}
7476

7577
@Override
7678
protected void doSetNextReader(LeafReaderContext context) throws IOException {

0 commit comments

Comments
 (0)