Skip to content

Commit 0156d94

Browse files
committed
Add sort and collapse info to SearchHits transport serialization (elastic#36555)
In order for CCS alternate execution mode (see elastic#32125) to be able to do the final reduction step on the CCS coordinating node, we need to serialize additional info in the transport layer as part of the `SearchHits`, specifically: - lucene `SortField[]` which contains info about the fields that sorting was performed on and their type, which depends on mappings (that the CCS node does not know about) - collapse field (`String`) that field collapsing was executed on, if requested - collapse values (`Object[]`) that field collapsing was based on, if requested This info is needed to be able to reconstruct the `TopFieldDocs` or `CollapseFieldTopDocs` in the CCS coordinating node to feed the `mergeTopDocs` method and reduce multiple search responses received (one per cluster) into one. This commit adds such information to the `SearchHits` class. It's nullable info that is not serialized through the REST layer. `SearchPhaseController` sets such info at the end of the hits reduction phase.
1 parent e52941c commit 0156d94

File tree

12 files changed

+561
-147
lines changed

12 files changed

+561
-147
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ private void innerRun() throws IOException {
109109
// query AND fetch optimization
110110
finishPhase.run();
111111
} else {
112-
final IntArrayList[] docIdsToLoad = searchPhaseController.fillDocIdsToLoad(numShards, reducedQueryPhase.scoreDocs);
113-
if (reducedQueryPhase.scoreDocs.length == 0) { // no docs to fetch -- sidestep everything and return
112+
ScoreDoc[] scoreDocs = reducedQueryPhase.sortedTopDocs.scoreDocs;
113+
final IntArrayList[] docIdsToLoad = searchPhaseController.fillDocIdsToLoad(numShards, scoreDocs);
114+
if (scoreDocs.length == 0) { // no docs to fetch -- sidestep everything and return
114115
phaseResults.stream()
115116
.map(SearchPhaseResult::queryResult)
116117
.forEach(this::releaseIrrelevantSearchContext); // we have to release contexts here to free up resources

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

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -207,18 +207,23 @@ static SortedTopDocs sortDocs(boolean ignoreFrom, Collection<? extends SearchPha
207207
}
208208
}
209209
}
210-
final boolean isSortedByField;
211-
final SortField[] sortFields;
210+
boolean isSortedByField = false;
211+
SortField[] sortFields = null;
212+
String collapseField = null;
213+
Object[] collapseValues = null;
212214
if (mergedTopDocs instanceof TopFieldDocs) {
213215
TopFieldDocs fieldDocs = (TopFieldDocs) mergedTopDocs;
214-
isSortedByField = (fieldDocs instanceof CollapseTopFieldDocs &&
215-
fieldDocs.fields.length == 1 && fieldDocs.fields[0].getType() == SortField.Type.SCORE) == false;
216216
sortFields = fieldDocs.fields;
217-
} else {
218-
isSortedByField = false;
219-
sortFields = null;
217+
if (fieldDocs instanceof CollapseTopFieldDocs) {
218+
isSortedByField = (fieldDocs.fields.length == 1 && fieldDocs.fields[0].getType() == SortField.Type.SCORE) == false;
219+
CollapseTopFieldDocs collapseTopFieldDocs = (CollapseTopFieldDocs) fieldDocs;
220+
collapseField = collapseTopFieldDocs.field;
221+
collapseValues = collapseTopFieldDocs.collapseValues;
222+
} else {
223+
isSortedByField = true;
224+
}
220225
}
221-
return new SortedTopDocs(scoreDocs, isSortedByField, sortFields);
226+
return new SortedTopDocs(scoreDocs, isSortedByField, sortFields, collapseField, collapseValues);
222227
} else {
223228
// no relevant docs
224229
return SortedTopDocs.EMPTY;
@@ -262,7 +267,7 @@ private static void setShardIndex(TopDocs topDocs, int shardIndex) {
262267
public ScoreDoc[] getLastEmittedDocPerShard(ReducedQueryPhase reducedQueryPhase, int numShards) {
263268
final ScoreDoc[] lastEmittedDocPerShard = new ScoreDoc[numShards];
264269
if (reducedQueryPhase.isEmptyResult == false) {
265-
final ScoreDoc[] sortedScoreDocs = reducedQueryPhase.scoreDocs;
270+
final ScoreDoc[] sortedScoreDocs = reducedQueryPhase.sortedTopDocs.scoreDocs;
266271
// from is always zero as when we use scroll, we ignore from
267272
long size = Math.min(reducedQueryPhase.fetchHits, reducedQueryPhase.size);
268273
// with collapsing we can have more hits than sorted docs
@@ -303,7 +308,7 @@ public InternalSearchResponse merge(boolean ignoreFrom, ReducedQueryPhase reduce
303308
if (reducedQueryPhase.isEmptyResult) {
304309
return InternalSearchResponse.empty();
305310
}
306-
ScoreDoc[] sortedDocs = reducedQueryPhase.scoreDocs;
311+
ScoreDoc[] sortedDocs = reducedQueryPhase.sortedTopDocs.scoreDocs;
307312
SearchHits hits = getHits(reducedQueryPhase, ignoreFrom, fetchResults, resultsLookup);
308313
if (reducedQueryPhase.suggest != null) {
309314
if (!fetchResults.isEmpty()) {
@@ -341,12 +346,12 @@ public InternalSearchResponse merge(boolean ignoreFrom, ReducedQueryPhase reduce
341346

342347
private SearchHits getHits(ReducedQueryPhase reducedQueryPhase, boolean ignoreFrom,
343348
Collection<? extends SearchPhaseResult> fetchResults, IntFunction<SearchPhaseResult> resultsLookup) {
344-
final boolean sorted = reducedQueryPhase.isSortedByField;
345-
ScoreDoc[] sortedDocs = reducedQueryPhase.scoreDocs;
349+
SortedTopDocs sortedTopDocs = reducedQueryPhase.sortedTopDocs;
346350
int sortScoreIndex = -1;
347-
if (sorted) {
348-
for (int i = 0; i < reducedQueryPhase.sortField.length; i++) {
349-
if (reducedQueryPhase.sortField[i].getType() == SortField.Type.SCORE) {
351+
if (sortedTopDocs.isSortedByField) {
352+
SortField[] sortFields = sortedTopDocs.sortFields;
353+
for (int i = 0; i < sortFields.length; i++) {
354+
if (sortFields[i].getType() == SortField.Type.SCORE) {
350355
sortScoreIndex = i;
351356
}
352357
}
@@ -358,12 +363,12 @@ private SearchHits getHits(ReducedQueryPhase reducedQueryPhase, boolean ignoreFr
358363
int from = ignoreFrom ? 0 : reducedQueryPhase.from;
359364
int numSearchHits = (int) Math.min(reducedQueryPhase.fetchHits - from, reducedQueryPhase.size);
360365
// with collapsing we can have more fetch hits than sorted docs
361-
numSearchHits = Math.min(sortedDocs.length, numSearchHits);
366+
numSearchHits = Math.min(sortedTopDocs.scoreDocs.length, numSearchHits);
362367
// merge hits
363368
List<SearchHit> hits = new ArrayList<>();
364369
if (!fetchResults.isEmpty()) {
365370
for (int i = 0; i < numSearchHits; i++) {
366-
ScoreDoc shardDoc = sortedDocs[i];
371+
ScoreDoc shardDoc = sortedTopDocs.scoreDocs[i];
367372
SearchPhaseResult fetchResultProvider = resultsLookup.apply(shardDoc.shardIndex);
368373
if (fetchResultProvider == null) {
369374
// this can happen if we are hitting a shard failure during the fetch phase
@@ -379,7 +384,7 @@ private SearchHits getHits(ReducedQueryPhase reducedQueryPhase, boolean ignoreFr
379384
SearchHit searchHit = fetchResult.hits().getHits()[index];
380385
searchHit.score(shardDoc.score);
381386
searchHit.shard(fetchResult.getSearchShardTarget());
382-
if (sorted) {
387+
if (sortedTopDocs.isSortedByField) {
383388
FieldDoc fieldDoc = (FieldDoc) shardDoc;
384389
searchHit.sortValues(fieldDoc.fields, reducedQueryPhase.sortValueFormats);
385390
if (sortScoreIndex != -1) {
@@ -389,7 +394,8 @@ private SearchHits getHits(ReducedQueryPhase reducedQueryPhase, boolean ignoreFr
389394
hits.add(searchHit);
390395
}
391396
}
392-
return new SearchHits(hits.toArray(new SearchHit[0]), reducedQueryPhase.totalHits, reducedQueryPhase.maxScore);
397+
return new SearchHits(hits.toArray(new SearchHit[0]), reducedQueryPhase.totalHits,
398+
reducedQueryPhase.maxScore, sortedTopDocs.sortFields, sortedTopDocs.collapseField, sortedTopDocs.collapseValues);
393399
}
394400

395401
/**
@@ -429,7 +435,7 @@ private ReducedQueryPhase reducedQueryPhase(Collection<? extends SearchPhaseResu
429435
Boolean terminatedEarly = null;
430436
if (queryResults.isEmpty()) { // early terminate we have nothing to reduce
431437
return new ReducedQueryPhase(topDocsStats.totalHits, topDocsStats.fetchHits, topDocsStats.maxScore,
432-
timedOut, terminatedEarly, null, null, null, EMPTY_DOCS, null, null, numReducePhases, false, 0, 0, true);
438+
timedOut, terminatedEarly, null, null, null, SortedTopDocs.EMPTY, null, numReducePhases, 0, 0, true);
433439
}
434440
final QuerySearchResult firstResult = queryResults.stream().findFirst().get().queryResult();
435441
final boolean hasSuggest = firstResult.suggest() != null;
@@ -491,10 +497,10 @@ private ReducedQueryPhase reducedQueryPhase(Collection<? extends SearchPhaseResu
491497
final InternalAggregations aggregations = aggregationsList.isEmpty() ? null : reduceAggs(aggregationsList,
492498
firstResult.pipelineAggregators(), reduceContext);
493499
final SearchProfileShardResults shardResults = profileResults.isEmpty() ? null : new SearchProfileShardResults(profileResults);
494-
final SortedTopDocs scoreDocs = sortDocs(isScrollRequest, queryResults, bufferedTopDocs, topDocsStats, from, size);
500+
final SortedTopDocs sortedTopDocs = sortDocs(isScrollRequest, queryResults, bufferedTopDocs, topDocsStats, from, size);
495501
return new ReducedQueryPhase(topDocsStats.totalHits, topDocsStats.fetchHits, topDocsStats.maxScore,
496-
timedOut, terminatedEarly, suggest, aggregations, shardResults, scoreDocs.scoreDocs, scoreDocs.sortFields,
497-
firstResult.sortValueFormats(), numReducePhases, scoreDocs.isSortedByField, size, from, false);
502+
timedOut, terminatedEarly, suggest, aggregations, shardResults, sortedTopDocs,
503+
firstResult.sortValueFormats(), numReducePhases, size, from, firstResult == null);
498504
}
499505

500506
/**
@@ -542,12 +548,8 @@ public static final class ReducedQueryPhase {
542548
final SearchProfileShardResults shardResults;
543549
// the number of reduces phases
544550
final int numReducePhases;
545-
// the searches merged top docs
546-
final ScoreDoc[] scoreDocs;
547-
// the top docs sort fields used to sort the score docs, <code>null</code> if the results are not sorted
548-
final SortField[] sortField;
549-
// <code>true</code> iff the result score docs is sorted by a field (not score), this implies that <code>sortField</code> is set.
550-
final boolean isSortedByField;
551+
//encloses info about the merged top docs, the sort fields used to sort the score docs etc.
552+
final SortedTopDocs sortedTopDocs;
551553
// the size of the top hits to return
552554
final int size;
553555
// <code>true</code> iff the query phase had no results. Otherwise <code>false</code>
@@ -558,9 +560,8 @@ public static final class ReducedQueryPhase {
558560
final DocValueFormat[] sortValueFormats;
559561

560562
ReducedQueryPhase(long totalHits, long fetchHits, float maxScore, boolean timedOut, Boolean terminatedEarly, Suggest suggest,
561-
InternalAggregations aggregations, SearchProfileShardResults shardResults, ScoreDoc[] scoreDocs,
562-
SortField[] sortFields, DocValueFormat[] sortValueFormats, int numReducePhases, boolean isSortedByField, int size,
563-
int from, boolean isEmptyResult) {
563+
InternalAggregations aggregations, SearchProfileShardResults shardResults, SortedTopDocs sortedTopDocs,
564+
DocValueFormat[] sortValueFormats, int numReducePhases, int size, int from, boolean isEmptyResult) {
564565
if (numReducePhases <= 0) {
565566
throw new IllegalArgumentException("at least one reduce phase must have been applied but was: " + numReducePhases);
566567
}
@@ -577,9 +578,7 @@ public static final class ReducedQueryPhase {
577578
this.aggregations = aggregations;
578579
this.shardResults = shardResults;
579580
this.numReducePhases = numReducePhases;
580-
this.scoreDocs = scoreDocs;
581-
this.sortField = sortFields;
582-
this.isSortedByField = isSortedByField;
581+
this.sortedTopDocs = sortedTopDocs;
583582
this.size = size;
584583
this.from = from;
585584
this.isEmptyResult = isEmptyResult;
@@ -719,7 +718,7 @@ InitialSearchPhase.ArraySearchPhaseResults<SearchPhaseResult> newSearchPhaseResu
719718
}
720719
return new InitialSearchPhase.ArraySearchPhaseResults<SearchPhaseResult>(numShards) {
721720
@Override
722-
public ReducedQueryPhase reduce() {
721+
ReducedQueryPhase reduce() {
723722
return reducedQueryPhase(results.asList(), isScrollRequest, trackTotalHits);
724723
}
725724
};
@@ -752,15 +751,23 @@ void add(TopDocs topDocs) {
752751
}
753752

754753
static final class SortedTopDocs {
755-
static final SortedTopDocs EMPTY = new SortedTopDocs(EMPTY_DOCS, false, null);
754+
static final SortedTopDocs EMPTY = new SortedTopDocs(EMPTY_DOCS, false, null, null, null);
755+
// the searches merged top docs
756756
final ScoreDoc[] scoreDocs;
757+
// <code>true</code> iff the result score docs is sorted by a field (not score), this implies that <code>sortField</code> is set.
757758
final boolean isSortedByField;
759+
// the top docs sort fields used to sort the score docs, <code>null</code> if the results are not sorted
758760
final SortField[] sortFields;
761+
final String collapseField;
762+
final Object[] collapseValues;
759763

760-
SortedTopDocs(ScoreDoc[] scoreDocs, boolean isSortedByField, SortField[] sortFields) {
764+
SortedTopDocs(ScoreDoc[] scoreDocs, boolean isSortedByField, SortField[] sortFields,
765+
String collapseField, Object[] collapseValues) {
761766
this.scoreDocs = scoreDocs;
762767
this.isSortedByField = isSortedByField;
763768
this.sortFields = sortFields;
769+
this.collapseField = collapseField;
770+
this.collapseValues = collapseValues;
764771
}
765772
}
766773
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import org.elasticsearch.search.query.ScrollQuerySearchResult;
3636
import org.elasticsearch.transport.Transport;
3737

38-
import java.io.IOException;
3938
import java.util.function.BiFunction;
4039

4140
final class SearchScrollQueryThenFetchAsyncAction extends SearchScrollAsyncAction<ScrollQuerySearchResult> {
@@ -68,16 +67,16 @@ protected void executeInitialPhase(Transport.Connection connection, InternalScro
6867
protected SearchPhase moveToNextPhase(BiFunction<String, String, DiscoveryNode> clusterNodeLookup) {
6968
return new SearchPhase("fetch") {
7069
@Override
71-
public void run() throws IOException {
70+
public void run() {
7271
final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = searchPhaseController.reducedScrollQueryPhase(
7372
queryResults.asList());
74-
if (reducedQueryPhase.scoreDocs.length == 0) {
73+
ScoreDoc[] scoreDocs = reducedQueryPhase.sortedTopDocs.scoreDocs;
74+
if (scoreDocs.length == 0) {
7575
sendResponse(reducedQueryPhase, fetchResults);
7676
return;
7777
}
7878

79-
final IntArrayList[] docIdsToLoad = searchPhaseController.fillDocIdsToLoad(queryResults.length(),
80-
reducedQueryPhase.scoreDocs);
79+
final IntArrayList[] docIdsToLoad = searchPhaseController.fillDocIdsToLoad(queryResults.length(), scoreDocs);
8180
final ScoreDoc[] lastEmittedDocPerShard = searchPhaseController.getLastEmittedDocPerShard(reducedQueryPhase,
8281
queryResults.length());
8382
final CountDown counter = new CountDown(docIdsToLoad.length);

server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -785,22 +785,36 @@ public <T> void writeArray(final Writer<T> writer, final T[] array) throws IOExc
785785
}
786786
}
787787

788-
public <T extends Writeable> void writeArray(T[] array) throws IOException {
789-
writeVInt(array.length);
790-
for (T value: array) {
791-
value.writeTo(this);
792-
}
793-
}
794-
795-
public <T extends Writeable> void writeOptionalArray(@Nullable T[] array) throws IOException {
788+
/**
789+
* Same as {@link #writeArray(Writer, Object[])} but the provided array may be null. An additional boolean value is
790+
* serialized to indicate whether the array was null or not.
791+
*/
792+
public <T> void writeOptionalArray(final Writer<T> writer, final @Nullable T[] array) throws IOException {
796793
if (array == null) {
797794
writeBoolean(false);
798795
} else {
799796
writeBoolean(true);
800-
writeArray(array);
797+
writeArray(writer, array);
801798
}
802799
}
803800

801+
/**
802+
* Writes the specified array of {@link Writeable}s. This method can be seen as
803+
* writer version of {@link StreamInput#readArray(Writeable.Reader, IntFunction)}. The length of array encoded as a variable-length
804+
* integer is first written to the stream, and then the elements of the array are written to the stream.
805+
*/
806+
public <T extends Writeable> void writeArray(T[] array) throws IOException {
807+
writeArray((out, value) -> value.writeTo(out), array);
808+
}
809+
810+
/**
811+
* Same as {@link #writeArray(Writeable[])} but the provided array may be null. An additional boolean value is
812+
* serialized to indicate whether the array was null or not.
813+
*/
814+
public <T extends Writeable> void writeOptionalArray(@Nullable T[] array) throws IOException {
815+
writeOptionalArray((out, value) -> value.writeTo(out), array);
816+
}
817+
804818
/**
805819
* Serializes a potential null value.
806820
*/

0 commit comments

Comments
 (0)