parent
3ed3aab127
commit
13e11966ca
|
@ -32,6 +32,7 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
||||||
|
import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteFilterRequest;
|
import org.elasticsearch.client.ml.DeleteFilterRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteJobRequest;
|
import org.elasticsearch.client.ml.DeleteJobRequest;
|
||||||
|
@ -155,6 +156,17 @@ final class MLRequestConverters {
|
||||||
return request;
|
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) {
|
static Request deleteJob(DeleteJobRequest deleteJobRequest) {
|
||||||
String endpoint = new EndpointBuilder()
|
String endpoint = new EndpointBuilder()
|
||||||
.addPathPartAsIs("_xpack")
|
.addPathPartAsIs("_xpack")
|
||||||
|
|
|
@ -26,6 +26,8 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
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.DeleteFilterRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteJobRequest;
|
import org.elasticsearch.client.ml.DeleteJobRequest;
|
||||||
|
@ -227,6 +229,48 @@ public final class MachineLearningClient {
|
||||||
Collections.emptySet());
|
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
|
* Deletes the given Machine Learning Job
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
||||||
|
import org.elasticsearch.client.ml.DeleteExpiredDataRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteFilterRequest;
|
import org.elasticsearch.client.ml.DeleteFilterRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteJobRequest;
|
import org.elasticsearch.client.ml.DeleteJobRequest;
|
||||||
|
@ -183,6 +184,14 @@ public class MLRequestConvertersTests extends ESTestCase {
|
||||||
requestEntityToString(request));
|
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() {
|
public void testDeleteJob() {
|
||||||
String jobId = randomAlphaOfLength(10);
|
String jobId = randomAlphaOfLength(10);
|
||||||
DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId);
|
DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId);
|
||||||
|
|
|
@ -27,12 +27,15 @@ import org.elasticsearch.action.get.GetResponse;
|
||||||
import org.elasticsearch.action.index.IndexRequest;
|
import org.elasticsearch.action.index.IndexRequest;
|
||||||
import org.elasticsearch.action.support.WriteRequest;
|
import org.elasticsearch.action.support.WriteRequest;
|
||||||
import org.elasticsearch.action.support.master.AcknowledgedResponse;
|
import org.elasticsearch.action.support.master.AcknowledgedResponse;
|
||||||
|
import org.elasticsearch.action.update.UpdateRequest;
|
||||||
import org.elasticsearch.client.ml.CloseJobRequest;
|
import org.elasticsearch.client.ml.CloseJobRequest;
|
||||||
import org.elasticsearch.client.ml.CloseJobResponse;
|
import org.elasticsearch.client.ml.CloseJobResponse;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
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.DeleteFilterRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteJobRequest;
|
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.unit.TimeValue;
|
||||||
import org.elasticsearch.common.xcontent.XContentType;
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
import org.elasticsearch.rest.RestStatus;
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
import org.elasticsearch.search.SearchHit;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -130,6 +134,7 @@ import static org.hamcrest.CoreMatchers.hasItems;
|
||||||
import static org.hamcrest.CoreMatchers.not;
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
@ -772,6 +777,142 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
|
||||||
assertThat(totalTotals, containsInAnyOrder(totals));
|
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 {
|
public void testDeleteForecast() throws Exception {
|
||||||
String jobId = "test-delete-forecast";
|
String jobId = "test-delete-forecast";
|
||||||
|
|
||||||
|
@ -1146,6 +1287,27 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
|
||||||
return generator.ofCodePointsLength(random(), 10, 10);
|
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) {
|
public static Job buildJob(String jobId) {
|
||||||
Job.Builder builder = new Job.Builder(jobId);
|
Job.Builder builder = new Job.Builder(jobId);
|
||||||
builder.setDescription(randomAlphaOfLength(10));
|
builder.setDescription(randomAlphaOfLength(10));
|
||||||
|
@ -1176,6 +1338,53 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
|
||||||
highLevelClient().machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT);
|
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 {
|
private String createAndPutDatafeed(String jobId, String indexName) throws IOException {
|
||||||
String datafeedId = jobId + "-feed";
|
String datafeedId = jobId + "-feed";
|
||||||
DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId)
|
DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId)
|
||||||
|
|
|
@ -39,6 +39,8 @@ import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
import org.elasticsearch.client.ml.DeleteCalendarRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteDatafeedRequest;
|
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.DeleteFilterRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
import org.elasticsearch.client.ml.DeleteForecastRequest;
|
||||||
import org.elasticsearch.client.ml.DeleteJobRequest;
|
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 {
|
public void testDeleteModelSnapshot() throws IOException, InterruptedException {
|
||||||
RestHighLevelClient client = highLevelClient();
|
RestHighLevelClient client = highLevelClient();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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[]
|
|
@ -280,6 +280,7 @@ The Java High Level REST Client supports the following Machine Learning APIs:
|
||||||
* <<{upid}-delete-model-snapshot>>
|
* <<{upid}-delete-model-snapshot>>
|
||||||
* <<{upid}-revert-model-snapshot>>
|
* <<{upid}-revert-model-snapshot>>
|
||||||
* <<{upid}-update-model-snapshot>>
|
* <<{upid}-update-model-snapshot>>
|
||||||
|
* <<{upid}-delete-expired-data>>
|
||||||
|
|
||||||
include::ml/put-job.asciidoc[]
|
include::ml/put-job.asciidoc[]
|
||||||
include::ml/get-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/delete-model-snapshot.asciidoc[]
|
||||||
include::ml/revert-model-snapshot.asciidoc[]
|
include::ml/revert-model-snapshot.asciidoc[]
|
||||||
include::ml/update-model-snapshot.asciidoc[]
|
include::ml/update-model-snapshot.asciidoc[]
|
||||||
|
include::ml/delete-expired-data.asciidoc[]
|
||||||
|
|
||||||
== Migration APIs
|
== Migration APIs
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -82,6 +82,12 @@ machine learning APIs and in advanced job configuration options in Kibana.
|
||||||
|
|
||||||
* <<get-ml-info,Machine learning info>>
|
* <<get-ml-info,Machine learning info>>
|
||||||
|
|
||||||
|
[float]
|
||||||
|
[[ml-api-delete-expired-data-endpoint]]
|
||||||
|
=== Delete Expired Data
|
||||||
|
|
||||||
|
* <<ml-delete-expired-data,Delete expired data>>
|
||||||
|
|
||||||
//ADD
|
//ADD
|
||||||
include::post-calendar-event.asciidoc[]
|
include::post-calendar-event.asciidoc[]
|
||||||
include::put-calendar-job.asciidoc[]
|
include::put-calendar-job.asciidoc[]
|
||||||
|
@ -101,6 +107,7 @@ include::delete-forecast.asciidoc[]
|
||||||
include::delete-job.asciidoc[]
|
include::delete-job.asciidoc[]
|
||||||
include::delete-calendar-job.asciidoc[]
|
include::delete-calendar-job.asciidoc[]
|
||||||
include::delete-snapshot.asciidoc[]
|
include::delete-snapshot.asciidoc[]
|
||||||
|
include::delete-expired-data.asciidoc[]
|
||||||
//FIND
|
//FIND
|
||||||
include::find-file-structure.asciidoc[]
|
include::find-file-structure.asciidoc[]
|
||||||
//FLUSH
|
//FLUSH
|
||||||
|
|
Loading…
Reference in New Issue