专栏首页Hadoop数据仓库触类旁通Elasticsearch:优化

触类旁通Elasticsearch:优化

一、合并请求

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 浅尝辄止MongoDB:基础

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    用户1148526
  • 初学乍练redis:一键部署集群

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    用户1148526
  • 初学乍练redis:使用redis-migrate-tool做redis在线数据迁移

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    用户1148526
  • Lucene基本知识入门

    Lucene 是一套用于全文检索和搜寻的开源程序库,提供了一个简单却强大的 API,能够做全文索引和搜寻。在 Java 开发环境里,Lucene 是一个成熟的免...

    剑影啸清寒
  • elasticsearch文档索引API(二)

    上篇文章和读者讨论了Elasticsearch中文档的索引API、自动创建索引、版本控制以及操作类型等问题,本文我们继续上文的话题,来看看文档索引的其他知识点。

    江南一点雨
  • Elasticsearch 的一些关键概念

    我更喜欢把 Elasticsearch 作为一种 nosql 去理解,它的一些开发概念和 MongoDB 以及 Redis 没有太大的区别,不过了解 Elast...

    潘成涛
  • Lucene构建个人搜索引擎解析

    简单来说,Lucene提供了一套完整的工具来帮助开发者构建自己的搜索引擎,开发者只需要import Lucene对应的package即可快速地开发构建自己的业务...

    潘少
  • Elasticsearch基本概念及特点

    Lucene:简单来说,就是一个jar包,里面包含了封装好的各种建立倒排索引,以及进行搜索的代码,包含各种算法,我们用java开发的时候,引入lucene.ja...

    create17
  • 快速掌握分布式搜索引擎ElasticSearch(一)

    由于最近在项目中接触使用到了ElasticSearch,从本篇博客开始将给大家分享这款风靡全球的产品。将涉及到ElasticSearch的安装、基础概念、基本用...

    用户2890438
  • 文本处理,第2部分:OH,倒排索引

    这是我的文本处理系列的第二部分。在这篇博客中,我们将研究如何将文本文档存储在可以通过查询轻松检索的表单中。我将使用流行的开源Apache Lucene索引进行说...

    沈唁

扫码关注云+社区

领取腾讯云代金券