HLRC: Add document _count API (#34267)

Add `count()` api method, `CountRequest` and `CountResponse` classes to HLRC. Code in server module is unchanged.

Relates to #27205
This commit is contained in:
Milan Mrdjen 2018-11-02 14:21:19 +01:00 committed by Michael Basnight
parent 12072b200e
commit 34677b9c83
12 changed files with 1073 additions and 1 deletions

View File

@ -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"));

View File

@ -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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html">Count API on elastic.co</a>
* @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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html">Count API on elastic.co</a>
* @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<CountResponse> listener) {
performRequestAsyncAndParseEntity(countRequest, RequestConverters::count, options,CountResponse::fromXContent,
listener, emptySet());
}
/**
* Updates a document using the Update API.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html">Update API on elastic.co</a>

View File

@ -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;
}
}

View File

@ -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<ShardSearchFailure> 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): "") +
'}';
}
}
}

View File

@ -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<String, String> 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<String, String> 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();

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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<Runnable> 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;
}
}

View File

@ -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 + "]");
}
}
}

View File

@ -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<CountResponse> listener =
new ActionListener<CountResponse>() {
@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());
}
}

View File

@ -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 <<java-rest-high-query-builders, Building Queries>> 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]
--------------------------------------------------

View File

@ -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