[ML] all multiple wildcard values for GET Calendars, Events, and DELETE forecasts (#62563) (#62629)

This commit adjusts the following APIs so now they not only support an `_all` case, but wildcard patterned Ids as well.

- `GET _ml/calendars/<calendar_id>/events`
- `GET _ml/calendars/<calendar_id>`
- `GET _ml/anomaly_detectors/<job_id>/model_snapshots/<snapshot_id>`
- `DELETE _ml/anomaly_detectors/<job_id>/_forecast/<forecast_id>`
This commit is contained in:
Benjamin Trent 2020-09-18 11:06:07 -04:00 committed by GitHub
parent 43ace5f80d
commit 0f142c6afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 452 additions and 140 deletions

View File

@ -42,7 +42,7 @@ For more information, see
`<forecast_id>`::
(Optional, string) A comma-separated list of forecast identifiers. If you do not
specify this optional parameter or if you specify `_all`, the API deletes all
specify this optional parameter or if you specify `_all` or `*` the API deletes all
forecasts from the job.
`<job_id>`::

View File

@ -25,8 +25,10 @@ Retrieves information about the scheduled events in calendars.
[[ml-get-calendar-event-desc]]
== {api-description-title}
You can get scheduled event information for a single calendar or for all
calendars by using `_all`.
You can get scheduled event information for multiple calendars in a single
API request by using a comma-separated list of ids or a wildcard expression.
You can get scheduled event information for all calendars by using `_all`,
by specifying `*` as the `<calendar_id>`, or by omitting the `<calendar_id>`.
For more information, see
{ml-docs}/ml-calendars.html[Calendars and scheduled events].

View File

@ -25,8 +25,10 @@ Retrieves configuration information for calendars.
[[ml-get-calendar-desc]]
== {api-description-title}
You can get information for a single calendar or for all calendars by using
`_all`.
You can get information for multiple calendars in a single API request by using a
comma-separated list of ids or a wildcard expression. You can get
information for all calendars by using `_all`, by specifying `*` as the
`<calendar_id>`, or by omitting the `<calendar_id>`.
For more information, see
{ml-docs}/ml-calendars.html[Calendars and scheduled events].

View File

@ -34,8 +34,10 @@ include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection]
include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=snapshot-id]
+
--
If you do not specify this optional parameter, the API returns information about
all model snapshots.
You can multiple snapshots for a single job in a single API request
by using a comma-separated list of `<snapshot_id>` or a wildcard expression.
You can get all snapshots for all calendars by using `_all`,
by specifying `*` as the `<snapshot_id>`, or by omitting the `<snapshot_id>`.
--
[[ml-get-snapshot-request-body]]

View File

@ -128,7 +128,7 @@ public class GetCalendarEventsAction extends ActionType<GetCalendarEventsAction.
if (jobId != null && Strings.isAllOrWildcard(calendarId) == false) {
e = ValidateActions.addValidationError("If " + Job.ID.getPreferredName() + " is used " +
Calendar.ID.getPreferredName() + " must be '" + GetCalendarsAction.Request.ALL + "'", e);
Calendar.ID.getPreferredName() + " must be '" + GetCalendarsAction.Request.ALL + "' or '*'", e);
}
return e;
}

View File

@ -53,7 +53,7 @@ public class GetCalendarEventsActionRequestTests extends AbstractSerializingTest
ActionRequestValidationException validationException = request.validate();
assertNotNull(validationException);
assertEquals("Validation Failed: 1: If job_id is used calendar_id must be '_all';", validationException.getMessage());
assertEquals("Validation Failed: 1: If job_id is used calendar_id must be '_all' or '*';", validationException.getMessage());
request = new GetCalendarEventsAction.Request("_all");
request.setJobId("foo");

View File

