diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 6d7fefe907fc6..44f23c597c818 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -56,6 +56,7 @@ import org.elasticsearch.client.ml.StartDatafeedRequest; import org.elasticsearch.client.ml.StopDatafeedRequest; import org.elasticsearch.client.ml.UpdateDatafeedRequest; +import org.elasticsearch.client.ml.UpdateFilterRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.Strings; @@ -510,4 +511,17 @@ static Request getFilter(GetFiltersRequest getFiltersRequest) { } return request; } + + static Request updateFilter(UpdateFilterRequest updateFilterRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("filters") + .addPathPart(updateFilterRequest.getFilterId()) + .addPathPartAsIs("_update") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + request.setEntity(createEntity(updateFilterRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index e327f9b2a5812..d977ad791a8d8 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -74,6 +74,7 @@ import org.elasticsearch.client.ml.StopDatafeedRequest; import org.elasticsearch.client.ml.StopDatafeedResponse; import org.elasticsearch.client.ml.UpdateDatafeedRequest; +import org.elasticsearch.client.ml.UpdateFilterRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.job.stats.JobStats; @@ -1288,4 +1289,44 @@ public void getFilterAsync(GetFiltersRequest request, RequestOptions options, Ac listener, Collections.emptySet()); } + + /** + * Updates a Machine Learning Filter + *

+ * For additional info + * see + * ML Update Filter documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return PutFilterResponse with the updated {@link org.elasticsearch.client.ml.job.config.MlFilter} object + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public PutFilterResponse updateFilter(UpdateFilterRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::updateFilter, + options, + PutFilterResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Updates a Machine Learning Filter asynchronously and notifies listener on completion + *

+ * For additional info + * see + * ML Update Filter documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void updateFilterAsync(UpdateFilterRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::updateFilter, + options, + PutFilterResponse::fromXContent, + listener, + Collections.emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/UpdateFilterRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/UpdateFilterRequest.java new file mode 100644 index 0000000000000..fb7d06a552514 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/UpdateFilterRequest.java @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.job.config.MlFilter; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collection; +import java.util.Objects; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Updates an existing {@link MlFilter} configuration + */ +public class UpdateFilterRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField ADD_ITEMS = new ParseField("add_items"); + public static final ParseField REMOVE_ITEMS = new ParseField("remove_items"); + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("update_filter_request", (a) -> new UpdateFilterRequest((String)a[0])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), MlFilter.ID); + PARSER.declareStringOrNull(UpdateFilterRequest::setDescription, MlFilter.DESCRIPTION); + PARSER.declareStringArray(UpdateFilterRequest::setAddItems, ADD_ITEMS); + PARSER.declareStringArray(UpdateFilterRequest::setRemoveItems, REMOVE_ITEMS); + } + + private String filterId; + private String description; + private SortedSet addItems; + private SortedSet removeItems; + + /** + * Construct a new request referencing a non-null, existing filter_id + * @param filterId Id referencing the filter to update + */ + public UpdateFilterRequest(String filterId) { + this.filterId = Objects.requireNonNull(filterId, "[" + MlFilter.ID.getPreferredName() + "] must not be null"); + } + + public String getFilterId() { + return filterId; + } + + public String getDescription() { + return description; + } + + /** + * The new description of the filter + * @param description the updated filter description + */ + public void setDescription(String description) { + this.description = description; + } + + public SortedSet getAddItems() { + return addItems; + } + + /** + * The collection of items to add to the filter + * @param addItems non-null items to add to the filter, defaults to empty array + */ + public void setAddItems(Collection addItems) { + this.addItems = new TreeSet<>(Objects.requireNonNull(addItems, + "[" + ADD_ITEMS.getPreferredName()+"] must not be null")); + } + + public SortedSet getRemoveItems() { + return removeItems; + } + + /** + * The collection of items to remove from the filter + * @param removeItems non-null items to remove from the filter, defaults to empty array + */ + public void setRemoveItems(Collection removeItems) { + this.removeItems = new TreeSet<>(Objects.requireNonNull(removeItems, + "[" + REMOVE_ITEMS.getPreferredName()+"] must not be null")); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(MlFilter.ID.getPreferredName(), filterId); + if (description != null) { + builder.field(MlFilter.DESCRIPTION.getPreferredName(), description); + } + if (addItems != null) { + builder.field(ADD_ITEMS.getPreferredName(), addItems); + } + if (removeItems != null) { + builder.field(REMOVE_ITEMS.getPreferredName(), removeItems); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(filterId, description, addItems, removeItems); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + UpdateFilterRequest other = (UpdateFilterRequest) obj; + return Objects.equals(filterId, other.filterId) + && Objects.equals(description, other.description) + && Objects.equals(addItems, other.addItems) + && Objects.equals(removeItems, other.removeItems); + } + + @Override + public final String toString() { + return Strings.toString(this); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index bb4822990722e..ed4d59de318a6 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -52,6 +52,7 @@ import org.elasticsearch.client.ml.StartDatafeedRequest; import org.elasticsearch.client.ml.StartDatafeedRequestTests; import org.elasticsearch.client.ml.StopDatafeedRequest; +import org.elasticsearch.client.ml.UpdateFilterRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.calendars.Calendar; import org.elasticsearch.client.ml.calendars.CalendarTests; @@ -74,6 +75,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -566,6 +568,23 @@ public void testGetFilter() throws IOException { assertThat(request.getParameters().get(PageParams.SIZE.getPreferredName()), equalTo("10")); } + public void testUpdateFilter() throws IOException { + String filterId = randomAlphaOfLength(10); + UpdateFilterRequest updateFilterRequest = new UpdateFilterRequest(filterId); + updateFilterRequest.setDescription(randomAlphaOfLength(10)); + updateFilterRequest.setRemoveItems(Arrays.asList("item1", "item2")); + updateFilterRequest.setAddItems(Arrays.asList("item3", "item5")); + + Request request = MLRequestConverters.updateFilter(updateFilterRequest); + + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertThat(request.getEndpoint(), equalTo("/_xpack/ml/filters/"+filterId+"/_update")); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + UpdateFilterRequest parsedFilterRequest = UpdateFilterRequest.PARSER.apply(parser, null); + assertThat(parsedFilterRequest, equalTo(updateFilterRequest)); + } + } + private static Job createValidJob(String jobId) { AnalysisConfig.Builder analysisConfig = AnalysisConfig.builder(Collections.singletonList( Detector.builder().setFunction("count").build())); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 7bf8969d66b16..51c583e139ccb 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -69,6 +69,7 @@ import org.elasticsearch.client.ml.StopDatafeedRequest; import org.elasticsearch.client.ml.StopDatafeedResponse; import org.elasticsearch.client.ml.UpdateDatafeedRequest; +import org.elasticsearch.client.ml.UpdateFilterRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.calendars.Calendar; import org.elasticsearch.client.ml.calendars.CalendarTests; @@ -101,6 +102,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -927,6 +929,28 @@ public void testGetFilters() throws Exception { } } + public void testUpdateFilter() throws Exception { + String filterId = "update-filter-test"; + MlFilter mlFilter = MlFilter.builder(filterId) + .setDescription("old description") + .setItems(Arrays.asList("olditem1", "olditem2")) + .build(); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putFilter(new PutFilterRequest(mlFilter), RequestOptions.DEFAULT); + + UpdateFilterRequest updateFilterRequest = new UpdateFilterRequest(filterId); + + updateFilterRequest.setAddItems(Arrays.asList("newItem1", "newItem2")); + updateFilterRequest.setRemoveItems(Collections.singletonList("olditem1")); + updateFilterRequest.setDescription("new description"); + MlFilter filter = execute(updateFilterRequest, + machineLearningClient::updateFilter, + machineLearningClient::updateFilterAsync).getResponse(); + + assertThat(filter.getDescription(), equalTo(updateFilterRequest.getDescription())); + assertThat(filter.getItems(), contains("newItem1", "newItem2", "olditem2")); + } + public static String randomValidJobId() { CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz0123456789".toCharArray()); return generator.ofCodePointsLength(random(), 10, 10); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index e6a8976a3c75c..1d9f511245a0f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -87,6 +87,7 @@ import org.elasticsearch.client.ml.StopDatafeedRequest; import org.elasticsearch.client.ml.StopDatafeedResponse; import org.elasticsearch.client.ml.UpdateDatafeedRequest; +import org.elasticsearch.client.ml.UpdateFilterRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.calendars.Calendar; import org.elasticsearch.client.ml.datafeed.ChunkingConfig; @@ -2229,4 +2230,66 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testUpdateFilter() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + String filterId = "update-filter-doc-test"; + MlFilter.Builder filterBuilder = MlFilter.builder(filterId).setDescription("test").setItems("*.google.com", "wikipedia.org"); + + client.machineLearning().putFilter(new PutFilterRequest(filterBuilder.build()), RequestOptions.DEFAULT); + + { + // tag::update-filter-request + UpdateFilterRequest request = new UpdateFilterRequest(filterId); // <1> + // end::update-filter-request + + // tag::update-filter-description + request.setDescription("my new description"); // <1> + // end::update-filter-description + + // tag::update-filter-add-items + request.setAddItems(Arrays.asList("*.bing.com", "*.elastic.co")); // <1> + // end::update-filter-add-items + + // tag::update-filter-remove-items + request.setRemoveItems(Arrays.asList("*.google.com")); // <1> + // end::update-filter-remove-items + + // tag::update-filter-execute + PutFilterResponse response = client.machineLearning().updateFilter(request, RequestOptions.DEFAULT); + // end::update-filter-execute + + // tag::update-filter-response + MlFilter updatedFilter = response.getResponse(); // <1> + // end::update-filter-response + assertEquals(request.getDescription(), updatedFilter.getDescription()); + } + { + UpdateFilterRequest request = new UpdateFilterRequest(filterId); + + // tag::update-filter-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(PutFilterResponse putFilterResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::update-filter-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::update-filter-execute-async + client.machineLearning().updateFilterAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::update-filter-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/UpdateFilterRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/UpdateFilterRequestTests.java new file mode 100644 index 0000000000000..ee340c03d0820 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/UpdateFilterRequestTests.java @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.util.ArrayList; +import java.util.List; + + +public class UpdateFilterRequestTests extends AbstractXContentTestCase { + + @Override + protected UpdateFilterRequest createTestInstance() { + UpdateFilterRequest request = new UpdateFilterRequest(randomAlphaOfLength(10)); + if (randomBoolean()) { + request.setDescription(randomAlphaOfLength(10)); + } + if (randomBoolean()) { + int items = randomInt(10); + List strings = new ArrayList<>(items); + for (int i = 0; i < items; i++) { + strings.add(randomAlphaOfLength(10)); + } + request.setAddItems(strings); + } + if (randomBoolean()) { + int items = randomInt(10); + List strings = new ArrayList<>(items); + for (int i = 0; i < items; i++) { + strings.add(randomAlphaOfLength(10)); + } + request.setRemoveItems(strings); + } + return request; + } + + @Override + protected UpdateFilterRequest doParseInstance(XContentParser parser) { + return UpdateFilterRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/docs/java-rest/high-level/ml/update-filter.asciidoc b/docs/java-rest/high-level/ml/update-filter.asciidoc new file mode 100644 index 0000000000000..b100000ddc1e8 --- /dev/null +++ b/docs/java-rest/high-level/ml/update-filter.asciidoc @@ -0,0 +1,57 @@ +-- +:api: update-filter +:request: UpdateFilterRequest +:response: PutFilterResponse +-- +[id="{upid}-{api}"] +=== Update Filter API + +The Update Filter API can be used to update an existing {ml} filter +in the cluster. The API accepts a +{request}+ object +as a request and returns a +{response}+. + +[id="{upid}-{api}-request"] +==== Update Filter Request + +A +{request}+ requires the following argument: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- +<1> The id of the existing {ml} filter + +==== Optional Arguments +The following arguments are optional: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-description] +-------------------------------------------------- +<1> The updated description of the {ml} filter + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-add-items] +-------------------------------------------------- +<1> The list of items to add to the {ml} filter + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-remove-items] +-------------------------------------------------- +<1> The list of items to remove from the {ml} filter + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Response + +The returned +{response}+ returns the full representation of +the updated {ml} filter if it has been successfully updated. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> The updated `MlFilter` diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index c174ce1a3aeda..9a960e6f6b966 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -267,6 +267,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <<{upid}-put-filter>> * <<{upid}-get-model-snapshots>> * <<{upid}-get-filters>> +* <<{upid}-update-filter>> include::ml/put-job.asciidoc[] include::ml/get-job.asciidoc[] @@ -298,6 +299,7 @@ include::ml/delete-calendar.asciidoc[] include::ml/put-filter.asciidoc[] include::ml/get-model-snapshots.asciidoc[] include::ml/get-filters.asciidoc[] +include::ml/update-filter.asciidoc[] == Migration APIs