Browse Source

[HLRC][ML] Add ML get model snapshots API (#35487)

Relates #29827
Ed Savage 7 years ago
parent
commit
e7b7d52a6a

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

@@ -43,6 +43,7 @@ import org.elasticsearch.client.ml.GetFiltersRequest;
 import org.elasticsearch.client.ml.GetInfluencersRequest;
 import org.elasticsearch.client.ml.GetJobRequest;
 import org.elasticsearch.client.ml.GetJobStatsRequest;
+import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
@@ -361,6 +362,19 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request getModelSnapshots(GetModelSnapshotsRequest getModelSnapshotsRequest) throws IOException {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("anomaly_detectors")
+            .addPathPart(getModelSnapshotsRequest.getJobId())
+            .addPathPartAsIs("model_snapshots")
+            .build();
+        Request request = new Request(HttpGet.METHOD_NAME, endpoint);
+        request.setEntity(createEntity(getModelSnapshotsRequest, REQUEST_BODY_CONTENT_TYPE));
+        return request;
+    }
+
     static Request getOverallBuckets(GetOverallBucketsRequest getOverallBucketsRequest) throws IOException {
         String endpoint = new EndpointBuilder()
                 .addPathPartAsIs("_xpack")

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

@@ -49,6 +49,8 @@ import org.elasticsearch.client.ml.GetJobRequest;
 import org.elasticsearch.client.ml.GetJobResponse;
 import org.elasticsearch.client.ml.GetJobStatsRequest;
 import org.elasticsearch.client.ml.GetJobStatsResponse;
+import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
+import org.elasticsearch.client.ml.GetModelSnapshotsResponse;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetOverallBucketsResponse;
 import org.elasticsearch.client.ml.GetRecordsRequest;
@@ -897,6 +899,46 @@ public final class MachineLearningClient {
                 Collections.emptySet());
     }
 
+    /**
+     * Gets the snapshots for a Machine Learning Job.
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-snapshot.html">
+     * ML GET model snapshots documentation</a>
+     *
+     * @param request The request
+     * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public GetModelSnapshotsResponse getModelSnapshots(GetModelSnapshotsRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::getModelSnapshots,
+            options,
+            GetModelSnapshotsResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Gets the snapshots for a Machine Learning Job, notifies listener once the requested snapshots are retrieved.
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-snapshot.html">
+     * ML GET model snapshots documentation</a>
+     *
+     * @param request  The request
+     * @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 getModelSnapshotsAsync(GetModelSnapshotsRequest request, RequestOptions options,
+                                       ActionListener<GetModelSnapshotsResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::getModelSnapshots,
+            options,
+            GetModelSnapshotsResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
+
     /**
      * Gets overall buckets for a set of Machine Learning Jobs.
      * <p>

+ 208 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetModelSnapshotsRequest.java

@@ -0,0 +1,208 @@
+/*
+ * 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.job.config.Job;
+import org.elasticsearch.client.ml.job.util.PageParams;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * A request to retrieve information about model snapshots for a given job
+ */
+public class GetModelSnapshotsRequest extends ActionRequest implements ToXContentObject {
+
+
+    public static final ParseField SNAPSHOT_ID = new ParseField("snapshot_id");
+    public static final ParseField SORT = new ParseField("sort");
+    public static final ParseField START = new ParseField("start");
+    public static final ParseField END = new ParseField("end");
+    public static final ParseField DESC = new ParseField("desc");
+
+    public static final ConstructingObjectParser<GetModelSnapshotsRequest, Void> PARSER = new ConstructingObjectParser<>(
+        "get_model_snapshots_request", a -> new GetModelSnapshotsRequest((String) a[0]));
+
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID);
+        PARSER.declareString(GetModelSnapshotsRequest::setSnapshotId, SNAPSHOT_ID);
+        PARSER.declareString(GetModelSnapshotsRequest::setSort, SORT);
+        PARSER.declareStringOrNull(GetModelSnapshotsRequest::setStart, START);
+        PARSER.declareStringOrNull(GetModelSnapshotsRequest::setEnd, END);
+        PARSER.declareBoolean(GetModelSnapshotsRequest::setDesc, DESC);
+        PARSER.declareObject(GetModelSnapshotsRequest::setPageParams, PageParams.PARSER, PageParams.PAGE);
+    }
+
+    private final String jobId;
+    private String snapshotId;
+    private String sort;
+    private String start;
+    private String end;
+    private Boolean desc;
+    private PageParams pageParams;
+
+    /**
+     * Constructs a request to retrieve snapshot information from a given job
+     * @param jobId id of the job from which to retrieve results
+     */
+    public GetModelSnapshotsRequest(String jobId) {
+        this.jobId = Objects.requireNonNull(jobId);
+    }
+
+    public String getJobId() {
+        return jobId;
+    }
+
+    public String getSnapshotId() {
+        return snapshotId;
+    }
+
+    /**
+     * Sets the id of the snapshot to retrieve.
+     * @param snapshotId the snapshot id
+     */
+    public void setSnapshotId(String snapshotId) {
+        this.snapshotId = snapshotId;
+    }
+
+    public String getSort() {
+        return sort;
+    }
+
+    /**
+     * Sets the value of "sort".
+     * Specifies the snapshot field to sort on.
+     * @param sort value of "sort".
+     */
+    public void setSort(String sort) {
+        this.sort = sort;
+    }
+
+    public PageParams getPageParams() {
+        return pageParams;
+    }
+
+    /**
+     * Sets the paging parameters
+     * @param pageParams the paging parameters
+     */
+    public void setPageParams(PageParams pageParams) {
+        this.pageParams = pageParams;
+    }
+
+    public String getStart() {
+        return start;
+    }
+
+    /**
+     * Sets the value of "start" which is a timestamp.
+     * Only snapshots whose timestamp is on or after the "start" value will be returned.
+     * @param start String representation of a timestamp; may be an epoch seconds, epoch millis or an ISO string
+     */
+    public void setStart(String start) {
+        this.start = start;
+    }
+
+
+    public String getEnd() {
+        return end;
+    }
+
+    /**
+     * Sets the value of "end" which is a timestamp.
+     * Only snapshots whose timestamp is before the "end" value will be returned.
+     * @param end String representation of a timestamp; may be an epoch seconds, epoch millis or an ISO string
+     */
+    public void setEnd(String end) {
+        this.end = end;
+    }
+
+    public Boolean getDesc() {
+        return desc;
+    }
+
+    /**
+     * Sets the value of "desc".
+     * Specifies the sorting order.
+     * @param desc value of "desc"
+     */
+    public void setDesc(boolean desc) {
+        this.desc = desc;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(Job.ID.getPreferredName(), jobId);
+        if (snapshotId != null) {
+            builder.field(SNAPSHOT_ID.getPreferredName(), snapshotId);
+        }
+        if (sort != null) {
+            builder.field(SORT.getPreferredName(), sort);
+        }
+        if (start != null) {
+            builder.field(START.getPreferredName(), start);
+        }
+        if (end != null) {
+            builder.field(END.getPreferredName(), end);
+        }
+        if (desc != null) {
+            builder.field(DESC.getPreferredName(), desc);
+        }
+        if (pageParams != null) {
+            builder.field(PageParams.PAGE.getPreferredName(), pageParams);
+        }        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        GetModelSnapshotsRequest request = (GetModelSnapshotsRequest) obj;
+        return Objects.equals(jobId, request.jobId)
+            && Objects.equals(snapshotId, request.snapshotId)
+            && Objects.equals(sort, request.sort)
+            && Objects.equals(start, request.start)
+            && Objects.equals(end, request.end)
+            && Objects.equals(desc, request.desc)
+            && Objects.equals(pageParams, request.pageParams);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(jobId, snapshotId, pageParams, start, end, sort, desc);
+    }
+}

+ 80 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetModelSnapshotsResponse.java

@@ -0,0 +1,80 @@
+/*
+ * 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.job.process.ModelSnapshot;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * A response containing the requested snapshots
+ */
+public class GetModelSnapshotsResponse extends AbstractResultResponse<ModelSnapshot> {
+
+    public static final ParseField SNAPSHOTS = new ParseField("model_snapshots");
+
+    @SuppressWarnings("unchecked")
+    public static final ConstructingObjectParser<GetModelSnapshotsResponse, Void> PARSER =
+            new ConstructingObjectParser<>("get_model_snapshots_response", true,
+                    a -> new GetModelSnapshotsResponse((List<ModelSnapshot.Builder>) a[0], (long) a[1]));
+
+    static {
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), ModelSnapshot.PARSER, SNAPSHOTS);
+        PARSER.declareLong(ConstructingObjectParser.constructorArg(), COUNT);
+    }
+
+    public static GetModelSnapshotsResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    GetModelSnapshotsResponse(List<ModelSnapshot.Builder> snapshotBuilders, long count) {
+        super(SNAPSHOTS, snapshotBuilders.stream().map(ModelSnapshot.Builder::build).collect(Collectors.toList()), count);
+    }
+
+    /**
+     * The retrieved snapshots
+     * @return the retrieved snapshots
+     */
+    public List<ModelSnapshot> snapshots() {
+        return results;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(count, results);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        GetModelSnapshotsResponse other = (GetModelSnapshotsResponse) obj;
+        return count == other.count && Objects.equals(results, other.results);
+    }
+}

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

