Skip to content

Commit 676893a

Browse files
authored
Merge mappings for composable index templates (#58521)
This PR implements recursive mapping merging for composable index templates. When creating an index, we perform the following: * Add each component template mapping in order, merging each one in after the last. * Merge in the index template mappings (if present). * Merge in the mappings on the index request itself (if present). Some principles: * All 'structural' changes are disallowed (but everything else is fine). An object mapper can never be changed between `type: object` and `type: nested`. A field mapper can never be changed to an object mapper, and vice versa. * Generally, each section is merged recursively. This includes `object` mappings, as well as root options like `dynamic_templates` and `meta`. Once we reach 'leaf components' like field definitions, they always overwrite an existing one instead of being merged. Relates to #53101.
1 parent ee5e683 commit 676893a

File tree

30 files changed

+833
-705
lines changed

30 files changed

+833
-705
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/15_composition.yml

+86
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,89 @@
200200
index: eggplant
201201

202202
- match: {eggplant.settings.index.number_of_shards: "3"}
203+
204+
---
205+
"Index template mapping merging":
206+
- skip:
207+
version: " - 7.99.99"
208+
reason: "index template v2 mapping merging not yet backported to 7.9"
209+
features: allowed_warnings
210+
211+
- do:
212+
cluster.put_component_template:
213+
name: red
214+
body:
215+
template:
216+
mappings:
217+
properties:
218+
object1.red:
219+
type: keyword
220+
object2.red:
221+
type: keyword
222+
223+
- do:
224+
cluster.put_component_template:
225+
name: blue
226+
body:
227+
template:
228+
mappings:
229+
properties:
230+
object2.red:
231+
type: text
232+
object1.blue:
233+
type: text
234+
object2.blue:
235+
type: text
236+
237+
- do:
238+
allowed_warnings:
239+
- "index template [my-template] has index patterns [baz*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation"
240+
indices.put_index_template:
241+
name: blue
242+
body:
243+
index_patterns: ["purple-index"]
244+
composed_of: ["red", "blue"]
245+
template:
246+
mappings:
247+
properties:
248+
object2.blue:
249+
type: integer
250+
object1.purple:
251+
type: integer
252+
object2.purple:
253+
type: integer
254+
nested:
255+
type: nested
256+
include_in_root: true
257+
258+
- do:
259+
indices.create:
260+
index: purple-index
261+
body:
262+
mappings:
263+
properties:
264+
object2.purple:
265+
type: double
266+
object3.purple:
267+
type: double
268+
nested:
269+
type: nested
270+
include_in_root: false
271+
include_in_parent: true
272+
273+
- do:
274+
indices.get:
275+
index: purple-index
276+
277+
- match: {purple-index.mappings.properties.object1.properties.red: {type: keyword}}
278+
- match: {purple-index.mappings.properties.object1.properties.blue: {type: text}}
279+
- match: {purple-index.mappings.properties.object1.properties.purple: {type: integer}}
280+
281+
- match: {purple-index.mappings.properties.object2.properties.red: {type: text}}
282+
- match: {purple-index.mappings.properties.object2.properties.blue: {type: integer}}
283+
- match: {purple-index.mappings.properties.object2.properties.purple: {type: double}}
284+
285+
- match: {purple-index.mappings.properties.object3.properties.purple: {type: double}}
286+
287+
- is_false: purple-index.mappings.properties.nested.include_in_root
288+
- is_true: purple-index.mappings.properties.nested.include_in_parent

server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java

+24-10
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@
2828
import org.elasticsearch.cluster.block.ClusterBlockLevel;
2929
import org.elasticsearch.cluster.metadata.AliasMetadata;
3030
import org.elasticsearch.cluster.metadata.AliasValidator;
31+
import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
3132
import org.elasticsearch.cluster.metadata.IndexMetadata;
3233
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
33-
import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
3434
import org.elasticsearch.cluster.metadata.Metadata;
3535
import org.elasticsearch.cluster.metadata.MetadataCreateIndexService;
3636
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
3737
import org.elasticsearch.cluster.metadata.Template;
3838
import org.elasticsearch.cluster.service.ClusterService;
39-
import org.elasticsearch.common.Strings;
4039
import org.elasticsearch.common.UUIDs;
4140
import org.elasticsearch.common.compress.CompressedXContent;
4241
import org.elasticsearch.common.inject.Inject;
4342
import org.elasticsearch.common.io.stream.StreamInput;
4443
import org.elasticsearch.common.settings.Settings;
4544
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
46-
import org.elasticsearch.common.xcontent.XContentFactory;
45+
import org.elasticsearch.index.mapper.DocumentMapper;
46+
import org.elasticsearch.index.mapper.MapperService;
4747
import org.elasticsearch.indices.IndicesService;
4848
import org.elasticsearch.tasks.Task;
4949
import org.elasticsearch.threadpool.ThreadPool;
@@ -58,7 +58,6 @@
5858
import java.util.function.Function;
5959
import java.util.stream.Collectors;
6060

61-
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.resolveV2Mappings;
6261
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV1Templates;
6362
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV2Templates;
6463
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findV2Template;
@@ -172,10 +171,6 @@ public static Template resolveTemplate(final String matchingTemplate, final Stri
172171
final AliasValidator aliasValidator) throws Exception {
173172
Settings settings = resolveSettings(simulatedState.metadata(), matchingTemplate);
174173

175-
// empty request mapping as the user can't specify any explicit mappings via the simulate api
176-
Map<String, Object> mappings = resolveV2Mappings("{}", simulatedState, matchingTemplate, xContentRegistry);
177-
String mappingsJson = Strings.toString(XContentFactory.jsonBuilder().map(mappings));
178-
179174
List<Map<String, AliasMetadata>> resolvedAliases = MetadataIndexTemplateService.resolveAliases(simulatedState.metadata(),
180175
matchingTemplate);
181176

@@ -200,8 +195,27 @@ public static Template resolveTemplate(final String matchingTemplate, final Stri
200195
// the context is only used for validation so it's fine to pass fake values for the
201196
// shard id and the current timestamp
202197
tempIndexService.newQueryShardContext(0, null, () -> 0L, null)));
198+
Map<String, AliasMetadata> aliasesByName = aliases.stream().collect(
199+
Collectors.toMap(AliasMetadata::getAlias, Function.identity()));
203200

204-
return new Template(settings, mappingsJson == null ? null : new CompressedXContent(mappingsJson),
205-
aliases.stream().collect(Collectors.toMap(AliasMetadata::getAlias, Function.identity())));
201+
// empty request mapping as the user can't specify any explicit mappings via the simulate api
202+
List<Map<String, Object>> mappings = MetadataCreateIndexService.collectV2Mappings(
203+
"{}", simulatedState, matchingTemplate, xContentRegistry);
204+
205+
CompressedXContent mergedMapping = indicesService.<CompressedXContent, Exception>withTempIndexService(indexMetadata,
206+
tempIndexService -> {
207+
MapperService mapperService = tempIndexService.mapperService();
208+
for (Map<String, Object> mapping : mappings) {
209+
if (!mapping.isEmpty()) {
210+
assert mapping.size() == 1 : mapping;
211+
mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MapperService.MergeReason.INDEX_TEMPLATE);
212+
}
213+
}
214+
215+
DocumentMapper documentMapper = mapperService.documentMapper();
216+
return documentMapper != null ? documentMapper.mappingSource() : null;
217+
});
218+
219+
return new Template(settings, mergedMapping, aliasesByName);
206220
}
207221
}

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

