Skip to content

Commit 18b5fe8

Browse files
Support duplicate suggestions in completion field (#121324) (#121457)
Currently if a document has duplicate suggestions across different contexts, only the first gets indexed, and when a user tries to search using the second context, she will get 0 results. This PR addresses this, but adding support for duplicate suggestions across different contexts, so documents like below with duplicate inputs can be searched across all provided contexts. ```json { "my_suggest": [ { "input": [ "foox", "boo" ], "weight" : 2, "contexts": { "color": [ "red" ] } }, { "input": [ "foox" ], "weight" : 3, "contexts": { "color": [ "blue" ] } } ] } ``` Closes #82432
1 parent 452f9cb commit 18b5fe8

File tree

6 files changed

+283
-20
lines changed

6 files changed

+283
-20
lines changed

docs/changelog/121324.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 121324
2+
summary: Support duplicate suggestions in completion field
3+
area: Suggesters
4+
type: bug
5+
issues:
6+
- 82432

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/suggest/30_context.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,75 @@ setup:
395395
field: suggest_multi_contexts
396396
contexts:
397397
location: []
398+
399+
---
400+
"Duplicate suggestions in different contexts":
401+
- requires:
402+
cluster_features: [ "search.completion_field.duplicate.support" ]
403+
reason: "Support for duplicate suggestions in different contexts"
404+
405+
- do:
406+
index:
407+
refresh: true
408+
index: test
409+
id: "1"
410+
body:
411+
suggest_context:
412+
-
413+
input: "foox"
414+
weight: 2
415+
contexts:
416+
color: ["red", "yellow"]
417+
-
418+
input: "foox"
419+
weight: 3
420+
contexts:
421+
color: ["blue", "green", "yellow"]
422+
- do:
423+
search:
424+
body:
425+
suggest:
426+
result:
427+
text: "foo"
428+
completion:
429+
field: suggest_context
430+
contexts:
431+
color: "red"
432+
433+
- length: { suggest.result: 1 }
434+
- length: { suggest.result.0.options: 1 }
435+
- match: { suggest.result.0.options.0.text: "foox" }
436+
- match: { suggest.result.0.options.0._score: 2 }
437+
438+
- do:
439+
search:
440+
body:
441+
suggest:
442+
result:
443+
text: "foo"
444+
completion:
445+
field: suggest_context
446+
contexts:
447+
color: "yellow"
448+
449+
- length: { suggest.result: 1 }
450+
- length: { suggest.result.0.options: 1 }
451+
- match: { suggest.result.0.options.0.text: "foox" }
452+
# the highest weight wins
453+
- match: { suggest.result.0.options.0._score: 3 }
454+
455+
- do:
456+
search:
457+
body:
458+
suggest:
459+
result:
460+
text: "foo"
461+
completion:
462+
field: suggest_context
463+
contexts:
464+
color: "blue"
465+
466+
- length: { suggest.result: 1 }
467+
- length: { suggest.result.0.options: 1 }
468+
- match: { suggest.result.0.options.0.text: "foox" }
469+
- match: { suggest.result.0.options.0._score: 3 }

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/suggest/50_completion_with_multi_fields.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,80 @@
268268

269269
- length: { suggest.result: 1 }
270270
- length: { suggest.result.0.options: 1 }
271+
272+
---
273+
"Duplicate suggestions in different contexts in sub-fields":
274+
- requires:
275+
cluster_features: [ "search.completion_field.duplicate.support" ]
276+
reason: "Support for duplicate suggestions in different contexts"
277+
278+
- do:
279+
indices.create:
280+
index: completion_with_context
281+
body:
282+
mappings:
283+
"properties":
284+
"suggest_1":
285+
"type": "completion"
286+
"contexts":
287+
-
288+
"name": "color"
289+
"type": "category"
290+
"fields":
291+
"suggest_2":
292+
"type": "completion"
293+
"contexts":
294+
-
295+
"name": "color"
296+
"type": "category"
297+
298+
299+
- do:
300+
index:
301+
refresh: true
302+
index: completion_with_context
303+
id: "1"
304+
body:
305+
suggest_1:
306+
-
307+
input: "foox"
308+
weight: 2
309+
contexts:
310+
color: ["red"]
311+
-
312+
input: "foox"
313+
weight: 3
314+
contexts:
315+
color: ["blue", "green"]
316+
- do:
317+
search:
318+
body:
319+
suggest:
320+
result:
321+
text: "foo"
322+
completion:
323+
field: suggest_1.suggest_2
324+
contexts:
325+
color: "red"
326+
327+
- length: { suggest.result: 1 }
328+
- length: { suggest.result.0.options: 1 }
329+
- match: { suggest.result.0.options.0.text: "foox" }
330+
- match: { suggest.result.0.options.0._score: 2 }
331+
332+
333+
- do:
334+
search:
335+
body:
336+
suggest:
337+
result:
338+
text: "foo"
339+
completion:
340+
field: suggest_1.suggest_2
341+
contexts:
342+
color: "blue"
343+
344+
- length: { suggest.result: 1 }
345+
- length: { suggest.result.0.options: 1 }
346+
- match: { suggest.result.0.options.0.text: "foox" }
347+
- match: { suggest.result.0.options.0._score: 3 }

server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ public void parse(DocumentParserContext context) throws IOException {
400400
// parse
401401
XContentParser parser = context.parser();
402402
Token token = parser.currentToken();
403-
Map<String, CompletionInputMetadata> inputMap = Maps.newMapWithExpectedSize(1);
403+
Map<String, CompletionInputMetadataContainer> inputMap = Maps.newMapWithExpectedSize(1);
404404

405405
if (token == Token.VALUE_NULL) { // ignore null values
406406
return;
@@ -413,7 +413,7 @@ public void parse(DocumentParserContext context) throws IOException {
413413
}
414414

415415
// index
416-
for (Map.Entry<String, CompletionInputMetadata> completionInput : inputMap.entrySet()) {
416+
for (Map.Entry<String, CompletionInputMetadataContainer> completionInput : inputMap.entrySet()) {
417417
String input = completionInput.getKey();
418418
if (input.trim().isEmpty()) {
419419
context.addIgnoredField(mappedFieldType.name());
@@ -428,21 +428,33 @@ public void parse(DocumentParserContext context) throws IOException {
428428
}
429429
input = input.substring(0, len);
430430
}
431-
CompletionInputMetadata metadata = completionInput.getValue();
431+
CompletionInputMetadataContainer cmc = completionInput.getValue();
432432
if (fieldType().hasContextMappings()) {
433-
fieldType().getContextMappings().addField(context.doc(), fieldType().name(), input, metadata.weight, metadata.contexts);
433+
for (CompletionInputMetadata metadata : cmc.getValues()) {
434+
fieldType().getContextMappings().addField(context.doc(), fieldType().name(), input, metadata.weight, metadata.contexts);
435+
}
434436
} else {
435-
context.doc().add(new SuggestField(fieldType().name(), input, metadata.weight));
437+
context.doc().add(new SuggestField(fieldType().name(), input, cmc.getWeight()));
436438
}
437439
}
438-
439440
context.addToFieldNames(fieldType().name());
440-
for (CompletionInputMetadata metadata : inputMap.values()) {
441-
multiFields().parse(
442-
this,
443-
context,
444-
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
445-
);
441+
for (CompletionInputMetadataContainer cmc : inputMap.values()) {
442+
if (fieldType().hasContextMappings()) {
443+
for (CompletionInputMetadata metadata : cmc.getValues()) {
444+
multiFields().parse(
445+
this,
446+
context,
447+
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
448+
);
449+
}
450+
} else {
451+
CompletionInputMetadata metadata = cmc.getValue();
452+
multiFields().parse(
453+
this,
454+
context,
455+
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
456+
);
457+
}
446458
}
447459
}
448460

@@ -455,11 +467,13 @@ private void parse(
455467
DocumentParserContext documentParserContext,
456468
Token token,
457469
XContentParser parser,
458-
Map<String, CompletionInputMetadata> inputMap
470+
Map<String, CompletionInputMetadataContainer> inputMap
459471
) throws IOException {
460472
String currentFieldName = null;
461473
if (token == Token.VALUE_STRING) {
462-
inputMap.put(parser.text(), new CompletionInputMetadata(parser.text(), Collections.<String, Set<String>>emptyMap(), 1));
474+
CompletionInputMetadataContainer cmc = new CompletionInputMetadataContainer(fieldType().hasContextMappings());
475+
cmc.add(new CompletionInputMetadata(parser.text(), Collections.emptyMap(), 1));
476+
inputMap.put(parser.text(), cmc);
463477
} else if (token == Token.START_OBJECT) {
464478
Set<String> inputs = new HashSet<>();
465479
int weight = 1;
@@ -539,8 +553,14 @@ private void parse(
539553
}
540554
}
541555
for (String input : inputs) {
542-
if (inputMap.containsKey(input) == false || inputMap.get(input).weight < weight) {
543-
inputMap.put(input, new CompletionInputMetadata(input, contextsMap, weight));
556+
CompletionInputMetadata cm = new CompletionInputMetadata(input, contextsMap, weight);
557+
CompletionInputMetadataContainer cmc = inputMap.get(input);
558+
if (cmc != null) {
559+
cmc.add(cm);
560+
} else {
561+
cmc = new CompletionInputMetadataContainer(fieldType().hasContextMappings());
562+
cmc.add(cm);
563+
inputMap.put(input, cmc);
544564
}
545565
}
546566
} else {
@@ -551,10 +571,46 @@ private void parse(
551571
}
552572
}
553573

574+
static class CompletionInputMetadataContainer {
575+
private final boolean hasContexts;
576+
private final List<CompletionInputMetadata> list;
577+
private CompletionInputMetadata single;
578+
579+
CompletionInputMetadataContainer(boolean hasContexts) {
580+
this.hasContexts = hasContexts;
581+
this.list = hasContexts ? new ArrayList<>() : null;
582+
}
583+
584+
void add(CompletionInputMetadata cm) {
585+
if (hasContexts) {
586+
list.add(cm);
587+
} else {
588+
if (single == null || single.weight < cm.weight) {
589+
single = cm;
590+
}
591+
}
592+
}
593+
594+
List<CompletionInputMetadata> getValues() {
595+
assert hasContexts;
596+
return list;
597+
}
598+
599+
CompletionInputMetadata getValue() {
600+
assert hasContexts == false;
601+
return single;
602+
}
603+
604+
int getWeight() {
605+
assert hasContexts == false;
606+
return single.weight;
607+
}
608+
}
609+
554610
static class CompletionInputMetadata {
555-
public final String input;
556-
public final Map<String, Set<String>> contexts;
557-
public final int weight;
611+
private final String input;
612+
private final Map<String, Set<String>> contexts;
613+
private final int weight;
558614

559615
CompletionInputMetadata(String input, Map<String, Set<String>> contexts, int weight) {
560616
this.input = input;

server/src/main/java/org/elasticsearch/search/SearchFeatures.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ public Set<NodeFeature> getFeatures() {
2222
}
2323

2424
public static final NodeFeature RETRIEVER_RESCORER_ENABLED = new NodeFeature("search.retriever.rescorer.enabled");
25+
public static final NodeFeature COMPLETION_FIELD_SUPPORTS_DUPLICATE_SUGGESTIONS = new NodeFeature(
26+
"search.completion_field.duplicate.support"
27+
);
2528

2629
@Override
2730
public Set<NodeFeature> getTestFeatures() {
28-
return Set.of(RETRIEVER_RESCORER_ENABLED);
31+
return Set.of(RETRIEVER_RESCORER_ENABLED, COMPLETION_FIELD_SUPPORTS_DUPLICATE_SUGGESTIONS);
2932
}
3033
}

server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,55 @@ public void testKeywordWithSubCompletionAndContext() throws Exception {
303303
);
304304
}
305305

306+
public void testDuplicateSuggestionsWithContexts() throws IOException {
307+
DocumentMapper defaultMapper = createDocumentMapper(fieldMapping(b -> {
308+
b.field("type", "completion");
309+
b.startArray("contexts");
310+
{
311+
b.startObject();
312+
b.field("name", "place");
313+
b.field("type", "category");
314+
b.endObject();
315+
}
316+
b.endArray();
317+
}));
318+
319+
ParsedDocument parsedDocument = defaultMapper.parse(source(b -> {
320+
b.startArray("field");
321+
{
322+
b.startObject();
323+
{
324+
b.array("input", "timmy", "starbucks");
325+
b.startObject("contexts").array("place", "cafe", "food").endObject();
326+
b.field("weight", 10);
327+
}
328+
b.endObject();
329+
b.startObject();
330+
{
331+
b.array("input", "timmy", "starbucks");
332+
b.startObject("contexts").array("place", "restaurant").endObject();
333+
b.field("weight", 1);
334+
}
335+
b.endObject();
336+
}
337+
b.endArray();
338+
}));
339+
340+
List<IndexableField> indexedFields = parsedDocument.rootDoc().getFields("field");
341+
assertThat(indexedFields, hasSize(4));
342+
343+
assertThat(
344+
indexedFields,
345+
containsInAnyOrder(
346+
contextSuggestField("timmy"),
347+
contextSuggestField("timmy"),
348+
contextSuggestField("starbucks"),
349+
contextSuggestField("starbucks")
350+
)
351+
);
352+
353+
}
354+
306355
public void testCompletionWithContextAndSubCompletion() throws Exception {
307356
DocumentMapper defaultMapper = createDocumentMapper(fieldMapping(b -> {
308357
b.field("type", "completion");

0 commit comments

Comments
 (0)