ソースを参照

[Transform] enhance the output of preview to return full desti… (#53572)

changes the output format of preview regarding deduced mappings and enhances
it to return all the details about auto-index creation. This allows the user
to customize the index creation. Using HLRC you can create a index request
from the output of the response.
Hendrik Muhs 5 年 前
コミット
fa6d197e7b
14 ファイル変更857 行追加240 行削除
  1. 145 15
      client/rest-high-level/src/main/java/org/elasticsearch/client/transform/PreviewTransformResponse.java
  2. 108 24
      client/rest-high-level/src/test/java/org/elasticsearch/client/transform/PreviewTransformResponseTests.java
  3. 118 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/transform/hlrc/PreviewTransformResponseTests.java
  4. 54 56
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java
  5. 145 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformDestIndexSettings.java
  6. 11 26
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformsActionResponseTests.java
  7. 28 13
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformsActionResponseWireTests.java
  8. 72 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformDestIndexSettingsTests.java
  9. 8 8
      x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml
  10. 37 12
      x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java
  11. 9 2
      x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java
  12. 12 5
      x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java
  13. 101 66
      x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformIndex.java
  14. 9 13
      x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java

+ 145 - 15
client/rest-high-level/src/main/java/org/elasticsearch/client/transform/PreviewTransformResponse.java

@@ -19,40 +19,167 @@
 
 package org.elasticsearch.client.transform;
 
+import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
 public class PreviewTransformResponse {
 
-    private static final String PREVIEW = "preview";
-    private static final String MAPPINGS = "mappings";
+    public static class GeneratedDestIndexSettings {
+        static final ParseField MAPPINGS = new ParseField("mappings");
+        private static final ParseField SETTINGS = new ParseField("settings");
+        private static final ParseField ALIASES = new ParseField("aliases");
 
-    @SuppressWarnings("unchecked")
-    public static PreviewTransformResponse fromXContent(final XContentParser parser) throws IOException {
-        Map<String, Object> previewMap = parser.mapOrdered();
-        Object previewDocs = previewMap.get(PREVIEW);
-        Object mappings = previewMap.get(MAPPINGS);
-        return new PreviewTransformResponse((List<Map<String, Object>>) previewDocs, (Map<String, Object>) mappings);
+        private final Map<String, Object> mappings;
+        private final Settings settings;
+        private final Set<Alias> aliases;
+
+        private static final ConstructingObjectParser<GeneratedDestIndexSettings, Void> PARSER = new ConstructingObjectParser<>(
+            "transform_preview_generated_dest_index",
+            true,
+            args -> {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> mappings = (Map<String, Object>) args[0];
+                Settings settings = (Settings) args[1];
+                @SuppressWarnings("unchecked")
+                Set<Alias> aliases = (Set<Alias>) args[2];
+
+                return new GeneratedDestIndexSettings(mappings, settings, aliases);
+            }
+        );
+
+        static {
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.mapOrdered(), MAPPINGS);
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> Settings.fromXContent(p), SETTINGS);
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> {
+                Set<Alias> aliases = new HashSet<>();
+                while ((p.nextToken()) != XContentParser.Token.END_OBJECT) {
+                    aliases.add(Alias.fromXContent(p));
+                }
+                return aliases;
+            }, ALIASES);
+        }
+
+        public GeneratedDestIndexSettings(Map<String, Object> mappings, Settings settings, Set<Alias> aliases) {
+            this.mappings = mappings == null ? Collections.emptyMap() : Collections.unmodifiableMap(mappings);
+            this.settings = settings == null ? Settings.EMPTY : settings;
+            this.aliases = aliases == null ? Collections.emptySet() : Collections.unmodifiableSet(aliases);
+        }
+
+        public Map<String, Object> getMappings() {
+            return mappings;
+        }
+
+        public Settings getSettings() {
+            return settings;
+        }
+
+        public Set<Alias> getAliases() {
+            return aliases;
+        }
+
+        public static GeneratedDestIndexSettings fromXContent(final XContentParser parser) {
+            return PARSER.apply(parser, null);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+
+            if (obj == null || obj.getClass() != getClass()) {
+                return false;
+            }
+
+            GeneratedDestIndexSettings other = (GeneratedDestIndexSettings) obj;
+            return Objects.equals(other.mappings, mappings)
+                && Objects.equals(other.settings, settings)
+                && Objects.equals(other.aliases, aliases);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mappings, settings, aliases);
+        }
     }
 
-    private List<Map<String, Object>> docs;
-    private Map<String, Object> mappings;
+    public static final ParseField PREVIEW = new ParseField("preview");
+    public static final ParseField GENERATED_DEST_INDEX_SETTINGS = new ParseField("generated_dest_index");
+
+    private final List<Map<String, Object>> docs;
+    private final GeneratedDestIndexSettings generatedDestIndexSettings;
+
+    private static final ConstructingObjectParser<PreviewTransformResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "data_frame_transform_preview",
+        true,
+        args -> {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> docs = (List<Map<String, Object>>) args[0];
+            GeneratedDestIndexSettings generatedDestIndex = (GeneratedDestIndexSettings) args[1];
+
+            // ensure generatedDestIndex is not null
+            if (generatedDestIndex == null) {
+                // BWC parsing the output from nodes < 7.7
+                @SuppressWarnings("unchecked")
+                Map<String, Object> mappings = (Map<String, Object>) args[2];
+                generatedDestIndex = new GeneratedDestIndexSettings(mappings, null, null);
+            }
 
-    public PreviewTransformResponse(List<Map<String, Object>> docs, Map<String, Object> mappings) {
+            return new PreviewTransformResponse(docs, generatedDestIndex);
+        }
+    );
+    static {
+        PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> p.mapOrdered(), PREVIEW);
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> GeneratedDestIndexSettings.fromXContent(p), GENERATED_DEST_INDEX_SETTINGS);
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.mapOrdered(), GeneratedDestIndexSettings.MAPPINGS);
+    }
+
+    public PreviewTransformResponse(List<Map<String, Object>> docs, GeneratedDestIndexSettings generatedDestIndexSettings) {
         this.docs = docs;
-        this.mappings = mappings;
+        this.generatedDestIndexSettings = generatedDestIndexSettings;
     }
 
     public List<Map<String, Object>> getDocs() {
         return docs;
     }
 
