小白学 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。

相关推荐
乐韵天城7 小时前
SpringBoot3.x下如何使用es进行数据查询
elasticsearch
放学有种别跑、10 小时前
GIT使用指南
大数据·linux·git·elasticsearch
越努力越幸运50811 小时前
git工具的学习
大数据·elasticsearch·搜索引擎
不会写程序的未来程序员11 小时前
详细的 Git 操作分步指南
大数据·git·elasticsearch
武子康12 小时前
大数据-167 ELK Elastic Stack(ELK) 实战:架构要点、索引与排错清单
大数据·后端·elasticsearch
20岁30年经验的码农13 小时前
Java Elasticsearch 实战指南
java·开发语言·elasticsearch
v***446714 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
h***673718 小时前
SpringBoot整合easy-es
spring boot·后端·elasticsearch
ALex_zry1 天前
Git大型仓库推送失败问题完整解决方案
大数据·git·elasticsearch
二进制coder1 天前
Git Fork 开发全流程教程
大数据·git·elasticsearch