Add MultiSearchTemplate support to High Level Rest client (#30836)

Add MultiSearchTemplate support to High Level Rest client.
Addresses part of #27205
This commit is contained in:
markharwood 2018-06-28 14:05:26 +01:00 committed by GitHub
parent 48cfb9b0db
commit 09dd19a403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 812 additions and 85 deletions

View File

@ -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;
@ -605,6 +606,21 @@ final class RequestConverters {
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) &&
(getAliasesRequest.aliases() == null || getAliasesRequest.aliases().length == 0)) {

View File

@ -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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-search-template.html">Multi Search Template API
* on elastic.co</a>.
*/
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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-search-template.html">Multi Search Template API
* on elastic.co</a>.
*/
public final void multiSearchTemplateAsync(MultiSearchTemplateRequest multiSearchTemplateRequest,
RequestOptions options,
ActionListener<MultiSearchTemplateResponse> listener) {
performRequestAsyncAndParseEntity(multiSearchTemplateRequest, RequestConverters::multiSearchTemplate,
options, MultiSearchTemplateResponse::fromXContext, listener, emptySet());
}
/**
* Asynchronously executes a request using the Ranking Evaluation API.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html">Ranking Evaluation API

View File

@ -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;
@ -1374,6 +1375,52 @@ public class RequestConvertersTests extends ESTestCase {
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<String, String> 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<String, Object> 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<SearchTemplateRequest> 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()) {

View File

@ -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;
@ -876,6 +879,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<String, Object> 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<String, Object> 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 {
{
ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "1");

View File

@ -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();
@ -841,6 +830,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<String, Object> 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<String, Object> 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<MultiSearchTemplateResponse> listener = new ActionListener<MultiSearchTemplateResponse>() {
@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();
RestHighLevelClient client = highLevelClient();

View File

@ -0,0 +1,81 @@
[[java-rest-high-multi-search-template]]
=== Multi-Search-Template API
The `multiSearchTemplate` API executes multiple <<java-rest-high-search-template,`search template`>>
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 <<java-rest-high-search-template,`search template`>>.
<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
<<java-rest-high-search-response,`SearchResponse`>> 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 <<java-rest-high-search-response,`SearchResponse`>> in
`getResponse`.

View File

@ -32,6 +32,7 @@ The Java High Level REST Client supports the following Search APIs:
* <<java-rest-high-search-scroll>>
* <<java-rest-high-clear-scroll>>
* <<java-rest-high-search-template>>
* <<java-rest-high-multi-search-template>>
* <<java-rest-high-multi-search>>
* <<java-rest-high-field-caps>>
* <<java-rest-high-rank-eval>>
@ -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[]

View File

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

View File

@ -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;
@ -185,6 +194,23 @@ public class MultiSearchTemplateResponse extends ActionResponse implements Itera
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() {
return Strings.toString(this);

View File

@ -68,6 +68,11 @@ public class SearchTemplateResponse extends ActionResponse implements StatusToXC
return response != null;
}
@Override
public String toString() {
return "SearchTemplateResponse [source=" + source + ", response=" + response + "]";
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);

View File

@ -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;
@ -98,4 +106,56 @@ public class MultiSearchTemplateRequestTests extends ESTestCase {
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<String, Object> 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);
}
}

View File

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

View File

@ -270,6 +270,25 @@ public class MultiSearchRequest extends ActionRequest implements CompositeIndice
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (SearchRequest request : multiSearchRequest.requests()) {
try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) {
writeSearchRequestParams(request, xContentBuilder);
BytesReference.bytes(xContentBuilder).writeTo(output);
}
output.write(xContent.streamSeparator());
try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) {
if (request.source() != null) {
request.source().toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS);
} else {
xContentBuilder.startObject();
xContentBuilder.endObject();
}
BytesReference.bytes(xContentBuilder).writeTo(output);
}
output.write(xContent.streamSeparator());
}
return output.toByteArray();
}
public static void writeSearchRequestParams(SearchRequest request, XContentBuilder xContentBuilder) throws IOException {
xContentBuilder.startObject();
if (request.indices() != null) {
xContentBuilder.field("index", request.indices());
@ -306,21 +325,6 @@ public class MultiSearchRequest extends ActionRequest implements CompositeIndice
xContentBuilder.field("allow_partial_search_results", request.allowPartialSearchResults());
}
xContentBuilder.endObject();
BytesReference.bytes(xContentBuilder).writeTo(output);
}
output.write(xContent.streamSeparator());
try (XContentBuilder xContentBuilder = XContentBuilder.builder(xContent)) {
if (request.source() != null) {
request.source().toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS);
} else {
xContentBuilder.startObject();
xContentBuilder.endObject();
}
BytesReference.bytes(xContentBuilder).writeTo(output);
}
output.write(xContent.streamSeparator());
}
return output.toByteArray();
}
}

View File

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

View File

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

View File

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

View File

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