Skip to content

Commit 6cad923

Browse files
committed
Remove unnecessary result sorting in SearchPhaseController (#23321)
In oder to use lucene's utilities to merge top docs the results need to be passed in a dense array where the index corresponds to the shard index in the result list. Yet, we were sorting results before merging them just to order them in the incoming order again for the above mentioned reason. This change removes the obsolet sort and prevents unnecessary materializing of results.
1 parent 8c02460 commit 6cad923

File tree

2 files changed

+53
-53
lines changed

2 files changed

+53
-53
lines changed

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

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,6 @@
7474
*/
7575
public class SearchPhaseController extends AbstractComponent {
7676

77-
private static final Comparator<AtomicArray.Entry<? extends QuerySearchResultProvider>> QUERY_RESULT_ORDERING = (o1, o2) -> {
78-
int i = o1.value.shardTarget().getIndex().compareTo(o2.value.shardTarget().getIndex());
79-
if (i == 0) {
80-
i = o1.value.shardTarget().getShardId().id() - o2.value.shardTarget().getShardId().id();
81-
}
82-
return i;
83-
};
84-
8577
private static final ScoreDoc[] EMPTY_DOCS = new ScoreDoc[0];
8678

8779
private final BigArrays bigArrays;
@@ -153,6 +145,9 @@ private static long optionalSum(long left, long right) {
153145
* named completion suggestion across all shards. If more than one named completion suggestion is specified in the
154146
* request, the suggest docs for a named suggestion are ordered by the suggestion name.
155147
*
148+
* Note: The order of the sorted score docs depends on the shard index in the result array if the merge process needs to disambiguate
149+
* the result. In oder to obtain stable results the shard index (index of the result in the result array) must be the same.
150+
*
156151
* @param ignoreFrom Whether to ignore the from and sort all hits in each shard result.
157152
* Enabled only for scroll search, because that only retrieves hits of length 'size' in the query phase.
158153
* @param resultsArr Shard result holder
@@ -163,26 +158,31 @@ public ScoreDoc[] sortDocs(boolean ignoreFrom, AtomicArray<? extends QuerySearch
163158
return EMPTY_DOCS;
164159
}
165160

161+
final QuerySearchResult result;
166162
boolean canOptimize = false;
167-
QuerySearchResult result = null;
168163
int shardIndex = -1;
169164
if (results.size() == 1) {
170165
canOptimize = true;
171166
result = results.get(0).value.queryResult();
172167
shardIndex = results.get(0).index;
173168
} else {
169+
boolean hasResult = false;
170+
QuerySearchResult resultToOptimize = null;
174171
// lets see if we only got hits from a single shard, if so, we can optimize...
175172
for (AtomicArray.Entry<? extends QuerySearchResultProvider> entry : results) {
176173
if (entry.value.queryResult().hasHits()) {
177-
if (result != null) { // we already have one, can't really optimize
174+
if (hasResult) { // we already have one, can't really optimize
178175
canOptimize = false;
179176
break;
180177
}
181178
canOptimize = true;
182-
result = entry.value.queryResult();
179+
hasResult = true;
180+
resultToOptimize = entry.value.queryResult();
183181
shardIndex = entry.index;
184182
}
185183
}
184+
result = canOptimize ? resultToOptimize : results.get(0).value.queryResult();
185+
assert result != null;
186186
}
187187
if (canOptimize) {
188188
int offset = result.from();
@@ -228,74 +228,62 @@ public ScoreDoc[] sortDocs(boolean ignoreFrom, AtomicArray<? extends QuerySearch
228228
return docs;
229229
}
230230

231-
@SuppressWarnings("unchecked")
232-
AtomicArray.Entry<? extends QuerySearchResultProvider>[] sortedResults = results.toArray(new AtomicArray.Entry[results.size()]);
233-
Arrays.sort(sortedResults, QUERY_RESULT_ORDERING);
234-
QuerySearchResultProvider firstResult = sortedResults[0].value;
235-
236-
int topN = firstResult.queryResult().size();
237-
int from = firstResult.queryResult().from();
238-
if (ignoreFrom) {
239-
from = 0;
240-
}
231+
final int topN = result.queryResult().size();
232+
final int from = ignoreFrom ? 0 : result.queryResult().from();
241233

242234
final TopDocs mergedTopDocs;
243-
int numShards = resultsArr.length();
244-
if (firstResult.queryResult().topDocs() instanceof CollapseTopFieldDocs) {
245-
CollapseTopFieldDocs firstTopDocs = (CollapseTopFieldDocs) firstResult.queryResult().topDocs();
235+
final int numShards = resultsArr.length();
236+
if (result.queryResult().topDocs() instanceof CollapseTopFieldDocs) {
237+
CollapseTopFieldDocs firstTopDocs = (CollapseTopFieldDocs) result.queryResult().topDocs();
246238
final Sort sort = new Sort(firstTopDocs.fields);
247239

248240
final CollapseTopFieldDocs[] shardTopDocs = new CollapseTopFieldDocs[numShards];
249-
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : sortedResults) {
241+
if (result.size() != shardTopDocs.length) {
242+
// TopDocs#merge can't deal with null shard TopDocs
243+
final CollapseTopFieldDocs empty = new CollapseTopFieldDocs(firstTopDocs.field, 0, new FieldDoc[0],
244+
sort.getSort(), new Object[0], Float.NaN);
245+
Arrays.fill(shardTopDocs, empty);
246+
}
247+
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : results) {
250248
TopDocs topDocs = sortedResult.value.queryResult().topDocs();
251249
// the 'index' field is the position in the resultsArr atomic array
252250
shardTopDocs[sortedResult.index] = (CollapseTopFieldDocs) topDocs;
253251
}
254-
// TopDocs#merge can't deal with null shard TopDocs
255-
for (int i = 0; i < shardTopDocs.length; ++i) {
256-
if (shardTopDocs[i] == null) {
257-
shardTopDocs[i] = new CollapseTopFieldDocs(firstTopDocs.field, 0, new FieldDoc[0],
258-
sort.getSort(), new Object[0], Float.NaN);
259-
}
260-
}
261252
mergedTopDocs = CollapseTopFieldDocs.merge(sort, from, topN, shardTopDocs);
262-
} else if (firstResult.queryResult().topDocs() instanceof TopFieldDocs) {
263-
TopFieldDocs firstTopDocs = (TopFieldDocs) firstResult.queryResult().topDocs();
253+
} else if (result.queryResult().topDocs() instanceof TopFieldDocs) {
254+
TopFieldDocs firstTopDocs = (TopFieldDocs) result.queryResult().topDocs();
264255
final Sort sort = new Sort(firstTopDocs.fields);
265256

266257
final TopFieldDocs[] shardTopDocs = new TopFieldDocs[resultsArr.length()];
267-
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : sortedResults) {
258+
if (result.size() != shardTopDocs.length) {
259+
// TopDocs#merge can't deal with null shard TopDocs
260+
final TopFieldDocs empty = new TopFieldDocs(0, new FieldDoc[0], sort.getSort(), Float.NaN);
261+
Arrays.fill(shardTopDocs, empty);
262+
}
263+
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : results) {
268264
TopDocs topDocs = sortedResult.value.queryResult().topDocs();
269265
// the 'index' field is the position in the resultsArr atomic array
270266
shardTopDocs[sortedResult.index] = (TopFieldDocs) topDocs;
271267
}
272-
// TopDocs#merge can't deal with null shard TopDocs
273-
for (int i = 0; i < shardTopDocs.length; ++i) {
274-
if (shardTopDocs[i] == null) {
275-
shardTopDocs[i] = new TopFieldDocs(0, new FieldDoc[0], sort.getSort(), Float.NaN);
276-
}
277-
}
278268
mergedTopDocs = TopDocs.merge(sort, from, topN, shardTopDocs);
279269
} else {
280270
final TopDocs[] shardTopDocs = new TopDocs[resultsArr.length()];
281-
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : sortedResults) {
271+
if (result.size() != shardTopDocs.length) {
272+
// TopDocs#merge can't deal with null shard TopDocs
273+
Arrays.fill(shardTopDocs, Lucene.EMPTY_TOP_DOCS);
274+
}
275+
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : results) {
282276
TopDocs topDocs = sortedResult.value.queryResult().topDocs();
283277
// the 'index' field is the position in the resultsArr atomic array
284278
shardTopDocs[sortedResult.index] = topDocs;
285279
}
286-
// TopDocs#merge can't deal with null shard TopDocs
287-
for (int i = 0; i < shardTopDocs.length; ++i) {
288-
if (shardTopDocs[i] == null) {
289-
shardTopDocs[i] = Lucene.EMPTY_TOP_DOCS;
290-
}
291-
}
292280
mergedTopDocs = TopDocs.merge(from, topN, shardTopDocs);
293281
}
294282

295283
ScoreDoc[] scoreDocs = mergedTopDocs.scoreDocs;
296284
final Map<String, List<Suggestion<CompletionSuggestion.Entry>>> groupedCompletionSuggestions = new HashMap<>();
297285
// group suggestions and assign shard index
298-
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : sortedResults) {
286+
for (AtomicArray.Entry<? extends QuerySearchResultProvider> sortedResult : results) {
299287
Suggest shardSuggest = sortedResult.value.queryResult().suggest();
300288
if (shardSuggest != null) {
301289
for (CompletionSuggestion suggestion : shardSuggest.filter(CompletionSuggestion.class)) {

core/src/test/java/org/elasticsearch/action/search/SearchPhaseControllerTests.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void testSort() throws Exception {
7373
}
7474
int nShards = randomIntBetween(1, 20);
7575
int queryResultSize = randomBoolean() ? 0 : randomIntBetween(1, nShards * 2);
76-
AtomicArray<QuerySearchResultProvider> results = generateQueryResults(nShards, suggestions, queryResultSize);
76+
AtomicArray<QuerySearchResultProvider> results = generateQueryResults(nShards, suggestions, queryResultSize, false);
7777
ScoreDoc[] sortedDocs = searchPhaseController.sortDocs(true, results);
7878
int accumulatedLength = Math.min(queryResultSize, getTotalQueryHits(results));
7979
for (Suggest.Suggestion<?> suggestion : reducedSuggest(results)) {
@@ -83,14 +83,26 @@ public void testSort() throws Exception {
8383
assertThat(sortedDocs.length, equalTo(accumulatedLength));
8484
}
8585

86+
public void testSortIsIdempotent() throws IOException {
87+
int nShards = randomIntBetween(1, 20);
88+
int queryResultSize = randomBoolean() ? 0 : randomIntBetween(1, nShards * 2);
89+
AtomicArray<QuerySearchResultProvider> results = generateQueryResults(nShards, Collections.emptyList(), queryResultSize,
90+
randomBoolean() || true);
91+
boolean ignoreFrom = randomBoolean();
92+
ScoreDoc[] sortedDocs = searchPhaseController.sortDocs(ignoreFrom, results);
93+
94+
ScoreDoc[] sortedDocs2 = searchPhaseController.sortDocs(ignoreFrom, results);
95+
assertArrayEquals(sortedDocs, sortedDocs2);
96+
}
97+
8698
public void testMerge() throws IOException {
8799
List<CompletionSuggestion> suggestions = new ArrayList<>();
88100
for (int i = 0; i < randomIntBetween(1, 5); i++) {
89101
suggestions.add(new CompletionSuggestion(randomAsciiOfLength(randomIntBetween(1, 5)), randomIntBetween(1, 20)));
90102
}
91103
int nShards = randomIntBetween(1, 20);
92104
int queryResultSize = randomBoolean() ? 0 : randomIntBetween(1, nShards * 2);
93-
AtomicArray<QuerySearchResultProvider> queryResults = generateQueryResults(nShards, suggestions, queryResultSize);
105+
AtomicArray<QuerySearchResultProvider> queryResults = generateQueryResults(nShards, suggestions, queryResultSize, false);
94106

95107
// calculate offsets and score doc array
96108
List<ScoreDoc> mergedScoreDocs = new ArrayList<>();
@@ -127,7 +139,7 @@ public void testMerge() throws IOException {
127139

128140
private AtomicArray<QuerySearchResultProvider> generateQueryResults(int nShards,
129141
List<CompletionSuggestion> suggestions,
130-
int searchHitsSize) {
142+
int searchHitsSize, boolean useConstantScore) {
131143
AtomicArray<QuerySearchResultProvider> queryResults = new AtomicArray<>(nShards);
132144
for (int shardIndex = 0; shardIndex < nShards; shardIndex++) {
133145
QuerySearchResult querySearchResult = new QuerySearchResult(shardIndex,
@@ -138,7 +150,7 @@ private AtomicArray<QuerySearchResultProvider> generateQueryResults(int nShards,
138150
ScoreDoc[] scoreDocs = new ScoreDoc[nDocs];
139151
float maxScore = 0F;
140152
for (int i = 0; i < nDocs; i++) {
141-
float score = Math.abs(randomFloat());
153+
float score = useConstantScore ? 1.0F : Math.abs(randomFloat());
142154
scoreDocs[i] = new ScoreDoc(i, score);
143155
if (score > maxScore) {
144156
maxScore = score;

0 commit comments

Comments
 (0)