Browse Source

HLRC: ML Add preview datafeed api (#34284)

* HLRC: ML Add preview datafeed api

* Changing deprecation handling for parser

* Removing some duplication in docs, will address other APIs in another PR
Benjamin Trent 7 years ago
parent
commit
2dd058d607

+ 12 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java

@@ -45,6 +45,7 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.PostDataRequest;
+import org.elasticsearch.client.ml.PreviewDatafeedRequest;
 import org.elasticsearch.client.ml.PutCalendarRequest;
 import org.elasticsearch.client.ml.PutDatafeedRequest;
 import org.elasticsearch.client.ml.PutJobRequest;
@@ -259,6 +260,17 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request previewDatafeed(PreviewDatafeedRequest previewDatafeedRequest) {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("datafeeds")
+            .addPathPart(previewDatafeedRequest.getDatafeedId())
+            .addPathPartAsIs("_preview")
+            .build();
+        return new Request(HttpGet.METHOD_NAME, endpoint);
+    }
+
     static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) {
         String endpoint = new EndpointBuilder()
             .addPathPartAsIs("_xpack")

+ 45 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java

@@ -52,6 +52,8 @@ import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
+import org.elasticsearch.client.ml.PreviewDatafeedRequest;
+import org.elasticsearch.client.ml.PreviewDatafeedResponse;
 import org.elasticsearch.client.ml.PutCalendarRequest;
 import org.elasticsearch.client.ml.PutCalendarResponse;
 import org.elasticsearch.client.ml.PutDatafeedRequest;
@@ -649,6 +651,49 @@ public final class MachineLearningClient {
             Collections.emptySet());
     }
 
+    /**
+     * Previews the given Machine Learning Datafeed
+     * <p>
+     * For additional info
+     * see <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/ml-preview-datafeed.html">
+     *     ML Preview Datafeed documentation</a>
+     *
+     * @param request The request to preview the datafeed
+     * @param options  Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return {@link PreviewDatafeedResponse} object containing a {@link org.elasticsearch.common.bytes.BytesReference} of the data in
+     * JSON format
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public PreviewDatafeedResponse previewDatafeed(PreviewDatafeedRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::previewDatafeed,
+            options,
+            PreviewDatafeedResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Previews the given Machine Learning Datafeed asynchronously and notifies the listener on completion
+     * <p>
+     * For additional info
+     * see <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/ml-preview-datafeed.html">
+     *         ML Preview Datafeed documentation</a>
+     *
+     * @param request The request to preview the datafeed
+     * @param options  Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener Listener to be notified upon request completion
+     */
+    public void previewDatafeedAsync(PreviewDatafeedRequest request,
+                                     RequestOptions options,
+                                     ActionListener<PreviewDatafeedResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::previewDatafeed,
+            options,
+            PreviewDatafeedResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
+
     /**
      * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job}
      * <p>

+ 100 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PreviewDatafeedRequest.java

@@ -0,0 +1,100 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+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.Objects;
+
+/**
+ * Request to preview a MachineLearning Datafeed 
+ */
+public class PreviewDatafeedRequest extends ActionRequest implements ToXContentObject {
+
+    public static final ConstructingObjectParser<PreviewDatafeedRequest, Void> PARSER = new ConstructingObjectParser<>(
+        "open_datafeed_request", true, a -> new PreviewDatafeedRequest((String) a[0]));
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DatafeedConfig.ID);
+    }
+
+    public static PreviewDatafeedRequest fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    private final String datafeedId;
+
+    /**
+     * Create a new request with the desired datafeedId
+     *
+     * @param datafeedId unique datafeedId, must not be null
+     */
+    public PreviewDatafeedRequest(String datafeedId) {
+        this.datafeedId = Objects.requireNonNull(datafeedId, "[datafeed_id] must not be null");
+    }
+
+    public String getDatafeedId() {
+        return datafeedId;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(DatafeedConfig.ID.getPreferredName(), datafeedId);
+        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(datafeedId);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        PreviewDatafeedRequest that = (PreviewDatafeedRequest) other;
+        return Objects.equals(datafeedId, that.datafeedId);
+    }
+}

+ 113 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PreviewDatafeedResponse.java

