Skip to content

Commit 050291d

Browse files
committed
CCS: skip empty search hits when minimizing round-trips (#40098)
When minimizing round-trips, each cluster returns its own independent search response. In case sort by field and/or field collapsing were requested, when one cluster has no results to return, the information about the field that sorting was based on (SortField array) as well as the field (and the values) that collapsing was performed on are missing in the search response. That causes problems as we can't build the proper `TopDocs` instance which would need to be either `TopFieldDocs` or `CollapseTopFieldDocs`. The merge routine expects that all the top docs are of the same exact type which can't be guaranteed. Given that the problematic results are empty, hence have no impact on the final results, we can simply skip them. Relates to #32125 Closes #40067
1 parent 0c64386 commit 050291d

File tree

2 files changed

+84
-7
lines changed

2 files changed

+84
-7
lines changed

server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,16 @@ SearchResponse getMergedResponse(Clusters clusters) {
168168
assert trackTotalHits == null || trackTotalHits;
169169
trackTotalHits = true;
170170
}
171+
171172
TopDocs topDocs = searchHitsToTopDocs(searchHits, totalHits, shards);
172173
topDocsStats.add(new TopDocsAndMaxScore(topDocs, searchHits.getMaxScore()),
173174
searchResponse.isTimedOut(), searchResponse.isTerminatedEarly());
174-
topDocsList.add(topDocs);
175+
if (searchHits.getHits().length > 0) {
176+
//there is no point in adding empty search hits and merging them with the others. Also, empty search hits always come
177+
//without sort fields and collapse info, despite sort by field and/or field collapsing was requested, which causes
178+
//issues reconstructing the proper TopDocs instance and breaks mergeTopDocs which expects the same type for each result.
179+
topDocsList.add(topDocs);
180+
}
175181
}
176182

177183
//after going through all the hits and collecting all their distinct shards, we can assign shardIndex and set it to the ScoreDocs
@@ -297,12 +303,17 @@ private static void setShardIndex(Map<ShardIdAndClusterAlias, Integer> shards, L
297303
}
298304

