diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
index 046b741ba3593..9ed23f61bf0ea 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
@@ -449,19 +449,19 @@ public Builder add(FieldMapper.Builder builder) {
return this;
}
- public Builder add(FieldMapper mapper) {
+ private void add(FieldMapper mapper) {
mapperBuilders.put(mapper.simpleName(), context -> mapper);
- return this;
}
- public Builder update(FieldMapper toMerge, MapperMergeContext context) {
+ private void update(FieldMapper toMerge, MapperMergeContext context) {
if (mapperBuilders.containsKey(toMerge.simpleName()) == false) {
- add(toMerge);
+ if (context.decrementFieldBudgetIfPossible(toMerge.mapperSize())) {
+ add(toMerge);
+ }
} else {
FieldMapper existing = mapperBuilders.get(toMerge.simpleName()).apply(context.getMapperBuilderContext());
add(existing.merge(toMerge, context));
}
- return this;
}
public boolean hasMultiFields() {
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java
index 75bc9b9c94ef7..ca15248c037bc 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java
@@ -75,7 +75,7 @@ public final String simpleName() {
/**
* Return the merge of {@code mergeWith} into this.
- * Both {@code this} and {@code mergeWith} will be left unmodified.
+ * Both {@code this} and {@code mergeWith} will be left unmodified.
*/
public abstract Mapper merge(Mapper mergeWith, MapperMergeContext mapperMergeContext);
@@ -135,4 +135,18 @@ public static FieldType freezeAndDeduplicateFieldType(FieldType fieldType) {
}
return fieldTypeDeduplicator.computeIfAbsent(fieldType, Function.identity());
}
+
+ /**
+ * Returns the size this mapper counts against the {@linkplain MapperService#INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING field limit}.
+ *
+ * Needs to be in sync with {@link MappingLookup#getTotalFieldsCount()}.
+ */
+ public int mapperSize() {
+ int size = 1;
+ for (Mapper mapper : this) {
+ size += mapper.mapperSize();
+ }
+ return size;
+ }
+
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java
index 699b67858deed..79adaf5966c5b 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java
@@ -16,37 +16,106 @@
public final class MapperMergeContext {
private final MapperBuilderContext mapperBuilderContext;
+ private final NewFieldsBudget newFieldsBudget;
+
+ private MapperMergeContext(MapperBuilderContext mapperBuilderContext, NewFieldsBudget newFieldsBudget) {
+ this.mapperBuilderContext = mapperBuilderContext;
+ this.newFieldsBudget = newFieldsBudget;
+ }
/**
* The root context, to be used when merging a tree of mappers
*/
- public static MapperMergeContext root(boolean isSourceSynthetic, boolean isDataStream) {
- return new MapperMergeContext(MapperBuilderContext.root(isSourceSynthetic, isDataStream));
+ public static MapperMergeContext root(boolean isSourceSynthetic, boolean isDataStream, long newFieldsBudget) {
+ return new MapperMergeContext(MapperBuilderContext.root(isSourceSynthetic, isDataStream), NewFieldsBudget.of(newFieldsBudget));
}
/**
* Creates a new {@link MapperMergeContext} from a {@link MapperBuilderContext}
* @param mapperBuilderContext the {@link MapperBuilderContext} for this {@link MapperMergeContext}
+ * @param newFieldsBudget limits how many fields can be added during the merge process
* @return a new {@link MapperMergeContext}, wrapping the provided {@link MapperBuilderContext}
*/
- public static MapperMergeContext from(MapperBuilderContext mapperBuilderContext) {
- return new MapperMergeContext(mapperBuilderContext);
- }
-
- private MapperMergeContext(MapperBuilderContext mapperBuilderContext) {
- this.mapperBuilderContext = mapperBuilderContext;
+ public static MapperMergeContext from(MapperBuilderContext mapperBuilderContext, long newFieldsBudget) {
+ return new MapperMergeContext(mapperBuilderContext, NewFieldsBudget.of(newFieldsBudget));
}
/**
- * Creates a new {@link MapperMergeContext} that is a child of this context
+ * Creates a new {@link MapperMergeContext} with a child {@link MapperBuilderContext}.
+ * The child {@link MapperMergeContext} context will share the same field limit.
* @param name the name of the child context
* @return a new {@link MapperMergeContext} with this context as its parent
*/
- public MapperMergeContext createChildContext(String name) {
- return new MapperMergeContext(mapperBuilderContext.createChildContext(name));
+ MapperMergeContext createChildContext(String name) {
+ return createChildContext(mapperBuilderContext.createChildContext(name));
+ }
+
+ /**
+ * Creates a new {@link MapperMergeContext} with a given child {@link MapperBuilderContext}
+ * The child {@link MapperMergeContext} context will share the same field limit.
+ * @param childContext the child {@link MapperBuilderContext}
+ * @return a new {@link MapperMergeContext}, wrapping the provided {@link MapperBuilderContext}
+ */
+ MapperMergeContext createChildContext(MapperBuilderContext childContext) {
+ return new MapperMergeContext(childContext, newFieldsBudget);
}
MapperBuilderContext getMapperBuilderContext() {
return mapperBuilderContext;
}
+
+ boolean decrementFieldBudgetIfPossible(int fieldSize) {
+ return newFieldsBudget.decrementIfPossible(fieldSize);
+ }
+
+ /**
+ * Keeps track of how many new fields can be added during mapper merge.
+ * The field budget is shared across instances of {@link MapperMergeContext} that are created via
+ * {@link MapperMergeContext#createChildContext}.
+ * This ensures that fields that are consumed by one child object mapper also decrement the budget for another child object.
+ * Not thread safe.The same instance may not be modified by multiple threads.
+ */
+ private interface NewFieldsBudget {
+
+ static NewFieldsBudget of(long fieldsBudget) {
+ if (fieldsBudget == Long.MAX_VALUE) {
+ return Unlimited.INSTANCE;
+ }
+ return new Limited(fieldsBudget);
+ }
+
+ boolean decrementIfPossible(long fieldSize);
+
+ final class Unlimited implements NewFieldsBudget {
+
+ private static final Unlimited INSTANCE = new Unlimited();
+
+ private Unlimited() {}
+
+ @Override
+ public boolean decrementIfPossible(long fieldSize) {
+ return true;
+ }
+
+ }
+
+ final class Limited implements NewFieldsBudget {
+
+ private long fieldsBudget;
+
+ Limited(long fieldsBudget) {
+ this.fieldsBudget = fieldsBudget;
+ }
+
+ @Override
+ public boolean decrementIfPossible(long fieldSize) {
+ if (fieldsBudget >= fieldSize) {
+ fieldsBudget -= fieldSize;
+ return true;
+ }
+ return false;
+ }
+
+ }
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java
index b714eabbd2636..61dd444be34b1 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java
@@ -559,11 +559,15 @@ public Mapping parseMapping(String mappingType, Map mappingSourc
}
public static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason) {
+ return mergeMappings(currentMapper, incomingMapping, reason, Long.MAX_VALUE);
+ }
+
+ static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason, long newFieldsBudget) {
Mapping newMapping;
if (currentMapper == null) {
- newMapping = incomingMapping;
+ newMapping = incomingMapping.withFieldsBudget(newFieldsBudget);
} else {
- newMapping = currentMapper.mapping().merge(incomingMapping, reason);
+ newMapping = currentMapper.mapping().merge(incomingMapping, reason, newFieldsBudget);
}
return newMapping;
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java
index 40ecc6af57e8e..903e4e5da5b29 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java
@@ -133,10 +133,12 @@ public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() {
*
* @param mergeWith the new mapping to merge into this one.
* @param reason the reason this merge was initiated.
+ * @param newFieldsBudget how many new fields can be added during the merge process
* @return the resulting merged mapping.
*/
- Mapping merge(Mapping mergeWith, MergeReason reason) {
- RootObjectMapper mergedRoot = root.merge(mergeWith.root, reason, MapperMergeContext.root(isSourceSynthetic(), false));
+ Mapping merge(Mapping mergeWith, MergeReason reason, long newFieldsBudget) {
+ MapperMergeContext mergeContext = MapperMergeContext.root(isSourceSynthetic(), false, newFieldsBudget);
+ RootObjectMapper mergedRoot = root.merge(mergeWith.root, reason, mergeContext);
// When merging metadata fields as part of applying an index template, new field definitions
// completely overwrite existing ones instead of being merged. This behavior matches how we
@@ -148,7 +150,7 @@ Mapping merge(Mapping mergeWith, MergeReason reason) {
if (mergeInto == null || reason == MergeReason.INDEX_TEMPLATE) {
merged = metaMergeWith;
} else {
- merged = (MetadataFieldMapper) mergeInto.merge(metaMergeWith, MapperMergeContext.root(isSourceSynthetic(), false));
+ merged = (MetadataFieldMapper) mergeInto.merge(metaMergeWith, mergeContext);
}
mergedMetadataMappers.put(merged.getClass(), merged);
}
@@ -169,6 +171,18 @@ Mapping merge(Mapping mergeWith, MergeReason reason) {
return new Mapping(mergedRoot, mergedMetadataMappers.values().toArray(new MetadataFieldMapper[0]), mergedMeta);
}
+ /**
+ * Returns a copy of this mapper that ensures that the number of fields isn't greater than the provided fields budget.
+ * @param fieldsBudget the maximum number of fields this mapping may have
+ */
+ public Mapping withFieldsBudget(long fieldsBudget) {
+ MapperMergeContext mergeContext = MapperMergeContext.root(isSourceSynthetic(), false, fieldsBudget);
+ // get a copy of the root mapper, without any fields
+ RootObjectMapper shallowRoot = root.withoutMappers();
+ // calling merge on the shallow root to ensure we're only adding as many fields as allowed by the fields budget
+ return new Mapping(shallowRoot.merge(root, MergeReason.MAPPING_RECOVERY, mergeContext), metadataMappers, meta);
+ }
+
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
root.toXContent(builder, params, (b, params1) -> {
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java
index 4880ce5edc204..0172c22c0b176 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java
@@ -271,7 +271,7 @@ private void checkFieldLimit(long limit) {
}
void checkFieldLimit(long limit, int additionalFieldsToAdd) {
- if (getTotalFieldsCount() + additionalFieldsToAdd - mapping.getSortedMetadataMappers().length > limit) {
+ if (exceedsLimit(limit, additionalFieldsToAdd)) {
throw new IllegalArgumentException(
"Limit of total fields ["
+ limit
@@ -281,6 +281,10 @@ void checkFieldLimit(long limit, int additionalFieldsToAdd) {
}
}
+ boolean exceedsLimit(long limit, int additionalFieldsToAdd) {
+ return getTotalFieldsCount() + additionalFieldsToAdd - mapping.getSortedMetadataMappers().length > limit;
+ }
+
private void checkDimensionFieldLimit(long limit) {
long dimensionFieldCount = fieldMappers.values()
.stream()
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java
index 87a8979934a4e..dd2d4407bed03 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java
@@ -186,6 +186,21 @@ public ObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) {
return builder;
}
+ @Override
+ NestedObjectMapper withoutMappers() {
+ return new NestedObjectMapper(
+ simpleName(),
+ fullPath(),
+ Map.of(),
+ enabled,
+ dynamic,
+ includeInParent,
+ includeInRoot,
+ nestedTypePath,
+ nestedTypeFilter
+ );
+ }
+
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(simpleName());
@@ -260,7 +275,9 @@ protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeCo
if (mapperBuilderContext instanceof NestedMapperBuilderContext == false) {
parentIncludedInRoot |= this.includeInParent.value();
}
- return MapperMergeContext.from(new NestedMapperBuilderContext(mapperBuilderContext.buildFullName(name), parentIncludedInRoot));
+ return mapperMergeContext.createChildContext(
+ new NestedMapperBuilderContext(mapperBuilderContext.buildFullName(name), parentIncludedInRoot)
+ );
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java
index 922beb25d8a2f..2c89f60f40975 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java
@@ -162,7 +162,7 @@ protected final Map buildMappers(MapperBuilderContext mapperBuil
// This can also happen due to multiple index templates being merged into a single mappings definition using
// XContentHelper#mergeDefaults, again in case some index templates contained mappings for the same field using a
// mix of object notation and dot notation.
- mapper = existing.merge(mapper, MapperMergeContext.from(mapperBuilderContext));
+ mapper = existing.merge(mapper, MapperMergeContext.from(mapperBuilderContext, Long.MAX_VALUE));
}
mappers.put(mapper.simpleName(), mapper);
}
@@ -403,6 +403,14 @@ public ObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) {
return builder;
}
+ /**
+ * Returns a copy of this object mapper that doesn't have any fields and runtime fields.
+ * This is typically used in the context of a mapper merge when there's not enough budget to add the entire object.
+ */
+ ObjectMapper withoutMappers() {
+ return new ObjectMapper(simpleName(), fullPath, enabled, subobjects, dynamic, Map.of());
+ }
+
@Override
public String name() {
return this.fullPath;
@@ -542,10 +550,15 @@ private static Map buildMergedMappers(
Mapper mergeWithMapper = iterator.next();
Mapper mergeIntoMapper = mergedMappers.get(mergeWithMapper.simpleName());
+ Mapper merged = null;
if (mergeIntoMapper == null) {
- mergedMappers.put(mergeWithMapper.simpleName(), mergeWithMapper);
+ if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.mapperSize())) {
+ merged = mergeWithMapper;
+ } else if (mergeWithMapper instanceof ObjectMapper om) {
+ merged = truncateObjectMapper(reason, objectMergeContext, om);
+ }
} else if (mergeIntoMapper instanceof ObjectMapper objectMapper) {
- mergedMappers.put(objectMapper.simpleName(), objectMapper.merge(mergeWithMapper, reason, objectMergeContext));
+ merged = objectMapper.merge(mergeWithMapper, reason, objectMergeContext);
} else {
assert mergeIntoMapper instanceof FieldMapper || mergeIntoMapper instanceof FieldAliasMapper;
if (mergeWithMapper instanceof NestedObjectMapper) {
@@ -557,14 +570,28 @@ private static Map buildMergedMappers(
// If we're merging template mappings when creating an index, then a field definition always
// replaces an existing one.
if (reason == MergeReason.INDEX_TEMPLATE) {
- mergedMappers.put(mergeWithMapper.simpleName(), mergeWithMapper);
+ merged = mergeWithMapper;
} else {
- mergedMappers.put(mergeWithMapper.simpleName(), mergeIntoMapper.merge(mergeWithMapper, objectMergeContext));
+ merged = mergeIntoMapper.merge(mergeWithMapper, objectMergeContext);
}
}
+ if (merged != null) {
+ mergedMappers.put(merged.simpleName(), merged);
+ }
}
return Map.copyOf(mergedMappers);
}
+
+ private static ObjectMapper truncateObjectMapper(MergeReason reason, MapperMergeContext context, ObjectMapper objectMapper) {
+ // there's not enough capacity for the whole object mapper,
+ // so we're just trying to add the shallow object, without it's sub-fields
+ ObjectMapper shallowObjectMapper = objectMapper.withoutMappers();
+ if (context.decrementFieldBudgetIfPossible(shallowObjectMapper.mapperSize())) {
+ // now trying to add the sub-fields one by one via a merge, until we hit the limit
+ return shallowObjectMapper.merge(objectMapper, reason, context);
+ }
+ return null;
+ }
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java
index 05f05dd5be941..014be192a4133 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java
@@ -160,7 +160,7 @@ public void addDynamicMappingsUpdate(Mapping update) {
if (dynamicMappingsUpdate == null) {
dynamicMappingsUpdate = update;
} else {
- dynamicMappingsUpdate = dynamicMappingsUpdate.merge(update, MergeReason.MAPPING_UPDATE);
+ dynamicMappingsUpdate = dynamicMappingsUpdate.merge(update, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE);
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java
index 3cc687161b8d6..82cda33aa77b6 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java
@@ -155,6 +155,22 @@ public RootObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) {
return builder;
}
+ @Override
+ RootObjectMapper withoutMappers() {
+ return new RootObjectMapper(
+ simpleName(),
+ enabled,
+ subobjects,
+ dynamic,
+ Map.of(),
+ Map.of(),
+ dynamicDateTimeFormatters,
+ dynamicTemplates,
+ dateDetection,
+ numericDetection
+ );
+ }
+
/**
* Public API
*/
@@ -242,12 +258,15 @@ public RootObjectMapper merge(Mapper mergeWith, MergeReason reason, MapperMergeC
dynamicTemplates = this.dynamicTemplates;
}
final Map runtimeFields = new HashMap<>(this.runtimeFields);
- assert this.runtimeFields != mergeWithObject.runtimeFields;
for (Map.Entry runtimeField : mergeWithObject.runtimeFields.entrySet()) {
if (runtimeField.getValue() == null) {
runtimeFields.remove(runtimeField.getKey());
- } else {
+ } else if (runtimeFields.containsKey(runtimeField.getKey())) {
runtimeFields.put(runtimeField.getKey(), runtimeField.getValue());
+ } else {
+ if (parentMergeContext.decrementFieldBudgetIfPossible(1)) {
+ runtimeFields.put(runtimeField.getValue().name(), runtimeField.getValue());
+ }
}
}
@@ -502,4 +521,13 @@ private static boolean processField(
}
return false;
}
+
+ @Override
+ public int mapperSize() {
+ int size = runtimeFields().size();
+ for (Mapper mapper : this) {
+ size += mapper.mapperSize();
+ }
+ return size;
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java
index f0458add93c78..e935ae2431131 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java
@@ -63,7 +63,7 @@ public void testAddFields() throws Exception {
}));
MergeReason reason = randomFrom(MergeReason.MAPPING_UPDATE, MergeReason.INDEX_TEMPLATE);
- Mapping merged = MapperService.mergeMappings(stage1, stage2.mapping(), reason);
+ Mapping merged = MapperService.mergeMappings(stage1, stage2.mapping(), reason, Long.MAX_VALUE);
// stage1 mapping should not have been modified
assertThat(stage1.mappers().getMapper("age"), nullValue());
assertThat(stage1.mappers().getMapper("obj1.prop1"), nullValue());
@@ -81,7 +81,7 @@ public void testMergeObjectDynamic() throws Exception {
DocumentMapper withDynamicMapper = createDocumentMapper(topMapping(b -> b.field("dynamic", "false")));
assertThat(withDynamicMapper.mapping().getRoot().dynamic(), equalTo(ObjectMapper.Dynamic.FALSE));
- Mapping merged = MapperService.mergeMappings(mapper, withDynamicMapper.mapping(), MergeReason.MAPPING_UPDATE);
+ Mapping merged = MapperService.mergeMappings(mapper, withDynamicMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE);
assertThat(merged.getRoot().dynamic(), equalTo(ObjectMapper.Dynamic.FALSE));
}
@@ -93,14 +93,14 @@ public void testMergeObjectAndNested() throws Exception {
{
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
- () -> MapperService.mergeMappings(objectMapper, nestedMapper.mapping(), reason)
+ () -> MapperService.mergeMappings(objectMapper, nestedMapper.mapping(), reason, Long.MAX_VALUE)
);
assertThat(e.getMessage(), containsString("can't merge a non-nested mapping [obj] with a nested mapping"));
}
{
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
- () -> MapperService.mergeMappings(nestedMapper, objectMapper.mapping(), reason)
+ () -> MapperService.mergeMappings(nestedMapper, objectMapper.mapping(), reason, Long.MAX_VALUE)
);
assertThat(e.getMessage(), containsString("can't merge a non-nested mapping [obj] with a nested mapping"));
}
@@ -235,11 +235,11 @@ public void testMergeMeta() throws IOException {
DocumentMapper updatedMapper = createDocumentMapper(fieldMapping(b -> b.field("type", "text")));
- Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE);
+ Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE);
assertThat(merged.getMeta().get("foo"), equalTo("bar"));
updatedMapper = createDocumentMapper(topMapping(b -> b.startObject("_meta").field("foo", "new_bar").endObject()));
- merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE);
+ merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE);
assertThat(merged.getMeta().get("foo"), equalTo("new_bar"));
}
@@ -262,7 +262,7 @@ public void testMergeMetaForIndexTemplate() throws IOException {
assertThat(initMapper.mapping().getMeta(), equalTo(expected));
DocumentMapper updatedMapper = createDocumentMapper(fieldMapping(b -> b.field("type", "text")));
- Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE);
+ Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE, Long.MAX_VALUE);
assertThat(merged.getMeta(), equalTo(expected));
updatedMapper = createDocumentMapper(topMapping(b -> {
@@ -278,7 +278,7 @@ public void testMergeMetaForIndexTemplate() throws IOException {
}
b.endObject();
}));
- merged = merged.merge(updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE);
+ merged = merged.merge(updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE, Long.MAX_VALUE);
expected = Map.of("field", "value", "object", Map.of("field1", "value1", "field2", "new_value", "field3", "value3"));
assertThat(merged.getMeta(), equalTo(expected));
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java
index 07f4c3c1346c4..f816f403be89f 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java
@@ -34,6 +34,7 @@ public void testParsing() throws IOException {
);
DocumentMapper mapper = createDocumentMapper(mapping);
assertEquals(mapping, mapper.mappingSource().toString());
+ assertEquals(2, mapper.mapping().getRoot().mapperSize());
}
public void testParsingWithMissingPath() {
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java
new file mode 100644
index 0000000000000..9c38487dbdf7b
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.mapper;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class MapperMergeContextTests extends ESTestCase {
+
+ public void testAddFieldIfPossibleUnderLimit() {
+ MapperMergeContext context = MapperMergeContext.root(false, false, 1);
+ assertTrue(context.decrementFieldBudgetIfPossible(1));
+ assertFalse(context.decrementFieldBudgetIfPossible(1));
+ }
+
+ public void testAddFieldIfPossibleAtLimit() {
+ MapperMergeContext context = MapperMergeContext.root(false, false, 0);
+ assertFalse(context.decrementFieldBudgetIfPossible(1));
+ }
+
+ public void testAddFieldIfPossibleUnlimited() {
+ MapperMergeContext context = MapperMergeContext.root(false, false, Long.MAX_VALUE);
+ assertTrue(context.decrementFieldBudgetIfPossible(Integer.MAX_VALUE));
+ assertTrue(context.decrementFieldBudgetIfPossible(Integer.MAX_VALUE));
+ }
+
+}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java
index e39e42bcade53..61d62c1e41969 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java
@@ -13,6 +13,7 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.IndexVersions;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
@@ -1508,16 +1509,47 @@ public void testMergeNested() {
MapperException e = expectThrows(
MapperException.class,
- () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false))
+ () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertThat(e.getMessage(), containsString("[include_in_parent] parameter can't be updated on a nested object mapping"));
NestedObjectMapper result = (NestedObjectMapper) firstMapper.merge(
secondMapper,
MapperService.MergeReason.INDEX_TEMPLATE,
- MapperMergeContext.root(false, false)
+ MapperMergeContext.root(false, false, Long.MAX_VALUE)
);
assertFalse(result.isIncludeInParent());
assertTrue(result.isIncludeInRoot());
}
+
+ public void testWithoutMappers() throws IOException {
+ ObjectMapper shallowObject = createNestedObjectMapperWithAllParametersSet(b -> {});
+ ObjectMapper object = createNestedObjectMapperWithAllParametersSet(b -> {
+ b.startObject("keyword");
+ {
+ b.field("type", "keyword");
+ }
+ b.endObject();
+ });
+ assertThat(object.withoutMappers().toString(), equalTo(shallowObject.toString()));
+ }
+
+ private NestedObjectMapper createNestedObjectMapperWithAllParametersSet(CheckedConsumer propertiesBuilder)
+ throws IOException {
+ DocumentMapper mapper = createDocumentMapper(mapping(b -> {
+ b.startObject("nested_object");
+ {
+ b.field("type", "nested");
+ b.field("enabled", false);
+ b.field("dynamic", false);
+ b.field("include_in_parent", true);
+ b.field("include_in_root", true);
+ b.startObject("properties");
+ propertiesBuilder.accept(b);
+ b.endObject();
+ }
+ b.endObject();
+ }));
+ return (NestedObjectMapper) mapper.mapping().getRoot().getMapper("nested_object");
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java
index 7cd1577dfabbd..8eb824884a591 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java
@@ -41,7 +41,7 @@ public void testMerge() {
ObjectMapper mergeWith = createMapping(false, true, true, true);
// WHEN merging mappings
- final ObjectMapper merged = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false));
+ final ObjectMapper merged = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE));
// THEN "baz" new field is added to merged mapping
final ObjectMapper mergedFoo = (ObjectMapper) merged.getMapper("foo");
@@ -63,7 +63,7 @@ public void testMergeWhenDisablingField() {
// THEN a MapperException is thrown with an excepted message
MapperException e = expectThrows(
MapperException.class,
- () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false))
+ () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertEquals("the [enabled] parameter can't be updated for the object mapping [foo]", e.getMessage());
}
@@ -75,7 +75,10 @@ public void testMergeDisabledField() {
new ObjectMapper.Builder("disabled", Explicit.IMPLICIT_TRUE)
).build(MapperBuilderContext.root(false, false));
- RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false));
+ RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(
+ mergeWith,
+ MapperMergeContext.root(false, false, Long.MAX_VALUE)
+ );
assertFalse(((ObjectMapper) merged.getMapper("disabled")).isEnabled());
}
@@ -84,14 +87,14 @@ public void testMergeEnabled() {
MapperException e = expectThrows(
MapperException.class,
- () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false))
+ () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertEquals("the [enabled] parameter can't be updated for the object mapping [disabled]", e.getMessage());
ObjectMapper result = rootObjectMapper.merge(
mergeWith,
MapperService.MergeReason.INDEX_TEMPLATE,
- MapperMergeContext.root(false, false)
+ MapperMergeContext.root(false, false, Long.MAX_VALUE)
);
assertTrue(result.isEnabled());
}
@@ -106,14 +109,14 @@ public void testMergeEnabledForRootMapper() {
MapperException e = expectThrows(
MapperException.class,
- () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false))
+ () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertEquals("the [enabled] parameter can't be updated for the object mapping [" + type + "]", e.getMessage());
ObjectMapper result = firstMapper.merge(
secondMapper,
MapperService.MergeReason.INDEX_TEMPLATE,
- MapperMergeContext.root(false, false)
+ MapperMergeContext.root(false, false, Long.MAX_VALUE)
);
assertFalse(result.isEnabled());
}
@@ -128,7 +131,10 @@ public void testMergeDisabledRootMapper() {
Collections.singletonMap("test", new TestRuntimeField("test", "long"))
).build(MapperBuilderContext.root(false, false));
- RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false));
+ RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(
+ mergeWith,
+ MapperMergeContext.root(false, false, Long.MAX_VALUE)
+ );
assertFalse(merged.isEnabled());
assertEquals(1, merged.runtimeFields().size());
assertEquals("test", merged.runtimeFields().iterator().next().name());
@@ -138,7 +144,7 @@ public void testMergedFieldNamesFieldWithDotsSubobjectsFalseAtRoot() {
RootObjectMapper mergeInto = createRootSubobjectFalseLeafWithDots();
RootObjectMapper mergeWith = createRootSubobjectFalseLeafWithDots();
- final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false));
+ final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE));
final KeywordFieldMapper keywordFieldMapper = (KeywordFieldMapper) merged.getMapper("host.name");
assertEquals("host.name", keywordFieldMapper.name());
@@ -153,7 +159,7 @@ public void testMergedFieldNamesFieldWithDotsSubobjectsFalse() {
createObjectSubobjectsFalseLeafWithDots()
).build(MapperBuilderContext.root(false, false));
- final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false));
+ final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE));
ObjectMapper foo = (ObjectMapper) merged.getMapper("foo");
ObjectMapper metrics = (ObjectMapper) foo.getMapper("metrics");
@@ -168,7 +174,7 @@ public void testMergedFieldNamesMultiFields() {
RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(createTextKeywordMultiField("text"))
.build(MapperBuilderContext.root(false, false));
- final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false));
+ final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE));
TextFieldMapper text = (TextFieldMapper) merged.getMapper("text");
assertEquals("text", text.name());
@@ -186,7 +192,7 @@ public void testMergedFieldNamesMultiFieldsWithinSubobjectsFalse() {
createObjectSubobjectsFalseLeafWithMultiField()
).build(MapperBuilderContext.root(false, false));
- final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false));
+ final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE));
ObjectMapper foo = (ObjectMapper) merged.getMapper("foo");
ObjectMapper metrics = (ObjectMapper) foo.getMapper("metrics");
@@ -198,6 +204,113 @@ public void testMergedFieldNamesMultiFieldsWithinSubobjectsFalse() {
assertEquals("keyword", fieldMapper.simpleName());
}
+ public void testMergeWithLimit() {
+ // GIVEN an enriched mapping with "baz" new field
+ ObjectMapper mergeWith = createMapping(false, true, true, true);
+
+ // WHEN merging mappings
+ final ObjectMapper mergedAdd0 = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, 0));
+ final ObjectMapper mergedAdd1 = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, 1));
+
+ // THEN "baz" new field is added to merged mapping
+ assertEquals(3, rootObjectMapper.mapperSize());
+ assertEquals(4, mergeWith.mapperSize());
+ assertEquals(3, mergedAdd0.mapperSize());
+ assertEquals(4, mergedAdd1.mapperSize());
+ }
+
+ public void testMergeWithLimitTruncatedObjectField() {
+ RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).build(MapperBuilderContext.root(false, false));
+ RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
+ new ObjectMapper.Builder("parent", Explicit.IMPLICIT_FALSE).add(
+ new KeywordFieldMapper.Builder("child1", IndexVersion.current())
+ ).add(new KeywordFieldMapper.Builder("child2", IndexVersion.current()))
+ ).build(MapperBuilderContext.root(false, false));
+
+ ObjectMapper mergedAdd0 = root.merge(mergeWith, MapperMergeContext.root(false, false, 0));
+ ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1));
+ ObjectMapper mergedAdd2 = root.merge(mergeWith, MapperMergeContext.root(false, false, 2));
+ ObjectMapper mergedAdd3 = root.merge(mergeWith, MapperMergeContext.root(false, false, 3));
+ assertEquals(0, root.mapperSize());
+ assertEquals(0, mergedAdd0.mapperSize());
+ assertEquals(1, mergedAdd1.mapperSize());
+ assertEquals(2, mergedAdd2.mapperSize());
+ assertEquals(3, mergedAdd3.mapperSize());
+
+ ObjectMapper parent1 = (ObjectMapper) mergedAdd1.getMapper("parent");
+ assertNull(parent1.getMapper("child1"));
+ assertNull(parent1.getMapper("child2"));
+
+ ObjectMapper parent2 = (ObjectMapper) mergedAdd2.getMapper("parent");
+ // the order is not deterministic, but we expect one to be null and the other to be non-null
+ assertTrue(parent2.getMapper("child1") == null ^ parent2.getMapper("child2") == null);
+
+ ObjectMapper parent3 = (ObjectMapper) mergedAdd3.getMapper("parent");
+ assertNotNull(parent3.getMapper("child1"));
+ assertNotNull(parent3.getMapper("child2"));
+ }
+
+ public void testMergeSameObjectDifferentFields() {
+ RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
+ new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add(new KeywordFieldMapper.Builder("child1", IndexVersion.current()))
+ ).build(MapperBuilderContext.root(false, false));
+ RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
+ new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add(
+ new KeywordFieldMapper.Builder("child1", IndexVersion.current()).ignoreAbove(42)
+ ).add(new KeywordFieldMapper.Builder("child2", IndexVersion.current()))
+ ).build(MapperBuilderContext.root(false, false));
+
+ ObjectMapper mergedAdd0 = root.merge(mergeWith, MapperMergeContext.root(false, false, 0));
+ ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1));
+ assertEquals(2, root.mapperSize());
+ assertEquals(2, mergedAdd0.mapperSize());
+ assertEquals(3, mergedAdd1.mapperSize());
+
+ ObjectMapper parent0 = (ObjectMapper) mergedAdd0.getMapper("parent");
+ assertNotNull(parent0.getMapper("child1"));
+ assertEquals(42, ((KeywordFieldMapper) parent0.getMapper("child1")).fieldType().ignoreAbove());
+ assertNull(parent0.getMapper("child2"));
+
+ ObjectMapper parent1 = (ObjectMapper) mergedAdd1.getMapper("parent");
+ assertNotNull(parent1.getMapper("child1"));
+ assertEquals(42, ((KeywordFieldMapper) parent1.getMapper("child1")).fieldType().ignoreAbove());
+ assertNotNull(parent1.getMapper("child2"));
+ }
+
+ public void testMergeWithLimitMultiField() {
+ RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
+ createTextKeywordMultiField("text", "keyword1")
+ ).build(MapperBuilderContext.root(false, false));
+ RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
+ createTextKeywordMultiField("text", "keyword2")
+ ).build(MapperBuilderContext.root(false, false));
+
+ assertEquals(2, mergeInto.mapperSize());
+ assertEquals(2, mergeWith.mapperSize());
+
+ ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0));
+ ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1));
+ assertEquals(2, mergedAdd0.mapperSize());
+ assertEquals(3, mergedAdd1.mapperSize());
+ }
+
+ public void testMergeWithLimitRuntimeField() {
+ RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).addRuntimeField(
+ new TestRuntimeField("existing_runtime_field", "keyword")
+ ).add(createTextKeywordMultiField("text", "keyword1")).build(MapperBuilderContext.root(false, false));
+ RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).addRuntimeField(
+ new TestRuntimeField("existing_runtime_field", "keyword")
+ ).addRuntimeField(new TestRuntimeField("new_runtime_field", "keyword")).build(MapperBuilderContext.root(false, false));
+
+ assertEquals(3, mergeInto.mapperSize());
+ assertEquals(2, mergeWith.mapperSize());
+
+ ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0));
+ ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1));
+ assertEquals(3, mergedAdd0.mapperSize());
+ assertEquals(4, mergedAdd1.mapperSize());
+ }
+
private static RootObjectMapper createRootSubobjectFalseLeafWithDots() {
FieldMapper.Builder fieldBuilder = new KeywordFieldMapper.Builder("host.name", IndexVersion.current());
FieldMapper fieldMapper = fieldBuilder.build(MapperBuilderContext.root(false, false));
@@ -231,8 +344,12 @@ private ObjectMapper.Builder createObjectSubobjectsFalseLeafWithMultiField() {
}
private TextFieldMapper.Builder createTextKeywordMultiField(String name) {
+ return createTextKeywordMultiField(name, "keyword");
+ }
+
+ private TextFieldMapper.Builder createTextKeywordMultiField(String name, String multiFieldName) {
TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers());
- builder.multiFieldsBuilder.add(new KeywordFieldMapper.Builder("keyword", IndexVersion.current()));
+ builder.multiFieldsBuilder.add(new KeywordFieldMapper.Builder(multiFieldName, IndexVersion.current()));
return builder;
}
}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java
index 3c77bf20b37d2..6e958ddbea904 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java
@@ -8,14 +8,17 @@
package org.elasticsearch.index.mapper;
+import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.index.mapper.ObjectMapper.Dynamic;
+import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentType;
@@ -23,6 +26,7 @@
import java.util.List;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
@@ -123,7 +127,7 @@ public void testMerge() throws IOException {
"_doc",
new CompressedXContent(BytesReference.bytes(topMapping(b -> b.field("dynamic", "strict"))))
);
- Mapping merged = mapper.mapping().merge(mergeWith, reason);
+ Mapping merged = mapper.mapping().merge(mergeWith, reason, Long.MAX_VALUE);
assertEquals(Dynamic.STRICT, merged.getRoot().dynamic());
}
@@ -468,7 +472,7 @@ public void testSubobjectsCannotBeUpdated() throws IOException {
}))));
MapperException exception = expectThrows(
MapperException.class,
- () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE)
+ () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE)
);
assertEquals("the [subobjects] parameter can't be updated for the object mapping [field]", exception.getMessage());
}
@@ -482,7 +486,7 @@ public void testSubobjectsCannotBeUpdatedOnRoot() throws IOException {
}))));
MapperException exception = expectThrows(
MapperException.class,
- () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE)
+ () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE)
);
assertEquals("the [subobjects] parameter can't be updated for the object mapping [_doc]", exception.getMessage());
}
@@ -525,4 +529,45 @@ public void testSyntheticSourceDocValuesFieldWithout() throws IOException {
assertThat(o.syntheticFieldLoader().docValuesLoader(null, null), nullValue());
assertThat(mapper.mapping().getRoot().syntheticFieldLoader().docValuesLoader(null, null), nullValue());
}
+
+ public void testNestedObjectWithMultiFieldsMapperSize() throws IOException {
+ ObjectMapper.Builder mapperBuilder = new ObjectMapper.Builder("parent_size_1", Explicit.IMPLICIT_TRUE).add(
+ new ObjectMapper.Builder("child_size_2", Explicit.IMPLICIT_TRUE).add(
+ new TextFieldMapper.Builder("grand_child_size_3", createDefaultIndexAnalyzers()).addMultiField(
+ new KeywordFieldMapper.Builder("multi_field_size_4", IndexVersion.current())
+ ).addMultiField(new KeywordFieldMapper.Builder("multi_field_size_5", IndexVersion.current()))
+ )
+ );
+ assertThat(mapperBuilder.build(MapperBuilderContext.root(false, false)).mapperSize(), equalTo(5));
+ }
+
+ public void testWithoutMappers() throws IOException {
+ ObjectMapper shallowObject = createObjectMapperWithAllParametersSet(b -> {});
+ ObjectMapper object = createObjectMapperWithAllParametersSet(b -> {
+ b.startObject("keyword");
+ {
+ b.field("type", "keyword");
+ }
+ b.endObject();
+ });
+ assertThat(object.withoutMappers().toString(), equalTo(shallowObject.toString()));
+ }
+
+ private ObjectMapper createObjectMapperWithAllParametersSet(CheckedConsumer propertiesBuilder)
+ throws IOException {
+ DocumentMapper mapper = createDocumentMapper(mapping(b -> {
+ b.startObject("object");
+ {
+ b.field("type", "object");
+ b.field("subobjects", false);
+ b.field("enabled", false);
+ b.field("dynamic", false);
+ b.startObject("properties");
+ propertiesBuilder.accept(b);
+ b.endObject();
+ }
+ b.endObject();
+ }));
+ return (ObjectMapper) mapper.mapping().getRoot().getMapper("object");
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java
index b2ba6f04849cf..562a30ba4f389 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java
@@ -345,7 +345,7 @@ public void testMerging() {
{"type":"test_mapper","fixed":true,"fixed2":true,"required":"value"}""");
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
- () -> mapper.merge(badMerge, MapperMergeContext.root(false, false))
+ () -> mapper.merge(badMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
String expectedError = """
Mapper for [field] conflicts with existing mapper:
@@ -358,7 +358,7 @@ public void testMerging() {
// TODO: should we have to include 'fixed' here? Or should updates take as 'defaults' the existing values?
TestMapper goodMerge = fromMapping("""
{"type":"test_mapper","fixed":false,"variable":"updated","required":"value"}""");
- TestMapper merged = (TestMapper) mapper.merge(goodMerge, MapperMergeContext.root(false, false));
+ TestMapper merged = (TestMapper) mapper.merge(goodMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE));
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper)); // original mapping is unaffected
assertEquals("""
@@ -376,7 +376,7 @@ public void testMultifields() throws IOException {
String addSubField = """
{"type":"test_mapper","variable":"foo","required":"value","fields":{"sub2":{"type":"keyword"}}}""";
TestMapper toMerge = fromMapping(addSubField);
- TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false));
+ TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE));
assertEquals(XContentHelper.stripWhitespace("""
{
"field": {
@@ -399,7 +399,7 @@ public void testMultifields() throws IOException {
TestMapper badToMerge = fromMapping(badSubField);
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
- () -> merged.merge(badToMerge, MapperMergeContext.root(false, false))
+ () -> merged.merge(badToMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertEquals("mapper [field.sub2] cannot be changed from type [keyword] to [binary]", e.getMessage());
}
@@ -415,13 +415,13 @@ public void testCopyTo() {
TestMapper toMerge = fromMapping("""
{"type":"test_mapper","variable":"updated","required":"value","copy_to":["foo","bar"]}""");
- TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false));
+ TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE));
assertEquals("""
{"field":{"type":"test_mapper","variable":"updated","required":"value","copy_to":["foo","bar"]}}""", Strings.toString(merged));
TestMapper removeCopyTo = fromMapping("""
{"type":"test_mapper","variable":"updated","required":"value"}""");
- TestMapper noCopyTo = (TestMapper) merged.merge(removeCopyTo, MapperMergeContext.root(false, false));
+ TestMapper noCopyTo = (TestMapper) merged.merge(removeCopyTo, MapperMergeContext.root(false, false, Long.MAX_VALUE));
assertEquals("""
{"field":{"type":"test_mapper","variable":"updated","required":"value"}}""", Strings.toString(noCopyTo));
}
@@ -487,7 +487,7 @@ public void testCustomSerialization() {
TestMapper toMerge = fromMapping(conflict);
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
- () -> mapper.merge(toMerge, MapperMergeContext.root(false, false))
+ () -> mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE))
);
assertEquals(
"Mapper for [field] conflicts with existing mapper:\n"
@@ -576,7 +576,10 @@ public void testAnalyzers() {
TestMapper original = mapper;
TestMapper toMerge = fromMapping(mapping);
- e = expectThrows(IllegalArgumentException.class, () -> original.merge(toMerge, MapperMergeContext.root(false, false)));
+ e = expectThrows(
+ IllegalArgumentException.class,
+ () -> original.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE))
+ );
assertEquals(
"Mapper for [field] conflicts with existing mapper:\n" + "\tCannot update parameter [analyzer] from [default] to [_standard]",
e.getMessage()
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java
index 5bd85a6dcdea7..b2a6651142181 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java
@@ -10,6 +10,7 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
@@ -19,6 +20,7 @@
import java.util.Collections;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
public class RootObjectMapperTests extends MapperServiceTestCase {
@@ -160,6 +162,7 @@ public void testRuntimeSection() throws IOException {
}));
MapperService mapperService = createMapperService(mapping);
assertEquals(mapping, mapperService.documentMapper().mappingSource().toString());
+ assertEquals(3, mapperService.documentMapper().mapping().getRoot().mapperSize());
}
public void testRuntimeSectionRejectedUpdate() throws IOException {
@@ -358,4 +361,51 @@ public void testEmptyType() throws Exception {
assertThat(e.getMessage(), containsString("type cannot be an empty string"));
}
+ public void testWithoutMappers() throws IOException {
+ RootObjectMapper shallowRoot = createRootObjectMapperWithAllParametersSet(b -> {}, b -> {});
+ RootObjectMapper root = createRootObjectMapperWithAllParametersSet(b -> {
+ b.startObject("keyword");
+ {
+ b.field("type", "keyword");
+ }
+ b.endObject();
+ }, b -> {
+ b.startObject("runtime");
+ b.startObject("field").field("type", "keyword").endObject();
+ b.endObject();
+ });
+ assertThat(root.withoutMappers().toString(), equalTo(shallowRoot.toString()));
+ }
+
+ private RootObjectMapper createRootObjectMapperWithAllParametersSet(
+ CheckedConsumer buildProperties,
+ CheckedConsumer buildRuntimeFields
+ ) throws IOException {
+ DocumentMapper mapper = createDocumentMapper(topMapping(b -> {
+ b.field("enabled", false);
+ b.field("subobjects", false);
+ b.field("dynamic", false);
+ b.field("date_detection", false);
+ b.field("numeric_detection", false);
+ b.field("dynamic_date_formats", Collections.singletonList("yyyy-MM-dd"));
+ b.startArray("dynamic_templates");
+ {
+ b.startObject();
+ {
+ b.startObject("my_template");
+ {
+ b.startObject("mapping").field("type", "keyword").endObject();
+ }
+ b.endObject();
+ }
+ b.endObject();
+ }
+ b.endArray();
+ b.startObject("properties");
+ buildProperties.accept(b);
+ b.endObject();
+ }));
+ return mapper.mapping().getRoot();
+ }
+
}