手记

Elastic Stack从入门到实践之Search,分布式,聚合分析

  • 实现对es中存储的数据进行查询分析,endpoint为**_search**,如下所示:
  • 指定索引查询,可以一次查询多个
GET/search
GET/my index/search
GET/my index1,my _index2/_search
GET/my_*/_search
  • 查询主要有两种形式
    • URI Search
      • 操作简便,方便通过命令行测试
      • 仅包含部分查询语法
  • Request Body Search
    • es 提供的完备查询语法Query DSL(Domain Specific Language)
  • 通过url query参数来实现搜索,常用参数如下:
    • q指定查询的语句,语法为Query String Syntax
      • term与phrase
        • alfred way 等效于alfred OR way
        • “alfred way"词语查询,要求先后顺序
      • 泛查询
        • alfred等效于在所有字段去匹配该term
      • 指定字段
        • name:alfred
      • Group分组设定,使用括号指定匹配的规则
        • (quick OR brown)AND fox
        • status:(active OR pending)title:(full text search)
      • 布尔操作符
        • AND(&&),OR(I),NOT(!)
          • name:(tom NOT lee)
          • 注意大写,不能小写
        • ±分别对应must和must not
          • name:(tom +lee-alfred) 简化
          • name:((lee&&lalfred)lI (tom && lee &&!alfred))
          • +在url中会被解析为空格,要使用encode后的结果才可以,为%2B
        • 范围查询,支持数值和日期
          • 区间写法,闭区间[],开区间用{}
          • age:[1 TO 10]意为1<=age<=10
          • age:[1 TO 10}意为1<=age<10
          • age[1 TO]意为age>=1
          • age:[* TO 10]意为age<=10
          • 算数符号写法
          • age:>=1
          • age(>=1&&<=10)或者age:(+>=1+<=10)
        • 通配符查询
          • *?代表1个字符,代表0或多个字符
          • name:t?m
          • name:tom*
          • name:t*m
          • 通配符匹配执行效率低,且占用较多内存,不建议使用
          • 如无特殊需求,不要将?/*放在最前面
        • 正则表达式匹配
          • name:/[mb]oat/
          • 占用较多内存
        • 模糊匹配fuzzy query
          • name:roam~1
            • 匹配与roam差1个character的词,比如foam roams等
        • 近似度查询 proximity search
          • “fox quick"~5
          • 以term为单位进行差异比较,比如“quick fox”"quick brown fox”都会被匹配
    • df q中不指定字段时默认查询的字段,如果不指定,es会查询所有字段
    • sort排序
    • timeout 指定超时时间,默认不超时
    • from,size 用于分页

通过profile:true来查看实际执行的语句

  • 将查询语句通过http request body发送到es,主要包含如下参数
    • query 符合Query DSL语法的查询语句
    • from,size
    • timeout
    • sort

Query DSL

  • 基于JSON定义的查询语言,主要包含如下两种类型:

字段类查询

	 - 
  • 如term,match,range等,只针对某一个字段进行查询
    • 字段类查询主要包括以下两类:
    • 全文匹配
      • 针对text 类型的字段进行全文检索,会对查询语句先进行分词处理,如match,match phrase等query类型
    • 单词匹配
      • 不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term,terms,range等query类型、


  • 通过operator 参数可以控制单词间的匹配关系,可选项为or和and
  • 通过minimum_ should_match参数可以控制需要匹配的单词数

相关性算分

  • 相关性算分是指文档与查询语句间的相关度,英文为relevance
    • 通过倒排索引可以获取与查询语句相匹配的文档列表,那么如何将最符合用户查询需求的文档放到前列呢?
    • 本质是一个排序问题,排序的依据是相关性算分

  • 相关性算分的几个重要概念如下:
    • **Term Frequency(TF)**词频,即单词在该文档中出现的次数。词频越高,相关度越高
    • **Document Frequency(DF)**文档频率,即单词出现的文档数
    • **Inverse Document Frequency(IDF)**逆向文档频率,与文档频率相反,简单理解为1/DF。即单词出现的文档数越少,相关度越高
    • Field-length Norm 文档越短,相关性越高

ES目前主要有两个相关性算分模型,如下:

TF/IDF模型,TF/IDF 模型是Lucene的经典模型

  • 可以通过explain参数来查看具体的计算方法,但要注意:
    • es的算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
    • 可以通过设置索引的分片数为1来避免这个问题
"settings":{
“index":{
	"number of shards":“1"
	}
}

BM25模型5.x之后的默认模型
BM25模型中BM指Best Match,25指迭代了25次才计算方法,是针对TF/IDF的一个优化

  • BM25相比TF/IDF的一大优化是降低了tf在过大时的权重

复合查询

  • 如bool 查询等,包含一个或多个字段类查询或者复合查询语句

Match Phrase Query

  • 对字段作检索,有顺序要求
  • 通过slop参数可以控制单词间的间隔

Query String Query

  • 类似于URI Search中的q参数查询

Simple Query String Query

  • 类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
  • 其常用的逻辑符号如下,不能使用AND、OR、NOT等关键词:
  • + 代指AND
  • | 代指OR
  • - 代指NOT

Term Query / Terms Query

  • 将查询语句作为整个单词进行查询,即不对查询语句做分词处理
  • Terms 一次传入多个单词进行查询

分布式 Cerebro

  • es支持集群模式,是一个分布式系统,其好处主要有两个:
  • 增大系统容量,如内存、磁盘,使得es集群可以支持PB级的数据
  • 提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务
  • es 集群由多个es实例组成
    • 不同集群通过集群名字来区分,可通过cluster.name 进行修改,默认为elasticsearch
    • 每个es实例本质上是一个JVM进程,且有自己的名字,通过node.name 进行修改

  • 运行如下命令可以启动一个es 节点实例
    • bin/elasticsearch -E cluster.name=my_cluster -E node.name=node1 -E http.port=5200
  • es 集群相关的数据称为cluster state,主要记录如下信息:
    • 节点信息,比如节点名称、连接地址等
    • 索引信息,比如索引名称、配置等
  • 可以修改cluster state的节点称为master 节点,一个集群只能有一个
    • cluster state 存储在每个节点上,master维护最新版本并同步给其他节点
    • master 节点是通过集群中所有节点选举产生的,可以被选举的节点称为master-eligible 节点,相关配置如下:
      • node.master:true
  • 处理请求的节点即为coordinating节点,该节点为所有节点的默认角色,不能取消
    • 路由请求到正确的节点处理,比如创建索引的请求到master 节点
  • 存储数据的节点即为data节点,默认节点都是data类型
    • node.data:true

服务可用性

  • 2个节点的情况下,允许其中1个节点停止服务
  • 数据可用性
    • 引入副本(Replication)解决
    • 每个节点上都有完备的数据

如何将数据分布于所有节点上?

  • 引入分片(Shard)解决问题
  • 分片是es支持PB级数据的基石
  • 分片存储了部分数据,可以分布于任意节点上
  • 分片数在索引创建时指定目后续不允许再更改,默认为5个
  • 分片有主分片和副本分片之分,以实现数据的高可用
  • 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量
PUT test index
"settings":{
"number of shards":3,
"number of replicas":1 //副本
}
  • 分片数的设定很重要,需要提前规划好
    • 过小会导致后续无法通过增加节点实现水平扩容
    • 过大会导致一个节点上分布过多分片,造成资源浪费,同时会影响查询性能

Cluster Health

  • 通过如下api可以查看集群健康状况,包括以下三种:
  • green健康状态,指所有主副分片都正常分配
  • yellow指所有主分片都正常分配,但是有副本分片未正常分配
  • red有主分片未分配
GET _cluster/health
  • node1所在机器宕机导致服务终止,此时集群会如何处理?
      1. node2和node3发现node1无法响应一段时间后会发起master选举,比如这里选择node2为master节点。此时由于主分片PO下线,集群状态变为Red。
    • 2.node2发现主分片PO未分配,将RO提升为主分片。此时由于所有主分片都正常分配,集群状态变为Yellow。
    • 3.node2为PO和P1生成新的副本,集群状态变为绿色

文档分布式存储

  • 文档到分片的映射算法
    • shard =hash(routing)% number_of_primary_shards
    • 使得文档均匀分布在所有分片上,以充分利用资源
    • 根据文档值实时计算对应的分片
    • hash 算法保证可以将数据均匀地分散在分片中
    • routing 是一个关键参数,默认是文档id,也可以自行指定
    • number_of _primary_shards是主分片数
  • 该算法与主分片数相关,这也是分片数一旦确定后便不能更改的原因

文档创建流程

文档读取流程

文档批量创建\读取流程


脑裂问题

脑裂问题,英文为split-brain,是分布式系统中的经典网络问题,如下图所示:

  • node2与node3会重新选举master,比如node2成为了新master,此时会更cluster state
  • node1自己组成集群后,也会更新cluster state
  • 同一个集群有两个master,而且维护不同的cluster state,网络恢复后无法选择正确master
  • 解决方案为仅在可选举master-eligible 节点数大于等于quorum 时才可以进行 master选
  • quorum=master-eligible 节点数/2+1,例如3个master-eligible节点时,quorum为2。
  • 设定discovery.zen.minimum_master_nodes为quorum 即可避免脑裂

倒排索引的不可改变

倒排索引一旦生成,不能更改
其好处如下:

  • 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
  • -由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,
  • 对该文件的读取都会从内存读取,性能高
  • -利于生成缓存数据
  • -利于对文件进行压缩存储,节省磁盘和内存存储空间
    坏处为需要写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,导致文档实时性差

文档搜索实时性

新文档直接生成新的倒排索引文件,查询的时候同时查询所有的倒排文件,然后做结果的汇总计算即可

  • Lucene 便是采用了这种方案,它构建的单个倒排索引称为segment,合在一起称为Index,与ES中的Index概念不同。ES中的一个Shard 对应一个Lucene Index
  • Lucene会有一个专门的文件来记录所有的segment信息,称为commit point

  • Refresh
  • segment 写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将
  • segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中被称为refresh
  • 在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成 segment
  • es默认每1秒执行一次refresh,因此文档的实时性被提高到1秒,这也是es被称为近实时(Near Real Time)的原因
  • refresh发生的时机主要有如下几种情况:
    • 间隔时间达到时,通过index.settings.refresh interval来设定,默认是1秒
    • index.buffer 占满时,其大小通过indices.memory.index_buffer_size设置,默认为jvm heap的10%,所有shard共享
    • flush 发生时也会发生refresh

如果在内存中的segment还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?

  • es引入translog 机制。写入文档到buffer时,同时将该操作写入translog。
  • translog文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每5秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
  • es启动时会检查translog文件,并从中恢复数据

flush 负责将内存中的segment写入磁盘,主要做如下的工作:

  • 将translog写入磁盘*
  • 将index buffer清空,其中的文档生成一个新的 segment,相当于一个refresh 操作
  • 更新commit point并写入磁盘
  • 执行fsync操作,将内存中的segment写入磁盘
  • 删除旧的translog文件
    flush 发生的时机主要有如下几种情况:
  • 间隔时间达到时,默认是30分钟,5.x之前可以通过**index.translog.flush
  • threshold_period**修改,之后无法修改
  • translog 占满时,其大小可以通过index.translog.flush_threshold _size控制,默认是512mb,每个index有自己的translog

删除更新文档

  • segment一旦生成就不能更改,那么如果你要删除文档该如何操作?
    • Lucene专门维护一个.del的文件,记录所有已经删除的文档,注意.del上记录的是文档在Lucene内部的id
    • 在查询结果返回前会过滤掉.del中的所有文档
  • 更新文档如何进行呢?
    • 首先删除文档,然后再创建新文档

Segment Merging

  • 随着segment的增多,由于一次查询的segment数增多,查询速度会变慢
  • es会定时在后台进行segment merge的操作,减少segment的数量
  • 通过force_ merge api可以手动强制做 segment merge的操作

ES Index与Lucene Index的术语对照如下所示:

深入Search

Search执行的时候实际分两个步骤运作的

Query阶段

Fetch阶段

相关性算分问题

  • 相关性算分在shard与shard间是相互独立的,也就意味着同一个Term的IDF等值在不同shard上是不同的。文档的相关性算分和它所处的
  • shard相关在文档数量不多时,会导致相关性算分严重不准的情况发生
  • 解决思路有两个:
    • 一是设置分片数为1个,从根本上排除问题,在文档数量不多的时候可以考虑该方案,比如百万到干万级别的文档数量
    • 二是使用DFS Query-then-Fetch 查询方式
      • DFS Query-then-Fetch 是在拿到所有文档后再重新完整的计算一次相关性算分,耗费更多的cpu和内存,执行性能也比较低下,一般不建议使用。
  • es默认会采用相关性算分排序,用户可以通过设定 sorting参数来自行设定排序规则

  • 按照字符串排序比较特殊,因为es有text和keyword 两种类型,针对text类型排序

排序的过程实质是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用,需要用到正排索引,也就是通过文档Id 和字段可以快速得到字段原始内容。
es对此提供了2种实现方式:fielddata默认禁用,doc values 默认启用,除了text类型(文档ID对字段值的映射)

  • Fielddata默认是关闭的,可以通过如下api开启:
    • 此时字符串是按照分词后的term排序,往往结果很难符合预期
    • 一般是在对分词做聚合分析的时候开启
  • Doc Values 默认是启用的,可以在创建索引的时候关闭:
    • 如果后面要再开启doc values,需要做reindex操作
  • docvalue_fields
    • 可以通过该字段获取 felddata 或者doc values中存储的内容

分页,遍历

from/size

最常用的分页方案

  • from指明开始位置
  • size 指明获取总数

深度分页是一个经典的问题:在数据分片存储的情况下如何获取前1000个文档?

  • 获取从990~1000的文档时,会在每个分片上都先获取1000个文档,然后再由Coordinating Node聚合所有分片的结果后再排序选取前1000个文档
  • 页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window 限定最多到10000条数据

scroll

遍历文档集的api,以快照的方式来避免深度分页的问题

  • 不能用来做实时搜索,因为数据不是实时的
  • 尽量不要使用复杂的sort条件,使用**_doc**最高效
  • 使用稍嫌复杂

  • 第一步需要发起1个scroll search,如下所示:
    • es在收到该请求后会根据查询条件创建文档Id合集的快照
  • 第二步调用scroll search的api,获取文档集合,如下所示:
    • 不断迭代调用直到返回 hits.hits数组为空时停止
  • 过多的scroll 调用会占用大量内存,可以通过clear api删除过多的scroll 快照

search_after

  • 避免深度分页的性能问题,提供实时的下一页文档获取功能
    • 缺点是不能使用from参数,即不能指定页数
    • 只能下一页,不能上一页、
    • 使用简单
  • 第一步为正常的搜索,但要指定sort值,并保证值唯一
  • 第二步为使用上一步最后一个文档的 sort值进行查询
  • 通过唯一排序值定位将每次要处理的文档数都控制在size内

聚合分析

  • 聚合分析,英文为Aggregation,是es除搜索功能外提供的针对es数据做统计分析的
  • 功能丰富,提供Bucket、Metric、Pipeline等多种分析方式,可以满足大部分的分析
  • 实时性高,所有的计算结果都是即时返回的,而 hadoop等大数据系统一般都是T+1级别的
  • 请告诉我公司目前在职人员工作岗位的分布情况?
  • 分类
    • Bucket,分桶类型,类似SQL中的GROUPBY 语法
    • Metric,指标分析类型,如计算最大值、最小值、平均值等等
      • 单值分析,只输出一个分析结果
        • min,maxavg,sum
        • cardinality
      • 多值分析,输出多个分析结果
        • stats(反回一系列数值类型的统计值,包含min、max、avg、sum和count),extended stats(对stats的扩展,包含了更多的统计数据,如方差、标准差等)
        • percentile(百分位数统计),percentile rank(百分位数统计,添加values字段,值做键,百分比做值)
        • top hits(一般用于分桶后获取该桶内最匹配的顶部文档列表,即详情数据)
    • Pipeline,管道分析类型,基于上一级的聚合分析结果进行再分析
    • Matrix,矩阵分析类型
      • Cardinality,意为集合的势,或者基数,是指不同数值的个数,类似SQL中的distinct count概念



Bucket

Bucket,意为桶,即按照一定的规则将文档分配到不同的桶中,达到分类分析的目的

  • Terms 该分桶策略最简单,直接按照term来分桶,如果是text类型,则按照分词后的结果分桶(打开fielddata,否则报错)
  • Range 通过指定数值的范围来设定分桶规则
  • Date Range 通过指定日期的范围来设定分桶规则
  • Histogram(直方图,以固定间隔的策略来分割数据)
  • Date Histogram 针对日期的直方图或者柱状图,是时序数据分析中常用的聚合分析类型

Bucket +Metric 聚合分析

Bucket 聚合分析允许通过添加子分析来进一步进行分析,该子分析可以是Bucket也可以是Metric。这也使得es的聚合分析能力变得异常强大


Pipeline

  • 针对聚合分析的结果再次进行聚合分析,而且支持链式调用,可以回答如下问题:
  • 订单月平均销售额是多少?

Pipeline的分析结果会输出到原结果中,根据输出位置的不同,分为以下两类:

  • Parent 结果内嵌到现有的聚合分析结果中
    • Derivative 导数
    • Moving Average 移动平均
    • Cumulative Sum 累计求和
  • Sibling结果与现有聚合分析结果同级
    • Max/Min/Avg/Sum Bucket
    • Stats/Extended Stats Bucket
    • Percentiles Bucket


聚合分析

es聚合分析默认作用范围是query的结果集,可以通过如下的方式改变其作用范围:

  • filter 为某个聚合分析设定过滤条件,从而在不更改整体query语句的情况下修改了作用范围
  • post_filter 作用于文档过滤,但在聚合分析后生效
  • global,size:0 无视 query过滤条件,基于全部文档进行分析,场景是整体和局部做对比的时候


排序

可以使用自带的关键数据进行排序,比如:

  • _count文档数
  • _key按照key值排序
  • 引用子聚合分析的子聚合分析,用>,json object 里面的某个数值用.

精准度问题

  • Terms不准确的原因
    • 数据分散在多Shard上,Coordinating Node 无法得恶数据全貌
    • 方案:
      • 设置Shard数为1,消除数据分散的问题,但无法承载大数据量
      • 合理设置 Shard_Size大小,即每次从Shard上额外多获取数据,以提升准确度
        • terms聚合返回结果中有如下两个统计值:
        • doc_count error_upper_bound 被遗漏的term可能的最大值(返回列表中的最小值)
        • sum_other doc_count 返回结果bucket的term外其他term的文档总数(doc_count error_upper_bound 加起来)

  • 设定 show_term doc_count_error可以查看每个bucket误算的最大值
  • Shard_Size 默认大小如下:
    • shard_size=(size×1.5)+10
  • 通过调整Shard_Size的大小降低doc_count error upper_bound 来提升准确度
    • 增大了整体的计算量,从而降低了响应时间

  • 在ES的聚合分析中,Cardinality和Percentile分析使用的是近似统计算法
    • 结果是近似准确的,但不一定精准、
    • 可以通过参数的调整使其结果精准,但同时也意味着更多的计算时间和更大的性能消耗
3人推荐
随时随地看视频
慕课网APP