《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

本节书摘来自华章计算机《深入理解Elasticsearch(原书第2版)》一书中的第2章,第2.4节,作者 [美]拉斐尔·酷奇(Rafal Ku)马雷克·罗戈任斯基(Marek Rogoziski),张世武 余洪淼 商旦 译,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.4 过滤器的使用及作用原理

接下来,我们一起认识一下Elasticsearch提供的过滤功能。初看起来,过滤好像一个多余的功能,因为几乎每个过滤器在Elasticsearch查询DSL中都以一个与查询代码极其相似的方式呈现的。不过,这些过滤器一定有其独到之处,不然它们就不会在查询性能优化时被广泛使用和推荐了。本节我们将着重探讨为什么过滤器如此重要,它们的工作原理,以及Elasticsearch都提供了哪些过滤器给我们使用。

2.4.1 过滤及查询相关性

普通查询和过滤的第一个差异在于它们对文档打分的影响。让我们举例对比一下查询和过滤的输出。首先执行如下查询:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理
《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

这个查询的结果如下:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

这个查询没有任何特异之处。Elasticsearch将返回所有在title字段中包含“front”的文档。需要指出的是,每个和查询匹配的文档都会被计算得分,其中得分最高的一组文档被作为查询结果返回给用户。在本例中,该查询返回了一篇得分为0.11506981的文档。以上这些就是查询的一般行为。

接着我们来对比一下查询和过滤。在一个同时包含查询和过滤的例子中,我们将加入一段代码片段,限制返回文档只能有一个副本(copies字段取值为1)。不使用过滤的查询方式如下:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理
《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

Elasticsearch返回的查询结果和上一个查询非常相似:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

上面这段查询代码中的bool查询由两个term查询构成,每个结果文档都需要同时匹配这两个term查询。这个查询返回了和上一查询相同的文档,不过文档得分变成了0.98976034。这和我们读完2.1节后的期望一致:每个词项都会影响得分。

接下来我们来看看使用过滤的查询方式,在titile字段匹配“front”的查询,同时针对copies字段进行过滤。

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

现在,我们构造好了一个term查询,同时还添加了一个term过滤器。从下面的返回代码中可以看出,输出的文档和不使用过滤时一样,不过文档得分发生了变化。

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理
《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

这个文档的得分为0.11506981,这和本节最开始的查询结果一模一样。通过得分对比我们得出结论:过滤不影响文档得分。

 旧版Elasticsearch使用“filter”而不是上述代码中的“post_filter”来标识查询语句中的过滤片段。在1.x版本中,这两种标记方式都可以正常使用,不过请注意,“filter”方式可能将在之后的版本中停用。

一般来说,查询和过滤在工作过程中存在一个主要的差异。过滤的唯一目的是用特定筛选条件来缩小结果范围。而查询不仅缩小结果范围,还会影响文档的得分,这一点在强调文档相关性时非常重要,不过需要付出一定的代价:需要额外的CPU消耗来计算文档得分。当然,这不是查询和过滤的唯一区别。本节剩余部分将着重探讨过滤器的工作原理和Elasticsearch提供的不同过滤方式之间的异同。

2.4.2 过滤器的工作原理

前一小节我们已经提到,过滤不影响所匹配文档的得分。基于两个原因,这一点非常重要。第1个原因是性能。针对索引中的一组文档进行过滤操作是非常简单高效的。过滤器持有的关于文档的唯一重要信息是该文档是否匹配这个过滤器—仅仅一个标记而已。

