diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java index 5f85b18091d..30a42eb333f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java @@ -57,6 +57,8 @@ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -661,6 +663,36 @@ public final class IndicesClient { PutIndexTemplateResponse::fromXContent, listener, emptySet()); } + /** + * Validate a potentially expensive query without executing it. + *

+ * See Validate Query API + * on elastic.co + * @param validateQueryRequest 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 ValidateQueryResponse validateQuery(ValidateQueryRequest validateQueryRequest, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(validateQueryRequest, RequestConverters::validateQuery, options, + ValidateQueryResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously validate a potentially expensive query without executing it. + *

+ * See Validate Query API + * on elastic.co + * @param validateQueryRequest 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 void validateQueryAsync(ValidateQueryRequest validateQueryRequest, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(validateQueryRequest, RequestConverters::validateQuery, options, + ValidateQueryResponse::fromXContent, listener, emptySet()); + } + /** * Gets index templates using the Index Templates API * See Index Templates API 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 93bf6a1a198..ab85af9f1fd 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 @@ -58,6 +58,7 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeType; import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; @@ -856,6 +857,20 @@ final class RequestConverters { return request; } + static Request validateQuery(ValidateQueryRequest validateQueryRequest) throws IOException { + String[] indices = validateQueryRequest.indices() == null ? Strings.EMPTY_ARRAY : validateQueryRequest.indices(); + String[] types = validateQueryRequest.types() == null || indices.length <= 0 ? Strings.EMPTY_ARRAY : validateQueryRequest.types(); + String endpoint = endpoint(indices, types, "_validate/query"); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + Params params = new Params(request); + params.withIndicesOptions(validateQueryRequest.indicesOptions()); + params.putParam("explain", Boolean.toString(validateQueryRequest.explain())); + params.putParam("all_shards", Boolean.toString(validateQueryRequest.allShards())); + params.putParam("rewrite", Boolean.toString(validateQueryRequest.rewrite())); + request.setEntity(createEntity(validateQueryRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request getAlias(GetAliasesRequest getAliasesRequest) { String[] indices = getAliasesRequest.indices() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.indices(); String[] aliases = getAliasesRequest.aliases() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.aliases(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java index 82ac161f5af..c226b534926 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java @@ -20,6 +20,7 @@ package org.elasticsearch.client; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; @@ -63,6 +64,8 @@ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; @@ -80,6 +83,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -1155,6 +1160,40 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase { assertThat(unknownSettingError.getDetailedMessage(), containsString("unknown setting [index.this-setting-does-not-exist]")); } + public void testValidateQuery() throws IOException{ + String index = "some_index"; + createIndex(index, Settings.EMPTY); + QueryBuilder builder = QueryBuilders + .boolQuery() + .must(QueryBuilders.queryStringQuery("*:*")) + .filter(QueryBuilders.termQuery("user", "kimchy")); + ValidateQueryRequest request = new ValidateQueryRequest(index).query(builder); + request.explain(randomBoolean()); + ValidateQueryResponse response = execute(request, highLevelClient().indices()::validateQuery, + highLevelClient().indices()::validateQueryAsync); + assertTrue(response.isValid()); + } + + public void testInvalidValidateQuery() throws IOException{ + String index = "shakespeare"; + + createIndex(index, Settings.EMPTY); + Request postDoc = new Request(HttpPost.METHOD_NAME, "/" + index + "/1"); + postDoc.setJsonEntity( + "{\"type\":\"act\",\"line_id\":1,\"play_name\":\"Henry IV\", \"speech_number\":\"\"," + + "\"line_number\":\"\",\"speaker\":\"\",\"text_entry\":\"ACT I\"}"); + assertOK(client().performRequest(postDoc)); + + QueryBuilder builder = QueryBuilders + .queryStringQuery("line_id:foo") + .lenient(false); + ValidateQueryRequest request = new ValidateQueryRequest(index).query(builder); + request.explain(true); + ValidateQueryResponse response = execute(request, highLevelClient().indices()::validateQuery, + highLevelClient().indices()::validateQueryAsync); + assertFalse(response.isValid()); + } + public void testGetIndexTemplate() throws Exception { RestHighLevelClient client = highLevelClient(); 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 aa8221f3099..60f427b4904 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 @@ -60,6 +60,7 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeType; import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.delete.DeleteRequest; @@ -1895,6 +1896,40 @@ public class RequestConvertersTests extends ESTestCase { assertToXContentBody(putTemplateRequest, request.getEntity()); } + public void testValidateQuery() throws Exception { + String[] indices = randomBoolean() ? null : randomIndicesNames(0, 5); + String[] types = randomBoolean() ? generateRandomStringArray(5, 5, false, false) : null; + ValidateQueryRequest validateQueryRequest; + if (randomBoolean()) { + validateQueryRequest = new ValidateQueryRequest(indices); + } else { + validateQueryRequest = new ValidateQueryRequest(); + validateQueryRequest.indices(indices); + } + validateQueryRequest.types(types); + Map expectedParams = new HashMap<>(); + setRandomIndicesOptions(validateQueryRequest::indicesOptions, validateQueryRequest::indicesOptions, expectedParams); + validateQueryRequest.explain(randomBoolean()); + validateQueryRequest.rewrite(randomBoolean()); + validateQueryRequest.allShards(randomBoolean()); + expectedParams.put("explain", Boolean.toString(validateQueryRequest.explain())); + expectedParams.put("rewrite", Boolean.toString(validateQueryRequest.rewrite())); + expectedParams.put("all_shards", Boolean.toString(validateQueryRequest.allShards())); + Request request = RequestConverters.validateQuery(validateQueryRequest); + StringJoiner endpoint = new StringJoiner("/", "/", ""); + if (indices != null && indices.length > 0) { + endpoint.add(String.join(",", indices)); + if (types != null && types.length > 0) { + endpoint.add(String.join(",", types)); + } + } + endpoint.add("_validate/query"); + assertThat(request.getEndpoint(), equalTo(endpoint.toString())); + assertThat(request.getParameters(), equalTo(expectedParams)); + assertToXContentBody(validateQueryRequest, request.getEntity()); + assertThat(request.getMethod(), equalTo(HttpGet.METHOD_NAME)); + } + public void testGetTemplateRequest() throws Exception { Map encodes = new HashMap<>(); encodes.put("log", "log"); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java index 7bd6b16cecc..9cc28152d03 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java @@ -62,6 +62,9 @@ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; +import org.elasticsearch.action.admin.indices.validate.query.QueryExplanation; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; +import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.IndicesOptions; @@ -81,6 +84,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.rest.RestStatus; @@ -2128,4 +2132,83 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + + public void testValidateQuery() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + + String index = "some_index"; + createIndex(index, Settings.EMPTY); + + // tag::validate-query-request + ValidateQueryRequest request = new ValidateQueryRequest(index); // <1> + // end::validate-query-request + + // tag::validate-query-request-query + QueryBuilder builder = QueryBuilders + .boolQuery() // <1> + .must(QueryBuilders.queryStringQuery("*:*")) + .filter(QueryBuilders.termQuery("user", "kimchy")); + request.query(builder); // <2> + // end::validate-query-request-query + + // tag::validate-query-request-explain + request.explain(true); // <1> + // end::validate-query-request-explain + + // tag::validate-query-request-allShards + request.allShards(true); // <1> + // end::validate-query-request-allShards + + // tag::validate-query-request-rewrite + request.rewrite(true); // <1> + // end::validate-query-request-rewrite + + // tag::validate-query-execute + ValidateQueryResponse response = client.indices().validateQuery(request, RequestOptions.DEFAULT); // <1> + // end::validate-query-execute + + // tag::validate-query-response + boolean isValid = response.isValid(); // <1> + int totalShards = response.getTotalShards(); // <2> + int successfulShards = response.getSuccessfulShards(); // <3> + int failedShards = response.getFailedShards(); // <4> + if (failedShards > 0) { + for(DefaultShardOperationFailedException failure: response.getShardFailures()) { // <5> + String failedIndex = failure.index(); // <6> + int shardId = failure.shardId(); // <7> + String reason = failure.reason(); // <8> + } + } + for(QueryExplanation explanation: response.getQueryExplanation()) { // <9> + String explanationIndex = explanation.getIndex(); // <10> + int shardId = explanation.getShard(); // <11> + String explanationString = explanation.getExplanation(); // <12> + } + // end::validate-query-response + + // tag::validate-query-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(ValidateQueryResponse validateQueryResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::validate-query-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::validate-query-execute-async + client.indices().validateQueryAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::validate-query-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } } diff --git a/docs/java-rest/high-level/indices/validate_query.asciidoc b/docs/java-rest/high-level/indices/validate_query.asciidoc new file mode 100644 index 00000000000..3b3b184b028 --- /dev/null +++ b/docs/java-rest/high-level/indices/validate_query.asciidoc @@ -0,0 +1,113 @@ +[[java-rest-high-indices-validate-query]] +=== Validate Query API + +[[java-rest-high-indices-validate-query-request]] +==== Validate Query Request + +A `ValidateQueryRequest` requires one or more `indices` on which the query is validated. If no index +is provided the request is executed on all indices. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-request] +-------------------------------------------------- +<1> The index on which to run the request. + +In addition it also needs the query that needs to be validated. The query can be built using the `QueryBuilders` utility class. +The following code snippet builds a sample boolean query. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-request-query] +-------------------------------------------------- +<1> Build the desired query. +<2> Set it to the request. + +==== Optional arguments +The following arguments can optionally be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-request-explain] +-------------------------------------------------- +<1> The explain parameter can be set to true to get more detailed information about why a query failed + +By default, the request is executed on a single shard only, which is randomly selected. The detailed explanation of +the query may depend on which shard is being hit, and therefore may vary from one request to another. So, in case of +query rewrite the `allShards` parameter should be used to get response from all available shards. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-request-allShards] +-------------------------------------------------- +<1> Set the allShards parameter. + +When the query is valid, the explanation defaults to the string representation of that query. With rewrite set to true, +the explanation is more detailed showing the actual Lucene query that will be executed + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-request-rewrite] +-------------------------------------------------- +<1> Set the rewrite parameter. + +[[java-rest-high-indices-validate-query-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-execute] +-------------------------------------------------- +<1> Execute the request and get back the response in a ValidateQueryResponse object. + +[[java-rest-high-indices-validate-query-async]] +==== Asynchronous Execution + +The asynchronous execution of a validate query request requires both the `ValidateQueryRequest` +instance and an `ActionListener` instance to be passed to the asynchronous +method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-execute-async] +-------------------------------------------------- +<1> The `ValidateQueryRequest` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `ValidateQueryResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument +<2> Called in case of failure. The raised exception is provided as an argument + +[[java-rest-high-indices-validate-query-response]] +==== Validate Query Response + +The returned `ValidateQueryResponse` allows to retrieve information about the executed + operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[validate-query-response] +-------------------------------------------------- +<1> Check if the query is valid or not. +<2> Get total number of shards. +<3> Get number of shards that were successful. +<4> Get number of shards that failed. +<5> Get the shard failures as `DefaultShardOperationFailedException`. +<6> Get the index of a failed shard. +<7> Get the shard id of a failed shard. +<8> Get the reason for shard failure. +<9> Get the detailed explanation for the shards (if explain was set to `true`). +<10> Get the index to which a particular explanation belongs. +<11> Get the shard id to which a particular explanation belongs. +<12> Get the actual explanation string. \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index b33c2421b06..4cd87a521d1 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -73,6 +73,7 @@ Index Management:: * <> * <> * <> +* <> Mapping Management:: * <> @@ -103,6 +104,7 @@ include::indices/get_alias.asciidoc[] include::indices/put_settings.asciidoc[] include::indices/get_settings.asciidoc[] include::indices/put_template.asciidoc[] +include::indices/validate_query.asciidoc[] include::indices/get_templates.asciidoc[] == Cluster APIs diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java index 780bf037f0e..e330a0b8565 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java @@ -20,16 +20,57 @@ package org.elasticsearch.action.admin.indices.validate.query; import org.elasticsearch.Version; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; +import java.util.Objects; -public class QueryExplanation implements Streamable { +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class QueryExplanation implements Streamable, ToXContentFragment { + + public static final String INDEX_FIELD = "index"; + public static final String SHARD_FIELD = "shard"; + public static final String VALID_FIELD = "valid"; + public static final String ERROR_FIELD = "error"; + public static final String EXPLANATION_FIELD = "explanation"; public static final int RANDOM_SHARD = -1; + @SuppressWarnings("unchecked") + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "query_explanation", + true, + a -> { + int shard = RANDOM_SHARD; + if (a[1] != null) { + shard = (int)a[1]; + } + return new QueryExplanation( + (String)a[0], + shard, + (boolean)a[2], + (String)a[3], + (String)a[4] + ); + } + ); + static { + PARSER.declareString(optionalConstructorArg(), new ParseField(INDEX_FIELD)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(SHARD_FIELD)); + PARSER.declareBoolean(constructorArg(), new ParseField(VALID_FIELD)); + PARSER.declareString(optionalConstructorArg(), new ParseField(EXPLANATION_FIELD)); + PARSER.declareString(optionalConstructorArg(), new ParseField(ERROR_FIELD)); + } + private String index; private int shard = RANDOM_SHARD; @@ -110,4 +151,43 @@ public class QueryExplanation implements Streamable { exp.readFrom(in); return exp; } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (getIndex() != null) { + builder.field(INDEX_FIELD, getIndex()); + } + if(getShard() >= 0) { + builder.field(SHARD_FIELD, getShard()); + } + builder.field(VALID_FIELD, isValid()); + if (getError() != null) { + builder.field(ERROR_FIELD, getError()); + } + if (getExplanation() != null) { + builder.field(EXPLANATION_FIELD, getExplanation()); + } + return builder; + } + + public static QueryExplanation fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueryExplanation other = (QueryExplanation) o; + return Objects.equals(getIndex(), other.getIndex()) && + Objects.equals(getShard(), other.getShard()) && + Objects.equals(isValid(), other.isValid()) && + Objects.equals(getError(), other.getError()) && + Objects.equals(getExplanation(), other.getExplanation()); + } + + @Override + public int hashCode() { + return Objects.hash(getIndex(), getShard(), isValid(), getError(), getExplanation()); + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java index 5953a5548c4..7694e7583c8 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java @@ -27,6 +27,8 @@ import org.elasticsearch.action.support.broadcast.BroadcastRequest; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -38,7 +40,7 @@ import java.util.Arrays; *

* The request requires the query to be set using {@link #query(QueryBuilder)} */ -public class ValidateQueryRequest extends BroadcastRequest { +public class ValidateQueryRequest extends BroadcastRequest implements ToXContentObject { private QueryBuilder query = new MatchAllQueryBuilder(); @@ -179,4 +181,12 @@ public class ValidateQueryRequest extends BroadcastRequest return "[" + Arrays.toString(indices) + "]" + Arrays.toString(types) + ", query[" + query + "], explain:" + explain + ", rewrite:" + rewrite + ", all_shards:" + allShards; } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("query"); + query.toXContent(builder, params); + return builder.endObject(); + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java index 5bb11dd56e0..f766e1d9c6a 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java @@ -21,16 +21,22 @@ package org.elasticsearch.action.admin.indices.validate.query; import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.elasticsearch.action.admin.indices.validate.query.QueryExplanation.readQueryExplanation; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * The response of the validate action. @@ -39,12 +45,33 @@ import static org.elasticsearch.action.admin.indices.validate.query.QueryExplana */ public class ValidateQueryResponse extends BroadcastResponse { - public static final String INDEX_FIELD = "index"; - public static final String SHARD_FIELD = "shard"; public static final String VALID_FIELD = "valid"; public static final String EXPLANATIONS_FIELD = "explanations"; - public static final String ERROR_FIELD = "error"; - public static final String EXPLANATION_FIELD = "explanation"; + + @SuppressWarnings("unchecked") + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "validate_query", + true, + arg -> { + BroadcastResponse response = (BroadcastResponse) arg[0]; + return + new ValidateQueryResponse( + (boolean)arg[1], + (List)arg[2], + response.getTotalShards(), + response.getSuccessfulShards(), + response.getFailedShards(), + Arrays.asList(response.getShardFailures()) + ); + } + ); + static { + declareBroadcastFields(PARSER); + PARSER.declareBoolean(constructorArg(), new ParseField(VALID_FIELD)); + PARSER.declareObjectArray( + optionalConstructorArg(), QueryExplanation.PARSER, new ParseField(EXPLANATIONS_FIELD) + ); + } private boolean valid; @@ -112,22 +139,14 @@ public class ValidateQueryResponse extends BroadcastResponse { builder.startArray(EXPLANATIONS_FIELD); for (QueryExplanation explanation : getQueryExplanation()) { builder.startObject(); - if (explanation.getIndex() != null) { - builder.field(INDEX_FIELD, explanation.getIndex()); - } - if(explanation.getShard() >= 0) { - builder.field(SHARD_FIELD, explanation.getShard()); - } - builder.field(VALID_FIELD, explanation.isValid()); - if (explanation.getError() != null) { - builder.field(ERROR_FIELD, explanation.getError()); - } - if (explanation.getExplanation() != null) { - builder.field(EXPLANATION_FIELD, explanation.getExplanation()); - } + explanation.toXContent(builder, params); builder.endObject(); } builder.endArray(); } } + + public static ValidateQueryResponse fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryAction.java index 57486396f91..d1a97d74d04 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryAction.java @@ -19,6 +19,7 @@ package org.elasticsearch.rest.action.admin.indices; +import org.elasticsearch.action.admin.indices.validate.query.QueryExplanation; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; import org.elasticsearch.action.support.IndicesOptions; @@ -101,7 +102,7 @@ public class RestValidateQueryAction extends BaseRestHandler { builder.startObject(); builder.field(ValidateQueryResponse.VALID_FIELD, false); if (explain) { - builder.field(ValidateQueryResponse.ERROR_FIELD, error); + builder.field(QueryExplanation.ERROR_FIELD, error); } builder.endObject(); return new BytesRestResponse(OK, builder); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanationTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanationTests.java new file mode 100644 index 00000000000..db167e0c766 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanationTests.java @@ -0,0 +1,59 @@ +/* + * 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.action.admin.indices.validate.query; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; + +import java.io.IOException; + +public class QueryExplanationTests extends AbstractStreamableXContentTestCase { + + static QueryExplanation createRandomQueryExplanation(boolean isValid) { + String index = "index_" + randomInt(1000); + int shard = randomInt(100); + Boolean valid = isValid; + String errorField = null; + if (!valid) { + errorField = randomAlphaOfLength(randomIntBetween(10, 100)); + } + String explanation = randomAlphaOfLength(randomIntBetween(10, 100)); + return new QueryExplanation(index, shard, valid, explanation, errorField); + } + + static QueryExplanation createRandomQueryExplanation() { + return createRandomQueryExplanation(randomBoolean()); + } + + @Override + protected QueryExplanation doParseInstance(XContentParser parser) throws IOException { + return QueryExplanation.fromXContent(parser); + } + + @Override + protected QueryExplanation createBlankInstance() { + return new QueryExplanation(); + } + + @Override + protected QueryExplanation createTestInstance() { + return createRandomQueryExplanation(); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponseTests.java new file mode 100644 index 00000000000..d72aae8fa2b --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponseTests.java @@ -0,0 +1,110 @@ +/* + * 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.action.admin.indices.validate.query; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.support.DefaultShardOperationFailedException; +import org.elasticsearch.action.support.broadcast.AbstractBroadcastResponseTestCase; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ValidateQueryResponseTests extends AbstractBroadcastResponseTestCase { + + private static ValidateQueryResponse createRandomValidateQueryResponse( + int totalShards, int successfulShards, int failedShards, List failures) { + boolean valid = failedShards == 0; + List queryExplanations = new ArrayList<>(totalShards); + for(DefaultShardOperationFailedException failure: failures) { + queryExplanations.add( + new QueryExplanation( + failure.index(), failure.shardId(), false, failure.reason(), null + ) + ); + } + return new ValidateQueryResponse( + valid, queryExplanations, totalShards, successfulShards, failedShards, failures + ); + } + + private static ValidateQueryResponse createRandomValidateQueryResponse() { + int totalShards = randomIntBetween(1, 10); + int successfulShards = randomIntBetween(0, totalShards); + int failedShards = totalShards - successfulShards; + boolean valid = failedShards == 0; + List queryExplanations = new ArrayList<>(totalShards); + List shardFailures = new ArrayList<>(failedShards); + for (int i=0; i queryExplSet = new HashSet<>(response.getQueryExplanation()); + assertEquals(response.isValid(), parsedResponse.isValid()); + assertEquals(response.getQueryExplanation().size(), parsedResponse.getQueryExplanation().size()); + assertTrue(queryExplSet.containsAll(parsedResponse.getQueryExplanation())); + } + + @Override + protected ValidateQueryResponse createTestInstance(int totalShards, int successfulShards, int failedShards, + List failures) { + return createRandomValidateQueryResponse(totalShards, successfulShards, failedShards, failures); + } + + @Override + public void testToXContent() { + ValidateQueryResponse response = createTestInstance(10, 10, 0, new ArrayList<>()); + String output = Strings.toString(response); + assertEquals("{\"_shards\":{\"total\":10,\"successful\":10,\"failed\":0},\"valid\":true}", output); + } +}