Browse Source

HLRC: Adding ML Update Filter API (#35522)

* HLRC: Adding ml get filters api

* HLRC: Adding ML Update Filter API
Benjamin Trent 7 years ago
parent
commit
803eccec11

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

@@ -56,6 +56,7 @@ import org.elasticsearch.client.ml.PutJobRequest;
 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 @@ final class MLRequestConverters {
         }
         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;
+    }
 }

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

@@ -74,6 +74,7 @@ import org.elasticsearch.client.ml.StartDatafeedResponse;
 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 final class MachineLearningClient {
             listener,
             Collections.emptySet());
     }
+
+    /**
+     * Updates a Machine Learning Filter
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-filter.html">
+     *     ML Update Filter documentation</a>
+     *
+     * @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
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-filter.html">
+     *     ML Update Filter documentation</a>
+     *
+     * @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<PutFilterResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::updateFilter,
+            options,
+            PutFilterResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
 }

+ 156 - 0
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<UpdateFilterRequest, Void> 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<String> addItems;
+    private SortedSet<String> 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<String> 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<String> addItems) {
+        this.addItems = new TreeSet<>(Objects.requireNonNull(addItems,
+            "[" + ADD_ITEMS.getPreferredName()+"] must not be null"));
+    }
+
+    public SortedSet<String> 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<String> 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;
+    }
+}

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

@@ -52,6 +52,7 @@ import org.elasticsearch.client.ml.PutJobRequest;
 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 org.elasticsearch.test.ESTestCase;
 
 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 class MLRequestConvertersTests extends ESTestCase {
         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()));

+ 24 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java

@@ -69,6 +69,7 @@ import org.elasticsearch.client.ml.StartDatafeedResponse;
 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 java.util.stream.Collectors;
 
 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 class MachineLearningIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    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);

+ 63 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java

@@ -87,6 +87,7 @@ import org.elasticsearch.client.ml.StartDatafeedResponse;
 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 class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
             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<PutFilterResponse> listener = new ActionListener<PutFilterResponse>() {
+                @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));
+        }
+    }
 }

+ 64 - 0
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<UpdateFilterRequest> {
+
+    @Override
+    protected UpdateFilterRequest createTestInstance() {
+        UpdateFilterRequest request = new UpdateFilterRequest(randomAlphaOfLength(10));
+        if (randomBoolean()) {
+            request.setDescription(randomAlphaOfLength(10));
+        }
+        if (randomBoolean()) {
+            int items = randomInt(10);
+            List<String> 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<String> 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;
+    }
+}

+ 57 - 0
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`

+ 2 - 0
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