我们发现一些用户经常编写了一些非常冗长和复杂的查询 - 在很多情况下,相同的查询会一遍又一遍地执行,但是会有一些不同的值作为参数来查询。在这种情况下,我们觉得使用一个 search template(搜索模板)来做这样的工作非常合适。搜索模板允许你使用可在执行时定义的参数定义查询。
Search template 的好处是:
- 避免在多个地方重复代码
- 更容易测试和执行您的查询
- 在应用程序间共享查询
- 允许用户只执行一些预定义的查询
- 将搜索逻辑与应用程序逻辑分离
定义一个Search template
首先,我们来定义一个 search template 来看看它到底是什么东西。使用_scripts端点将模板存储在集群状态中。在 search template中使用的语言叫做 mustache。
POST _scripts/my_search_template
{
"script": {
"lang": "mustache",
"source": {
"query": {
"match": {
"{{my_field}}": "{{my_value}}"
}
}
}
}
}
在这里,我们定义了一个叫做 my_search_template 的 search template。如果我们想更新这个 search template,我们可以直接进行修改,然后再次运行上面的命令即可。
我们可以试验如下的命令来验证我们的 search template:
POST _render/template
{
"id": "my_search_template",
"params": {
"my_field": "city",
"my_value": "Beijing"
}
}
上述命令输出搜索请求正文:
{
"template_output": {
"query": {
"match": {
"city": "Beijing"
}
}
}
}
我们也可以使用如下的 API 来测试 inline templates:
POST _render/template
{
"source": {
"query": {
"match": {
"{{my_field}}": "{{my_value}}"
}
}
},
"params": {
"my_field": "city",
"my_value": "Beijing"
}
}
上面的命令将返回和上述命令一样的结果。
运行一个带有 template 的搜索
要使用搜索模板运行搜索,请使用搜索模板 API。 你可以为每个请求指定不同的参数。
在 match 的字段里,我们定义了两个参数:my_field 及 my_value。下面,我们来首先建立一个叫做 twitter 的索引:
PUT twitter/_doc/1
{
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
PUT twitter/_doc/2
{
"user" : "虹桥-老吴",
"message" : "好友来了都今天我生日,好友来了,什么 birthday happy 就成!",
"uid" : 7,
"age" : 90,
"city" : "上海",
"province" : "上海",
"country" : "中国",
"address" : "中国上海市闵行区",
"location" : {
"lat" : "31.175927",
"lon" : "121.383328"
}
}
我们这里把上面的两个文档存于到 twitter 的i ndex 之中。我们现在可以使用我们刚才定义的 search template 来进行搜索:
GET twitter/_search/template
{
"id": "my_search_template",
"params": {
"my_field": "city",
"my_value": "北京"
}
}
显示的结果是:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.9808292,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.9808292,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
]
}
}
显示它只显示了我们的 city 为北京的一个文档,另外一个上海的文档没有做任何的显示。说明我们定义的 search template 是工作的。
运行多个模板的搜索
要使用单个请求运行多个模板化搜索,请使用多搜索模板 API。 与多个单独的搜索相比,这些请求通常具有更少的开销和更快的速度:
GET my-index/_msearch/template
{ }
{ "id": "my-search-template", "params": { "query_string": "hello world", "from": 0, "size": 10 }}
{ }
{ "id": "my-other-search-template", "params": { "query_type": "match_all" }}
设置默认值
要为变量设置默认值,请使用以下语法:
{{my-var}}{{^my-var}}default value{{/my-var}}
如果模板化搜索未在其参数中指定值,则搜索将使用默认值。 例如,以下模板设置了 from 和 size 的默认值。
POST _render/template
{
"source": {
"query": {
"match": {
"message": "{{query_string}}"
}
},
"from": "{{from}}{{^from}}0{{/from}}",
"size": "{{size}}{{^size}}10{{/size}}"
},
"params": {
"query_string": "hello world"
}
}
URL encode strings
使用 {{#url}} 函数对字符串进行 URL 编码。
POST _render/template
{
"source": {
"query": {
"term": {
"url.full": "{{#url}}{{host}}/{{page}}{{/url}}"
}
}
},
"params": {
"host": "http://example.com",
"page": "hello-world"
}
}
模板呈现为:
{
"template_output": {
"query": {
"term": {
"url.full": "http%3A%2F%2Fexample.com%2Fhello-world"
}
}
}
}
连接数值
使用 {{#join}} 函数将数组值连接为逗号分隔的字符串。 例如,以下模板连接两个电子邮件地址。
POST _render/template
{
"source": {
"query": {
"match": {
"user.group.emails": "{{#join}}emails{{/join}}"
}
}
},
"params": {
"emails": [ "user1@example.com", "user_one@example.com" ]
}
}
模板呈现为:
{
"template_output": {
"query": {
"match": {
"user.group.emails": "user1@example.com,user_one@example.com"
}
}
}
}
你还可以指定自定义分隔符。
POST _render/template
{
"source": {
"query": {
"range": {
"user.effective.date": {
"gte": "{{date.min}}",
"lte": "{{date.max}}",
"format": "{{#join delimiter='||'}}date.formats{{/join delimiter='||'}}"
}
}
}
},
"params": {
"date": {
"min": "2098",
"max": "06/05/2099",
"formats": ["dd/MM/yyyy", "yyyy"]
}
}
}
模板呈现为:
{
"template_output": {
"query": {
"range": {
"user.effective.date": {
"gte": "2098",
"lte": "06/05/2099",
"format": "dd/MM/yyyy||yyyy"
}
}
}
}
}
转换为 JSON
使用 {{#toJson}} 函数将变量值转换为其 JSON 表示形式。
例如,以下模板使用 {{#toJson}} 传递数组。 为了确保请求体是有效的 JSON,source 以字符串格式编写。
POST _render/template
{
"source": "{ \"query\": { \"terms\": { \"tags\": {{#toJson}}tags{{/toJson}} }}}",
"params": {
"tags": [
"prod",
"es01"
]
}
}
模板呈现为:
{
"template_output": {
"query": {
"terms": {
"tags": [
"prod",
"es01"
]
}
}
}
}
你还可以使用 {{#toJson}} 传递对象。
POST _render/template
{
"source": "{ \"query\": {{#toJson}}my_query{{/toJson}} }",
"params": {
"my_query": {
"match_all": { }
}
}
}
模板呈现为:
{
"template_output" : {
"query" : {
"match_all" : { }
}
}
}
你还可以传递一个对象数组:
POST _render/template
{
"source": "{ \"query\": { \"bool\": { \"must\": {{#toJson}}clauses{{/toJson}} }}}",
"params": {
"clauses": [
{
"term": {
"user.id": "kimchy"
}
},
{
"term": {
"url.domain": "example.com"
}
}
]
}
}
模板呈现为:
{
"template_output": {
"query": {
"bool": {
"must": [
{
"term": {
"user.id": "kimchy"
}
},
{
"term": {
"url.domain": "example.com"
}
}
]
}
}
}
}
条件判断
在 Mustache 语言中,它没有 if/else 这样的判断,但是你可以定 section 来跳过它如果那个变量是 false 还是没有被定义:
{{#param1}}
"This section is skipped if param1 is null or false"
{{/param1}}
我们定义如下的一个 search template:
POST _scripts/docs_from_beijing_and_age
{
"script": {
"lang": "mustache",
"source":
"""
{
"query": {
"bool": {
"must": [
{
"match": {
"city": "{{search_term}}"
}
}
{{#search_age}}
,
{
"range": {
"age": {
"gte": {{search_age}}
}
}
}
{{/search_age}}
]
}
}
}
"""
}
}
在这里,我们同时定义了两个变量:search_term 及 search_age。针对 search_age,我们做了一个判断,如果它有定义,及做一个 range 的查询。如果没有定义,就只用 search_term。那么我们来做如下的实验:
GET twitter/_search/template
{
"id": "docs_from_beijing_and_age",
"params": {
"search_term": "北京"
}
}
显示的结果是:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.9808292,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.9808292,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
]
}
}
显然,city 为北京的文档已经被搜索到了。如果我们做如下的查询:
GET twitter/_search/template
{
"id": "docs_from_beijing_and_age",
"params": {
"search_term": "北京",
"search_age": "30"
}
}
我们将搜索不到任何的结果,这是因为在这次查询中 search_age 已经被启用,而且在数据库中没有一个文档是来自“北京”,并且年龄大于 30 的。我们可以做如下的查询:
GET twitter/_search/template
{
"id": "docs_from_beijing_and_age",
"params": {
"search_term": "北京",
"search_age": "20"
}
}
那么这次的显示结果为:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.9808292,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.9808292,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
]
}
}
显然这次我们搜索到我们想要的结果。
查询 search template
GET _scripts/<templateid>
针对我们的情况:
GET _scripts/docs_from_beijing_and_age
显示的结果为:
{
"_id" : "docs_from_beijing_and_age",
"found" : true,
"script" : {
"lang" : "mustache",
"source" : """
{
"query": {
"bool": {
"must": [
{
"match": {
"city": "{{search_term}}"
}
}
{{#search_age}}
,
{
"range": {
"age": {
"gte": {{search_age}}
}
}
}
{{/search_age}}
]
}
}
}
"""
}
}
这个正是我们之前定义的一个 search template。
删除一个 search template
我们可以通过如下的命令来删除一个已经创建的 search template:
DELETE _scripts/<templateid>
验证 search template
我们可以通过 _render 端点来验证我们的 search template。比如:
GET _render/template
{
"source": """
{
"query": {
"bool": {
"must": [
{
"match": {
"city": "{{search_term}}"
}
}
{{#search_age}}
,
{
"range": {
"age": {
"gte": {{search_age}}
}
}
}
{{/search_age}}
]
}
}
}
""",
"params": {
"search_term": "北京",
"search_age": "20"
}
}
那么显示的结果是:
{
"template_output" : {
"query" : {
"bool" : {
"must" : [
{
"match" : {
"city" : "北京"
}
},
{
"range" : {
"age" : {
"gte" : 20
}
}
}
]
}
}
}
}
显然,这个就是我们想要的结果。
参考: