浏览代码

Add support for search templates to the high-level REST client. (#30473)

Julie Tibshirani 7 年之前
父节点
当前提交
4f9dd37169
共有 18 个文件被更改,包括 1090 次插入172 次删除
  1. 1 0
      client/rest-high-level/build.gradle
  2. 28 5
      client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
  3. 28 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
  4. 95 30
      client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
  5. 104 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java
  6. 129 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java
  7. 117 0
      docs/java-rest/high-level/search/search-template.asciidoc
  8. 2 0
      docs/java-rest/high-level/supported-apis.asciidoc
  9. 1 1
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java
  10. 1 1
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java
  11. 1 32
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java
  12. 84 1
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java
  13. 31 2
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java
  14. 3 3
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java
  15. 56 96
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java
  16. 197 0
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java
  17. 211 0
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java
  18. 1 1
      test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java

+ 1 - 0
client/rest-high-level/build.gradle

@@ -40,6 +40,7 @@ dependencies {
   compile "org.elasticsearch.plugin:parent-join-client:${version}"
   compile "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}"
   compile "org.elasticsearch.plugin:rank-eval-client:${version}"
+  compile "org.elasticsearch.plugin:lang-mustache-client:${version}"
 
   testCompile "org.elasticsearch.client:test:${version}"
   testCompile "org.elasticsearch.test:framework:${version}"

+ 28 - 5
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -80,6 +80,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.SearchTemplateRequest;
 import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
 
 import java.io.ByteArrayOutputStream;
@@ -458,6 +459,15 @@ final class RequestConverters {
         Request request = new Request(HttpPost.METHOD_NAME, endpoint(searchRequest.indices(), searchRequest.types(), "_search"));
 
         Params params = new Params(request);
+        addSearchRequestParams(params, searchRequest);
+
+        if (searchRequest.source() != null) {
+            request.setEntity(createEntity(searchRequest.source(), REQUEST_BODY_CONTENT_TYPE));
+        }
+        return request;
+    }
+
+    private static void addSearchRequestParams(Params params, SearchRequest searchRequest) {
         params.putParam(RestSearchAction.TYPED_KEYS_PARAM, "true");
         params.withRouting(searchRequest.routing());
         params.withPreference(searchRequest.preference());
@@ -473,11 +483,6 @@ final class RequestConverters {
         if (searchRequest.scroll() != null) {
             params.putParam("scroll", searchRequest.scroll().keepAlive());
         }
-
-        if (searchRequest.source() != null) {
-            request.setEntity(createEntity(searchRequest.source(), REQUEST_BODY_CONTENT_TYPE));
-        }
-        return request;
     }
 
     static Request searchScroll(SearchScrollRequest searchScrollRequest) throws IOException {
@@ -507,6 +512,24 @@ final class RequestConverters {
         return request;
     }
 
+    static Request searchTemplate(SearchTemplateRequest searchTemplateRequest) throws IOException {
+        Request request;
+
+        if (searchTemplateRequest.isSimulate()) {
+            request = new Request(HttpGet.METHOD_NAME, "_render/template");
+        } else {
+            SearchRequest searchRequest = searchTemplateRequest.getRequest();
+            String endpoint = endpoint(searchRequest.indices(), searchRequest.types(), "_search/template");
+            request = new Request(HttpGet.METHOD_NAME, endpoint);
+
+            Params params = new Params(request);
+            addSearchRequestParams(params, searchRequest);
+        }
+
+        request.setEntity(createEntity(searchTemplateRequest, REQUEST_BODY_CONTENT_TYPE));
+        return request;
+    }
+
     static Request existsAlias(GetAliasesRequest getAliasesRequest) {
         if ((getAliasesRequest.indices() == null || getAliasesRequest.indices().length == 0) &&
                 (getAliasesRequest.aliases() == null || getAliasesRequest.aliases().length == 0)) {

+ 28 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java

@@ -64,6 +64,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.SearchTemplateRequest;
+import org.elasticsearch.script.mustache.SearchTemplateResponse;
 import org.elasticsearch.search.aggregations.Aggregation;
 import org.elasticsearch.search.aggregations.bucket.adjacency.AdjacencyMatrixAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.adjacency.ParsedAdjacencyMatrix;
@@ -501,6 +503,32 @@ public class RestHighLevelClient implements Closeable {
                 listener, emptySet(), headers);
     }
 
+    /**
+     * Executes a request using the Search Template API.
+     *
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html">Search Template API
+     * on elastic.co</a>.
+     */
+    public final SearchTemplateResponse searchTemplate(SearchTemplateRequest searchTemplateRequest,
+                                                       Header... headers) throws IOException {
+        return performRequestAndParseEntity(searchTemplateRequest, RequestConverters::searchTemplate,
+            SearchTemplateResponse::fromXContent, emptySet(), headers);
+    }
+
+    /**
+     * Asynchronously executes a request using the Search Template API
+     *
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html">Search Template API
+     * on elastic.co</a>.
+     */
+    public final void searchTemplateAsync(SearchTemplateRequest searchTemplateRequest,
+                                          ActionListener<SearchTemplateResponse> listener,
+                                          Header... headers) {
+        performRequestAsyncAndParseEntity(searchTemplateRequest, RequestConverters::searchTemplate,
+            SearchTemplateResponse::fromXContent, listener, emptySet(), headers);
+    }
+
+
     /**
      * Executes a request using the Ranking Evaluation API.
      *

+ 95 - 30
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -95,6 +95,8 @@ import org.elasticsearch.index.rankeval.RankEvalSpec;
 import org.elasticsearch.index.rankeval.RatedRequest;
 import org.elasticsearch.index.rankeval.RestRankEvalAction;
 import org.elasticsearch.rest.action.search.RestSearchAction;
+import org.elasticsearch.script.ScriptType;
+import org.elasticsearch.script.mustache.SearchTemplateRequest;
 import org.elasticsearch.search.Scroll;
 import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
 import org.elasticsearch.search.aggregations.support.ValueType;
@@ -1011,36 +1013,7 @@ public class RequestConvertersTests extends ESTestCase {
         searchRequest.types(types);
 
         Map<String, String> expectedParams = new HashMap<>();
-        expectedParams.put(RestSearchAction.TYPED_KEYS_PARAM, "true");
-        if (randomBoolean()) {
-            searchRequest.routing(randomAlphaOfLengthBetween(3, 10));
-            expectedParams.put("routing", searchRequest.routing());
-        }
-        if (randomBoolean()) {
-            searchRequest.preference(randomAlphaOfLengthBetween(3, 10));
-            expectedParams.put("preference", searchRequest.preference());
-        }
-        if (randomBoolean()) {
-            searchRequest.searchType(randomFrom(SearchType.values()));
-        }
-        expectedParams.put("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT));
-        if (randomBoolean()) {
-            searchRequest.requestCache(randomBoolean());
-            expectedParams.put("request_cache", Boolean.toString(searchRequest.requestCache()));
-        }
-        if (randomBoolean()) {
-            searchRequest.allowPartialSearchResults(randomBoolean());
-            expectedParams.put("allow_partial_search_results", Boolean.toString(searchRequest.allowPartialSearchResults()));
-        }
-        if (randomBoolean()) {
-            searchRequest.setBatchedReduceSize(randomIntBetween(2, Integer.MAX_VALUE));
-        }
-        expectedParams.put("batched_reduce_size", Integer.toString(searchRequest.getBatchedReduceSize()));
-        if (randomBoolean()) {
-            searchRequest.scroll(randomTimeValue());
-            expectedParams.put("scroll", searchRequest.scroll().keepAlive().getStringRep());
-        }
-
+        setRandomSearchParams(searchRequest, expectedParams);
         setRandomIndicesOptions(searchRequest::indicesOptions, searchRequest::indicesOptions, expectedParams);
 
         SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
@@ -1189,6 +1162,65 @@ public class RequestConvertersTests extends ESTestCase {
         assertEquals(REQUEST_BODY_CONTENT_TYPE.mediaTypeWithoutParameters(), request.getEntity().getContentType().getValue());
     }
 
+    public void testSearchTemplate() throws Exception {
+        // Create a random request.
+        String[] indices = randomIndicesNames(0, 5);
+        SearchRequest searchRequest = new SearchRequest(indices);
+
+        Map<String, String> expectedParams = new HashMap<>();
+        setRandomSearchParams(searchRequest, expectedParams);
+        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", "soren");
+        searchTemplateRequest.setScriptParams(scriptParams);
+
+        // Verify that the resulting REST request looks as expected.
+        Request request = RequestConverters.searchTemplate(searchTemplateRequest);
+        StringJoiner endpoint = new StringJoiner("/", "/", "");
+        String index = String.join(",", indices);
+        if (Strings.hasLength(index)) {
+            endpoint.add(index);
+        }
+        endpoint.add("_search/template");
+
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals(endpoint.toString(), request.getEndpoint());
+        assertEquals(expectedParams, request.getParameters());
+        assertToXContentBody(searchTemplateRequest, request.getEntity());
+    }
+
+    public void testRenderSearchTemplate() throws Exception {
+        // Create a simple request.
+        SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest();
+        searchTemplateRequest.setSimulate(true); // Setting simulate true means the template should only be rendered.
+
+        searchTemplateRequest.setScript("template1");
+        searchTemplateRequest.setScriptType(ScriptType.STORED);
+        searchTemplateRequest.setProfile(randomBoolean());
+
+        Map<String, Object> scriptParams = new HashMap<>();
+        scriptParams.put("field", "name");
+        scriptParams.put("value", "soren");
+        searchTemplateRequest.setScriptParams(scriptParams);
+
+        // Verify that the resulting REST request looks as expected.
+        Request request = RequestConverters.searchTemplate(searchTemplateRequest);
+        String endpoint = "_render/template";
+
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals(endpoint, request.getEndpoint());
+        assertEquals(Collections.emptyMap(), request.getParameters());
+        assertToXContentBody(searchTemplateRequest, request.getEntity());
+    }
+
     public void testExistsAlias() {
         GetAliasesRequest getAliasesRequest = new GetAliasesRequest();
         String[] indices = randomBoolean() ? null : randomIndicesNames(0, 5);
@@ -1662,6 +1694,39 @@ public class RequestConvertersTests extends ESTestCase {
         }
     }
 
+    private static void setRandomSearchParams(SearchRequest searchRequest,
+                                              Map<String, String> expectedParams) {
+        expectedParams.put(RestSearchAction.TYPED_KEYS_PARAM, "true");
+        if (randomBoolean()) {
+            searchRequest.routing(randomAlphaOfLengthBetween(3, 10));
+            expectedParams.put("routing", searchRequest.routing());
+        }
+        if (randomBoolean()) {
+            searchRequest.preference(randomAlphaOfLengthBetween(3, 10));
+            expectedParams.put("preference", searchRequest.preference());
+        }
+        if (randomBoolean()) {
+            searchRequest.searchType(randomFrom(SearchType.values()));
+        }
+        expectedParams.put("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT));
+        if (randomBoolean()) {
+            searchRequest.requestCache(randomBoolean());
+            expectedParams.put("request_cache", Boolean.toString(searchRequest.requestCache()));
+        }
+        if (randomBoolean()) {
+            searchRequest.allowPartialSearchResults(randomBoolean());
+            expectedParams.put("allow_partial_search_results", Boolean.toString(searchRequest.allowPartialSearchResults()));
+        }
+        if (randomBoolean()) {
+            searchRequest.setBatchedReduceSize(randomIntBetween(2, Integer.MAX_VALUE));
+        }
+        expectedParams.put("batched_reduce_size", Integer.toString(searchRequest.getBatchedReduceSize()));
+        if (randomBoolean()) {
+            searchRequest.scroll(randomTimeValue());
+            expectedParams.put("scroll", searchRequest.scroll().keepAlive().getStringRep());
+        }
+    }
+
     private static void setRandomIndicesOptions(Consumer<IndicesOptions> setter, Supplier<IndicesOptions> getter,
                                                 Map<String, String> expectedParams) {
 

+ 104 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java

@@ -38,8 +38,11 @@ import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchScrollRequest;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.query.MatchQueryBuilder;
 import org.elasticsearch.index.query.ScriptQueryBuilder;
 import org.elasticsearch.index.query.TermsQueryBuilder;
@@ -48,6 +51,8 @@ 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.SearchTemplateRequest;
+import org.elasticsearch.script.mustache.SearchTemplateResponse;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.aggregations.BucketOrder;
 import org.elasticsearch.search.aggregations.bucket.range.Range;
@@ -69,10 +74,12 @@ import org.junit.Before;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
 import static org.hamcrest.Matchers.both;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.either;
@@ -733,6 +740,103 @@ public class SearchIT extends ESRestHighLevelClientTestCase {
         assertThat(multiSearchResponse.getResponses()[1].getResponse(), nullValue());
     }
 
+    public void testSearchTemplate() throws IOException {
+        SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest();
+        searchTemplateRequest.setRequest(new SearchRequest("index"));
+
+        searchTemplateRequest.setScriptType(ScriptType.INLINE);
+        searchTemplateRequest.setScript(
+            "{" +
+            "  \"query\": {" +
+            "    \"match\": {" +
+            "      \"num\": {{number}}" +
+            "    }" +
+            "  }" +
+            "}");
+
+        Map<String, Object> scriptParams = new HashMap<>();
+        scriptParams.put("number", 10);
+        searchTemplateRequest.setScriptParams(scriptParams);
+
+        searchTemplateRequest.setExplain(true);
+        searchTemplateRequest.setProfile(true);
+
+        SearchTemplateResponse searchTemplateResponse = execute(searchTemplateRequest,
+            highLevelClient()::searchTemplate,
+            highLevelClient()::searchTemplateAsync);
+
+        assertNull(searchTemplateResponse.getSource());
+
+        SearchResponse searchResponse = searchTemplateResponse.getResponse();
+        assertNotNull(searchResponse);
+
+        assertEquals(1, searchResponse.getHits().totalHits);
+        assertEquals(1, searchResponse.getHits().getHits().length);
+        assertThat(searchResponse.getHits().getMaxScore(), greaterThan(0f));
+
+        SearchHit hit = searchResponse.getHits().getHits()[0];
+        assertNotNull(hit.getExplanation());
+
+        assertFalse(searchResponse.getProfileResults().isEmpty());
+    }
+
+    public void testNonExistentSearchTemplate() {
+        SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest();
+        searchTemplateRequest.setRequest(new SearchRequest("index"));
+
+        searchTemplateRequest.setScriptType(ScriptType.STORED);
+        searchTemplateRequest.setScript("non-existent");
+        searchTemplateRequest.setScriptParams(Collections.emptyMap());
+
+        ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class,
+            () -> execute(searchTemplateRequest,
+                highLevelClient()::searchTemplate,
+                highLevelClient()::searchTemplateAsync));
+
+        assertEquals(RestStatus.NOT_FOUND, exception.status());
+    }
+
+    public void testRenderSearchTemplate() throws IOException {
+        SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest();
+
+        searchTemplateRequest.setScriptType(ScriptType.INLINE);
+        searchTemplateRequest.setScript(
+            "{" +
+            "  \"query\": {" +
+            "    \"match\": {" +
+            "      \"num\": {{number}}" +
+            "    }" +
+            "  }" +
+            "}");
+
+        Map<String, Object> scriptParams = new HashMap<>();
+        scriptParams.put("number", 10);
+        searchTemplateRequest.setScriptParams(scriptParams);
+
+        // Setting simulate true causes the template to only be rendered.
+        searchTemplateRequest.setSimulate(true);
+
+        SearchTemplateResponse searchTemplateResponse = execute(searchTemplateRequest,
+            highLevelClient()::searchTemplate,
+            highLevelClient()::searchTemplateAsync);
+        assertNull(searchTemplateResponse.getResponse());
+
+        BytesReference expectedSource = BytesReference.bytes(
+            XContentFactory.jsonBuilder()
+                .startObject()
+                    .startObject("query")
+                        .startObject("match")
+                            .field("num", 10)
+                        .endObject()
+                    .endObject()
+                .endObject());
+
+        BytesReference actualSource = searchTemplateResponse.getSource();
+        assertNotNull(actualSource);
+
+        assertToXContentEquivalent(expectedSource, actualSource, XContentType.JSON);
+    }
+
     public void testFieldCaps() throws IOException {
         FieldCapabilitiesRequest request = new FieldCapabilitiesRequest()
             .indices("index1", "index2")

+ 129 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java

@@ -41,7 +41,11 @@ import org.elasticsearch.action.search.ShardSearchFailure;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.text.Text;
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.unit.TimeValue;
@@ -60,6 +64,9 @@ import org.elasticsearch.index.rankeval.RatedDocument;
 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.SearchTemplateRequest;
+import org.elasticsearch.script.mustache.SearchTemplateResponse;
 import org.elasticsearch.search.Scroll;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.SearchHits;
@@ -92,6 +99,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
@@ -706,9 +714,130 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testSearchTemplateWithInlineScript() throws Exception {
+        indexSearchTestData();
+        RestHighLevelClient client = highLevelClient();
+
+        // tag::search-template-request-inline
+        SearchTemplateRequest request = new SearchTemplateRequest();
+        request.setRequest(new SearchRequest("posts")); // <1>
+
+        request.setScriptType(ScriptType.INLINE);
+        request.setScript( // <2>
+            "{" +
+            "  \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," +
+            "  \"size\" : \"{{size}}\"" +
+            "}");
+
+        Map<String, Object> scriptParams = new HashMap<>();
+        scriptParams.put("field", "title");
+        scriptParams.put("value", "elasticsearch");
+        scriptParams.put("size", 5);
+        request.setScriptParams(scriptParams); // <3>
+        // end::search-template-request-inline
+
+        // tag::search-template-response
+        SearchTemplateResponse response = client.searchTemplate(request);
+        SearchResponse searchResponse = response.getResponse();
+        // end::search-template-response
+
+        assertNotNull(searchResponse);
+        assertTrue(searchResponse.getHits().totalHits > 0);
+
+        // tag::render-search-template-request
+        request.setSimulate(true); // <1>
+        // end::render-search-template-request
+
+        // tag::render-search-template-response
+        SearchTemplateResponse renderResponse = client.searchTemplate(request);
+        BytesReference source = renderResponse.getSource(); // <1>
+        // end::render-search-template-response
+
+        assertNotNull(source);
+        assertEquals((
+            "{" +
+            "  \"size\" : \"5\"," +
+            "  \"query\": { \"match\" : { \"title\" : \"elasticsearch\" } }" +
+            "}").replaceAll("\\s+", ""), source.utf8ToString());
+    }
+
+    public void testSearchTemplateWithStoredScript() throws Exception {
+        indexSearchTestData();
+        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());
+
+        // tag::search-template-request-stored
+        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", "elasticsearch");
+        params.put("size", 5);
+        request.setScriptParams(params);
+        // end::search-template-request-stored
+
+        // tag::search-template-request-options
+        request.setExplain(true);
+        request.setProfile(true);
+        // end::search-template-request-options
+
+        // tag::search-template-execute
+        SearchTemplateResponse response = client.searchTemplate(request);
+        // end::search-template-execute
+
+        SearchResponse searchResponse = response.getResponse();
+        assertNotNull(searchResponse);
+        assertTrue(searchResponse.getHits().totalHits > 0);
+
+        // tag::search-template-execute-listener
+        ActionListener<SearchTemplateResponse> listener = new ActionListener<SearchTemplateResponse>() {
+            @Override
+            public void onResponse(SearchTemplateResponse response) {
+                // <1>
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                // <2>
+            }
+        };
+        // end::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::search-template-execute-async
+        client.searchTemplateAsync(request, listener); // <1>
+        // end::search-template-execute-async
+
+        assertTrue(latch.await(30L, TimeUnit.SECONDS));
+    }
+
     public void testFieldCaps() throws Exception {
         indexSearchTestData();
         RestHighLevelClient client = highLevelClient();
+
         // tag::field-caps-request
         FieldCapabilitiesRequest request = new FieldCapabilitiesRequest()
             .fields("user")

+ 117 - 0
docs/java-rest/high-level/search/search-template.asciidoc

@@ -0,0 +1,117 @@
+[[java-rest-high-search-template]]
+=== Search Template API
+
+The search template API allows for searches to be executed from a template based
+on the mustache language, and also for previewing rendered templates.
+
+[[java-rest-high-search-template-request]]
+==== Search Template Request
+
+===== Inline Templates
+
+In the most basic form of request, the search template is specified inline:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-inline]
+--------------------------------------------------
+<1> The search is executed against the `posts` index.
+<2> The template defines the structure of the search source. It is passed
+as a string because mustache templates are not always valid JSON.
+<3> Before running the search, the template is rendered with the provided parameters.
+
+===== Registered Templates
+
+Search templates can be registered in advance through stored scripts API. Note that
+the stored scripts API is not yet available in the high-level REST client, so in this
+example we use the low-level REST client.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[register-script]
+--------------------------------------------------
+
+Instead of providing an inline script, we can refer to this registered template in the request:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-stored]
+--------------------------------------------------
+
+===== Rendering Templates
+
+Given parameter values, a template can be rendered without executing a search:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[render-search-template-request]
+--------------------------------------------------
+<1> Setting `simulate` to `true` causes the search template to only be rendered.
+
+Both inline and pre-registered templates can be rendered.
+
+===== Optional Arguments
+
+As in standard search requests, the `explain` and `profile` options are supported:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-options]
+--------------------------------------------------
+
+===== Additional References
+
+The {ref}/search-template.html[Search Template documentation] contains further examples of how search requests can be templated.
+
+[[java-rest-high-search-template-sync]]
+==== Synchronous Execution
+
+The `searchTemplate` method executes the request synchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute]
+--------------------------------------------------
+
+==== Asynchronous Execution
+
+A search template request can be executed asynchronously through the `searchTemplateAsync`
+method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute-async]
+--------------------------------------------------
+<1> The `SearchTemplateRequest` to execute and the `ActionListener` to call when the execution completes.
+
+The asynchronous method does not block and returns immediately. Once the request completes, the
+`ActionListener` is called back using the `onResponse` method if the execution completed successfully,
+or using the `onFailure` method if it failed.
+
+A typical listener for `SearchTemplateResponse` is constructed as follows:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed.
+<2> Called when the whole `SearchTemplateRequest` fails.
+
+==== Search Template Response
+
+For a standard search template request, the response contains a `SearchResponse` object
+with the result of executing the search:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-response]
+--------------------------------------------------
+
+If `simulate` was set to `true` in the request, then the response
+will contain the rendered search source instead of a `SearchResponse`:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[render-search-template-response]
+--------------------------------------------------
+<1> The rendered source in bytes, in our example `{"query": { "match" : { "title" : "elasticsearch" }}, "size" : 5}`.

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -31,6 +31,7 @@ The Java High Level REST Client supports the following Search APIs:
 * <<java-rest-high-search>>
 * <<java-rest-high-search-scroll>>
 * <<java-rest-high-clear-scroll>>
+* <<java-rest-high-search-template>>
 * <<java-rest-high-multi-search>>
 * <<java-rest-high-field-caps>>
 * <<java-rest-high-rank-eval>>
@@ -38,6 +39,7 @@ The Java High Level REST Client supports the following Search APIs:
 include::search/search.asciidoc[]
 include::search/scroll.asciidoc[]
 include::search/multi-search.asciidoc[]
+include::search/search-template.asciidoc[]
 include::search/field-caps.asciidoc[]
 include::search/rank-eval.asciidoc[]
 

+ 1 - 1
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java

@@ -77,7 +77,7 @@ public class RestMultiSearchTemplateAction extends BaseRestHandler {
 
         RestMultiSearchAction.parseMultiLineRequest(restRequest, multiRequest.indicesOptions(), allowExplicitIndex,
                 (searchRequest, bytes) -> {
-                    SearchTemplateRequest searchTemplateRequest = RestSearchTemplateAction.parse(bytes);
+                    SearchTemplateRequest searchTemplateRequest = SearchTemplateRequest.fromXContent(bytes);
                     if (searchTemplateRequest.getScript() != null) {
                         searchTemplateRequest.setRequest(searchRequest);
                         multiRequest.add(searchTemplateRequest);

+ 1 - 1
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java

@@ -52,7 +52,7 @@ public class RestRenderSearchTemplateAction extends BaseRestHandler {
         // Creates the render template request
         SearchTemplateRequest renderRequest;
         try (XContentParser parser = request.contentOrSourceParamParser()) {
-            renderRequest = RestSearchTemplateAction.parse(parser);
+            renderRequest = SearchTemplateRequest.fromXContent(parser);
         }
         renderRequest.setSimulate(true);
 

+ 1 - 32
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java

@@ -47,33 +47,6 @@ public class RestSearchTemplateAction extends BaseRestHandler {
 
     private static final Set<String> RESPONSE_PARAMS = Collections.singleton(RestSearchAction.TYPED_KEYS_PARAM);
 
-    private static final ObjectParser<SearchTemplateRequest, Void> PARSER;
-    static {
-        PARSER = new ObjectParser<>("search_template");
-        PARSER.declareField((parser, request, s) ->
-                        request.setScriptParams(parser.map())
-                , new ParseField("params"), ObjectParser.ValueType.OBJECT);
-        PARSER.declareString((request, s) -> {
-            request.setScriptType(ScriptType.STORED);
-            request.setScript(s);
-        }, new ParseField("id"));
-        PARSER.declareBoolean(SearchTemplateRequest::setExplain, new ParseField("explain"));
-        PARSER.declareBoolean(SearchTemplateRequest::setProfile, new ParseField("profile"));
-        PARSER.declareField((parser, request, value) -> {
-            request.setScriptType(ScriptType.INLINE);
-            if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
-                //convert the template to json which is the only supported XContentType (see CustomMustacheFactory#createEncoder)
-                try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
-                    request.setScript(Strings.toString(builder.copyCurrentStructure(parser)));
-                } catch (IOException e) {
-                    throw new ParsingException(parser.getTokenLocation(), "Could not parse inline template", e);
-                }
-            } else {
-                request.setScript(parser.text());
-            }
-        }, new ParseField("source", "inline", "template"), ObjectParser.ValueType.OBJECT_OR_STRING);
-    }
-
     public RestSearchTemplateAction(Settings settings, RestController controller) {
         super(settings);
 
@@ -99,17 +72,13 @@ public class RestSearchTemplateAction extends BaseRestHandler {
         // Creates the search template request
         SearchTemplateRequest searchTemplateRequest;
         try (XContentParser parser = request.contentOrSourceParamParser()) {
-            searchTemplateRequest = PARSER.parse(parser, new SearchTemplateRequest(), null);
+            searchTemplateRequest = SearchTemplateRequest.fromXContent(parser);
         }
         searchTemplateRequest.setRequest(searchRequest);
 
         return channel -> client.execute(SearchTemplateAction.INSTANCE, searchTemplateRequest, new RestStatusToXContentListener<>(channel));
     }
 
-    public static SearchTemplateRequest parse(XContentParser parser) throws IOException {
-        return PARSER.parse(parser, new SearchTemplateRequest(), null);
-    }
-
     @Override
     protected Set<String> responseParams() {
         return RESPONSE_PARAMS;

+ 84 - 1
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java

@@ -23,19 +23,28 @@ import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.CompositeIndicesRequest;
 import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.script.ScriptType;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Objects;
 
 import static org.elasticsearch.action.ValidateActions.addValidationError;
 
 /**
  * A request to execute a search based on a search template.
  */
-public class SearchTemplateRequest extends ActionRequest implements CompositeIndicesRequest {
+public class SearchTemplateRequest extends ActionRequest implements CompositeIndicesRequest, ToXContentObject {
 
     private SearchRequest request;
     private boolean simulate = false;
@@ -60,6 +69,24 @@ public class SearchTemplateRequest extends ActionRequest implements CompositeInd
         return request;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SearchTemplateRequest request1 = (SearchTemplateRequest) o;
+        return simulate == request1.simulate &&
+            explain == request1.explain &&
+            profile == request1.profile &&
+            Objects.equals(request, request1.request) &&
+            scriptType == request1.scriptType &&
+            Objects.equals(script, request1.script) &&
+            Objects.equals(scriptParams, request1.scriptParams);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(request, simulate, explain, profile, scriptType, script, scriptParams);
+    }
 
     public boolean isSimulate() {
         return simulate;
@@ -134,6 +161,62 @@ public class SearchTemplateRequest extends ActionRequest implements CompositeInd
         return validationException;
     }
 
+    private static ParseField ID_FIELD = new ParseField("id");
+    private static ParseField SOURCE_FIELD = new ParseField("source", "inline", "template");
+
+    private static ParseField PARAMS_FIELD = new ParseField("params");
+    private static ParseField EXPLAIN_FIELD = new ParseField("explain");
+    private static ParseField PROFILE_FIELD = new ParseField("profile");
+
+    private static final ObjectParser<SearchTemplateRequest, Void> PARSER;
+    static {
+        PARSER = new ObjectParser<>("search_template");
+        PARSER.declareField((parser, request, s) ->
+                request.setScriptParams(parser.map())
+            , PARAMS_FIELD, ObjectParser.ValueType.OBJECT);
+        PARSER.declareString((request, s) -> {
+            request.setScriptType(ScriptType.STORED);
+            request.setScript(s);
+        }, ID_FIELD);
+        PARSER.declareBoolean(SearchTemplateRequest::setExplain, EXPLAIN_FIELD);
+        PARSER.declareBoolean(SearchTemplateRequest::setProfile, PROFILE_FIELD);
+        PARSER.declareField((parser, request, value) -> {
+            request.setScriptType(ScriptType.INLINE);
+            if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
+                //convert the template to json which is the only supported XContentType (see CustomMustacheFactory#createEncoder)
+                try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
+                    request.setScript(Strings.toString(builder.copyCurrentStructure(parser)));
+                } catch (IOException e) {
+                    throw new ParsingException(parser.getTokenLocation(), "Could not parse inline template", e);
+                }
+            } else {
+                request.setScript(parser.text());
+            }
+        }, SOURCE_FIELD, ObjectParser.ValueType.OBJECT_OR_STRING);
+    }
+
+    public static SearchTemplateRequest fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, new SearchTemplateRequest(), null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+
+        if (scriptType == ScriptType.STORED) {
+            builder.field(ID_FIELD.getPreferredName(), script);
+        } else if (scriptType == ScriptType.INLINE) {
+            builder.field(SOURCE_FIELD.getPreferredName(), script);
+        } else {
+            throw new UnsupportedOperationException("Unrecognized script type [" + scriptType + "].");
+        }
+
+        return builder.field(PARAMS_FIELD.getPreferredName(), scriptParams)
+            .field(EXPLAIN_FIELD.getPreferredName(), explain)
+            .field(PROFILE_FIELD.getPreferredName(), profile)
+            .endObject();
+    }
+
     @Override
     public void readFrom(StreamInput in) throws IOException {
         super.readFrom(in);

+ 31 - 2
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java

@@ -21,18 +21,23 @@ package org.elasticsearch.script.mustache;
 
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.common.ParseField;
 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.StatusToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Map;
 
-public class SearchTemplateResponse  extends ActionResponse implements StatusToXContentObject {
+public class SearchTemplateResponse extends ActionResponse implements StatusToXContentObject {
+    public static ParseField TEMPLATE_OUTPUT_FIELD = new ParseField("template_output");
 
     /** Contains the source of the rendered template **/
     private BytesReference source;
@@ -77,6 +82,30 @@ public class SearchTemplateResponse  extends ActionResponse implements StatusToX
         response = in.readOptionalStreamable(SearchResponse::new);
     }
 
+    public static SearchTemplateResponse fromXContent(XContentParser parser) throws IOException {
+        SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse();
+        Map<String, Object> contentAsMap = parser.map();
+
+        if (contentAsMap.containsKey(TEMPLATE_OUTPUT_FIELD.getPreferredName())) {
+            Object source = contentAsMap.get(TEMPLATE_OUTPUT_FIELD.getPreferredName());
+            XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)
+                .value(source);
+            searchTemplateResponse.setSource(BytesReference.bytes(builder));
+        } else {
+            XContentType contentType = parser.contentType();
+            XContentBuilder builder = XContentFactory.contentBuilder(contentType)
+                .map(contentAsMap);
+            XContentParser searchResponseParser = contentType.xContent().createParser(
+                parser.getXContentRegistry(),
+                parser.getDeprecationHandler(),
+                BytesReference.bytes(builder).streamInput());
+
+            SearchResponse searchResponse = SearchResponse.fromXContent(searchResponseParser);
+            searchTemplateResponse.setResponse(searchResponse);
+        }
+        return searchTemplateResponse;
+    }
+
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         if (hasResponse()) {
@@ -85,7 +114,7 @@ public class SearchTemplateResponse  extends ActionResponse implements StatusToX
             builder.startObject();
             //we can assume the template is always json as we convert it before compiling it
             try (InputStream stream = source.streamInput()) {
-                builder.rawField("template_output", stream, XContentType.JSON);
+                builder.rawField(TEMPLATE_OUTPUT_FIELD.getPreferredName(), stream, XContentType.JSON);
             }
             builder.endObject();
         }

+ 3 - 3
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java

@@ -101,7 +101,7 @@ public class SearchTemplateIT extends ESSingleNodeTestCase {
                 + "    \"size\": 1"
                 + "  }"
                 + "}";
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, query));
+        SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, query));
         request.setRequest(searchRequest);
         SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get();
         assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1));
@@ -122,7 +122,7 @@ public class SearchTemplateIT extends ESSingleNodeTestCase {
                 + "    \"use_size\": true"
                 + "  }"
                 + "}";
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, templateString));
+        SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, templateString));
         request.setRequest(searchRequest);
         SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get();
         assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1));
@@ -143,7 +143,7 @@ public class SearchTemplateIT extends ESSingleNodeTestCase {
                 + "    \"use_size\": true"
                 + "  }"
                 + "}";
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, templateString));
+        SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, templateString));
         request.setRequest(searchRequest);
         SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get();
         assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1));

+ 56 - 96
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java

@@ -19,117 +19,77 @@
 
 package org.elasticsearch.script.mustache;
 
-import org.elasticsearch.common.xcontent.XContentParseException;
-import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.script.ScriptType;
-import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.search.RandomSearchRequestGenerator;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.test.AbstractStreamableTestCase;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
 
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasEntry;
-import static org.hamcrest.Matchers.hasItems;
-import static org.hamcrest.Matchers.hasKey;
-import static org.hamcrest.Matchers.nullValue;
-
-public class SearchTemplateRequestTests extends ESTestCase {
-
-    public void testParseInlineTemplate() throws Exception {
-        String source = "{" +
-                "    'source' : {\n" +
-                "    'query': {\n" +
-                "      'terms': {\n" +
-                "        'status': [\n" +
-                "          '{{#status}}',\n" +
-                "          '{{.}}',\n" +
-                "          '{{/status}}'\n" +
-                "        ]\n" +
-                "      }\n" +
-                "    }\n" +
-                "  }" +
-                "}";
-
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("{\"query\":{\"terms\":{\"status\":[\"{{#status}}\",\"{{.}}\",\"{{/status}}\"]}}}"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
-        assertThat(request.getScriptParams(), nullValue());
-    }
+public class SearchTemplateRequestTests extends AbstractStreamableTestCase<SearchTemplateRequest> {
 
-    public void testParseInlineTemplateWithParams() throws Exception {
-        String source = "{" +
-                "    'source' : {" +
-                "      'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," +
-                "      'size' : '{{my_size}}'" +
-                "    }," +
-                "    'params' : {" +
-                "        'my_field' : 'foo'," +
-                "        'my_value' : 'bar'," +
-                "        'my_size' : 5" +
-                "    }" +
-                "}";
-
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
-        assertThat(request.getScriptParams().size(), equalTo(3));
-        assertThat(request.getScriptParams(), hasEntry("my_field", "foo"));
-        assertThat(request.getScriptParams(), hasEntry("my_value", "bar"));
-        assertThat(request.getScriptParams(), hasEntry("my_size", 5));
+    @Override
+    protected SearchTemplateRequest createBlankInstance() {
+        return new SearchTemplateRequest();
     }
 
-    public void testParseInlineTemplateAsString() throws Exception {
-        String source = "{'source' : '{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\":{\\\"match\\\":{\\\"foo\\\":\\\"{{text}}\\\"}}}}}'}";
-
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("{\"query\":{\"bool\":{\"must\":{\"match\":{\"foo\":\"{{text}}\"}}}}}"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
-        assertThat(request.getScriptParams(), nullValue());
+    @Override
+    protected SearchTemplateRequest createTestInstance() {
+        return createRandomRequest();
     }
 
-    @SuppressWarnings("unchecked")
-    public void testParseInlineTemplateAsStringWithParams() throws Exception {
-        String source = "{'source' : '{\\\"query\\\":{\\\"match\\\":{\\\"{{field}}\\\":\\\"{{value}}\\\"}}}', " +
-                "'params': {'status': ['pending', 'published']}}";
-
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{field}}\":\"{{value}}\"}}}"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
-        assertThat(request.getScriptParams().size(), equalTo(1));
-        assertThat(request.getScriptParams(), hasKey("status"));
-        assertThat((List<String>) request.getScriptParams().get("status"), hasItems("pending", "published"));
+    @Override
+    protected SearchTemplateRequest mutateInstance(SearchTemplateRequest instance) throws IOException {
+        List<Consumer<SearchTemplateRequest>> mutators = new ArrayList<>();
+
+        mutators.add(request -> request.setScriptType(
+            randomValueOtherThan(request.getScriptType(), () -> randomFrom(ScriptType.values()))));
+        mutators.add(request -> request.setScript(
+            randomValueOtherThan(request.getScript(), () -> randomAlphaOfLength(50))));
+
+        mutators.add(request -> {
+            Map<String, Object> mutatedScriptParams = new HashMap<>(request.getScriptParams());
+            String newField = randomValueOtherThanMany(mutatedScriptParams::containsKey, () -> randomAlphaOfLength(5));
+            mutatedScriptParams.put(newField, randomAlphaOfLength(10));
+            request.setScriptParams(mutatedScriptParams);
+        });
+
+        mutators.add(request -> request.setProfile(!request.isProfile()));
+        mutators.add(request -> request.setExplain(!request.isExplain()));
+        mutators.add(request -> request.setSimulate(!request.isSimulate()));
+
+        mutators.add(request -> request.setRequest(
+            RandomSearchRequestGenerator.randomSearchRequest(SearchSourceBuilder::searchSource)));
+
+        SearchTemplateRequest mutatedInstance = copyInstance(instance);
+        Consumer<SearchTemplateRequest> mutator = randomFrom(mutators);
+        mutator.accept(mutatedInstance);
+        return mutatedInstance;
     }
 
-    public void testParseStoredTemplate() throws Exception {
-        String source = "{'id' : 'storedTemplate'}";
-
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("storedTemplate"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.STORED));
-        assertThat(request.getScriptParams(), nullValue());
-    }
 
-    public void testParseStoredTemplateWithParams() throws Exception {
-        String source = "{'id' : 'another_template', 'params' : {'bar': 'foo'}}";
+    public static SearchTemplateRequest createRandomRequest() {
+        SearchTemplateRequest request = new SearchTemplateRequest();
+        request.setScriptType(randomFrom(ScriptType.values()));
+        request.setScript(randomAlphaOfLength(50));
 
-        SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
-        assertThat(request.getScript(), equalTo("another_template"));
-        assertThat(request.getScriptType(), equalTo(ScriptType.STORED));
-        assertThat(request.getScriptParams().size(), equalTo(1));
-        assertThat(request.getScriptParams(), hasEntry("bar", "foo"));
-    }
+        Map<String, Object> scriptParams = new HashMap<>();
+        for (int i = 0; i < randomInt(10); i++) {
+            scriptParams.put(randomAlphaOfLength(5), randomAlphaOfLength(10));
+        }
+        request.setScriptParams(scriptParams);
 
-    public void testParseWrongTemplate() {
-        // Unclosed template id
-        expectThrows(XContentParseException.class, () -> RestSearchTemplateAction.parse(newParser("{'id' : 'another_temp }")));
-    }
+        request.setExplain(randomBoolean());
+        request.setProfile(randomBoolean());
+        request.setSimulate(randomBoolean());
 
-    /**
-     * Creates a {@link XContentParser} with the given String while replacing single quote to double quotes.
-     */
-    private XContentParser newParser(String s) throws IOException {
-        assertNotNull(s);
-        return createParser(JsonXContent.jsonXContent, s.replace("'", "\""));
+        request.setRequest(RandomSearchRequestGenerator.randomSearchRequest(
+            SearchSourceBuilder::searchSource));
+        return request;
     }
 }

+ 197 - 0
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java

@@ -0,0 +1,197 @@
+/*
+ * 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.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParseException;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.script.ScriptType;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.nullValue;
+
+public class SearchTemplateRequestXContentTests extends AbstractXContentTestCase<SearchTemplateRequest> {
+
+    @Override
+    public SearchTemplateRequest createTestInstance() {
+        return SearchTemplateRequestTests.createRandomRequest();
+    }
+
+    @Override
+    protected SearchTemplateRequest doParseInstance(XContentParser parser) throws IOException {
+        return SearchTemplateRequest.fromXContent(parser);
+    }
+
+    /**
+     * Note that when checking equality for xContent parsing, we omit two parts of the request:
+     * - The 'simulate' option, since this parameter is not included in the
+     *   request's xContent (it's instead used to determine the request endpoint).
+     * - The random SearchRequest, since this component only affects the request
+     *   parameters and also isn't captured in the request's xContent.
+     */
+    @Override
+    protected void assertEqualInstances(SearchTemplateRequest expectedInstance, SearchTemplateRequest newInstance) {
+        assertTrue(
+            expectedInstance.isExplain() == newInstance.isExplain() &&
+            expectedInstance.isProfile() == newInstance.isProfile() &&
+            expectedInstance.getScriptType() == newInstance.getScriptType() &&
+            Objects.equals(expectedInstance.getScript(), newInstance.getScript()) &&
+            Objects.equals(expectedInstance.getScriptParams(), newInstance.getScriptParams()));
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    public void testToXContentWithInlineTemplate() throws IOException {
+        SearchTemplateRequest request = new SearchTemplateRequest();
+
+        request.setScriptType(ScriptType.INLINE);
+        request.setScript("{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }");
+        request.setProfile(true);
+
+        Map<String, Object> scriptParams = new HashMap<>();
+        scriptParams.put("my_field", "foo");
+        scriptParams.put("my_value", "bar");
+        request.setScriptParams(scriptParams);
+
+        XContentType contentType = randomFrom(XContentType.values());
+        XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType)
+            .startObject()
+                .field("source", "{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }")
+                .startObject("params")
+                    .field("my_field", "foo")
+                    .field("my_value", "bar")
+                .endObject()
+                .field("explain", false)
+                .field("profile", true)
+            .endObject();
+
+        XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType);
+        request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS);
+
+        assertToXContentEquivalent(BytesReference.bytes(expectedRequest),
+            BytesReference.bytes(actualRequest),
+            contentType);
+    }
+
+    public void testToXContentWithStoredTemplate() throws IOException {
+        SearchTemplateRequest request = new SearchTemplateRequest();
+
+        request.setScriptType(ScriptType.STORED);
+        request.setScript("match_template");
+        request.setExplain(true);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("my_field", "foo");
+        params.put("my_value", "bar");
+        request.setScriptParams(params);
+
+        XContentType contentType = randomFrom(XContentType.values());
+        XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType)
+            .startObject()
+                .field("id", "match_template")
+                .startObject("params")
+                    .field("my_field", "foo")
+                    .field("my_value", "bar")
+                .endObject()
+                .field("explain", true)
+                .field("profile", false)
+            .endObject();
+
+        XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType);
+        request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS);
+
+        assertToXContentEquivalent(
+            BytesReference.bytes(expectedRequest),
+            BytesReference.bytes(actualRequest),
+            contentType);
+    }
+
+    public void testFromXContentWithEmbeddedTemplate() throws Exception {
+        String source = "{" +
+                "    'source' : {\n" +
+                "    'query': {\n" +
+                "      'terms': {\n" +
+                "        'status': [\n" +
+                "          '{{#status}}',\n" +
+                "          '{{.}}',\n" +
+                "          '{{/status}}'\n" +
+                "        ]\n" +
+                "      }\n" +
+                "    }\n" +
+                "  }" +
+                "}";
+
+        SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source));
+        assertThat(request.getScript(), equalTo("{\"query\":{\"terms\":{\"status\":[\"{{#status}}\",\"{{.}}\",\"{{/status}}\"]}}}"));
+        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
+        assertThat(request.getScriptParams(), nullValue());
+    }
+
+    public void testFromXContentWithEmbeddedTemplateAndParams() throws Exception {
+        String source = "{" +
+            "    'source' : {" +
+            "      'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," +
+            "      'size' : '{{my_size}}'" +
+            "    }," +
+            "    'params' : {" +
+            "        'my_field' : 'foo'," +
+            "        'my_value' : 'bar'," +
+            "        'my_size' : 5" +
+            "    }" +
+            "}";
+
+        SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source));
+        assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}"));
+        assertThat(request.getScriptType(), equalTo(ScriptType.INLINE));
+        assertThat(request.getScriptParams().size(), equalTo(3));
+        assertThat(request.getScriptParams(), hasEntry("my_field", "foo"));
+        assertThat(request.getScriptParams(), hasEntry("my_value", "bar"));
+        assertThat(request.getScriptParams(), hasEntry("my_size", 5));
+    }
+
+    public void testFromXContentWithMalformedRequest() {
+        // Unclosed template id
+        expectThrows(XContentParseException.class, () -> SearchTemplateRequest.fromXContent(newParser("{'id' : 'another_temp }")));
+    }
+
+    /**
+     * Creates a {@link XContentParser} with the given String while replacing single quote to double quotes.
+     */
+    private XContentParser newParser(String s) throws IOException {
+        assertNotNull(s);
+        return createParser(JsonXContent.jsonXContent, s.replace("'", "\""));
+    }
+}