@ -187,7 +187,7 @@ public class ForecastIT extends MlNativeAutodetectIntegTestCase {
equalTo("Cannot run forecast: Forecast cannot be executed as job requires data to have been processed and modeled"));
}
public void testMemoryStatus() throws Exception {
public void testMemoryStatus() {
Detector.Builder detector = new Detector.Builder("mean", "value");
detector.setByFieldName("clientIP");
@ -287,6 +287,74 @@ public class ForecastIT extends MlNativeAutodetectIntegTestCase {
}
public void testDeleteWildCard() throws Exception {
Detector.Builder detector = new Detector.Builder("mean", "value");
TimeValue bucketSpan = TimeValue.timeValueHours(1);
AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(Collections.singletonList(detector.build()));
analysisConfig.setBucketSpan(bucketSpan);
DataDescription.Builder dataDescription = new DataDescription.Builder();
dataDescription.setTimeFormat("epoch");
Job.Builder job = new Job.Builder("forecast-it-test-delete-wildcard");
job.setAnalysisConfig(analysisConfig);
job.setDataDescription(dataDescription);
registerJob(job);
putJob(job);
openJob(job.getId());
long now = Instant.now().getEpochSecond();
long timestamp = now - 50 * bucketSpan.seconds();
List<String> data = new ArrayList<>();
while (timestamp < now) {
data.add(createJsonRecord(createRecord(timestamp, 10.0)));
data.add(createJsonRecord(createRecord(timestamp, 30.0)));
timestamp += bucketSpan.seconds();
}
postData(job.getId(), data.stream().collect(Collectors.joining()));
flushJob(job.getId(), false);
String forecastIdDefaultDurationDefaultExpiry = forecast(job.getId(), null, null);
String forecastIdDuration1HourNoExpiry = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
String forecastId2Duration1HourNoExpiry = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
String forecastId2Duration1HourNoExpiry2 = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
waitForecastToFinish(job.getId(), forecastIdDefaultDurationDefaultExpiry);
waitForecastToFinish(job.getId(), forecastIdDuration1HourNoExpiry);
waitForecastToFinish(job.getId(), forecastId2Duration1HourNoExpiry);
waitForecastToFinish(job.getId(), forecastId2Duration1HourNoExpiry2);
closeJob(job.getId());
assertNotNull(getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry));
assertNotNull(getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry2));
{
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(),
forecastIdDefaultDurationDefaultExpiry.substring(0, forecastIdDefaultDurationDefaultExpiry.length() - 2) + "*"
+ ","
+ forecastIdDuration1HourNoExpiry);
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
assertTrue(response.isAcknowledged());
assertNull(getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry));
assertNull(getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry2));
}
{
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(), "*");
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
assertTrue(response.isAcknowledged());
assertNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry));
assertNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry2));
}
}
public void testDelete() throws Exception {
Detector.Builder detector = new Detector.Builder("mean", "value");
@ -317,6 +385,8 @@ public class ForecastIT extends MlNativeAutodetectIntegTestCase {
flushJob(job.getId(), false);
String forecastIdDefaultDurationDefaultExpiry = forecast(job.getId(), null, null);
String forecastIdDuration1HourNoExpiry = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
String forecastId2Duration1HourNoExpiry = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
String forecastId2Duration1HourNoExpiry2 = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
waitForecastToFinish(job.getId(), forecastIdDefaultDurationDefaultExpiry);
waitForecastToFinish(job.getId(), forecastIdDuration1HourNoExpiry);
closeJob(job.getId());
@ -333,13 +403,11 @@ public class ForecastIT extends MlNativeAutodetectIntegTestCase {
forecastIdDefaultDurationDefaultExpiry + "," + forecastIdDuration1HourNoExpiry);
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
assertTrue(response.isAcknowledged());
}
{
ForecastRequestStats forecastStats = getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry);
assertNull(forecastStats);
ForecastRequestStats otherStats = getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry);
assertNull(otherStats);
assertNull(getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry));
assertNull(getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry));
assertNotNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry2));
}
{
@ -354,6 +422,9 @@ public class ForecastIT extends MlNativeAutodetectIntegTestCase {
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(), Metadata.ALL);
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
assertTrue(response.isAcknowledged());
assertNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry));
assertNull(getForecastStats(job.getId(), forecastId2Duration1HourNoExpiry2));
}
{

View File

@ -16,6 +16,7 @@ import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.client.OriginSettingClient;
import org.elasticsearch.cluster.metadata.AliasMetadata;
@ -37,6 +38,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.ClientHelper;
import org.elasticsearch.xpack.core.action.util.PageParams;
import org.elasticsearch.xpack.core.action.util.QueryPage;
import org.elasticsearch.xpack.core.ml.MlMetaIndex;
import org.elasticsearch.xpack.core.ml.MlMetadata;
@ -72,6 +74,7 @@ import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService;
import org.junit.Before;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
@ -255,21 +258,69 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
calendars.add(new Calendar("cat foo calendar", Arrays.asList("cat", "foo"), null));
indexCalendars(calendars);
List<Calendar> queryResult = getCalendars("ted");
List<Calendar> queryResult = getCalendars(CalendarQueryBuilder.builder().jobId("ted"));
assertThat(queryResult, is(empty()));
queryResult = getCalendars("foo");
queryResult = getCalendars(CalendarQueryBuilder.builder().jobId("foo"));
assertThat(queryResult, hasSize(3));
Long matchedCount = queryResult.stream().filter(
c -> c.getId().equals("foo calendar") || c.getId().equals("foo bar calendar") || c.getId().equals("cat foo calendar"))
.count();
assertEquals(Long.valueOf(3), matchedCount);
queryResult = getCalendars("bar");
queryResult = getCalendars(CalendarQueryBuilder.builder().jobId("bar"));
assertThat(queryResult, hasSize(1));
assertEquals("foo bar calendar", queryResult.get(0).getId());
}
public void testGetCalandarById() throws Exception {
List<Calendar> calendars = new ArrayList<>();
calendars.add(new Calendar("empty calendar", Collections.emptyList(), null));
calendars.add(new Calendar("foo calendar", Collections.singletonList("foo"), null));
calendars.add(new Calendar("foo bar calendar", Arrays.asList("foo", "bar"), null));
calendars.add(new Calendar("cat calendar", Collections.singletonList("cat"), null));
calendars.add(new Calendar("cat foo calendar", Arrays.asList("cat", "foo"), null));
indexCalendars(calendars);
List<Calendar> queryResult = getCalendars(CalendarQueryBuilder.builder()
.calendarIdTokens(new String[]{"foo*"})
.sort(true));
assertThat(queryResult, hasSize(2));
assertThat(queryResult.get(0).getId(), equalTo("foo bar calendar"));
assertThat(queryResult.get(1).getId(), equalTo("foo calendar"));
queryResult = getCalendars(CalendarQueryBuilder.builder()
.calendarIdTokens(new String[]{"foo calendar", "cat calendar"})
.sort(true));
assertThat(queryResult, hasSize(2));
assertThat(queryResult.get(0).getId(), equalTo("cat calendar"));
assertThat(queryResult.get(1).getId(), equalTo("foo calendar"));
}
public void testGetCalendarByIdAndPaging() throws Exception {
List<Calendar> calendars = new ArrayList<>();
calendars.add(new Calendar("empty calendar", Collections.emptyList(), null));
calendars.add(new Calendar("foo calendar", Collections.singletonList("foo"), null));
calendars.add(new Calendar("foo bar calendar", Arrays.asList("foo", "bar"), null));
calendars.add(new Calendar("cat calendar", Collections.singletonList("cat"), null));
calendars.add(new Calendar("cat foo calendar", Arrays.asList("cat", "foo"), null));
indexCalendars(calendars);
List<Calendar> queryResult = getCalendars(CalendarQueryBuilder.builder()
.calendarIdTokens(new String[]{"foo*"})
.pageParams(new PageParams(0, 1))
.sort(true));
assertThat(queryResult, hasSize(1));
assertThat(queryResult.get(0).getId(), equalTo("foo bar calendar"));
queryResult = getCalendars(CalendarQueryBuilder.builder()
.calendarIdTokens(new String[]{"foo calendar", "cat calendar"})
.sort(true)
.pageParams(new PageParams(1, 1)));
assertThat(queryResult, hasSize(1));
assertThat(queryResult.get(0).getId(), equalTo("foo calendar"));
}
public void testUpdateCalendar() throws Exception {
MlMetadata.Builder mlBuilder = new MlMetadata.Builder();
mlBuilder.putJob(createJob("foo").build(), false);
@ -319,7 +370,7 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
throw exceptionHolder.get();
}
List<Calendar> updatedCalendars = getCalendars(null);
List<Calendar> updatedCalendars = getCalendars(CalendarQueryBuilder.builder());
assertEquals(5, updatedCalendars.size());
for (Calendar cal: updatedCalendars) {
assertThat("bar", is(not(in(cal.getJobIds()))));
@ -341,7 +392,7 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
throw exceptionHolder.get();
}
updatedCalendars = getCalendars(null);
updatedCalendars = getCalendars(CalendarQueryBuilder.builder());
assertEquals(5, updatedCalendars.size());
for (Calendar cal: updatedCalendars) {
assertThat("bar", is(not(in(cal.getJobIds()))));
@ -384,16 +435,11 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
return aliasMetadataList.stream().map(AliasMetadata::alias).collect(Collectors.toSet());
}
private List<Calendar> getCalendars(String jobId) throws Exception {
private List<Calendar> getCalendars(CalendarQueryBuilder query) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Exception> exceptionHolder = new AtomicReference<>();
AtomicReference<QueryPage<Calendar>> result = new AtomicReference<>();
CalendarQueryBuilder query = new CalendarQueryBuilder();
if (jobId != null) {
query.jobId(jobId);
}
jobProvider.calendars(query, ActionListener.wrap(
r -> {
result.set(r);
@ -455,7 +501,7 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
return calendarHolder.get();
}
public void testScheduledEvents() throws Exception {
public void testScheduledEventsForJobs() throws Exception {
Job.Builder jobA = createJob("job_a");
Job.Builder jobB = createJob("job_b");
Job.Builder jobC = createJob("job_c");
@ -504,6 +550,59 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
assertEquals(events.get(3), returnedEvents.get(1));
}
public void testScheduledEvents() throws Exception {
createJob("job_a");
createJob("job_b");
createJob("job_c");
String calendarAId = "maintenance_a";
List<Calendar> calendars = new ArrayList<>();
calendars.add(new Calendar(calendarAId, Collections.singletonList("job_a"), null));
ZonedDateTime now = ZonedDateTime.now();
List<ScheduledEvent> events = new ArrayList<>();
events.add(buildScheduledEvent("downtime", now.plusDays(1), now.plusDays(2), calendarAId));
events.add(buildScheduledEvent("downtime_AA", now.plusDays(8), now.plusDays(9), calendarAId));
events.add(buildScheduledEvent("downtime_AAA", now.plusDays(15), now.plusDays(16), calendarAId));
String calendarABId = "maintenance_a_and_b";
calendars.add(new Calendar(calendarABId, Arrays.asList("job_a", "job_b"), null));
events.add(buildScheduledEvent("downtime_AB", now.plusDays(12), now.plusDays(13), calendarABId));
indexCalendars(calendars);
indexScheduledEvents(events);
List<ScheduledEvent> returnedEvents = getScheduledEvents(new ScheduledEventsQueryBuilder());
assertEquals(4, returnedEvents.size());
assertEquals(events.get(0), returnedEvents.get(0));
assertEquals(events.get(1), returnedEvents.get(1));
assertEquals(events.get(3), returnedEvents.get(2));
assertEquals(events.get(2), returnedEvents.get(3));
returnedEvents = getScheduledEvents(ScheduledEventsQueryBuilder.builder().calendarIds(new String[]{"maintenance_a"}));
assertEquals(3, returnedEvents.size());
assertEquals(events.get(0), returnedEvents.get(0));
assertEquals(events.get(1), returnedEvents.get(1));
assertEquals(events.get(2), returnedEvents.get(2));
returnedEvents = getScheduledEvents(ScheduledEventsQueryBuilder.builder()
.calendarIds(new String[]{"maintenance_a", "maintenance_a_and_b"}));
assertEquals(4, returnedEvents.size());
assertEquals(events.get(0), returnedEvents.get(0));
assertEquals(events.get(1), returnedEvents.get(1));
assertEquals(events.get(3), returnedEvents.get(2));
assertEquals(events.get(2), returnedEvents.get(3));
returnedEvents = getScheduledEvents(ScheduledEventsQueryBuilder.builder()
.calendarIds(new String[]{"maintenance_a*"}));
assertEquals(4, returnedEvents.size());
assertEquals(events.get(0), returnedEvents.get(0));
assertEquals(events.get(1), returnedEvents.get(1));
assertEquals(events.get(3), returnedEvents.get(2));
assertEquals(events.get(2), returnedEvents.get(3));
}
public void testScheduledEventsForJob_withGroup() throws Exception {
String groupA = "group-a";
String groupB = "group-b";
@ -545,6 +644,49 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
.build();
}
public void testGetSnapshots() {
String jobId = "test_get_snapshots";
Job.Builder job = createJob(jobId);
indexModelSnapshot(new ModelSnapshot.Builder(jobId).setSnapshotId("snap_2")
.setTimestamp(Date.from(Instant.ofEpochMilli(10)))
.build());
indexModelSnapshot(new ModelSnapshot.Builder(jobId).setSnapshotId("snap_1")
.setTimestamp(Date.from(Instant.ofEpochMilli(11)))
.build());
indexModelSnapshot(new ModelSnapshot.Builder(jobId).setSnapshotId("other_snap")
.setTimestamp(Date.from(Instant.ofEpochMilli(12)))
.build());
client().admin().indices().prepareRefresh(AnomalyDetectorsIndex.jobStateIndexPattern(),
AnomalyDetectorsIndex.jobResultsAliasedName(jobId)).get();
PlainActionFuture<QueryPage<ModelSnapshot>> future = new PlainActionFuture<>();
jobProvider.modelSnapshots(jobId, 0, 4, "9", "15", "", false, "snap_2,snap_1", future::onResponse, future::onFailure);
List<ModelSnapshot> snapshots = future.actionGet().results();
assertThat(snapshots.get(0).getSnapshotId(), equalTo("snap_2"));
assertThat(snapshots.get(1).getSnapshotId(), equalTo("snap_1"));
future = new PlainActionFuture<>();
jobProvider.modelSnapshots(jobId, 0, 4, "9", "15", "", false, "snap_*", future::onResponse, future::onFailure);
snapshots = future.actionGet().results();
assertThat(snapshots.get(0).getSnapshotId(), equalTo("snap_2"));
assertThat(snapshots.get(1).getSnapshotId(), equalTo("snap_1"));
future = new PlainActionFuture<>();
jobProvider.modelSnapshots(jobId, 0, 4, "9", "15", "", false, "snap_*,other_snap", future::onResponse, future::onFailure);
snapshots = future.actionGet().results();
assertThat(snapshots.get(0).getSnapshotId(), equalTo("snap_2"));
assertThat(snapshots.get(1).getSnapshotId(), equalTo("snap_1"));
assertThat(snapshots.get(2).getSnapshotId(), equalTo("other_snap"));
future = new PlainActionFuture<>();
jobProvider.modelSnapshots(jobId, 0, 4, "9", "15", "", false, "*", future::onResponse, future::onFailure);
snapshots = future.actionGet().results();
assertThat(snapshots.get(0).getSnapshotId(), equalTo("snap_2"));
assertThat(snapshots.get(1).getSnapshotId(), equalTo("snap_1"));
assertThat(snapshots.get(2).getSnapshotId(), equalTo("other_snap"));
}
public void testGetAutodetectParams() throws Exception {
String jobId = "test_get_autodetect_params";
Job.Builder job = createJob(jobId, Arrays.asList("fruit", "tea"));
@ -664,6 +806,27 @@ public class JobResultsProviderIT extends MlSingleNodeTestCase {
return searchResultHolder.get().results();
}
private List<ScheduledEvent> getScheduledEvents(ScheduledEventsQueryBuilder query) throws Exception {
AtomicReference<Exception> errorHolder = new AtomicReference<>();
AtomicReference<QueryPage<ScheduledEvent>> searchResultHolder = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
jobProvider.scheduledEvents(query, ActionListener.wrap(
params -> {
searchResultHolder.set(params);
latch.countDown();
}, e -> {
errorHolder.set(e);
latch.countDown();
}));
latch.await();
if (errorHolder.get() != null) {
throw errorHolder.get();
}
return searchResultHolder.get().results();
}
private Job.Builder createJob(String jobId) {
return createJob(jobId, Collections.emptyList());
}

View File

@ -54,11 +54,11 @@ import org.elasticsearch.xpack.core.ml.job.results.ForecastRequestStats;
import org.elasticsearch.xpack.core.ml.job.results.ForecastRequestStats.ForecastRequestStatus;
import org.elasticsearch.xpack.core.ml.job.results.Result;
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
import org.elasticsearch.xpack.ml.utils.QueryBuilderHelper;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
@ -96,22 +96,21 @@ public class TransportDeleteForecastAction extends HandledTransportAction<Delete
protected void doExecute(Task task, DeleteForecastAction.Request request, ActionListener<AcknowledgedResponse> listener) {
final String jobId = request.getJobId();
String forecastsExpression = request.getForecastId();
final String[] forecastIds = Strings.tokenizeToStringArray(forecastsExpression, ",");
final String forecastsExpression = request.getForecastId();
final String[] forecastIds = Strings.splitStringByCommaToArray(forecastsExpression);
ActionListener<SearchResponse> forecastStatsHandler = ActionListener.wrap(
searchResponse -> deleteForecasts(searchResponse, request, listener),
e -> listener.onFailure(new ElasticsearchException("An error occurred while searching forecasts to delete", e)));
SearchSourceBuilder source = new SearchSourceBuilder();
BoolQueryBuilder builder = QueryBuilders.boolQuery();
BoolQueryBuilder innerBool = QueryBuilders.boolQuery().must(
QueryBuilders.termQuery(Result.RESULT_TYPE.getPreferredName(), ForecastRequestStats.RESULT_TYPE_VALUE));
if (Strings.isAllOrWildcard(forecastIds) == false) {
innerBool.must(QueryBuilders.termsQuery(Forecast.FORECAST_ID.getPreferredName(), new HashSet<>(Arrays.asList(forecastIds))));
}
source.query(builder.filter(innerBool));
BoolQueryBuilder builder = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery(Result.RESULT_TYPE.getPreferredName(), ForecastRequestStats.RESULT_TYPE_VALUE));
QueryBuilderHelper
.buildTokenFilterQuery(Forecast.FORECAST_ID.getPreferredName(), forecastIds)
.ifPresent(builder::filter);
source.query(builder);
SearchRequest searchRequest = new SearchRequest(AnomalyDetectorsIndex.jobResultsAliasedName(jobId));
searchRequest.source(source);
@ -143,7 +142,7 @@ public class TransportDeleteForecastAction extends HandledTransportAction<Delete
}
if (forecastsToDelete.isEmpty()) {
if (Strings.isAllOrWildcard(new String[]{request.getForecastId()}) &&
if (Strings.isAllOrWildcard(request.getForecastId()) &&
request.isAllowNoForecasts()) {
listener.onResponse(new AcknowledgedResponse(true));
} else {

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ml.action;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ResourceNotFoundException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
@ -90,7 +91,7 @@ public class TransportDeleteModelSnapshotAction extends HandledTransportAction<D
deleteCandidate.getSnapshotId(), deleteCandidate.getDescription());
auditor.info(request.getJobId(), msg);
logger.debug("[{}] {}", request.getJobId(), msg);
logger.debug(() -> new ParameterizedMessage("[{}] {}", request.getJobId(), msg));
// We don't care about the bulk response, just that it succeeded
listener.onResponse(new AcknowledgedResponse(true));
}

View File

@ -17,6 +17,7 @@ import org.elasticsearch.xpack.core.ml.action.GetCalendarEventsAction;
import org.elasticsearch.xpack.core.ml.calendars.ScheduledEvent;
import org.elasticsearch.xpack.core.ml.job.config.Job;
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
import org.elasticsearch.xpack.ml.job.persistence.CalendarQueryBuilder;
import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider;
import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider;
import org.elasticsearch.xpack.ml.job.persistence.ScheduledEventsQueryBuilder;
@ -41,17 +42,15 @@ public class TransportGetCalendarEventsAction extends HandledTransportAction<Get
@Override
protected void doExecute(Task task, GetCalendarEventsAction.Request request,
ActionListener<GetCalendarEventsAction.Response> listener) {
final String[] calendarId = Strings.splitStringByCommaToArray(request.getCalendarId());
ActionListener<Boolean> calendarExistsListener = ActionListener.wrap(
r -> {
ScheduledEventsQueryBuilder query = new ScheduledEventsQueryBuilder()
.start(request.getStart())
.end(request.getEnd())
.from(request.getPageParams().getFrom())
.size(request.getPageParams().getSize());
if (Strings.isAllOrWildcard(request.getCalendarId()) == false) {
query.calendarIds(Collections.singletonList(request.getCalendarId()));
}
.start(request.getStart())
.end(request.getEnd())
.from(request.getPageParams().getFrom())
.size(request.getPageParams().getSize())
.calendarIds(calendarId);
ActionListener<QueryPage<ScheduledEvent>> eventsListener = ActionListener.wrap(
events -> {
@ -63,8 +62,8 @@ public class TransportGetCalendarEventsAction extends HandledTransportAction<Get
if (request.getJobId() != null) {
jobConfigProvider.getJob(request.getJobId(), ActionListener.wrap(
jobBuiler -> {
Job job = jobBuiler.build();
jobBuilder -> {
Job job = jobBuilder.build();
jobResultsProvider.scheduledEventsForJob(request.getJobId(), job.getGroups(), query, eventsListener);
},
@ -74,7 +73,10 @@ public class TransportGetCalendarEventsAction extends HandledTransportAction<Get
groupExists -> {
if (groupExists) {
jobResultsProvider.scheduledEventsForJob(
null, Collections.singletonList(request.getJobId()), query, eventsListener);
null,
Collections.singletonList(request.getJobId()),
query,
eventsListener);
} else {
listener.onFailure(ExceptionsHelper.missingJobException(request.getJobId()));
}
@ -89,16 +91,16 @@ public class TransportGetCalendarEventsAction extends HandledTransportAction<Get
},
listener::onFailure);
checkCalendarExists(request.getCalendarId(), calendarExistsListener);
checkCalendarExists(calendarId, calendarExistsListener);
}
private void checkCalendarExists(String calendarId, ActionListener<Boolean> listener) {
private void checkCalendarExists(String[] calendarId, ActionListener<Boolean> listener) {
if (Strings.isAllOrWildcard(calendarId)) {
listener.onResponse(true);
return;
}
jobResultsProvider.calendar(calendarId, ActionListener.wrap(
jobResultsProvider.calendars(CalendarQueryBuilder.builder().calendarIdTokens(calendarId), ActionListener.wrap(
c -> listener.onResponse(true),
listener::onFailure
));

View File

@ -14,12 +14,9 @@ import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.ml.action.GetCalendarsAction;
import org.elasticsearch.xpack.core.action.util.PageParams;
import org.elasticsearch.xpack.core.action.util.QueryPage;
import org.elasticsearch.xpack.core.ml.calendars.Calendar;
import org.elasticsearch.xpack.ml.job.persistence.CalendarQueryBuilder;
import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider;
import java.util.Collections;
public class TransportGetCalendarsAction extends HandledTransportAction<GetCalendarsAction.Request, GetCalendarsAction.Response> {
@ -34,35 +31,18 @@ public class TransportGetCalendarsAction extends HandledTransportAction<GetCalen
@Override
protected void doExecute(Task task, GetCalendarsAction.Request request, ActionListener<GetCalendarsAction.Response> listener) {
final String calendarId = request.getCalendarId();
if (request.getCalendarId() != null && Strings.isAllOrWildcard(request.getCalendarId()) == false) {
getCalendar(calendarId, listener);
} else {
PageParams pageParams = request.getPageParams();
if (pageParams == null) {
pageParams = PageParams.defaultParams();
}
getCalendars(pageParams, listener);
final String[] calendarIds = Strings.splitStringByCommaToArray(request.getCalendarId());
PageParams pageParams = request.getPageParams();
if (pageParams == null) {
pageParams = PageParams.defaultParams();
}
getCalendars(calendarIds, pageParams, listener);
}
private void getCalendar(String calendarId, ActionListener<GetCalendarsAction.Response> listener) {
jobResultsProvider.calendar(calendarId, ActionListener.wrap(
calendar -> {
QueryPage<Calendar> page = new QueryPage<>(Collections.singletonList(calendar), 1, Calendar.RESULTS_FIELD);
listener.onResponse(new GetCalendarsAction.Response(page));
},
listener::onFailure
));
}
private void getCalendars(PageParams pageParams, ActionListener<GetCalendarsAction.Response> listener) {
CalendarQueryBuilder query = new CalendarQueryBuilder().pageParams(pageParams).sort(true);
private void getCalendars(String[] idTokens, PageParams pageParams, ActionListener<GetCalendarsAction.Response> listener) {
CalendarQueryBuilder query = new CalendarQueryBuilder().pageParams(pageParams).calendarIdTokens(idTokens).sort(true);
jobResultsProvider.calendars(query, ActionListener.wrap(
calendars -> {
listener.onResponse(new GetCalendarsAction.Response(calendars));
},
calendars -> listener.onResponse(new GetCalendarsAction.Response(calendars)),
listener::onFailure
));
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ml.action;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
@ -40,10 +41,17 @@ public class TransportGetModelSnapshotsAction extends HandledTransportAction<Get
@Override
protected void doExecute(Task task, GetModelSnapshotsAction.Request request,
ActionListener<GetModelSnapshotsAction.Response> listener) {
logger.debug("Get model snapshots for job {} snapshot ID {}. from = {}, size = {}"
+ " start = '{}', end='{}', sort={} descending={}",
request.getJobId(), request.getSnapshotId(), request.getPageParams().getFrom(), request.getPageParams().getSize(),
request.getStart(), request.getEnd(), request.getSort(), request.getDescOrder());
logger.debug(
() -> new ParameterizedMessage(
"Get model snapshots for job {} snapshot ID {}. from = {}, size = {} start = '{}', end='{}', sort={} descending={}",
request.getJobId(),
request.getSnapshotId(),
request.getPageParams().getFrom(),
request.getPageParams().getSize(),
request.getStart(),
request.getEnd(),
request.getSort(),
request.getDescOrder()));
jobManager.jobExists(request.getJobId(), ActionListener.wrap(
ok -> {

View File

@ -5,13 +5,16 @@
*/
package org.elasticsearch.xpack.ml.job.persistence;
import org.elasticsearch.ResourceNotFoundException;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.common.Strings;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermsQueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.xpack.core.action.util.PageParams;
import org.elasticsearch.xpack.core.ml.calendars.Calendar;
import org.elasticsearch.xpack.ml.utils.QueryBuilderHelper;
import java.util.ArrayList;
import java.util.Collections;
@ -23,6 +26,11 @@ public class CalendarQueryBuilder {
private String jobId;
private List<String> jobGroups = Collections.emptyList();
private boolean sort = false;
private String[] idTokens = new String[0];
public static CalendarQueryBuilder builder() {
return new CalendarQueryBuilder();
}
/**
* Page the query result
@ -49,6 +57,19 @@ public class CalendarQueryBuilder {
return this;
}
public CalendarQueryBuilder calendarIdTokens(String[] idTokens) {
this.idTokens = idTokens;
return this;
}
public boolean isForAllCalendars() {
return Strings.isAllOrWildcard(idTokens);
}
public Exception buildNotFoundException() {
return new ResourceNotFoundException("No calendar with id [" + Strings.arrayToCommaDelimitedString(idTokens) + "]");
}
/**
* Sort results by calendar_id
* @param sort Sort if true
@ -60,7 +81,8 @@ public class CalendarQueryBuilder {
}
public SearchSourceBuilder build() {
QueryBuilder qb;
BoolQueryBuilder qb = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery(Calendar.TYPE.getPreferredName(), Calendar.CALENDAR_TYPE));
List<String> jobIdAndGroups = new ArrayList<>(jobGroups);
if (jobId != null) {
jobIdAndGroups.add(jobId);
@ -68,12 +90,11 @@ public class CalendarQueryBuilder {
if (jobIdAndGroups.isEmpty() == false) {
jobIdAndGroups.add(Metadata.ALL);
qb = new BoolQueryBuilder()
.filter(new TermsQueryBuilder(Calendar.TYPE.getPreferredName(), Calendar.CALENDAR_TYPE))
.filter(new TermsQueryBuilder(Calendar.JOB_IDS.getPreferredName(), jobIdAndGroups));
} else {
qb = new TermsQueryBuilder(Calendar.TYPE.getPreferredName(), Calendar.CALENDAR_TYPE);
qb.filter(new TermsQueryBuilder(Calendar.JOB_IDS.getPreferredName(), jobIdAndGroups));
}
QueryBuilderHelper
.buildTokenFilterQuery(Calendar.ID.getPreferredName(), idTokens)
.ifPresent(qb::filter);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder().query(qb);

View File

@ -1037,12 +1037,12 @@ public class JobResultsProvider {
String snapshotId,
Consumer<QueryPage<ModelSnapshot>> handler,
Consumer<Exception> errorHandler) {
ResultsFilterBuilder fb = new ResultsFilterBuilder();
if (snapshotId != null && !snapshotId.isEmpty()) {
fb.term(ModelSnapshotField.SNAPSHOT_ID.getPreferredName(), snapshotId);
}
String[] snapshotIds = Strings.splitStringByCommaToArray(snapshotId);
QueryBuilder qb = new ResultsFilterBuilder()
.resourceTokenFilters(ModelSnapshotField.SNAPSHOT_ID.getPreferredName(), snapshotIds)
.timeRange(Result.TIMESTAMP.getPreferredName(), startEpochMs, endEpochMs)
.build();
QueryBuilder qb = fb.timeRange(Result.TIMESTAMP.getPreferredName(), startEpochMs, endEpochMs).build();
modelSnapshots(jobId, from, size, sortField, sortDescending, qb, handler, errorHandler);
}
@ -1298,7 +1298,7 @@ public class JobResultsProvider {
handler.onResponse(new QueryPage<>(Collections.emptyList(), 0, ScheduledEvent.RESULTS_FIELD));
return;
}
List<String> calendarIds = calendars.results().stream().map(Calendar::getId).collect(Collectors.toList());
String[] calendarIds = calendars.results().stream().map(Calendar::getId).toArray(String[]::new);
queryBuilder.calendarIds(calendarIds);
scheduledEvents(queryBuilder, handler);
},
@ -1487,6 +1487,10 @@ public class JobResultsProvider {
List<Calendar> calendars = new ArrayList<>();
SearchHit[] hits = response.getHits().getHits();
try {
if (queryBuilder.isForAllCalendars() == false && hits.length == 0) {
listener.onFailure(queryBuilder.buildNotFoundException());
return;
}
for (SearchHit hit : hits) {
calendars.add(MlParserUtils.parse(hit, Calendar.LENIENT_PARSER).build());
}

View File

@ -12,6 +12,7 @@ import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.xpack.core.ml.job.results.Result;
import org.elasticsearch.xpack.ml.utils.QueryBuilderHelper;
import java.util.ArrayList;
import java.util.List;
@ -88,6 +89,11 @@ public class ResultsFilterBuilder {
return this;
}
public ResultsFilterBuilder resourceTokenFilters(String fieldName, String[] tokens) {
QueryBuilderHelper.buildTokenFilterQuery(fieldName, tokens).ifPresent(this::addQuery);
return this;
}
public ResultsFilterBuilder resultType(String resultType) {
return term(Result.RESULT_TYPE.getPreferredName(), resultType);
}

View File

@ -6,16 +6,13 @@
package org.elasticsearch.xpack.ml.job.persistence;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.TermsQueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.xpack.core.ml.calendars.Calendar;
import org.elasticsearch.xpack.core.ml.calendars.ScheduledEvent;
import org.elasticsearch.xpack.ml.utils.QueryBuilderHelper;
import java.util.ArrayList;
import java.util.List;
/**
* Query builder for {@link ScheduledEvent}s
@ -27,11 +24,15 @@ public class ScheduledEventsQueryBuilder {
private Integer from = 0;
private Integer size = DEFAULT_SIZE;
private List<String> calendarIds;
private String[] calendarIds;
private String start;
private String end;
public ScheduledEventsQueryBuilder calendarIds(List<String> calendarIds) {
public static ScheduledEventsQueryBuilder builder() {
return new ScheduledEventsQueryBuilder();
}
public ScheduledEventsQueryBuilder calendarIds(String[] calendarIds) {
this.calendarIds = calendarIds;
return this;
}
@ -72,46 +73,31 @@ public class ScheduledEventsQueryBuilder {
}
public SearchSourceBuilder build() {
List<QueryBuilder> queries = new ArrayList<>();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery(ScheduledEvent.TYPE.getPreferredName(), ScheduledEvent.SCHEDULED_EVENT_TYPE));
if (start != null) {
RangeQueryBuilder startQuery = QueryBuilders.rangeQuery(ScheduledEvent.END_TIME.getPreferredName());
startQuery.gt(start);
queries.add(startQuery);
boolQueryBuilder.filter(startQuery);
}
if (end != null) {
RangeQueryBuilder endQuery = QueryBuilders.rangeQuery(ScheduledEvent.START_TIME.getPreferredName());
endQuery.lt(end);
queries.add(endQuery);
boolQueryBuilder.filter(endQuery);
}
if (calendarIds != null && calendarIds.isEmpty() == false) {
queries.add(new TermsQueryBuilder(Calendar.ID.getPreferredName(), calendarIds));
}
QueryBuilderHelper.buildTokenFilterQuery(Calendar.ID.getPreferredName(), calendarIds).ifPresent(boolQueryBuilder::filter);
QueryBuilder typeQuery = new TermsQueryBuilder(ScheduledEvent.TYPE.getPreferredName(), ScheduledEvent.SCHEDULED_EVENT_TYPE);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.sort(ScheduledEvent.START_TIME.getPreferredName());
searchSourceBuilder.sort(ScheduledEvent.DESCRIPTION.getPreferredName());
SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource()
.sort(ScheduledEvent.START_TIME.getPreferredName())
.sort(ScheduledEvent.DESCRIPTION.getPreferredName())
.query(boolQueryBuilder);
if (from != null) {
searchSourceBuilder.from(from);
}
if (size != null) {
searchSourceBuilder.size(size);
}
if (queries.isEmpty()) {
searchSourceBuilder.query(typeQuery);
} else {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.filter(typeQuery);
for (QueryBuilder query : queries) {
boolQueryBuilder.filter(query);
}
searchSourceBuilder.query(boolQueryBuilder);
}
return searchSourceBuilder;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ml.utils;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.TermsQueryBuilder;
import org.elasticsearch.index.query.WildcardQueryBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class QueryBuilderHelper {
private QueryBuilderHelper() { }
/**
* Helper function for adding OR type queries for a given identity field.
*
* The filter consists of should clauses (i.e. "or" boolean queries).
*
* - When a token is a wildcard token, a wildcard query is added
* - When a token is NOT a wildcard, a term query is added
*
* @param identityField The field to query for the tokens
* @param tokens A non-null collection of tokens. Can include wildcards
* @return An optional boolean query builder filled with "should" queries for the supplied tokens and identify field
*/
public static Optional<QueryBuilder> buildTokenFilterQuery(String identityField, String[] tokens) {
if (Strings.isAllOrWildcard(tokens)) {
return Optional.empty();
}
BoolQueryBuilder shouldQueries = new BoolQueryBuilder();
List<String> terms = new ArrayList<>();
for (String token : tokens) {
if (Regex.isSimpleMatchPattern(token)) {
shouldQueries.should(new WildcardQueryBuilder(identityField, token));
} else {
terms.add(token);
}
}
if (terms.isEmpty() == false) {
shouldQueries.should(new TermsQueryBuilder(identityField, terms));
}
if (shouldQueries.should().isEmpty()) {
return Optional.empty();
}
return Optional.of(shouldQueries);
}
}

View File

@ -1,5 +1,9 @@
---
"Test calendar CRUD":
- do:
ml.get_calendars:
calendar_id: _all
- match: { count: 0 }
- do:
ml.put_job: