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 6e96ecd9c5c..839d86bf9f1 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 @@ -103,6 +103,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.rankeval.RankEvalRequest; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.tasks.TaskId; @@ -604,6 +605,21 @@ final class RequestConverters { request.setEntity(createEntity(searchTemplateRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } + + static Request multiSearchTemplate(MultiSearchTemplateRequest multiSearchTemplateRequest) throws IOException { + Request request = new Request(HttpPost.METHOD_NAME, "/_msearch/template"); + + Params params = new Params(request); + params.putParam(RestSearchAction.TYPED_KEYS_PARAM, "true"); + if (multiSearchTemplateRequest.maxConcurrentSearchRequests() != MultiSearchRequest.MAX_CONCURRENT_SEARCH_REQUESTS_DEFAULT) { + params.putParam("max_concurrent_searches", Integer.toString(multiSearchTemplateRequest.maxConcurrentSearchRequests())); + } + + XContent xContent = REQUEST_BODY_CONTENT_TYPE.xContent(); + byte[] source = MultiSearchTemplateRequest.writeMultiLineFormat(multiSearchTemplateRequest, xContent); + request.setEntity(new ByteArrayEntity(source, createContentType(xContent.type()))); + return request; + } static Request existsAlias(GetAliasesRequest getAliasesRequest) { if ((getAliasesRequest.indices() == null || getAliasesRequest.indices().length == 0) && diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 7d9b02b06a1..48277d67e6d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -68,6 +68,8 @@ import org.elasticsearch.index.rankeval.RankEvalResponse; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; +import org.elasticsearch.script.mustache.MultiSearchTemplateResponse; import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.aggregations.Aggregation; @@ -666,6 +668,32 @@ public class RestHighLevelClient implements Closeable { emptySet()); } + + /** + * Executes a request using the Multi Search Template API. + * + * See Multi Search Template API + * on elastic.co. + */ + public final MultiSearchTemplateResponse multiSearchTemplate(MultiSearchTemplateRequest multiSearchTemplateRequest, + RequestOptions options) throws IOException { + return performRequestAndParseEntity(multiSearchTemplateRequest, RequestConverters::multiSearchTemplate, + options, MultiSearchTemplateResponse::fromXContext, emptySet()); + } + + /** + * Asynchronously executes a request using the Multi Search Template API + * + * See Multi Search Template API + * on elastic.co. + */ + public final void multiSearchTemplateAsync(MultiSearchTemplateRequest multiSearchTemplateRequest, + RequestOptions options, + ActionListener listener) { + performRequestAsyncAndParseEntity(multiSearchTemplateRequest, RequestConverters::multiSearchTemplate, + options, MultiSearchTemplateResponse::fromXContext, listener, emptySet()); + } + /** * Asynchronously executes a request using the Ranking Evaluation API. * See Ranking Evaluation API 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 5887feefa63..e838989a0c8 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 @@ -124,6 +124,7 @@ import org.elasticsearch.index.rankeval.RestRankEvalAction; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; @@ -1373,7 +1374,53 @@ public class RequestConvertersTests extends ESTestCase { assertEquals(Collections.emptyMap(), request.getParameters()); assertToXContentBody(searchTemplateRequest, request.getEntity()); } + + public void testMultiSearchTemplate() throws Exception { + final int numSearchRequests = randomIntBetween(1, 10); + MultiSearchTemplateRequest multiSearchTemplateRequest = new MultiSearchTemplateRequest(); + + for (int i = 0; i < numSearchRequests; i++) { + // Create a random request. + String[] indices = randomIndicesNames(0, 5); + SearchRequest searchRequest = new SearchRequest(indices); + + Map expectedParams = new HashMap<>(); + setRandomSearchParams(searchRequest, expectedParams); + + // scroll is not supported in the current msearch or msearchtemplate api, so unset it: + searchRequest.scroll((Scroll) null); + // batched reduce size is currently not set-able on a per-request basis as it is a query string parameter only + searchRequest.setBatchedReduceSize(SearchRequest.DEFAULT_BATCHED_REDUCE_SIZE); + + setRandomIndicesOptions(searchRequest::indicesOptions, searchRequest::indicesOptions, expectedParams); + + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(searchRequest); + + searchTemplateRequest.setScript("{\"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" }}}"); + searchTemplateRequest.setScriptType(ScriptType.INLINE); + searchTemplateRequest.setProfile(randomBoolean()); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "name"); + scriptParams.put("value", randomAlphaOfLengthBetween(2, 5)); + searchTemplateRequest.setScriptParams(scriptParams); + + multiSearchTemplateRequest.add(searchTemplateRequest); + } + Request multiRequest = RequestConverters.multiSearchTemplate(multiSearchTemplateRequest); + + assertEquals(HttpPost.METHOD_NAME, multiRequest.getMethod()); + assertEquals("/_msearch/template", multiRequest.getEndpoint()); + List searchRequests = multiSearchTemplateRequest.requests(); + assertEquals(numSearchRequests, searchRequests.size()); + + HttpEntity actualEntity = multiRequest.getEntity(); + byte[] expectedBytes = MultiSearchTemplateRequest.writeMultiLineFormat(multiSearchTemplateRequest, XContentType.JSON.xContent()); + assertEquals(XContentType.JSON.mediaTypeWithoutParameters(), actualEntity.getContentType().getValue()); + assertEquals(new BytesArray(expectedBytes), new BytesArray(EntityUtils.toByteArray(actualEntity))); + } + public void testExistsAlias() { GetAliasesRequest getAliasesRequest = new GetAliasesRequest(); String[] indices = randomBoolean() ? null : randomIndicesNames(0, 5); @@ -2385,7 +2432,7 @@ public class RequestConvertersTests extends ESTestCase { expectedParams.put("preference", searchRequest.preference()); } if (randomBoolean()) { - searchRequest.searchType(randomFrom(SearchType.values())); + searchRequest.searchType(randomFrom(SearchType.CURRENTLY_SUPPORTED)); } expectedParams.put("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT)); if (randomBoolean()) { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index b83cc263be9..18a43ffa8d4 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -54,6 +54,9 @@ import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; +import org.elasticsearch.script.mustache.MultiSearchTemplateResponse; +import org.elasticsearch.script.mustache.MultiSearchTemplateResponse.Item; import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.SearchHit; @@ -875,6 +878,105 @@ public class SearchIT extends ESRestHighLevelClientTestCase { assertToXContentEquivalent(expectedSource, actualSource, XContentType.JSON); } + + + public void testMultiSearchTemplate() throws Exception { + MultiSearchTemplateRequest multiSearchTemplateRequest = new MultiSearchTemplateRequest(); + + SearchTemplateRequest goodRequest = new SearchTemplateRequest(); + goodRequest.setRequest(new SearchRequest("index")); + goodRequest.setScriptType(ScriptType.INLINE); + goodRequest.setScript( + "{" + + " \"query\": {" + + " \"match\": {" + + " \"num\": {{number}}" + + " }" + + " }" + + "}"); + Map scriptParams = new HashMap<>(); + scriptParams.put("number", 10); + goodRequest.setScriptParams(scriptParams); + goodRequest.setExplain(true); + goodRequest.setProfile(true); + multiSearchTemplateRequest.add(goodRequest); + + + SearchTemplateRequest badRequest = new SearchTemplateRequest(); + badRequest.setRequest(new SearchRequest("index")); + badRequest.setScriptType(ScriptType.INLINE); + badRequest.setScript("{ NOT VALID JSON {{number}} }"); + scriptParams = new HashMap<>(); + scriptParams.put("number", 10); + badRequest.setScriptParams(scriptParams); + + multiSearchTemplateRequest.add(badRequest); + + MultiSearchTemplateResponse multiSearchTemplateResponse = + execute(multiSearchTemplateRequest, highLevelClient()::multiSearchTemplate, + highLevelClient()::multiSearchTemplateAsync); + + Item[] responses = multiSearchTemplateResponse.getResponses(); + + assertEquals(2, responses.length); + + + assertNull(responses[0].getResponse().getSource()); + SearchResponse goodResponse =responses[0].getResponse().getResponse(); + assertNotNull(goodResponse); + assertThat(responses[0].isFailure(), Matchers.is(false)); + assertEquals(1, goodResponse.getHits().totalHits); + assertEquals(1, goodResponse.getHits().getHits().length); + assertThat(goodResponse.getHits().getMaxScore(), greaterThan(0f)); + SearchHit hit = goodResponse.getHits().getHits()[0]; + assertNotNull(hit.getExplanation()); + assertFalse(goodResponse.getProfileResults().isEmpty()); + + + assertNull(responses[0].getResponse().getSource()); + assertThat(responses[1].isFailure(), Matchers.is(true)); + assertNotNull(responses[1].getFailureMessage()); + assertThat(responses[1].getFailureMessage(), containsString("json_parse_exception")); + } + + public void testMultiSearchTemplateAllBad() throws Exception { + MultiSearchTemplateRequest multiSearchTemplateRequest = new MultiSearchTemplateRequest(); + + SearchTemplateRequest badRequest1 = new SearchTemplateRequest(); + badRequest1.setRequest(new SearchRequest("index")); + badRequest1.setScriptType(ScriptType.INLINE); + badRequest1.setScript( + "{" + + " \"query\": {" + + " \"match\": {" + + " \"num\": {{number}}" + + " }" + + " }" + + "}"); + Map scriptParams = new HashMap<>(); + scriptParams.put("number", "BAD NUMBER"); + badRequest1.setScriptParams(scriptParams); + multiSearchTemplateRequest.add(badRequest1); + + + SearchTemplateRequest badRequest2 = new SearchTemplateRequest(); + badRequest2.setRequest(new SearchRequest("index")); + badRequest2.setScriptType(ScriptType.INLINE); + badRequest2.setScript("BAD QUERY TEMPLATE"); + scriptParams = new HashMap<>(); + scriptParams.put("number", "BAD NUMBER"); + badRequest2.setScriptParams(scriptParams); + + multiSearchTemplateRequest.add(badRequest2); + + // The whole HTTP request should fail if no nested search requests are valid + ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, + () -> execute(multiSearchTemplateRequest, highLevelClient()::multiSearchTemplate, + highLevelClient()::multiSearchTemplateAsync)); + + assertEquals(RestStatus.BAD_REQUEST, exception.status()); + assertThat(exception.getMessage(), containsString("no requests added")); + } public void testExplain() throws IOException { { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java index 3e484b0c86d..308d9ba5699 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java @@ -71,6 +71,9 @@ import org.elasticsearch.index.rankeval.RatedRequest; import org.elasticsearch.index.rankeval.RatedSearchHit; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; +import org.elasticsearch.script.mustache.MultiSearchTemplateResponse; +import org.elasticsearch.script.mustache.MultiSearchTemplateResponse.Item; import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.Scroll; @@ -773,21 +776,7 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase { RestHighLevelClient client = highLevelClient(); RestClient restClient = client(); - // tag::register-script - Request scriptRequest = new Request("POST", "_scripts/title_search"); - scriptRequest.setJsonEntity( - "{" + - " \"script\": {" + - " \"lang\": \"mustache\"," + - " \"source\": {" + - " \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," + - " \"size\" : \"{{size}}\"" + - " }" + - " }" + - "}"); - Response scriptResponse = restClient.performRequest(scriptRequest); - // end::register-script - assertEquals(RestStatus.OK.getStatus(), scriptResponse.getStatusLine().getStatusCode()); + registerQueryScript(restClient); // tag::search-template-request-stored SearchTemplateRequest request = new SearchTemplateRequest(); @@ -840,6 +829,144 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + + public void testMultiSearchTemplateWithInlineScript() throws Exception { + indexSearchTestData(); + RestHighLevelClient client = highLevelClient(); + + // tag::multi-search-template-request-inline + String [] searchTerms = {"elasticsearch", "logstash", "kibana"}; + + MultiSearchTemplateRequest multiRequest = new MultiSearchTemplateRequest(); // <1> + for (String searchTerm : searchTerms) { + SearchTemplateRequest request = new SearchTemplateRequest(); // <2> + request.setRequest(new SearchRequest("posts")); + + request.setScriptType(ScriptType.INLINE); + request.setScript( + "{" + + " \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," + + " \"size\" : \"{{size}}\"" + + "}"); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "title"); + scriptParams.put("value", searchTerm); + scriptParams.put("size", 5); + request.setScriptParams(scriptParams); + + multiRequest.add(request); // <3> + } + // end::multi-search-template-request-inline + + // tag::multi-search-template-request-sync + MultiSearchTemplateResponse multiResponse = client.multiSearchTemplate(multiRequest, RequestOptions.DEFAULT); + // end::multi-search-template-request-sync + + // tag::multi-search-template-response + for (Item item : multiResponse.getResponses()) { // <1> + if (item.isFailure()) { + String error = item.getFailureMessage(); // <2> + } else { + SearchTemplateResponse searchTemplateResponse = item.getResponse(); // <3> + SearchResponse searchResponse = searchTemplateResponse.getResponse(); + searchResponse.getHits(); + } + } + // end::multi-search-template-response + + assertNotNull(multiResponse); + assertEquals(searchTerms.length, multiResponse.getResponses().length); + assertNotNull(multiResponse.getResponses()[0]); + SearchResponse searchResponse = multiResponse.getResponses()[0].getResponse().getResponse(); + assertTrue(searchResponse.getHits().totalHits > 0); + + } + + public void testMultiSearchTemplateWithStoredScript() throws Exception { + indexSearchTestData(); + RestHighLevelClient client = highLevelClient(); + RestClient restClient = client(); + + registerQueryScript(restClient); + + // tag::multi-search-template-request-stored + MultiSearchTemplateRequest multiRequest = new MultiSearchTemplateRequest(); + + String [] searchTerms = {"elasticsearch", "logstash", "kibana"}; + for (String searchTerm : searchTerms) { + + SearchTemplateRequest request = new SearchTemplateRequest(); + request.setRequest(new SearchRequest("posts")); + + request.setScriptType(ScriptType.STORED); + request.setScript("title_search"); + + Map params = new HashMap<>(); + params.put("field", "title"); + params.put("value", searchTerm); + params.put("size", 5); + request.setScriptParams(params); + multiRequest.add(request); + } + // end::multi-search-template-request-stored + + + + + // tag::multi-search-template-execute + MultiSearchTemplateResponse multiResponse = client.multiSearchTemplate(multiRequest, RequestOptions.DEFAULT); + // end::multi-search-template-execute + + assertNotNull(multiResponse); + assertEquals(searchTerms.length, multiResponse.getResponses().length); + assertNotNull(multiResponse.getResponses()[0]); + SearchResponse searchResponse = multiResponse.getResponses()[0].getResponse().getResponse(); + assertTrue(searchResponse.getHits().totalHits > 0); + + // tag::multi-search-template-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(MultiSearchTemplateResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::multi-search-template-execute-listener + + // Replace the empty listener by a blocking listener for tests. + CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::multi-search-template-execute-async + client.multiSearchTemplateAsync(multiRequest, RequestOptions.DEFAULT, listener); + // end::multi-search-template-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + + protected void registerQueryScript(RestClient restClient) throws IOException { + // tag::register-script + Request scriptRequest = new Request("POST", "_scripts/title_search"); + scriptRequest.setJsonEntity( + "{" + + " \"script\": {" + + " \"lang\": \"mustache\"," + + " \"source\": {" + + " \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," + + " \"size\" : \"{{size}}\"" + + " }" + + " }" + + "}"); + Response scriptResponse = restClient.performRequest(scriptRequest); + // end::register-script + assertEquals(RestStatus.OK.getStatus(), scriptResponse.getStatusLine().getStatusCode()); + } + public void testExplain() throws Exception { indexSearchTestData(); diff --git a/docs/java-rest/high-level/search/multi-search-template.asciidoc b/docs/java-rest/high-level/search/multi-search-template.asciidoc new file mode 100644 index 00000000000..c5133f6614e --- /dev/null +++ b/docs/java-rest/high-level/search/multi-search-template.asciidoc @@ -0,0 +1,81 @@ +[[java-rest-high-multi-search-template]] +=== Multi-Search-Template API + +The `multiSearchTemplate` API executes multiple <> +requests in a single http request in parallel. + +[[java-rest-high-multi-search-template-request]] +==== Multi-Search-Template Request + +The `MultiSearchTemplateRequest` is built empty and you add all of the searches that +you wish to execute to it: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[multi-search-template-request-inline] +-------------------------------------------------- +<1> Create an empty `MultiSearchTemplateRequest`. +<2> Create one or more `SearchTemplateRequest` objects and populate them just like you +would for a regular <>. +<3> Add the `SearchTemplateRequest` to the `MultiSearchTemplateRequest`. + +===== Optional arguments + +The multiSearchTemplate's `max_concurrent_searches` request parameter can be used to control +the maximum number of concurrent searches the multi search api will execute. +This default is based on the number of data nodes and the default search thread pool size. + +[[java-rest-high-multi-search-template-sync]] +==== Synchronous Execution + +The `multiSearchTemplate` method executes `MultiSearchTemplateRequest`s synchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[multi-search-template-request-sync] +-------------------------------------------------- + +[[java-rest-high-multi-search-template-async]] +==== Asynchronous Execution + +The `multiSearchTemplateAsync` method executes `MultiSearchTemplateRequest`s asynchronously, +calling the provided `ActionListener` when the response is ready. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[multi-search-template-execute-async] +-------------------------------------------------- +The parameters are the `MultiSearchTemplateRequest` 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 `MultiSearchTemplateResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[multi-search-template-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. +<2> Called when the whole `MultiSearchTemplateRequest` fails. + +==== MultiSearchTemplateResponse + +The `MultiSearchTemplateResponse` that is returned by executing the `multiSearchTemplate` method contains +a `MultiSearchTemplateResponse.Item` for each `SearchTemplateRequest` in the +`MultiSearchTemplateRequest`. Each `MultiSearchTemplateResponse.Item` contains an +exception in `getFailure` if the request failed or a +<> in `getResponse` if +the request succeeded: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[multi-search-template-response] +-------------------------------------------------- +<1> An array of responses is returned - one response for each request +<2> Failed search template requests have error messages +<3> Successful requests contain a <> in +`getResponse`. diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 1a126d82cbb..d2484db1d78 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -32,6 +32,7 @@ The Java High Level REST Client supports the following Search APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -41,6 +42,7 @@ include::search/search.asciidoc[] include::search/scroll.asciidoc[] include::search/multi-search.asciidoc[] include::search/search-template.asciidoc[] +include::search/multi-search-template.asciidoc[] include::search/field-caps.asciidoc[] include::search/rank-eval.asciidoc[] include::search/explain.asciidoc[] diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java index bd294db6d29..caa9fa4831a 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java @@ -23,13 +23,21 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.CompositeIndicesRequest; +import org.elasticsearch.action.search.MultiSearchRequest; +import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -126,4 +134,39 @@ public class MultiSearchTemplateRequest extends ActionRequest implements Composi } out.writeStreamableList(requests); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MultiSearchTemplateRequest that = (MultiSearchTemplateRequest) o; + return maxConcurrentSearchRequests == that.maxConcurrentSearchRequests && + Objects.equals(requests, that.requests) && + Objects.equals(indicesOptions, that.indicesOptions); + } + + @Override + public int hashCode() { + return Objects.hash(maxConcurrentSearchRequests, requests, indicesOptions); + } + + public static byte[] writeMultiLineFormat(MultiSearchTemplateRequest multiSearchTemplateRequest, + XContent xContent) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + for (SearchTemplateRequest templateRequest : multiSearchTemplateRequest.requests()) { + final SearchRequest searchRequest = templateRequest.getRequest(); + try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) { + MultiSearchRequest.writeSearchRequestParams(searchRequest, xContentBuilder); + BytesReference.bytes(xContentBuilder).writeTo(output); + } + output.write(xContent.streamSeparator()); + try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) { + templateRequest.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + BytesReference.bytes(xContentBuilder).writeTo(output); + } + output.write(xContent.streamSeparator()); + } + return output.toByteArray(); + } + } diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponse.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponse.java index b5bc86679ae..74f3bc743ae 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponse.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponse.java @@ -22,6 +22,7 @@ package org.elasticsearch.script.mustache; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -31,6 +32,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Arrays; @@ -106,6 +108,13 @@ public class MultiSearchTemplateResponse extends ActionResponse implements Itera public Exception getFailure() { return exception; } + + @Override + public String toString() { + return "Item [response=" + response + ", exception=" + exception + "]"; + } + + } private Item[] items; @@ -117,7 +126,7 @@ public class MultiSearchTemplateResponse extends ActionResponse implements Itera public MultiSearchTemplateResponse(Item[] items, long tookInMillis) { this.items = items; this.tookInMillis = tookInMillis; - } + } @Override public Iterator iterator() { @@ -184,6 +193,23 @@ public class MultiSearchTemplateResponse extends ActionResponse implements Itera static final class Fields { static final String RESPONSES = "responses"; } + + public static MultiSearchTemplateResponse fromXContext(XContentParser parser) { + //The MultiSearchTemplateResponse is identical to the multi search response so we reuse the parsing logic in multi search response + MultiSearchResponse mSearchResponse = MultiSearchResponse.fromXContext(parser); + org.elasticsearch.action.search.MultiSearchResponse.Item[] responses = mSearchResponse.getResponses(); + Item[] templateResponses = new Item[responses.length]; + int i = 0; + for (org.elasticsearch.action.search.MultiSearchResponse.Item item : responses) { + SearchTemplateResponse stResponse = null; + if(item.getResponse() != null){ + stResponse = new SearchTemplateResponse(); + stResponse.setResponse(item.getResponse()); + } + templateResponses[i++] = new Item(stResponse, item.getFailure()); + } + return new MultiSearchTemplateResponse(templateResponses, mSearchResponse.getTook().millis()); + } @Override public String toString() { diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java index 500a5a399ef..6d19afbfd6f 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java @@ -66,6 +66,11 @@ public class SearchTemplateResponse extends ActionResponse implements StatusToXC public boolean hasResponse() { return response != null; + } + + @Override + public String toString() { + return "SearchTemplateResponse [source=" + source + ", response=" + response + "]"; } @Override diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequestTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequestTests.java index 1ff54796237..ee967237d3c 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequestTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequestTests.java @@ -19,14 +19,22 @@ package org.elasticsearch.script.mustache; +import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.Scroll; +import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.StreamsUtils; import org.elasticsearch.test.rest.FakeRestRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -97,5 +105,57 @@ public class MultiSearchTemplateRequestTests extends ESTestCase { expectThrows(IllegalArgumentException.class, () -> request.maxConcurrentSearchRequests(randomIntBetween(Integer.MIN_VALUE, 0))); } + + public void testMultiSearchTemplateToJson() throws Exception { + final int numSearchRequests = randomIntBetween(1, 10); + MultiSearchTemplateRequest multiSearchTemplateRequest = new MultiSearchTemplateRequest(); + for (int i = 0; i < numSearchRequests; i++) { + // Create a random request. + String[] indices = {"test"}; + SearchRequest searchRequest = new SearchRequest(indices); + // scroll is not supported in the current msearch or msearchtemplate api, so unset it: + searchRequest.scroll((Scroll) null); + // batched reduce size is currently not set-able on a per-request basis as it is a query string parameter only + searchRequest.setBatchedReduceSize(SearchRequest.DEFAULT_BATCHED_REDUCE_SIZE); + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(searchRequest); + + searchTemplateRequest.setScript("{\"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" }}}"); + searchTemplateRequest.setScriptType(ScriptType.INLINE); + searchTemplateRequest.setProfile(randomBoolean()); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "name"); + scriptParams.put("value", randomAlphaOfLengthBetween(2, 5)); + searchTemplateRequest.setScriptParams(scriptParams); + + multiSearchTemplateRequest.add(searchTemplateRequest); + } + + //Serialize the request + String serialized = toJsonString(multiSearchTemplateRequest); + + //Deserialize the request + RestRequest restRequest = new FakeRestRequest.Builder(xContentRegistry()) + .withContent(new BytesArray(serialized), XContentType.JSON).build(); + MultiSearchTemplateRequest deser = RestMultiSearchTemplateAction.parseRequest(restRequest, true); + + // For object equality purposes need to set the search requests' source to non-null + for (SearchTemplateRequest str : deser.requests()) { + SearchRequest sr = str.getRequest(); + if (sr.source() == null) { + sr.source(new SearchSourceBuilder()); + } + } + // Compare the deserialized request object with the original request object + assertEquals(multiSearchTemplateRequest, deser); + + // Finally, serialize the deserialized request to compare JSON equivalence (in case Object.equals() fails to reveal a discrepancy) + assertEquals(serialized, toJsonString(deser)); + } + + protected String toJsonString(MultiSearchTemplateRequest multiSearchTemplateRequest) throws IOException { + byte[] bytes = MultiSearchTemplateRequest.writeMultiLineFormat(multiSearchTemplateRequest, XContentType.JSON.xContent()); + return new String(bytes, StandardCharsets.UTF_8); + } } diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java new file mode 100644 index 00000000000..2c67dd4709b --- /dev/null +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java @@ -0,0 +1,138 @@ +/* + * 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.script.mustache; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.internal.InternalSearchResponse; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class MultiSearchTemplateResponseTests extends AbstractXContentTestCase { + + @Override + protected MultiSearchTemplateResponse createTestInstance() { + int numItems = randomIntBetween(0, 128); + long overallTookInMillis = randomNonNegativeLong(); + MultiSearchTemplateResponse.Item[] items = new MultiSearchTemplateResponse.Item[numItems]; + for (int i = 0; i < numItems; i++) { + // Creating a minimal response is OK, because SearchResponse self + // is tested elsewhere. + long tookInMillis = randomNonNegativeLong(); + int totalShards = randomIntBetween(1, Integer.MAX_VALUE); + int successfulShards = randomIntBetween(0, totalShards); + int skippedShards = totalShards - successfulShards; + InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty(); + SearchResponse.Clusters clusters = new SearchResponse.Clusters(totalShards, successfulShards, skippedShards); + SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse(); + SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, totalShards, + successfulShards, skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, clusters); + searchTemplateResponse.setResponse(searchResponse); + items[i] = new MultiSearchTemplateResponse.Item(searchTemplateResponse, null); + } + return new MultiSearchTemplateResponse(items, overallTookInMillis); + } + + + private static MultiSearchTemplateResponse createTestInstanceWithFailures() { + int numItems = randomIntBetween(0, 128); + long overallTookInMillis = randomNonNegativeLong(); + MultiSearchTemplateResponse.Item[] items = new MultiSearchTemplateResponse.Item[numItems]; + for (int i = 0; i < numItems; i++) { + if (randomBoolean()) { + // Creating a minimal response is OK, because SearchResponse self + // is tested elsewhere. + long tookInMillis = randomNonNegativeLong(); + int totalShards = randomIntBetween(1, Integer.MAX_VALUE); + int successfulShards = randomIntBetween(0, totalShards); + int skippedShards = totalShards - successfulShards; + InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty(); + SearchResponse.Clusters clusters = new SearchResponse.Clusters(totalShards, successfulShards, skippedShards); + SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse(); + SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, totalShards, + successfulShards, skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, clusters); + searchTemplateResponse.setResponse(searchResponse); + items[i] = new MultiSearchTemplateResponse.Item(searchTemplateResponse, null); + } else { + items[i] = new MultiSearchTemplateResponse.Item(null, new ElasticsearchException("an error")); + } + } + return new MultiSearchTemplateResponse(items, overallTookInMillis); + } + + @Override + protected MultiSearchTemplateResponse doParseInstance(XContentParser parser) throws IOException { + return MultiSearchTemplateResponse.fromXContext(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { + return field -> field.startsWith("responses"); + } + + @Override + protected void assertEqualInstances(MultiSearchTemplateResponse expectedInstance, MultiSearchTemplateResponse newInstance) { + assertThat(newInstance.getTook(), equalTo(expectedInstance.getTook())); + assertThat(newInstance.getResponses().length, equalTo(expectedInstance.getResponses().length)); + for (int i = 0; i < expectedInstance.getResponses().length; i++) { + MultiSearchTemplateResponse.Item expectedItem = expectedInstance.getResponses()[i]; + MultiSearchTemplateResponse.Item actualItem = newInstance.getResponses()[i]; + if (expectedItem.isFailure()) { + assertThat(actualItem.getResponse(), nullValue()); + assertThat(actualItem.getFailureMessage(), containsString(expectedItem.getFailureMessage())); + } else { + assertThat(actualItem.getResponse().toString(), equalTo(expectedItem.getResponse().toString())); + assertThat(actualItem.getFailure(), nullValue()); + } + } + } + + /** + * Test parsing {@link MultiSearchTemplateResponse} with inner failures as they don't support asserting on xcontent equivalence, given + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier instanceSupplier = MultiSearchTemplateResponseTests::createTestInstanceWithFailures; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); + } + +} diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index c7c711253ba..056c4c29c7a 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -270,42 +270,7 @@ public class MultiSearchRequest extends ActionRequest implements CompositeIndice ByteArrayOutputStream output = new ByteArrayOutputStream(); for (SearchRequest request : multiSearchRequest.requests()) { try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) { - xContentBuilder.startObject(); - if (request.indices() != null) { - xContentBuilder.field("index", request.indices()); - } - if (request.indicesOptions() != null && request.indicesOptions() != SearchRequest.DEFAULT_INDICES_OPTIONS) { - if (request.indicesOptions().expandWildcardsOpen() && request.indicesOptions().expandWildcardsClosed()) { - xContentBuilder.field("expand_wildcards", "all"); - } else if (request.indicesOptions().expandWildcardsOpen()) { - xContentBuilder.field("expand_wildcards", "open"); - } else if (request.indicesOptions().expandWildcardsClosed()) { - xContentBuilder.field("expand_wildcards", "closed"); - } else { - xContentBuilder.field("expand_wildcards", "none"); - } - xContentBuilder.field("ignore_unavailable", request.indicesOptions().ignoreUnavailable()); - xContentBuilder.field("allow_no_indices", request.indicesOptions().allowNoIndices()); - } - if (request.types() != null) { - xContentBuilder.field("types", request.types()); - } - if (request.searchType() != null) { - xContentBuilder.field("search_type", request.searchType().name().toLowerCase(Locale.ROOT)); - } - if (request.requestCache() != null) { - xContentBuilder.field("request_cache", request.requestCache()); - } - if (request.preference() != null) { - xContentBuilder.field("preference", request.preference()); - } - if (request.routing() != null) { - xContentBuilder.field("routing", request.routing()); - } - if (request.allowPartialSearchResults() != null) { - xContentBuilder.field("allow_partial_search_results", request.allowPartialSearchResults()); - } - xContentBuilder.endObject(); + writeSearchRequestParams(request, xContentBuilder); BytesReference.bytes(xContentBuilder).writeTo(output); } output.write(xContent.streamSeparator()); @@ -322,5 +287,44 @@ public class MultiSearchRequest extends ActionRequest implements CompositeIndice } return output.toByteArray(); } + + public static void writeSearchRequestParams(SearchRequest request, XContentBuilder xContentBuilder) throws IOException { + xContentBuilder.startObject(); + if (request.indices() != null) { + xContentBuilder.field("index", request.indices()); + } + if (request.indicesOptions() != null && request.indicesOptions() != SearchRequest.DEFAULT_INDICES_OPTIONS) { + if (request.indicesOptions().expandWildcardsOpen() && request.indicesOptions().expandWildcardsClosed()) { + xContentBuilder.field("expand_wildcards", "all"); + } else if (request.indicesOptions().expandWildcardsOpen()) { + xContentBuilder.field("expand_wildcards", "open"); + } else if (request.indicesOptions().expandWildcardsClosed()) { + xContentBuilder.field("expand_wildcards", "closed"); + } else { + xContentBuilder.field("expand_wildcards", "none"); + } + xContentBuilder.field("ignore_unavailable", request.indicesOptions().ignoreUnavailable()); + xContentBuilder.field("allow_no_indices", request.indicesOptions().allowNoIndices()); + } + if (request.types() != null) { + xContentBuilder.field("types", request.types()); + } + if (request.searchType() != null) { + xContentBuilder.field("search_type", request.searchType().name().toLowerCase(Locale.ROOT)); + } + if (request.requestCache() != null) { + xContentBuilder.field("request_cache", request.requestCache()); + } + if (request.preference() != null) { + xContentBuilder.field("preference", request.preference()); + } + if (request.routing() != null) { + xContentBuilder.field("routing", request.routing()); + } + if (request.allowPartialSearchResults() != null) { + xContentBuilder.field("allow_partial_search_results", request.allowPartialSearchResults()); + } + xContentBuilder.endObject(); + } } diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index a390538ec29..e67517c4852 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -60,6 +60,7 @@ public final class SearchRequest extends ActionRequest implements IndicesRequest private static final ToXContent.Params FORMAT_PARAMS = new ToXContent.MapParams(Collections.singletonMap("pretty", "false")); public static final int DEFAULT_PRE_FILTER_SHARD_SIZE = 128; + public static final int DEFAULT_BATCHED_REDUCE_SIZE = 512; private SearchType searchType = SearchType.DEFAULT; @@ -79,7 +80,7 @@ public final class SearchRequest extends ActionRequest implements IndicesRequest private Scroll scroll; - private int batchedReduceSize = 512; + private int batchedReduceSize = DEFAULT_BATCHED_REDUCE_SIZE; private int maxConcurrentShardRequests = 0; diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index 9ad8a20cb17..2a97798764e 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -422,7 +422,7 @@ public class SearchResponse extends ActionResponse implements StatusToXContentOb private final int successful; private final int skipped; - Clusters(int total, int successful, int skipped) { + public Clusters(int total, int successful, int skipped) { assert total >= 0 && successful >= 0 && skipped >= 0 : "total: " + total + " successful: " + successful + " skipped: " + skipped; assert successful <= total && skipped == total - successful diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchType.java b/server/src/main/java/org/elasticsearch/action/search/SearchType.java index b8001204087..910bc3d676a 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchType.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchType.java @@ -50,6 +50,11 @@ public enum SearchType { */ public static final SearchType DEFAULT = QUERY_THEN_FETCH; + /** + * Non-deprecated types + */ + public static final SearchType [] CURRENTLY_SUPPORTED = {QUERY_THEN_FETCH, DFS_QUERY_THEN_FETCH}; + private byte id; SearchType(byte id) { @@ -94,4 +99,5 @@ public enum SearchType { throw new IllegalArgumentException("No search type for [" + searchType + "]"); } } + } diff --git a/server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java b/server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java index 4f1fa4cf061..4bd4406d81c 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java @@ -19,50 +19,43 @@ package org.elasticsearch.action.search; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.search.internal.InternalSearchResponse; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; +import java.util.function.Predicate; +import java.util.function.Supplier; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; -public class MultiSearchResponseTests extends ESTestCase { +public class MultiSearchResponseTests extends AbstractXContentTestCase { - public void testFromXContent() throws IOException { - for (int runs = 0; runs < 20; runs++) { - MultiSearchResponse expected = createTestInstance(); - XContentType xContentType = randomFrom(XContentType.values()); - BytesReference shuffled = toShuffledXContent(expected, xContentType, ToXContent.EMPTY_PARAMS, false); - MultiSearchResponse actual; - try (XContentParser parser = createParser(XContentFactory.xContent(xContentType), shuffled)) { - actual = MultiSearchResponse.fromXContext(parser); - assertThat(parser.nextToken(), nullValue()); - } - - assertThat(actual.getTook(), equalTo(expected.getTook())); - assertThat(actual.getResponses().length, equalTo(expected.getResponses().length)); - for (int i = 0; i < expected.getResponses().length; i++) { - MultiSearchResponse.Item expectedItem = expected.getResponses()[i]; - MultiSearchResponse.Item actualItem = actual.getResponses()[i]; - if (expectedItem.isFailure()) { - assertThat(actualItem.getResponse(), nullValue()); - assertThat(actualItem.getFailureMessage(), containsString(expectedItem.getFailureMessage())); - } else { - assertThat(actualItem.getResponse().toString(), equalTo(expectedItem.getResponse().toString())); - assertThat(actualItem.getFailure(), nullValue()); - } - } + @Override + protected MultiSearchResponse createTestInstance() { + int numItems = randomIntBetween(0, 128); + MultiSearchResponse.Item[] items = new MultiSearchResponse.Item[numItems]; + for (int i = 0; i < numItems; i++) { + // Creating a minimal response is OK, because SearchResponse self + // is tested elsewhere. + long tookInMillis = randomNonNegativeLong(); + int totalShards = randomIntBetween(1, Integer.MAX_VALUE); + int successfulShards = randomIntBetween(0, totalShards); + int skippedShards = totalShards - successfulShards; + InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty(); + SearchResponse.Clusters clusters = new SearchResponse.Clusters(totalShards, successfulShards, skippedShards); + SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, totalShards, + successfulShards, skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, clusters); + items[i] = new MultiSearchResponse.Item(searchResponse, null); } + return new MultiSearchResponse(items, randomNonNegativeLong()); } - private static MultiSearchResponse createTestInstance() { + private static MultiSearchResponse createTestInstanceWithFailures() { int numItems = randomIntBetween(0, 128); MultiSearchResponse.Item[] items = new MultiSearchResponse.Item[numItems]; for (int i = 0; i < numItems; i++) { @@ -85,4 +78,52 @@ public class MultiSearchResponseTests extends ESTestCase { return new MultiSearchResponse(items, randomNonNegativeLong()); } + @Override + protected MultiSearchResponse doParseInstance(XContentParser parser) throws IOException { + return MultiSearchResponse.fromXContext(parser); + } + + @Override + protected void assertEqualInstances(MultiSearchResponse expected, MultiSearchResponse actual) { + assertThat(actual.getTook(), equalTo(expected.getTook())); + assertThat(actual.getResponses().length, equalTo(expected.getResponses().length)); + for (int i = 0; i < expected.getResponses().length; i++) { + MultiSearchResponse.Item expectedItem = expected.getResponses()[i]; + MultiSearchResponse.Item actualItem = actual.getResponses()[i]; + if (expectedItem.isFailure()) { + assertThat(actualItem.getResponse(), nullValue()); + assertThat(actualItem.getFailureMessage(), containsString(expectedItem.getFailureMessage())); + } else { + assertThat(actualItem.getResponse().toString(), equalTo(expectedItem.getResponse().toString())); + assertThat(actualItem.getFailure(), nullValue()); + } + } + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { + return field -> field.startsWith("responses"); + } + + /** + * Test parsing {@link MultiSearchResponse} with inner failures as they don't support asserting on xcontent equivalence, given that + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier instanceSupplier = MultiSearchResponseTests::createTestInstanceWithFailures; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); + } + }