Skip to content

Commit 4574802

Browse files
authored
Merge V2 index/component template mappings in specific manner (#55607)
This commit changes the way that V2 index, component, and request mappings are merged. Specifically: - Fields are merged in a "replacement" manner, meaning that the entire definition is replaced rather than merging the interior configuration - Mapping metadata (all fields outside of `properties`) are merged recursively. The merging for V1 templates does not change. Relates to #53101
1 parent bd64da0 commit 4574802

File tree

4 files changed

+154
-21
lines changed

4 files changed

+154
-21
lines changed

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ private ClusterState applyCreateIndexRequestWithV1Templates(final ClusterState c
456456
logger.info("applying create index request using v1 templates {}",
457457
templates.stream().map(IndexTemplateMetadata::name).collect(Collectors.toList()));
458458

459-
final Map<String, Object> mappings = Collections.unmodifiableMap(parseMappings(request.mappings(),
459+
final Map<String, Object> mappings = Collections.unmodifiableMap(parseV1Mappings(request.mappings(),
460460
templates.stream().map(IndexTemplateMetadata::getMappings).collect(toList()), xContentRegistry));
461461

462462
final Settings aggregatedIndexSettings =
@@ -483,8 +483,7 @@ private ClusterState applyCreateIndexRequestWithV2Template(final ClusterState cu
483483
throws Exception {
484484
logger.info("applying create index request using v2 template [{}]", templateName);
485485

486-
final Map<String, Object> mappings = Collections.unmodifiableMap(parseMappings(request.mappings(),
487-
MetadataIndexTemplateService.resolveMappings(currentState, templateName), xContentRegistry));
486+
final Map<String, Object> mappings = resolveV2Mappings(request.mappings(), currentState, templateName, xContentRegistry);
488487

489488
final Settings aggregatedIndexSettings =
490489
aggregateIndexSettings(currentState, request,
@@ -503,6 +502,15 @@ private ClusterState applyCreateIndexRequestWithV2Template(final ClusterState cu
503502
Collections.singletonList(templateName), metadataTransformer);
504503
}
505504

505+
static Map<String, Object> resolveV2Mappings(final String requestMappings,
506+
final ClusterState currentState,
507+
final String templateName,
508+
final NamedXContentRegistry xContentRegistry) throws Exception {
509+
final Map<String, Object> mappings = Collections.unmodifiableMap(parseV2Mappings(requestMappings,
510+
MetadataIndexTemplateService.resolveMappings(currentState, templateName), xContentRegistry));
511+
return mappings;
512+
}
513+
506514
private ClusterState applyCreateIndexRequestWithExistingMetadata(final ClusterState currentState,
507515
final CreateIndexClusterStateUpdateRequest request,
508516
final boolean silent,
@@ -529,13 +537,76 @@ private ClusterState applyCreateIndexRequestWithExistingMetadata(final ClusterSt
529537
}
530538

531539
/**
532-
* Parses the provided mappings json and the inheritable mappings from the templates (if any) into a map.
540+
* Parses the provided mappings json and the inheritable mappings from the templates (if any)
541+
* into a map.
533542
*
534-
* The template mappings are applied in the order they are encountered in the list (clients should make sure the lower index, closer
535-
* to the head of the list, templates have the highest {@link IndexTemplateMetadata#order()})
543+
* The template mappings are applied in the order they are encountered in the list, with the
544+
* caveat that mapping fields are only merged at the top-level, meaning that field settings are
545+
* not merged, instead they replace any previous field definition.
546+
*/
547+
@SuppressWarnings("unchecked")
548+
static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedXContent> templateMappings,
549+
NamedXContentRegistry xContentRegistry) throws Exception {
550+
Map<String, Object> requestMappings = MapperService.parseMapping(xContentRegistry, mappingsJson);
551+
// apply templates, merging the mappings into the request mapping if exists
552+
Map<String, Object> properties = new HashMap<>();
553+
Map<String, Object> nonProperties = new HashMap<>();
554+
for (CompressedXContent mapping : templateMappings) {
555+
if (mapping != null) {
556+
Map<String, Object> templateMapping = MapperService.parseMapping(xContentRegistry, mapping.string());
557+
if (templateMapping.isEmpty()) {
558+
// Someone provided an empty '{}' for mappings, which is okay, but to avoid
559+
// tripping the below assertion, we can safely ignore it
560+
continue;
561+
}
562+
assert templateMapping.size() == 1 : "expected exactly one mapping value, got: " + templateMapping;
563+
if (templateMapping.get(MapperService.SINGLE_MAPPING_NAME) instanceof Map == false) {
564+
throw new IllegalStateException("invalid mapping definition, expected a single map underneath [" +
565+
MapperService.SINGLE_MAPPING_NAME + "] but it was: [" + templateMapping + "]");
566+
}
567+
568+
Map<String, Object> innerTemplateMapping = (Map<String, Object>) templateMapping.get(MapperService.SINGLE_MAPPING_NAME);
569+
Map<String, Object> innerTemplateNonProperties = new HashMap<>(innerTemplateMapping);
570+
Map<String, Object> maybeProperties = (Map<String, Object>) innerTemplateNonProperties.remove("properties");
571+
572+
XContentHelper.mergeDefaults(innerTemplateNonProperties, nonProperties);
573+
nonProperties = innerTemplateNonProperties;
574+
575+
if (maybeProperties != null) {
576+
properties.putAll(maybeProperties);
577+
}
578+
}
579+
}
580+
581+
if (requestMappings.get(MapperService.SINGLE_MAPPING_NAME) != null) {
582+
Map<String, Object> innerRequestMappings = (Map<String, Object>) requestMappings.get(MapperService.SINGLE_MAPPING_NAME);
583+
Map<String, Object> innerRequestNonProperties = new HashMap<>(innerRequestMappings);
584+
Map<String, Object> maybeRequestProperties = (Map<String, Object>) innerRequestNonProperties.remove("properties");
585+
586+
XContentHelper.mergeDefaults(innerRequestNonProperties, nonProperties);
587+
nonProperties = innerRequestNonProperties;
588+
589+
if (maybeRequestProperties != null) {
590+
properties.putAll(maybeRequestProperties);
591+
}
592+
}
593+
594+
Map<String, Object> finalMappings = new HashMap<>(nonProperties);
595+
finalMappings.put("properties", properties);
596+
return Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, finalMappings);
597+
}
598+
599+
/**
600+
* Parses the provided mappings json and the inheritable mappings from the templates (if any)
601+
* into a map.
602+
*
603+
* The template mappings are applied in the order they are encountered in the list (clients
604+
* should make sure the lower index, closer to the head of the list, templates have the highest
605+
* {@link IndexTemplateMetadata#order()}). This merging makes no distinction between field
606+
* definitions, as may result in an invalid field definition
536607
*/
537-
static Map<String, Object> parseMappings(String mappingsJson, List<CompressedXContent> templateMappings,
538-
NamedXContentRegistry xContentRegistry) throws Exception {
608+
static Map<String, Object> parseV1Mappings(String mappingsJson, List<CompressedXContent> templateMappings,
609+
NamedXContentRegistry xContentRegistry) throws Exception {
539610
Map<String, Object> mappings = MapperService.parseMapping(xContentRegistry, mappingsJson);
540611
// apply templates, merging the mappings into the request mapping if exists
541612
for (CompressedXContent mapping : templateMappings) {

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,6 @@ public static List<CompressedXContent> resolveMappings(final ClusterState state,
768768
Optional.ofNullable(template.template())
769769
.map(Template::mappings)
770770
.ifPresent(mappings::add);
771-
// When actually merging mappings, the highest precedence ones should go first, so reverse the list
772-
Collections.reverse(mappings);
773771
return Collections.unmodifiableList(mappings);
774772
}
775773

server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import java.util.Collection;
7474
import java.util.Collections;
7575
import java.util.Comparator;
76+
import java.util.HashMap;
7677
import java.util.List;
7778
import java.util.Locale;
7879
import java.util.Map;
@@ -98,7 +99,7 @@
9899
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.buildIndexMetadata;
99100
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.clusterStateCreateIndex;
100101
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.getIndexNumberOfRoutingShards;
101-
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.parseMappings;
102+
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.parseV1Mappings;
102103
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.resolveAndValidateAliases;
103104
import static org.elasticsearch.cluster.shards.ClusterShardLimitIT.ShardCounts.forDataNodeCount;
104105
import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING;
@@ -622,7 +623,7 @@ public void testParseMappingsAppliesDataFromTemplateAndRequest() throws Exceptio
622623
});
623624
request.mappings(createMapping("mapping_from_request", "text").string());
624625

625-
Map<String, Object> parsedMappings = MetadataCreateIndexService.parseMappings(request.mappings(),
626+
Map<String, Object> parsedMappings = MetadataCreateIndexService.parseV1Mappings(request.mappings(),
626627
List.of(templateMetadata.getMappings()), NamedXContentRegistry.EMPTY);
627628

628629
assertThat(parsedMappings, hasKey("_doc"));
@@ -678,7 +679,7 @@ public void testRequestDataHavePriorityOverTemplateData() throws Exception {
678679
request.aliases(Set.of(new Alias("alias").searchRouting("fromRequest")));
679680
request.settings(Settings.builder().put("key1", "requestValue").build());
680681

681-
Map<String, Object> parsedMappings = MetadataCreateIndexService.parseMappings(request.mappings(),
682+
Map<String, Object> parsedMappings = MetadataCreateIndexService.parseV1Mappings(request.mappings(),
682683
List.of(templateMetadata.mappings()), xContentRegistry());
683684
List<AliasMetadata> resolvedAliases = resolveAndValidateAliases(request.index(), request.aliases(),
684685
MetadataIndexTemplateService.resolveAliases(List.of(templateMetadata)),
@@ -848,7 +849,7 @@ public void testParseMappingsWithTypedTemplateAndTypelessIndexMapping() throws E
848849
}
849850
});
850851

851-
Map<String, Object> mappings = parseMappings("{\"_doc\":{}}", List.of(templateMetadata.mappings()), xContentRegistry());
852+
Map<String, Object> mappings = parseV1Mappings("{\"_doc\":{}}", List.of(templateMetadata.mappings()), xContentRegistry());
852853
assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME));
853854
}
854855

@@ -861,7 +862,7 @@ public void testParseMappingsWithTypedTemplate() throws Exception {
861862
ExceptionsHelper.reThrowIfNotNull(e);
862863
}
863864
});
864-
Map<String, Object> mappings = parseMappings("", List.of(templateMetadata.mappings()), xContentRegistry());
865+
Map<String, Object> mappings = parseV1Mappings("", List.of(templateMetadata.mappings()), xContentRegistry());
865866
assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME));
866867
}
867868

