2.5 选择正确的查询方式
在《Elasticsearch Server,Second Edition》一书中,我们详细介绍了Elasticsearch的查询DSL,这一种使用JSON结构化的查询语言,可以构建极其复杂的查询语句。不过,在那本书中我们没有探讨在不同的场合可以用到哪些查询方式,以及应该使用哪种查询方式。对于一个在全文搜索引擎领域没有经验储备的人来说,Elasticsearch提供的查询方式显得太多了,而且容易让人迷惑。因此我们将在本书中对这方面的知识做一些深入探讨,从而引导读者如何选择合适的查询方式。
我们把本节内容划分为独立的两部分。第1部分试图对所有查询方式进行分类,并告诉你在特定分类下的查询将会产生什么输出。第2部分将针对每种分类举例加以说明,并探讨分类间的不同。请注意,本节接下来的内容不是对Elasticsearch的查询DSL的完整阐释,如果你需要了解查询DSL的基本知识,请参考《Elasticsearch Server,Second Edition》一书,或者查阅Elasticsearch的官方文档:http://www.Elasticsearch.org/guide/en/Elasticsearch/reference/current/query-dsl.html。
2.5.1 查询方式分类
当然,对查询方式进行分类是一件艰难的任务,我们也不敢打包票说在这里给出的分类列表是唯一正确的。我们甚至可以说,如果你询问其他Elasticsearch使用者,他们可能会给出自己的分类方式,或者声称每个查询方式都可以被归入多个类别。有趣的是,他们有可能是对的。我们也曾考虑过多种分类方式存在的情况,不过,最终我们认为,每个查询方式都可以被归入以下列出的一个或多个类别中。
- 基本查询:这类查询允许针对索引的一部分进行检索,其输入数据既可以分析也可以不做分析。这类查询的一个关键特征是,不支持在其内部再嵌套其他查询。基本查询的一个示例是term查询。
- 组合查询:在这类查询中可以包含其他查询和过滤器,比如bool查询和dismax查询。
- 无分析查询:这类查询不分析输入内容,直接将它们原样传递给Lucene。term查询就是这类查询的一员。
- 全文检索查询:这类查询成员众多。许多查询都支持全文检索、输入内容分析、同时很可能支持可被Lucene识别的查询语法。比如match查询。
- 模式匹配查询:这类查询都在查询语句中支持各种通配符。比如,前缀查询可以归入此类。
- 支持相似度操作的查询:这类查询拥有一个共同的特性—支持近似词语的匹配。这类查询的成员如fuzzy_like_this、more_like_this查询等。
- 支持打分操作的查询:这类查询非常重要,尤其是在和全文搜索查询组合使用的场景下。这个类别包括那些允许在查询时修改打分计算过程的查询方式。在第3章介绍的function_score查询可以归入此类。
- 位置敏感查询:这类查询允许我们使用索引中存储的词项位置信息。span_term查询就是一个很好的例子。
- 结构敏感查询:这类查询的工作基于结构数据,如父子文档结构。这个类别的一个例子是nested one查询。
当然,我们在这里只讨论查询分类,不探讨过滤器的分类。不过,对过滤器来说,你也可以使用相同的分类逻辑。让我们先把过滤器置之脑后。在距离阐释每种查询类别之前,我们先简短描述一下每种查询类别的目的。
1. 基本查询
在基本查询内部不可以包含其他查询,它们只有索引检索这一个用途。这类查询通常作为其他复杂查询的一部分或者单独传递给Elasticsearch。你可以把基本查询比作修筑大厦的砖块,而大厦就是各种复杂查询。举个例子,如果你想匹配某个文档中的一个特定词项,且没有其他要求,可以考虑使用基本查询。本例中,match查询就能很好地满足需求,无需再跟其他查询组合使用。
归属于基本查询的一些查询方式举例如下:
- match查询:一种(实际上指好几种)查询方式,适用于执行全文检索且需要对输入进行分析的场景。一般来说,当需要分析输入内容却不需要完整Lucene查询语法支持时,可以使用这种查询方式。这种查询不需要进行查询语法解析,发生解析错误的概率极低,因此特别适合接收用户输入文本的场景。
- match_all查询:这个查询匹配所有文档,常用于需要对所有索引内容进行归类处理的场景。
- term查询:一种简单的、无需对输入进行分析的查询方式,可以查询单个词语。这种查询方式的使用场景包括针对不分词字段进行检索,比如在我们的测试代码中检索tags字段。term查询还经常跟过滤器配合使用,比如在我们的测试代码中针对category字段进行过滤操作。
简单查询分类可包括:match,multi_match,common,fuzzy_like_this,fuzzy_like_this_field,geoshape,ids,match_all,query_string,simple_query_string,range,prefix, regexp,span_term,term,terms,wildcard查询。
2. 组合查询
组合查询的唯一用途是把其他查询组合在一起使用。如果说简单查询是建造高楼的砖块,组合查询就是粘合这些砖块的水泥。我们可以把组合查询无穷嵌套,用来构建极其复杂的查询,唯一能够阻止我们这样嵌套的障碍是性能。
组合查询的一些示例和用法如下。
- bool查询:最常用的组合查询方式之一。能够把多个查询用布尔逻辑组织在一起,可以控制查询的某个子查询部分是必须匹配、可以匹配还是不应该匹配。如果我们要把匹配不同查询条件的查询组合在一起使用,bool查询就是一个很好的选择。Bool查询还可以用在这样的场景:我们希望结果文档的最终得分为所有子查询得分的和。
- dis_max查询:一种非常有用的查询方式。这种查询的结果文档得分和最高权重的子查询打分高度相关,而不是如bool查询那样对所有子查询得分进行求和。Dis_max查询返回匹配所有子查询的文档,并通过一个简单公式计算最终得分:max(各子查询的得分)+tie_breaker*(非最高得分子查询的得分之和)。如果你希望最高得分子查询能够在打分过程中起决定作用,dis_max查询是不二选择。
组合查询类别可包括这些查询方式:bool,boosting,constant_score,dis_max,filtered,function_score,has_child,has_parent,indices,nested,span_first,span_multi, span_first,span_multi,span_near,span_not,span_or,span_term,top_children查询。
3. 无分析查询
有一类查询不会被分析,而是直接传递给Lucene索引。这意味着我们既不需要操心分析过程是否如我们期望的方式执行并生成合适的词项,也不需要针对特定的不分词字段执行查询。如果你把Elasticsearch当作NoSQL数据库使用,这种查询方式就比较适合你。这类查询精确匹配传入的词语,不会使用语言分析器等工具对词语进行分词和其他处理。
以下示例可帮你理解无分析查询的目的。
- term查询:即词项查询。当提及无分析查询时,最常用的无分析查询就是term查询。它可以匹配某个字段中的给定值。比如说,如果你希望匹配一个拥有特定标签(我们示例文档中的tags字段)的文档,可以使用term查询。
- prefix查询:即前缀查询。另一种无需分析的查询方式。前缀查询常用于自动完成功能,用户输入一段文本,搜索系统返回所有以这个文本开头的文档。需要注意的是,尽管前缀查询没有被分析,Elasticsearch还是对它进行了重写,以确保它能高速执行。
这类查询包括:common,ids,prefix,span_term,term,terms,wildcard查询。
4. 全文检索查询
当你需要构建类似Google的查询界面时,可以使用这种查询类别。这类查询会根据索引映射配置对输入进行分析,支持Lucene语法和打分计算等功能。一般来说,如果查询的一部分文本来自于用户输入,则可以从全文检索查询类别中选择其一,比如query_string、match或simpe_query_string查询。
全文检索查询类别的示例和用法如下。
- simle_query_string查询:该查询方式构建于Lucene的SimpleQueryParser类(参考http://lucene.apache.org/core/4_9_0/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html ,被设计为解析人类可读的查询串)之上。通常情况下,如果你不希望在遭遇解析错误时直接失败,而是尝试给出用户期望的答案,那么这种查询方式是不错的选择。
属于本类的查询方式包括:match,multi_match,query_string,simple_query_string查询。
5. 模式匹配查询
Elasticsearch直接或间接提供了一些支持通配符的查询方式,比如通配符查询(wildcard query)和前缀查询(prefix query)。除此之外,我们还可以使用正则表达式查询(regexp query),这种查询能够找出内容中包含给定模式的文档。
我们在之前已经展示过一个前缀查询的示例,因此在这里主要介绍一下正则表达式查询。如果想找出其词项匹配某个固定模式的文档,正则表达式查询是唯一的选择。举个例子,假定你的各种日志存储于Elasticsearch中,可以使用正则表达式查询找出所有含有如下词项的日志记录:词项以“err”前缀开头、以“memory”结尾、中间可以有任意数量的字符。最后,需要注意的是,所有模式匹配查询如果包含可匹配海量词项的表达式,性能代价将十分高昂。
本类查询包括:prefix,regexp,wildcard查询。
6. 支持相似度操作的查询
我们认为这类查询是一些可以根据给定词项查找近似词项或文档的查询方式的集合。举例来说,假定我们需要找出包含“crimea”近似词项的文档,可以执行一个fuzzy查询。这类查询的另一个用途是提供类似“你是不是想找XXX”的功能。比如你希望找出文档标题和输入文本相似的文档,可以使用more_like_this查询。一般来说,你可以使用本类别下的某个查询来查找包含和给定输入近似词项或字段的文档。
属于这个类别的查询有:fuzzy_like_this,fuzzy_like_this_field,fuzzy,more_like_this,more_like_this_field查询。
7. 支持打分操作的查询
这是一组用于改善查询精度和相关度的查询方式。这类查询可以通过指定自定义权重因子或提供额外处理逻辑的方式来改变文档得分。这类查询的一个很好的例子是function_score查询。function_score查询可以让我们使用函数,从而通过数学计算的方式改变文档得分。举个例子,如果你希望离给定地理定位点越近的文档得分越高,则function_score查询可以帮助实现这个目的。
本类查询包括:boosting,constant_score,function_score,indices查询。
8. 位置敏感查询
这类查询不仅可以匹配特定词项,还能匹配词项的位置信息。Elasticsearch提供的各种范围查询就是这类查询的典型代表。我们还可以把match_phrase查询归入本类,因为至少从某种程度上来说,它也需要考虑被索引词项的位置信息。如果需要找出一组和其他单词保持一定距离的单词,比如“找出以下文档,同时包含mastering和Elasticsearch且这两个单词相互临近,其后不超过距离3的位置包含second和edition单词”,可以使用各种范围查询。不过,需要注意的是,这些范围查询将在未来版本的Lucene库中将被移除,届时Elasticsearch也不再提供支持。这是因为这些查询开销很大,需要消耗大量CPU资源才能保证正确处理。
本类查询包括:match_phrase,span_first,span_multi,span_near,span_not,span_or,span_term查询。
9. 结构敏感查询
最后一类查询是结构敏感查询(structure aware query)。这类查询包括:
- nested查询
- has_child查询
- has_parent查询
- top_children查询
一般来说,所有支持对文档结构进行检索并且不需要对文档数据进行扁平化处理的查询方式都可以归入此类。如果你正在寻找一种查询方式,能够在子文档或嵌套文档中进行查询,或查找属于给定父文档的子文档,则需要使用刚刚提及的查询方式之一。换句话说,如果需要处理文档中的数据关系,请选择使用这类查询。不过需要注意的是,尽管Elasticsearch可以支持一些数据关系,但它毕竟不是真正的关系数据库。
2.5.2 使用示例
既然我们已经了解了各类查询方式的适用场合以及期望结果,现在可以趁热打铁,用具体的使用示例来进一步加强对它们的认知。注意,这些例子并不能覆盖Elasticsearch查询的方方面面,而仅仅是对于我们通过查询获取所需信息的一些简单示例说明。
1. 测试数据
为了达到本节的目的,我们给library索引加入了两个新文档。
首先,我们需要微调一下索引映射以支持嵌套文档(本节某些例子中需要用到)。修改映射的命令如下:
然后,接着索引两个新文档。相关命令如下:
2. 基本查询示例
让我们看一下使用基本查询类别的例子。
(1)查询给定范围的数据
匹配给定取值范围的文档的查询是最简单的查询方式之一。通常,这种查询作为一个更复杂查询或过滤器的一部分而存在。举例来说,一个可以查出副本数在[1,3]区间的书籍的查询如下所示:
(2)简化的多词项查询
想象一个场景,用户需要传入一组书籍标签,期望查询出匹配这些标签的书籍。还有一个条件,如果用户给出的标签超过3个,则只要求至少75%的给定标签与索引中的书籍匹配。通常我们可以通过bool查询去实现这个目的,不过Elasticsearch提供了可以实现相同目的的terms查询。执行查询的命令如下:
3. 组合查询示例
现在我们看看如何使用组合查询来组合其他查询方式。
(1)对匹配文档加权
最简单的示例是使用包含一个可选的加权片段的bool查询来实现对部分文档的权重提升。举例来说,如果需要找出所有至少拥有一个副本的书籍,并对1950年后出版的书籍进行加权,可以使用如下查询命令:
(2)忽略查询的较低得分部分
我们之前提到的dis_max查询可以控制查询中较低得分部分的影响。举例来说,如果我们期望找出所有title字段匹配“crime punishment”或characters字段匹配“raskolnikov”的文档,并在文档打分时仅考虑得分最高的查询片段,可以执行如下查询命令:
查询结果如下:
我们来单独看一下查询各部分的打分。可以单独执行如下查询片段:
查询结果如下:
单独执行下一个查询片段的命令如下:
查询结果如下:
可以看出,dis_max查询返回的文档得分等于打分最高的查询片段的得分(上面的第一个查询片段)。这是因为我们设置tie_breaker属性为0.0。
4. 无分析查询示例
让我们看看两个不使用任何分析器的查询示例。
(1)找出符合标签的结果
Elasticsearch提供的term查询是最简单的无分析查询之一。我们一般很少单独使用term查询,而是常常将其使用在各种复合查询中。举个例子,假设我们想要查找出所有tags字段包含“novel”值的书籍。为了达到这个目的,需要执行如下查询命令:
(2)在查询时高效处理停用词
Elasticsearch提供了普通的terms查询,可以在查询时用一种高效的方式处理停用词。它将查询词项分成两组—重要的词项和不重要词项。重要词项的出现频率较低,相反,不重要词项的出现频率很高。Elasticsearch首先用重要词项执行查询并计算文档得分,然后再使用不重要词项执行查询,这时不再计算文档得分。因此查询可以变得更快。
举例来说,以下两个查询单就查询结果而言非常相似,而结果的打分却不一样。注意,如果想清楚地看出两者打分的不同,我们需要准备大量的数据样本,并在索引时禁用停用词。
第二个查询如下:
5. 全文检索查询示例
全文检索是一个宽泛的主题,其使用场景也十分广泛。在这里我们选出两个简单场景的查询示例加以展示。
(1)使用Lucene查询语法
某些时候,使用Lucene查询语法是不错的选择。我们曾在1.1.4节介绍过Lucene查询语法。举个例子,假如我们想找出title字段包含“sorrows”和“young”词项、author字段包含“von goethe”短语,并且副本数不超过5个的文档,可以执行如下查询:
在这个查询中,我们使用了Lucene查询语法来传递所有匹配条件,让Lucene通过查询解析器来构造合适的查询。
(2)对用户查询串进行容错处理
在某些场景下,来自用户的查询可能包含错误。比如,下面这个查询:
Elasticsearch将返回如下响应:
这意味着在构建查询时遇到了解析错误,查询无法被成功地构建出来。这也是Elasticsearch引入simple_query_string查询的原因。它使用一个可尝试处理用户输入错误的查询解析器,并试图猜测用户的查询用意。如果用simple_query_string查询来改写上面这个例子,代码如下:
如果执行这个查询,你将看到Elasticsearch能够返回合适的文档结果,尽管查询并未被恰当构造。
6. 模式匹配查询示例
模式匹配查询的例子很多,不过在这里我们只打算展示其中两个。
(1)使用前缀查询实现自动完成功能
针对索引数据提供自动完成功能是一种常见的应用场景。如我们所知,前缀查询不会被分析,直接工作于特定字段中被索引的词项上。因此,实际的功能依赖于索引时生成词条的方式。举例来说,假定我们希望针对title字段的所有词条提供自动完成功能。此时用户输入的前缀是“wes”,符合条件的对应查询构造如下:
(2)模式匹配
如果我们想匹配特定模式,而此时索引中的词条无法支持,可以尝试使用regexp查询。读者需要注意的是,这种查询非常昂贵,请尽量避免使用。当然,有时候我们不得不使用它。还有一点需要注意的是,regexp查询的执行性能与所选正则表达式相关。如果你选择了一个能够被改写成大量词项的正则表达式,执行性能将极其糟糕。
现在我们来看一下使用regexp查询的例子。假定我们需要找出符合以下条件的文档:文档的characters字段中包含以“wat”开头、以“n”结尾、中间有两个任意字符的词项。为了实现这些条件,可以使用类似下面的regexp查询命令:
7. 支持相似度操作的查询示例
让我们看两个关于如何查找近似文档和词项的简单示例。
(1)找出给定词项的近似词项
一个非常简单的例子是使用fuzzy查询找出包含给定词项近似词项的文档。比如,如果我们需要查找包含“crimea”的近似词项的文档,可以执行如下查询命令:
(2)找出拥有近似字段值的文档
另一个相似度查询的案例是,根据我们在查询中提供的字段值,找出包含类似字段值的文档。比如,我们想找出title字段值类似“western front battles”的书籍,可以构造如下查询:
查询结果如下:
从上面结果可见,有时候查询结果跟我们的期望有些出入(比如结果中第2本书的标题)。这是因为Elasticsearch认为它们之间有相似性。在前面这个查询中,Elasticsearch将对所有词项执行一次模糊查询,然后为匹配的文档选择出一组最佳查分词项。
8. 支持打分操作的查询示例
涉及相关度,Elasticsearch提供了一些可以按需修改文档得分的查询。当然,大多数查询方式都支持权重,可以让我们拥有更多的操作余地。接下来让我们看看两个支持打分操作的查询示例。
(1)偏爱新书
假定我们更喜欢新出版的书籍,因此1986年出版的书籍要比1870年出版的书籍拥有更高的得分。满足这个需求的查询命令如下:
我们将在第3章介绍function_score查询。在这里,如果你仔细观察刚才这个查询的响应结果,可以发现越新的书籍得分越高。
(2)对拥有某些值的书籍扣分
有时候,我们需要降低某些文档的重要性,却依然要在结果列表中输出它们。举个例子,我们可能想要列出所有的书籍,不过要通过降低书籍得分的方式把那些当前无货的书籍放到结果列表的末尾。我们不希望按标记是否有货的字段进行排序,因为用户有时候清楚地知道他要找什么书,因此针对全文检索查询的结果得分是很重要的。不过,如果仅仅想把当前无货的书籍排到结果尾部,可以执行如下查询命令:
9. 位置敏感查询示例
位置敏感查询允许我们匹配包含正确次序短语和词项的文档。这类查询因为资源占用问题,不经常被使用。让我们看两个例子。
(1)匹配短语
匹配短语的match_phrase查询是最简单的位置敏感查询,也是本类别中使用最多的。举例来说,otitle字段匹配短语“leiden des jungen”的查询命令如下:
(2)处处可见范围查询
当然,短语查询在处理位置敏感需求时非常简便。不过,如果我们想执行一个查询,找出符合以下条件的文档:在“die”词项后面不超过两个位置的地方包含一个“des jungen”短语,并且紧跟着短语后面是一个“werthers”词项。这时候,该怎么办呢?我们可以使用范围查询。符合这些条件的查询命令类似如下:
注意,范围查询是不经过分析器处理的。我们可以通过查看Explain API的响应来确认这一点。为了看到Explain API的响应,我们需要把刚才这个查询命令的请求体(或者叫查询内容)发送到/library/book/5/_explain这个REST端点。响应的有趣部分如下:
10. 结构敏感查询示例
如果涉及嵌套文档或父子文档关系,迟早会用到结构敏感查询。让我们从下面两个例子中看一下这类查询是如何使用的。
(1)返回包含某个嵌套子文档的父文档
第一个例子非常简单。假定我们要查找所有拥有4星及以上评论的书籍。相应的查询命令如下:
(2)嵌套子文档的得分影响父文档得分
假定我们要返回所有拥有评论的书籍,并且按照最高评论星级对这些书籍进行排序。满足这些条件的查询命令如下: