Browse Source

[HLRC][ML] Add delete expired data API (#35906)

Relates to #29827
Ed Savage 6 years ago
parent
commit
13e11966ca

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

@@ -32,6 +32,7 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
 import org.elasticsearch.client.ml.DeleteFilterRequest;
 import org.elasticsearch.client.ml.DeleteForecastRequest;
 import org.elasticsearch.client.ml.DeleteJobRequest;
@@ -155,6 +156,17 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request deleteExpiredData(DeleteExpiredDataRequest deleteExpiredDataRequest) {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("_delete_expired_data")
+            .build();
+        Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
+
+        return request;
+    }
+
     static Request deleteJob(DeleteJobRequest deleteJobRequest) {
         String endpoint = new EndpointBuilder()
                 .addPathPartAsIs("_xpack")

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

@@ -26,6 +26,8 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataResponse;
 import org.elasticsearch.client.ml.DeleteFilterRequest;
 import org.elasticsearch.client.ml.DeleteForecastRequest;
 import org.elasticsearch.client.ml.DeleteJobRequest;
@@ -227,6 +229,48 @@ public final class MachineLearningClient {
                 Collections.emptySet());
     }
 
+    /**
+     * Deletes expired data from Machine Learning Jobs
+     * <p>
+     * For additional info
+     * see <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-expired-data.html">ML Delete Expired Data
+     * documentation</a>
+     *
+     * @param request The request to delete expired ML data
+     * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return The action response which contains the acknowledgement or the task id depending on whether the action was set to wait for
+     * completion
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public DeleteExpiredDataResponse deleteExpiredData(DeleteExpiredDataRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::deleteExpiredData,
+            options,
+            DeleteExpiredDataResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Deletes expired data from Machine Learning Jobs asynchronously and notifies the listener on completion
+     * <p>
+     * For additional info
+     * see <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-expired-data.html">ML Delete Expired Data
+     * documentation</a>
+     *
+     * @param request  The request to delete expired ML data
+     * @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 deleteExpiredDataAsync(DeleteExpiredDataRequest request, RequestOptions options,
+                               ActionListener<DeleteExpiredDataResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::deleteExpiredData,
+            options,
+            DeleteExpiredDataResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
+
     /**
      * Deletes the given Machine Learning Job
      * <p>

+ 39 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteExpiredDataRequest.java

@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Request to delete expired model snapshots and forecasts
+ */
+public class DeleteExpiredDataRequest extends ActionRequest {
+
+   /**
+     * Create a new request to delete expired data
+     */
+    public DeleteExpiredDataRequest() {
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+}

+ 88 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteExpiredDataResponse.java

@@ -0,0 +1,88 @@
+/*
+ * 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.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+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;
+
+
+/**
+ * A response acknowledging the deletion of expired data
+ */
+public class DeleteExpiredDataResponse extends ActionResponse implements ToXContentObject {
+
+    private static final ParseField DELETED = new ParseField("deleted");
+
+    public DeleteExpiredDataResponse(boolean deleted) {
+        this.deleted = deleted;
+    }
+
+    public static final ConstructingObjectParser<DeleteExpiredDataResponse, Void> PARSER =
+        new ConstructingObjectParser<>("delete_expired_data_response", true,
+            a -> new DeleteExpiredDataResponse((Boolean) a[0]));
+
+    static {
+        PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), DELETED);
+    }
+
+    public static DeleteExpiredDataResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    private final Boolean deleted;
+
+    public Boolean getDeleted() {
+        return deleted;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deleted);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject();
+        if (deleted != null) {
+            builder.field(DELETED.getPreferredName(), deleted);
+        }
+        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DeleteExpiredDataResponse response = (DeleteExpiredDataResponse) obj;
+        return Objects.equals(deleted, response.deleted);
+    }
+}

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

@@ -28,6 +28,7 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
 import org.elasticsearch.client.ml.DeleteFilterRequest;
 import org.elasticsearch.client.ml.DeleteForecastRequest;
 import org.elasticsearch.client.ml.DeleteJobRequest;
@@ -183,6 +184,14 @@ public class MLRequestConvertersTests extends ESTestCase {
             requestEntityToString(request));
     }
 
+    public void testDeleteExpiredData() {
+        DeleteExpiredDataRequest deleteExpiredDataRequest = new DeleteExpiredDataRequest();
+
+        Request request = MLRequestConverters.deleteExpiredData(deleteExpiredDataRequest);
+        assertEquals(HttpDelete.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/_delete_expired_data", request.getEndpoint());
+    }
+
     public void testDeleteJob() {
         String jobId = randomAlphaOfLength(10);
         DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId);

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

@@ -27,12 +27,15 @@ import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.update.UpdateRequest;
 import org.elasticsearch.client.ml.CloseJobRequest;
 import org.elasticsearch.client.ml.CloseJobResponse;
 import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataResponse;
 import org.elasticsearch.client.ml.DeleteFilterRequest;
 import org.elasticsearch.client.ml.DeleteForecastRequest;
 import org.elasticsearch.client.ml.DeleteJobRequest;
@@ -110,6 +113,7 @@ import org.elasticsearch.client.ml.job.util.PageParams;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.search.SearchHit;
 import org.junit.After;
 
 import java.io.IOException;
@@ -130,6 +134,7 @@ import static org.hamcrest.CoreMatchers.hasItems;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 
@@ -772,6 +777,142 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         assertThat(totalTotals, containsInAnyOrder(totals));
     }
 
+    public void testDeleteExpiredDataGivenNothingToDelete() throws Exception {
+        // Tests that nothing goes wrong when there's nothing to delete
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        DeleteExpiredDataResponse response = execute(new DeleteExpiredDataRequest(),
+            machineLearningClient::deleteExpiredData,
+            machineLearningClient::deleteExpiredDataAsync);
+
+        assertTrue(response.getDeleted());
+    }
+
+    private  String createExpiredData(String jobId) throws Exception {
+        String indexId = jobId + "-data";
+        // Set up the index and docs
+        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexId);
+        createIndexRequest.mapping("doc", "timestamp", "type=date,format=epoch_millis", "total", "type=long");
+        highLevelClient().indices().create(createIndexRequest, RequestOptions.DEFAULT);
+        BulkRequest bulk = new BulkRequest();
+        bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+
+        long nowMillis = System.currentTimeMillis();
+        int totalBuckets = 2 * 24;
+        int normalRate = 10;
+        int anomalousRate = 100;
+        int anomalousBucket = 30;
+        for (int bucket = 0; bucket < totalBuckets; bucket++) {
+            long timestamp = nowMillis - TimeValue.timeValueHours(totalBuckets - bucket).getMillis();
+            int bucketRate = bucket == anomalousBucket ? anomalousRate : normalRate;
+            for (int point = 0; point < bucketRate; point++) {
+                IndexRequest indexRequest = new IndexRequest(indexId, "doc");
+                indexRequest.source(XContentType.JSON, "timestamp", timestamp, "total", randomInt(1000));
+                bulk.add(indexRequest);
+            }
+        }
+        highLevelClient().bulk(bulk, RequestOptions.DEFAULT);
+
+        {
+            // Index a randomly named unused state document
+            String docId = "non_existing_job_" + randomFrom("model_state_1234567#1", "quantiles", "categorizer_state#1");
+            IndexRequest indexRequest = new IndexRequest(".ml-state", "doc", docId);
+            indexRequest.source(Collections.emptyMap(), XContentType.JSON);
+            indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+            highLevelClient().index(indexRequest, RequestOptions.DEFAULT);
+        }
+
+        Job job = buildJobForExpiredDataTests(jobId);
+        putJob(job);
+        openJob(job);
+        String datafeedId = createAndPutDatafeed(jobId, indexId);
+
+        startDatafeed(datafeedId, String.valueOf(0), String.valueOf(nowMillis - TimeValue.timeValueHours(24).getMillis()));
+
+        waitForJobToClose(jobId);
+
+        // Update snapshot timestamp to force it out of snapshot retention window
+        long oneDayAgo = nowMillis - TimeValue.timeValueHours(24).getMillis() - 1;
+        updateModelSnapshotTimestamp(jobId, String.valueOf(oneDayAgo));
+
+        openJob(job);
+
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+        ForecastJobRequest forecastJobRequest = new ForecastJobRequest(jobId);
+        forecastJobRequest.setDuration(TimeValue.timeValueHours(3));
+        forecastJobRequest.setExpiresIn(TimeValue.timeValueSeconds(1));
+        ForecastJobResponse forecastJobResponse = machineLearningClient.forecastJob(forecastJobRequest, RequestOptions.DEFAULT);
+
+        waitForForecastToComplete(jobId, forecastJobResponse.getForecastId());
+
+        // Wait for the forecast to expire
+        awaitBusy(() -> false, 1, TimeUnit.SECONDS);
+
+        // Run up to now
+        startDatafeed(datafeedId, String.valueOf(0), String.valueOf(nowMillis));
+
+        waitForJobToClose(jobId);
+
+        return forecastJobResponse.getForecastId();
+    }
+
+    public void testDeleteExpiredData() throws Exception {
+
+        String jobId = "test-delete-expired-data";
+
+        String forecastId = createExpiredData(jobId);
+
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        GetModelSnapshotsRequest getModelSnapshotsRequest = new GetModelSnapshotsRequest(jobId);
+        GetModelSnapshotsResponse getModelSnapshotsResponse = execute(getModelSnapshotsRequest, machineLearningClient::getModelSnapshots,
+            machineLearningClient::getModelSnapshotsAsync);
+
+        assertEquals(2L, getModelSnapshotsResponse.count());
+
+        assertTrue(forecastExists(jobId, forecastId));
+
+        {
+            // Verify .ml-state contains the expected unused state document
+            Iterable<SearchHit> hits = searchAll(".ml-state");
+            List<SearchHit> target = new ArrayList<>();
+            hits.forEach(target::add);
+            long numMatches = target.stream()
+                .filter(c -> c.getId().startsWith("non_existing_job"))
+                .count();
+
+            assertThat(numMatches, equalTo(1L));
+        }
+
+        DeleteExpiredDataRequest request = new DeleteExpiredDataRequest();
+        DeleteExpiredDataResponse response = execute(request, machineLearningClient::deleteExpiredData,
+            machineLearningClient::deleteExpiredDataAsync);
+
+        assertTrue(response.getDeleted());
+
+        awaitBusy(() -> false, 1, TimeUnit.SECONDS);
+
+        GetModelSnapshotsRequest getModelSnapshotsRequest1 = new GetModelSnapshotsRequest(jobId);
+        GetModelSnapshotsResponse getModelSnapshotsResponse1 = execute(getModelSnapshotsRequest1, machineLearningClient::getModelSnapshots,
+            machineLearningClient::getModelSnapshotsAsync);
+
+        assertEquals(1L, getModelSnapshotsResponse1.count());
+
+        assertFalse(forecastExists(jobId, forecastId));
+
+        {
+            // Verify .ml-state doesn't contain unused state documents
+            Iterable<SearchHit> hits = searchAll(".ml-state");
+            List<SearchHit> hitList = new ArrayList<>();
+            hits.forEach(hitList::add);
+            long numMatches = hitList.stream()
+                .filter(c -> c.getId().startsWith("non_existing_job"))
+                .count();
+
+            assertThat(numMatches, equalTo(0L));
+        }
+    }
+
     public void testDeleteForecast() throws Exception {
         String jobId = "test-delete-forecast";
 
@@ -1146,6 +1287,27 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         return generator.ofCodePointsLength(random(), 10, 10);
     }
 
+    private static Job buildJobForExpiredDataTests(String jobId) {
+        Job.Builder builder = new Job.Builder(jobId);
+        builder.setDescription(randomAlphaOfLength(10));
+
+        Detector detector = new Detector.Builder()
+            .setFunction("count")
+            .setDetectorDescription(randomAlphaOfLength(10))
+            .build();
+        AnalysisConfig.Builder configBuilder = new AnalysisConfig.Builder(Arrays.asList(detector));
+        //should not be random, see:https://github.com/elastic/ml-cpp/issues/208
+        configBuilder.setBucketSpan(new TimeValue(1, TimeUnit.HOURS));
+        builder.setAnalysisConfig(configBuilder);
+
+        DataDescription.Builder dataDescription = new DataDescription.Builder();
+        dataDescription.setTimeFormat(DataDescription.EPOCH_MS);
+        dataDescription.setTimeField("timestamp");
+        builder.setDataDescription(dataDescription);
+
+        return builder.build();
+    }
+
     public static Job buildJob(String jobId) {
         Job.Builder builder = new Job.Builder(jobId);
         builder.setDescription(randomAlphaOfLength(10));
@@ -1176,6 +1338,53 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         highLevelClient().machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT);
     }
 
+    private void waitForJobToClose(String jobId) throws Exception {
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        assertBusy(() -> {
+            JobStats stats = machineLearningClient.getJobStats(new GetJobStatsRequest(jobId), RequestOptions.DEFAULT).jobStats().get(0);
+            assertEquals(JobState.CLOSED, stats.getState());
+        }, 30, TimeUnit.SECONDS);
+    }
+
+    private void startDatafeed(String datafeedId, String start, String end) throws Exception {
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        StartDatafeedRequest startDatafeedRequest = new StartDatafeedRequest(datafeedId);
+        startDatafeedRequest.setStart(start);
+        startDatafeedRequest.setEnd(end);
+        StartDatafeedResponse response = execute(startDatafeedRequest,
+            machineLearningClient::startDatafeed,
+            machineLearningClient::startDatafeedAsync);
+
+        assertTrue(response.isStarted());
+    }
+
+    private void updateModelSnapshotTimestamp(String jobId, String timestamp) throws Exception {
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+
+        GetModelSnapshotsRequest getModelSnapshotsRequest = new GetModelSnapshotsRequest(jobId);
+        GetModelSnapshotsResponse getModelSnapshotsResponse = execute(getModelSnapshotsRequest, machineLearningClient::getModelSnapshots,
+            machineLearningClient::getModelSnapshotsAsync);
+
+        assertThat(getModelSnapshotsResponse.count(), greaterThanOrEqualTo(1L));
+
+        ModelSnapshot modelSnapshot = getModelSnapshotsResponse.snapshots().get(0);
+
+        String snapshotId = modelSnapshot.getSnapshotId();
+        String documentId = jobId + "_model_snapshot_" + snapshotId;
+
+        String snapshotUpdate = "{ \"timestamp\": " + timestamp + "}";
+        UpdateRequest updateSnapshotRequest = new UpdateRequest(".ml-anomalies-" + jobId, "doc", documentId);
+        updateSnapshotRequest.doc(snapshotUpdate.getBytes(StandardCharsets.UTF_8), XContentType.JSON);
+        highLevelClient().update(updateSnapshotRequest, RequestOptions.DEFAULT);
+
+        // Wait a second to ensure subsequent model snapshots will have a different ID (it depends on epoch seconds)
+        awaitBusy(() -> false, 1, TimeUnit.SECONDS);
+    }
+
+
+
     private String createAndPutDatafeed(String jobId, String indexName) throws IOException {
         String datafeedId = jobId + "-feed";
         DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId)

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