@@ -873,7 +874,7 @@ public void testParseMappingsWithTypelessTemplate() throws Exception {
873874
ExceptionsHelper.reThrowIfNotNull(e);
874875
}
875876
});
876-
Map<String, Object> mappings = parseMappings("", List.of(templateMetadata.mappings()), xContentRegistry());
877+
Map<String, Object> mappings = parseV1Mappings("", List.of(templateMetadata.mappings()), xContentRegistry());
877878
assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME));
878879
}
879880

@@ -983,6 +984,69 @@ public void testDeprecateTranslogRetentionSettings() {
983984
+ "and [index.translog.retention.size] are deprecated and effectively ignored. They will be removed in a future version.");
984985
}
985986

987+
@SuppressWarnings("unchecked")
988+
public void testMappingsMergingIsSmart() throws Exception {
989+
Template ctt1 = new Template(null,
990+
new CompressedXContent("{\"_doc\":{\"_source\":{\"enabled\": false},\"_meta\":{\"ct1\":{\"ver\": \"text\"}}," +
991+
"\"properties\":{\"foo\":{\"type\":\"text\",\"ignore_above\":7,\"analyzer\":\"english\"}}}}"), null);
992+
Template ctt2 = new Template(null,
993+
new CompressedXContent("{\"_doc\":{\"_meta\":{\"ct1\":{\"ver\": \"keyword\"},\"ct2\":\"potato\"}," +
994+
"\"properties\":{\"foo\":{\"type\":\"keyword\",\"ignore_above\":13}}}}"), null);
995+
996+
ComponentTemplate ct1 = new ComponentTemplate(ctt1, null, null);
997+
ComponentTemplate ct2 = new ComponentTemplate(ctt2, null, null);
998+
999+
boolean shouldBeText = randomBoolean();
1000+
List<String> composedOf = shouldBeText ? Arrays.asList("ct2", "ct1") : Arrays.asList("ct1", "ct2");
1001+
logger.info("--> the {} analyzer should win ({})", shouldBeText ? "text" : "keyword", composedOf);
1002+
IndexTemplateV2 template = new IndexTemplateV2(Collections.singletonList("index"), null, composedOf, null, null, null);
1003+
1004+
ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE)
1005+
.metadata(Metadata.builder(Metadata.EMPTY_METADATA)
1006+
.put("ct1", ct1)
1007+
.put("ct2", ct2)
1008+
.put("index-template", template)
1009+
.build())
1010+
.build();
1011+
1012+
Map<String, Object> resolved =
1013+
MetadataCreateIndexService.resolveV2Mappings("{\"_doc\":{\"_meta\":{\"ct2\":\"eggplant\"}," +
1014+
"\"properties\":{\"bar\":{\"type\":\"text\"}}}}", state,
1015+
"index-template", new NamedXContentRegistry(Collections.emptyList()));
1016+
1017+
assertThat("expected exactly one type but was: " + resolved, resolved.size(), equalTo(1));
1018+
Map<String, Object> innerResolved = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
1019+
assertThat("was: " + innerResolved, innerResolved.size(), equalTo(3));
1020+
1021+
Map<String, Object> nonProperties = new HashMap<>(innerResolved);
1022+
nonProperties.remove("properties");
1023+
Map<String, Object> expectedNonProperties = new HashMap<>();
1024+
expectedNonProperties.put("_source", Collections.singletonMap("enabled", false));
1025+
Map<String, Object> meta = new HashMap<>();
1026+
meta.put("ct2", "eggplant");
1027+
if (shouldBeText) {
1028+
meta.put("ct1", Collections.singletonMap("ver", "text"));
1029+
} else {
1030+
meta.put("ct1", Collections.singletonMap("ver", "keyword"));
1031+
}
1032+
expectedNonProperties.put("_meta", meta);
1033+
assertThat(nonProperties, equalTo(expectedNonProperties));
1034+
1035+
Map<String, Object> innerInnerResolved = (Map<String, Object>) innerResolved.get("properties");
1036+
assertThat(innerInnerResolved.size(), equalTo(2));
1037+
assertThat(innerInnerResolved.get("bar"), equalTo(Collections.singletonMap("type", "text")));
1038+
Map<String, Object> fooMappings = new HashMap<>();
1039+
if (shouldBeText) {
1040+
fooMappings.put("type", "text");
1041+
fooMappings.put("ignore_above", 7);
1042+
fooMappings.put("analyzer", "english");
1043+
} else {
1044+
fooMappings.put("type", "keyword");
1045+
fooMappings.put("ignore_above", 13);
1046+
}
1047+
assertThat(innerInnerResolved.get("foo"), equalTo(fooMappings));
1048+
}
1049+
9861050
private IndexTemplateMetadata addMatchingTemplate(Consumer<IndexTemplateMetadata.Builder> configurator) {
9871051
IndexTemplateMetadata.Builder builder = templateMetadataBuilder("template1", "te*");
9881052
configurator.accept(builder);

server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,16 @@ public void testResolveMappings() throws Exception {
696696
.collect(Collectors.toList());
697697

698698
// The order of mappings should be:
699-
// - index template
700-
// - ct_high
701699
// - ct_low
702-
// Because the first elements when merging mappings have the highest precedence
700+
// - ct_high
701+
// - index template
702+
// Because the first elements when merging mappings have the lowest precedence
703703
assertThat(parsedMappings.get(0),
704-
equalTo(Map.of("_doc", Map.of("properties", Map.of("field", Map.of("type", "keyword"))))));
704+
equalTo(Map.of("_doc", Map.of("properties", Map.of("field2", Map.of("type", "text"))))));
705705
assertThat(parsedMappings.get(1),
706706
equalTo(Map.of("_doc", Map.of("properties", Map.of("field2", Map.of("type", "keyword"))))));
707707
assertThat(parsedMappings.get(2),
708-
equalTo(Map.of("_doc", Map.of("properties", Map.of("field2", Map.of("type", "text"))))));
708+
equalTo(Map.of("_doc", Map.of("properties", Map.of("field", Map.of("type", "keyword"))))));
709709
}
710710

711711
public void testResolveSettings() throws Exception {

0 commit comments

Comments
 (0)