Skip to content

Commit d524718

Browse files
Merge branch 'main' into ml-remove-stream-any
2 parents d6bbd6b + 0698d73 commit d524718

File tree

11 files changed

+344
-26
lines changed

11 files changed

+344
-26
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
@@ -392,7 +392,7 @@ public void parse(DocumentParserContext context) throws IOException {
392392
// parse
393393
XContentParser parser = context.parser();
394394
Token token = parser.currentToken();
395-
Map<String, CompletionInputMetadata> inputMap = Maps.newMapWithExpectedSize(1);
395+
Map<String, CompletionInputMetadataContainer> inputMap = Maps.newMapWithExpectedSize(1);
396396

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

407407
// index
408-
for (Map.Entry<String, CompletionInputMetadata> completionInput : inputMap.entrySet()) {
408+
for (Map.Entry<String, CompletionInputMetadataContainer> completionInput : inputMap.entrySet()) {
409409
String input = completionInput.getKey();
410410
if (input.trim().isEmpty()) {
411411
context.addIgnoredField(mappedFieldType.name());
@@ -420,21 +420,33 @@ public void parse(DocumentParserContext context) throws IOException {
420420
}
421421
input = input.substring(0, len);
422422
}
423-
CompletionInputMetadata metadata = completionInput.getValue();
423+
CompletionInputMetadataContainer cmc = completionInput.getValue();
424424
if (fieldType().hasContextMappings()) {
425-
fieldType().getContextMappings().addField(context.doc(), fieldType().name(), input, metadata.weight, metadata.contexts);
425+
for (CompletionInputMetadata metadata : cmc.getValues()) {
426+
fieldType().getContextMappings().addField(context.doc(), fieldType().name(), input, metadata.weight, metadata.contexts);
427+
}
426428
} else {
427-
context.doc().add(new SuggestField(fieldType().name(), input, metadata.weight));
429+
context.doc().add(new SuggestField(fieldType().name(), input, cmc.getWeight()));
428430
}
429431
}
430-
431432
context.addToFieldNames(fieldType().name());
432-
for (CompletionInputMetadata metadata : inputMap.values()) {
433-
multiFields().parse(
434-
this,
435-
context,
436-
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
437-
);
433+
for (CompletionInputMetadataContainer cmc : inputMap.values()) {
434+
if (fieldType().hasContextMappings()) {
435+
for (CompletionInputMetadata metadata : cmc.getValues()) {
436+
multiFields().parse(
437+
this,
438+
context,
439+
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
440+
);
441+
}
442+
} else {
443+
CompletionInputMetadata metadata = cmc.getValue();
444+
multiFields().parse(
445+
this,
446+
context,
447+
() -> context.switchParser(new MultiFieldParser(metadata, fieldType().name(), context.parser().getTokenLocation()))
448+
);
449+
}
438450
}
439451
}
440452

@@ -447,11 +459,13 @@ private void parse(
447459
DocumentParserContext documentParserContext,
448460
Token token,
449461
XContentParser parser,
450-
Map<String, CompletionInputMetadata> inputMap
462+
Map<String, CompletionInputMetadataContainer> inputMap
451463
) throws IOException {
452464
String currentFieldName = null;
453465
if (token == Token.VALUE_STRING) {
454-
inputMap.put(parser.text(), new CompletionInputMetadata(parser.text(), Collections.<String, Set<String>>emptyMap(), 1));
466+
CompletionInputMetadataContainer cmc = new CompletionInputMetadataContainer(fieldType().hasContextMappings());
467+
cmc.add(new CompletionInputMetadata(parser.text(), Collections.emptyMap(), 1));
468+
inputMap.put(parser.text(), cmc);
455469
} else if (token == Token.START_OBJECT) {
456470
Set<String> inputs = new HashSet<>();
457471
int weight = 1;
@@ -531,8 +545,14 @@ private void parse(
531545
}
532546
}
533547
for (String input : inputs) {
534-
if (inputMap.containsKey(input) == false || inputMap.get(input).weight < weight) {
535-
inputMap.put(input, new CompletionInputMetadata(input, contextsMap, weight));
548+
CompletionInputMetadata cm = new CompletionInputMetadata(input, contextsMap, weight);
549+
CompletionInputMetadataContainer cmc = inputMap.get(input);
550+
if (cmc != null) {
551+
cmc.add(cm);
552+
} else {
553+
cmc = new CompletionInputMetadataContainer(fieldType().hasContextMappings());
554+
cmc.add(cm);
555+
inputMap.put(input, cmc);
536556
}
537557
}
538558
} else {
@@ -543,10 +563,46 @@ private void parse(
543563
}
544564
}
545565

566+
static class CompletionInputMetadataContainer {
567+
private final boolean hasContexts;
568+
private final List<CompletionInputMetadata> list;
569+
private CompletionInputMetadata single;
570+
571+
CompletionInputMetadataContainer(boolean hasContexts) {
572+
this.hasContexts = hasContexts;
573+
this.list = hasContexts ? new ArrayList<>() : null;
574+
}
575+
576+
void add(CompletionInputMetadata cm) {
577+
if (hasContexts) {
578+
list.add(cm);
579+
} else {
580+
if (single == null || single.weight < cm.weight) {
581+
single = cm;
582+
}
583+
}
584+
}
585+
586+
List<CompletionInputMetadata> getValues() {
587+
assert hasContexts;
588+
return list;
589+
}
590+
591+
CompletionInputMetadata getValue() {
592+
assert hasContexts == false;
593+
return single;
594+
}
595+
596+
int getWeight() {
597+
assert hasContexts == false;
598+
return single.weight;
599+
}
600+
}
601+
546602
static class CompletionInputMetadata {
547-
public final String input;
548-
public final Map<String, Set<String>> contexts;
549-
public final int weight;
603+
private final String input;
604+
private final Map<String, Set<String>> contexts;
605+
private final int weight;
550606

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

server/src/main/java/org/elasticsearch/node/NodeConstruction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ public Map<String, String> searchFields() {
10671067
actionModule.getReservedClusterStateService().installStateHandler(new ReservedRepositoryAction(repositoriesService));
10681068
actionModule.getReservedClusterStateService().installStateHandler(new ReservedPipelineAction());
10691069

1070-
FileSettingsHealthIndicatorService fileSettingsHealthIndicatorService = new FileSettingsHealthIndicatorService();
1070+
FileSettingsHealthIndicatorService fileSettingsHealthIndicatorService = new FileSettingsHealthIndicatorService(settings);
10711071
FileSettingsService fileSettingsService = new FileSettingsService(
10721072
clusterService,
10731073
actionModule.getReservedClusterStateService(),

server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
2222
import org.elasticsearch.cluster.service.ClusterService;
2323
import org.elasticsearch.common.file.MasterNodeFileWatchingService;
24+
import org.elasticsearch.common.settings.Setting;
25+
import org.elasticsearch.common.settings.Settings;
2426
import org.elasticsearch.env.Environment;
2527
import org.elasticsearch.health.HealthIndicatorDetails;
2628
import org.elasticsearch.health.HealthIndicatorImpact;
@@ -212,7 +214,7 @@ protected void onProcessFileChangesException(Exception e) {
212214
}
213215

214216
@Override
215-
protected void processInitialFileMissing() throws ExecutionException, InterruptedException, IOException {
217+
protected void processInitialFileMissing() throws ExecutionException, InterruptedException {
216218
PlainActionFuture<ActionResponse.Empty> completion = new PlainActionFuture<>();
217219
logger.info("setting file [{}] not found, initializing [{}] as empty", watchedFile(), NAMESPACE);
218220
stateService.initEmpty(NAMESPACE, completion);
@@ -236,11 +238,29 @@ public static class FileSettingsHealthIndicatorService implements HealthIndicato
236238
)
237239
);
238240

241+
/**
242+
* We want a length limit so we don't blow past the indexing limit in the case of a long description string.
243+
* This is an {@code OperatorDynamic} setting so that if the truncation hampers troubleshooting efforts,
244+
* the operator could override it and retry the operation without necessarily restarting the cluster.
245+
*/
246+
public static final String DESCRIPTION_LENGTH_LIMIT_KEY = "fileSettings.descriptionLengthLimit";
247+
static final Setting<Integer> DESCRIPTION_LENGTH_LIMIT = Setting.intSetting(
248+
DESCRIPTION_LENGTH_LIMIT_KEY,
249+
100,
250+
1, // Need room for the ellipsis
251+
Setting.Property.OperatorDynamic
252+
);
253+
254+
private final Settings settings;
239255
private boolean isActive = false;
240256
private long changeCount = 0;
241257
private long failureStreak = 0;
242258
private String mostRecentFailure = null;
243259

260+
public FileSettingsHealthIndicatorService(Settings settings) {
261+
this.settings = settings;
262+
}
263+
244264
public synchronized void startOccurred() {
245265
isActive = true;
246266
failureStreak = 0;
@@ -262,7 +282,16 @@ public synchronized void successOccurred() {
262282

263283
public synchronized void failureOccurred(String description) {
264284
++failureStreak;
265-
mostRecentFailure = description;
285+
mostRecentFailure = limitLength(description);
286+
}
287+
288+
private String limitLength(String description) {
289+
int descriptionLengthLimit = DESCRIPTION_LENGTH_LIMIT.get(settings);
290+
if (description.length() > descriptionLengthLimit) {
291+
return description.substring(0, descriptionLengthLimit - 1) + "…";
292+
} else {
293+
return description;
294+
}
266295
}
267296

268297
@Override

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

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

2727
public static final NodeFeature RETRIEVER_RESCORER_ENABLED = new NodeFeature("search.retriever.rescorer.enabled");
28+
public static final NodeFeature COMPLETION_FIELD_SUPPORTS_DUPLICATE_SUGGESTIONS = new NodeFeature(
29+
"search.completion_field.duplicate.support"
30+
);
2831

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

0 commit comments

Comments
 (0)