+    public GeneratedDestIndexSettings getGeneratedDestIndexSettings() {
+        return generatedDestIndexSettings;
+    }
+
     public Map<String, Object> getMappings() {
-        return mappings;
+        return generatedDestIndexSettings.getMappings();
+    }
+
+    public Settings getSettings() {
+        return generatedDestIndexSettings.getSettings();
+    }
+
+    public Set<Alias> getAliases() {
+        return generatedDestIndexSettings.getAliases();
+    }
+
+    public CreateIndexRequest getCreateIndexRequest(String index) {
+        CreateIndexRequest createIndexRequest = new CreateIndexRequest(index);
+        createIndexRequest.aliases(generatedDestIndexSettings.getAliases());
+        createIndexRequest.settings(generatedDestIndexSettings.getSettings());
+        createIndexRequest.mapping(generatedDestIndexSettings.getMappings());
+
+        return createIndexRequest;
     }
 
     @Override
@@ -66,12 +193,15 @@ public class PreviewTransformResponse {
         }
 
         PreviewTransformResponse other = (PreviewTransformResponse) obj;
-        return Objects.equals(other.docs, docs) && Objects.equals(other.mappings, mappings);
+        return Objects.equals(other.docs, docs) && Objects.equals(other.generatedDestIndexSettings, generatedDestIndexSettings);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(docs, mappings);
+        return Objects.hash(docs, generatedDestIndexSettings);
     }
 
+    public static PreviewTransformResponse fromXContent(final XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
 }

+ 108 - 24
client/rest-high-level/src/test/java/org/elasticsearch/client/transform/PreviewTransformResponseTests.java

@@ -19,47 +19,75 @@
 
 package org.elasticsearch.client.transform;
 
+import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester;
+import static org.hamcrest.Matchers.equalTo;
 
 public class PreviewTransformResponseTests extends ESTestCase {
 
     public void testFromXContent() throws IOException {
-        xContentTester(this::createParser,
-                this::createTestInstance,
-                this::toXContent,
-                PreviewTransformResponse::fromXContent)
-                .supportsUnknownFields(true)
-                .randomFieldsExcludeFilter(path -> path.isEmpty() == false)
-                .test();
+        xContentTester(this::createParser, this::createTestInstance, this::toXContent, PreviewTransformResponse::fromXContent)
+            .supportsUnknownFields(true)
+            .randomFieldsExcludeFilter(path -> path.isEmpty() == false)
+            .test();
     }
 
-    private PreviewTransformResponse createTestInstance() {
-        int numDocs = randomIntBetween(5, 10);
-        List<Map<String, Object>> docs = new ArrayList<>(numDocs);
-        for (int i=0; i<numDocs; i++) {
-            int numFields = randomIntBetween(1, 4);
-            Map<String, Object> doc = new HashMap<>();
-            for (int j=0; j<numFields; j++) {
-                doc.put(randomAlphaOfLength(5), randomAlphaOfLength(5));
-            }
-            docs.add(doc);
-        }
-        int numMappingEntries = randomIntBetween(5, 10);
-        Map<String, Object> mappings = new HashMap<>(numMappingEntries);
-        for (int i = 0; i < numMappingEntries; i++) {
-            mappings.put(randomAlphaOfLength(10), Map.of("type", randomAlphaOfLength(10)));
+    public void testCreateIndexRequest() throws IOException {
+        PreviewTransformResponse previewResponse = randomPreviewResponse();
+
+        CreateIndexRequest createIndexRequest = previewResponse.getCreateIndexRequest("dest_index");
+        assertEquals("dest_index", createIndexRequest.index());
+        assertThat(createIndexRequest.aliases(), equalTo(previewResponse.getAliases()));
+        assertThat(createIndexRequest.settings(), equalTo(previewResponse.getSettings()));
+
+        XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON);
+        builder.map(previewResponse.getMappings());
+
+        assertThat(BytesReference.bytes(builder), equalTo(createIndexRequest.mappings()));
+    }
+
+    public void testBWCPre77XContent() throws IOException {
+        PreviewTransformResponse response = randomPreviewResponse();
+
+        XContentBuilder builder = XContentFactory.jsonBuilder();
+
+        builder.startObject();
+        builder.startArray("preview");
+        for (Map<String, Object> doc : response.getDocs()) {
+            builder.map(doc);
         }
+        builder.endArray();
+        builder.field("mappings", response.getGeneratedDestIndexSettings().getMappings());
+        builder.endObject();
+        XContentParser parser = createParser(builder);
+        PreviewTransformResponse oldResponse = PreviewTransformResponse.fromXContent(parser);
 
-        return new PreviewTransformResponse(docs, mappings);
+        assertThat(response.getDocs(), equalTo(oldResponse.getDocs()));
+        assertThat(response.getMappings(), equalTo(oldResponse.getMappings()));
+        assertTrue(oldResponse.getAliases().isEmpty());
+        assertThat(oldResponse.getSettings(), equalTo(Settings.EMPTY));
+    }
+
+    private PreviewTransformResponse createTestInstance() {
+        return randomPreviewResponse();
     }
 
     private void toXContent(PreviewTransformResponse response, XContentBuilder builder) throws IOException {
@@ -69,7 +97,63 @@ public class PreviewTransformResponseTests extends ESTestCase {
             builder.map(doc);
         }
         builder.endArray();
-        builder.field("mappings", response.getMappings());
+        builder.startObject("generated_dest_index");
+        builder.field("mappings", response.getGeneratedDestIndexSettings().getMappings());
+
+        builder.startObject("settings");
+        response.getGeneratedDestIndexSettings().getSettings().toXContent(builder, ToXContent.EMPTY_PARAMS);
+        builder.endObject();
+
+        builder.startObject("aliases");
+        for (Alias alias : response.getGeneratedDestIndexSettings().getAliases()) {
+            alias.toXContent(builder, ToXContent.EMPTY_PARAMS);
+        }
+        builder.endObject();
         builder.endObject();
+        builder.endObject();
+    }
+
+    private static PreviewTransformResponse randomPreviewResponse() {
+        int size = randomIntBetween(0, 10);
+        List<Map<String, Object>> data = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) {
+            data.add(Map.of(randomAlphaOfLength(10), Map.of("value1", randomIntBetween(1, 100))));
+        }
+
+        return new PreviewTransformResponse(data, randomGeneratedDestIndexSettings());
+    }
+
+    private static PreviewTransformResponse.GeneratedDestIndexSettings randomGeneratedDestIndexSettings() {
+        int size = randomIntBetween(0, 10);
+
+        Map<String, Object> mappings = null;
+        if (randomBoolean()) {
+            mappings = new HashMap<>(size);
+
+            for (int i = 0; i < size; i++) {
+                mappings.put(randomAlphaOfLength(10), Map.of("type", randomAlphaOfLength(10)));
+            }
+        }
+
+        Settings settings = null;
+        if (randomBoolean()) {
+            Settings.Builder settingsBuilder = Settings.builder();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                settingsBuilder.put(randomAlphaOfLength(10), randomBoolean());
+            }
+            settings = settingsBuilder.build();
+        }
+
+        Set<Alias> aliases = null;
+        if (randomBoolean()) {
+            aliases = new HashSet<>();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                aliases.add(new Alias(randomAlphaOfLength(10)));
+            }
+        }
+
+        return new PreviewTransformResponse.GeneratedDestIndexSettings(mappings, settings, aliases);
     }
 }