+ 211 - 0
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java

@@ -0,0 +1,211 @@
+/*
+ * 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.action.search.SearchResponse;
+import org.elasticsearch.action.search.ShardSearchFailure;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.text.Text;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHits;
+import org.elasticsearch.search.internal.InternalSearchResponse;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.function.Predicate;
+
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+
+public class SearchTemplateResponseTests extends AbstractXContentTestCase<SearchTemplateResponse> {
+
+    @Override
+    protected SearchTemplateResponse createTestInstance() {
+        SearchTemplateResponse response = new SearchTemplateResponse();
+        if (randomBoolean()) {
+            response.setResponse(createSearchResponse());
+        } else {
+            response.setSource(createSource());
+        }
+        return response;
+    }
+
+    @Override
+    protected SearchTemplateResponse doParseInstance(XContentParser parser) throws IOException {
+        return SearchTemplateResponse.fromXContent(parser);
+    }
+
+    /**
+     * For simplicity we create a minimal response, as there is already a dedicated
+     * test class for search response parsing and serialization.
+     */
+    private static SearchResponse createSearchResponse() {
+        long tookInMillis = randomNonNegativeLong();
+        int totalShards = randomIntBetween(1, Integer.MAX_VALUE);
+        int successfulShards = randomIntBetween(0, totalShards);
+        int skippedShards = randomIntBetween(0, totalShards);
+        InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty();
+
+        return new SearchResponse(internalSearchResponse, null, totalShards, successfulShards,
+            skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY);
+    }
+
+    private static BytesReference createSource() {
+        try {
+            XContentBuilder source = XContentFactory.jsonBuilder()
+                .startObject()
+                    .startObject("query")
+                        .startObject("match")
+                            .field(randomAlphaOfLength(5), randomAlphaOfLength(10))
+                        .endObject()
+                    .endObject()
+                .endObject();
+            return BytesReference.bytes(source);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    protected Predicate<String> getRandomFieldsExcludeFilter() {
+        String templateOutputField = SearchTemplateResponse.TEMPLATE_OUTPUT_FIELD.getPreferredName();
+        return field -> field.equals(templateOutputField) || field.startsWith(templateOutputField + ".");
+    }
+
+    /**
+     * Note that we can't rely on normal equals and hashCode checks, since {@link SearchResponse} doesn't
+     * currently implement equals and hashCode. Instead, we compare the template outputs for equality,
+     * and perform some sanity checks on the search response instances.
+     */
+    @Override
+    protected void assertEqualInstances(SearchTemplateResponse expectedInstance, SearchTemplateResponse newInstance) {
+        assertNotSame(newInstance, expectedInstance);
+
+        BytesReference expectedSource = expectedInstance.getSource();
+        BytesReference newSource = newInstance.getSource();
+        assertEquals(expectedSource == null, newSource == null);
+        if (expectedSource != null) {
+            try {
+                assertToXContentEquivalent(expectedSource, newSource, XContentType.JSON);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        assertEquals(expectedInstance.hasResponse(), newInstance.hasResponse());
+        if (expectedInstance.hasResponse()) {
+            SearchResponse expectedResponse = expectedInstance.getResponse();
+            SearchResponse newResponse = newInstance.getResponse();
+
+            assertEquals(expectedResponse.getHits().totalHits, newResponse.getHits().totalHits);
+            assertEquals(expectedResponse.getHits().getMaxScore(), newResponse.getHits().getMaxScore(), 0.0001);
+        }
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+
+    public void testSourceToXContent() throws IOException {
+        SearchTemplateResponse response = new SearchTemplateResponse();
+
+        XContentBuilder source = XContentFactory.jsonBuilder()
+            .startObject()
+                .startObject("query")
+                    .startObject("terms")
+                        .field("status", new String[]{"pending", "published"})
+                    .endObject()
+                .endObject()
+            .endObject();
+        response.setSource(BytesReference.bytes(source));
+
+        XContentType contentType = randomFrom(XContentType.values());
+        XContentBuilder expectedResponse = XContentFactory.contentBuilder(contentType)
+            .startObject()
+                .startObject("template_output")
+                    .startObject("query")
+                        .startObject("terms")
+                            .field("status", new String[]{"pending", "published"})
+                        .endObject()
+                    .endObject()
+                .endObject()
+            .endObject();
+
+        XContentBuilder actualResponse = XContentFactory.contentBuilder(contentType);
+        response.toXContent(actualResponse, ToXContent.EMPTY_PARAMS);
+
+        assertToXContentEquivalent(
+            BytesReference.bytes(expectedResponse),
+            BytesReference.bytes(actualResponse),
+            contentType);
+    }
+
+    public void testSearchResponseToXContent() throws IOException {
+        SearchHit hit = new SearchHit(1, "id", new Text("type"), Collections.emptyMap());
+        hit.score(2.0f);
+        SearchHit[] hits = new SearchHit[] { hit };
+
+        InternalSearchResponse internalSearchResponse = new InternalSearchResponse(
+            new SearchHits(hits, 100, 1.5f), null, null, null, false, null, 1);
+        SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null,
+            0, 0, 0, 0, ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY);
+
+        SearchTemplateResponse response = new SearchTemplateResponse();
+        response.setResponse(searchResponse);
+
+        XContentType contentType = randomFrom(XContentType.values());
+        XContentBuilder expectedResponse = XContentFactory.contentBuilder(contentType)
+            .startObject()
+                .field("took", 0)
+                .field("timed_out", false)
+                .startObject("_shards")
+                    .field("total", 0)
+                    .field("successful", 0)
+                    .field("skipped", 0)
+                    .field("failed", 0)
+                .endObject()
+                .startObject("hits")
+                    .field("total", 100)
+                    .field("max_score", 1.5F)
+                    .startArray("hits")
+                        .startObject()
+                            .field("_type", "type")
+                            .field("_id", "id")
+                            .field("_score", 2.0F)
+                        .endObject()
+                    .endArray()
+                .endObject()
+            .endObject();
+
+        XContentBuilder actualResponse = XContentFactory.contentBuilder(contentType);
+        response.toXContent(actualResponse, ToXContent.EMPTY_PARAMS);
+
+        assertToXContentEquivalent(
+            BytesReference.bytes(expectedResponse),
+            BytesReference.bytes(actualResponse),
+            contentType);
+    }
+}

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java

@@ -82,7 +82,7 @@ public class RandomSearchRequestGenerator {
      * @param randomSearchSourceBuilder builds a random {@link SearchSourceBuilder}. You can use
      *        {@link #randomSearchSourceBuilder(Supplier, Supplier, Supplier, Supplier, Supplier)}.
      */
-    public static SearchRequest randomSearchRequest(Supplier<SearchSourceBuilder> randomSearchSourceBuilder) throws IOException {
+    public static SearchRequest randomSearchRequest(Supplier<SearchSourceBuilder> randomSearchSourceBuilder) {
         SearchRequest searchRequest = new SearchRequest();
         searchRequest.allowPartialSearchResults(true);
         if (randomBoolean()) {