@@ -0,0 +1,113 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.xcontent.DeprecationHandler;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+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 java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Response containing a datafeed preview in JSON format
+ */
+public class PreviewDatafeedResponse extends ActionResponse implements ToXContentObject {
+
+    private BytesReference preview;
+
+    public static PreviewDatafeedResponse fromXContent(XContentParser parser) throws IOException {
+        try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
+            parser.nextToken();
+            builder.copyCurrentStructure(parser);
+            return new PreviewDatafeedResponse(BytesReference.bytes(builder));
+        }
+    }
+
+    public PreviewDatafeedResponse(BytesReference preview) {
+        this.preview = preview;
+    }
+
+    public BytesReference getPreview() {
+        return preview;
+    }
+
+    /**
+     * Parses the preview to a list of {@link Map} objects
+     * @return List of previewed data
+     * @throws IOException If there is a parsing issue with the {@link BytesReference}
+     * @throws java.lang.ClassCastException If casting the raw {@link Object} entries to a {@link Map} fails
+     */
+    @SuppressWarnings("unchecked")
+    public List<Map<String, Object>> getDataList() throws IOException {
+        try(StreamInput streamInput = preview.streamInput();
+            XContentParser parser = XContentType.JSON.xContent()
+                .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, streamInput)) {
+            XContentParser.Token token = parser.nextToken();
+            if (token == XContentParser.Token.START_ARRAY) {
+                return parser.listOrderedMap().stream().map(obj -> (Map<String, Object>)obj).collect(Collectors.toList());
+            } else {
+                return Collections.singletonList(parser.mapOrdered());
+            }
+        }
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        try (InputStream stream = preview.streamInput()) {
+            builder.rawValue(stream, XContentType.JSON);
+        }
+        return builder;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(preview);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        PreviewDatafeedResponse other = (PreviewDatafeedResponse) obj;
+        return Objects.equals(preview, other.preview);
+    }
+
+    @Override
+    public final String toString() {
+        return Strings.toString(this);
+    }
+}

+ 8 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java

@@ -41,6 +41,7 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.PostDataRequest;
+import org.elasticsearch.client.ml.PreviewDatafeedRequest;
 import org.elasticsearch.client.ml.PutCalendarRequest;
 import org.elasticsearch.client.ml.PutDatafeedRequest;
 import org.elasticsearch.client.ml.PutJobRequest;
@@ -293,6 +294,13 @@ public class MLRequestConvertersTests extends ESTestCase {
         }
     }
 
+    public void testPreviewDatafeed() {
+        PreviewDatafeedRequest datafeedRequest = new PreviewDatafeedRequest("datafeed_1");
+        Request request = MLRequestConverters.previewDatafeed(datafeedRequest);
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/datafeeds/" + datafeedRequest.getDatafeedId() + "/_preview", request.getEndpoint());
+    }
+
     public void testDeleteForecast() {
         String jobId = randomAlphaOfLength(10);
         DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId);

+ 55 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java

@@ -49,6 +49,8 @@ import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
+import org.elasticsearch.client.ml.PreviewDatafeedRequest;
+import org.elasticsearch.client.ml.PreviewDatafeedResponse;
 import org.elasticsearch.client.ml.PutCalendarRequest;
 import org.elasticsearch.client.ml.PutCalendarResponse;
 import org.elasticsearch.client.ml.PutDatafeedRequest;