过滤器通过返回一个被称为DocIdSet(org.apache.lucene.search.DocIdSet)的数据结构来提供这类匹配信息。这个数据结构的用途是为索引段提供经过滤器过滤后的数据。它可以使用Bits接口(org.apache.lucene.util.Bits)的有关实现。Bits接口可以随机访问过滤器中的文档信息(主要是检查索引段中的某个文档是否和该过滤器匹配)。Bits的数据结构非常高效,因为CPU可以使用位运算来完成过滤(有一个精巧的CPU部件用来处理这类运算,详情参考环形移位的*http://en.wikipedia.org/wiki/Circular_shift)。DocIdSet还针对内部文档标识的有序集合提供了一个DocIdSetIterator迭代器给我们使用。

下表展示了这些类是如何使用Bits进行工作的。

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

Lucene(以及Elasticsearch)提供了DocIdSet的多种实现来应对不同场景。不同实现的性能各不相同。不过,选择合适的实现是Lucene和Elasticsearch的职责,我们一般不需要关心这一点,除非我们要针对它们进行功能扩展。

 不是所有的过滤器都使用Bits结构,比如数值区间过滤器、脚本过滤器、以及基于地理位置的一组过滤器。这些特殊的过滤器选择把数据记录在字段缓存里,然后再遍历所需处理的文档集合,逐个进行过滤操作。这意味着过滤器链条中的下一个过滤器只能获取到匹配前一个过滤器的文档集合。因此,可以针对这些过滤器进行优化,比如把最重的(译者注:匹配文档最多的,或者性能最差的)过滤器放到过滤器链的最后去执行。

布尔过滤器和与或非过滤器

我们在《Elasticsearch Server,Second Edition》一书中探讨了过滤器的有关知识,在这里只需要提醒读者注意一点:与或非过滤器不使用Bits,而布尔过滤器使用了Bits。因此,请尽可能使用布尔过滤器。与或非过滤器一般在需要脚本过滤、地理位置过滤和数值区间过滤时使用。还需要注意的是,如果把任何不使用Bits的过滤器嵌套在与或非过滤器中,它们同样不会用到Bits。

一般来讲,在组合使用多个处理器时,如果其中包含不使用Bits的处理器,则需要使用与或非处理器来对它们进行组合。而如果要组合的所有处理器都使用Bits,则可以选择使用布尔过滤器来组合它们。

2.4.3 性能考量

通常,过滤器都是很快的。这一点有多种原因。首先,最重要的一点是,由过滤器所处理的查询部分不需要计算文档得分。之前我们就提到过,打分过程与给定查询和索引中的文档集合密切相关。

 使用过滤器时需要注意一点:在Elasticsearch 1.4.0版本发布后,执行嵌套查询时所使用的bitsets默认提前就加载好了。这样做的目的是使嵌套查询执行得更快,不过可能伴随内存使用问题。可以通过设置index.load_fixed_bitset_filters_eagerly配置项为false来禁用提前加载。如果需要查看固定大小bitsets的内存占用情况可以执行以下命令:curl -XGET ‘localhost:9200/_cluster/stats?human&pretty’,在响应中寻找fixed_bit_set_memory_in_bytes属性即可。

在使用过滤器时,过滤结果不依赖于查询,因此过滤结果可以被轻易地缓存起来供后续查询使用。值得一提的是,每个Lucene索引段都有一个过滤结果缓存。这意味着无需在每次commit时重建缓存,重建操作只发生在段生成和合并时。

 当然,有得必有失,过滤器也有一些不好的地方。不是所有的过滤器都可以被缓存。考虑那些依赖于当前时间的过滤器,对它们做缓存不会有任何意义。在某些场景下不值得做缓存,因为可能存在非常多的唯一值,缓存命中率极低,比如基于地理位置过滤的场景。

2.4.4 后置过滤和过滤查询

如果某人说过滤比实现相同功能的查询执行更快,这不一定是真的。的确,过滤器需要考虑的东西更少,并且可以在后续查询中复用,不过Lucene早就针对查询做了高度优化,以确保查询能够高速执行,甚至在考虑文档评分的情境下。当然,如果匹配结果数量极多,过滤器会执行得更快一些。不过,还有一些事我们没有告诉你。某些时候,在使用后置过滤(post_filter)时,Elasticsearch查询的执行速度没有我们期望的那么快。假如我们执行如下查询:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

下图展示了查询的执行过程:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

当然,针对大量数据的过滤是很有价值的。不过在本例中,我们使用现有的少量数据。从上图可见,索引中包含4个文档。例子中的terms查询匹配了3个文档:Doc1、Doc3和Doc4。每个匹配的文档都被计算得分并根据得分做了排序。之后,post_filter开始工作。在索引的所有文档中,它只通过了两个文档:Doc1和Doc4。可以看到,一共传递给过滤器3个文档,而只有其中的两个被作为结果输出。既然如此,还有必要对Doc3计算得分吗?本例我们浪费了一部分CPU时间来计算一个最终不匹配的文档的得分。如果类似的文档数量很多,这将是一个性能问题。

 本例中我们使用了term过滤器。该过滤器在Elasticsearch 1.5版本之前都是默认缓存的。而从1.5版本开始,默认不再缓存(参考https://github.com/Elasticsearch/Elasticsearch/pull/7583 )。因此,我们在例子中使用term过滤器时特意使用了强制缓存。

让我们修改一下这个查询,让文档过滤操作发生在Scorer计算文档得分之前。修改后的查询如下:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理
《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

在这里我们使用了过滤查询(filtered query)。返回的查询结果和前一个查询一模一样,不过执行过程稍微有一些变化,特别是在执行过滤操作时。下图揭示了这个查询在理论上的执行过程:

《深入理解Elasticsearch(原书第2版)》——2.4 过滤器的使用及作用原理

现在,最初的工作是由term过滤器完成的。如果这个过滤器在之前被使用过,它将从缓存中加载,整个文档集合将被筛成只剩两个文档。最后,这两个文档仍然需要被计算得分,不过评分模块需要做的工作少了一些。当然,本例中,查询和过滤后的文档相匹配,不过这一点并非在所有查询场景下都满足。

从技巧上看,我们让过滤器被一个查询所包裹,让Lucene库能够只收集被过滤通过的结果。当然,过滤通过的结果还需要被传递给主查询做进一步处理。多亏了过滤器,打分程序需要处理的文档数量减少了。

2.4.5 选择正确的过滤方式

读了前述关于后置过滤和过滤查询的解释,你可能会在以后只考虑使用过滤查询并远离后置过滤。这一规则在绝大多数情况下是正确的,不过在某些条件下,存在例外情况。经验法则告诉我们,开销最大的操作需要移动到查询处理链条的尾部。如果过滤器执行很快,开销很小,并且易于缓存,很简单,直接选择过滤查询即可。相反,如果过滤器执行很慢,CPU开销大,并且难于缓存(比如有大量唯一值的情况),请使用后置过滤,或者尝试优化过滤器。优化途径包括简化过滤器和使得过滤器对缓存更友好,比如,可以降低时间区间过滤器的时间粒度。

上一篇:SQL Server 2016新特性:DROP IF EXISTS


下一篇:《软件功能测试自动化实战教程》目录—导读