diff --git a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc index 3eadef83728..9785e58a1ad 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc @@ -6,7 +6,7 @@ Delete forecast ++++ -Deletes forecasts from a {ml} job. +Deletes forecasts from a {ml} job. [[ml-delete-forecast-request]] == {api-request-title} @@ -27,12 +27,12 @@ Deletes forecasts from a {ml} job. [[ml-delete-forecast-desc]] == {api-description-title} -By default, forecasts are retained for 14 days. You can specify a different +By default, forecasts are retained for 14 days. You can specify a different retention period with the `expires_in` parameter in the <>. The delete forecast API enables you to delete one or more forecasts before they expire. -NOTE: When you delete a job, its associated forecasts are deleted. +NOTE: When you delete a job, its associated forecasts are deleted. For more information, see {ml-docs}/ml-overview.html#ml-forecasting[Forecasting the future]. @@ -42,26 +42,26 @@ For more information, see ``:: (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 -forecasts from the job. - +specify this optional parameter or if you specify `_all` or `*` the API deletes all +forecasts from the job. + ``:: (Required, string) include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] - + [[ml-delete-forecast-query-parms]] == {api-query-parms-title} `allow_no_forecasts`:: (Optional, boolean) Specifies whether an error occurs when there are no - forecasts. In particular, if this parameter is set to `false` and there are no + forecasts. In particular, if this parameter is set to `false` and there are no forecasts associated with the job, attempts to delete all forecasts return an error. The default value is `true`. `timeout`:: - (Optional, <>) Specifies the period of time to wait - for the completion of the delete operation. When this period of time elapses, + (Optional, <>) Specifies the period of time to wait + for the completion of the delete operation. When this period of time elapses, the API fails and returns an error. The default value is `30s`. [[ml-delete-forecast-example]] diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc index 5c0d2f5aed5..3f3516f9e2c 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc @@ -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 ``, or by omitting the ``. For more information, see {ml-docs}/ml-calendars.html[Calendars and scheduled events]. diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc index aaa5787fac0..7b4e5bbe12b 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc @@ -25,10 +25,12 @@ 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 +``, or by omitting the ``. -For more information, see +For more information, see {ml-docs}/ml-calendars.html[Calendars and scheduled events]. [[ml-get-calendar-path-parms]] diff --git a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc index 7ece93cc4b6..5fcc2784c94 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc @@ -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 `` or a wildcard expression. +You can get all snapshots for all calendars by using `_all`, +by specifying `*` as the ``, or by omitting the ``. -- [[ml-get-snapshot-request-body]] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetCalendarEventsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetCalendarEventsAction.java index 095f90e4a50..9c1d17bad02 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetCalendarEventsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetCalendarEventsAction.java @@ -128,7 +128,7 @@ public class GetCalendarEventsAction extends ActionType 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)); } { diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java index 8f46663daa6..0d46a66e187 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java @@ -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 queryResult = getCalendars("ted"); + List 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 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 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 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 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 updatedCalendars = getCalendars(null); + List 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 getCalendars(String jobId) throws Exception { + private List getCalendars(CalendarQueryBuilder query) throws Exception { CountDownLatch latch = new CountDownLatch(1); AtomicReference exceptionHolder = new AtomicReference<>(); AtomicReference> 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 calendars = new ArrayList<>(); + calendars.add(new Calendar(calendarAId, Collections.singletonList("job_a"), null)); + + ZonedDateTime now = ZonedDateTime.now(); + List 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 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> future = new PlainActionFuture<>(); + jobProvider.modelSnapshots(jobId, 0, 4, "9", "15", "", false, "snap_2,snap_1", future::onResponse, future::onFailure); + List 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 getScheduledEvents(ScheduledEventsQueryBuilder query) throws Exception { + AtomicReference errorHolder = new AtomicReference<>(); + AtomicReference> 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()); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java index 57d082e64bf..ea866934f92 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java @@ -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 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 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 new ParameterizedMessage("[{}] {}", request.getJobId(), msg)); // We don't care about the bulk response, just that it succeeded listener.onResponse(new AcknowledgedResponse(true)); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarEventsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarEventsAction.java index b88c80d93bc..516ed012c1a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarEventsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarEventsAction.java @@ -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 listener) { + final String[] calendarId = Strings.splitStringByCommaToArray(request.getCalendarId()); ActionListener 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> eventsListener = ActionListener.wrap( events -> { @@ -63,8 +62,8 @@ public class TransportGetCalendarEventsAction extends HandledTransportAction { - 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 { 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 listener) { + private void checkCalendarExists(String[] calendarId, ActionListener 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 )); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarsAction.java index df346958a5d..194aadb68fb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetCalendarsAction.java @@ -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 { @@ -34,35 +31,18 @@ public class TransportGetCalendarsAction extends HandledTransportAction 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 listener) { - - jobResultsProvider.calendar(calendarId, ActionListener.wrap( - calendar -> { - QueryPage page = new QueryPage<>(Collections.singletonList(calendar), 1, Calendar.RESULTS_FIELD); - listener.onResponse(new GetCalendarsAction.Response(page)); - }, - listener::onFailure - )); - } - - private void getCalendars(PageParams pageParams, ActionListener listener) { - CalendarQueryBuilder query = new CalendarQueryBuilder().pageParams(pageParams).sort(true); + private void getCalendars(String[] idTokens, PageParams pageParams, ActionListener 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 )); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java index 43a42501bd0..b600982159a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java @@ -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 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 -> { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/CalendarQueryBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/CalendarQueryBuilder.java index a338400bb55..58796ac244a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/CalendarQueryBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/CalendarQueryBuilder.java @@ -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 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 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); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java index f96ece95043..6ab99923016 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java @@ -1037,12 +1037,12 @@ public class JobResultsProvider { String snapshotId, Consumer> handler, Consumer 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 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 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()); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ResultsFilterBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ResultsFilterBuilder.java index 745793d9722..287aa9d22a3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ResultsFilterBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ResultsFilterBuilder.java @@ -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); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ScheduledEventsQueryBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ScheduledEventsQueryBuilder.java index 618e680b0a6..c57be5d8959 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ScheduledEventsQueryBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/ScheduledEventsQueryBuilder.java @@ -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 calendarIds; + private String[] calendarIds; private String start; private String end; - public ScheduledEventsQueryBuilder calendarIds(List 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 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; } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/QueryBuilderHelper.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/QueryBuilderHelper.java new file mode 100644 index 00000000000..811fb742ad9 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/QueryBuilderHelper.java @@ -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 buildTokenFilterQuery(String identityField, String[] tokens) { + if (Strings.isAllOrWildcard(tokens)) { + return Optional.empty(); + } + + BoolQueryBuilder shouldQueries = new BoolQueryBuilder(); + List 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); + } + +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml index b4b5c9ad9da..ba85f9d093f 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml @@ -1,5 +1,9 @@ --- "Test calendar CRUD": + - do: + ml.get_calendars: + calendar_id: _all + - match: { count: 0 } - do: ml.put_job: