浏览代码

Adding a merge_type parameter to the ingest simulate API (#132210)

Keith Massey 2 月之前
父节点
当前提交
e223df117f

+ 6 - 0
docs/changelog/132210.yaml

@@ -0,0 +1,6 @@
+pr: 132210
+summary: Adding a `merge_type` parameter to the ingest simulate API
+area: Ingest Node
+type: enhancement
+issues:
+ - 131608

+ 154 - 0
qa/smoke-test-ingest-with-all-dependencies/src/yamlRestTest/resources/rest-api-spec/test/ingest/80_ingest_simulate.yml

@@ -1931,3 +1931,157 @@ setup:
   - match: { docs.0.doc._index: "simple-data-stream1" }
   - match: { docs.0.doc._index: "simple-data-stream1" }
   - match: { docs.0.doc._source.bar: "baz" }
   - match: { docs.0.doc._source.bar: "baz" }
   - match: { docs.0.doc.error.type: "document_parsing_exception" }
   - match: { docs.0.doc.error.type: "document_parsing_exception" }
+
+---
+"Test ingest simulate with mapping addition on subobjects":
+
+  - skip:
+      features:
+        - headers
+        - allowed_warnings
+
+  - do:
+      indices.put_index_template:
+        name: subobject-template
+        body:
+          index_patterns: subobject-index*
+          template:
+            mappings:
+              properties:
+                a.b:
+                  type: match_only_text
+
+  - do:
+      headers:
+        Content-Type: application/json
+      simulate.ingest:
+        body: >
+          {
+            "docs": [
+              {
+                "_index": "subobject-index-1",
+                "_id": "AZgsHA0B41JjTOmNiBKC",
+                "_source": {
+                  "a.b": "some text"
+                }
+              }
+            ],
+            "mapping_addition": {
+              "properties": {
+                "a.b": {
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+  - length: { docs: 1 }
+  - match: { docs.0.doc._index: "subobject-index-1" }
+  - match: { docs.0.doc._source.a\.b: "some text" }
+  - match: { docs.0.doc.error.type: "mapper_parsing_exception" }
+
+  # Here we provide a mapping_substitution to the subobject, and make sure that it is applied rather than throwing an
+  # exception.
+  - do:
+      headers:
+        Content-Type: application/json
+      simulate.ingest:
+        merge_type: "template"
+        body: >
+          {
+            "docs": [
+              {
+                "_index": "subobject-index-1",
+                "_id": "AZgsHA0B41JjTOmNiBKC",
+                "_source": {
+                  "a.b": "some text"
+                }
+              }
+            ],
+            "mapping_addition": {
+              "properties": {
+                "a.b": {
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+  - length: { docs: 1 }
+  - match: { docs.0.doc._index: "subobject-index-1" }
+  - match: { docs.0.doc._source.a\.b: "some text" }
+  - not_exists: docs.0.doc.error
+
+  # Now we run the same test but with index_template_substitutions rather than mapping_addition. In this case though,
+  # the mappings are _substituted_, not merged. That is, the original template and its mappings are completely replaced
+  # with the new one. So the merge_type has no impact.
+  - do:
+      headers:
+        Content-Type: application/json
+      simulate.ingest:
+        merge_type: "index"
+        body: >
+          {
+            "docs": [
+              {
+                "_index": "subobject-index-1",
+                "_id": "AZgsHA0B41JjTOmNiBKC",
+                "_source": {
+                  "a.b": "some text"
+                }
+              }
+            ],
+            "index_template_substitutions": {
+              "subobject-template": {
+                "index_patterns": ["subobject-index*"],
+                "template": {
+                  "mappings": {
+                    "properties": {
+                      "a.b": {
+                        "type": "keyword"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+  - length: { docs: 1 }
+  - match: { docs.0.doc._index: "subobject-index-1" }
+  - match: { docs.0.doc._source.a\.b: "some text" }
+  - not_exists: docs.0.doc.error
+
+  # This makes sure that we get the same result for merge_type "template" for index_template_substitutions
+  - do:
+      headers:
+        Content-Type: application/json
+      simulate.ingest:
+        merge_type: "template"
+        body: >
+          {
+            "docs": [
+              {
+                "_index": "subobject-index-1",
+                "_id": "AZgsHA0B41JjTOmNiBKC",
+                "_source": {
+                  "a.b": "some text"
+                }
+              }
+            ],
+            "index_template_substitutions": {
+              "subobject-template": {
+                "index_patterns": ["subobject-index*"],
+                "template": {
+                  "mappings": {
+                    "properties": {
+                      "a.b": {
+                        "type": "keyword"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+  - length: { docs: 1 }
+  - match: { docs.0.doc._index: "subobject-index-1" }
+  - match: { docs.0.doc._source.a\.b: "some text" }
+  - not_exists: docs.0.doc.error

+ 5 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/simulate.ingest.json

@@ -38,6 +38,11 @@
       "pipeline":{
       "pipeline":{
         "type":"string",
         "type":"string",
         "description":"The pipeline id to preprocess incoming documents with if no pipeline is given for a particular document"
         "description":"The pipeline id to preprocess incoming documents with if no pipeline is given for a particular document"
+      },
+      "merge_type":{
+        "type":"string",
+        "description":"The mapping merge type if mapping overrides are being provided in mapping_addition. The allowed values are one of index or template. The index option merges mappings the way they would be merged into an existing index. The template option merges mappings the way they would be merged into a template.",
+        "default": "index"
       }
       }
     },
     },
     "body":{
     "body":{

+ 13 - 10
server/src/internalClusterTest/java/org/elasticsearch/action/bulk/TransportSimulateBulkActionIT.java

@@ -61,7 +61,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
             }
             }
             """;
             """;
         indicesAdmin().create(new CreateIndexRequest(indexName).mapping(mapping)).actionGet();
         indicesAdmin().create(new CreateIndexRequest(indexName).mapping(mapping)).actionGet();
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
         bulkRequest.add(new IndexRequest(indexName).source("""
         bulkRequest.add(new IndexRequest(indexName).source("""
             {
             {
               "foo1": "baz"
               "foo1": "baz"
@@ -163,7 +163,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
             """, XContentType.JSON).id(randomUUID());
             """, XContentType.JSON).id(randomUUID());
         {
         {
             // First we use the original component template, and expect a failure in the second document:
             // First we use the original component template, and expect a failure in the second document:
-            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest2);
             bulkRequest.add(indexRequest2);
             BulkResponse response = client().execute(new ActionType<BulkResponse>(SimulateBulkAction.NAME), bulkRequest).actionGet();
             BulkResponse response = client().execute(new ActionType<BulkResponse>(SimulateBulkAction.NAME), bulkRequest).actionGet();
@@ -197,7 +197,8 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
                     )
                     )
                 ),
                 ),
                 Map.of(),
                 Map.of(),
-                Map.of()
+                Map.of(),
+                null
             );
             );
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest2);
             bulkRequest.add(indexRequest2);
@@ -235,7 +236,8 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
                     indexTemplateName,
                     indexTemplateName,
                     Map.of("index_patterns", List.of(indexName), "composed_of", List.of("test-component-template-2"))
                     Map.of("index_patterns", List.of(indexName), "composed_of", List.of("test-component-template-2"))
                 ),
                 ),
-                Map.of()
+                Map.of(),
+                null
             );
             );
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest2);
             bulkRequest.add(indexRequest2);
@@ -258,7 +260,8 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
                 Map.of(
                 Map.of(
                     "_doc",
                     "_doc",
                     Map.of("dynamic", "strict", "properties", Map.of("foo1", Map.of("type", "text"), "foo3", Map.of("type", "text")))
                     Map.of("dynamic", "strict", "properties", Map.of("foo1", Map.of("type", "text"), "foo3", Map.of("type", "text")))
-                )
+                ),
+                null
             );
             );
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest1);
             bulkRequest.add(indexRequest2);
             bulkRequest.add(indexRequest2);
@@ -277,7 +280,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
          * mapping-less "random-index-template" created by the parent class), so we expect no mapping validation failure.
          * mapping-less "random-index-template" created by the parent class), so we expect no mapping validation failure.
          */
          */
         String indexName = randomAlphaOfLength(20).toLowerCase(Locale.ROOT);
         String indexName = randomAlphaOfLength(20).toLowerCase(Locale.ROOT);
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
         bulkRequest.add(new IndexRequest(indexName).source("""
         bulkRequest.add(new IndexRequest(indexName).source("""
             {
             {
               "foo1": "baz"
               "foo1": "baz"
@@ -324,7 +327,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
         request.indexTemplate(composableIndexTemplate);
         request.indexTemplate(composableIndexTemplate);
 
 
         client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
         client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
         bulkRequest.add(new IndexRequest(indexName).source("""
         bulkRequest.add(new IndexRequest(indexName).source("""
             {
             {
               "foo1": "baz"
               "foo1": "baz"
@@ -356,7 +359,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
         indicesAdmin().putTemplate(
         indicesAdmin().putTemplate(
             new PutIndexTemplateRequest("test-template").patterns(List.of("my-index-*")).mapping("foo1", "type=integer")
             new PutIndexTemplateRequest("test-template").patterns(List.of("my-index-*")).mapping("foo1", "type=integer")
         ).actionGet();
         ).actionGet();
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
         bulkRequest.add(new IndexRequest(indexName).source("""
         bulkRequest.add(new IndexRequest(indexName).source("""
             {
             {
               "foo1": "baz"
               "foo1": "baz"
@@ -410,7 +413,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
         client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
         client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
         {
         {
             // First, try with no @timestamp to make sure we're picking up data-stream-specific templates
             // First, try with no @timestamp to make sure we're picking up data-stream-specific templates
-            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
             bulkRequest.add(new IndexRequest(indexName).source("""
             bulkRequest.add(new IndexRequest(indexName).source("""
                 {
                 {
                   "foo1": "baz"
                   "foo1": "baz"
@@ -437,7 +440,7 @@ public class TransportSimulateBulkActionIT extends ESIntegTestCase {
         }
         }
         {
         {
             // Now with @timestamp
             // Now with @timestamp
-            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+            BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
             bulkRequest.add(new IndexRequest(indexName).source("""
             bulkRequest.add(new IndexRequest(indexName).source("""
                 {
                 {
                   "@timestamp": "2024-08-27",
                   "@timestamp": "2024-08-27",

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -359,6 +359,7 @@ public class TransportVersions {
     public static final TransportVersion TRANSPORT_NODE_USAGE_STATS_FOR_THREAD_POOLS_ACTION = def(9_135_0_00);
     public static final TransportVersion TRANSPORT_NODE_USAGE_STATS_FOR_THREAD_POOLS_ACTION = def(9_135_0_00);
     public static final TransportVersion INDEX_TEMPLATE_TRACKING_INFO = def(9_136_0_00);
     public static final TransportVersion INDEX_TEMPLATE_TRACKING_INFO = def(9_136_0_00);
     public static final TransportVersion EXTENDED_SNAPSHOT_STATS_IN_NODE_INFO = def(9_137_0_00);
     public static final TransportVersion EXTENDED_SNAPSHOT_STATS_IN_NODE_INFO = def(9_137_0_00);
+    public static final TransportVersion SIMULATE_INGEST_MAPPING_MERGE_TYPE = def(9_138_0_00);
 
 
     /*
     /*
      * STOP! READ THIS FIRST! No, really,
      * STOP! READ THIS FIRST! No, really,

+ 18 - 2
server/src/main/java/org/elasticsearch/action/bulk/SimulateBulkRequest.java

@@ -103,6 +103,7 @@ public class SimulateBulkRequest extends BulkRequest {
     private final Map<String, Map<String, Object>> componentTemplateSubstitutions;
     private final Map<String, Map<String, Object>> componentTemplateSubstitutions;
     private final Map<String, Map<String, Object>> indexTemplateSubstitutions;
     private final Map<String, Map<String, Object>> indexTemplateSubstitutions;
     private final Map<String, Object> mappingAddition;
     private final Map<String, Object> mappingAddition;
+    private final String mappingMergeType;
 
 
     /**
     /**
      * @param pipelineSubstitutions The pipeline definitions that are to be used in place of any pre-existing pipeline definitions with
      * @param pipelineSubstitutions The pipeline definitions that are to be used in place of any pre-existing pipeline definitions with
@@ -118,7 +119,8 @@ public class SimulateBulkRequest extends BulkRequest {
         Map<String, Map<String, Object>> pipelineSubstitutions,
         Map<String, Map<String, Object>> pipelineSubstitutions,
         Map<String, Map<String, Object>> componentTemplateSubstitutions,
         Map<String, Map<String, Object>> componentTemplateSubstitutions,
         Map<String, Map<String, Object>> indexTemplateSubstitutions,
         Map<String, Map<String, Object>> indexTemplateSubstitutions,
-        Map<String, Object> mappingAddition
+        Map<String, Object> mappingAddition,
+        String mappingMergeType
     ) {
     ) {
         super();
         super();
         Objects.requireNonNull(pipelineSubstitutions);
         Objects.requireNonNull(pipelineSubstitutions);
@@ -129,6 +131,7 @@ public class SimulateBulkRequest extends BulkRequest {
         this.componentTemplateSubstitutions = componentTemplateSubstitutions;
         this.componentTemplateSubstitutions = componentTemplateSubstitutions;
         this.indexTemplateSubstitutions = indexTemplateSubstitutions;
         this.indexTemplateSubstitutions = indexTemplateSubstitutions;
         this.mappingAddition = mappingAddition;
         this.mappingAddition = mappingAddition;
+        this.mappingMergeType = mappingMergeType;
     }
     }
 
 
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
@@ -147,6 +150,11 @@ public class SimulateBulkRequest extends BulkRequest {
         } else {
         } else {
             mappingAddition = Map.of();
             mappingAddition = Map.of();
         }
         }
+        if (in.getTransportVersion().onOrAfter(TransportVersions.SIMULATE_INGEST_MAPPING_MERGE_TYPE)) {
+            mappingMergeType = in.readOptionalString();
+        } else {
+            mappingMergeType = null;
+        }
     }
     }
 
 
     @Override
     @Override
@@ -160,6 +168,9 @@ public class SimulateBulkRequest extends BulkRequest {
         if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_17_0)) {
         if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_17_0)) {
             out.writeGenericValue(mappingAddition);
             out.writeGenericValue(mappingAddition);
         }
         }
+        if (out.getTransportVersion().onOrAfter(TransportVersions.SIMULATE_INGEST_MAPPING_MERGE_TYPE)) {
+            out.writeOptionalString(mappingMergeType);
+        }
     }
     }
 
 
     public Map<String, Map<String, Object>> getPipelineSubstitutions() {
     public Map<String, Map<String, Object>> getPipelineSubstitutions() {
@@ -189,6 +200,10 @@ public class SimulateBulkRequest extends BulkRequest {
         return mappingAddition;
         return mappingAddition;
     }
     }
 
 
+    public String getMappingMergeType() {
+        return mappingMergeType;
+    }
+
     private static ComponentTemplate convertRawTemplateToComponentTemplate(Map<String, Object> rawTemplate) {
     private static ComponentTemplate convertRawTemplateToComponentTemplate(Map<String, Object> rawTemplate) {
         ComponentTemplate componentTemplate;
         ComponentTemplate componentTemplate;
         try (var parser = XContentHelper.mapToXContentParser(XContentParserConfiguration.EMPTY, rawTemplate)) {
         try (var parser = XContentHelper.mapToXContentParser(XContentParserConfiguration.EMPTY, rawTemplate)) {
@@ -215,7 +230,8 @@ public class SimulateBulkRequest extends BulkRequest {
             pipelineSubstitutions,
             pipelineSubstitutions,
             componentTemplateSubstitutions,
             componentTemplateSubstitutions,
             indexTemplateSubstitutions,
             indexTemplateSubstitutions,
-            mappingAddition
+            mappingAddition,
+            mappingMergeType
         );
         );
         bulkRequest.setRefreshPolicy(getRefreshPolicy());
         bulkRequest.setRefreshPolicy(getRefreshPolicy());
         bulkRequest.waitForActiveShards(waitForActiveShards());
         bulkRequest.waitForActiveShards(waitForActiveShards());

+ 25 - 10
server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java

@@ -136,6 +136,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
         Map<String, ComponentTemplate> componentTemplateSubstitutions = bulkRequest.getComponentTemplateSubstitutions();
         Map<String, ComponentTemplate> componentTemplateSubstitutions = bulkRequest.getComponentTemplateSubstitutions();
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions = bulkRequest.getIndexTemplateSubstitutions();
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions = bulkRequest.getIndexTemplateSubstitutions();
         Map<String, Object> mappingAddition = ((SimulateBulkRequest) bulkRequest).getMappingAddition();
         Map<String, Object> mappingAddition = ((SimulateBulkRequest) bulkRequest).getMappingAddition();
+        MapperService.MergeReason mappingMergeReason = getMergeReason(((SimulateBulkRequest) bulkRequest).getMappingMergeType());
         for (int i = 0; i < bulkRequest.requests.size(); i++) {
         for (int i = 0; i < bulkRequest.requests.size(); i++) {
             DocWriteRequest<?> docRequest = bulkRequest.requests.get(i);
             DocWriteRequest<?> docRequest = bulkRequest.requests.get(i);
             assert docRequest instanceof IndexRequest : "TransportSimulateBulkAction should only ever be called with IndexRequests";
             assert docRequest instanceof IndexRequest : "TransportSimulateBulkAction should only ever be called with IndexRequests";
@@ -144,7 +145,8 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
                 componentTemplateSubstitutions,
                 componentTemplateSubstitutions,
                 indexTemplateSubstitutions,
                 indexTemplateSubstitutions,
                 mappingAddition,
                 mappingAddition,
-                request
+                request,
+                mappingMergeReason
             );
             );
             Exception mappingValidationException = validationResult.v2();
             Exception mappingValidationException = validationResult.v2();
             responses.set(
             responses.set(
@@ -170,6 +172,16 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
         );
         );
     }
     }
 
 
+    private MapperService.MergeReason getMergeReason(String mergeType) {
+        return Optional.ofNullable(mergeType).map(type -> switch (type) {
+            case "index" -> MapperService.MergeReason.MAPPING_UPDATE;
+            case "template" -> MapperService.MergeReason.INDEX_TEMPLATE;
+            default -> throw new IllegalArgumentException(
+                "Unsupported merge type '" + mergeType + "'. Valid values are 'index' and 'template'."
+            );
+        }).orElse(MapperService.MergeReason.MAPPING_UPDATE);
+    }
+
     /**
     /**
      * This creates a temporary index with the mappings of the index in the request, and then attempts to index the source from the request
      * This creates a temporary index with the mappings of the index in the request, and then attempts to index the source from the request
      * into it. If there is a mapping exception, that exception is returned. On success the returned exception is null.
      * into it. If there is a mapping exception, that exception is returned. On success the returned exception is null.
@@ -182,7 +194,8 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
         Map<String, ComponentTemplate> componentTemplateSubstitutions,
         Map<String, ComponentTemplate> componentTemplateSubstitutions,
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions,
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions,
         Map<String, Object> mappingAddition,
         Map<String, Object> mappingAddition,
-        IndexRequest request
+        IndexRequest request,
+        MapperService.MergeReason mappingMergeReason
     ) {
     ) {
         final SourceToParse sourceToParse = new SourceToParse(
         final SourceToParse sourceToParse = new SourceToParse(
             request.id(),
             request.id(),
@@ -207,7 +220,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
                 IndexMetadata imd = project.getIndexSafe(indexAbstraction.getWriteIndex(request, project));
                 IndexMetadata imd = project.getIndexSafe(indexAbstraction.getWriteIndex(request, project));
                 CompressedXContent mappings = Optional.ofNullable(imd.mapping()).map(MappingMetadata::source).orElse(null);
                 CompressedXContent mappings = Optional.ofNullable(imd.mapping()).map(MappingMetadata::source).orElse(null);
                 CompressedXContent mergedMappings = mappingAddition == null ? null : mergeMappings(mappings, mappingAddition);
                 CompressedXContent mergedMappings = mappingAddition == null ? null : mergeMappings(mappings, mappingAddition);
-                ignoredFields = validateUpdatedMappingsFromIndexMetadata(imd, mergedMappings, request, sourceToParse);
+                ignoredFields = validateUpdatedMappingsFromIndexMetadata(imd, mergedMappings, request, sourceToParse, mappingMergeReason);
             } else {
             } else {
                 /*
                 /*
                  * The index did not exist, or we have component template substitutions, so we put together the mappings from existing
                  * The index did not exist, or we have component template substitutions, so we put together the mappings from existing
@@ -265,7 +278,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
                     );
                     );
                     CompressedXContent mappings = template.mappings();
                     CompressedXContent mappings = template.mappings();
                     CompressedXContent mergedMappings = mergeMappings(mappings, mappingAddition);
                     CompressedXContent mergedMappings = mergeMappings(mappings, mappingAddition);
-                    ignoredFields = validateUpdatedMappings(mappings, mergedMappings, request, sourceToParse);
+                    ignoredFields = validateUpdatedMappings(mappings, mergedMappings, request, sourceToParse, mappingMergeReason);
                 } else {
                 } else {
                     List<IndexTemplateMetadata> matchingTemplates = findV1Templates(simulatedProjectMetadata, request.index(), false);
                     List<IndexTemplateMetadata> matchingTemplates = findV1Templates(simulatedProjectMetadata, request.index(), false);
                     if (matchingTemplates.isEmpty() == false) {
                     if (matchingTemplates.isEmpty() == false) {
@@ -279,7 +292,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
                             xContentRegistry
                             xContentRegistry
                         );
                         );
                         final CompressedXContent combinedMappings = mergeMappings(new CompressedXContent(mappingsMap), mappingAddition);
                         final CompressedXContent combinedMappings = mergeMappings(new CompressedXContent(mappingsMap), mappingAddition);
-                        ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
+                        ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse, mappingMergeReason);
                     } else {
                     } else {
                         /*
                         /*
                          * The index matched no templates and had no mapping of its own. If there were component template substitutions
                          * The index matched no templates and had no mapping of its own. If there were component template substitutions
@@ -287,7 +300,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
                          * and validate.
                          * and validate.
                          */
                          */
                         final CompressedXContent combinedMappings = mergeMappings(null, mappingAddition);
                         final CompressedXContent combinedMappings = mergeMappings(null, mappingAddition);
-                        ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
+                        ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse, mappingMergeReason);
                     }
                     }
                 }
                 }
             }
             }
@@ -305,7 +318,8 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
         @Nullable CompressedXContent originalMappings,
         @Nullable CompressedXContent originalMappings,
         @Nullable CompressedXContent updatedMappings,
         @Nullable CompressedXContent updatedMappings,
         IndexRequest request,
         IndexRequest request,
-        SourceToParse sourceToParse
+        SourceToParse sourceToParse,
+        MapperService.MergeReason mappingMergeReason
     ) throws IOException {
     ) throws IOException {
         Settings dummySettings = Settings.builder()
         Settings dummySettings = Settings.builder()
             .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current())
             .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current())
@@ -318,14 +332,15 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
             originalIndexMetadataBuilder.putMapping(new MappingMetadata(originalMappings));
             originalIndexMetadataBuilder.putMapping(new MappingMetadata(originalMappings));
         }
         }
         final IndexMetadata originalIndexMetadata = originalIndexMetadataBuilder.build();
         final IndexMetadata originalIndexMetadata = originalIndexMetadataBuilder.build();
-        return validateUpdatedMappingsFromIndexMetadata(originalIndexMetadata, updatedMappings, request, sourceToParse);
+        return validateUpdatedMappingsFromIndexMetadata(originalIndexMetadata, updatedMappings, request, sourceToParse, mappingMergeReason);
     }
     }
 
 
     private Collection<String> validateUpdatedMappingsFromIndexMetadata(
     private Collection<String> validateUpdatedMappingsFromIndexMetadata(
         IndexMetadata originalIndexMetadata,
         IndexMetadata originalIndexMetadata,
         @Nullable CompressedXContent updatedMappings,
         @Nullable CompressedXContent updatedMappings,
         IndexRequest request,
         IndexRequest request,
-        SourceToParse sourceToParse
+        SourceToParse sourceToParse,
+        MapperService.MergeReason mappingMergeReason
     ) throws IOException {
     ) throws IOException {
         if (updatedMappings == null) {
         if (updatedMappings == null) {
             return List.of(); // no validation to do
             return List.of(); // no validation to do
@@ -335,7 +350,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
             .putMapping(new MappingMetadata(updatedMappings))
             .putMapping(new MappingMetadata(updatedMappings))
             .build();
             .build();
         Engine.Index result = indicesService.withTempIndexService(originalIndexMetadata, indexService -> {
         Engine.Index result = indicesService.withTempIndexService(originalIndexMetadata, indexService -> {
-            indexService.mapperService().merge(updatedIndexMetadata, MapperService.MergeReason.MAPPING_UPDATE);
+            indexService.mapperService().merge(updatedIndexMetadata, mappingMergeReason);
             return IndexShard.prepareIndex(
             return IndexShard.prepareIndex(
                 indexService.mapperService(),
                 indexService.mapperService(),
                 sourceToParse,
                 sourceToParse,

+ 3 - 1
server/src/main/java/org/elasticsearch/rest/action/ingest/RestSimulateIngestAction.java

@@ -85,11 +85,13 @@ public class RestSimulateIngestAction extends BaseRestHandler {
             "index_template_substitutions"
             "index_template_substitutions"
         );
         );
         Object mappingAddition = sourceMap.remove("mapping_addition");
         Object mappingAddition = sourceMap.remove("mapping_addition");
+        String mappingMergeType = request.param("merge_type");
         SimulateBulkRequest bulkRequest = new SimulateBulkRequest(
         SimulateBulkRequest bulkRequest = new SimulateBulkRequest(
             pipelineSubstitutions == null ? Map.of() : pipelineSubstitutions,
             pipelineSubstitutions == null ? Map.of() : pipelineSubstitutions,
             componentTemplateSubstitutions == null ? Map.of() : componentTemplateSubstitutions,
             componentTemplateSubstitutions == null ? Map.of() : componentTemplateSubstitutions,
             indexTemplateSubstitutions == null ? Map.of() : indexTemplateSubstitutions,
             indexTemplateSubstitutions == null ? Map.of() : indexTemplateSubstitutions,
-            mappingAddition == null ? Map.of() : Map.of("_doc", mappingAddition)
+            mappingAddition == null ? Map.of() : Map.of("_doc", mappingAddition),
+            mappingMergeType
         );
         );
         BytesReference transformedData = convertToBulkRequestXContentBytes(sourceMap);
         BytesReference transformedData = convertToBulkRequestXContentBytes(sourceMap);
         bulkRequest.add(
         bulkRequest.add(

+ 52 - 12
server/src/test/java/org/elasticsearch/action/bulk/SimulateBulkRequestTests.java

@@ -33,7 +33,8 @@ public class SimulateBulkRequestTests extends ESTestCase {
             getMapOrEmpty(getTestPipelineSubstitutions()),
             getMapOrEmpty(getTestPipelineSubstitutions()),
             getMapOrEmpty(getTestComponentTemplateSubstitutions()),
             getMapOrEmpty(getTestComponentTemplateSubstitutions()),
             getMapOrEmpty(getTestIndexTemplateSubstitutions()),
             getMapOrEmpty(getTestIndexTemplateSubstitutions()),
-            getMapOrEmpty(getTestMappingAddition())
+            getMapOrEmpty(getTestMappingAddition()),
+            getTestMappingMergeType()
         );
         );
     }
     }
 
 
@@ -52,7 +53,8 @@ public class SimulateBulkRequestTests extends ESTestCase {
                 null,
                 null,
                 getTestPipelineSubstitutions(),
                 getTestPipelineSubstitutions(),
                 getTestComponentTemplateSubstitutions(),
                 getTestComponentTemplateSubstitutions(),
-                getTestMappingAddition()
+                getTestMappingAddition(),
+                getTestMappingMergeType()
             )
             )
         );
         );
         assertThrows(
         assertThrows(
@@ -61,12 +63,19 @@ public class SimulateBulkRequestTests extends ESTestCase {
                 getTestPipelineSubstitutions(),
                 getTestPipelineSubstitutions(),
                 null,
                 null,
                 getTestComponentTemplateSubstitutions(),
                 getTestComponentTemplateSubstitutions(),
-                getTestMappingAddition()
+                getTestMappingAddition(),
+                getTestMappingMergeType()
             )
             )
         );
         );
         assertThrows(
         assertThrows(
             NullPointerException.class,
             NullPointerException.class,
-            () -> new SimulateBulkRequest(getTestPipelineSubstitutions(), getTestPipelineSubstitutions(), null, getTestMappingAddition())
+            () -> new SimulateBulkRequest(
+                getTestPipelineSubstitutions(),
+                getTestPipelineSubstitutions(),
+                null,
+                getTestMappingAddition(),
+                getTestMappingMergeType()
+            )
         );
         );
         assertThrows(
         assertThrows(
             NullPointerException.class,
             NullPointerException.class,
@@ -74,7 +83,8 @@ public class SimulateBulkRequestTests extends ESTestCase {
                 getTestPipelineSubstitutions(),
                 getTestPipelineSubstitutions(),
                 getTestPipelineSubstitutions(),
                 getTestPipelineSubstitutions(),
                 getTestComponentTemplateSubstitutions(),
                 getTestComponentTemplateSubstitutions(),
-                null
+                null,
+                getTestMappingMergeType()
             )
             )
         );
         );
     }
     }
@@ -83,13 +93,15 @@ public class SimulateBulkRequestTests extends ESTestCase {
         Map<String, Map<String, Object>> pipelineSubstitutions,
         Map<String, Map<String, Object>> pipelineSubstitutions,
         Map<String, Map<String, Object>> componentTemplateSubstitutions,
         Map<String, Map<String, Object>> componentTemplateSubstitutions,
         Map<String, Map<String, Object>> indexTemplateSubstitutions,
         Map<String, Map<String, Object>> indexTemplateSubstitutions,
-        Map<String, Object> mappingAddition
+        Map<String, Object> mappingAddition,
+        String mappingMergeType
     ) throws IOException {
     ) throws IOException {
         SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(
         SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(
             pipelineSubstitutions,
             pipelineSubstitutions,
             componentTemplateSubstitutions,
             componentTemplateSubstitutions,
             indexTemplateSubstitutions,
             indexTemplateSubstitutions,
-            mappingAddition
+            mappingAddition,
+            mappingMergeType
         );
         );
         /*
         /*
          * Note: SimulateBulkRequest does not implement equals or hashCode, so we can't test serialization in the usual way for a
          * Note: SimulateBulkRequest does not implement equals or hashCode, so we can't test serialization in the usual way for a
@@ -97,11 +109,18 @@ public class SimulateBulkRequestTests extends ESTestCase {
          */
          */
         SimulateBulkRequest copy = copyWriteable(simulateBulkRequest, null, SimulateBulkRequest::new);
         SimulateBulkRequest copy = copyWriteable(simulateBulkRequest, null, SimulateBulkRequest::new);
         assertThat(copy.getPipelineSubstitutions(), equalTo(simulateBulkRequest.getPipelineSubstitutions()));
         assertThat(copy.getPipelineSubstitutions(), equalTo(simulateBulkRequest.getPipelineSubstitutions()));
+        assertThat(copy.getMappingMergeType(), equalTo(simulateBulkRequest.getMappingMergeType()));
     }
     }
 
 
     @SuppressWarnings({ "unchecked", "rawtypes" })
     @SuppressWarnings({ "unchecked", "rawtypes" })
     public void testGetComponentTemplateSubstitutions() throws IOException {
     public void testGetComponentTemplateSubstitutions() throws IOException {
-        SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(
+            Map.of(),
+            Map.of(),
+            Map.of(),
+            Map.of(),
+            getTestMappingMergeType()
+        );
         assertThat(simulateBulkRequest.getComponentTemplateSubstitutions(), equalTo(Map.of()));
         assertThat(simulateBulkRequest.getComponentTemplateSubstitutions(), equalTo(Map.of()));
         String substituteComponentTemplatesString = """
         String substituteComponentTemplatesString = """
               {
               {
@@ -135,7 +154,13 @@ public class SimulateBulkRequestTests extends ESTestCase {
             XContentType.JSON
             XContentType.JSON
         ).v2();
         ).v2();
         Map<String, Map<String, Object>> substituteComponentTemplates = (Map<String, Map<String, Object>>) tempMap;
         Map<String, Map<String, Object>> substituteComponentTemplates = (Map<String, Map<String, Object>>) tempMap;
-        simulateBulkRequest = new SimulateBulkRequest(Map.of(), substituteComponentTemplates, Map.of(), Map.of());
+        simulateBulkRequest = new SimulateBulkRequest(
+            Map.of(),
+            substituteComponentTemplates,
+            Map.of(),
+            Map.of(),
+            getTestMappingMergeType()
+        );
         Map<String, ComponentTemplate> componentTemplateSubstitutions = simulateBulkRequest.getComponentTemplateSubstitutions();
         Map<String, ComponentTemplate> componentTemplateSubstitutions = simulateBulkRequest.getComponentTemplateSubstitutions();
         assertThat(componentTemplateSubstitutions.size(), equalTo(2));
         assertThat(componentTemplateSubstitutions.size(), equalTo(2));
         assertThat(
         assertThat(
@@ -160,7 +185,13 @@ public class SimulateBulkRequestTests extends ESTestCase {
     }
     }
 
 
     public void testGetIndexTemplateSubstitutions() throws IOException {
     public void testGetIndexTemplateSubstitutions() throws IOException {
-        SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(
+            Map.of(),
+            Map.of(),
+            Map.of(),
+            Map.of(),
+            getTestMappingMergeType()
+        );
         assertThat(simulateBulkRequest.getIndexTemplateSubstitutions(), equalTo(Map.of()));
         assertThat(simulateBulkRequest.getIndexTemplateSubstitutions(), equalTo(Map.of()));
         String substituteIndexTemplatesString = """
         String substituteIndexTemplatesString = """
               {
               {
@@ -196,7 +227,7 @@ public class SimulateBulkRequestTests extends ESTestCase {
             randomBoolean(),
             randomBoolean(),
             XContentType.JSON
             XContentType.JSON
         ).v2();
         ).v2();
-        simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), substituteIndexTemplates, Map.of());
+        simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), substituteIndexTemplates, Map.of(), getTestMappingMergeType());
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions = simulateBulkRequest.getIndexTemplateSubstitutions();
         Map<String, ComposableIndexTemplate> indexTemplateSubstitutions = simulateBulkRequest.getIndexTemplateSubstitutions();
         assertThat(indexTemplateSubstitutions.size(), equalTo(2));
         assertThat(indexTemplateSubstitutions.size(), equalTo(2));
         assertThat(
         assertThat(
@@ -222,7 +253,8 @@ public class SimulateBulkRequestTests extends ESTestCase {
             getTestPipelineSubstitutions(),
             getTestPipelineSubstitutions(),
             getTestComponentTemplateSubstitutions(),
             getTestComponentTemplateSubstitutions(),
             getTestIndexTemplateSubstitutions(),
             getTestIndexTemplateSubstitutions(),
-            getTestMappingAddition()
+            getTestMappingAddition(),
+            getTestMappingMergeType()
         );
         );
         simulateBulkRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
         simulateBulkRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
         simulateBulkRequest.waitForActiveShards(randomIntBetween(1, 10));
         simulateBulkRequest.waitForActiveShards(randomIntBetween(1, 10));
@@ -308,4 +340,12 @@ public class SimulateBulkRequestTests extends ESTestCase {
             )
             )
         );
         );
     }
     }
+
+    private static String getTestMappingMergeType() {
+        if (randomBoolean()) {
+            return null;
+        } else {
+            return randomFrom("index", "template");
+        }
+    }
 }
 }

+ 2 - 2
server/src/test/java/org/elasticsearch/action/bulk/TransportSimulateBulkActionTests.java

@@ -138,7 +138,7 @@ public class TransportSimulateBulkActionTests extends ESTestCase {
 
 
     public void testIndexData() throws IOException {
     public void testIndexData() throws IOException {
         Task task = mock(Task.class); // unused
         Task task = mock(Task.class); // unused
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
         int bulkItemCount = randomIntBetween(0, 200);
         int bulkItemCount = randomIntBetween(0, 200);
         for (int i = 0; i < bulkItemCount; i++) {
         for (int i = 0; i < bulkItemCount; i++) {
             Map<String, ?> source = Map.of(randomAlphaOfLength(10), randomAlphaOfLength(5));
             Map<String, ?> source = Map.of(randomAlphaOfLength(10), randomAlphaOfLength(5));
@@ -225,7 +225,7 @@ public class TransportSimulateBulkActionTests extends ESTestCase {
          * Here we only add a mapping_addition because if there is no mapping at all TransportSimulateBulkAction skips mapping validation
          * Here we only add a mapping_addition because if there is no mapping at all TransportSimulateBulkAction skips mapping validation
          * altogether, and we need it to run for this test to pass.
          * altogether, and we need it to run for this test to pass.
          */
          */
-        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of("_doc", Map.of("dynamic", "strict")));
+        BulkRequest bulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of("_doc", Map.of("dynamic", "strict")), null);
         int bulkItemCount = randomIntBetween(0, 200);
         int bulkItemCount = randomIntBetween(0, 200);
         Map<String, IndexMetadata> indicesMap = new HashMap<>();
         Map<String, IndexMetadata> indicesMap = new HashMap<>();
         Map<String, IndexTemplateMetadata> v1Templates = new HashMap<>();
         Map<String, IndexTemplateMetadata> v1Templates = new HashMap<>();

+ 3 - 3
server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java

@@ -71,7 +71,7 @@ public class SimulateIngestServiceTests extends ESTestCase {
         ingestService.innerUpdatePipelines(projectId, ingestMetadata);
         ingestService.innerUpdatePipelines(projectId, ingestMetadata);
         {
         {
             // First we make sure that if there are no substitutions that we get our original pipeline back:
             // First we make sure that if there are no substitutions that we get our original pipeline back:
-            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of());
+            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(Map.of(), Map.of(), Map.of(), Map.of(), null);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             Pipeline pipeline = simulateIngestService.getPipeline(projectId, "pipeline1");
             Pipeline pipeline = simulateIngestService.getPipeline(projectId, "pipeline1");
             assertThat(pipeline.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1"))));
             assertThat(pipeline.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1"))));
@@ -89,7 +89,7 @@ public class SimulateIngestServiceTests extends ESTestCase {
             );
             );
             pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap()))));
             pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap()))));
 
 
-            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions, Map.of(), Map.of(), Map.of());
+            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions, Map.of(), Map.of(), Map.of(), null);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             Pipeline pipeline1 = simulateIngestService.getPipeline(projectId, "pipeline1");
             Pipeline pipeline1 = simulateIngestService.getPipeline(projectId, "pipeline1");
             assertThat(
             assertThat(
@@ -109,7 +109,7 @@ public class SimulateIngestServiceTests extends ESTestCase {
              */
              */
             Map<String, Map<String, Object>> pipelineSubstitutions = new HashMap<>();
             Map<String, Map<String, Object>> pipelineSubstitutions = new HashMap<>();
             pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap()))));
             pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap()))));
-            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions, Map.of(), Map.of(), Map.of());
+            SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions, Map.of(), Map.of(), Map.of(), null);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest);
             Pipeline pipeline1 = simulateIngestService.getPipeline(projectId, "pipeline1");
             Pipeline pipeline1 = simulateIngestService.getPipeline(projectId, "pipeline1");
             assertThat(pipeline1.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1"))));
             assertThat(pipeline1.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1"))));