Skip to content

Commit 784c12e

Browse files
author
Christoph Büscher
authored
Fix exception when merging completion suggestions (#70414)
Under certain circumstances we can get "array_index_out_of_bounds" exceptions in the fetch phase when merging comletion suggestion results with additonal field collapsing in place. We store the ScoreDoc array in the SortedTopDocs score docs contain both regular search hits and completion suggestion results added in SearchPhaseController#sortDocs, so when merging these we need to know the correct array offset where the completion results begin. This is based on the hits length calculated in SearchPhaseController#getHits, which doesn't take into account that there might be suggestion results present when calculating the number of hits. This change adds the number of suggestions to SortedTopDocs in order to be able to later account for it when calculating the hits. Closes #70328
1 parent 45878e7 commit 784c12e

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

server/src/internalClusterTest/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import org.elasticsearch.common.xcontent.XContentBuilder;
2727
import org.elasticsearch.common.xcontent.XContentFactory;
2828
import org.elasticsearch.index.mapper.MapperParsingException;
29+
import org.elasticsearch.index.query.QueryBuilders;
2930
import org.elasticsearch.plugins.Plugin;
3031
import org.elasticsearch.search.aggregations.AggregationBuilders;
3132
import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode;
33+
import org.elasticsearch.search.collapse.CollapseBuilder;
3234
import org.elasticsearch.search.sort.FieldSortBuilder;
3335
import org.elasticsearch.search.suggest.completion.CompletionStats;
3436
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
@@ -1217,6 +1219,58 @@ public void testSuggestOnlyExplain() throws Exception {
12171219
assertSuggestions(searchResponse, "foo", "suggestion10", "suggestion9", "suggestion8", "suggestion7", "suggestion6");
12181220
}
12191221

1222+
public void testCompletionWithCollapse() throws Exception {
1223+
String suggestField = "suggest_field";
1224+
XContentBuilder mapping = jsonBuilder().startObject()
1225+
.startObject("properties")
1226+
.startObject("collapse_field")
1227+
.field("type", "keyword")
1228+
.endObject()
1229+
.startObject(suggestField)
1230+
.field("type", "completion")
1231+
.field("analyzer", "whitespace")
1232+
.endObject()
1233+
.endObject()
1234+
.endObject();
1235+
1236+
String index = "test";
1237+
assertAcked(
1238+
client().admin()
1239+
.indices()
1240+
.prepareCreate(index)
1241+
.setSettings(Settings.builder().put("index.number_of_shards", 2))
1242+
.setMapping(mapping)
1243+
.get()
1244+
);
1245+
1246+
int numDocs = 2;
1247+
for (int i = 0; i < numDocs; i++) {
1248+
XContentBuilder builder = jsonBuilder().startObject();
1249+
builder.startObject(suggestField).field("input", "suggestion" + i).field("weight", i).endObject();
1250+
builder.field("collapse_field", "collapse me").endObject(); // all docs the same value for collapsing
1251+
client().prepareIndex(index).setId("" + i).setSource(builder).get();
1252+
}
1253+
client().admin().indices().prepareRefresh(index).get();
1254+
CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion(suggestField).prefix("sug").size(1);
1255+
1256+
SearchResponse searchResponse = client().prepareSearch("test")
1257+
.setQuery(QueryBuilders.matchAllQuery())
1258+
.setFrom(1)
1259+
.setSize(1)
1260+
.setCollapse(new CollapseBuilder("collapse_field"))
1261+
.suggest(new SuggestBuilder().addSuggestion("the_suggestion", prefix))
1262+
.get();
1263+
assertAllSuccessful(searchResponse);
1264+
1265+
assertThat(searchResponse.getSuggest().getSuggestion("the_suggestion"), is(notNullValue()));
1266+
Suggest.Suggestion<Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option>> suggestion = searchResponse.getSuggest()
1267+
.getSuggestion("the_suggestion");
1268+
1269+
List<String> suggestionList = getNames(suggestion.getEntries().get(0));
1270+
assertThat(suggestionList, contains("suggestion" + (numDocs - 1)));
1271+
assertEquals(0, searchResponse.getHits().getHits().length);
1272+
}
1273+
12201274
public static boolean isReservedChar(char c) {
12211275
switch (c) {
12221276
case '\u001F':

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ static SortedTopDocs sortDocs(boolean ignoreFrom, final Collection<TopDocs> topD
149149
final TopDocs mergedTopDocs = mergeTopDocs(topDocs, size, ignoreFrom ? 0 : from);
150150
final ScoreDoc[] mergedScoreDocs = mergedTopDocs == null ? EMPTY_DOCS : mergedTopDocs.scoreDocs;
151151
ScoreDoc[] scoreDocs = mergedScoreDocs;
152+
int numSuggestDocs = 0;
152153
if (reducedCompletionSuggestions.isEmpty() == false) {
153-
int numSuggestDocs = 0;
154154
for (CompletionSuggestion completionSuggestion : reducedCompletionSuggestions) {
155155
assert completionSuggestion != null;
156156
numSuggestDocs += completionSuggestion.getOptions().size();
@@ -180,7 +180,7 @@ static SortedTopDocs sortDocs(boolean ignoreFrom, final Collection<TopDocs> topD
180180
isSortedByField = true;
181181
}
182182
}
183-
return new SortedTopDocs(scoreDocs, isSortedByField, sortFields, collapseField, collapseValues);
183+
return new SortedTopDocs(scoreDocs, isSortedByField, sortFields, collapseField, collapseValues, numSuggestDocs);
184184
}
185185

186186
static TopDocs mergeTopDocs(Collection<TopDocs> results, int topN, int from) {
@@ -316,7 +316,8 @@ private SearchHits getHits(ReducedQueryPhase reducedQueryPhase, boolean ignoreFr
316316
int from = ignoreFrom ? 0 : reducedQueryPhase.from;
317317
int numSearchHits = (int) Math.min(reducedQueryPhase.fetchHits - from, reducedQueryPhase.size);
318318
// with collapsing we can have more fetch hits than sorted docs
319-
numSearchHits = Math.min(sortedTopDocs.scoreDocs.length, numSearchHits);
319+
// also we need to take into account that we potentially have completion suggestions stored in the scoreDocs array
320+
numSearchHits = Math.min(sortedTopDocs.scoreDocs.length - sortedTopDocs.numberOfCompletionsSuggestions, numSearchHits);
320321
// merge hits
321322
List<SearchHit> hits = new ArrayList<>();
322323
if (fetchResults.isEmpty() == false) {
@@ -665,7 +666,7 @@ void add(TopDocsAndMaxScore topDocs, boolean timedOut, Boolean terminatedEarly)
665666
}
666667

667668
static final class SortedTopDocs {
668-
static final SortedTopDocs EMPTY = new SortedTopDocs(EMPTY_DOCS, false, null, null, null);
669+
static final SortedTopDocs EMPTY = new SortedTopDocs(EMPTY_DOCS, false, null, null, null, 0);
669670
// the searches merged top docs
670671
final ScoreDoc[] scoreDocs;
671672
// <code>true</code> iff the result score docs is sorted by a field (not score), this implies that <code>sortField</code> is set.
@@ -674,14 +675,16 @@ static final class SortedTopDocs {
674675
final SortField[] sortFields;
675676
final String collapseField;
676677
final Object[] collapseValues;
678+
final int numberOfCompletionsSuggestions;
677679

678680
SortedTopDocs(ScoreDoc[] scoreDocs, boolean isSortedByField, SortField[] sortFields,
679-
String collapseField, Object[] collapseValues) {
681+
String collapseField, Object[] collapseValues, int numberOfCompletionsSuggestions) {
680682
this.scoreDocs = scoreDocs;
681683
this.isSortedByField = isSortedByField;
682684
this.sortFields = sortFields;
683685
this.collapseField = collapseField;
684686
this.collapseValues = collapseValues;
687+
this.numberOfCompletionsSuggestions = numberOfCompletionsSuggestions;
685688
}
686689
}
687690
}

0 commit comments

Comments
 (0)