+7-12
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
2828
import org.elasticsearch.common.xcontent.ToXContentObject;
2929
import org.elasticsearch.common.xcontent.XContentBuilder;
30-
import org.elasticsearch.common.xcontent.XContentHelper;
3130
import org.elasticsearch.common.xcontent.XContentParser;
3231
import org.elasticsearch.index.Index;
3332

@@ -244,28 +243,24 @@ public TimestampField(StreamInput in) throws IOException {
244243
}
245244

246245
/**
247-
* Force fully inserts the timestamp field mapping into the provided mapping.
248-
* Existing mapping definitions for the timestamp field will be completely overwritten.
249-
* Takes into account if the name of the timestamp field is nested.
250-
*
251-
* @param mappings The mapping to update
246+
* Creates a map representing the full timestamp field mapping, taking into
247+
* account if the timestamp field is nested under object mappers (its path
248+
* contains dots).
252249
*/
253-
public void insertTimestampFieldMapping(Map<String, Object> mappings) {
254-
assert mappings.containsKey("_doc");
255-
250+
public Map<String, Object> getTimestampFieldMapping() {
256251
String mappingPath = convertFieldPathToMappingPath(name);
257252
String parentObjectFieldPath = "_doc." + mappingPath.substring(0, mappingPath.lastIndexOf('.'));
258253
String leafFieldName = mappingPath.substring(mappingPath.lastIndexOf('.') + 1);
259254

260-
Map<String, Object> changes = new HashMap<>();
261-
Map<String, Object> current = changes;
255+
Map<String, Object> result = new HashMap<>();
256+
Map<String, Object> current = result;
262257
for (String key : parentObjectFieldPath.split("\\.")) {
263258
Map<String, Object> map = new HashMap<>();
264259
current.put(key, map);
265260
current = map;
266261
}
267262
current.put(leafFieldName, fieldMapping);
268-
XContentHelper.update(mappings, changes, false);
263+
return result;
269264
}
270265

271266
@Override

0 commit comments

Comments
 (0)