Skip to content

Commit ccdc95d

Browse files
committed
[ML] Allow datafeed and job configs for datafeed preview API (elastic#70836)
Previously, a datafeed and job must already exist for the `_preview` API to work. With this change, users can get an accurate preview of the data that will be sent to the anomaly detection job without creating either of them. closes elastic#70264
1 parent 766bb76 commit ccdc95d

File tree

13 files changed

+595
-60
lines changed

13 files changed

+595
-60
lines changed

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -326,14 +326,18 @@ static Request getDatafeedStats(GetDatafeedStatsRequest getDatafeedStatsRequest)
326326
return request;
327327
}
328328

329-
static Request previewDatafeed(PreviewDatafeedRequest previewDatafeedRequest) {
330-
String endpoint = new EndpointBuilder()
329+
static Request previewDatafeed(PreviewDatafeedRequest previewDatafeedRequest) throws IOException {
330+
EndpointBuilder builder = new EndpointBuilder()
331331
.addPathPartAsIs("_ml")
332-
.addPathPartAsIs("datafeeds")
333-
.addPathPart(previewDatafeedRequest.getDatafeedId())
334-
.addPathPartAsIs("_preview")
335-
.build();
336-
return new Request(HttpGet.METHOD_NAME, endpoint);
332+
.addPathPartAsIs("datafeeds");
333+
String endpoint = previewDatafeedRequest.getDatafeedId() != null ?
334+
builder.addPathPart(previewDatafeedRequest.getDatafeedId()).addPathPartAsIs("_preview").build() :
335+
builder.addPathPartAsIs("_preview").build();
336+
Request request = new Request(HttpPost.METHOD_NAME, endpoint);
337+
if (previewDatafeedRequest.getDatafeedId() == null) {
338+
request.setEntity(createEntity(previewDatafeedRequest, REQUEST_BODY_CONTENT_TYPE));
339+
}
340+
return request;
337341
}
338342

339343
static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) {

client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PreviewDatafeedRequest.java

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import org.elasticsearch.action.ActionRequest;
1111
import org.elasticsearch.action.ActionRequestValidationException;
1212
import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
13+
import org.elasticsearch.client.ml.job.config.Job;
14+
import org.elasticsearch.common.Nullable;
15+
import org.elasticsearch.common.ParseField;
1316
import org.elasticsearch.common.Strings;
1417
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
1518
import org.elasticsearch.common.xcontent.ToXContentObject;
@@ -24,18 +27,34 @@
2427
*/
2528
public class PreviewDatafeedRequest extends ActionRequest implements ToXContentObject {
2629

30+
private static final ParseField DATAFEED_CONFIG = new ParseField("datafeed_config");
31+
private static final ParseField JOB_CONFIG = new ParseField("job_config");
32+
2733
public static final ConstructingObjectParser<PreviewDatafeedRequest, Void> PARSER = new ConstructingObjectParser<>(
28-
"open_datafeed_request", true, a -> new PreviewDatafeedRequest((String) a[0]));
34+
"preview_datafeed_request",
35+
a -> new PreviewDatafeedRequest((String) a[0], (DatafeedConfig.Builder) a[1], (Job.Builder) a[2]));
2936

3037
static {
31-
PARSER.declareString(ConstructingObjectParser.constructorArg(), DatafeedConfig.ID);
38+
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), DatafeedConfig.ID);
39+
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), DatafeedConfig.PARSER, DATAFEED_CONFIG);
40+
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), Job.PARSER, JOB_CONFIG);
3241
}
3342

3443
public static PreviewDatafeedRequest fromXContent(XContentParser parser) throws IOException {
3544
return PARSER.parse(parser, null);
3645
}
3746

3847
private final String datafeedId;
48+
private final DatafeedConfig datafeedConfig;
49+
private final Job jobConfig;
50+
51+
private PreviewDatafeedRequest(@Nullable String datafeedId,
52+
@Nullable DatafeedConfig.Builder datafeedConfig,
53+
@Nullable Job.Builder jobConfig) {
54+
this.datafeedId = datafeedId;
55+
this.datafeedConfig = datafeedConfig == null ? null : datafeedConfig.build();
56+
this.jobConfig = jobConfig == null ? null : jobConfig.build();
57+
}
3958

