diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 39bc03f4117..38dbbb8f151 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -49,6 +49,7 @@ import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.core.CountRequest; import org.elasticsearch.client.security.RefreshPolicy; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Nullable; @@ -442,6 +443,16 @@ final class RequestConverters { return request; } + static Request count(CountRequest countRequest) throws IOException { + Request request = new Request(HttpPost.METHOD_NAME, endpoint(countRequest.indices(), countRequest.types(), "_count")); + Params params = new Params(request); + params.withRouting(countRequest.routing()); + params.withPreference(countRequest.preference()); + params.withIndicesOptions(countRequest.indicesOptions()); + request.setEntity(createEntity(countRequest.source(), REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request explain(ExplainRequest explainRequest) throws IOException { Request request = new Request(HttpGet.METHOD_NAME, endpoint(explainRequest.index(), explainRequest.type(), explainRequest.id(), "_explain")); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index d8dce990a4d..11fff4c0a6b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -56,6 +56,8 @@ import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.client.core.CountRequest; +import org.elasticsearch.client.core.CountResponse; import org.elasticsearch.client.core.TermVectorsResponse; import org.elasticsearch.client.core.TermVectorsRequest; import org.elasticsearch.common.CheckedConsumer; @@ -791,6 +793,31 @@ public class RestHighLevelClient implements Closeable { emptySet()); } + /** + * Executes a count request using the Count API. + * See Count API on elastic.co + * @param countRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public final CountResponse count(CountRequest countRequest, RequestOptions options) throws IOException { + return performRequestAndParseEntity(countRequest, RequestConverters::count, options, CountResponse::fromXContent, + emptySet()); + } + + /** + * Asynchronously executes a count request using the Count API. + * See Count API on elastic.co + * @param countRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public final void countAsync(CountRequest countRequest, RequestOptions options, ActionListener listener) { + performRequestAsyncAndParseEntity(countRequest, RequestConverters::count, options,CountResponse::fromXContent, + listener, emptySet()); + } + /** * Updates a document using the Update API. * See Update API on elastic.co diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java new file mode 100644 index 00000000000..6d4589c7861 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java @@ -0,0 +1,206 @@ +/* + * 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.core; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.Strings; +import org.elasticsearch.search.builder.SearchSourceBuilder; + +import java.util.Arrays; +import java.util.Objects; + +import static org.elasticsearch.action.search.SearchRequest.DEFAULT_INDICES_OPTIONS; + +/** + * Encapsulates a request to _count API against one, several or all indices. + */ +public final class CountRequest extends ActionRequest implements IndicesRequest.Replaceable { + + private String[] indices = Strings.EMPTY_ARRAY; + private String[] types = Strings.EMPTY_ARRAY; + private String routing; + private String preference; + private SearchSourceBuilder searchSourceBuilder; + private IndicesOptions indicesOptions = DEFAULT_INDICES_OPTIONS; + + public CountRequest() { + this.searchSourceBuilder = new SearchSourceBuilder(); + } + + /** + * Constructs a new count request against the indices. No indices provided here means that count will execute on all indices. + */ + public CountRequest(String... indices) { + this(indices, new SearchSourceBuilder()); + } + + /** + * Constructs a new search request against the provided indices with the given search source. + */ + public CountRequest(String[] indices, SearchSourceBuilder searchSourceBuilder) { + indices(indices); + this.searchSourceBuilder = searchSourceBuilder; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + /** + * Sets the indices the count will be executed on. + */ + public CountRequest indices(String... indices) { + Objects.requireNonNull(indices, "indices must not be null"); + for (String index : indices) { + Objects.requireNonNull(index, "index must not be null"); + } + this.indices = indices; + return this; + } + + /** + * The source of the count request. + */ + public CountRequest source(SearchSourceBuilder searchSourceBuilder) { + this.searchSourceBuilder = Objects.requireNonNull(searchSourceBuilder, "source must not be null"); + return this; + } + + /** + * The document types to execute the count against. Defaults to be executed against all types. + * + * @deprecated Types are going away, prefer filtering on a type. + */ + @Deprecated + public CountRequest types(String... types) { + Objects.requireNonNull(types, "types must not be null"); + for (String type : types) { + Objects.requireNonNull(type, "type must not be null"); + } + this.types = types; + return this; + } + + /** + * The routing values to control the shards that the search will be executed on. + */ + public CountRequest routing(String routing) { + this.routing = routing; + return this; + } + + /** + * A comma separated list of routing values to control the shards the count will be executed on. + */ + public CountRequest routing(String... routings) { + this.routing = Strings.arrayToCommaDelimitedString(routings); + return this; + } + + /** + * Returns the indices options used to resolve indices. They tell for instance whether a single index is accepted, whether an empty + * array will be converted to _all, and how wildcards will be expanded if needed. + * + * @see org.elasticsearch.action.support.IndicesOptions + */ + public CountRequest indicesOptions(IndicesOptions indicesOptions) { + this.indicesOptions = Objects.requireNonNull(indicesOptions, "indicesOptions must not be null"); + return this; + } + + /** + * Sets the preference to execute the count. Defaults to randomize across shards. Can be set to {@code _local} to prefer local shards + * or a custom value, which guarantees that the same order will be used across different requests. + */ + public CountRequest preference(String preference) { + this.preference = preference; + return this; + } + + public IndicesOptions indicesOptions() { + return this.indicesOptions; + } + + public String routing() { + return this.routing; + } + + public String preference() { + return this.preference; + } + + public String[] indices() { + return Arrays.copyOf(this.indices, this.indices.length); + } + + public Float minScore() { + return this.searchSourceBuilder.minScore(); + } + + public CountRequest minScore(Float minScore) { + this.searchSourceBuilder.minScore(minScore); + return this; + } + + public int terminateAfter() { + return this.searchSourceBuilder.terminateAfter(); + } + + public CountRequest terminateAfter(int terminateAfter) { + this.searchSourceBuilder.terminateAfter(terminateAfter); + return this; + } + + public String[] types() { + return Arrays.copyOf(this.types, this.types.length); + } + + public SearchSourceBuilder source() { + return this.searchSourceBuilder; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CountRequest that = (CountRequest) o; + return Objects.equals(indicesOptions, that.indicesOptions) && + Arrays.equals(indices, that.indices) && + Arrays.equals(types, that.types) && + Objects.equals(routing, that.routing) && + Objects.equals(preference, that.preference); + } + + @Override + public int hashCode() { + int result = Objects.hash(indicesOptions, routing, preference); + result = 31 * result + Arrays.hashCode(indices); + result = 31 * result + Arrays.hashCode(types); + return result; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java new file mode 100644 index 00000000000..f97f79127e6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java @@ -0,0 +1,236 @@ +/* + * 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.core; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.RestStatus; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A response to _count API request. + */ +public final class CountResponse extends ActionResponse { + + static final ParseField COUNT = new ParseField("count"); + static final ParseField TERMINATED_EARLY = new ParseField("terminated_early"); + static final ParseField SHARDS = new ParseField("_shards"); + + private final long count; + private final Boolean terminatedEarly; + private final ShardStats shardStats; + + public CountResponse(long count, Boolean terminatedEarly, ShardStats shardStats) { + this.count = count; + this.terminatedEarly = terminatedEarly; + this.shardStats = shardStats; + } + + public ShardStats getShardStats() { + return shardStats; + } + + /** + * Number of documents matching request. + */ + public long getCount() { + return count; + } + + /** + * The total number of shards the search was executed on. + */ + public int getTotalShards() { + return shardStats.totalShards; + } + + /** + * The successful number of shards the search was executed on. + */ + public int getSuccessfulShards() { + return shardStats.successfulShards; + } + + /** + * The number of shards skipped due to pre-filtering + */ + public int getSkippedShards() { + return shardStats.skippedShards; + } + + /** + * The failed number of shards the search was executed on. + */ + public int getFailedShards() { + return shardStats.shardFailures.length; + } + + /** + * The failures that occurred during the search. + */ + public ShardSearchFailure[] getShardFailures() { + return shardStats.shardFailures; + } + + public RestStatus status() { + return RestStatus.status(shardStats.successfulShards, shardStats.totalShards, shardStats.shardFailures); + } + + public static CountResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + parser.nextToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); + String currentName = parser.currentName(); + Boolean terminatedEarly = null; + long count = 0; + ShardStats shardStats = new ShardStats(-1, -1,0, ShardSearchFailure.EMPTY_ARRAY); + + for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { + if (token == XContentParser.Token.FIELD_NAME) { + currentName = parser.currentName(); + } else if (token.isValue()) { + if (COUNT.match(currentName, parser.getDeprecationHandler())) { + count = parser.longValue(); + } else if (TERMINATED_EARLY.match(currentName, parser.getDeprecationHandler())) { + terminatedEarly = parser.booleanValue(); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (SHARDS.match(currentName, parser.getDeprecationHandler())) { + shardStats = ShardStats.fromXContent(parser); + } else { + parser.skipChildren(); + } + } + } + return new CountResponse(count, terminatedEarly, shardStats); + } + + @Override + public String toString() { + String s = "{" + + "count=" + count + + (isTerminatedEarly() != null ? ", terminatedEarly=" + terminatedEarly : "") + + ", " + shardStats + + '}'; + return s; + } + + public Boolean isTerminatedEarly() { + return terminatedEarly; + } + + /** + * Encapsulates _shards section of count api response. + */ + public static final class ShardStats { + + static final ParseField FAILED = new ParseField("failed"); + static final ParseField SKIPPED = new ParseField("skipped"); + static final ParseField TOTAL = new ParseField("total"); + static final ParseField SUCCESSFUL = new ParseField("successful"); + static final ParseField FAILURES = new ParseField("failures"); + + private final int successfulShards; + private final int totalShards; + private final int skippedShards; + private final ShardSearchFailure[] shardFailures; + + public ShardStats(int successfulShards, int totalShards, int skippedShards, ShardSearchFailure[] shardFailures) { + this.successfulShards = successfulShards; + this.totalShards = totalShards; + this.skippedShards = skippedShards; + this.shardFailures = Arrays.copyOf(shardFailures, shardFailures.length); + } + + public int getSuccessfulShards() { + return successfulShards; + } + + public int getTotalShards() { + return totalShards; + } + + public int getSkippedShards() { + return skippedShards; + } + + public ShardSearchFailure[] getShardFailures() { + return Arrays.copyOf(shardFailures, shardFailures.length, ShardSearchFailure[].class); + } + + static ShardStats fromXContent(XContentParser parser) throws IOException { + int successfulShards = -1; + int totalShards = -1; + int skippedShards = 0; //BWC @see org.elasticsearch.action.search.SearchResponse + List failures = new ArrayList<>(); + XContentParser.Token token; + String currentName = parser.currentName(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentName = parser.currentName(); + } else if (token.isValue()) { + if (FAILED.match(currentName, parser.getDeprecationHandler())) { + parser.intValue(); + } else if (SKIPPED.match(currentName, parser.getDeprecationHandler())) { + skippedShards = parser.intValue(); + } else if (TOTAL.match(currentName, parser.getDeprecationHandler())) { + totalShards = parser.intValue(); + } else if (SUCCESSFUL.match(currentName, parser.getDeprecationHandler())) { + successfulShards = parser.intValue(); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (FAILURES.match(currentName, parser.getDeprecationHandler())) { + while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { + failures.add(ShardSearchFailure.fromXContent(parser)); + } + } else { + parser.skipChildren(); + } + } else { + parser.skipChildren(); + } + } + return new ShardStats(successfulShards, totalShards, skippedShards, failures.toArray(new ShardSearchFailure[failures.size()])); + } + + @Override + public String toString() { + return "_shards : {" + + "total=" + totalShards + + ", successful=" + successfulShards + + ", skipped=" + skippedShards + + ", failed=" + (shardFailures != null && shardFailures.length > 0 ? shardFailures.length : 0 ) + + (shardFailures != null && shardFailures.length > 0 ? ", failures: " + Arrays.asList(shardFailures): "") + + '}'; + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 76b64241ef6..1e0f0f70b2f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -56,6 +56,7 @@ import org.elasticsearch.action.support.replication.ReplicationRequest; import org.elasticsearch.client.core.TermVectorsRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.RequestConverters.EndpointBuilder; +import org.elasticsearch.client.core.CountRequest; import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -968,6 +969,72 @@ public class RequestConvertersTests extends ESTestCase { expectThrows(NullPointerException.class, () -> new SearchRequest().types((String[]) null)); } + public void testCountNotNullSource() throws IOException { + //as we create SearchSourceBuilder in CountRequest constructor + CountRequest countRequest = new CountRequest(); + Request request = RequestConverters.count(countRequest); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals("/_count", request.getEndpoint()); + assertNotNull(request.getEntity()); + } + + public void testCount() throws Exception { + String[] indices = randomIndicesNames(0, 5); + CountRequest countRequest = new CountRequest(indices); + + int numTypes = randomIntBetween(0, 5); + String[] types = new String[numTypes]; + for (int i = 0; i < numTypes; i++) { + types[i] = "type-" + randomAlphaOfLengthBetween(2, 5); + } + countRequest.types(types); + + Map expectedParams = new HashMap<>(); + setRandomCountParams(countRequest, expectedParams); + setRandomIndicesOptions(countRequest::indicesOptions, countRequest::indicesOptions, expectedParams); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + if (frequently()) { + if (randomBoolean()) { + searchSourceBuilder.minScore(randomFloat()); + } + } + countRequest.source(searchSourceBuilder); + Request request = RequestConverters.count(countRequest); + StringJoiner endpoint = new StringJoiner("/", "/", ""); + String index = String.join(",", indices); + if (Strings.hasLength(index)) { + endpoint.add(index); + } + String type = String.join(",", types); + if (Strings.hasLength(type)) { + endpoint.add(type); + } + endpoint.add("_count"); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals(endpoint.toString(), request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(searchSourceBuilder, request.getEntity()); + } + + public void testCountNullIndicesAndTypes() { + expectThrows(NullPointerException.class, () -> new CountRequest((String[]) null)); + expectThrows(NullPointerException.class, () -> new CountRequest().indices((String[]) null)); + expectThrows(NullPointerException.class, () -> new CountRequest().types((String[]) null)); + } + + private static void setRandomCountParams(CountRequest countRequest, + Map expectedParams) { + if (randomBoolean()) { + countRequest.routing(randomAlphaOfLengthBetween(3, 10)); + expectedParams.put("routing", countRequest.routing()); + } + if (randomBoolean()) { + countRequest.preference(randomAlphaOfLengthBetween(3, 10)); + expectedParams.put("preference", countRequest.preference()); + } + } + public void testMultiSearch() throws IOException { int numberOfSearchRequests = randomIntBetween(0, 32); MultiSearchRequest multiSearchRequest = new MultiSearchRequest(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index d6c77d6f2a5..38810285a5d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -662,7 +662,6 @@ public class RestHighLevelClientTests extends ESTestCase { //this list should be empty once the high-level client is feature complete String[] notYetSupportedApi = new String[]{ "cluster.remote_info", - "count", "create", "get_source", "indices.delete_alias", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index f6aa97def28..e6a2f5d6178 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -35,6 +35,8 @@ import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchScrollRequest; +import org.elasticsearch.client.core.CountRequest; +import org.elasticsearch.client.core.CountResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.TimeValue; @@ -1233,4 +1235,69 @@ public class SearchIT extends ESRestHighLevelClientTestCase { assertEquals(0, searchResponse.getShardFailures().length); assertEquals(SearchResponse.Clusters.EMPTY, searchResponse.getClusters()); } + + public void testCountOneIndexNoQuery() throws IOException { + CountRequest countRequest = new CountRequest("index"); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(5, countResponse.getCount()); + } + + public void testCountMultipleIndicesNoQuery() throws IOException { + CountRequest countRequest = new CountRequest("index", "index1"); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(7, countResponse.getCount()); + } + + public void testCountAllIndicesNoQuery() throws IOException { + CountRequest countRequest = new CountRequest(); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(12, countResponse.getCount()); + } + + public void testCountOneIndexMatchQuery() throws IOException { + CountRequest countRequest = new CountRequest("index"); + countRequest.source(new SearchSourceBuilder().query(new MatchQueryBuilder("num", 10))); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(1, countResponse.getCount()); + } + + public void testCountMultipleIndicesMatchQueryUsingConstructor() throws IOException { + + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder().query(new MatchQueryBuilder("field", "value1")); + CountRequest countRequest = new CountRequest(new String[]{"index1", "index2", "index3"}, sourceBuilder); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(3, countResponse.getCount()); + + } + + public void testCountMultipleIndicesMatchQuery() throws IOException { + + CountRequest countRequest = new CountRequest("index1", "index2", "index3"); + countRequest.source(new SearchSourceBuilder().query(new MatchQueryBuilder("field", "value1"))); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(3, countResponse.getCount()); + } + + public void testCountAllIndicesMatchQuery() throws IOException { + + CountRequest countRequest = new CountRequest(); + countRequest.source(new SearchSourceBuilder().query(new MatchQueryBuilder("field", "value1"))); + CountResponse countResponse = execute(countRequest, highLevelClient()::count, highLevelClient()::countAsync); + assertCountHeader(countResponse); + assertEquals(3, countResponse.getCount()); + } + + private static void assertCountHeader(CountResponse countResponse) { + assertEquals(0, countResponse.getSkippedShards()); + assertEquals(0, countResponse.getFailedShards()); + assertThat(countResponse.getTotalShards(), greaterThan(0)); + assertEquals(countResponse.getTotalShards(), countResponse.getSuccessfulShards()); + assertEquals(0, countResponse.getShardFailures().length); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountRequestTests.java new file mode 100644 index 00000000000..1030f4401e1 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountRequestTests.java @@ -0,0 +1,95 @@ +/* + * 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.core; + +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.util.ArrayUtils; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; + +//similar to SearchRequestTests as CountRequest inline several members (and functionality) from SearchRequest +public class CountRequestTests extends ESTestCase { + + public void testIllegalArguments() { + CountRequest countRequest = new CountRequest(); + assertNotNull(countRequest.indices()); + assertNotNull(countRequest.indicesOptions()); + assertNotNull(countRequest.types()); + + NullPointerException e = expectThrows(NullPointerException.class, () -> countRequest.indices((String[]) null)); + assertEquals("indices must not be null", e.getMessage()); + e = expectThrows(NullPointerException.class, () -> countRequest.indices((String) null)); + assertEquals("index must not be null", e.getMessage()); + + e = expectThrows(NullPointerException.class, () -> countRequest.indicesOptions(null)); + assertEquals("indicesOptions must not be null", e.getMessage()); + + e = expectThrows(NullPointerException.class, () -> countRequest.types((String[]) null)); + assertEquals("types must not be null", e.getMessage()); + e = expectThrows(NullPointerException.class, () -> countRequest.types((String) null)); + assertEquals("type must not be null", e.getMessage()); + + e = expectThrows(NullPointerException.class, () -> countRequest.source(null)); + assertEquals("source must not be null", e.getMessage()); + + } + + public void testEqualsAndHashcode() { + checkEqualsAndHashCode(createCountRequest(), CountRequestTests::copyRequest, this::mutate); + } + + private CountRequest createCountRequest() { + CountRequest countRequest = new CountRequest("index"); + countRequest.source(new SearchSourceBuilder().query(new MatchQueryBuilder("num", 10))); + return countRequest; + } + + private CountRequest mutate(CountRequest countRequest) { + CountRequest mutation = copyRequest(countRequest); + List mutators = new ArrayList<>(); + mutators.add(() -> mutation.indices(ArrayUtils.concat(countRequest.indices(), new String[]{randomAlphaOfLength(10)}))); + mutators.add(() -> mutation.indicesOptions(randomValueOtherThan(countRequest.indicesOptions(), + () -> IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean())))); + mutators.add(() -> mutation.types(ArrayUtils.concat(countRequest.types(), new String[]{randomAlphaOfLength(10)}))); + mutators.add(() -> mutation.preference(randomValueOtherThan(countRequest.preference(), () -> randomAlphaOfLengthBetween(3, 10)))); + mutators.add(() -> mutation.routing(randomValueOtherThan(countRequest.routing(), () -> randomAlphaOfLengthBetween(3, 10)))); + randomFrom(mutators).run(); + return mutation; + } + + private static CountRequest copyRequest(CountRequest countRequest) { + CountRequest result = new CountRequest(); + result.indices(countRequest.indices()); + result.indicesOptions(countRequest.indicesOptions()); + result.types(countRequest.types()); + result.routing(countRequest.routing()); + result.preference(countRequest.preference()); + if (countRequest.source() != null) { + result.source(countRequest.source()); + } + return result; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountResponseTests.java new file mode 100644 index 00000000000..c2fc668d604 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/core/CountResponseTests.java @@ -0,0 +1,126 @@ +/* + * 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.core; + +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.rest.action.RestActions; +import org.elasticsearch.search.SearchShardTarget; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester; + +public class CountResponseTests extends ESTestCase { + + // Not comparing XContent for equivalence as we cannot compare the ShardSearchFailure#cause, because it will be wrapped in an outer + // ElasticSearchException. Best effort: try to check that the original message appears somewhere in the rendered xContent + // For more see ShardSearchFailureTests. + public void testFromXContent() throws IOException { + xContentTester( + this::createParser, + this::createTestInstance, + this::toXContent, + CountResponse::fromXContent) + .supportsUnknownFields(false) + .assertEqualsConsumer(this::assertEqualInstances) + .assertToXContentEquivalence(false) + .test(); + } + + private CountResponse createTestInstance() { + long count = 5; + Boolean terminatedEarly = randomBoolean() ? null : randomBoolean(); + int totalShards = randomIntBetween(1, Integer.MAX_VALUE); + int successfulShards = randomIntBetween(0, totalShards); + int skippedShards = randomIntBetween(0, totalShards); + int numFailures = randomIntBetween(1, 5); + ShardSearchFailure[] failures = new ShardSearchFailure[numFailures]; + for (int i = 0; i < failures.length; i++) { + failures[i] = createShardFailureTestItem(); + } + CountResponse.ShardStats shardStats = new CountResponse.ShardStats(successfulShards, totalShards, skippedShards, + randomBoolean() ? ShardSearchFailure.EMPTY_ARRAY : failures); + return new CountResponse(count, terminatedEarly, shardStats); + } + + private void toXContent(CountResponse response, XContentBuilder builder) throws IOException { + builder.startObject(); + builder.field(CountResponse.COUNT.getPreferredName(), response.getCount()); + if (response.isTerminatedEarly() != null) { + builder.field(CountResponse.TERMINATED_EARLY.getPreferredName(), response.isTerminatedEarly()); + } + toXContent(response.getShardStats(), builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + } + + private void toXContent(CountResponse.ShardStats stats, XContentBuilder builder, ToXContent.Params params) throws IOException { + RestActions.buildBroadcastShardsHeader(builder, params, stats.getTotalShards(), stats.getSuccessfulShards(), stats + .getSkippedShards(), stats.getShardFailures().length, stats.getShardFailures()); + } + + @SuppressWarnings("Duplicates") + private static ShardSearchFailure createShardFailureTestItem() { + String randomMessage = randomAlphaOfLengthBetween(3, 20); + Exception ex = new ParsingException(0, 0, randomMessage, new IllegalArgumentException("some bad argument")); + SearchShardTarget searchShardTarget = null; + if (randomBoolean()) { + String nodeId = randomAlphaOfLengthBetween(5, 10); + String indexName = randomAlphaOfLengthBetween(5, 10); + searchShardTarget = new SearchShardTarget(nodeId, + new ShardId(new Index(indexName, IndexMetaData.INDEX_UUID_NA_VALUE), randomInt()), null, null); + } + return new ShardSearchFailure(ex, searchShardTarget); + } + + private void assertEqualInstances(CountResponse expectedInstance, CountResponse newInstance) { + assertEquals(expectedInstance.getCount(), newInstance.getCount()); + assertEquals(expectedInstance.status(), newInstance.status()); + assertEquals(expectedInstance.isTerminatedEarly(), newInstance.isTerminatedEarly()); + assertEquals(expectedInstance.getTotalShards(), newInstance.getTotalShards()); + assertEquals(expectedInstance.getFailedShards(), newInstance.getFailedShards()); + assertEquals(expectedInstance.getSkippedShards(), newInstance.getSkippedShards()); + assertEquals(expectedInstance.getSuccessfulShards(), newInstance.getSuccessfulShards()); + assertEquals(expectedInstance.getShardFailures().length, newInstance.getShardFailures().length); + + ShardSearchFailure[] expectedFailures = expectedInstance.getShardFailures(); + ShardSearchFailure[] newFailures = newInstance.getShardFailures(); + + for (int i = 0; i < newFailures.length; i++) { + ShardSearchFailure parsedFailure = newFailures[i]; + ShardSearchFailure originalFailure = expectedFailures[i]; + assertEquals(originalFailure.index(), parsedFailure.index()); + assertEquals(originalFailure.shard(), parsedFailure.shard()); + assertEquals(originalFailure.shardId(), parsedFailure.shardId()); + String originalMsg = originalFailure.getCause().getMessage(); + assertEquals(parsedFailure.getCause().getMessage(), "Elasticsearch exception [type=parsing_exception, reason=" + + originalMsg + "]"); + String nestedMsg = originalFailure.getCause().getCause().getMessage(); + assertEquals(parsedFailure.getCause().getCause().getMessage(), + "Elasticsearch exception [type=illegal_argument_exception, reason=" + nestedMsg + "]"); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java index 1e596158750..831c39ed28b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java @@ -49,6 +49,8 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.core.CountRequest; +import org.elasticsearch.client.core.CountResponse; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.text.Text; @@ -1287,4 +1289,124 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase { assertSame(RestStatus.OK, bulkResponse.status()); assertFalse(bulkResponse.hasFailures()); } + + + @SuppressWarnings({"unused", "unchecked"}) + public void testCount() throws Exception { + indexCountTestData(); + RestHighLevelClient client = highLevelClient(); + { + // tag::count-request-basic + CountRequest countRequest = new CountRequest(); // <1> + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // <2> + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); // <3> + countRequest.source(searchSourceBuilder); // <4> + // end::count-request-basic + } + { + // tag::count-request-indices-types + CountRequest countRequest = new CountRequest("blog"); // <1> + countRequest.types("doc"); // <2> + // end::count-request-indices-types + // tag::count-request-routing + countRequest.routing("routing"); // <1> + // end::count-request-routing + // tag::count-request-indicesOptions + countRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); // <1> + // end::count-request-indicesOptions + // tag::count-request-preference + countRequest.preference("_local"); // <1> + // end::count-request-preference + assertNotNull(client.count(countRequest, RequestOptions.DEFAULT)); + } + { + // tag::count-source-basics + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // <1> + sourceBuilder.query(QueryBuilders.termQuery("user", "kimchy")); // <2> + // end::count-source-basics + + // tag::count-source-setter + CountRequest countRequest = new CountRequest(); + countRequest.indices("blog", "author"); + countRequest.source(sourceBuilder); + // end::count-source-setter + + // tag::count-execute + CountResponse countResponse = client + .count(countRequest, RequestOptions.DEFAULT); + // end::count-execute + + // tag::count-execute-listener + ActionListener listener = + new ActionListener() { + + @Override + public void onResponse(CountResponse countResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::count-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::count-execute-async + client.countAsync(countRequest, RequestOptions.DEFAULT, listener); // <1> + // end::count-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + + // tag::count-response-1 + long count = countResponse.getCount(); + RestStatus status = countResponse.status(); + Boolean terminatedEarly = countResponse.isTerminatedEarly(); + // end::count-response-1 + + // tag::count-response-2 + int totalShards = countResponse.getTotalShards(); + int skippedShards = countResponse.getSkippedShards(); + int successfulShards = countResponse.getSuccessfulShards(); + int failedShards = countResponse.getFailedShards(); + for (ShardSearchFailure failure : countResponse.getShardFailures()) { + // failures should be handled here + } + // end::count-response-2 + assertNotNull(countResponse); + assertEquals(4, countResponse.getCount()); + } + } + + private static void indexCountTestData() throws IOException { + CreateIndexRequest authorsRequest = new CreateIndexRequest("author") + .mapping("doc", "user", "type=keyword,doc_values=false"); + CreateIndexResponse authorsResponse = highLevelClient().indices().create(authorsRequest, RequestOptions.DEFAULT); + assertTrue(authorsResponse.isAcknowledged()); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest("blog", "doc", "1") + .source(XContentType.JSON, "title", "Doubling Down on Open?", "user", + Collections.singletonList("kimchy"), "innerObject", Collections.singletonMap("key", "value"))); + bulkRequest.add(new IndexRequest("blog", "doc", "2") + .source(XContentType.JSON, "title", "Swiftype Joins Forces with Elastic", "user", + Arrays.asList("kimchy", "matt"), "innerObject", Collections.singletonMap("key", "value"))); + bulkRequest.add(new IndexRequest("blog", "doc", "3") + .source(XContentType.JSON, "title", "On Net Neutrality", "user", + Arrays.asList("tyler", "kimchy"), "innerObject", Collections.singletonMap("key", "value"))); + + bulkRequest.add(new IndexRequest("author", "doc", "1") + .source(XContentType.JSON, "user", "kimchy")); + + + bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + BulkResponse bulkResponse = highLevelClient().bulk(bulkRequest, RequestOptions.DEFAULT); + assertSame(RestStatus.OK, bulkResponse.status()); + assertFalse(bulkResponse.hasFailures()); + } + } diff --git a/docs/java-rest/high-level/search/count.asciidoc b/docs/java-rest/high-level/search/count.asciidoc new file mode 100644 index 00000000000..f70e1e1fd4d --- /dev/null +++ b/docs/java-rest/high-level/search/count.asciidoc @@ -0,0 +1,114 @@ +-- +:api: count +:request: CountRequest +:response: CountResponse +-- +[id="{upid}-{api}"] + +=== Count API + +[id="{upid}-{api}-request"] + +==== Count Request + +The +{request}+ is used to execute a query and get the number of matches for the query. The query to use in +{request}+ can be +set in similar way as query in `SearchRequest` using `SearchSourceBuilder`. + +In its most basic form, we can add a query to the request: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-basic] +-------------------------------------------------- + +<1> Creates the +{request}+. Without arguments this runs against all indices. +<2> Most search parameters are added to the `SearchSourceBuilder`. +<3> Add a `match_all` query to the `SearchSourceBuilder`. +<4> Add the `SearchSourceBuilder` to the +{request}+. + +[[java-rest-high-count-request-optional]] +===== Count Request optional arguments + +Let's first look at some of the optional arguments of a +{request}+: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-indices-types] +-------------------------------------------------- +<1> Restricts the request to an index +<2> Limits the request to a type + +There are a couple of other interesting optional parameters: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-routing] +-------------------------------------------------- +<1> Set a routing parameter + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-indicesOptions] +-------------------------------------------------- +<1> Setting `IndicesOptions` controls how unavailable indices are resolved and how wildcard expressions are expanded + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-preference] +-------------------------------------------------- +<1> Use the preference parameter e.g. to execute the search to prefer local shards. The default is to randomize across shards. + +===== Using the SearchSourceBuilder in CountRequest + +Both in search and count API calls, most options controlling the search behavior can be set on the `SearchSourceBuilder`, +which contains more or less the equivalent of the options in the search request body of the Rest API. + +Here are a few examples of some common options: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-source-basics] +-------------------------------------------------- +<1> Create a `SearchSourceBuilder` with default options. +<2> Set the query. Can be any type of `QueryBuilder` + +After this, the `SearchSourceBuilder` only needs to be added to the ++{request}+: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-source-setter] +-------------------------------------------------- + +Note subtle difference when using `SearchSourceBuilder` in `SearchRequest` and using `SearchSourceBuilder` in +{request}+ - using +`SearchSourceBuilder` in `SearchRequest` one can use `SearchSourceBuilder.size()` and `SearchSourceBuilder.from()` methods to set the +number of search hits to return, and the starting index. In +{request}+ we're interested in total number of matches and these methods +have no meaning. + +The <> page gives a list of all available search queries with +their corresponding `QueryBuilder` objects and `QueryBuilders` helper methods. + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== CountResponse + +The +{response}+ that is returned by executing the count API call provides total count of hits and details about the count execution +itself, like the HTTP status code, or whether the request terminated early: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response-1] +-------------------------------------------------- + +The response also provides information about the execution on the +shard level by offering statistics about the total number of shards that were +affected by the underlying search, and the successful vs. unsuccessful shards. Possible +failures can also be handled by iterating over an array off +`ShardSearchFailures` like in the following example: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response-2] +-------------------------------------------------- + diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 4411a6b375f..dc17296e5c2 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -54,6 +54,7 @@ The Java High Level REST Client supports the following Search APIs: * <<{upid}-field-caps>> * <<{upid}-rank-eval>> * <<{upid}-explain>> +* <<{upid}-count>> include::search/search.asciidoc[] include::search/scroll.asciidoc[] @@ -63,6 +64,7 @@ include::search/multi-search-template.asciidoc[] include::search/field-caps.asciidoc[] include::search/rank-eval.asciidoc[] include::search/explain.asciidoc[] +include::search/count.asciidoc[] == Miscellaneous APIs