Skip to content

Commit b4cdf86

Browse files
committed
Merge V2 index/component template mappings in specific manner (elastic#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 elastic#53101
1 parent c370b83 commit b4cdf86

4 files changed

Lines changed: 157 additions & 25 deletions

File tree

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

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

460-
final Map<String, Map<String, Object>> mappings = Collections.unmodifiableMap(parseMappings(request.mappings(),
460+
final Map<String, Map<String, Object>> mappings = Collections.unmodifiableMap(parseV1Mappings(request.mappings(),
461461
templates.stream().map(IndexTemplateMetadata::getMappings)
462462
// Converts the ImmutableOpenMap into a non-terrible HashMap
463463
.map(iom -> {
@@ -494,11 +494,10 @@ private ClusterState applyCreateIndexRequestWithV2Template(final ClusterState cu
494494
throws Exception {
495495
logger.info("applying create index request using v2 template [{}]", templateName);
496496

497-
final Map<String, Map<String, Object>> mappings = Collections.unmodifiableMap(parseMappings(request.mappings(),
498-
MetadataIndexTemplateService.resolveMappings(currentState, templateName).stream()
499-
.map(compressedMapping -> Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, compressedMapping))
500-
.collect(toList()),
501-
xContentRegistry));
497+
assert request.mappings().size() == 1 : "expected request metadata mappings to have 1 type but it had: " + request.mappings();
498+
String sourceMappings = request.mappings().values().iterator().next();
499+
final Map<String, Map<String, Object>> mappings = resolveV2Mappings(sourceMappings,
500+
currentState, templateName, xContentRegistry);
502501

503502
final Settings aggregatedIndexSettings =
504503
aggregateIndexSettings(currentState, request,
@@ -517,6 +516,15 @@ private ClusterState applyCreateIndexRequestWithV2Template(final ClusterState cu
517516
Collections.singletonList(templateName), metadataTransformer);
518517
}
519518

519+
static Map<String, Map<String, Object>> resolveV2Mappings(final String requestMappings,
520+
final ClusterState currentState,
521+
final String templateName,
522+
final NamedXContentRegistry xContentRegistry) throws Exception {
523+
final Map<String, Map<String, Object>> mappings = Collections.unmodifiableMap(parseV2Mappings(requestMappings,
524+
MetadataIndexTemplateService.resolveMappings(currentState, templateName), xContentRegistry));
525+
return mappings;
526+
}
527+
520528
private ClusterState applyCreateIndexRequestWithExistingMetadata(final ClusterState currentState,
521529
final CreateIndexClusterStateUpdateRequest request,
522530
final boolean silent,
@@ -551,14 +559,77 @@ private ClusterState applyCreateIndexRequestWithExistingMetadata(final ClusterSt
551559
}
552560

553561
/**
554-
* Parses the provided mappings json and the inheritable mappings from the templates (if any) into a map.
562+
* Parses the provided mappings json and the inheritable mappings from the templates (if any)
563+
* into a map.
555564
*
556-
* The template mappings are applied in the order they are encountered in the list (clients should make sure the lower index, closer
557-
* to the head of the list, templates have the highest {@link IndexTemplateMetadata#order()})
565+
* The template mappings are applied in the order they are encountered in the list, with the
566+
* caveat that mapping fields are only merged at the top-level, meaning that field settings are
567+
* not merged, instead they replace any previous field definition.
568+
*/
569+
@SuppressWarnings("unchecked")
570+
static Map<String, Map<String, Object>> parseV2Mappings(String mappingsJson, List<CompressedXContent> templateMappings,
571+
NamedXContentRegistry xContentRegistry) throws Exception {
572+
Map<String, Object> requestMappings = MapperService.parseMapping(xContentRegistry, mappingsJson);
573+
// apply templates, merging the mappings into the request mapping if exists
574+
Map<String, Object> properties = new HashMap<>();
575+
Map<String, Object> nonProperties = new HashMap<>();
576+
for (CompressedXContent mapping : templateMappings) {
577+
if (mapping != null) {
578+
Map<String, Object> templateMapping = MapperService.parseMapping(xContentRegistry, mapping.string());
579+
if (templateMapping.isEmpty()) {
580+
// Someone provided an empty '{}' for mappings, which is okay, but to avoid
581+
// tripping the below assertion, we can safely ignore it
582+
continue;
583+
}
584+
assert templateMapping.size() == 1 : "expected exactly one mapping value, got: " + templateMapping;
585+
if (templateMapping.get(MapperService.SINGLE_MAPPING_NAME) instanceof Map == false) {
586+
throw new IllegalStateException("invalid mapping definition, expected a single map underneath [" +
587+
MapperService.SINGLE_MAPPING_NAME + "] but it was: [" + templateMapping + "]");
588+
}
589+
590+
Map<String, Object> innerTemplateMapping = (Map<String, Object>) templateMapping.get(MapperService.SINGLE_MAPPING_NAME);
591+
Map<String, Object> innerTemplateNonProperties = new HashMap<>(innerTemplateMapping);
592+
Map<String, Object> maybeProperties = (Map<String, Object>) innerTemplateNonProperties.remove("properties");
593+
594+
XContentHelper.mergeDefaults(innerTemplateNonProperties, nonProperties);
595+
nonProperties = innerTemplateNonProperties;
596+
597+
if (maybeProperties != null) {
598+
properties.putAll(maybeProperties);
599+
}
600+
}
601+
}
602+
603+
if (requestMappings.get(MapperService.SINGLE_MAPPING_NAME) != null) {
604+
Map<String, Object> innerRequestMappings = (Map<String, Object>) requestMappings.get(MapperService.SINGLE_MAPPING_NAME);
605+
Map<String, Object> innerRequestNonProperties = new HashMap<>(innerRequestMappings);
606+
Map<String, Object> maybeRequestProperties = (Map<String, Object>) innerRequestNonProperties.remove("properties");
607+
608+
XContentHelper.mergeDefaults(innerRequestNonProperties, nonProperties);
609+
nonProperties = innerRequestNonProperties;
610+
611+
if (maybeRequestProperties != null) {
612+
properties.putAll(maybeRequestProperties);
613+
}
614+
}
615+
616+
Map<String, Object> finalMappings = new HashMap<>(nonProperties);
617+
finalMappings.put("properties", properties);
618+
return Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, finalMappings);
619+
}
620+
621+
/**
622+
* Parses the provided mappings json and the inheritable mappings from the templates (if any)
623+
* into a map.
624+
*
625+
* The template mappings are applied in the order they are encountered in the list (clients
626+
* should make sure the lower index, closer to the head of the list, templates have the highest
627+
* {@link IndexTemplateMetadata#order()}). This merging makes no distinction between field
628+
* definitions, as may result in an invalid field definition
558629
*/
559-
static Map<String, Map<String, Object>> parseMappings(Map<String, String> requestMappings,
560-
List<Map<String, CompressedXContent>> templateMappings,
561-
NamedXContentRegistry xContentRegistry) throws Exception {
630+
static Map<String, Map<String, Object>> parseV1Mappings(Map<String, String> requestMappings,
631+
List<Map<String, CompressedXContent>> templateMappings,
632+
NamedXContentRegistry xContentRegistry) throws Exception {
562633
Map<String, Map<String, Object>> mappings = new HashMap<>();
563634
for (Map.Entry<String, String> entry : requestMappings.entrySet()) {
564635
Map<String, Object> mapping = MapperService.parseMapping(xContentRegistry, entry.getValue());

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: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.buildIndexMetadata;
105105
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.clusterStateCreateIndex;
106106
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.getIndexNumberOfRoutingShards;
107-
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.parseMappings;
107+
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.parseV1Mappings;
108108
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.resolveAndValidateAliases;
109109
import static org.elasticsearch.cluster.shards.ClusterShardLimitIT.ShardCounts.forDataNodeCount;
110110
import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING;
@@ -647,7 +647,7 @@ public void testParseMappingsAppliesDataFromTemplateAndRequest() throws Exceptio
647647
});
648648
request.mappings(singletonMap("type", createMapping("mapping_from_request", "text").string()));
649649

650-
Map<String, Map<String, Object>> parsedMappings = MetadataCreateIndexService.parseMappings(request.mappings(),
650+
Map<String, Map<String, Object>> parsedMappings = MetadataCreateIndexService.parseV1Mappings(request.mappings(),
651651
Collections.singletonList(convertMappings(templateMetadata.getMappings())), NamedXContentRegistry.EMPTY);
652652

653653
assertThat(parsedMappings, hasKey("type"));
@@ -705,7 +705,7 @@ public void testRequestDataHavePriorityOverTemplateData() throws Exception {
705705
request.aliases(singleton(new Alias("alias").searchRouting("fromRequest")));
706706
request.settings(Settings.builder().put("key1", "requestValue").build());
707707

708-
Map<String, Map<String, Object>> parsedMappings = MetadataCreateIndexService.parseMappings(request.mappings(),
708+
Map<String, Map<String, Object>> parsedMappings = MetadataCreateIndexService.parseV1Mappings(request.mappings(),
709709
Collections.singletonList(convertMappings(templateMetadata.mappings())), xContentRegistry());
710710
List<AliasMetadata> resolvedAliases = resolveAndValidateAliases(request.index(), request.aliases(),
711711
MetadataIndexTemplateService.resolveAliases(Collections.singletonList(templateMetadata)),
@@ -878,7 +878,7 @@ public void testParseMappingsWithTypedTemplateAndTypelessIndexMapping() throws E
878878
}
879879
});
880880

881-
Map<String, Map<String, Object>> mappings = parseMappings(singletonMap(MapperService.SINGLE_MAPPING_NAME, "{\"_doc\":{}}"),
881+
Map<String, Map<String, Object>> mappings = parseV1Mappings(singletonMap(MapperService.SINGLE_MAPPING_NAME, "{\"_doc\":{}}"),
882882
Collections.singletonList(convertMappings(templateMetadata.mappings())), xContentRegistry());
883883
assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME));
884884
}
@@ -892,7 +892,7 @@ public void testParseMappingsWithTypedTemplate() throws Exception {
892892
ExceptionsHelper.reThrowIfNotNull(e);
893893
}
894894
});
895-
Map<String, Map<String, Object>> mappings = parseMappings(emptyMap(),
895+
Map<String, Map<String, Object>> mappings = parseV1Mappings(emptyMap(),
896896
Collections.singletonList(convertMappings(templateMetadata.mappings())), xContentRegistry());
897897
assertThat(mappings, Matchers.hasKey("type"));
898898
}
@@ -905,7 +905,7 @@ public void testParseMappingsWithTypelessTemplate() throws Exception {
905905
ExceptionsHelper.reThrowIfNotNull(e);
906906
}
907907
});
908-
Map<String, Map<String, Object>> mappings = parseMappings(emptyMap(),
908+
Map<String, Map<String, Object>> mappings = parseV1Mappings(emptyMap(),
909909
Collections.singletonList(convertMappings(templateMetadata.mappings())), xContentRegistry());
910910
assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME));
911911
}
@@ -1001,6 +1001,69 @@ public void testValidateTranslogRetentionSettings() {
10011001
+ "and [index.translog.retention.size] are deprecated and effectively ignored. They will be removed in a future version.");
10021002
}
10031003

1004+
@SuppressWarnings("unchecked")
1005+
public void testMappingsMergingIsSmart() throws Exception {
1006+
Template ctt1 = new Template(null,
1007+
new CompressedXContent("{\"_doc\":{\"_source\":{\"enabled\": false},\"_meta\":{\"ct1\":{\"ver\": \"text\"}}," +
1008+
"\"properties\":{\"foo\":{\"type\":\"text\",\"ignore_above\":7,\"analyzer\":\"english\"}}}}"), null);
1009+
Template ctt2 = new Template(null,
1010+
new CompressedXContent("{\"_doc\":{\"_meta\":{\"ct1\":{\"ver\": \"keyword\"},\"ct2\":\"potato\"}," +
1011+
"\"properties\":{\"foo\":{\"type\":\"keyword\",\"ignore_above\":13}}}}"), null);
1012+
1013+
ComponentTemplate ct1 = new ComponentTemplate(ctt1, null, null);
1014+
ComponentTemplate ct2 = new ComponentTemplate(ctt2, null, null);
1015+
1016+
boolean shouldBeText = randomBoolean();
1017+
List<String> composedOf = shouldBeText ? Arrays.asList("ct2", "ct1") : Arrays.asList("ct1", "ct2");
1018+
logger.info("--> the {} analyzer should win ({})", shouldBeText ? "text" : "keyword", composedOf);
1019+
IndexTemplateV2 template = new IndexTemplateV2(Collections.singletonList("index"), null, composedOf, null, null, null);
1020+
1021+
ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE)
1022+
.metadata(Metadata.builder(Metadata.EMPTY_METADATA)
1023+
.put("ct1", ct1)
1024+
.put("ct2", ct2)
1025+
.put("index-template", template)
1026+
.build())
1027+
.build();
1028+
1029+
Map<String, Map<String, Object>> resolved =
1030+
MetadataCreateIndexService.resolveV2Mappings("{\"_doc\":{\"_meta\":{\"ct2\":\"eggplant\"}," +
1031+
"\"properties\":{\"bar\":{\"type\":\"text\"}}}}", state,
1032+
"index-template", new NamedXContentRegistry(Collections.emptyList()));
1033+
1034+
assertThat("expected exactly one type but was: " + resolved, resolved.size(), equalTo(1));
1035+
Map<String, Object> innerResolved = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
1036+
assertThat("was: " + innerResolved, innerResolved.size(), equalTo(3));
1037+
1038+
Map<String, Object> nonProperties = new HashMap<>(innerResolved);
1039+
nonProperties.remove("properties");
1040+
Map<String, Object> expectedNonProperties = new HashMap<>();
1041+
expectedNonProperties.put("_source", Collections.singletonMap("enabled", false));
1042+
Map<String, Object> meta = new HashMap<>();
1043+
meta.put("ct2", "eggplant");
1044+
if (shouldBeText) {
1045+
meta.put("ct1", Collections.singletonMap("ver", "text"));
1046+
} else {
1047+
meta.put("ct1", Collections.singletonMap("ver", "keyword"));
1048+
}
1049+
expectedNonProperties.put("_meta", meta);
1050+
assertThat(nonProperties, equalTo(expectedNonProperties));
1051+
1052+
Map<String, Object> innerInnerResolved = (Map<String, Object>) innerResolved.get("properties");
1053+
assertThat(innerInnerResolved.size(), equalTo(2));
1054+
assertThat(innerInnerResolved.get("bar"), equalTo(Collections.singletonMap("type", "text")));
1055+
Map<String, Object> fooMappings = new HashMap<>();
1056+
if (shouldBeText) {
1057+
fooMappings.put("type", "text");
1058+
fooMappings.put("ignore_above", 7);
1059+
fooMappings.put("analyzer", "english");
1060+
} else {
1061+
fooMappings.put("type", "keyword");
1062+
fooMappings.put("ignore_above", 13);
1063+
}
1064+
assertThat(innerInnerResolved.get("foo"), equalTo(fooMappings));
1065+
}
1066+
10041067
private IndexTemplateMetadata addMatchingTemplate(Consumer<IndexTemplateMetadata.Builder> configurator) {
10051068
IndexTemplateMetadata.Builder builder = templateMetadataBuilder("template1", "te*");
10061069
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
@@ -699,19 +699,19 @@ public void testResolveMappings() throws Exception {
699699
.collect(Collectors.toList());
700700

701701
// The order of mappings should be:
702-
// - index template
703-
// - ct_high
704702
// - ct_low
705-
// Because the first elements when merging mappings have the highest precedence
703+
// - ct_high
704+
// - index template
705+
// Because the first elements when merging mappings have the lowest precedence
706706
assertThat(parsedMappings.get(0),
707707
equalTo(Collections.singletonMap("_doc", Collections.singletonMap("properties",
708-
Collections.singletonMap("field", Collections.singletonMap("type", "keyword"))))));
708+
Collections.singletonMap("field2", Collections.singletonMap("type", "text"))))));
709709
assertThat(parsedMappings.get(1),
710710
equalTo(Collections.singletonMap("_doc", Collections.singletonMap("properties",
711711
Collections.singletonMap("field2", Collections.singletonMap("type", "keyword"))))));
712712
assertThat(parsedMappings.get(2),
713713
equalTo(Collections.singletonMap("_doc", Collections.singletonMap("properties",
714-
Collections.singletonMap("field2", Collections.singletonMap("type", "text"))))));
714+
Collections.singletonMap("field", Collections.singletonMap("type", "keyword"))))));
715715
}
716716

717717
public void testResolveSettings() throws Exception {

0 commit comments

Comments
 (0)