Skip to content

Commit b6e57da

Browse files
authored
Add optimistic collection to DiversifyingNearestChildren* vector queries (#15063)
Optimistic knn search addresses a major issue where we return inconsistent results due to race conditions in the shared queue previously used over multi-segment search. It not only returns consistent results, but is generally better overall when it comes to recall & latency. This PR refactors the optimistic querying in AbstractKnnVectorQuery so that more sub-queries can take advantage of this logic. Additionally, this refactors PatienceKnnVectorQuery to better fit this API and utilize the underlying multi-segment search logic. Here are the results for nested vs. baseline. Note how baseline recall fluctuates, where it is consistent in candidate. closes: #15059
1 parent 278e967 commit b6e57da

File tree

8 files changed

+115
-53
lines changed

8 files changed

+115
-53
lines changed

lucene/CHANGES.txt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ Improvements
4848
This avoids clashes with other ASM versions on classpath because it removes the dependency.
4949
(Uwe Schindler)
5050

51-
* GITHUB#14226: Use optimistic-with-checking KNN Query execution strategy in place of cross-thread
52-
global queue min-score checking. Improves performance and consistency. (Mike Sokolov, Dzung Bui,
53-
Ben Trent)
51+
5452

5553
* GITHUB#15002: Remove synchronized WeakHashMap from IndexReader. This was added to help prevent
5654
SIGSEGV with the old unsafe "mmap hack", which has been replaced by MemorySegments.
@@ -171,7 +169,14 @@ Improvements
171169

172170
* GITHUB#14737: Clean up redundant code and comments in query node classes. (Stefan Vodita)
173171

174-
* GITHUB#14816: Expose search strategy in KNN query
172+
* GITHUB#14816: Expose search strategy in KNN query (Tommaso Teofili)
173+
174+
* GITHUB#14226: Use optimistic-with-checking KNN Query execution strategy in place of cross-thread
175+
global queue min-score checking. Improves performance and consistency. (Mike Sokolov, Dzung Bui,
176+
Ben Trent)
177+
178+
* GITHUB#15063: Use optimistic-with-checking KNN Query strategy for `DiversifyingChildren*` queries.
179+
(Ben Trent)
175180

176181
Optimizations
177182
---------------------

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,29 +103,31 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException {
103103
filterWeight = null;
104104
}
105105

106-
KnnCollectorManager knnCollectorManagerInner = getKnnCollectorManager(k, indexSearcher);
107-
TimeLimitingKnnCollectorManager knnCollectorManager =
108-
new TimeLimitingKnnCollectorManager(knnCollectorManagerInner, indexSearcher.getTimeout());
106+
KnnCollectorManager knnCollectorManager = getKnnCollectorManager(k, indexSearcher);
107+
OptimisticKnnCollectorManager optimisticCollectorManager =
108+
new OptimisticKnnCollectorManager(k, knnCollectorManager);
109+
TimeLimitingKnnCollectorManager timeLimitingKnnCollectorManager =
110+
new TimeLimitingKnnCollectorManager(optimisticCollectorManager, indexSearcher.getTimeout());
109111
TaskExecutor taskExecutor = indexSearcher.getTaskExecutor();
110112
List<LeafReaderContext> leafReaderContexts = new ArrayList<>(reader.leaves());
111113
List<Callable<TopDocs>> tasks = new ArrayList<>(leafReaderContexts.size());
112114
for (LeafReaderContext context : leafReaderContexts) {
113-
tasks.add(() -> searchLeaf(context, filterWeight, knnCollectorManager));
115+
tasks.add(() -> searchLeaf(context, filterWeight, timeLimitingKnnCollectorManager));
114116
}
115117
Map<Integer, TopDocs> perLeafResults = new HashMap<>();
116118
TopDocs topK = runSearchTasks(tasks, taskExecutor, perLeafResults, leafReaderContexts);
117119
int reentryCount = 0;
118120
if (topK.scoreDocs.length > 0
119121
&& perLeafResults.size() > 1
120122
// only re-enter if we used the optimistic collection
121-
&& knnCollectorManagerInner instanceof OptimisticKnnCollectorManager
123+
&& knnCollectorManager.isOptimistic()
122124
// don't re-enter the search if we early terminated
123125
&& topK.totalHits.relation() == TotalHits.Relation.EQUAL_TO) {
124126
float minTopKScore = topK.scoreDocs[topK.scoreDocs.length - 1].score;
125127
TimeLimitingKnnCollectorManager knnCollectorManagerPhase2 =
126128
new TimeLimitingKnnCollectorManager(
127129
new ReentrantKnnCollectorManager(
128-
new TopKnnCollectorManager(k, indexSearcher), perLeafResults),
130+
getKnnCollectorManager(k, indexSearcher), perLeafResults),
129131
indexSearcher.getTimeout());
130132
Iterator<LeafReaderContext> ctxIter = leafReaderContexts.iterator();
131133
while (ctxIter.hasNext()) {
@@ -236,26 +238,33 @@ private TopDocs getLeafResults(
236238
}
237239

238240
protected KnnCollectorManager getKnnCollectorManager(int k, IndexSearcher searcher) {
239-
return new OptimisticKnnCollectorManager(k);
241+
return new TopKnnCollectorManager(k, searcher);
240242
}
241243

242244
static class OptimisticKnnCollectorManager implements KnnCollectorManager {
243245
private final int k;
246+
private final KnnCollectorManager delegate;
244247

245-
OptimisticKnnCollectorManager(int k) {
248+
OptimisticKnnCollectorManager(int k, KnnCollectorManager delegate) {
246249
this.k = k;
250+
this.delegate = delegate;
247251
}
248252

249253
@Override
250254
public KnnCollector newCollector(
251255
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context)
252256
throws IOException {
253-
@SuppressWarnings("resource")
254-
float leafProportion = context.reader().maxDoc() / (float) context.parent.reader().maxDoc();
255-
int perLeafTopK = perLeafTopKCalculation(k, leafProportion);
256-
// if we divided by zero above, leafProportion can be NaN and then this would be 0
257-
assert perLeafTopK > 0;
258-
return new TopKnnCollector(perLeafTopK, visitedLimit, searchStrategy);
257+
// The delegate supports optimistic collection
258+
if (delegate.isOptimistic()) {
259+
@SuppressWarnings("resource")
260+
float leafProportion = context.reader().maxDoc() / (float) context.parent.reader().maxDoc();
261+
int perLeafTopK = perLeafTopKCalculation(k, leafProportion);
262+
// if we divided by zero above, leafProportion can be NaN and then this would be 0
263+
assert perLeafTopK > 0;
264+
return delegate.newOptimisticCollector(visitedLimit, searchStrategy, context, perLeafTopK);
265+
}
266+
// We don't support optimistic collection, so just do regular execution path
267+
return delegate.newCollector(visitedLimit, searchStrategy, context);
259268
}
260269
}
261270

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,13 @@ public KnnSearchStrategy getSearchStrategy() {
9999
return new KnnSearchStrategy.Patience(this, hnswStrategy.filteredSearchThreshold());
100100
} else if (delegateStrategy instanceof KnnSearchStrategy.Seeded seededStrategy) {
101101
if (seededStrategy.originalStrategy() instanceof KnnSearchStrategy.Hnsw hnswStrategy) {
102-
return new KnnSearchStrategy.Patience(this, hnswStrategy.filteredSearchThreshold());
102+
// rewrap the underlying HNSW strategy with patience
103+
// this way we still use the seeded entry points, filter threshold,
104+
// and can utilize patience thresholds
105+
KnnSearchStrategy.Patience patienceStrategy =
106+
new KnnSearchStrategy.Patience(this, hnswStrategy.filteredSearchThreshold());
107+
return new KnnSearchStrategy.Seeded(
108+
seededStrategy.entryPoints(), seededStrategy.numberOfEntryPoints(), patienceStrategy);
103109
}
104110
}
105111
return delegateStrategy;

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public String toString(String field) {
190190

191191
@Override
192192
protected KnnCollectorManager getKnnCollectorManager(int k, IndexSearcher searcher) {
193-
return delegate.getKnnCollectorManager(k, searcher);
193+
return new PatienceCollectorManager(delegate.getKnnCollectorManager(k, searcher));
194194
}
195195

196196
@Override
@@ -200,8 +200,7 @@ protected TopDocs approximateSearch(
200200
int visitedLimit,
201201
KnnCollectorManager knnCollectorManager)
202202
throws IOException {
203-
return delegate.approximateSearch(
204-
context, acceptDocs, visitedLimit, new PatienceCollectorManager(knnCollectorManager));
203+
return delegate.approximateSearch(context, acceptDocs, visitedLimit, knnCollectorManager);
205204
}
206205

207206
@Override
@@ -273,6 +272,25 @@ public KnnCollector newCollector(
273272
saturationThreshold,
274273
patience);
275274
}
275+
276+
@Override
277+
public KnnCollector newOptimisticCollector(
278+
int visitLimit, KnnSearchStrategy searchStrategy, LeafReaderContext ctx, int k)
279+
throws IOException {
280+
if (knnCollectorManager.isOptimistic()) {
281+
return new HnswQueueSaturationCollector(
282+
knnCollectorManager.newOptimisticCollector(visitLimit, searchStrategy, ctx, k),
283+
saturationThreshold,
284+
patience);
285+
} else {
286+
return null;
287+
}
288+
}
289+
290+
@Override
291+
public boolean isOptimistic() {
292+
return knnCollectorManager.isOptimistic();
293+
}
276294
}
277295

278296
@Override

lucene/core/src/java/org/apache/lucene/search/knn/KnnCollectorManager.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,25 @@ public interface KnnCollectorManager {
3838
KnnCollector newCollector(
3939
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context)
4040
throws IOException;
41+
42+
/**
43+
* Return a new {@link KnnCollector} instance, generally with a specific k value, scaled per leaf
44+
* statistics
45+
*
46+
* @param visitedLimit the maximum number of nodes that the search is allowed to visit
47+
* @param searchStrategy the optional search strategy configuration
48+
* @param context the leaf reader context
49+
* @param k the number of neighbors to collect, this is the expected number of results
50+
* @return a new KnnCollector instance
51+
* @throws IOException if there is an error creating the collector
52+
*/
53+
default KnnCollector newOptimisticCollector(
54+
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context, int k)
55+
throws IOException {
56+
return null;
57+
}
58+
59+
default boolean isOptimistic() {
60+
return false;
61+
}
4162
}

lucene/core/src/java/org/apache/lucene/search/knn/KnnSearchStrategy.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ public int hashCode() {
165165
}
166166

167167
@Override
168-
public void nextVectorsBlock() {}
168+
public void nextVectorsBlock() {
169+
originalStrategy.nextVectorsBlock();
170+
}
169171
}
170172

171173
/**

lucene/core/src/java/org/apache/lucene/search/knn/TopKnnCollectorManager.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,15 @@
2222
import org.apache.lucene.search.IndexSearcher;
2323
import org.apache.lucene.search.KnnCollector;
2424
import org.apache.lucene.search.TopKnnCollector;
25-
import org.apache.lucene.util.hnsw.BlockingFloatHeap;
2625

27-
/**
28-
* TopKnnCollectorManager responsible for creating {@link TopKnnCollector} instances. When
29-
* concurrency is supported, the {@link BlockingFloatHeap} is used to track the global top scores
30-
* collected across all leaves.
31-
*/
26+
/** TopKnnCollectorManager responsible for creating {@link TopKnnCollector} instances. */
3227
public class TopKnnCollectorManager implements KnnCollectorManager {
3328

3429
// the number of docs to collect
3530
private final int k;
36-
// the global score queue used to track the top scores collected across all leaves
37-
private final BlockingFloatHeap globalScoreQueue;
3831

3932
public TopKnnCollectorManager(int k, IndexSearcher indexSearcher) {
40-
boolean isMultiSegments = indexSearcher.getIndexReader().leaves().size() > 1;
4133
this.k = k;
42-
this.globalScoreQueue = isMultiSegments ? new BlockingFloatHeap(k) : null;
4334
}
4435

4536
/**
@@ -52,11 +43,17 @@ public TopKnnCollectorManager(int k, IndexSearcher indexSearcher) {
5243
public KnnCollector newCollector(
5344
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context)
5445
throws IOException {
55-
if (globalScoreQueue == null) {
56-
return new TopKnnCollector(k, visitedLimit, searchStrategy);
57-
} else {
58-
return new MultiLeafKnnCollector(
59-
k, globalScoreQueue, new TopKnnCollector(k, visitedLimit, searchStrategy));
60-
}
46+
return new TopKnnCollector(k, visitedLimit, searchStrategy);
47+
}
48+
49+
@Override
50+
public KnnCollector newOptimisticCollector(
51+
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context, int k) {
52+
return new TopKnnCollector(k, visitedLimit, searchStrategy);
53+
}
54+
55+
@Override
56+
public boolean isOptimistic() {
57+
return true;
6158
}
6259
}

lucene/join/src/java/org/apache/lucene/search/join/DiversifyingNearestChildrenKnnCollectorManager.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
import org.apache.lucene.search.KnnCollector;
2424
import org.apache.lucene.search.knn.KnnCollectorManager;
2525
import org.apache.lucene.search.knn.KnnSearchStrategy;
26-
import org.apache.lucene.search.knn.MultiLeafKnnCollector;
2726
import org.apache.lucene.util.BitSet;
28-
import org.apache.lucene.util.hnsw.BlockingFloatHeap;
2927

3028
/**
3129
* DiversifyingNearestChildrenKnnCollectorManager responsible for creating {@link
@@ -37,7 +35,6 @@ public class DiversifyingNearestChildrenKnnCollectorManager implements KnnCollec
3735
private final int k;
3836
// filter identifying the parent documents.
3937
private final BitSetProducer parentsFilter;
40-
private final BlockingFloatHeap globalScoreQueue;
4138

4239
/**
4340
* Constructor
@@ -49,8 +46,6 @@ public DiversifyingNearestChildrenKnnCollectorManager(
4946
int k, BitSetProducer parentsFilter, IndexSearcher indexSearcher) {
5047
this.k = k;
5148
this.parentsFilter = parentsFilter;
52-
this.globalScoreQueue =
53-
indexSearcher.getIndexReader().leaves().size() > 1 ? new BlockingFloatHeap(k) : null;
5449
}
5550

5651
/**
@@ -67,15 +62,24 @@ public KnnCollector newCollector(
6762
if (parentBitSet == null) {
6863
return null;
6964
}
70-
if (globalScoreQueue == null) {
71-
return new DiversifyingNearestChildrenKnnCollector(
72-
k, visitedLimit, searchStrategy, parentBitSet);
73-
} else {
74-
return new MultiLeafKnnCollector(
75-
k,
76-
globalScoreQueue,
77-
new DiversifyingNearestChildrenKnnCollector(
78-
k, visitedLimit, searchStrategy, parentBitSet));
65+
return new DiversifyingNearestChildrenKnnCollector(
66+
k, visitedLimit, searchStrategy, parentBitSet);
67+
}
68+
69+
@Override
70+
public KnnCollector newOptimisticCollector(
71+
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context, int k)
72+
throws IOException {
73+
BitSet parentBitSet = parentsFilter.getBitSet(context);
74+
if (parentBitSet == null) {
75+
return null;
7976
}
77+
return new DiversifyingNearestChildrenKnnCollector(
78+
k, visitedLimit, searchStrategy, parentBitSet);
79+
}
80+
81+
@Override
82+
public boolean isOptimistic() {
83+
return true;
8084
}
8185
}

0 commit comments

Comments
 (0)