4059
/**
4160
* Create a new request with the desired datafeedId
@@ -44,12 +63,33 @@ public static PreviewDatafeedRequest fromXContent(XContentParser parser) throws
4463
*/
4564
public PreviewDatafeedRequest(String datafeedId) {
4665
this.datafeedId = Objects.requireNonNull(datafeedId, "[datafeed_id] must not be null");
66+
this.datafeedConfig = null;
67+
this.jobConfig = null;
68+
}
69+
70+
/**
71+
* Create a new request to preview the provided datafeed config and optional job config
72+
* @param datafeedConfig The datafeed to preview
73+
* @param jobConfig The associated job config (required if the datafeed does not refer to an existing job)
74+
*/
75+
public PreviewDatafeedRequest(DatafeedConfig datafeedConfig, Job jobConfig) {
76+
this.datafeedId = null;
77+
this.datafeedConfig = datafeedConfig;
78+
this.jobConfig = jobConfig;
4779
}
4880

4981
public String getDatafeedId() {
5082
return datafeedId;
5183
}
5284

85+
public DatafeedConfig getDatafeedConfig() {
86+
return datafeedConfig;
87+
}
88+
89+
public Job getJobConfig() {
90+
return jobConfig;
91+
}
92+
5393
@Override
5494
public ActionRequestValidationException validate() {
5595
return null;
@@ -58,7 +98,15 @@ public ActionRequestValidationException validate() {
5898
@Override
5999
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
60100
builder.startObject();
61-
builder.field(DatafeedConfig.ID.getPreferredName(), datafeedId);
101+
if (datafeedId != null) {
102+
builder.field(DatafeedConfig.ID.getPreferredName(), datafeedId);
103+
}
104+
if (datafeedConfig != null) {
105+
builder.field(DATAFEED_CONFIG.getPreferredName(), datafeedConfig);
106+
}
107+
if (jobConfig != null) {
108+
builder.field(JOB_CONFIG.getPreferredName(), jobConfig);
109+
}
62110
builder.endObject();
63111
return builder;
64112
}
@@ -70,7 +118,7 @@ public String toString() {
70118

71119
@Override
72120
public int hashCode() {
73-
return Objects.hash(datafeedId);
121+
return Objects.hash(datafeedId, datafeedConfig, jobConfig);
74122
}
75123

76124
@Override
@@ -84,6 +132,8 @@ public boolean equals(Object other) {
84132
}
85133

86134
PreviewDatafeedRequest that = (PreviewDatafeedRequest) other;
87-
return Objects.equals(datafeedId, that.datafeedId);
135+
return Objects.equals(datafeedId, that.datafeedId)
136+
&& Objects.equals(datafeedConfig, that.datafeedConfig)
137+
&& Objects.equals(jobConfig, that.jobConfig);
88138
}
89139
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import org.elasticsearch.client.ml.job.config.AnalysisConfig;
9292
import org.elasticsearch.client.ml.job.config.Detector;
9393
import org.elasticsearch.client.ml.job.config.Job;
94+
import org.elasticsearch.client.ml.job.config.JobTests;
9495
import org.elasticsearch.client.ml.job.config.JobUpdate;
9596
import org.elasticsearch.client.ml.job.config.JobUpdateTests;
9697
import org.elasticsearch.client.ml.job.config.MlFilter;
@@ -390,11 +391,24 @@ public void testGetDatafeedStats() {
390391
assertEquals(Boolean.toString(true), request.getParameters().get("allow_no_match"));
391392
}
392393

393-
public void testPreviewDatafeed() {
394+
public void testPreviewDatafeed() throws IOException {
394395
PreviewDatafeedRequest datafeedRequest = new PreviewDatafeedRequest("datafeed_1");
395396
Request request = MLRequestConverters.previewDatafeed(datafeedRequest);
396-
assertEquals(HttpGet.METHOD_NAME, request.getMethod());
397+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
397398
assertEquals("/_ml/datafeeds/" + datafeedRequest.getDatafeedId() + "/_preview", request.getEndpoint());
399+
assertThat(request.getEntity(), is(nullValue()));
400+
401+
datafeedRequest = new PreviewDatafeedRequest(
402+
DatafeedConfigTests.createRandom(),
403+
randomBoolean() ? null : JobTests.createRandomizedJob()
404+
);
405+
request = MLRequestConverters.previewDatafeed(datafeedRequest);
406+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
407+
assertEquals("/_ml/datafeeds/_preview", request.getEndpoint());
408+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) {
409+
PreviewDatafeedRequest parsedDatafeedRequest = PreviewDatafeedRequest.PARSER.apply(parser, null);
410+
assertThat(parsedDatafeedRequest, equalTo(datafeedRequest));
411+
}
398412
}
399413

400414
public void testDeleteForecast() {

client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PreviewDatafeedRequestTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.client.ml;
99

1010
import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests;
11+
import org.elasticsearch.client.ml.job.config.JobTests;
1112
import org.elasticsearch.common.xcontent.XContentParser;
1213
import org.elasticsearch.test.AbstractXContentTestCase;
1314

@@ -17,7 +18,9 @@ public class PreviewDatafeedRequestTests extends AbstractXContentTestCase<Previe
1718

1819
@Override
1920
protected PreviewDatafeedRequest createTestInstance() {
20-
return new PreviewDatafeedRequest(DatafeedConfigTests.randomValidDatafeedId());
21+
return randomBoolean() ?
22+
new PreviewDatafeedRequest(DatafeedConfigTests.randomValidDatafeedId()) :
23+
new PreviewDatafeedRequest(DatafeedConfigTests.createRandom(), randomBoolean() ? null : JobTests.createRandomizedJob());
2124
}
2225

2326
@Override
@@ -27,6 +30,6 @@ protected PreviewDatafeedRequest doParseInstance(XContentParser parser) throws I
2730

2831
@Override
2932
protected boolean supportsUnknownFields() {
30-
return true;
33+
return false;
3134
}
3235
}

docs/reference/ml/anomaly-detection/apis/preview-datafeed.asciidoc

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ Previews a {dfeed}.
1313
[[ml-preview-datafeed-request]]
1414
== {api-request-title}
1515

16-
`GET _ml/datafeeds/<datafeed_id>/_preview`
16+
`GET _ml/datafeeds/<datafeed_id>/_preview` +
17+
18+
`POST _ml/datafeeds/<datafeed_id>/_preview` +
19+
20+
`GET _ml/datafeeds/_preview` +
21+
22+
`POST _ml/datafeeds/_preview`
1723

1824
[[ml-preview-datafeed-prereqs]]
1925
== {api-prereq-title}
@@ -25,9 +31,10 @@ Previews a {dfeed}.
2531
[[ml-preview-datafeed-desc]]
2632
== {api-description-title}
2733

28-
The preview {dfeeds} API returns the first "page" of results from the `search`
29-
that is created by using the current {dfeed} settings. This preview shows the
30-
structure of the data that will be passed to the anomaly detection engine.
34+
The preview {dfeeds} API returns the first "page" of search results from a
35+
{dfeed}. You can preview an existing {dfeed} or provide configuration details
36+
for the {dfeed} and {anomaly-job} in the API. The preview shows the structure of
37+
the data that will be passed to the anomaly detection engine.
3138

3239
IMPORTANT: When {es} {security-features} are enabled, the {dfeed} query is
3340
previewed using the credentials of the user calling the preview {dfeed} API.
@@ -43,12 +50,32 @@ supply the credentials.
4350
== {api-path-parms-title}
4451

4552
`<datafeed_id>`::
46-
(Required, string)
53+
(Optional, string)
4754
include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=datafeed-id]
55+
+
56+
NOTE: If you provide the `<datafeed_id>` as a path parameter, you cannot
57+
provide {dfeed} or {anomaly-job} configuration details in the request body.
58+
59+
[[ml-preview-datafeed-request-body]]
60+
== {api-request-body-title}
61+
62+
`datafeed_config`::
63+
(Optional, object) The {dfeed} definition to preview. For valid definitions, see
64+
the <<ml-put-datafeed-request-body,create {dfeeds} API>>.
65+
66+
`job_config`::
67+
(Optional, object) The configuration details for the {anomaly-job} that is
68+
associated with the {dfeed}. If the `datafeed_config` object does not include a
69+
`job_id` that references an existing {anomaly-job}, you must supply this
70+
`job_config` object. If you include both a `job_id` and a `job_config`, the
71+
latter information is used. You cannot specify a `job_config` object unless you also supply a `datafeed_config` object. For valid definitions, see the
72+
<<ml-put-job-request-body,create {anomaly-jobs} API>>.
4873

