小白学 ElasticSerach(一):原理介绍篇(上)

一、ES的初步介绍

1.ES与Lucene

提到ES必然绕不开Lucene。Lucene是一个Java语言的搜索引擎类库,是Apache组织的项目,DougCutting于1999年研发。Lucene方便扩展,基于倒排索引,高性能。但Lucene仅仅是一个基础类库,必须使用Java作为开发语言并将其直接集成到开发的应用中,也没有考虑到高并发和分布式的场景,且学习曲线陡峭,上手时间较长。

Elasticsearch使用Java开发,以 Lucene 作为其核心来实现所有索引和搜索的功能,数据的输入输出采用 JSON 格式。Elasticsearch通过简单的 RESTful API 隐藏了Lucene 的复杂性,从而让全文搜索变得简单。

相比与lucene,elasticsearch支持分布式,可水平扩展;提供Restful接口,可被任何语言调用。

2.ELK

ELK 是Elasticsearch、Logstash 和 Kibana 这三个产品的首字母缩写。Elastic stack主要包括下面四个工具

Logstash是 ELK 的中央数据流引擎,用于从不同目标(文件/数据存储/MQ)收集的不同格式数据,经过过滤后支持输出到不同目的地(文件/MQ/redis/elasticsearch/kafka等)。Kibana可以将 elasticsearch 的数据通过友好的页面展示出来,提供实时分析的功能。

Elastic 后面又引入了 Beats 家族。这是一系列非常轻量级的数据收集端,比如:

  • Packetbeat 可以实时监听网卡流量,并实时解析网络协议数据,可用来做 NPM 网络数据分析;

  • Metricbeat 可以用来收集服务器,以及服务器上部署的应用服务的各项监控指标数据,这样就可以替代Zabbix 等传统的监控软件,来做服务器的性能指标分析;

  • Auditbeat 可以实时收集服务器的行为事件,用于安全方面的入侵检测和安全日志审计分析;

  • Winlogbeat 用于 Windows 平台的事件日志收集;

  • Filebeat 用于日志文件的收集等。

二、基础概念

1.文档Document

ES是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为Json格式后存储在ES中,而Json文档中往往包含很多的字段(Field),类似于数据库中的列。如下所示:

2.索引Index

索引是由具有相同字段的文档列表组成。可以把索引类比成Mysql数据库中的表。数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

比如我们可以在生成日志的时候可以按月建立索引存储:

ngix-log-202311

ngix-log-202312

ngix-log-202401

三、正向索引与倒排索引对比

在上一部分介绍Lucene和ES时,都提到了倒排索引这一概念,那什么是倒排索引呢。下面通过两个简化的过程来对比正向索引和倒排索引的差异。

1.正向索引查找过程

与倒排索引相对的是正向索引,传统数据库(如MySQL)采用正向索引

如下是个货物表(tb_goods)

id name price quantity
1 小米耳机 119 10
2 小米手环 199 20
3 小米手机 2000 100
4 华为手机 5000 30
5 华为手环 299 50

当我们想要搜索"手环",使用MYSQL进行查询,过程可以简要概括如下(这里隐藏回表查询等具体细节)

上图的逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

2.倒排索引查找过程

ES采用倒排索引:

文档(document):每条数据就是一个文档

词条(term):文档按照语义分成的词语

id name price quantity
1 小米耳机 119 10
2 小米手环 199 20
3 小米手机 2000 100
4 华为手机 5000 30
5 华为手环 299 50

倒排索引之后

词条(term) 文档id
小米 1,2,3
耳机 1
手环 2,5
手机 3,4
华为 4,5

1)用户输入条件"小米手环"进行搜索。

2)对用户输入内容分词,得到词条:小米、手环。

3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3、5。

4)拿着文档id到正向索引中查找具体文档。

四、倒排索引底层结构与实现

1.倒排索引的核心组成

倒排索引的实现必须有两个核心组成部分:单词词典和对应的倒排列表(每个词条对应的文档id组成的集合),类似下图

词条(term) 文档id
小米 1,2,3
耳机 1
手环 2,5
手机 3,4
华为 4,5

1.1 单词词典:(Term Dictionary)

  • 记录所有文档的单词,记录单词到倒排列表的关联关系

  • 单词词典一般比较大,可以通过B+树或哈希拉链法实现,以满足高性能的插入与查询

1.2 倒排列表:(Posting List)

记录了单词对应的文档集合,由倒排索引项组成

ES的倒排索引项:

  • 文档ID:用于获取原始信息

  • 词频TF:该单词在文档中出现的次数,用于相关性评分

  • 位置(Positon):单词在文档中分词的位置,用于语句搜索

  • 偏移(Offset):记录单词的开始结束位置,实现高亮显示

举个例子

2.Lucene/ES对于倒排索引的实现

Lucene/ES 的倒排索引,在上面的表格的基础上,在左边增加了一层字典树 Term Index,它不存储所有的单词,只存储单词前缀。查找过程可以概括为:

  • 先通过字典树找到单词所在的块,也就是单词的大概位置,

  • 在单词对应的块里进行二分查找,找到对应的单词

  • 通过单词找到单词对应的文档列表。