299305
private static SearchHits topDocsToSearchHits(TopDocs topDocs, TopDocsStats topDocsStats) {
300-
SearchHit[] searchHits = new SearchHit[topDocs.scoreDocs.length];
301-
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
302-
FieldDocAndSearchHit scoreDoc = (FieldDocAndSearchHit)topDocs.scoreDocs[i];
303-
searchHits[i] = scoreDoc.searchHit;
306+
SearchHit[] searchHits;
307+
if (topDocs == null) {
308+
//merged TopDocs is null whenever all clusters have returned empty hits
309+
searchHits = new SearchHit[0];
310+
} else {
311+
searchHits = new SearchHit[topDocs.scoreDocs.length];
312+
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
313+
FieldDocAndSearchHit scoreDoc = (FieldDocAndSearchHit)topDocs.scoreDocs[i];
314+
searchHits[i] = scoreDoc.searchHit;
315+
}
304316
}
305-
306317
SortField[] sortFields = null;
307318
String collapseField = null;
308319
Object[] collapseValues = null;
@@ -370,7 +381,7 @@ public int compareTo(ShardIdAndClusterAlias o) {
370381
}
371382
int clusterAliasCompareTo = clusterAlias.compareTo(o.clusterAlias);
372383
if (clusterAliasCompareTo != 0) {
373-
//TODO we may want to fix this, CCS returns remote results before local ones (TransportSearchAction#mergeShardsIterators)
384+
//CCS returns remote results before local ones (TransportSearchAction#mergeShardsIterators), fixed from 7.1 on
374385
if (clusterAlias.equals(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) {
375386
return 1;
376387
}

server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,72 @@ public void testMergeNoResponsesAdded() {
504504
assertEquals(0, response.getShardFailures().length);
505505
}
506506

507+
public void testMergeEmptySearchHitsWithNonEmpty() {
508+
long currentRelativeTime = randomLong();
509+
final SearchTimeProvider timeProvider = new SearchTimeProvider(randomLong(), 0, () -> currentRelativeTime);
510+
SearchResponseMerger merger = new SearchResponseMerger(0, 10, Integer.MAX_VALUE, timeProvider, flag -> null);
511+
SearchResponse.Clusters clusters = SearchResponseTests.randomClusters();
512+
int numFields = randomIntBetween(1, 3);
513+
SortField[] sortFields = new SortField[numFields];
514+
for (int i = 0; i < numFields; i++) {
515+
sortFields[i] = new SortField("field-" + i, SortField.Type.INT, randomBoolean());
516+
}
517+
PriorityQueue<SearchHit> priorityQueue = new PriorityQueue<>(new SearchHitComparator(sortFields));
518+
SearchHit[] hits = randomSearchHitArray(10, 1, "remote", new Index[]{new Index("index", "uuid")}, Float.NaN, 1,
519+
sortFields, priorityQueue);
520+
{
521+
SearchHits searchHits = new SearchHits(hits, new TotalHits(10, TotalHits.Relation.EQUAL_TO), Float.NaN, sortFields, null, null);
522+
InternalSearchResponse response = new InternalSearchResponse(searchHits, null, null, null, false, false, 1);
523+
SearchResponse searchResponse = new SearchResponse(response, null, 1, 1, 0, 1L,
524+
ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY);
525+
merger.add(searchResponse);
526+
}
527+
{
528+
SearchHits empty = new SearchHits(new SearchHit[0], new TotalHits(0, TotalHits.Relation.EQUAL_TO), Float.NaN, null, null, null);
529+
InternalSearchResponse response = new InternalSearchResponse(empty, null, null, null, false, false, 1);
530+
SearchResponse searchResponse = new SearchResponse(response, null, 1, 1, 0, 1L,
531+
ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY);
532+
merger.add(searchResponse);
533+
}
534+
assertEquals(2, merger.numResponses());
535+
SearchResponse mergedResponse = merger.getMergedResponse(clusters);
536+
assertEquals(10, mergedResponse.getHits().getTotalHits().value);
537+
assertEquals(10, mergedResponse.getHits().getHits().length);
538+
assertEquals(2, mergedResponse.getTotalShards());
539+
assertEquals(2, mergedResponse.getSuccessfulShards());
540+
assertEquals(0, mergedResponse.getSkippedShards());
541+
assertArrayEquals(sortFields, mergedResponse.getHits().getSortFields());
542+
assertArrayEquals(hits, mergedResponse.getHits().getHits());
543+
assertEquals(clusters, mergedResponse.getClusters());
544+
}
545+
546+
public void testMergeOnlyEmptyHits() {
547+
long currentRelativeTime = randomLong();
548+
final SearchTimeProvider timeProvider = new SearchTimeProvider(randomLong(), 0, () -> currentRelativeTime);
549+
SearchResponse.Clusters clusters = SearchResponseTests.randomClusters();
550+
Tuple<Integer, TotalHits.Relation> randomTrackTotalHits = randomTrackTotalHits();
551+
int trackTotalHitsUpTo = randomTrackTotalHits.v1();
552+
TotalHits.Relation totalHitsRelation = randomTrackTotalHits.v2();
553+
SearchResponseMerger merger = new SearchResponseMerger(0, 10, trackTotalHitsUpTo, timeProvider, flag -> null);
554+
int numResponses = randomIntBetween(1, 5);
555+
TotalHits expectedTotalHits = null;
556+
for (int i = 0; i < numResponses; i++) {
557+
TotalHits totalHits = null;
558+
if (trackTotalHitsUpTo != SearchContext.TRACK_TOTAL_HITS_DISABLED) {
559+
totalHits = new TotalHits(randomLongBetween(0, 1000), totalHitsRelation);
560+
long previousValue = expectedTotalHits == null ? 0 : expectedTotalHits.value;
561+
expectedTotalHits = new TotalHits(Math.min(previousValue + totalHits.value, trackTotalHitsUpTo), totalHitsRelation);
562+
}
563+
SearchHits empty = new SearchHits(new SearchHit[0], totalHits, Float.NaN, null, null, null);
564+
InternalSearchResponse response = new InternalSearchResponse(empty, null, null, null, false, false, 1);
565+
SearchResponse searchResponse = new SearchResponse(response, null, 1, 1, 0, 1L,
566+
ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY);
567+
merger.add(searchResponse);
568+
}
569+
SearchResponse mergedResponse = merger.getMergedResponse(clusters);
570+
assertEquals(expectedTotalHits, mergedResponse.getHits().getTotalHits());
571+
}
572+
507573
private static Tuple<Integer, TotalHits.Relation> randomTrackTotalHits() {
508574
switch(randomIntBetween(0, 2)) {
509575
case 0:

0 commit comments

Comments
 (0)