@@ -39,6 +39,7 @@ import org.elasticsearch.client.ml.GetFiltersRequest;
 import org.elasticsearch.client.ml.GetInfluencersRequest;
 import org.elasticsearch.client.ml.GetJobRequest;
 import org.elasticsearch.client.ml.GetJobStatsRequest;
+import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
@@ -391,6 +392,21 @@ public class MLRequestConvertersTests extends ESTestCase {
         }
     }
 
+    public void testGetModelSnapshots() throws IOException {
+        String jobId = randomAlphaOfLength(10);
+        GetModelSnapshotsRequest getModelSnapshotsRequest = new GetModelSnapshotsRequest(jobId);
+        getModelSnapshotsRequest.setPageParams(new PageParams(100, 300));
+
+
+        Request request = MLRequestConverters.getModelSnapshots(getModelSnapshotsRequest);
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/model_snapshots", request.getEndpoint());
+        try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) {
+            GetModelSnapshotsRequest parsedRequest = GetModelSnapshotsRequest.PARSER.apply(parser, null);
+            assertThat(parsedRequest, equalTo(getModelSnapshotsRequest));
+        }
+    }
+
     public void testGetOverallBuckets() throws IOException {
         String jobId = randomAlphaOfLength(10);
         GetOverallBucketsRequest getOverallBucketsRequest = new GetOverallBucketsRequest(jobId);

+ 377 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java

@@ -25,6 +25,8 @@ import org.elasticsearch.client.ml.GetBucketsRequest;
 import org.elasticsearch.client.ml.GetBucketsResponse;
 import org.elasticsearch.client.ml.GetCategoriesRequest;
 import org.elasticsearch.client.ml.GetCategoriesResponse;
+import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
+import org.elasticsearch.client.ml.GetModelSnapshotsResponse;
 import org.elasticsearch.client.ml.GetInfluencersRequest;
 import org.elasticsearch.client.ml.GetInfluencersResponse;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
@@ -36,6 +38,7 @@ import org.elasticsearch.client.ml.job.config.AnalysisConfig;
 import org.elasticsearch.client.ml.job.config.DataDescription;
 import org.elasticsearch.client.ml.job.config.Detector;
 import org.elasticsearch.client.ml.job.config.Job;
+import org.elasticsearch.client.ml.job.process.ModelSizeStats;
 import org.elasticsearch.client.ml.job.results.AnomalyRecord;
 import org.elasticsearch.client.ml.job.results.Bucket;
 import org.elasticsearch.client.ml.job.results.Influencer;
@@ -48,6 +51,7 @@ import org.junit.Before;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
 
 import static org.hamcrest.Matchers.closeTo;
@@ -145,11 +149,384 @@ public class MachineLearningGetResultsIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    private void addModelSnapshotIndexRequests(BulkRequest bulkRequest) {
+        {
+            IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC);
+            indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"timestamp\":1541587919000, " +
+                "\"description\":\"State persisted due to job close at 2018-11-07T10:51:59+0000\", \"snapshot_id\":\"1541587919\"," +
+                "\"snapshot_doc_count\":1, \"model_size_stats\":{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"model_size_stats\"," +
+                "\"model_bytes\":51722, \"total_by_field_count\":3, \"total_over_field_count\":0, \"total_partition_field_count\":2," +
+                "\"bucket_allocation_failures_count\":0, \"memory_status\":\"ok\", \"log_time\":1541587919000," +
+                " \"timestamp\":1519930800000},\"latest_record_time_stamp\":1519931700000, \"latest_result_time_stamp\":1519930800000," +
+                " \"retain\":false }", XContentType.JSON);
+            bulkRequest.add(indexRequest);
+        }
+        {
+            IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC);
+            indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"timestamp\":1541588919000, " +
+                "\"description\":\"State persisted due to job close at 2018-11-07T11:08:39+0000\", \"snapshot_id\":\"1541588919\"," +
+                "\"snapshot_doc_count\":1, \"model_size_stats\":{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"model_size_stats\"," +
+                "\"model_bytes\":51722, \"total_by_field_count\":3, \"total_over_field_count\":0, \"total_partition_field_count\":2," +
+                "\"bucket_allocation_failures_count\":0, \"memory_status\":\"ok\", \"log_time\":1541588919000," +
+                "\"timestamp\":1519930800000},\"latest_record_time_stamp\":1519931700000, \"latest_result_time_stamp\":1519930800000, " +
+                "\"retain\":false }", XContentType.JSON);
+            bulkRequest.add(indexRequest);
+        }
+        {
+            IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC);
+            indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"timestamp\":1541589919000, " +
+                "\"description\":\"State persisted due to job close at 2018-11-07T11:25:19+0000\", \"snapshot_id\":\"1541589919\"," +
+                "\"snapshot_doc_count\":1, \"model_size_stats\":{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"model_size_stats\"," +
+                "\"model_bytes\":51722, \"total_by_field_count\":3, \"total_over_field_count\":0, \"total_partition_field_count\":2," +
+                "\"bucket_allocation_failures_count\":0, \"memory_status\":\"ok\", \"log_time\":1541589919000," +
+                "\"timestamp\":1519930800000},\"latest_record_time_stamp\":1519931700000, \"latest_result_time_stamp\":1519930800000," +
+                "\"retain\":false }", XContentType.JSON);
+            bulkRequest.add(indexRequest);
+        }
+    }
+
     @After
     public void deleteJob() throws IOException {
         new MlTestStateCleaner(logger, highLevelClient().machineLearning()).clearMlMetadata();
     }
 