4974
[[ml-preview-datafeed-example]]
5075
== {api-examples-title}
5176

77+
This is an example of providing the ID of an existing {dfeed}:
78+
5279
[source,console]
5380
--------------------------------------------------
5481
GET _ml/datafeeds/datafeed-high_sum_total_sales/_preview
@@ -87,3 +114,88 @@ The data that is returned for this example is as follows:
87114
...
88115
]
89116
----
117+
118+
The following example provides {dfeed} and {anomaly-job} configuration
119+
details in the API:
120+
121+
[source,console]
122+
--------------------------------------------------
123+
POST _ml/datafeeds/_preview
124+
{
125+
"datafeed_config": {
126+
"indices" : [
127+
"kibana_sample_data_ecommerce"
128+
],
129+
"query" : {
130+
"bool" : {
131+
"filter" : [
132+
{
133+
"term" : {
134+
"_index" : "kibana_sample_data_ecommerce"
135+
}
136+
}
137+
]
138+
}
139+
},
140+
"scroll_size" : 1000
141+
},
142+
"job_config": {
143+
"description" : "Find customers spending an unusually high amount in an hour",
144+
"analysis_config" : {
145+
"bucket_span" : "1h",
146+
"detectors" : [
147+
{
148+
"detector_description" : "High total sales",
149+
"function" : "high_sum",
150+
"field_name" : "taxful_total_price",
151+
"over_field_name" : "customer_full_name.keyword"
152+
}
153+
],
154+
"influencers" : [
155+
"customer_full_name.keyword",
156+
"category.keyword"
157+
]
158+
},
159+
"analysis_limits" : {
160+
"model_memory_limit" : "10mb"
161+
},
162+
"data_description" : {
163+
"time_field" : "order_date",
164+
"time_format" : "epoch_ms"
165+
}
166+
}
167+
}
168+
--------------------------------------------------
169+
// TEST[skip:set up Kibana sample data]
170+
171+
The data that is returned for this example is as follows:
172+
173+
[source,console-result]
174+
----
175+
[
176+
{
177+
"order_date" : 1574294659000,
178+
"category.keyword" : "Men's Clothing",
179+
"customer_full_name.keyword" : "Sultan Al Benson",
180+
"taxful_total_price" : 35.96875
181+
},
182+
{
183+
"order_date" : 1574294918000,
184+
"category.keyword" : [
185+
"Women's Accessories",
186+
"Women's Clothing"
187+
],
188+
"customer_full_name.keyword" : "Pia Webb",
189+
"taxful_total_price" : 83.0
190+
},
191+
{
192+
"order_date" : 1574295782000,
193+
"category.keyword" : [
194+
"Women's Accessories",
195+
"Women's Shoes"
196+
],
197+
"customer_full_name.keyword" : "Brigitte Graham",
198+
"taxful_total_price" : 72.0
199+
}
200+
]
201+
----

0 commit comments

Comments
 (0)