本节书摘来自华章计算机《深入理解Elasticsearch(原书第2版)》一书中的第2章,第2.3节,作者 [美]拉斐尔·酷奇(Rafal Ku)马雷克·罗戈任斯基(Marek Rogoziski),张世武 余洪淼 商旦 译,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.3 查询模板
在应用程序迭代的同时,它的运行环境很可能会越来越复杂。在你所处的组织中,很可能同一个应用程序的不同部分分别有专人负责,比如说,至少有一个前端工程师和一个负责数据库层的后端工程师。将应用程序划分为几个模块分别开发的方式非常便捷高效,它能够让开发人员针对程序的不同部分并行进行开发工作,而无需在开发者之间和开发小组内部时刻同步代码。当然,你正在阅读的这本书不是关于项目管理的,而是聚焦于搜索的,因此让我们回到正题上。有时候,我们可以整理出程序使用的所有查询语句交给搜索引擎工程师,让他们协助从性能和相关性两个方面对查询语句进行优化。这种做法通常是很有帮助的。在这种情况下,应用程序开发者只需要把查询传递给Elasticsearch,而不需要考虑查询语句的构造、查询DSL语法、查询结果过滤等细节知识。
2.3.1 引入查询模板
自Elasticsearch 1.1.0版本开始,我们可以自定义查询模板。让我们回到本书开头的在线书店例子中。假定我们已经确定了需要传递给Elasticsearch的查询语句的类型,不过查询结构并未最终确定,我们还需要对它进行微调和优化。通过使用查询模板,我们可以快速构建出查询的基础骨架,然后让应用程序来提供对应的参数,最终由Elasticsearch完成查询参数的替换。
假定我们有一个针对library索引的查询语句,可以返回最相关的书籍记录。在这个查询中,我们还允许用户选择是否对书籍的库存状态做筛选。在这个场景中,我们需要传入两个参数—一个查询短语和一个代表书籍库存状态的布尔变量。最初的简化示例如下:
代码中的QUERY和BOOLEAN是占位符,代表应用程序传递给查询的变量。显然这个查询语句对当前示例场景来说实在太简陋了,不过之前我们已经说过,这只是它的最初版本,我们马上将对它进行改进。
既然已经有了最初版本的查询语句,我们可以基于它创建第一个查询模板。对该查询语句做简单修改如下:
可以看出,原来的占位符被替换成了{{phrase}}和{{avail}}两个变量,并且添加了一个新的params片段。当Elasticsearch在解析查询语句时,遇到一个{{phrase}}变量,它将尝试从params片段中查找出名为phrase的参数,并用参数值替换掉{{phrase}}变量。通常,我们需要把参数值放到params片段中,并在query中使用形如{{var}}的标记来引用params片段中参数名为var的参数。此外,查询本身被嵌套进一个template元素中。通过这种方式,我们实现了查询的参数化。
接下来让我们使用HTTP GET请求把以下查询语句发送给地址为/library/_search/template的REST端点(注意这里不是我们通常使用的/library/_search端点)。请求命令构造如下:
字符串形式的查询模板
查询模板也可以以字符串的形式提供。比如,刚才的查询模板可以变成这样:
可见,这种形式不太适合阅读和书写,每个引号都需要被转义,换行符容易引发格式问题,因此需要避免使用。尽管如此,如果你需要使用Mustache(一个模板引擎,我们将在下一小节探讨),则必须使用这种格式(至少在Elasticsearch的1.1.0到1.4.0之间的所有版本中必须这样做)。
本书写作时,笔者所使用的Elasticsearch相关版本中有一个关于查询模板的小陷阱。如果你提供的查询模板中有错误,被Elasticsearch检测到后,会把错误写到服务日志里,但是从API的视角来看,错误查询将被忽略,接口将返回所有文档,就好像你刚刚发送了一个match_all查询一样。记得复查你的查询模板,直到这个缺陷不再存在。
2.3.2 Mustache模板引擎
Elasticsearch使用Mustache模板引擎(参考http://mustache.github.io )来为查询模板生成可用的查询语句。如你所见,每个变量被双大括号包裹,这一点是Mustache规范要求的,是该模板引擎间接引用变量的方式。Mustache模板引擎的完整语法不在本书讨论范围内,不过我们可以在这里简单介绍一下它最具魅力的部分,包括条件表达式、循环和默认值。
Mustache语法的详细内容请参阅http://mustache.github.io/mustache.5.html。
1. 条件表达式
{{val}}表达式用来插入变量val的值。{{#val}}和{{/val}}则用来在变量val取值计算为true时把位于它们之间的变量标记替换为变量值。
我们看一下下面这个示例:
这个命令将返回library索引中的所有文档。不过,假如我们把limit参数的取值改为true,则再次查询后,我们将只能得到两个文档。这是因为判断条件满足了,模板内容因此被激活。
不幸的是,似乎直到本书写作时,笔者所使用的Elasticsearch版本在处理条件表达式时仍然有些问题。比如,其中一个相关问题可以在这里看到:https://github.com/Elasticsearch/Elasticsearch/issues/8308 。我们决定保留条件表达式这一小节,以期望相关问题都能在未来得到解决。使用条件表达式可以更方便地构造查询模板。
2. 循环
循环结构定义和条件表达式一模一样,都位于{{#val}}和{{/val}}之间。如果表达式中变量取值是数组,则可以使用{{.}}标记来指代当前变量值。
例如,假定我们需要模板引擎遍历一个词项数组来生成一个词项查询,可以执行如下命令:
3. 默认值
默认值标记允许我们在参数未定义时给它设置默认取值。比如,给var变量设置默认值语法的代码如下:
举个例子,假定我们要给查询模板中的phrase参数设置默认值“crime”,可以使用如下命令:
这个命令将从Elasticsearch查询出所有title字段中包含front的文档。而如果我们在params片段中不指定phrase参数的值,则使用crime来代替。
2.3.3 把查询模板保存到文件
抛开之前定义模板的方式不说,我们距离把查询跟应用程序解耦还有相当长的一段路要走。我们能够做的仅仅是把查询语句参数化,而整个查询模板字符串仍然需要保存在应用程序中。幸运的是,有一种简单的方法来改变目前这种查询定义方式,它允许Elasticsearch从config/scripts 目录中动态读取查询模板。
举例来说,让我们创建一个名为bookList.mustache的文件(在config/scripts目录中)。使用如下命令:
接下来我们就可以在查询中用模板名称来使用该文件的内容了(模板名称就是模板文件名称去掉.mustache后缀)。例如,如果我们使用bookList模板,则可以使用如下命令:
Elasticsearch有一个非常方便的特性:它可以无需重启就检测到模板文件的变更。当然,我们还是需要在每个负责查询的Elasticsearch节点上部署查询模板文件。从Elasticsearch 1.4.0版本开始,你可以把模板索引到一个名为.scripts的特殊索引中。更多相关信息请参考Elasticsearch的官方文档:http://www.Elasticsearch.org/guide/en/Elasticsearch/reference/current/search-template.html。