触类旁通Elasticsearch:优化

目录

一、合并请求

1. 批量操作(bulk)

2. 多条搜索和多条获取

二、优化Lucene分段的处理

1. refresh和flush

2. 合并以及合并策略

三、缓存

1. 过滤器和过滤器缓存

2. 分片查询缓存

3. JVM堆和操作系统缓存

四、其它的性能权衡

1. 非精确匹配

2. 脚本

3. 网络

4. 分页


《Elasticsearch In Action》学习笔记。

一、合并请求

1. 批量操作(bulk)

(1)批量索引
        单条索引操作有以下两方面的性能损失:

  • 每篇文档要与ES服务器进行一次交互,每次交互应用程序必须等待ES的答复,才能继续运行下去。
  • 对于每篇被索引的文档,ES必须处理请求中的所有数据。

        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

        批量索引操作对数据格式有如下要求:

  • 每个索引请求由两个JSON对象组成,由换行符分隔:一个是操作(本例中的index)和元数据(如索引、类型和ID),另一个是文档的内容。
  • 每行只有一个JSON对象。这意味着每行需要使用换行符(\n,或者是ASCII码10)结尾,包括整个批量请求的最后一行。

        操作类型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

2. 多条搜索和多条获取

        多条搜索(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"]
}'

二、优化Lucene分段的处理

1. refresh和flush

        向ES发送索引请求的时候,ES先将文档添加到索引缓冲区(index-buffer),并且追加到了translog,如图2所示。此时新文档是不能被搜索的,只有refresh操作后才能被搜索。

图2 新的文档被添加到内存缓冲区并且被追加到了事务日志

 

        刷新(refresh)完成以下工作:

  1. 将索引缓冲区中的文档写入到一个新的Lucene段中,且不进行进行fsync操作。实际上是写入filesystem cache中。
  2. 这个段被打开,使其可被搜索。
  3. 清空索引缓冲区。

        刷新完成后的分片状态如图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操作完成的,其过程如下:

  1. 一个提交点被写入硬盘。
  2. 文件系统缓存通过fsync被写入磁盘。
  3. 老的translog被删除。

        translog是ES的事务日志,提供所有还没有被刷到磁盘的操作的一个持久化纪录。当Elasticsearch启动的时候,它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放translog中所有在最后一次提交后发生的变更操作。translog也被用来提供实时CRUD。当通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前,首先检查translog任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

        从以上描述可见,ES的translog作用类似于SQL数据库的事务日志,在每一次对ES进行操作时均进行了日志记录,其功能总结如下:

  • 保证在filesystem cache中的数据不会因为ES重启或是发生意外故障的时候丢失。
  • 当系统重启时会从translog中恢复之前记录的操作。
  • 当对ES进行CRUD操作的时候,会先到translog之中进行查找,因为tranlog之中保存的是最新的数据。
  • translog的清除时间是进行flush操作之后(将数据从filesystem cache刷入disk之中)。

        flush后,一个新的translog被创建,并且一个全量提交被执行,之后分片状态如图4所示。

图4 在刷新(flush)之后,段被全量提交,并且事务日志被清空


 
        满足下列条件之一就会触发flush操作:

  • 索引缓冲区满。
  • translog达到一定的阈值。

        为了控制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查看所有分段,然后将结果合并到一个整体的分片中。搜索时每个分片上的结果将被聚集为一个完整的结果集合,然后返回给应用程序。

2. 合并以及合并策略

        Lucene分段是一组不变的文件,ES用其存储索引的数据。由于分段是不变的,它们很容易被缓存。此外,修改数据时,如添加一篇文档,无须重建现有分段中的数据索引。这使得新文档的索引也很快。但更新文档不能修改实际的文档,只是索引一篇新的文档。如此处理还需要删除原有的文档。删除也不能从分段中移除文档(这需要重建倒排索引),只是在单独的.del文件中将其标记为“已删除”。文档只会在分段合并的时候真正地被移除。

        于是得出分段合并的两个目的,一是将分段的总数量保持在可控的范围内,用以保障查询性能;二是真正地删除文档。

        按照已定义的合并策略,分段是在后台进行的。默认的合并策略是分层配置,如图5所示,该策略将分段划分为多个层次,如果分段多于某一层中所设置的最大分段数,该层的合并就会被触发。

图5 当分层合并策略发现某层中存在过多的分段时,它将进行一次合并

 

(1)调优合并策略的选项
        合并的最终目的是提升搜索的性能而均衡I/O和CPU计算能力。合并发生在索引、更新或者删除文档的时候,所以合并的越多,这些操作的成本就越高。反之,如果想快速索引,需要较少的合并,并牺牲一些查询性能。一下是几个最重要的合并设置选项。

  • index.merge.policy.segments_per_tier:这个值越大,每层可以拥有的分段数越多。这就意味着更少的合并以及更好的索引性能。如果索引次数不多,同时希望获得更好的搜索性能,可将这个值设置的低一些。
  • index.merge.policy.max_merge_at_once:这个设置限制了每次可以合并多少个分段。通常可以将其等同于segments_per_tier值。可以降低max_merge_at_once值来强制性地减少合并,但是最好通过增加segments_per_tier来实现这个目的。要确保max_merge_at_once的值不会比segments_per_tier的值高,因为这会引起过多的合并。
  • index.merge.policy.max_merged_segment:这个设置定义了最大的分段规模。不会再使用其它的分段来合并比这个更大的分段了。如果想获得较少的合并次数,以及更快的索引速度,最好降低这个值,因为较大的分段更难以合并。
  • index.merge.scheduler.max_thread_count:在后台,合并发生于多个彼此分隔的线程中,而这个设置控制了可用于合并的最大线程数量。这是每次可以进行的合并的硬性限制。在一台多CPU和高速I/O的机器上,可以增加这个设置来实行激进的合并,在低速CPU和I/O的机器上需要降低这个值。

        所有这些选项是具体到索引上的,而且和事务日志刷新设置一样,可以在运行时修改这些设置,例如,下面的代码将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)。

三、缓存

1. 过滤器和过滤器缓存

        默认过滤器查询结果是可以被缓存的,也可以通过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都不得不遍历所有文档。

        位集合和简单的结果缓存不同之处在于位集合具有如下特点:

  • 它们很紧凑而且很容易创建,所以在过滤器首次运行时创建缓存的开销并不大。
  • 它们是按照独立的过滤器来存储的。例如,如果在两个不同查询中或者bool过滤器使用了一个terms过滤器,该term的位集合就可以重用。
  • 它们很容易和其它的位集合进行组合。如果有两个使用位集合的查询,ES很容易进行一个AND和OR操作,来判断哪些文档和这个组合匹配。

        下面的代码同时使用位集合和非位集合的过滤器:

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)过滤器之前运行。经过先前的过滤,耗资源的过滤器可以在较小的文档集合上运行。

2. 分片查询缓存

        过滤器缓存的设计是为了让某些搜索(也就是配置为可缓存的过滤器)运行得更快。它也是和分片相关的:如果在合并过程中某些分段被移除了,其它分段的缓存仍然是保持完整的。对比之下,分片查询缓存在分片级别上,维护了整个请求及其结果之间的映射,如图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所控制。

3. JVM堆和操作系统缓存

        如果ES没有足够的堆来完成一个操作,它将抛出一个out-of-memory的异常,很快该节点就会宕机,并被移出集群。这会给其它节点带来额外的负载,因为系统需要复制和重新分配分片,以恢复到初始配置所需的状态。由于节点通常是相同的配置,额外的负载很可能使得至少另一个节点耗尽内存,这种多米诺骨牌效应将会拖垮整个集群。

        当JVM堆的资源很紧张时,即使在日志中没有看到out-of-memory的异常,节点还是可能变得没有响应。这可能是因为,内存不够迫使垃圾回收器(GC)运行的更久或者更频繁来释放空闲的内存。由于GC消耗了更多的资源,节点花费在服务请求甚至是应答主节点ping的计算能力就更少了,最后导致节点被移出集群。

        很显然堆太小了是不利的,但是堆太大了也不是好事。达到32GB的堆会自动地使用未压缩指针,并且浪费了内存。在不知道堆的实际使用情况时,经验法则是将节点内存的一半分配给ES,但是不要超过32G。这个“一半”的法则通常给出了堆大小和系统缓存之间良好的平衡点。如果可以监控实际的堆使用情况,一个好的堆大小就是足够容纳常规使用,外加可预期的高峰冲击。如果不知道会有怎样的高峰冲击,经验法则同样是一半:将堆大小设置为比常规高出50%。

        对于操作系统缓存,主要依赖于服务器的内存。总体来说,如果可以使用基于时间的索引、基于用户索引或者路由,将“热门”数据放入同一组索引或分片,将充分利用操作系统缓存。

四、其它的性能权衡

1. 非精确匹配

        非精确匹配可以使用一系列的查询来实现。

  • 模糊查询:这个查询匹配和原有词条有一定编辑距离的词条,比如,删除或者增加一个字符将产生1的编辑距离。
  • 前缀查询或过滤器:这个查询匹配以某个序列开头的词条。
  • 通配符:允许使用?和*来代替一个或多个字符。

        另一个解决方案来兼容错拼和其它非精确匹配是N元语法(ngram)。通过N元语法为单词的每个部分产生分词。如果在索引和查询的时候都使用N元语法,将获得和模糊查询类似的功能,如图8所示。

图8 相比模糊查询,N元语法产生了更多的词条,但是匹配的时候是精确的

 

        对于性能而言,需要权衡考虑为哪些期望付出成本。

  • 模糊查询拖慢了查询,但是索引和精确匹配一样,保持不变。
  • 另一方面,N元语法增加了索引的大小。根据N元语法和词条数量的大小,引入N元语法的索引其规模可以增加数倍。同样,如果想修改N元语法的设置,不得不重建全部数据,所以灵活性更小,不过使用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 使用滑动窗口来匹配复合词

 

2. 脚本

        通过脚本可以获得许多灵活性,但是灵活性对于性能有着重大影响。脚本的结果永远不会被缓存,因为ES无法理解脚本内是什么。可能有一些外部信息,比如一个随机数,这会使得谋篇文档现在是匹配的,但是下一次就不匹配了。这类似于Oracle或者MySQL定义函数时的DETERMINISTIC属性。

        使用的时候,脚本常常是最消耗CPU资源的搜索了。如果想加速查询,好的起点是完全放弃脚本。如果不可能放弃,通用的原则是尽可能地深入代码并优化性能。

(1)避免使用脚本
        例1:使用预计算避免脚本。可以在索引的流水线里统计会员的数量并将其添加到一个新的字段,而不是在索引的时候什么都不做,让脚本查看数组长度来统计分组会员的数量。图12比较了这两种方法。

图12 在脚本中统计会员或在索引过程中统计会员

 

        和N元语法类似,这种方法在索引阶段进行计算。如果查询延迟的优先级比索引吞吐量的优先级更高的话,这个方式就能奏效。

        例2:使用ES现有功能避免脚本。运行寻找“elasticsearch”活动的查询,但是基于如下假设,使用这样的方式来提升或降低得分。

  • 即将举行的活动更为相关。将使得活动得分随着举行时间的推远而呈指数下降,最多60天。
  • 参与者越多的活动越热门而且越相关。将根据参与人数的增多而线性地增加活动的得分。

        如果在索引阶段计算了活动参与者的数量(将字段命名为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'。

3. 网络

        当发送一个搜索请求到某个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。

4. 分页

        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没有必要为当前页面而排列所有之前的页面。

        只有当事先知道需要深度分也时,滚动才是有用的。当只需要少数几页结果时并不推荐滚动操作。

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值