+ 118 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/transform/hlrc/PreviewTransformResponseTests.java

@@ -0,0 +1,118 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.transform.hlrc;
+
+import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.client.transform.PreviewTransformResponse;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction;
+import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction.Response;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class PreviewTransformResponseTests extends AbstractResponseTestCase<
+    PreviewTransformAction.Response,
+    org.elasticsearch.client.transform.PreviewTransformResponse> {
+
+    public static Response randomPreviewResponse() {
+        int size = randomIntBetween(0, 10);
+        List<Map<String, Object>> data = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) {
+            data.add(Map.of(randomAlphaOfLength(10), Map.of("value1", randomIntBetween(1, 100))));
+        }
+
+        return new Response(data, randomGeneratedDestIndexSettings());
+    }
+
+    private static TransformDestIndexSettings randomGeneratedDestIndexSettings() {
+        int size = randomIntBetween(0, 10);
+
+        Map<String, Object> mappings = null;
+
+        if (randomBoolean()) {
+            mappings = new HashMap<>(size);
+
+            for (int i = 0; i < size; i++) {
+                mappings.put(randomAlphaOfLength(10), Map.of("type", randomAlphaOfLength(10)));
+            }
+        }
+
+        Settings settings = null;
+        if (randomBoolean()) {
+            Settings.Builder settingsBuilder = Settings.builder();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                settingsBuilder.put(randomAlphaOfLength(10), randomBoolean());
+            }
+            settings = settingsBuilder.build();
+        }
+
+        Set<Alias> aliases = null;
+
+        if (randomBoolean()) {
+            aliases = new HashSet<>();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                aliases.add(new Alias(randomAlphaOfLength(10)));
+            }
+        }
+
+        return new TransformDestIndexSettings(mappings, settings, aliases);
+    }
+
+    @Override
+    protected Response createServerTestInstance(XContentType xContentType) {
+        return randomPreviewResponse();
+    }
+
+    @Override
+    protected PreviewTransformResponse doParseToClientInstance(XContentParser parser) throws IOException {
+        return org.elasticsearch.client.transform.PreviewTransformResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected void assertInstances(Response serverTestInstance, PreviewTransformResponse clientInstance) {
+        assertThat(serverTestInstance.getDocs(), equalTo(clientInstance.getDocs()));
+        assertThat(
+            serverTestInstance.getGeneratedDestIndexSettings().getAliases(),
+            equalTo(clientInstance.getGeneratedDestIndexSettings().getAliases())
+        );
+        assertThat(
+            serverTestInstance.getGeneratedDestIndexSettings().getMappings(),
+            equalTo(clientInstance.getGeneratedDestIndexSettings().getMappings())
+        );
+        assertThat(
+            serverTestInstance.getGeneratedDestIndexSettings().getSettings(),
+            equalTo(clientInstance.getGeneratedDestIndexSettings().getSettings())
+        );
+    }
+}

+ 54 - 56
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java

@@ -12,11 +12,12 @@ import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.ActionType;
 import org.elasticsearch.action.support.master.AcknowledgedRequest;
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
-import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
@@ -26,6 +27,7 @@ import org.elasticsearch.xpack.core.common.validation.SourceDestValidator;
 import org.elasticsearch.xpack.core.transform.TransformField;
 import org.elasticsearch.xpack.core.transform.transforms.DestConfig;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -35,6 +37,7 @@ import java.util.Map;
 import java.util.Objects;
 
 import static org.elasticsearch.action.ValidateActions.addValidationError;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
 public class PreviewTransformAction extends ActionType<PreviewTransformAction.Response> {
 
@@ -74,7 +77,7 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
                 }
             }
             content.put(TransformField.DESTINATION.getPreferredName(), tempDestination);
-            content.put(TransformField.ID.getPreferredName(), "transform-preview");
+            content.putIfAbsent(TransformField.ID.getPreferredName(), "transform-preview");
             try (
                 XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(content);
                 XContentParser newParser = XContentType.JSON.xContent()
@@ -84,7 +87,7 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
                         BytesReference.bytes(xContentBuilder).streamInput()
                     )
             ) {
-                return new Request(TransformConfig.fromXContent(newParser, "transform-preview", false));
+                return new Request(TransformConfig.fromXContent(newParser, null, false));
             }
         }
 
@@ -140,18 +143,36 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
 
     public static class Response extends ActionResponse implements ToXContentObject {
 
-        private List<Map<String, Object>> docs;
-        private Map<String, Object> mappings;
-        public static ParseField PREVIEW = new ParseField("preview");
-        public static ParseField MAPPINGS = new ParseField("mappings");
+        public static final ParseField PREVIEW = new ParseField("preview");
+        public static final ParseField GENERATED_DEST_INDEX_SETTINGS = new ParseField("generated_dest_index");
 
-        static final ObjectParser<Response, Void> PARSER = new ObjectParser<>("data_frame_transform_preview", Response::new);
+        private final List<Map<String, Object>> docs;
+        private final TransformDestIndexSettings generatedDestIndexSettings;
+
+        private static final ConstructingObjectParser<Response, Void> PARSER = new ConstructingObjectParser<>(
+            "data_frame_transform_preview",
+            true,
+            args -> {
+                @SuppressWarnings("unchecked")
+                List<Map<String, Object>> docs = (List<Map<String, Object>>) args[0];
+                TransformDestIndexSettings generatedDestIndex = (TransformDestIndexSettings) args[1];
+
+                return new Response(docs, generatedDestIndex);
+            }
+        );
         static {
-            PARSER.declareObjectArray(Response::setDocs, (p, c) -> p.mapOrdered(), PREVIEW);
-            PARSER.declareObject(Response::setMappings, (p, c) -> p.mapOrdered(), MAPPINGS);
+            PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> p.mapOrdered(), PREVIEW);
+            PARSER.declareObject(
+                optionalConstructorArg(),
+                (p, c) -> TransformDestIndexSettings.fromXContent(p),
+                GENERATED_DEST_INDEX_SETTINGS
+            );
         }
 
-        public Response() {}
+        public Response(List<Map<String, Object>> docs, TransformDestIndexSettings generatedDestIndexSettings) {
+            this.docs = docs;
+            this.generatedDestIndexSettings = generatedDestIndexSettings;
+        }
 
         public Response(StreamInput in) throws IOException {
             int size = in.readInt();
@@ -159,50 +180,22 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
             for (int i = 0; i < size; i++) {
                 this.docs.add(in.readMap());
             }
-            if (in.getVersion().onOrAfter(Version.V_7_3_0)) {
+            if (in.getVersion().onOrAfter(Version.V_8_0_0)) { // todo: V_7_7_0
+                this.generatedDestIndexSettings = new TransformDestIndexSettings(in);
+            } else if (in.getVersion().onOrAfter(Version.V_7_3_0)) {
                 Map<String, Object> objectMap = in.readMap();
-                this.mappings = objectMap == null ? null : Map.copyOf(objectMap);
+                this.generatedDestIndexSettings = new TransformDestIndexSettings(objectMap, null, null);
+            } else {
+                this.generatedDestIndexSettings = new TransformDestIndexSettings(null, null, null);
             }
         }
 
-        public Response(List<Map<String, Object>> docs) {
-            this.docs = new ArrayList<>(docs);
-        }
-
-        public void setDocs(List<Map<String, Object>> docs) {
-            this.docs = new ArrayList<>(docs);
-        }
-
-        public void setMappings(Map<String, Object> mappings) {
-            this.mappings = Map.copyOf(mappings);
+        public List<Map<String, Object>> getDocs() {
+            return docs;
         }
 
-        /**
-         * This takes the a {@code Map<String, String>} of the type "fieldname: fieldtype" and transforms it into the
-         * typical mapping format.
-         *
-         * Example:
-         *
-         * input:
-         * {"field1.subField1": "long", "field2": "keyword"}
-         *
-         * output:
-         * {
-         *     "properties": {
-         *         "field1.subField1": {
-         *             "type": "long"
-         *         },
-         *         "field2": {
-         *             "type": "keyword"
-         *         }
-         *     }
-         * }
-         * @param mappings A Map of the form {"fieldName": "fieldType"}
-         */
-        public void setMappingsFromStringMap(Map<String, String> mappings) {
-            Map<String, Object> fieldMappings = new HashMap<>();
-            mappings.forEach((k, v) -> fieldMappings.put(k, Map.of("type", v)));
-            this.mappings = Map.of("properties", fieldMappings);
+        public TransformDestIndexSettings getGeneratedDestIndexSettings() {
+            return generatedDestIndexSettings;
         }
 
         @Override
@@ -211,8 +204,10 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
             for (Map<String, Object> doc : docs) {
                 out.writeMapWithConsistentOrder(doc);
             }
-            if (out.getVersion().onOrAfter(Version.V_7_3_0)) {
-                out.writeMap(mappings);
+            if (out.getVersion().onOrAfter(Version.V_8_0_0)) { // todo: V_7_7_0
+                generatedDestIndexSettings.writeTo(out);
+            } else if (out.getVersion().onOrAfter(Version.V_7_3_0)) {
+                out.writeMap(generatedDestIndexSettings.getMappings());
             }
         }
 
@@ -220,9 +215,7 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
         public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
             builder.startObject();
             builder.field(PREVIEW.getPreferredName(), docs);
-            if (mappings != null) {
-                builder.field(MAPPINGS.getPreferredName(), mappings);
-            }
+            builder.field(GENERATED_DEST_INDEX_SETTINGS.getPreferredName(), generatedDestIndexSettings);
             builder.endObject();
             return builder;
         }
@@ -238,12 +231,17 @@ public class PreviewTransformAction extends ActionType<PreviewTransformAction.Re
             }
 
             Response other = (Response) obj;
-            return Objects.equals(other.docs, docs) && Objects.equals(other.mappings, mappings);
+            return Objects.equals(other.docs, docs) && Objects.equals(other.generatedDestIndexSettings, generatedDestIndexSettings);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(docs, mappings);
+            return Objects.hash(docs, generatedDestIndexSettings);
+        }
+
+        @Override
+        public String toString() {
+            return Strings.toString(this, true, true);
         }
 
         public static Response fromXContent(final XContentParser parser) throws IOException {

+ 145 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformDestIndexSettings.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.transform.transforms;
+
+import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.cluster.AbstractDiffable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent.Params;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class TransformDestIndexSettings extends AbstractDiffable<TransformDestIndexSettings> implements Writeable, ToXContentObject {
+
+    public static final ParseField MAPPINGS = new ParseField("mappings");
+    public static final ParseField SETTINGS = new ParseField("settings");
+    public static final ParseField ALIASES = new ParseField("aliases");
+
+    private static final ConstructingObjectParser<TransformDestIndexSettings, Void> STRICT_PARSER = createParser(false);
+
+    private final Map<String, Object> mappings;
+    private final Settings settings;
+    private final Set<Alias> aliases;
+
+    private static ConstructingObjectParser<TransformDestIndexSettings, Void> createParser(boolean lenient) {
+        ConstructingObjectParser<TransformDestIndexSettings, Void> PARSER = new ConstructingObjectParser<>(
+            "transform_preview_generated_dest_index",
+            lenient,
+            args -> {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> mappings = (Map<String, Object>) args[0];
+                Settings settings = (Settings) args[1];
+                @SuppressWarnings("unchecked")
+                Set<Alias> aliases = (Set<Alias>) args[2];
+
+                return new TransformDestIndexSettings(mappings, settings, aliases);
+            }
+        );
+
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.mapOrdered(), MAPPINGS);
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> Settings.fromXContent(p), SETTINGS);
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> {
+            Set<Alias> aliases = new HashSet<>();
+            while ((p.nextToken()) != XContentParser.Token.END_OBJECT) {
+                aliases.add(Alias.fromXContent(p));
+            }
+            return aliases;
+        }, ALIASES);
+
+        return PARSER;
+    }
+
+    public TransformDestIndexSettings(Map<String, Object> mappings, Settings settings, Set<Alias> aliases) {
+        this.mappings = mappings == null ? Collections.emptyMap() : Collections.unmodifiableMap(mappings);
+        this.settings = settings == null ? Settings.EMPTY : settings;
+        this.aliases = aliases == null ? Collections.emptySet() : Collections.unmodifiableSet(aliases);
+    }
+
+    public TransformDestIndexSettings(StreamInput in) throws IOException {
+        mappings = in.readMap();
+        settings = Settings.readSettingsFromStream(in);
+        aliases = new HashSet<>(in.readList(Alias::new));
+    }
+
+    public Map<String, Object> getMappings() {
+        return mappings;
+    }
+
+    public Settings getSettings() {
+        return settings;
+    }
+
+    public Set<Alias> getAliases() {
+        return aliases;
+    }
+
+    @Override
+    public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
+        // note: we write out the full object, even if parts are empty to gain visibility of options
+        builder.startObject();
+        builder.field(MAPPINGS.getPreferredName(), mappings);
+
+        builder.startObject(SETTINGS.getPreferredName());
+        settings.toXContent(builder, params);
+        builder.endObject();
+
+        builder.startObject(ALIASES.getPreferredName());
+        for (Alias alias : aliases) {
+            alias.toXContent(builder, params);
+        }
+        builder.endObject();
+        builder.endObject();
+        return builder;
+    }
+
+    public static TransformDestIndexSettings fromXContent(final XContentParser parser) {
+        return STRICT_PARSER.apply(parser, null);
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeMap(mappings);
+        Settings.writeSettingsToStream(settings, out);
+        out.writeCollection(aliases);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj == null || obj.getClass() != getClass()) {
+            return false;
+        }
+
+        TransformDestIndexSettings other = (TransformDestIndexSettings) obj;
+        return Objects.equals(other.mappings, mappings)
+            && Objects.equals(other.settings, settings)
+            && Objects.equals(other.aliases, aliases);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mappings, settings, aliases);
+    }
+}

+ 11 - 26
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformsActionResponseTests.java

@@ -10,15 +10,24 @@ import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.test.AbstractSerializingTestCase;
 import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction.Response;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettingsTests;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public class PreviewTransformsActionResponseTests extends AbstractSerializingTestCase<Response> {
 
+    public static Response randomPreviewResponse() {
+        int size = randomIntBetween(0, 10);
+        List<Map<String, Object>> data = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) {
+            data.add(Map.of(randomAlphaOfLength(10), Map.of("value1", randomIntBetween(1, 100))));
+        }
+
+        return new Response(data, TransformDestIndexSettingsTests.randomDestIndexSettings());
+    }
 
     @Override
     protected Response doParseInstance(XContentParser parser) throws IOException {
@@ -32,31 +41,7 @@ public class PreviewTransformsActionResponseTests extends AbstractSerializingTes
 
     @Override
     protected Response createTestInstance() {
-        int size = randomIntBetween(0, 10);
-        List<Map<String, Object>> data = new ArrayList<>(size);
-        for (int i = 0; i < size; i++) {
-            data.add(Map.of(randomAlphaOfLength(10), Map.of("value1", randomIntBetween(1, 100))));
-        }
-
-        Response response = new Response(data);
-        if (randomBoolean()) {
-            size = randomIntBetween(0, 10);
-            if (randomBoolean()) {
-                Map<String, Object> mappings = new HashMap<>(size);
-                for (int i = 0; i < size; i++) {
-                    mappings.put(randomAlphaOfLength(10), Map.of("type", randomAlphaOfLength(10)));
-                }
-                response.setMappings(mappings);
-            } else {
-                Map<String, String> mappings = new HashMap<>(size);
-                for (int i = 0; i < size; i++) {
-                    mappings.put(randomAlphaOfLength(10), randomAlphaOfLength(10));
-                }
-                response.setMappingsFromStringMap(mappings);
-            }
-        }
-
-        return response;
+        return randomPreviewResponse();
     }
 
     @Override

+ 28 - 13
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformsActionResponseWireTests.java

@@ -6,28 +6,20 @@
 
 package org.elasticsearch.xpack.core.transform.action;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.Writeable.Reader;
 import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction.Response;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
+import java.io.IOException;
 import java.util.Map;
 
 public class PreviewTransformsActionResponseWireTests extends AbstractWireSerializingTransformTestCase<Response> {
 
     @Override
     protected Response createTestInstance() {
-        int size = randomIntBetween(0, 10);
-        List<Map<String, Object>> data = new ArrayList<>(size);
-        for (int i = 0; i < size; i++) {
-            Map<String, Object> datum = new HashMap<>();
-            Map<String, Object> entry = new HashMap<>();
-            entry.put("value1", randomIntBetween(1, 100));
-            datum.put(randomAlphaOfLength(10), entry);
-            data.add(datum);
-        }
-        return new Response(data);
+        return PreviewTransformsActionResponseTests.randomPreviewResponse();
     }
 
     @Override
@@ -35,4 +27,27 @@ public class PreviewTransformsActionResponseWireTests extends AbstractWireSerial
         return Response::new;
     }
 
+    public void testBackwardsSerialization76() throws IOException {
+        Response response = PreviewTransformsActionResponseTests.randomPreviewResponse();
+
+        try (BytesStreamOutput output = new BytesStreamOutput()) {
+            output.setVersion(Version.V_7_6_0);
+            output.writeInt(response.getDocs().size());
+            for (Map<String, Object> doc : response.getDocs()) {
+                output.writeMapWithConsistentOrder(doc);
+            }
+
+            output.writeMap(response.getGeneratedDestIndexSettings().getMappings());
+            try (StreamInput in = output.bytes().streamInput()) {
+                in.setVersion(Version.V_7_6_0);
+                Response streamedResponse = new Response(in);
+                assertEquals(
+                    response.getGeneratedDestIndexSettings().getMappings(),
+                    streamedResponse.getGeneratedDestIndexSettings().getMappings()
+                );
+                assertEquals(response.getDocs(), streamedResponse.getDocs());
+            }
+        }
+    }
+
 }

+ 72 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformDestIndexSettingsTests.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.transform.transforms;
+
+import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.common.io.stream.Writeable.Reader;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class TransformDestIndexSettingsTests extends AbstractSerializingTransformTestCase<TransformDestIndexSettings> {
+
+    public static TransformDestIndexSettings randomDestIndexSettings() {
+        int size = randomIntBetween(0, 10);
+
+        Map<String, Object> mappings = null;
+
+        if (randomBoolean()) {
+            mappings = new HashMap<>(size);
+
+            for (int i = 0; i < size; i++) {
+                mappings.put(randomAlphaOfLength(10), Map.of("type", randomAlphaOfLength(10)));
+            }
+        }
+
+        Settings settings = null;
+        if (randomBoolean()) {
+            Settings.Builder settingsBuilder = Settings.builder();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                settingsBuilder.put(randomAlphaOfLength(10), randomBoolean());
+            }
+            settings = settingsBuilder.build();
+        }
+
+        Set<Alias> aliases = null;
+
+        if (randomBoolean()) {
+            aliases = new HashSet<>();
+            size = randomIntBetween(0, 10);
+            for (int i = 0; i < size; i++) {
+                aliases.add(new Alias(randomAlphaOfLength(10)));
+            }
+        }
+
+        return new TransformDestIndexSettings(mappings, settings, aliases);
+    }
+
+    @Override
+    protected TransformDestIndexSettings doParseInstance(XContentParser parser) throws IOException {
+        return TransformDestIndexSettings.fromXContent(parser);
+    }
+
+    @Override
+    protected Reader<TransformDestIndexSettings> instanceReader() {
+        return TransformDestIndexSettings::new;
+    }
+
+    @Override
+    protected TransformDestIndexSettings createTestInstance() {
+        return randomDestIndexSettings();
+    }
+}