存储结构优化

  • 字典树缓存在内存中,内存大小有限,FST(Finite State Transducers)对 Term Index 做进一步压缩。

  • Term dictionary 在磁盘上是以分 block 的方式保存的,一个block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 B 树更节约磁盘空间。

原生的 Posting List 有两个可以改进的地方

  • 如何压缩以节省空间

  • 如何快速求并交集

3.Posting List压缩策略:FOR

在Lucene/ES里,数据按照 Segment 存储的,每个 Segment 最多存 65536 个文档 ID, 对应的文档ID序号从 0 到 2^16-1,需要16位表示,所以如果不进行压缩,那么每个文档ID的表示都会占用 2 bytes 。

Frame Of Reference(FOR)是一种压缩技术,主要分为:增量编码、分块压缩、按需分配空间三个步骤,下面通过一个文档ID组成的数组:[73, 300, 302, 332, 343, 372]来展示FOR所做的工作。如果不通过FOR,那么这个数组需要占用空间:6 * 2 bytes = 12 bytes。

3.1 增量编码

如果我们只记录元素与基准元素之间的增量,以73作为基准,其它元素用增量表示,数组变成了:

Markdown 复制代码
[73, 227, 2, 30, 11, 29]

3.2 分块操作

Lucene 里每个块包含 256 个文档 ID,这样可以保证每个块在经过增量编码后,每个元素都不会超过 256(1 byte),另外还方便进行后面求交并集的跳表运算。

为了方便演示,这里的例子假设每个块是 3 个文档 ID,那么上面的数组进一步变成:

css 复制代码
[73, 227, 2], [30, 11, 29]

3.3 按需分配空间

对于第一个块,[73, 227, 2]:

  • 最大元素是227,最少8 bits可以表示,所以就给每个元素都分配 8 bits的空间

  • 三个元素占用的空间:3 * 8 bits=24 bits,占用3 bytes

对于第二个块,[30, 11, 29]:

  • 最大的元素30,最少5 bits可以表示,所有给每个元素只分配 5 bits 的空间

  • 三个元素占用的空间:3 * 5 bits=15 bits,占用2 bytes

两个块加起来只需要:5 bytes,比一开始的12 bytes,减少了7 bytes的空间占用,压缩率很高。

4.快速求交并集

设想这样一个场景,我们去一个北京旅游,需要提前预定酒店,我们在预定酒店的平台输入下列筛选条件:

  • 城市:北京

  • 价位区间:100---200

  • 酒店名字:如家酒店

这样就需要根据三个字段,去三个倒排索引里去查,当然,磁盘里的数据用了 FOR 进行压缩,所以我们要把数据进行反向处理,即解压,才能还原成原始的文档 ID,然后把这三个文档 ID 数组在内存中做一个交集。

假设我们三个条件搜到的文档id数组分别是

Markdown 复制代码
[2, 13, 17,20, 98]
[1, 13, 22, 35, 98, 99]
[1, 3, 13, 20,35,80,98]

那么同时满足我们想要条件的,就是这三个数组的交集。需要把这三个数组放到内存做运算,做运算,这里有两个策略可以选择。

4.1 位图和"与"运算

一种方式是用位图表示,我们可以拿一个简单的数组举例:

假设有这样一个数组:

csharp 复制代码
[3,6,7,10]

那么可以这样通过使用 bitmap (位图)来表示:

用0表示对应的数字不存在,用1表示对应的数字存在

这样带来了两个好处:

(1)节省空间

假设有100M 个文档 ID,每个文档 ID 占 2 bytes,那已经是 200 MB,而这些数据是要放到内存中进行处理的,把这么大量的数据,从磁盘解压后丢到内存,内存肯定撑不住。

如果使用位图,只需要 0 和 1,那每个文档 ID 就只需要 1 bit,还是假设有 100M 个文档,那只需要 100M bits = 100M * 1/8 bytes = 12.5 MB,比用 Integer 数组的 200 MB 节省了大量的内存。

(2)运算更快

0 和 1,天然就适合进行位运算,通过与运算就可以快速求交集。

那么对于上面我们想求的三个数组交集,我们只需要将它们做与运算,就可以求出来交集。

4.2 Integer和跳表

4.2.1 使用场景分析

上面使用的位图有个问题:不管有多少个文档,占用的空间都是一样的。在上面说过,Lucene Posting List 的每个 Segement 最多放 65536 个文档ID。

有的时候想做交集的数组,可能比较稀疏。举一个极端的例子,有一个数组,里面只有两个文档 ID:

[0, 65535]

如果使用位图表示,那就需要:

[1,0,0,0,....(超级多个0),...,0,0,1]

需要 65536 个 bit,也就是 65536/8 = 8192 bytes,而用 Integer 数组,只需要 2 * 2 bytes = 4 bytes

可见在文档数量不多的时候,使用 Integer 数组更加节省内存。

4.2.2 求交集过程

需要查找的每一个 int 数组建立跳表,然后由最短的 posting list 开始遍历,遍历的过程中各自可以跳过不少元素。

以上是三个posting list。现在需要把它们用AND的关系合并,得出posting list的交集。首先选择最短的posting list,然后从小到大遍历。遍历的过程可以跳过一些元素,比如我们遍历到绿色的13的时候,就可以跳过蓝色的3了,因为3比13要小。

详细过程如下:

  • 选择最短的绿色数组,获取当前的最小元素 ---> 2

  • 在橙色数组找大于等于2的第一个元素 ---> 13,不等于2,2必然不是三个数组的交集

  • 在最短的绿色数组找大于等于13的第一个元素 ---> 13

  • 去蓝色数组找大于等于13的第一个元素 ---> 13,13出现三次,属于交集的元素

  • 去绿色数组找下一个元素 ---> 17

  • 在橙色数组找大于等于17的第一个元素 ---> 22,不等于17,17必然不是三个数组的交集

  • 在最短的绿色数组找大于等于22的第一个元素 ---> 98,不等于17,17必然不是三个数组的交集

  • 在橙色数组找大于等于98的第一个元素 ---> 98

  • 去蓝色数组找大于等于98的第一个元素 ---> 98,98出现三次,属于交集的元素

  • 最短的绿色数组已经没有元素了,遍历结束,交集为[13,98]

    Next -> 2 Advance(2) -> 13 Advance(13) -> 13 Already on 13 Advance(13) -> 13 MATCH!!! Next -> 17 Advance(17) -> 22 Advance(22) -> 98 Advance(98) -> 98 Advance(98) -> 98 MATCH!!!

4.3 Roaring Bitmaps

在4.2我们提到:在文档数量不多的时候,使用整数数组更省空间,那么多少可以称作不多呢。

这里我们可以计算一下临界值:

  • 对于使用整数的形式,占据空间大小为2x bytes(x 表示文档数量);

  • 对于使用位图的形式,无论文档数量,大小固定为8192 bytes;

  • 当x=4096,两者相等。也就是说,当文档数量少于 4096 时,用 Integer 数组,否则,用 bitmap。

Roaring Bitmaps的目标是更好地利用好上面的两个选项,根据块内文件数量动态选择使用位图还是整数数组,数量小于4096使用整数数组,否则位图。

开始的时候我们把集合按16位的最大值(65536)来切分成数据块。这也就意味着,第一个数据块可以被0到65535之间的数值编码,第二个数据块编码范围是65536到131071。然后在每个数据块,我们使用16位来进行独立编码:如果它有少于4096个值,就会使用数组,否则的话就使用bitmap。

图源:www.elastic.co/cn/blog/fra...

对上图的过程解释:

  • 我们当前的索引集合为:[ 1000,62101,131385,132052,191173,196658 ],我们要对其分块,每块的文档ID数值编码为16位,那么第一块可以表示[0,65535],第二块可以表示[65536,131071]........

以此类推,第n块数字范围是:[(n-1)*65536,(n-1)*65536+65535]

  • 对于每一个文档id,我们通过除65536的除数确定它在第几块,余数就是它压缩编码之后的新数字表示比如1000:1000/65536=0,1000%65536=10000,在第一块。

  • 因此我们可以知道:

第一块有:1000,62101

第三块有:313,980,60101。 实际上是[131385,132052,191173]分别对65536进行取模运算

第四块有:50。实际上是[196658]对65536进行取模运算

5.总结:FOR、位图、整数数组

  • Frame Of Reference 是压缩数据,减少磁盘占用空间,所以当从磁盘取数据时,也需要一个反向的过程,即解压

  • 解压后才这样的文档ID数组:[73, 300, 302, 303, 343, 372]

  • 解压后需要对数据进行处理,求交集或者并集,这时候数据是需要放到内存进行处理的,我们有三个这样的数组,这些数组可能很大,而内存空间比磁盘还宝贵,于是需要更强有力的压缩算法,同时还要有利于快速的求交并集,于是有了Roaring Bitmaps 算法(根据文档数量选择用整数数组还说位图)

另外,Lucene 还会把从磁盘取出来的数据,通过 Roaring bitmaps 处理后,缓存到内存中,Lucene 称之为 filter cache。

相关推荐
+码农快讯+6 分钟前
Git入门学习(1)
git·学习·elasticsearch
码爸4 小时前
java 执行es中的sql
java·sql·elasticsearch
学习使我快乐——玉祥6 小时前
es查询语法
大数据·elasticsearch·搜索引擎
徐*红9 小时前
Elasticsearch 8.+ 版本查询方式
大数据·elasticsearch
码爸9 小时前
flink 例子(scala)
大数据·elasticsearch·flink·scala
txtsteve10 小时前
es由一个集群迁移到另外一个集群es的数据迁移
大数据·elasticsearch·搜索引擎
工作中的程序员10 小时前
ES 索引或索引模板
大数据·数据库·elasticsearch
Lill_bin1 天前
深入理解ElasticSearch集群:架构、高可用性与数据一致性
大数据·分布式·elasticsearch·搜索引擎·zookeeper·架构·全文检索
RwTo1 天前
Elasticsearch 聚合搜索
大数据·elasticsearch·搜索引擎·全文检索
求学小火龙1 天前
ElasticSearch介绍+使用
java·大数据·elasticsearch