@@ -76,8 +78,11 @@ import org.elasticsearch.rest.RestStatus;
 import org.junit.After;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -564,6 +569,56 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testPreviewDatafeed() throws Exception {
+        String jobId = "test-preview-datafeed";
+        String indexName = "preview_data_1";
+
+        // Set up the index and docs
+        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
+        createIndexRequest.mapping("doc", "timestamp", "type=date", "total", "type=long");
+        highLevelClient().indices().create(createIndexRequest, RequestOptions.DEFAULT);
+        BulkRequest bulk = new BulkRequest();
+        bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+        long now = (System.currentTimeMillis()/1000)*1000;
+        long thePast = now - 60000;
+        int i = 0;
+        List<Integer> totalTotals = new ArrayList<>(60);
+        while(thePast < now) {
+            Integer total = randomInt(1000);
+            IndexRequest doc = new IndexRequest();
+            doc.index(indexName);
+            doc.type("doc");
+            doc.id("id" + i);
+            doc.source("{\"total\":" + total + ",\"timestamp\":"+ thePast +"}", XContentType.JSON);
+            bulk.add(doc);
+            thePast += 1000;
+            i++;
+            totalTotals.add(total);
+        }
+        highLevelClient().bulk(bulk, RequestOptions.DEFAULT);
+
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+        // create the job and the datafeed
+        Job job = buildJob(jobId);
+        putJob(job);
+        openJob(job);
+
+        String datafeedId = jobId + "-feed";
+        DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId)
+            .setIndices(indexName)
+            .setQueryDelay(TimeValue.timeValueSeconds(1))
+            .setTypes(Collections.singletonList("doc"))
+            .setFrequency(TimeValue.timeValueSeconds(1)).build();
+        machineLearningClient.putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT);
+
+        PreviewDatafeedResponse response = execute(new PreviewDatafeedRequest(datafeedId),
+            machineLearningClient::previewDatafeed,
+            machineLearningClient::previewDatafeedAsync);
+
+        Integer[] totals = response.getDataList().stream().map(map -> (Integer)map.get("total")).toArray(Integer[]::new);
+        assertThat(totalTotals, containsInAnyOrder(totals));
+    }
+
     public void testDeleteForecast() throws Exception {
         String jobId = "test-delete-forecast";
 

+ 63 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java

@@ -65,6 +65,8 @@ import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
+import org.elasticsearch.client.ml.PreviewDatafeedRequest;
+import org.elasticsearch.client.ml.PreviewDatafeedResponse;
 import org.elasticsearch.client.ml.PutCalendarRequest;
 import org.elasticsearch.client.ml.PutCalendarResponse;
 import org.elasticsearch.client.ml.PutDatafeedRequest;
@@ -97,6 +99,7 @@ import org.elasticsearch.client.ml.job.results.Influencer;
 import org.elasticsearch.client.ml.job.results.OverallBucket;
 import org.elasticsearch.client.ml.job.stats.JobStats;
 import org.elasticsearch.client.ml.job.util.PageParams;
+import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.query.QueryBuilders;
@@ -708,6 +711,66 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testPreviewDatafeed() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        Job job = MachineLearningIT.buildJob("preview-datafeed-job");
+        client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT);
+        String datafeedId = job.getId() + "-feed";
+        String indexName = "preview_data_2";
+        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
+        createIndexRequest.mapping("doc", "timestamp", "type=date", "total", "type=long");
+        highLevelClient().indices().create(createIndexRequest, RequestOptions.DEFAULT);
+        DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, job.getId())
+            .setTypes(Arrays.asList("doc"))
+            .setIndices(indexName)
+            .build();
+        client.machineLearning().putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT);
+        {
+            //tag::preview-datafeed-request
+            PreviewDatafeedRequest request = new PreviewDatafeedRequest(datafeedId); // <1>
+            //end::preview-datafeed-request
+
+            //tag::preview-datafeed-execute
+            PreviewDatafeedResponse response = client.machineLearning().previewDatafeed(request, RequestOptions.DEFAULT);
+            //end::preview-datafeed-execute
+
+            //tag::preview-datafeed-response
+            BytesReference rawPreview = response.getPreview(); // <1>
+            List<Map<String, Object>> semiParsedPreview = response.getDataList(); // <2>
+            //end::preview-datafeed-response
+
+            assertTrue(semiParsedPreview.isEmpty());
+        }
+        {
+            PreviewDatafeedRequest request = new PreviewDatafeedRequest(datafeedId);
+
+            // tag::preview-datafeed-execute-listener
+            ActionListener<PreviewDatafeedResponse> listener = new ActionListener<PreviewDatafeedResponse>() {
+                @Override
+                public void onResponse(PreviewDatafeedResponse response) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::preview-datafeed-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::preview-datafeed-execute-async
+            client.machineLearning().previewDatafeedAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::preview-datafeed-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testStartDatafeed() throws Exception {
         RestHighLevelClient client = highLevelClient();
 

+ 43 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PreviewDatafeedRequestTests.java

@@ -0,0 +1,43 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+
+public class PreviewDatafeedRequestTests extends AbstractXContentTestCase<PreviewDatafeedRequest> {
+
+    @Override
+    protected PreviewDatafeedRequest createTestInstance() {
+        return new PreviewDatafeedRequest(DatafeedConfigTests.randomValidDatafeedId());
+    }
+
+    @Override
+    protected PreviewDatafeedRequest doParseInstance(XContentParser parser) throws IOException {
+        return PreviewDatafeedRequest.fromXContent(parser);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}

+ 99 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PreviewDatafeedResponseTests.java

@@ -0,0 +1,99 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
+import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.stream.Collectors;
+
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+public class PreviewDatafeedResponseTests extends ESTestCase {
+
+    protected PreviewDatafeedResponse createTestInstance() throws IOException {
+        //This is just to create a random object to stand in the place of random data
+        DatafeedConfig datafeedConfig = DatafeedConfigTests.createRandom();
+        BytesReference bytes = XContentHelper.toXContent(datafeedConfig, XContentType.JSON, false);
+        return new PreviewDatafeedResponse(bytes);
+    }
+
+    public void testGetDataList() throws IOException {
+        String rawData = "[\n" +
+            "  {\n" +
+            "    \"time\": 1454803200000,\n" +
+            "    \"airline\": \"JZA\",\n" +
+            "    \"doc_count\": 5,\n" +
+            "    \"responsetime\": 990.4628295898438\n" +
+            "  },\n" +
+            "  {\n" +
+            "    \"time\": 1454803200000,\n" +
+            "    \"airline\": \"JBU\",\n" +
+            "    \"doc_count\": 23,\n" +
+            "    \"responsetime\": 877.5927124023438\n" +
+            "  },\n" +
+            "  {\n" +
+            "    \"time\": 1454803200000,\n" +
+            "    \"airline\": \"KLM\",\n" +
+            "    \"doc_count\": 42,\n" +
+            "    \"responsetime\": 1355.481201171875\n" +
+            "  }\n" +
+            "]";
+        BytesReference bytes = new BytesArray(rawData);
+        PreviewDatafeedResponse response = new PreviewDatafeedResponse(bytes);
+        assertThat(response.getDataList()
+            .stream()
+            .map(map -> (String)map.get("airline"))
+            .collect(Collectors.toList()), containsInAnyOrder("JZA", "JBU", "KLM"));
+
+        rawData = "{\"key\":\"my_value\"}";
+        bytes = new BytesArray(rawData);
+        response = new PreviewDatafeedResponse(bytes);
+        assertThat(response.getDataList()
+            .stream()
+            .map(map -> (String)map.get("key"))
+            .collect(Collectors.toList()), containsInAnyOrder("my_value"));
+
+    }
+
+    //Because this is raw a BytesReference, the shuffling done via `AbstractXContentTestCase` is unacceptable and causes equality failures
+    public void testSerializationDeserialization() throws IOException {
+        for (int runs = 0; runs < 20; runs++) {
+            XContentType xContentType = XContentType.JSON;
+            PreviewDatafeedResponse testInstance = createTestInstance();
+            BytesReference originalXContent = XContentHelper.toXContent(testInstance, xContentType, false);
+            XContentParser parser = this.createParser(xContentType.xContent(), originalXContent);
+            PreviewDatafeedResponse parsed = PreviewDatafeedResponse.fromXContent(parser);
+            assertEquals(testInstance, parsed);
+            assertToXContentEquivalent(
+                XContentHelper.toXContent(testInstance, xContentType, false),
+                XContentHelper.toXContent(parsed, xContentType, false),
+                xContentType);
+        }
+    }
+
+}

+ 34 - 0
docs/java-rest/high-level/ml/preview-datafeed.asciidoc

@@ -0,0 +1,34 @@
+--
+:api: preview-datafeed
+:request: PreviewDatafeedRequest
+:response: PreviewDatafeedResponse
+--
+[id="{upid}-{api}"]
+=== Preview Datafeed API
+
+The Preview Datafeed API provides the ability to preview a {ml} datafeed's data
+in the cluster. It accepts a +{request}+ object and responds
+with a +{response}+ object.
+
+[id="{upid}-{api}-request"]
+==== Preview Datafeed Request
+
+A +{request}+ object is created referencing a non-null `datafeedId`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+<1> Constructing a new request referencing an existing `datafeedId`
+
+[id="{upid}-{api}-response"]
+==== Preview Datafeed Response
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> The raw +BytesReference+ of the data preview
+<2> A +List<Map<String,Object>>+ that represents the previewed data
+
+include::../execution.asciidoc[]

+ 28 - 24
docs/java-rest/high-level/supported-apis.asciidoc

@@ -219,33 +219,36 @@ include::licensing/get-license.asciidoc[]
 include::licensing/delete-license.asciidoc[]
 
 == Machine Learning APIs
+:upid: {mainid}-x-pack-ml
+:doc-tests-file: {doc-tests}/MlClientDocumentationIT.java
 
 The Java High Level REST Client supports the following Machine Learning APIs:
 
-* <<java-rest-high-x-pack-ml-put-job>>
-* <<java-rest-high-x-pack-ml-get-job>>
-* <<java-rest-high-x-pack-ml-delete-job>>
-* <<java-rest-high-x-pack-ml-open-job>>
-* <<java-rest-high-x-pack-ml-close-job>>
-* <<java-rest-high-x-pack-ml-flush-job>>
-* <<java-rest-high-x-pack-ml-update-job>>
-* <<java-rest-high-x-pack-ml-get-job-stats>>
-* <<java-rest-high-x-pack-ml-put-datafeed>>
-* <<java-rest-high-x-pack-ml-get-datafeed>>
-* <<java-rest-high-x-pack-ml-delete-datafeed>>
-* <<java-rest-high-x-pack-ml-start-datafeed>>
-* <<java-rest-high-x-pack-ml-stop-datafeed>>
-* <<java-rest-high-x-pack-ml-forecast-job>>
-* <<java-rest-high-x-pack-ml-delete-forecast>>
-* <<java-rest-high-x-pack-ml-get-buckets>>
-* <<java-rest-high-x-pack-ml-get-overall-buckets>>
-* <<java-rest-high-x-pack-ml-get-records>>
-* <<java-rest-high-x-pack-ml-post-data>>
-* <<java-rest-high-x-pack-ml-get-influencers>>
-* <<java-rest-high-x-pack-ml-get-categories>>
-* <<java-rest-high-x-pack-ml-get-calendars>>
-* <<java-rest-high-x-pack-ml-put-calendar>>
-* <<java-rest-high-x-pack-ml-delete-calendar>>
+* <<{upid}-put-job>>
+* <<{upid}-get-job>>
+* <<{upid}-delete-job>>
+* <<{upid}-open-job>>
+* <<{upid}-close-job>>
+* <<{upid}-flush-job>>
+* <<{upid}-update-job>>
+* <<{upid}-get-job-stats>>
+* <<{upid}-put-datafeed>>
+* <<{upid}-get-datafeed>>
+* <<{upid}-delete-datafeed>>
+* <<{upid}-preview-datafeed>>
+* <<{upid}-start-datafeed>>
+* <<{upid}-stop-datafeed>>
+* <<{upid}-forecast-job>>
+* <<{upid}-delete-forecast>>
+* <<{upid}-get-buckets>>
+* <<{upid}-get-overall-buckets>>
+* <<{upid}-get-records>>
+* <<{upid}-post-data>>
+* <<{upid}-get-influencers>>
+* <<{upid}-get-categories>>
+* <<{upid}-get-calendars>>
+* <<{upid}-put-calendar>>
+* <<{upid}-delete-calendar>>
 
 include::ml/put-job.asciidoc[]
 include::ml/get-job.asciidoc[]
@@ -257,6 +260,7 @@ include::ml/flush-job.asciidoc[]
 include::ml/put-datafeed.asciidoc[]
 include::ml/get-datafeed.asciidoc[]
 include::ml/delete-datafeed.asciidoc[]
+include::ml/preview-datafeed.asciidoc[]
 include::ml/start-datafeed.asciidoc[]
 include::ml/stop-datafeed.asciidoc[]
 include::ml/get-job-stats.asciidoc[]