Skip to content

Commit 4d23310

Browse files
authored
HLRC: ML Forecast Job (#33506)
* HLRC: ML Forecast job
1 parent 42469a9 commit 4d23310

File tree

11 files changed

+585
-0
lines changed

11 files changed

+585
-0
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.elasticsearch.client.ml.CloseJobRequest;
3131
import org.elasticsearch.client.ml.DeleteJobRequest;
3232
import org.elasticsearch.client.ml.FlushJobRequest;
33+
import org.elasticsearch.client.ml.ForecastJobRequest;
3334
import org.elasticsearch.client.ml.GetBucketsRequest;
3435
import org.elasticsearch.client.ml.GetInfluencersRequest;
3536
import org.elasticsearch.client.ml.GetJobRequest;
@@ -153,6 +154,19 @@ static Request flushJob(FlushJobRequest flushJobRequest) throws IOException {
153154
return request;
154155
}
155156

157+
static Request forecastJob(ForecastJobRequest forecastJobRequest) throws IOException {
158+
String endpoint = new EndpointBuilder()
159+
.addPathPartAsIs("_xpack")
160+
.addPathPartAsIs("ml")
161+
.addPathPartAsIs("anomaly_detectors")
162+
.addPathPart(forecastJobRequest.getJobId())
163+
.addPathPartAsIs("_forecast")
164+
.build();
165+
Request request = new Request(HttpPost.METHOD_NAME, endpoint);
166+
request.setEntity(createEntity(forecastJobRequest, REQUEST_BODY_CONTENT_TYPE));
167+
return request;
168+
}
169+
156170
static Request updateJob(UpdateJobRequest updateJobRequest) throws IOException {
157171
String endpoint = new EndpointBuilder()
158172
.addPathPartAsIs("_xpack")

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.elasticsearch.client;
2020

2121
import org.elasticsearch.action.ActionListener;
22+
import org.elasticsearch.client.ml.ForecastJobRequest;
23+
import org.elasticsearch.client.ml.ForecastJobResponse;
2224
import org.elasticsearch.client.ml.PostDataRequest;
2325
import org.elasticsearch.client.ml.PostDataResponse;
2426
import org.elasticsearch.client.ml.UpdateJobRequest;
@@ -360,6 +362,28 @@ public void flushJobAsync(FlushJobRequest request, RequestOptions options, Actio
360362
Collections.emptySet());
361363
}
362364

365+
/**
366+
* Creates a forecast of an existing, opened Machine Learning Job
367+
*
368+
* This predicts the future behavior of a time series by using its historical behavior.
369+
*
370+
* <p>
371+
* For additional info
372+
* see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-forecast.html">Forecast ML Job Documentation</a>
373+
* </p>
374+
* @param request ForecastJobRequest with forecasting options
375+
* @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
376+
* @return response containing forecast acknowledgement and new forecast's ID
377+
* @throws IOException when there is a serialization issue sending the request or receiving the response
378+
*/
379+
public ForecastJobResponse forecastJob(ForecastJobRequest request, RequestOptions options) throws IOException {
380+
return restHighLevelClient.performRequestAndParseEntity(request,
381+
MLRequestConverters::forecastJob,
382+
options,
383+
ForecastJobResponse::fromXContent,
384+
Collections.emptySet());
385+
}
386+
363387
/**
364388
* Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job}
365389
*
@@ -376,6 +400,28 @@ public PutJobResponse updateJob(UpdateJobRequest request, RequestOptions options
376400
Collections.emptySet());
377401
}
378402

403+
/**
404+
* Creates a forecast of an existing, opened Machine Learning Job asynchronously
405+
*
406+
* This predicts the future behavior of a time series by using its historical behavior.
407+
*
408+
* <p>
409+
* For additional info
410+
* see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-forecast.html">Forecast ML Job Documentation</a>
411+
* </p>
412+
* @param request ForecastJobRequest with forecasting options
413+
* @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
414+
* @param listener Listener to be notified upon request completion
415+
*/
416+
public void forecastJobAsync(ForecastJobRequest request, RequestOptions options, ActionListener<ForecastJobResponse> listener) {
417+
restHighLevelClient.performRequestAsyncAndParseEntity(request,
418+
MLRequestConverters::forecastJob,
419+
options,
420+
ForecastJobResponse::fromXContent,
421+
listener,
422+
Collections.emptySet());
423+
}
424+
379425
/**
380426
* Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} asynchronously
381427
*
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.client.ml;
20+
21+
import org.elasticsearch.action.ActionRequest;
22+
import org.elasticsearch.action.ActionRequestValidationException;
23+
import org.elasticsearch.client.ml.job.config.Job;
24+
import org.elasticsearch.common.ParseField;
25+
import org.elasticsearch.common.unit.TimeValue;
26+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
27+
import org.elasticsearch.common.xcontent.ToXContent;
28+
import org.elasticsearch.common.xcontent.ToXContentObject;
29+
import org.elasticsearch.common.xcontent.XContentBuilder;
30+
31+
import java.io.IOException;
32+
import java.util.Objects;
33+
34+
/**
35+
* Pojo for forecasting an existing and open Machine Learning Job
36+
*/
37+
public class ForecastJobRequest extends ActionRequest implements ToXContentObject {
38+
39+
public static final ParseField DURATION = new ParseField("duration");
40+
public static final ParseField EXPIRES_IN = new ParseField("expires_in");
41+
42+
public static final ConstructingObjectParser<ForecastJobRequest, Void> PARSER =
43+
new ConstructingObjectParser<>("forecast_job_request", (a) -> new ForecastJobRequest((String)a[0]));
44+
45+
static {
46+
PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID);
47+
PARSER.declareString(
48+
(request, val) -> request.setDuration(TimeValue.parseTimeValue(val, DURATION.getPreferredName())), DURATION);
49+
PARSER.declareString(
50+
(request, val) -> request.setExpiresIn(TimeValue.parseTimeValue(val, EXPIRES_IN.getPreferredName())), EXPIRES_IN);
51+
}
52+
53+
private final String jobId;
54+
private TimeValue duration;
55+
private TimeValue expiresIn;
56+
57+
/**
58+
* A new forecast request
59+
*
60+
* @param jobId the non-null, existing, and opened jobId to forecast
61+
*/
62+
public ForecastJobRequest(String jobId) {
63+
this.jobId = jobId;
64+
}
65+
66+
public String getJobId() {
67+
return jobId;
68+
}
69+
70+
public TimeValue getDuration() {
71+
return duration;
72+
}
73+
74+
/**
75+
* Set the forecast duration
76+
*
77+
* A period of time that indicates how far into the future to forecast.
78+
* The default value is 1 day. The forecast starts at the last record that was processed.
79+
*
80+
* @param duration TimeValue for the duration of the forecast
81+
*/
82+
public void setDuration(TimeValue duration) {
83+
this.duration = duration;
84+
}
85+
86+
public TimeValue getExpiresIn() {
87+
return expiresIn;
88+
}
89+
90+
/**
91+
* Set the forecast expiration
92+
*
93+
* The period of time that forecast results are retained.
94+
* After a forecast expires, the results are deleted. The default value is 14 days.
95+
* If set to a value of 0, the forecast is never automatically deleted.
96+
*
97+
* @param expiresIn TimeValue for the forecast expiration
98+
*/
99+
public void setExpiresIn(TimeValue expiresIn) {
100+
this.expiresIn = expiresIn;
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return Objects.hash(jobId, duration, expiresIn);
106+
}
107+
108+
@Override
109+
public boolean equals(Object obj) {
110+
if (this == obj) {
111+
return true;
112+
}
113+
if (obj == null || getClass() != obj.getClass()) {
114+
return false;
115+
}
116+
ForecastJobRequest other = (ForecastJobRequest) obj;
117+
return Objects.equals(jobId, other.jobId)
118+
&& Objects.equals(duration, other.duration)
119+
&& Objects.equals(expiresIn, other.expiresIn);
120+
}
121+
122+
@Override
123+
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
124+
builder.startObject();
125+
builder.field(Job.ID.getPreferredName(), jobId);
126+
if (duration != null) {
127+
builder.field(DURATION.getPreferredName(), duration.getStringRep());
128+
}
129+
if (expiresIn != null) {
130+
builder.field(EXPIRES_IN.getPreferredName(), expiresIn.getStringRep());
131+
}
132+
builder.endObject();
133+
return builder;
134+
}
135+
136+
@Override
137+
public ActionRequestValidationException validate() {
138+
return null;
139+
}
140+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.client.ml;
20+
21+
import org.elasticsearch.action.ActionResponse;
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
24+
import org.elasticsearch.common.xcontent.ToXContentObject;
25+
import org.elasticsearch.common.xcontent.XContentBuilder;
26+
import org.elasticsearch.common.xcontent.XContentParser;
27+
28+
import java.io.IOException;
29+
import java.util.Objects;
30+
31+
/**
32+
* Forecast response object
33+
*/
34+
public class ForecastJobResponse extends ActionResponse implements ToXContentObject {
35+
36+
public static final ParseField ACKNOWLEDGED = new ParseField("acknowledged");
37+
public static final ParseField FORECAST_ID = new ParseField("forecast_id");
38+
39+
public static final ConstructingObjectParser<ForecastJobResponse, Void> PARSER =
40+
new ConstructingObjectParser<>("forecast_job_response",
41+
true,
42+
(a) -> new ForecastJobResponse((Boolean)a[0], (String)a[1]));
43+
44+
static {
45+
PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), ACKNOWLEDGED);
46+
PARSER.declareString(ConstructingObjectParser.constructorArg(), FORECAST_ID);
47+
}
48+
49+
public static ForecastJobResponse fromXContent(XContentParser parser) throws IOException {
50+
return PARSER.parse(parser, null);
51+
}
52+
53+
private final boolean acknowledged;
54+
private final String forecastId;
55+
56+
public ForecastJobResponse(boolean acknowledged, String forecastId) {
57+
this.acknowledged = acknowledged;
58+
this.forecastId = forecastId;
59+
}
60+
61+
/**
62+
* Forecast creating acknowledgement
63+
* @return {@code true} indicates success, {@code false} otherwise
64+
*/
65+
public boolean isAcknowledged() {
66+
return acknowledged;
67+
}
68+
69+
/**
70+
* The created forecast ID
71+
*/
72+
public String getForecastId() {
73+
return forecastId;
74+
}
75+
76+
@Override
77+
public int hashCode() {
78+
return Objects.hash(acknowledged, forecastId);
79+
}
80+
81+
@Override
82+
public boolean equals(Object obj) {
83+
if (this == obj) {
84+
return true;
85+
}
86+
if (obj == null || getClass() != obj.getClass()) {
87+
return false;
88+
}
89+
ForecastJobResponse other = (ForecastJobResponse) obj;
90+
return Objects.equals(acknowledged, other.acknowledged)
91+
&& Objects.equals(forecastId, other.forecastId);
92+
}
93+
94+
@Override
95+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
96+
builder.startObject();
97+
builder.field(ACKNOWLEDGED.getPreferredName(), acknowledged);
98+
builder.field(FORECAST_ID.getPreferredName(), forecastId);
99+
builder.endObject();
100+
return builder;
101+
}
102+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.client.ml.CloseJobRequest;
2727
import org.elasticsearch.client.ml.DeleteJobRequest;
2828
import org.elasticsearch.client.ml.FlushJobRequest;
29+
import org.elasticsearch.client.ml.ForecastJobRequest;
2930
import org.elasticsearch.client.ml.GetBucketsRequest;
3031
import org.elasticsearch.client.ml.GetInfluencersRequest;
3132
import org.elasticsearch.client.ml.GetJobRequest;
@@ -173,6 +174,21 @@ public void testFlushJob() throws Exception {
173174
requestEntityToString(request));
174175
}
175176

177+
public void testForecastJob() throws Exception {
178+
String jobId = randomAlphaOfLength(10);
179+
ForecastJobRequest forecastJobRequest = new ForecastJobRequest(jobId);
180+
181+
forecastJobRequest.setDuration(TimeValue.timeValueHours(10));
182+
forecastJobRequest.setExpiresIn(TimeValue.timeValueHours(12));
183+
Request request = MLRequestConverters.forecastJob(forecastJobRequest);
184+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
185+
assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_forecast", request.getEndpoint());
186+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) {
187+
ForecastJobRequest parsedRequest = ForecastJobRequest.PARSER.apply(parser, null);
188+
assertThat(parsedRequest, equalTo(forecastJobRequest));
189+
}
190+
}
191+
176192
public void testUpdateJob() throws Exception {
177193
String jobId = randomAlphaOfLength(10);
178194
JobUpdate updates = JobUpdateTests.createRandom(jobId);

0 commit comments

Comments
 (0)