从SQL到DSL简析ElasticSearch结构化查询

最近项目里有用到ES,顺便简单学习了一下基础的查询以及执行过程,对比是一种很好的学习方法,所以我们先从一个熟悉的SQL开始:

SELECT COUNT(DISTINCT a), AVG(a) ...
FROM t
WHERE a BETWEEN (1, 10) AND b = 2 OR c = 3
GROUP BY a, b, c
HAVING a > 3
ORDER BY b
LIMIT 0, 10

以上就是我们工作中经常使用的一个比较完整的SQL,此次分析不涉及join,子查询,以及ES非结构化查询等,本文涉及到Trie树,Skip List,倒排索引,BKD树,正排索引等数据结构知识,建议先去了解相关的数据结构,下面对DSL中的相应的功能进行逐个分析。

一、天然分布式

首先,ES是天然分布式的结构,所以我先从宏观的角度来介绍一下ES的查询过程,然后在深入到各个分片内部的执行过程分析。
从SQL到DSL简析ElasticSearch结构化查询_第1张图片

  1. 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。
  2. Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
  4. 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
  5. 每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。
  6. 一旦所有的文档都被取回了,协调节点返回结果给客户端。

二、SELECT -> _source

ES可以使用_source参数指定需要返回的字段,比如:

{
  "_source": ["binded_phone"],
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        }
      ],
      "must_not": [],
      "should": [],
      "filter": []
    }
  },
  "from": 0,
  "size": 10,
  "sort": [],
  "profile": false
}

这个查询的结果就只会返回binded_phone这个字段。

三、FROM -> index,type

ES里的index可以类比于MySQL中的库,type则可以类比于表,通过index和type可以唯一确定一个数据源。

四、WHERE -> bool

  1. 条件运算符
    SQL中的条件运算符有,and、or、not,对应在ES中分别是must/filter、should、must_not
    这里需要注意的是,must会进行相关性打分,filter不会,而且filter的查询结果缓存所以如果只是做结构化查询的话,建议不要使用must;
    另外如果must/filter和should一起用,需要加一个参数,“minimum_should_match”: 1来保证should至少有一项可以命中,否则should条件会被全部忽略。
    官网说法如下:
    The clause (query) should appear in the matching document. If the bool query is in a query context and has a must or filter clause then a document will match the bool query even if none of the should queries match. In this case these clauses are only used to influence the score. If thebool query is a filter context or has neither must or filter then at least one of the should queries must match a document for it to match the bool query. This behavior may be explicitly controlled by settings the minimum_should_match parameter.

  2. 结构化匹配查询
    ES的结构化匹配型查询条件很丰富,有term,terms,exists,prefix,wildcard,fuzzy,regexp
    在一个分片上执行匹配查询的过程基于FST(finite state transducers)、倒排索引以及跳表,性能特别高。
    从SQL到DSL简析ElasticSearch结构化查询_第2张图片
    ES一次匹配查询的过程要经过这三个数据结构,首先在Term Index进行第一步前缀匹配,然后定位Term Dictionary中的偏移,之后进行线性的向后查找定位到最终的Term,得到倒排表,最后将多个组合条件的倒排表结果进行合并,就是取交集,并集,差集。
    Term Index是什么?
    Term Index是进行关键匹配的数据结构,ES中使用的是FST,就是Trie树的一种变形,FST有三个优点:
    1)空间占用小
    通过对词典中单词前缀和后缀的重复利用,压缩了存储空间,对比Trie树和FST插入mon,tues,thurs等词的结果,Trie树的构建结果:
    从SQL到DSL简析ElasticSearch结构化查询_第3张图片
    FST的构建结果:
    从SQL到DSL简析ElasticSearch结构化查询_第4张图片
    Trie树相对于B+树等搜索树的压缩效果已经很好了,ES仍然不满意,在此基础上又对后缀进行了公用,就是FST。
    2)查询速度快
    Trie树的查询复杂度O(len(str)),没什么可说的
    3)匹配能力强
    FST是基于Trie树,Trie树的匹配能力比B+树,红黑树等这种搜索树强大很多,比如正则匹配的支持能力
    Term Dictionary是什么?
    就是ES最闻名的数据结构,倒排索引
    Posting List是什么?
    Posting List是对应的倒排表,是一个有序的链表,基于Skip List存储,可以加速组合结果的合并,如何利用Skip List快速合并结果呢,如图
    从SQL到DSL简析ElasticSearch结构化查询_第5张图片
    比如求交集,ES会选出最短的倒排链,开始遍历,比如图中的绿色这条,然后与其他倒排表进行比较,Skip List我们知道,会构建多层,就像线段树一样,越高层范围越大,所以可以在高层进行比较,跳过低层中不可能匹配的docId,上图匹配到13的时候,就可以跳过对1和13的比较了。
    但是如果倒排表很长,很多的时候,这种方式可能就不太适用了,这时候可以把倒排表转成bitSet,bitSet做交并差操作也很方便,但是受到最大docid的影响,所以在数据量小的时候不如Skip List高效。

  3. 结构化范围查询
    ES的结构化范围型查询条件就是range,ES的范围性查询支持数值型,日期型,地理位置等字段,在每个分片上的执行基于BKD (Balance K - Dimensional ) Tree ,BKD树只是完全二叉树和B+树的组合,在k=1的时候会退化成简单二叉搜索树,用于范围查询和最近邻查询
    从SQL到DSL简析ElasticSearch结构化查询_第6张图片
    这个树有以下几个特点:
    1)所有的节点都在内存里,不可变,构成一个完全二叉树,完全二叉树不用存指向子节点的指针,block在磁盘上
    2)docId在block中是无序的,所以结果合并不方便(排序,bitSet)
    范围性查询和B+树的执行过程基本一样的。

五、GROUP BY -> Aggregations

ES支持丰富的聚合功能,包括Metric Aggregations,Bucket Aggregations,Pipeline Aggregations,Matrix Aggregations四种

  1. Bucket Aggregations
    Bucket Aggregation就是SQL中的group by,进行根据字段进行分桶,如何根据倒排索引分桶?比如我们根据一些条件得到了匹配的docId链表,然后对这些docId做聚合,这时候根据倒排索引找所有匹配的doc有哪些字段值只能遍历整个倒排表,所以ES还建立了另外一种索引结构,即Doc Value(或者叫正排索引)
    从SQL到DSL简析ElasticSearch结构化查询_第7张图片
    这样就可以根据docId快速得到某些字段的值是什么,聚合的时候把桶都放到内存里,如果有多级聚合最终会在内存中构成一颗树,基于这些桶来可以进行下列聚合指标的计算

  2. Metric Aggregations
    三角选择原则:
    在精准、实时、大数据 这三个条件中只能选择2个放弃一个。(类似于分布式系统CPA理论?)
    精准+实时: 数据可以存入单台机器的内存之中,我们可以随心所欲,使用任何想用的算法。结果会 100% 精确,响应会相对快速。
    精准+大数据:传统的 Hadoop。可以处理 PB 级的数据并且为我们提供精确的答案,但它可能需要几周的时间才能为我们提供这个答案。
    大数据+实时:比如es,用近似算法为我们提供准确但不精确的结果。
    max,min,ave,sum,count等基本指标聚合是易并行的就是可以无误差地支持map reduce操作,但是更加复杂的指标聚合cardinalitypercentiles不是准确的。
    cardinality:
    cardinality的中文意思是基数,即count distinct,ES中使用HLL(HyperLogLog)算法,这是个很经典的基数估计算法
    我们知道用bitSet存储一亿个统计数据大概需要12M内存;而在HLL中,只需要不到1K内存就能做到;redis中实现的HyperLogLog,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据
    HLL算法的原理源于伯努利试验,可以证明:进行n次进行抛硬币实验,每次分别记录下第一次抛到正面的抛掷次数K1~Kn,那么可以用n次实验中最大的抛掷次数Kmax,则可以预估实验组数量为n的预估值=2^Kmax.
    从SQL到DSL简析ElasticSearch结构化查询_第8张图片
    HLL算法示意图
    要配置精度,我们可以指定 precision_threshold (范围0 – 40000)参数的值。 这个阈值定义了在何种基数水平下我们希望得到一个近乎精确的结果
    对于指定的阈值,HLL 的数据结构会大概使用 precision_threshold * 8 字节的内存,在基数小于precision_threshold的时候,准确率几乎是100%,大于阈值就会丢失准确率。
    在实际应用中, 100 的阈值可以基数为百万的情况下仍然将误差维持 5% 以内。
    如果感兴趣,可以阅读这篇论文: HyperLogLog++
    percentiles
    percentiles的意思就是百分比,常用于统计一个服务的性能,99,999,9999线,ES中使用的是TDigest算法,在spark和kylin中也使用了这个算法
    我们同样可以通过修改参数 compression 来控制内存与准确度之间的比值。
    TDigest 算法用节点近似计算百分比:节点越多,准确度越高(同时内存消耗也越大),这都与数据量成正比。 compression 参数限制节点的最大数目为 20 * compression 。
    因此,通过增加压缩比值,可以以消耗更多内存为代价提高百分位数准确性。更大的压缩比值会使算法运行更慢,因为底层的树形数据结构的存储也会增长,也导致操作的代价更高。默认的压缩比值是 100 。
    一个节点大约使用 32 字节的内存,所以在最坏的情况下(例如,大量数据有序存入),默认设置会生成一个大小约为 64KB 的 TDigest。 在实际应用中,数据会更随机,所以 TDigest 使用的内存会更少。
    如果感兴趣,可以阅读这篇论文:TDigest

    这两个算法都有四个很牛逼的地方:
    1)内存占用小,比bitSet还要优化了上万倍;
    2)准确率高,误差基本能稳定在5%内;
    3)可以支持对流式数据的计算;
    4)可以支持map reduce,将一个完全不可并行的算法,转化成了一个可并行算法;

  3. Pipeline Aggregations 和 Matrix Aggregations
    This functionality is experimental and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but experimental features are not subject to the support SLA of official GA features.
    在5.6版本官网提出这两个功能仅仅是试用,使用率很低。
    Pipeline顾名思义就是管道,接受Aggregations的输出结果然后可以进一步执行给定的脚本,比如支持SQL中的having就可以通过这种聚合来支持
    Matrix是通过指定多个字段来生成矩阵,然后进行计算,这类聚合不支持执行脚本

六、ORDER BY & LIMIT -> sort,from,size

ES默认就会根据相关性得分进行排序,而分页大多在排序情况的下才有意义,所以把这两个放在一起说
排序也是基于我们前面提到的Doc Value,基于倒排索引没法排序
排序采用:堆排序 + 分布式归并的方式,在每个分片上内部排好序之后,取出需要的top from + size,然后汇总到协调节点选出最终的top from + size,返回给客户端
深分页问题:scroll 和 search after (不建议支持深分页)

参考文献:
Elasticsearch: 权威指南
基于Lucene查询原理分析Elasticsearch的性能
elasticsearch 倒排索引原理
Lucene BKD树-动态磁盘优化BSP树
Elasticsearch干货(三):对于数值类型索引优化
elasticsearch的Doc Values 和 Fielddata
神奇的HyperLogLog算法
HyperLogLog in Practice: Algorithmic Engineering of a State of The Art Cardinality Estimation Algorithm
TDigest 算法原理
t-digest

你可能感兴趣的:(大数据)