+ 8 - 8
x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml

@@ -98,11 +98,11 @@ setup:
   - match: { preview.2.avg_response: 42.0 }
   - match: { preview.2.time.max: "2017-02-18T01:01:00.000Z" }
   - match: { preview.2.time.min: "2017-02-18T01:01:00.000Z" }
-  - match: { mappings.properties.airline.type: "keyword" }
-  - match: { mappings.properties.by-hour.type: "date" }
-  - match: { mappings.properties.avg_response.type: "double" }
-  - match: { mappings.properties.time\.max.type: "date" }
-  - match: { mappings.properties.time\.min.type: "date" }
+  - match: { generated_dest_index.mappings.properties.airline.type: "keyword" }
+  - match: { generated_dest_index.mappings.properties.by-hour.type: "date" }
+  - match: { generated_dest_index.mappings.properties.avg_response.type: "double" }
+  - match: { generated_dest_index.mappings.properties.time\.max.type: "date" }
+  - match: { generated_dest_index.mappings.properties.time\.min.type: "date" }
 
   - do:
       ingest.put_pipeline:
@@ -146,9 +146,9 @@ setup:
   - match: { preview.2.by-hour: 1487379600000 }
   - match: { preview.2.avg_response: 42.0 }
   - match: { preview.2.my_field: 42 }
-  - match: { mappings.properties.airline.type: "keyword" }
-  - match: { mappings.properties.by-hour.type: "date" }
-  - match: { mappings.properties.avg_response.type: "double" }
+  - match: { generated_dest_index.mappings.properties.airline.type: "keyword" }
+  - match: { generated_dest_index.mappings.properties.by-hour.type: "date" }
+  - match: { generated_dest_index.mappings.properties.avg_response.type: "double" }
 
 ---
 "Test preview transform with invalid config":

+ 37 - 12
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java

@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.transform.action;
 
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
+import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ingest.SimulatePipelineAction;
@@ -48,11 +49,14 @@ import org.elasticsearch.xpack.core.transform.TransformMessages;
 import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction;
 import org.elasticsearch.xpack.core.transform.transforms.SourceConfig;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
 import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerStats;
+import org.elasticsearch.xpack.transform.persistence.TransformIndex;
 import org.elasticsearch.xpack.transform.transforms.pivot.AggregationResultUtils;
 import org.elasticsearch.xpack.transform.transforms.pivot.Pivot;
 import org.elasticsearch.xpack.transform.utils.SourceDestValidations;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -62,8 +66,9 @@ import java.util.stream.Collectors;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.elasticsearch.xpack.transform.transforms.TransformIndexer.COMPOSITE_AGGREGATION_NAME;
 
-public class TransportPreviewTransformAction extends
-    HandledTransportAction<PreviewTransformAction.Request, PreviewTransformAction.Response> {
+public class TransportPreviewTransformAction extends HandledTransportAction<
+    PreviewTransformAction.Request,
+    PreviewTransformAction.Response> {
 
     private static final Logger logger = LogManager.getLogger(TransportPreviewTransformAction.class);
     private static final int NUMBER_OF_PREVIEW_BUCKETS = 100;
@@ -165,7 +170,14 @@ public class TransportPreviewTransformAction extends
                     return;
                 }
 
-                getPreview(pivot, config.getSource(), config.getDestination().getPipeline(), config.getDestination().getIndex(), listener);
+                getPreview(
+                    config.getId(), // note: @link{PreviewTransformAction} sets an id, so this is never null
+                    pivot,
+                    config.getSource(),
+                    config.getDestination().getPipeline(),
+                    config.getDestination().getIndex(),
+                    listener
+                );
 
             }, listener::onFailure)
         );
