一、合并请求
(1)批量索引 单条索引操作有以下两方面的性能损失:
ES提供的批量(bulk)API,可以用来一次索引多篇文档,从而大幅加快索引速度。如图1所示,可以使用http完成这个操作,并且将获得包含全部索引请求结果的答复。
图1 批量索引允许在同一个请求中发送多篇文档
下面的代码在单个批量请求中索引两篇文档:
REQUESTS_FILE=/tmp/test_bulk
echo '{"index":{"_index":"get-together","_type":"_doc","_id":"10"}}
{"name":"Elasticsearch Bucharest"}
{"index":{"_index":"get-together","_type":"_doc","_id":"11"}}
{"name":"Big Data Bucharest"}' > $REQUESTS_FILE
curl -H "Content-Type: application/json" -XPOST "172.16.1.127:9200/_bulk?pretty&refresh" --data-binary @$REQUESTS_FILE
批量索引操作对数据格式有如下要求:
操作类型index表示索引数据,如果同样ID的文档已经存在,那么这个操作将使用新数据覆盖原有文档。如果将index改为create,则已有文档不会被覆盖。index和create类似于MySQL中的replace into和insert into命令,而批量索引功能则类似于MySQL的insert into ... values (),(),...()命令。 也可以在URL中加上索引和类型,使它们成为bulk中每次操作的默认索引和类型。
curl -H "Content-Type: application/json" -XPOST "172.16.1.127:9200/get-together/_bulk?pretty&refresh" --data-binary @$REQUESTS_FILE
curl -H "Content-Type: application/json" -XPOST "172.16.1.127:9200/get-together/_doc/_bulk?pretty&refresh" --data-binary @$REQUESTS_FILE
这样就可以不用在JSON文件中放入_index和_type。如果在JSON中指定了索引和类型值,它们将覆盖URL中所带的值。 _id字段表示索引文档的ID。如果省略此参数,ES会自动生成一个ID,在文档没有唯一ID时,这点很有帮助。下面代码在JSON文件中省略了_index、_doc和_id。
REQUESTS_FILE=/tmp/test_bulk
echo '{"index":{}}
{"name":"Elasticsearch Bucharest"}
{"index":{}}
{"name":"Big Data Bucharest"}' > $REQUESTS_FILE
URL='172.16.1.127:9200/get-together/_doc'
curl -H "Content-Type: application/json" -XPOST "$URL/_bulk?pretty&refresh" --data-binary @$REQUESTS_FILE
由于使用了自动生成的ID,操作index会被转变为create。与MySQL中的单条多值insert语句不同,ES同一个批量操作中的各项是彼此独立的,某篇文档索引失败不会影响其它文档。这也是为什么每篇文档操作都会返回一个请求回复,而不是整个批量只返回一个回复。这样应用可以使用回复的JSON确定哪些操作成功而哪些失败了。(2)批量更新或删除 在单个批量中,可以包含任意数量的index和create操作,同样也可以包含任意数量的update和delete操作。
REQUESTS_FILE=/tmp/test_bulk
echo '{"index":{}}
{"title":"Elasticsearch Bucharest"}
{"index":{}}
{"title":"Big Data Romania"}
{"update":{"_id":"11"}}
{"doc":{"create_on":"2014-05-06"}}
{"delete":{"_id":"10"}}' > $REQUESTS_FILE
URL='172.16.1.127:9200/get-together/_doc'
curl -H "Content-Type: application/json" -XPOST "$URL/_bulk?pretty&refresh" --data-binary @$REQUESTS_FILE
多条搜索(multisearch)和索条获取(multiget)所带来的好处和批量相似,节省花费在网络延迟上的时间。 (1)多条搜索
REQUESTS_FILE=/tmp/test_bulk
echo '{"index":"get-together", "type":"_doc"}
{"query":{"match":{"name":"elasticsearch"}}}
{"index":"get-together","type":"_doc"}
{"query":{"match":{"title":"elasticsearch"}}}' > $REQUESTS_FILE
curl -H "Content-Type: application/json" 172.16.1.127:9200/_msearch?pretty --data-binary @$REQUESTS_FILE
(2)多条获取
curl 172.16.1.127:9200/_mget?pretty -H "Content-Type: application/json" -d'
{
"docs": [
{
"_index": "get-together",
"_type": "_doc",
"_id": 1
},
{
"_index": "get-together",
"_type": "_doc",
"_id": 2
}
]
}'
curl 172.16.1.127:9200/get-together/_doc/_mget?pretty -H "Content-Type: application/json" -d'
{
"ids": ["1", "2"]
}'
向ES发送索引请求的时候,ES先将文档添加到索引缓冲区(index-buffer),并且追加到了translog,如图2所示。此时新文档是不能被搜索的,只有refresh操作后才能被搜索。
图2 新的文档被添加到内存缓冲区并且被追加到了事务日志
刷新(refresh)完成以下工作:
刷新完成后的分片状态如图3所示。
图3 刷新完成后, 缓存被清空但不清除事务日志
ES默认的refresh间隔时间是1秒,这也是为什么ES可以进行近乎实时的搜索。可以修改其设置,改变一个索引的刷新间隔,这是可以在运行时完成的。,例如,下面的命令将自动刷新的时间间隔设置为5秒:
curl -XPUT 172.16.1.127:9200/get-together/_settings?pretty -H "Content-Type: application/json" -d'
{
"index.refresh_interval": "5s"
}'
为确定修改生效,可以执行下面的命令获得索引设置:
curl 172.16.1.127:9200/get-together/_settings?pretty
增加refresh_interval值将获得更大的索引吞吐量,因为花在刷新上的系统资源少了。也可以将refresh_interval设置为-1,彻底关闭自动刷新并依赖手动刷新。手动刷新访问待刷新索引的_refresh端点:
curl 172.16.1.127:9200/get-together/_refresh?pretty
刷新操作后文档还是处于文件系统cache中,而没有被持久化到磁盘,translog也没有被删除,这些工作是依赖flush操作完成的,其过程如下:
translog是ES的事务日志,提供所有还没有被刷到磁盘的操作的一个持久化纪录。当Elasticsearch启动的时候,它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放translog中所有在最后一次提交后发生的变更操作。translog也被用来提供实时CRUD。当通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前,首先检查translog任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
从以上描述可见,ES的translog作用类似于SQL数据库的事务日志,在每一次对ES进行操作时均进行了日志记录,其功能总结如下:
flush后,一个新的translog被创建,并且一个全量提交被执行,之后分片状态如图4所示。
图4 在刷新(flush)之后,段被全量提交,并且事务日志被清空
满足下列条件之一就会触发flush操作:
为了控制flush发生的频率,需要调整控制这两个条件的设置。 内存缓冲区的大小在elasticsearch.yml配置文件中定义,通过indices.memory.index_buffer_size来设置。这个设置控制了整个节点的缓冲区,其值可以是全部JVM堆内存的百分比,如10%,也可以是100MB这样的固定值。
translog的设置是具体到索引上的,控制了触动flush的规模(flush_threshold_size)。
curl -XPUT 172.16.1.127:9200/get-together/_settings?pretty -H "Content-Type: application/json" -d'
{
"index.translog": {
"flush_threshold_size": "512mb"
}
}'
当flush发生的时候,它会在磁盘上创建一个或多个分段。执行一个查询的时候,ES通过Lucene查看所有分段,然后将结果合并到一个整体的分片中。搜索时每个分片上的结果将被聚集为一个完整的结果集合,然后返回给应用程序。
Lucene分段是一组不变的文件,ES用其存储索引的数据。由于分段是不变的,它们很容易被缓存。此外,修改数据时,如添加一篇文档,无须重建现有分段中的数据索引。这使得新文档的索引也很快。但更新文档不能修改实际的文档,只是索引一篇新的文档。如此处理还需要删除原有的文档。删除也不能从分段中移除文档(这需要重建倒排索引),只是在单独的.del文件中将其标记为“已删除”。文档只会在分段合并的时候真正地被移除。
于是得出分段合并的两个目的,一是将分段的总数量保持在可控的范围内,用以保障查询性能;二是真正地删除文档。
按照已定义的合并策略,分段是在后台进行的。默认的合并策略是分层配置,如图5所示,该策略将分段划分为多个层次,如果分段多于某一层中所设置的最大分段数,该层的合并就会被触发。
图5 当分层合并策略发现某层中存在过多的分段时,它将进行一次合并
(1)调优合并策略的选项 合并的最终目的是提升搜索的性能而均衡I/O和CPU计算能力。合并发生在索引、更新或者删除文档的时候,所以合并的越多,这些操作的成本就越高。反之,如果想快速索引,需要较少的合并,并牺牲一些查询性能。一下是几个最重要的合并设置选项。
所有这些选项是具体到索引上的,而且和事务日志刷新设置一样,可以在运行时修改这些设置,例如,下面的代码将segments_per_tier设置成5,会导致更多的合并,将最大分段规模降低到1GB,并将线程数降低到1,让磁盘更好地运转。
curl -XPUT 172.16.1.127:9200/get-together/_settings?pretty -H "Content-Type: application/json" -d'
{
"index.merge": {
"policy": {
"segments_per_tier": 5,
"max_merge_at_once": 5,
"max_merged_segment": "1gb"
},
"scheduler.max_thread_count": 1
}
}'
(2)优化索引 一次手动触发的强制合并也被称为优化(optimize)。对于激进合并而言,优化是非常消耗I/O的,而且使得许多缓存失效。如果持续地索引、更新和删除索引文件中的文档,新的分段就会被创建,而优化操作的好处就无法体现出来。因此,在一个不断变化的索引上,如果希望分段的数量较少,应该调优合并策略。
在静态的索引上优化是很有意义的。如图6所示,系统会减少分段的总数量,一旦缓存再次被预热加载,就会加速查询。
图6 对于没有更新的索引而言,优化操作是很有意义的
为了优化,需要访问待优化索引的_optimize端点。选项max_num_segments表示每个分片终止拥有多少分段。
curl -X POST "172.16.1.127:9200/get-together/_forcemerge?max_num_segments=100&pretty"
在一个大型索引上进行的优化操作可能需要花费很长时间。可以通过设置wait_for_merge为false,将操作发送到后台进行。导致优化(和合并)操作缓慢的可能原因之一是,默认情况下ES限制了合并操作所能使用的I/O吞吐量的份额。该限制称为存储限流(store throttling)。
默认过滤器查询结果是可以被缓存的,也可以通过request_cache控制一个过滤器是否被缓存。
curl -X GET "172.16.1.127:9200/get-together/_search?request_cache=false&pretty" -H "Content-Type: application/json" -d'
{
"query": {
"bool": {
"filter": {
"term": {
"tags.verbatim": "elasticsearch"
}
}
}
}
}'
过滤器缓存在ES 6中被替换为Node Query Cache,indices.queries.cache.size参数控制过滤器缓存大小,默认值为10%。该参数伪静态参数,需要在每个ES节点的elasticsearch.yml文件中配置。index.queries.cache.enabled参数控制是否启用查询缓存,默认值为true。该参数可以基于每个索引进行配置。
当过滤器查询条件是组合条件时,ES可以使用位集合(bitset)缓存某个文档是否和过滤器匹配。位集合是一个紧凑的位数组,类似于Oracle的位图索引。多数过滤器(如range过滤器和terms过滤器)使用位集合进行缓存。有些过滤器(如script过滤器)不使用位集合,因为无论如何ES都不得不遍历所有文档。
位集合和简单的结果缓存不同之处在于位集合具有如下特点:
下面的代码同时使用位集合和非位集合的过滤器:
curl 172.16.1.127:9200/get-together/_search?pretty -H "Content-Type: application/json" -d'
{
"query": {
"bool": {
"filter": { # 过滤查询意味着查询只会在匹配过滤器的文档上运行
"bool": {
"must": [
{
"bool": { # 在缓存时,bool操作更快,因为它利用了两个词条过滤器的位集合
"should": [
{
"term": {
"tags.verbatim": "elasticsearch"
}
},
{
"term": {
"members": "lee"
}
}
]
}
},
{
"script": { # 脚本过滤器只会在匹配bool过滤器的文档上运行
"script": {
"source": "doc['"'members'"'].values.length > params.minMembers",
"params": {
"minMembers": 2
}
}
}
}
]
}
}
}
}
}'
组合过滤器的执行顺序非常关键。轻量级的过滤器(如terms过滤器)应该在更耗资源的过滤器(如scrip)过滤器之前运行。经过先前的过滤,耗资源的过滤器可以在较小的文档集合上运行。
过滤器缓存的设计是为了让某些搜索(也就是配置为可缓存的过滤器)运行得更快。它也是和分片相关的:如果在合并过程中某些分段被移除了,其它分段的缓存仍然是保持完整的。对比之下,分片查询缓存在分片级别上,维护了整个请求及其结果之间的映射,如图7所示。对于新的请求,如果某个分片之前已经答复过一模一样的请求,那么它将使用缓存来服务新请求。
图7 分片查询缓存比过滤器更高一层
在默认情况下就开启了索引上的分片查询缓存,可以使用索引更新设置的API接口:
curl -X POST "172.16.1.127:9200/get-together/_close?pretty"
curl -XPUT "172.16.1.127:9200/get-together/_settings?&pretty" -H "Content-Type: application/json" -d'
{
"index.queries.cache.enabled": true
}'
curl -X POST "172.16.1.127:9200/get-together/_open?pretty"
对于每个查询,可以加入request_cache参数来开启或者关闭分片查询缓存,覆盖掉索引级别的设置:
URL="172.16.1.127:9200/get-together/_search"
curl "$URL?request_cache&pretty" -H "Content-Type: application/json" -d'
{
"size": 0,
"aggs": {
"top_tags": {
"terms": {
"field": "tags.verbatim"
}
}
}
}'
过滤器缓存与分片缓存在ES 6中统一称为Node Query Cache,由参数indices.queries.cache.size和index.queries.cache.enabled所控制。
如果ES没有足够的堆来完成一个操作,它将抛出一个out-of-memory的异常,很快该节点就会宕机,并被移出集群。这会给其它节点带来额外的负载,因为系统需要复制和重新分配分片,以恢复到初始配置所需的状态。由于节点通常是相同的配置,额外的负载很可能使得至少另一个节点耗尽内存,这种多米诺骨牌效应将会拖垮整个集群。
当JVM堆的资源很紧张时,即使在日志中没有看到out-of-memory的异常,节点还是可能变得没有响应。这可能是因为,内存不够迫使垃圾回收器(GC)运行的更久或者更频繁来释放空闲的内存。由于GC消耗了更多的资源,节点花费在服务请求甚至是应答主节点ping的计算能力就更少了,最后导致节点被移出集群。
很显然堆太小了是不利的,但是堆太大了也不是好事。达到32GB的堆会自动地使用未压缩指针,并且浪费了内存。在不知道堆的实际使用情况时,经验法则是将节点内存的一半分配给ES,但是不要超过32G。这个“一半”的法则通常给出了堆大小和系统缓存之间良好的平衡点。如果可以监控实际的堆使用情况,一个好的堆大小就是足够容纳常规使用,外加可预期的高峰冲击。如果不知道会有怎样的高峰冲击,经验法则同样是一半:将堆大小设置为比常规高出50%。
对于操作系统缓存,主要依赖于服务器的内存。总体来说,如果可以使用基于时间的索引、基于用户索引或者路由,将“热门”数据放入同一组索引或分片,将充分利用操作系统缓存。
非精确匹配可以使用一系列的查询来实现。
另一个解决方案来兼容错拼和其它非精确匹配是N元语法(ngram)。通过N元语法为单词的每个部分产生分词。如果在索引和查询的时候都使用N元语法,将获得和模糊查询类似的功能,如图8所示。
图8 相比模糊查询,N元语法产生了更多的词条,但是匹配的时候是精确的
对于性能而言,需要权衡考虑为哪些期望付出成本。
当查询延迟是关键的时候,或者有很多并发查询需要支持的时候,需要每个查询消耗更少的CPU资源,此种情况下N元语法的方法常常会更好。N元语法使得索引变得更大,因此需要操作系统能容纳得下这些索引,或者是读写更快的磁盘。否则,性能将会因为索引过大而下降。 另一方面,当需要较高的索引吞吐量,或者磁盘读写较慢时,模糊查询的方法就更好一些。如果需要经常修改查询,模糊查询也是很有帮助的。例如,调整编辑距离,无需重建所有的数据,就能进行修改。
(1)前缀查询和侧边N元语法 对于非精确的匹配,经常假设开头的字符是准确的,这时可以考虑前缀查询。和模糊查询一样,前缀查询比普通的词条查询成本更高,因为需要查找更多的词条。
可能的替换方法是侧边N元语法(edge ngram)。图9展示了侧边N元语法和前缀查询的对比。
图9 和侧边N元语法相比,前缀查询需要匹配更多的词条,不过索引量更小
同模糊查询和N元语法的比较相类似,前缀查询和侧边N元语法需要权衡灵活性和索引规模,这里前缀方法更有优势。而权衡查询的延迟和CPU的使用率,侧边N元语法则更有优势。
(2)通配符 通配符查询中,总是要放入通配符号,如elastic*。这个查询和前缀查询的功能相当,也可以使用侧边N元语法作为替代。如果通配符在中间,如e*search,那么没有在索引阶段的等同方案。仍然可以使用N元语法来匹配字符e和search,但是如果无法控制通配符怎样使用,那么通配符查询是你唯一的选择。
如果通配符总是在开头,那么通配符查询常常比结尾通配的查询更耗性能。原因是没有前缀来提示在词条字典的哪个部分来查找相匹配词。在这种情况下,替换的方案可以是结合使用reverse分词过滤器和侧边N元语法,如图10所示。
图10 可以使用反向和侧边N元语法分词过滤器来匹配后缀
(3)词组查询和滑动窗口 但需要考虑彼此相邻的单词时,可以使用match_phrase查询。词组查询比较慢,因为它们不仅需要考虑多个词条,还要考虑这些词条在文档中的位置。词组查询在索引阶段的替换方案是使用滑动窗口(shingle)。滑动窗口将增加索引的大小,但通过较慢的索引换取更快的查询。
词组查询和滑动窗口这两个方法也不是完全对等的。词组查询可以设置slop,允许词组中间出现其它的单词。但是滑动窗口要求每个词条都是有效的,中间不能加入其它非有效词条。
滑动窗口包含的是单个词条,这一点允许更好地将它们用于复合词匹配。例如,很多用户仍然用“elastic search”来表示Elasticsearch。通过滑动窗口,可以使用一个空字符串而非默认的空格作为分隔符来解决这个问题,如图11所示。
图11 使用滑动窗口来匹配复合词
通过脚本可以获得许多灵活性,但是灵活性对于性能有着重大影响。脚本的结果永远不会被缓存,因为ES无法理解脚本内是什么。可能有一些外部信息,比如一个随机数,这会使得谋篇文档现在是匹配的,但是下一次就不匹配了。这类似于Oracle或者MySQL定义函数时的DETERMINISTIC属性。
使用的时候,脚本常常是最消耗CPU资源的搜索了。如果想加速查询,好的起点是完全放弃脚本。如果不可能放弃,通用的原则是尽可能地深入代码并优化性能。
(1)避免使用脚本 例1:使用预计算避免脚本。可以在索引的流水线里统计会员的数量并将其添加到一个新的字段,而不是在索引的时候什么都不做,让脚本查看数组长度来统计分组会员的数量。图12比较了这两种方法。
图12 在脚本中统计会员或在索引过程中统计会员
和N元语法类似,这种方法在索引阶段进行计算。如果查询延迟的优先级比索引吞吐量的优先级更高的话,这个方式就能奏效。
例2:使用ES现有功能避免脚本。运行寻找“elasticsearch”活动的查询,但是基于如下假设,使用这样的方式来提升或降低得分。
如果在索引阶段计算了活动参与者的数量(将字段命名为attendees_count),可以无须使用任何脚本而获得这两个条件。
"function_score": {
"functions": [
"exp": {
"date": {
"origin": "2013-07-25T18:00",
"scale": "60d"
}
},
"field_value_factor": {
"field": "attendees_count"
}
]
}
(2)本地脚本 如果想获得某个脚本的最佳性能,使用Java语言书写本地脚本是最好的方式。这种本地脚本可以成为ES插件。本地脚本需要存储在每个节点的ES类路径中。修改脚本就意味着在所有集群节点上更新它们,并重启节点。
为了在查询中运行本地脚本,将lang设置为native,将script的内容设置为脚本名称。例如,有一个插件脚本名为numberOfAttendees,它即时地计算活动参与人数,可以像这样在stats聚合中使用它:
"aggregations": {
"attendees_stats": {
"stats": {
"script": "numberOfAttendees",
"lang": "native"
}
}
}
(3)Lucene表达式 如果要经常修改脚本,或者在不重启整个集群的前提下修改,而脚本又是在数值字段上运作,那么Lucene表达式可能是最好的选择。所谓Lucene表达式,就是查询的时候在脚本中提供一个JavaScript表达式,ES将其编译为本地代码,让它和本地脚本一样快。这种方法最大的局限性在于只能访问索引的数值型字段。另外,如果某个文档缺失这个字段,那么其默认就会取0,在某些场景下可能会导致错误。
为了使用Lucene表达式,在脚本中要将lang设置为expression。例如,已经有了参与者的数量,但想根据一半的人数进行统计:
"aggs": {
"expected_attendees": {
"stats": {
"script": "doc['attendees_count'].value/2",
"lang": "expression"
}
}
}
如果必须使用非数值型或非索引型的字段,而又想能够很容易地修改脚本,可以使用painless,这是ES6默认的脚本语言。(4)访问字段数据 字段数据是为了随机访问而进行的调优,所以在脚本里使用也是非常好的。即使首次运行的时候字段数据尚未被加载,它常常要比_source或_fields要快上几个数量级。例如:
curl -XPOST "172.16.1.127:9200/get-together/_mapping/_doc?pretty" -H 'Content-Type: application/json' -d'
{
"properties": {
"organizer": {
"type": "text",
"fielddata": "true"
}
}
}'
curl "172.16.1.127:9200/get-together/_search?pretty" -H "Content-Type: application/json" -d'
{
"query": {
"bool": {
"filter": {
"script": {
"script":"if (doc.organizer.values.size() > 0 && doc.members.values.size() > 0) {(doc.members.values.indexOf(doc.organizer.value) == -1)}"
}
}
}
},
"_source": ["_id", "organizer", "members"]
}'
使用doc.organizer而非_source['organizer'](或_fields)的时候,有一点需要注意:访问的是词条,而不是原有的字段。如果组织者是'Lee',而字段经过默认分析器分析之后,从_source将得到'Lee',而从doc将得到'lee'。
当发送一个搜索请求到某个ES节点的时候,该节点将请求发送到所有涉及的分片,并将单个分片的答复聚合为一个最终的答复,并返回给应用程序。最简答的方法从所有涉及的每个分片那里各获得N篇(N是size参数的值)文档,将它们在接受HTTP请求的节点上(将其称为协调节点)排序,挑选排名最靠前的N个文档,然后返回给应用程序。假设发送的请求使用了默认为10的size,而接受请求的索引默认拥有5个分片。这意味着协调节点将从每个分片那里获取10篇文档,排序这些文档,然后从50篇文档中仅仅挑出排名靠前的10篇进行返回。但是,如果有10个分片,取100个结果呢?传送这些文档的网络开销,以及在协调节点上处理文档的内存开销将爆炸式增长,这类似于为聚合指定一个很大的shard_size,对性能不利。
只返回50篇文档的ID以及用于排序的元数据给协调节点,这样做如何?排序后,协调节点从分片只要获取所需的前10篇文档。这将减少多数情况下网络的开销,不过会引发两次网络传输。这种方法的思想与SQL数据库中所谓的延迟关联异曲同工。
对于ES而言,两种选择都是可以的,只需设置搜索请求中的search_type参数。简单的获取全部相关文档的实现是用query_and_fetch,而传输两次的方法叫作query_then_fetch,这也是默认选项。两者的对比参见图13。
图13 比较query_and_fetch和query_then_fetch
要命中更多的分片,使用size请求更多的文档,文档数量变得很大,那么默认的query_then_fetch是更好的选择。因为它在网络上传输了更少的数据。只有当命中一个分片时,query_and_fetch才会更快,这就是为什么当搜索单个分片、使用路由、只需要数量的时候,ES内部会用到它。
(1)分布式得分 默认情况下,分数是在每个分片上计算,这可能会导致不够精准,例如,如果搜索一个词条,一个因素是文档频率(DF),它展示了所搜索的词条在所有文档中出现了多少次。“所有的文档”默认是指“这个分片上的所有文档”。如果不同分片之间某个词条的文档频率值差距显著,得分可能就无法反映真实的情况。参考图14,尽管文档1中出现“elasticsearch”的次数更多,但是由于分片2中出现该词的文档数量较少,最后导致文档2的得分比文档1高。
图14 分布不均的文档频率可能导致不准确的排名
如果分数的准确性是高优先级的,或者文档频率对于应用而言还是不均衡的(比如使用了定制路由),那么需要将搜索类型从query_then_fetch改为dfs_query_then_fetch。这个dfs的部分将告诉协调节点向分片发送一次额外的请求,来收集被搜索词条的文档频率。如图15所示,聚合的频率将被用于计算分数并正确地将文档1和文档2进行排序。
图15 dfs的搜索类型使用了额外的网络请求来计算全局的文档频率,并将其用于文档评分
由于额外的网络请求,DFS的查询会更慢。所以在切换之前,需要确保获得了更好的评分。如果有一个低延时的网络,这种开销可以忽略不计。另一方面,如果网络不够快,或者查询的并发很高,可能会遇见明显的额外负载。
(2)只返回数量 如果不关心得分,也不需要文档内容,只需要数量或聚合,这种情况下推荐使用size=0。
ES使用size和from参数对查询结果进行分页。例如,为了在get-together活动中搜索“elasticsearch”,并获取每页100个结果的第5页,需要运行类似如下的请求:
curl "172.16.1.127:9200/get-together/_search?pretty" -H "Content-Type: application/json" -d'
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"from": 400,
"size": 100
}'
这实际上获取了前500个结果,然后只返回最后的100个。可以想象,越靠后的分页效率越低。对于这种情形,可以使用scan的搜索类型来遍历所有get-together分组:
curl -X POST "172.16.1.127:9200/get-together/_search?pretty&q=elasticsearch&scroll=1m&size=100"
curl -X POST "172.16.1.127:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d'
{
"scroll" : "1m",
"scroll_id" : "DnF1ZXJ5VGhlbkZldGNoAgAAAAAAAAAcFnlPOUFFZy1CVFMyMFY5Qmh1RVdldUEAAAAAAAAAHRZ5TzlBRWctQlRTMjBWOUJodUVXZXVB"
}'
初始的答复返回了滚动ID(_scroll_id),它唯一标识了这个请求并会记住哪些页面已经被返回。在开始获取结果之时,发生一个包含滚动ID的请求。重复同样的请求来获取下一页的内容,直到有了足够的数据或者没有更多的命中返回。
和其它搜索一样,扫描查询接受size的参数来控制每页的结果数量。不过这一次,页面的大小是按照每个分片来计算的,所以返回的数量将是size的值乘以分片数量。请求的scroll参数中给出的超时会在每次获取新页面时被刷新,这就是为什么每个新的请求中可以可以设置不同的超时。
scan的搜索类型总是按照结果在索引中被发现的顺序来返回它们,而忽略了排序条件。如果同时需要深度分页和排序,可以为普通的搜索请求增加scroll参数。向滚动ID发送GET请求,将获得下一页的结果。这次,size参数可以精准地工作,而忽略分片的数量。第一个请求中也将获得第一页的结果,这和普通搜索一样。
curl "172.16.1.127:9200/get-together/_search?pretty&scroll=1m" -H "Content-Type: application/json" -d'
{
"query": {
"match": {
"title": "elasticsearch"
}
}
}'
从性能的角度而言,将scroll加入普通的搜索比使用scan的搜索类型更耗资源,原因是当结果被排序的时候,需要在内存中保留更多的信息。也就是说,深度分页比默认的搜索更高效,因为ES没有必要为当前页面而排列所有之前的页面。
只有当事先知道需要深度分也时,滚动才是有用的。当只需要少数几页结果时并不推荐滚动操作。