REST high-level client: add validate query API (#31077)

Adds the validate query API to the high level rest client.
This commit is contained in:
Sohaib Iftikhar 2018-06-18 15:59:29 +02:00 committed by Nik Everett
parent 47095357bc
commit c4f8df3ad6
13 changed files with 618 additions and 20 deletions

View File

@ -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.get.GetIndexTemplatesResponse;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; 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 org.elasticsearch.rest.RestStatus;
import java.io.IOException; import java.io.IOException;
@ -661,6 +663,36 @@ public final class IndicesClient {
PutIndexTemplateResponse::fromXContent, listener, emptySet()); PutIndexTemplateResponse::fromXContent, listener, emptySet());
} }
/**
* Validate a potentially expensive query without executing it.
* <p>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html"> Validate Query API
* on elastic.co</a>
* @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.
* <p>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html"> Validate Query API
* on elastic.co</a>
* @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<ValidateQueryResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(validateQueryRequest, RequestConverters::validateQuery, options,
ValidateQueryResponse::fromXContent, listener, emptySet());
}
/** /**
* Gets index templates using the Index Templates API * Gets index templates using the Index Templates API
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html"> Index Templates API * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html"> Index Templates API

View File

@ -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.shrink.ResizeType;
import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest; import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; 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.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
@ -856,6 +857,20 @@ final class RequestConverters {
return request; 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) { static Request getAlias(GetAliasesRequest getAliasesRequest) {
String[] indices = getAliasesRequest.indices() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.indices(); String[] indices = getAliasesRequest.indices() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.indices();
String[] aliases = getAliasesRequest.aliases() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.aliases(); String[] aliases = getAliasesRequest.aliases() == null ? Strings.EMPTY_ARRAY : getAliasesRequest.aliases();

View File

@ -20,6 +20,7 @@
package org.elasticsearch.client; package org.elasticsearch.client;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpPut;
import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException; 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.get.GetIndexTemplatesResponse;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; 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.index.IndexRequest;
import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.support.WriteRequest; 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.json.JsonXContent;
import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.RestStatus;
import java.io.IOException; 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]")); 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 { public void testGetIndexTemplate() throws Exception {
RestHighLevelClient client = highLevelClient(); RestHighLevelClient client = highLevelClient();

View File

@ -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.shrink.ResizeType;
import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest; import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; 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.BulkRequest;
import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.bulk.BulkShardRequest;
import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteRequest;
@ -1895,6 +1896,40 @@ public class RequestConvertersTests extends ESTestCase {
assertToXContentBody(putTemplateRequest, request.getEntity()); 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<String, String> 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 { public void testGetTemplateRequest() throws Exception {
Map<String, String> encodes = new HashMap<>(); Map<String, String> encodes = new HashMap<>();
encodes.put("log", "log"); encodes.put("log", "log");

View File

@ -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.get.GetIndexTemplatesResponse;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; 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.ActiveShardCount;
import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.DefaultShardOperationFailedException;
import org.elasticsearch.action.support.IndicesOptions; 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.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.RestStatus;
@ -2128,4 +2132,83 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase
assertTrue(latch.await(30L, TimeUnit.SECONDS)); 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<ValidateQueryResponse> listener =
new ActionListener<ValidateQueryResponse>() {
@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));
}
} }

View File

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

View File

@ -73,6 +73,7 @@ Index Management::
* <<java-rest-high-rollover-index>> * <<java-rest-high-rollover-index>>
* <<java-rest-high-indices-put-settings>> * <<java-rest-high-indices-put-settings>>
* <<java-rest-high-get-settings>> * <<java-rest-high-get-settings>>
* <<java-rest-high-indices-validate-query>>
Mapping Management:: Mapping Management::
* <<java-rest-high-put-mapping>> * <<java-rest-high-put-mapping>>
@ -103,6 +104,7 @@ include::indices/get_alias.asciidoc[]
include::indices/put_settings.asciidoc[] include::indices/put_settings.asciidoc[]
include::indices/get_settings.asciidoc[] include::indices/get_settings.asciidoc[]
include::indices/put_template.asciidoc[] include::indices/put_template.asciidoc[]
include::indices/validate_query.asciidoc[]
include::indices/get_templates.asciidoc[] include::indices/get_templates.asciidoc[]
== Cluster APIs == Cluster APIs

View File

@ -20,16 +20,57 @@
package org.elasticsearch.action.admin.indices.validate.query; package org.elasticsearch.action.admin.indices.validate.query;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Streamable; 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.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; public static final int RANDOM_SHARD = -1;
@SuppressWarnings("unchecked")
static ConstructingObjectParser<QueryExplanation, Void> 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 String index;
private int shard = RANDOM_SHARD; private int shard = RANDOM_SHARD;
@ -110,4 +151,43 @@ public class QueryExplanation implements Streamable {
exp.readFrom(in); exp.readFrom(in);
return exp; 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());
}
} }

View File

@ -27,6 +27,8 @@ import org.elasticsearch.action.support.broadcast.BroadcastRequest;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; 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.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilder;
@ -38,7 +40,7 @@ import java.util.Arrays;
* <p> * <p>
* The request requires the query to be set using {@link #query(QueryBuilder)} * The request requires the query to be set using {@link #query(QueryBuilder)}
*/ */
public class ValidateQueryRequest extends BroadcastRequest<ValidateQueryRequest> { public class ValidateQueryRequest extends BroadcastRequest<ValidateQueryRequest> implements ToXContentObject {
private QueryBuilder query = new MatchAllQueryBuilder(); private QueryBuilder query = new MatchAllQueryBuilder();
@ -179,4 +181,12 @@ public class ValidateQueryRequest extends BroadcastRequest<ValidateQueryRequest>
return "[" + Arrays.toString(indices) + "]" + Arrays.toString(types) + ", query[" + query + "], explain:" + explain + return "[" + Arrays.toString(indices) + "]" + Arrays.toString(types) + ", query[" + query + "], explain:" + explain +
", rewrite:" + rewrite + ", all_shards:" + allShards; ", 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();
}
} }

View File

@ -21,16 +21,22 @@ package org.elasticsearch.action.admin.indices.validate.query;
import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.DefaultShardOperationFailedException;
import org.elasticsearch.action.support.broadcast.BroadcastResponse; 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.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.elasticsearch.action.admin.indices.validate.query.QueryExplanation.readQueryExplanation; 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. * 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 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 VALID_FIELD = "valid";
public static final String EXPLANATIONS_FIELD = "explanations"; 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<ValidateQueryResponse, Void> PARSER = new ConstructingObjectParser<>(
"validate_query",
true,
arg -> {
BroadcastResponse response = (BroadcastResponse) arg[0];
return
new ValidateQueryResponse(
(boolean)arg[1],
(List<QueryExplanation>)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; private boolean valid;
@ -112,22 +139,14 @@ public class ValidateQueryResponse extends BroadcastResponse {
builder.startArray(EXPLANATIONS_FIELD); builder.startArray(EXPLANATIONS_FIELD);
for (QueryExplanation explanation : getQueryExplanation()) { for (QueryExplanation explanation : getQueryExplanation()) {
builder.startObject(); builder.startObject();
if (explanation.getIndex() != null) { explanation.toXContent(builder, params);
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());
}
builder.endObject(); builder.endObject();
} }
builder.endArray(); builder.endArray();
} }
} }
public static ValidateQueryResponse fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
} }

View File

@ -19,6 +19,7 @@
package org.elasticsearch.rest.action.admin.indices; 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.ValidateQueryRequest;
import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse;
import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.IndicesOptions;
@ -101,7 +102,7 @@ public class RestValidateQueryAction extends BaseRestHandler {
builder.startObject(); builder.startObject();
builder.field(ValidateQueryResponse.VALID_FIELD, false); builder.field(ValidateQueryResponse.VALID_FIELD, false);
if (explain) { if (explain) {
builder.field(ValidateQueryResponse.ERROR_FIELD, error); builder.field(QueryExplanation.ERROR_FIELD, error);
} }
builder.endObject(); builder.endObject();
return new BytesRestResponse(OK, builder); return new BytesRestResponse(OK, builder);

View File

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

View File

@ -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<ValidateQueryResponse> {
private static ValidateQueryResponse createRandomValidateQueryResponse(
int totalShards, int successfulShards, int failedShards, List<DefaultShardOperationFailedException> failures) {
boolean valid = failedShards == 0;
List<QueryExplanation> 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<QueryExplanation> queryExplanations = new ArrayList<>(totalShards);
List<DefaultShardOperationFailedException> shardFailures = new ArrayList<>(failedShards);
for (int i=0; i<successfulShards; i++) {
QueryExplanation queryExplanation = QueryExplanationTests.createRandomQueryExplanation(true);
queryExplanations.add(queryExplanation);
}
for (int i=0; i<failedShards; i++) {
QueryExplanation queryExplanation = QueryExplanationTests.createRandomQueryExplanation(false);
ElasticsearchException exc = new ElasticsearchException("some_error_" + randomInt());
shardFailures.add(
new DefaultShardOperationFailedException(
queryExplanation.getIndex(), queryExplanation.getShard(),
exc
)
);
queryExplanations.add(queryExplanation);
}
Collections.shuffle(queryExplanations, random());
return new ValidateQueryResponse(valid, queryExplanations, totalShards, successfulShards, failedShards, shardFailures);
}
@Override
protected ValidateQueryResponse doParseInstance(XContentParser parser) throws IOException {
return ValidateQueryResponse.fromXContent(parser);
}
@Override
protected ValidateQueryResponse createTestInstance() {
return createRandomValidateQueryResponse();
}
@Override
protected void assertEqualInstances(ValidateQueryResponse response, ValidateQueryResponse parsedResponse) {
super.assertEqualInstances(response, parsedResponse);
Set<QueryExplanation> 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<DefaultShardOperationFailedException> 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);
}
}