+    public void testGetModelSnapshots() throws IOException {
+
+        // index some model_snapshot results
+        BulkRequest bulkRequest = new BulkRequest();
+        bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+
+        addModelSnapshotIndexRequests(bulkRequest);
+
+        highLevelClient().bulk(bulkRequest, RequestOptions.DEFAULT);
+
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(false);
+            request.setPageParams(new PageParams(0, 10000));
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(3L));
+            assertThat(response.snapshots().size(), equalTo(3));
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541587919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T10:51:59+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541587919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+            assertThat(response.snapshots().get(1).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getSnapshotId(), equalTo("1541588919"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:08:39+0000"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getTimestamp(), equalTo(new Date(1541588919000L)));
+            assertThat(response.snapshots().get(1).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(1).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+            assertThat(response.snapshots().get(2).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(2).getSnapshotId(), equalTo("1541589919"));
+            assertThat(response.snapshots().get(2).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(2).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:25:19+0000"));
+            assertThat(response.snapshots().get(2).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(2).getTimestamp(), equalTo(new Date(1541589919000L)));
+            assertThat(response.snapshots().get(2).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(2).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(true);
+            request.setPageParams(new PageParams(0, 10000));
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(3L));
+            assertThat(response.snapshots().size(), equalTo(3));
+            assertThat(response.snapshots().get(2).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(2).getSnapshotId(), equalTo("1541587919"));
+            assertThat(response.snapshots().get(2).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(2).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T10:51:59+0000"));
+            assertThat(response.snapshots().get(2).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(2).getTimestamp(), equalTo(new Date(1541587919000L)));
+            assertThat(response.snapshots().get(2).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(2).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(2).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+            assertThat(response.snapshots().get(1).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getSnapshotId(), equalTo("1541588919"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:08:39+0000"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getTimestamp(), equalTo(new Date(1541588919000L)));
+            assertThat(response.snapshots().get(1).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(1).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541589919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:25:19+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541589919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(false);
+            request.setPageParams(new PageParams(0, 1));
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(3L));
+            assertThat(response.snapshots().size(), equalTo(1));
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541587919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T10:51:59+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541587919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(false);
+            request.setPageParams(new PageParams(1, 2));
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(3L));
+            assertThat(response.snapshots().size(), equalTo(2));
+
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541588919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:08:39+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541588919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+
+            assertThat(response.snapshots().get(1).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getSnapshotId(), equalTo("1541589919"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:25:19+0000"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getTimestamp(), equalTo(new Date(1541589919000L)));
+            assertThat(response.snapshots().get(1).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(1).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSnapshotId("1541588919");
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(1L));
+            assertThat(response.snapshots().size(), equalTo(1));
+
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541588919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:08:39+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541588919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSnapshotId("1541586919"); // request a non-existent snapshotId
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(0L));
+            assertThat(response.snapshots().size(), equalTo(0));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(false);
+            request.setStart("1541586919000");
+            request.setEnd("1541589019000");
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(2L));
+            assertThat(response.snapshots().size(), equalTo(2));
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541587919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T10:51:59+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541587919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+
+            assertThat(response.snapshots().get(1).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getSnapshotId(), equalTo("1541588919"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:08:39+0000"));
+            assertThat(response.snapshots().get(1).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(1).getTimestamp(), equalTo(new Date(1541588919000L)));
+            assertThat(response.snapshots().get(1).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(1).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(1).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(JOB_ID);
+            request.setSort("timestamp");
+            request.setDesc(false);
+            request.setStart("1541589019000");
+
+            GetModelSnapshotsResponse response = execute(request, machineLearningClient::getModelSnapshots,
+                machineLearningClient::getModelSnapshotsAsync);
+
+            assertThat(response.count(), equalTo(1L));
+            assertThat(response.snapshots().size(), equalTo(1));
+            assertThat(response.snapshots().get(0).getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getSnapshotId(), equalTo("1541589919"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getDescription(), equalTo("State persisted due to job close at" +
+                " 2018-11-07T11:25:19+0000"));
+            assertThat(response.snapshots().get(0).getSnapshotDocCount(), equalTo(1));
+            assertThat(response.snapshots().get(0).getTimestamp(), equalTo(new Date(1541589919000L)));
+            assertThat(response.snapshots().get(0).getLatestRecordTimeStamp(), equalTo(new Date(1519931700000L)));
+            assertThat(response.snapshots().get(0).getLatestResultTimeStamp(), equalTo(new Date(1519930800000L)));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getJobId(), equalTo(JOB_ID));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getModelBytes(), equalTo(51722L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalByFieldCount(), equalTo(3L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalOverFieldCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getTotalPartitionFieldCount(), equalTo(2L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getBucketAllocationFailuresCount(), equalTo(0L));
+            assertThat(response.snapshots().get(0).getModelSizeStats().getMemoryStatus(),
+                equalTo(ModelSizeStats.MemoryStatus.fromString("ok")));
+        }
+    }
+
     public void testGetCategories() throws IOException {
 
         // index some category results

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

@@ -50,6 +50,8 @@ import org.elasticsearch.client.ml.GetCalendarsRequest;
 import org.elasticsearch.client.ml.GetCalendarsResponse;
 import org.elasticsearch.client.ml.GetCategoriesRequest;
 import org.elasticsearch.client.ml.GetCategoriesResponse;
+import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
+import org.elasticsearch.client.ml.GetModelSnapshotsResponse;
 import org.elasticsearch.client.ml.GetDatafeedRequest;
 import org.elasticsearch.client.ml.GetDatafeedResponse;
 import org.elasticsearch.client.ml.GetDatafeedStatsRequest;
@@ -103,6 +105,7 @@ import org.elasticsearch.client.ml.job.config.ModelPlotConfig;
 import org.elasticsearch.client.ml.job.config.Operator;
 import org.elasticsearch.client.ml.job.config.RuleCondition;
 import org.elasticsearch.client.ml.job.process.DataCounts;
+import org.elasticsearch.client.ml.job.process.ModelSnapshot;
 import org.elasticsearch.client.ml.job.results.AnomalyRecord;
 import org.elasticsearch.client.ml.job.results.Bucket;
 import org.elasticsearch.client.ml.job.results.CategoryDefinition;
@@ -1863,6 +1866,102 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testGetModelSnapshots() throws IOException, InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+
+        String jobId = "test-get-model-snapshots";
+        Job job = MachineLearningIT.buildJob(jobId);
+        client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT);
+
+        // Let us index a snapshot
+        IndexRequest indexRequest = new IndexRequest(".ml-anomalies-shared", "doc");
+        indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+        indexRequest.source("{\"job_id\":\"test-get-model-snapshots\", \"timestamp\":1541587919000, " +
+            "\"description\":\"State persisted due to job close at 2018-11-07T10:51:59+0000\", " +
+            "\"snapshot_id\":\"1541587919\", \"snapshot_doc_count\":1, \"model_size_stats\":{" +
+            "\"job_id\":\"test-get-model-snapshots\", \"result_type\":\"model_size_stats\",\"model_bytes\":51722, " +
+            "\"total_by_field_count\":3, \"total_over_field_count\":0, \"total_partition_field_count\":2," +
+            "\"bucket_allocation_failures_count\":0, \"memory_status\":\"ok\", \"log_time\":1541587919000, " +
+            "\"timestamp\":1519930800000}, \"latest_record_time_stamp\":1519931700000," +
+            "\"latest_result_time_stamp\":1519930800000, \"retain\":false}", XContentType.JSON);
+        client.index(indexRequest, RequestOptions.DEFAULT);
+
+        {
+            // tag::get-model-snapshots-request
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(jobId); // <1>
+            // end::get-model-snapshots-request
+
+            // tag::get-model-snapshots-snapshot-id
+            request.setSnapshotId("1541587919"); // <1>
+            // end::get-model-snapshots-snapshot-id
+
+            // Set snapshot id to null as it is incompatible with other args
+            request.setSnapshotId(null);
+
+            // tag::get-model-snapshots-desc
+            request.setDesc(true); // <1>
+            // end::get-model-snapshots-desc
+
+            // tag::get-model-snapshots-end
+            request.setEnd("2018-11-07T21:00:00Z"); // <1>
+            // end::get-model-snapshots-end
+
+            // tag::get-model-snapshots-page
+            request.setPageParams(new PageParams(100, 200)); // <1>
+            // end::get-model-snapshots-page
+
+            // Set page params back to null so the response contains the snapshot we indexed
+            request.setPageParams(null);
+
+            // tag::get-model-snapshots-sort
+            request.setSort("latest_result_time_stamp"); // <1>
+            // end::get-model-snapshots-sort
+
+            // tag::get-model-snapshots-start
+            request.setStart("2018-11-07T00:00:00Z"); // <1>
+            // end::get-model-snapshots-start
+
+            // tag::get-model-snapshots-execute
+            GetModelSnapshotsResponse response = client.machineLearning().getModelSnapshots(request, RequestOptions.DEFAULT);
+            // end::get-model-snapshots-execute
+
+            // tag::get-model-snapshots-response
+            long count = response.count(); // <1>
+            List<ModelSnapshot> modelSnapshots = response.snapshots(); // <2>
+            // end::get-model-snapshots-response
+
+            assertEquals(1, modelSnapshots.size());
+        }
+        {
+            GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(jobId);
+
+            // tag::get-model-snapshots-execute-listener
+            ActionListener<GetModelSnapshotsResponse> listener =
+                new ActionListener<GetModelSnapshotsResponse>() {
+                    @Override
+                    public void onResponse(GetModelSnapshotsResponse getModelSnapshotsResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::get-model-snapshots-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::get-model-snapshots-execute-async
+            client.machineLearning().getModelSnapshotsAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::get-model-snapshots-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testPutCalendar() throws IOException, InterruptedException {
         RestHighLevelClient client = highLevelClient();
 

+ 67 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetModelSnapshotsRequestTests.java

@@ -0,0 +1,67 @@
+/*
+ * 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.job.util.PageParams;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+
+
+public class GetModelSnapshotsRequestTests extends AbstractXContentTestCase<GetModelSnapshotsRequest> {
+
+    @Override
+    protected GetModelSnapshotsRequest createTestInstance() {
+        GetModelSnapshotsRequest request = new GetModelSnapshotsRequest(randomAlphaOfLengthBetween(1, 20));
+        if (randomBoolean()) {
+            request.setSnapshotId(String.valueOf(randomNonNegativeLong()));
+        }
+        else {
+            if (randomBoolean()) {
+                request.setStart(String.valueOf(randomLong()));
+            }
+            if (randomBoolean()) {
+                request.setEnd(String.valueOf(randomLong()));
+            }
+            if (randomBoolean()) {
+                int from = randomInt(10000);
+                int size = randomInt(10000);
+                request.setPageParams(new PageParams(from, size));
+            }
+            if (randomBoolean()) {
+                request.setSort("description");
+            }
+            if (randomBoolean()) {
+                request.setDesc(randomBoolean());
+            }
+        }
+        return request;
+    }
+
+    @Override
+    protected GetModelSnapshotsRequest doParseInstance(XContentParser parser) throws IOException {
+        return GetModelSnapshotsRequest.PARSER.apply(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+}

+ 51 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetModelSnapshotsResponseTests.java

@@ -0,0 +1,51 @@
+/*
+ * 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.job.process.ModelSnapshot;
+import org.elasticsearch.client.ml.job.process.ModelSnapshotTests;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GetModelSnapshotsResponseTests extends AbstractXContentTestCase<GetModelSnapshotsResponse> {
+
+    @Override
+    protected GetModelSnapshotsResponse createTestInstance() {
+        int listSize = randomInt(10);
+        List<ModelSnapshot.Builder> modelSnapshots = new ArrayList<>(listSize);
+        for (int j = 0; j < listSize; j++) {
+            modelSnapshots.add(ModelSnapshotTests.createRandomizedBuilder());
+        }
+        return new GetModelSnapshotsResponse(modelSnapshots, listSize);
+    }
+
+    @Override
+    protected GetModelSnapshotsResponse doParseInstance(XContentParser parser) throws IOException {
+        return GetModelSnapshotsResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}

+ 5 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSnapshotTests.java

@@ -158,6 +158,10 @@ public class ModelSnapshotTests extends AbstractXContentTestCase<ModelSnapshot>
     }
 
     public static ModelSnapshot createRandomized() {
+        return createRandomizedBuilder().build();
+    }
+
+    public static ModelSnapshot.Builder createRandomizedBuilder() {
         ModelSnapshot.Builder modelSnapshot = new ModelSnapshot.Builder(randomAlphaOfLengthBetween(1, 20));
         modelSnapshot.setMinVersion(Version.CURRENT);
         modelSnapshot.setTimestamp(new Date(TimeValue.parseTimeValue(randomTimeValue(), "test").millis()));
@@ -171,7 +175,7 @@ public class ModelSnapshotTests extends AbstractXContentTestCase<ModelSnapshot>
                 new Date(TimeValue.parseTimeValue(randomTimeValue(), "test").millis()));
         modelSnapshot.setQuantiles(QuantilesTests.createRandomized());
         modelSnapshot.setRetain(randomBoolean());
-        return modelSnapshot.build();
+        return modelSnapshot;
     }
 
     @Override

+ 76 - 0
docs/java-rest/high-level/ml/get-model-snapshots.asciidoc

@@ -0,0 +1,76 @@
+--
+:api: get-model-snapshots
+:request: GetModelSnapshotsRequest
+:response: GetModelSnapshotsResponse
+--
+[id="{upid}-{api}"]
+=== Get Model Snapshots API
+
+The Get Model Snapshots API retrieves one or more model snapshot results.
+It accepts a +{request}+ object and responds
+with a +{response}+ object.
+
+[id="{upid}-{api}-request"]
+==== Get Model Snapshots Request
+
+A +{request}+ object gets created with an existing non-null `jobId`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+<1> Constructing a new request referencing an existing `jobId`
+
+==== Optional Arguments
+The following arguments are optional:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-snapshot-id]
+--------------------------------------------------
+<1> The id of the snapshot to get. Otherwise it will return all snapshots.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-desc]
+--------------------------------------------------
+<1> If `true`, the snapshots are sorted in descending order. Defaults to `false`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-end]
+--------------------------------------------------
+<1> Snapshots with timestamps earlier than this time will be returned.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-sort]
+--------------------------------------------------
+<1> The field to sort snapshots on. Defaults to `timestamp`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-start]
+--------------------------------------------------
+<1> Snapshots with timestamps on or after this time will be returned.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-page]
+--------------------------------------------------
+<1> The page parameters `from` and `size`. `from` specifies the number of snapshots to skip.
+`size` specifies the maximum number of snapshots to retrieve. Defaults to `0` and `100` respectively.
+
+include::../execution.asciidoc[]
+
+[id="{upid}-{api}-response"]
+==== Get Model Snapshots Response
+
+The returned +{response}+ contains the requested snapshots:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> The count of snapshots that were matched
+<2> The snapshots retrieved

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -265,6 +265,7 @@ The Java High Level REST Client supports the following Machine Learning APIs:
 * <<{upid}-put-calendar>>
 * <<{upid}-delete-calendar>>
 * <<{upid}-put-filter>>
+* <<{upid}-get-model-snapshots>>
 * <<{upid}-get-filters>>
 
 include::ml/put-job.asciidoc[]
@@ -295,6 +296,7 @@ include::ml/get-calendars.asciidoc[]
 include::ml/put-calendar.asciidoc[]
 include::ml/delete-calendar.asciidoc[]
 include::ml/put-filter.asciidoc[]
+include::ml/get-model-snapshots.asciidoc[]
 include::ml/get-filters.asciidoc[]
 
 == Migration APIs