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

This commit is contained in:
Julie Tibshirani 2018-05-15 13:07:58 -07:00 committed by GitHub
parent 03dd2ab499
commit 4f9dd37169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1089 additions and 171 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 AbstractStreamableTestCase<SearchTemplateRequest> {
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());
@Override
protected SearchTemplateRequest createBlankInstance() {
return new 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 createTestInstance() {
return createRandomRequest();
}
public void testParseInlineTemplateAsString() throws Exception {
String source = "{'source' : '{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\":{\\\"match\\\":{\\\"foo\\\":\\\"{{text}}\\\"}}}}}'}";
@Override
protected SearchTemplateRequest mutateInstance(SearchTemplateRequest instance) throws IOException {
List<Consumer<SearchTemplateRequest>> mutators = new ArrayList<>();
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());
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;
}
@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"));
}
public static SearchTemplateRequest createRandomRequest() {
SearchTemplateRequest request = new SearchTemplateRequest();
request.setScriptType(randomFrom(ScriptType.values()));
request.setScript(randomAlphaOfLength(50));
public void testParseStoredTemplate() throws Exception {
String source = "{'id' : 'storedTemplate'}";
Map<String, Object> scriptParams = new HashMap<>();
for (int i = 0; i < randomInt(10); i++) {
scriptParams.put(randomAlphaOfLength(5), randomAlphaOfLength(10));
}
request.setScriptParams(scriptParams);
SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source));
assertThat(request.getScript(), equalTo("storedTemplate"));
assertThat(request.getScriptType(), equalTo(ScriptType.STORED));
assertThat(request.getScriptParams(), nullValue());
}
request.setExplain(randomBoolean());
request.setProfile(randomBoolean());
request.setSimulate(randomBoolean());
public void testParseStoredTemplateWithParams() throws Exception {
String source = "{'id' : 'another_template', 'params' : {'bar': 'foo'}}";
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"));
}
public void testParseWrongTemplate() {
// Unclosed template id
expectThrows(XContentParseException.class, () -> RestSearchTemplateAction.parse(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("'", "\""));
request.setRequest(RandomSearchRequestGenerator.randomSearchRequest(
SearchSourceBuilder::searchSource));
return request;
}
}

View File

@ -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("'", "\""));
}
}

View File

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

View File

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