@@ -173,27 +185,34 @@ public class TransportPreviewTransformAction extends
 
     @SuppressWarnings("unchecked")
     private void getPreview(
+        String transformId,
         Pivot pivot,
         SourceConfig source,
         String pipeline,
         String dest,
         ActionListener<PreviewTransformAction.Response> listener
     ) {
-        final PreviewTransformAction.Response previewResponse = new PreviewTransformAction.Response();
+        final SetOnce<Map<String, String>> mappings = new SetOnce<>();
+
         ActionListener<SimulatePipelineResponse> pipelineResponseActionListener = ActionListener.wrap(simulatePipelineResponse -> {
-            List<Map<String, Object>> response = new ArrayList<>(simulatePipelineResponse.getResults().size());
+            List<Map<String, Object>> docs = new ArrayList<>(simulatePipelineResponse.getResults().size());
             for (var simulateDocumentResult : simulatePipelineResponse.getResults()) {
                 try (XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()) {
                     XContentBuilder content = simulateDocumentResult.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS);
                     Map<String, Object> tempMap = XContentHelper.convertToMap(BytesReference.bytes(content), true, XContentType.JSON).v2();
-                    response.add((Map<String, Object>) XContentMapValues.extractValue("doc._source", tempMap));
+                    docs.add((Map<String, Object>) XContentMapValues.extractValue("doc._source", tempMap));
                 }
             }
-            previewResponse.setDocs(response);
-            listener.onResponse(previewResponse);
+            TransformDestIndexSettings generateddestIndexSettings = TransformIndex.createTransformDestIndexSettings(
+                mappings.get(),
+                transformId,
+                Clock.systemUTC()
+            );
+
+            listener.onResponse(new PreviewTransformAction.Response(docs, generateddestIndexSettings));
         }, listener::onFailure);
         pivot.deduceMappings(client, source, ActionListener.wrap(deducedMappings -> {
-            previewResponse.setMappingsFromStringMap(deducedMappings);
+            mappings.set(deducedMappings);
             ClientHelper.executeWithHeadersAsync(
                 threadPool.getThreadContext().getHeaders(),
                 ClientHelper.TRANSFORM_ORIGIN,
@@ -214,11 +233,17 @@ public class TransportPreviewTransformAction extends
                         // remove all internal fields
 
                         if (pipeline == null) {
-                            List<Map<String, Object>> results = pivot.extractResults(agg, deducedMappings, stats)
+                            List<Map<String, Object>> docs = pivot.extractResults(agg, deducedMappings, stats)
                                 .peek(doc -> doc.keySet().removeIf(k -> k.startsWith("_")))
                                 .collect(Collectors.toList());
-                            previewResponse.setDocs(results);
-                            listener.onResponse(previewResponse);
+
+                            TransformDestIndexSettings generateddestIndexSettings = TransformIndex.createTransformDestIndexSettings(
+                                mappings.get(),
+                                transformId,
+                                Clock.systemUTC()
+                            );
+
+                            listener.onResponse(new PreviewTransformAction.Response(docs, generateddestIndexSettings));
                         } else {
                             List<Map<String, Object>> results = pivot.extractResults(agg, deducedMappings, stats).map(doc -> {
                                 Map<String, Object> src = new HashMap<>();

+ 9 - 2
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java

@@ -44,6 +44,7 @@ import org.elasticsearch.xpack.core.common.validation.SourceDestValidator;
 import org.elasticsearch.xpack.core.transform.TransformMessages;
 import org.elasticsearch.xpack.core.transform.action.StartTransformAction;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
 import org.elasticsearch.xpack.core.transform.transforms.TransformState;
 import org.elasticsearch.xpack.core.transform.transforms.TransformTaskParams;
 import org.elasticsearch.xpack.core.transform.transforms.TransformTaskState;
@@ -298,8 +299,14 @@ public class TransportStartTransformAction extends TransportMasterNodeAction<Sta
 
         final Pivot pivot = new Pivot(config.getPivotConfig());
 
-        ActionListener<Map<String, String>> deduceMappingsListener = ActionListener.wrap(
-            mappings -> TransformIndex.createDestinationIndex(client, Clock.systemUTC(), config, mappings, listener),
+        ActionListener<Map<String, String>> deduceMappingsListener = ActionListener.wrap(mappings -> {
+            TransformDestIndexSettings generateddestIndexSettings = TransformIndex.createTransformDestIndexSettings(
+                mappings,
+                config.getId(),
+                Clock.systemUTC()
+            );
+            TransformIndex.createDestinationIndex(client, config, generateddestIndexSettings, listener);
+        },
             deduceTargetMappingsException -> listener.onFailure(
                 new RuntimeException(TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_DEDUCE_DEST_MAPPINGS, deduceTargetMappingsException)
             )

+ 12 - 5
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java

@@ -50,6 +50,7 @@ import org.elasticsearch.xpack.core.transform.action.UpdateTransformAction.Reque
 import org.elasticsearch.xpack.core.transform.action.UpdateTransformAction.Response;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfigUpdate;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
 import org.elasticsearch.xpack.transform.TransformServices;
 import org.elasticsearch.xpack.transform.notifications.TransformAuditor;
 import org.elasticsearch.xpack.transform.persistence.SeqNoPrimaryTermAndIndex;
@@ -350,14 +351,20 @@ public class TransportUpdateTransformAction extends TransportMasterNodeAction<Re
     }
 
     private void createDestination(Pivot pivot, TransformConfig config, ActionListener<Void> listener) {
-        ActionListener<Map<String, String>> deduceMappingsListener = ActionListener.wrap(
-            mappings -> TransformIndex.createDestinationIndex(
+        ActionListener<Map<String, String>> deduceMappingsListener = ActionListener.wrap(mappings -> {
+            TransformDestIndexSettings generateddestIndexSettings = TransformIndex.createTransformDestIndexSettings(
+                mappings,
+                config.getId(),
+                Clock.systemUTC()
+            );
+            TransformIndex.createDestinationIndex(
                 client,
-                Clock.systemUTC(),
                 config,
-                mappings,
+                generateddestIndexSettings,
                 ActionListener.wrap(r -> listener.onResponse(null), listener::onFailure)
-            ),
+            );
+        },
+
             deduceTargetMappingsException -> listener.onFailure(
                 new RuntimeException(TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_DEDUCE_DEST_MAPPINGS, deduceTargetMappingsException)
             )

+ 101 - 66
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformIndex.java

@@ -10,99 +10,134 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.alias.Alias;
 import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.xpack.core.transform.TransformField;
 import org.elasticsearch.xpack.core.transform.TransformMessages;
 import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
+import org.elasticsearch.xpack.core.transform.transforms.TransformDestIndexSettings;
 
-import java.io.IOException;
 import java.time.Clock;
+import java.util.HashMap;
 import java.util.Map;
-import java.util.Map.Entry;
-
-import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
-import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME;
+import java.util.Set;
 
 public final class TransformIndex {
     private static final Logger logger = LogManager.getLogger(TransformIndex.class);
 
     private static final String PROPERTIES = "properties";
-    private static final String TYPE = "type";
     private static final String META = "_meta";
 
-    private TransformIndex() {
-    }
+    private TransformIndex() {}
 
-    public static void createDestinationIndex(Client client,
-                                              Clock clock,
-                                              TransformConfig transformConfig,
-                                              Map<String, String> mappings,
-                                              ActionListener<Boolean> listener) {
+    public static void createDestinationIndex(
+        Client client,
+        TransformConfig transformConfig,
+        TransformDestIndexSettings destIndexSettings,
+        ActionListener<Boolean> listener
+    ) {
         CreateIndexRequest request = new CreateIndexRequest(transformConfig.getDestination().getIndex());
 
-        // TODO: revisit number of shards, number of replicas
-        request.settings(Settings.builder() // <1>
-                .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
-                .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1"));
-
-        request.mapping(createMappingXContent(mappings, transformConfig.getId(), clock));
-
-        client.execute(CreateIndexAction.INSTANCE, request, ActionListener.wrap(createIndexResponse -> {
-            listener.onResponse(true);
-        }, e -> {
-            String message = TransformMessages.getMessage(TransformMessages.FAILED_TO_CREATE_DESTINATION_INDEX,
-                    transformConfig.getDestination().getIndex(), transformConfig.getId());
-            logger.error(message);
-            listener.onFailure(new RuntimeException(message, e));
-        }));
-    }
-
-    private static XContentBuilder createMappingXContent(Map<String, String> mappings,
-                                                         String id,
-                                                         Clock clock) {
-        try {
-            XContentBuilder builder = jsonBuilder().startObject();
-            builder.startObject(SINGLE_MAPPING_NAME);
-            addProperties(builder, mappings);
-            addMetaData(builder, id, clock);
-            builder.endObject(); // _doc type
-            return builder.endObject();
-        } catch (IOException e) {
-            throw new RuntimeException(e);
+        request.settings(destIndexSettings.getSettings());
+        request.mapping(destIndexSettings.getMappings());
+        for (Alias alias : destIndexSettings.getAliases()) {
+            request.alias(alias);
         }
+
+        client.execute(
+            CreateIndexAction.INSTANCE,
+            request,
+            ActionListener.wrap(createIndexResponse -> { listener.onResponse(true); }, e -> {
+                String message = TransformMessages.getMessage(
+                    TransformMessages.FAILED_TO_CREATE_DESTINATION_INDEX,
+                    transformConfig.getDestination().getIndex(),
+                    transformConfig.getId()
+                );
+                logger.error(message);
+                listener.onFailure(new RuntimeException(message, e));
+            })
+        );
     }
 
-    private static XContentBuilder addProperties(XContentBuilder builder,
-                                                 Map<String, String> mappings) throws IOException {
-        builder.startObject(PROPERTIES);
-        for (Entry<String, String> field : mappings.entrySet()) {
-            String fieldName = field.getKey();
-            String fieldType = field.getValue();
+    public static TransformDestIndexSettings createTransformDestIndexSettings(Map<String, String> mappings, String id, Clock clock) {
+        Map<String, Object> indexMappings = new HashMap<>();
+        indexMappings.put(PROPERTIES, createMappingsFromStringMap(mappings));
+        indexMappings.put(META, createMetaData(id, clock));
 
-            builder.startObject(fieldName);
-            builder.field(TYPE, fieldType);
+        Settings settings = createSettings();
 
-            builder.endObject();
-        }
-        builder.endObject(); // PROPERTIES
-        return builder;
+        // transform does not create aliases, however the user might customize this in future
+        Set<Alias> aliases = null;
+        return new TransformDestIndexSettings(indexMappings, settings, aliases);
+    }
+
+    /*
+     * Return meta data that stores some useful information about the transform index, stored as "_meta":
+     *
+     * {
+     *   "created_by" : "transform",
+     *   "_transform" : {
+     *     "transform" : "id",
+     *     "version" : {
+     *       "created" : "8.0.0"
+     *     },
+     *     "creation_date_in_millis" : 1584025695202
+     *   }
+     * }
+     */
+    private static Map<String, Object> createMetaData(String id, Clock clock) {
+
+        Map<String, Object> metaData = new HashMap<>();
+        metaData.put(TransformField.CREATED_BY, TransformField.TRANSFORM_SIGNATURE);
+
+        Map<String, Object> transformMetaData = new HashMap<>();
+        transformMetaData.put(TransformField.CREATION_DATE_MILLIS, clock.millis());
+        transformMetaData.put(TransformField.VERSION.getPreferredName(), Map.of(TransformField.CREATED, Version.CURRENT));
+        transformMetaData.put(TransformField.TRANSFORM, id);
+
+        metaData.put(TransformField.META_FIELDNAME, transformMetaData);
+        return metaData;
+    }
+
+    /**
+     * creates generated index settings, hardcoded at the moment, in future this might be customizable or generation could
+     * be based on source settings.
+     */
+    private static Settings createSettings() {
+        return Settings.builder() // <1>
+            .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
+            .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1")
+            .build();
     }
 
-    private static XContentBuilder addMetaData(XContentBuilder builder, String id, Clock clock) throws IOException {
-        return builder.startObject(META)
-            .field(TransformField.CREATED_BY, TransformField.TRANSFORM_SIGNATURE)
-            .startObject(TransformField.META_FIELDNAME)
-                .field(TransformField.CREATION_DATE_MILLIS, clock.millis())
-                .startObject(TransformField.VERSION.getPreferredName())
-                    .field(TransformField.CREATED, Version.CURRENT)
-                .endObject()
-                .field(TransformField.TRANSFORM, id)
-            .endObject() // META_FIELDNAME
-        .endObject(); // META
+    /**
+     * This takes the a {@code Map<String, String>} of the type "fieldname: fieldtype" and transforms it into the
+     * typical mapping format.
+     *
+     * Example:
+     *
+     * input:
+     * {"field1.subField1": "long", "field2": "keyword"}
+     *
+     * output:
+     * {
+     *   "field1.subField1": {
+     *     "type": "long"
+     *   },
+     *   "field2": {
+     *     "type": "keyword"
+     *   }
+     * }
+     * @param mappings A Map of the form {"fieldName": "fieldType"}
+     */
+    private static Map<String, Object> createMappingsFromStringMap(Map<String, String> mappings) {
+        Map<String, Object> fieldMappings = new HashMap<>();
+        mappings.forEach((k, v) -> fieldMappings.put(k, Map.of("type", v)));
+
+        return fieldMappings;
     }
 }

+ 9 - 13
x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java

@@ -42,23 +42,19 @@ public class TransformIndexTests extends ESTestCase {
     private Clock clock = Clock.fixed(Instant.ofEpochMilli(CURRENT_TIME_MILLIS), ZoneId.systemDefault());
 
     public void testCreateDestinationIndex() throws IOException {
-        doAnswer(
-            invocationOnMock -> {
-                @SuppressWarnings("unchecked")
-                ActionListener<CreateIndexResponse> listener = (ActionListener<CreateIndexResponse>) invocationOnMock.getArguments()[2];
-                listener.onResponse(null);
-                return null;
-            })
-            .when(client).execute(any(), any(), any());
+        doAnswer(invocationOnMock -> {
+            @SuppressWarnings("unchecked")
+            ActionListener<CreateIndexResponse> listener = (ActionListener<CreateIndexResponse>) invocationOnMock.getArguments()[2];
+            listener.onResponse(null);
+            return null;
+        }).when(client).execute(any(), any(), any());
 
         TransformIndex.createDestinationIndex(
             client,
-            clock,
             TransformConfigTests.randomTransformConfig(TRANSFORM_ID),
-            new HashMap<>(),
-            ActionListener.wrap(
-                value -> assertTrue(value),
-                e -> fail(e.getMessage())));
+            TransformIndex.createTransformDestIndexSettings(new HashMap<>(), TRANSFORM_ID, clock),
+            ActionListener.wrap(value -> assertTrue(value), e -> fail(e.getMessage()))
+        );
 
         ArgumentCaptor<CreateIndexRequest> createIndexRequestCaptor = ArgumentCaptor.forClass(CreateIndexRequest.class);
         verify(client).execute(eq(CreateIndexAction.INSTANCE), createIndexRequestCaptor.capture(), any());