@@ -39,6 +39,8 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
+import org.elasticsearch.client.ml.DeleteExpiredDataResponse;
 import org.elasticsearch.client.ml.DeleteFilterRequest;
 import org.elasticsearch.client.ml.DeleteForecastRequest;
 import org.elasticsearch.client.ml.DeleteJobRequest;
@@ -1959,6 +1961,56 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testDeleteExpiredData() throws IOException, InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+
+        String jobId = "test-delete-expired-data";
+        MachineLearningIT.buildJob(jobId);
+       {
+            // tag::delete-expired-data-request
+            DeleteExpiredDataRequest request = new DeleteExpiredDataRequest(); // <1>
+            // end::delete-expired-data-request
+
+            // tag::delete-expired-data-execute
+            DeleteExpiredDataResponse response = client.machineLearning().deleteExpiredData(request, RequestOptions.DEFAULT);
+            // end::delete-expired-data-execute
+
+            // tag::delete-expired-data-response
+            boolean deleted = response.getDeleted(); // <1>
+            // end::delete-expired-data-response
+
+            assertTrue(deleted);
+        }
+        {
+            // tag::delete-expired-data-execute-listener
+            ActionListener<DeleteExpiredDataResponse> listener = new ActionListener<DeleteExpiredDataResponse>() {
+                @Override
+                public void onResponse(DeleteExpiredDataResponse deleteExpiredDataResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::delete-expired-data-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            DeleteExpiredDataRequest deleteExpiredDataRequest = new DeleteExpiredDataRequest();
+
+            // tag::delete-expired-data-execute-async
+            client.machineLearning().deleteExpiredDataAsync(deleteExpiredDataRequest, RequestOptions.DEFAULT, listener); // <1>
+            // end::delete-expired-data-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
+
     public void testDeleteModelSnapshot() throws IOException, InterruptedException {
         RestHighLevelClient client = highLevelClient();
 

+ 43 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteExpiredDataResponseTests.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.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+
+
+public class DeleteExpiredDataResponseTests extends AbstractXContentTestCase<DeleteExpiredDataResponse> {
+
+    @Override
+    protected DeleteExpiredDataResponse createTestInstance() {
+        return new DeleteExpiredDataResponse(randomBoolean());
+    }
+
+    @Override
+    protected DeleteExpiredDataResponse doParseInstance(XContentParser parser) throws IOException {
+        return DeleteExpiredDataResponse.PARSER.apply(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}

+ 34 - 0
docs/java-rest/high-level/ml/delete-expired-data.asciidoc

@@ -0,0 +1,34 @@
+
+--
+:api: delete-expired-data
+:request: DeleteExpiredRequest
+:response: DeleteExpiredResponse
+--
+[id="{upid}-{api}"]
+=== Delete Expired Data API
+Delete expired {ml} data.
+The API accepts a +{request}+ and responds
+with a +{response}+ object.
+
+[id="{upid}-{api}-request"]
+==== Delete Expired Data Request
+
+A `DeleteExpiredDataRequest` object does not require any arguments.
+
+["source","java",subs="attributes,callouts,macros"]
+---------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+---------------------------------------------------
+<1> Constructing a new request.
+
+[id="{upid}-{api}-response"]
+==== Delete Expired Data Response
+
+The returned +{response}+ object indicates the acknowledgement of the request:
+["source","java",subs="attributes,callouts,macros"]
+---------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+---------------------------------------------------
+<1> `getDeleted` acknowledges the deletion request.
+
+include::../execution.asciidoc[]

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

@@ -280,6 +280,7 @@ The Java High Level REST Client supports the following Machine Learning APIs:
 * <<{upid}-delete-model-snapshot>>
 * <<{upid}-revert-model-snapshot>>
 * <<{upid}-update-model-snapshot>>
+* <<{upid}-delete-expired-data>>
 
 include::ml/put-job.asciidoc[]
 include::ml/get-job.asciidoc[]
@@ -321,6 +322,7 @@ include::ml/get-model-snapshots.asciidoc[]
 include::ml/delete-model-snapshot.asciidoc[]
 include::ml/revert-model-snapshot.asciidoc[]
 include::ml/update-model-snapshot.asciidoc[]
+include::ml/delete-expired-data.asciidoc[]
 
 == Migration APIs
 

+ 47 - 0
docs/reference/ml/apis/delete-expired-data.asciidoc

@@ -0,0 +1,47 @@
+[role="xpack"]
+[testenv="platinum"]
+[[ml-delete-expired-data]]
+=== Delete Expired Data API
+++++
+<titleabbrev>Delete Expired Data</titleabbrev>
+++++
+
+Deletes expired and unused machine learning data.
+
+==== Request
+
+`DELETE _xpack/ml/_delete_expired_data`
+
+==== Description
+
+Deletes all job results, model snapshots and forecast data that have exceeded their
+`retention days` period.
+Machine Learning state documents that are not associated with any job are also deleted.
+
+==== Authorization
+
+You must have `manage_ml`, or `manage` cluster privileges to use this API.
+For more information, see
+{stack-ov}/security-privileges.html[Security Privileges] and
+{stack-ov}/built-in-roles.html[Built-in Roles].
+
+
+==== Examples
+
+The endpoint takes no arguments:
+
+[source,js]
+--------------------------------------------------
+DELETE _xpack/ml/_delete_expired_data
+--------------------------------------------------
+// CONSOLE
+// TEST
+
+When the expired data is deleted, you receive the following response:
+[source,js]
+----
+{
+  "deleted": true
+}
+----
+// TESTRESPONSE

+ 7 - 0
docs/reference/ml/apis/ml-api.asciidoc

@@ -82,6 +82,12 @@ machine learning APIs and in advanced job configuration options in Kibana.
 
 * <<get-ml-info,Machine learning info>>
 
+[float]
+[[ml-api-delete-expired-data-endpoint]]
+=== Delete Expired Data
+
+* <<ml-delete-expired-data,Delete expired data>>
+
 //ADD
 include::post-calendar-event.asciidoc[]
 include::put-calendar-job.asciidoc[]
@@ -101,6 +107,7 @@ include::delete-forecast.asciidoc[]
 include::delete-job.asciidoc[]
 include::delete-calendar-job.asciidoc[]
 include::delete-snapshot.asciidoc[]
+include::delete-expired-data.asciidoc[]
 //FIND
 include::find-file-structure.asciidoc[]
 //FLUSH