04.ES原理
# 01.ES 原理
# 1、搜索引擎原理
- 一次完整的搜索从用户输入要查询的关键词开始
- 比如想查找 Lucene 的相关学习资料,我们都会在 Google 或百度等搜索引擎中输入关键词
- 比如输入“Lucene ,全文检索框架”,之后系统根据用户输入的关键词返回相关信息
一次检索大致可分为四步
第一步:查询分析
- 正常情况下用户输入正确的查询,比如搜索“里约奥运会”这个关键词
- 用户输入正确完成一次搜索,但是搜索通常都是全开放的,任何的用户输入都是有可能的
- 很大一部分还是非常口语化和个性化的,有时候还会存在拼写错误,用户不小心把“淘宝”打成“涛宝”
- 这时候需要用自然语言处理技术来做拼写纠错等处理,以正确理解用户需求
第二步:分词技术
- 这一步利用自然语言处理技术将用户输入的查询语句进行分词
- 如标准分词会把“lucene全文检索框架”分成 lucene | 全 | 文|检|索|框|架|
- IK分词会分成: lucene|全文|检索|框架|,还有简单分词等多种分词方法
第三步:关键词检索
- 提交关键词后在倒排索引库中进行匹配,倒排索引就是关键词和文档之间的对应关系,就像给文档贴上标签
- 比如在文档集中含有 "lucene" 关键词的有文档1 、文档 6、文档9
- 含有 "全文检索" 关键词的有文档1 、文档6 那么做与运算
- 同时含有 "lucene" 和 "全文检索" 的文档就是文档1和文档6,在实际的搜索中会有更复杂的文档匹配模型
第四步:搜索排序
- 对多个相关文档进行相关度计算、排序,返回给用户检索结果
# 2、倒排索引
- 在构建索引时,首先会对文档进行分词处理,得到一系列的词项
- 然后将这些词项添加到词项字典中,并更新相关的Posting List
- 在此过程中,还会更新Term Index,以保证可以快速定位到词项字典中的词项
- 在进行搜索时,首先会在Term Index中查找词项
- 然后通过Term Dictionary找到对应的Posting List,从而找到包含该词项的所有文档
# 3、ES 创建索引
# 0、索引不变性
索引的不变性
- 由于倒排索引的结构特性,在索引建立完成后对其进行修改将会非常复杂
- 再加上几层索引嵌套,更让索引的更新变成了几乎不可能的动作
- 所以索性设计成不可改变的:倒排索引被写入磁盘后是不可改变的,它永远不会修改
不变性有重要的价值
- 不需要锁
- 如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性
- 只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘
- 其它缓存(像filter缓存),在索引的生命周期内始终有效
- 它们不需要在每次数据改变时被重建,因为数据不会变化
- 不需要锁
# 1、动态更新索引
更新索引原理
怎样在保留不变性的前提下实现倒排索引的更新?答案是: 用更多的索引
通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引
每一个倒排索引都会被轮流查询到,从最早的开始,查询完后再对结果进行合并
Lucene 段(Segment)
在Lucene中,索引是以段(Segment)为单位进行管理的
一个段就是一个倒排索引,包含了一部分文档的全部信息
每当有新的文档写入时,Lucene会创建一个新的段
段一旦创建,就不会再改变,也就是说,段内部的信息是不可变的
这种设计使得Lucene在写入性能上有很大优势
多个 segment 也会不断的被 merge 成一个大的 segment
- 在老的 segment 还有查询在读取的时候,不会被删除
- 没有被读取且被 merge 的 segement 会被删除
ES段搜索
- Elasticsearch继承了Lucene的这种设计,并引入了按段搜索的概念
- 当我们进行搜索时,Elasticsearch会对每个段分别进行搜索,然后再将结果合并
- 这种按段搜索的方式,使得Elasticsearch可以充分利用多核CPU的优势,提高搜索效率
- 同时,由于段内部的信息是不可变的,所以可以在搜索过程中避免加锁,进一步提高性能
- 举例
- 假设我们有一个包含1亿篇文档的大型索引,这个索引被分成了10个段,每个段包含1000万篇文档
- 当我们进行搜索时,Elasticsearch会启动10个线程,对这10个段分别进行搜索
- 每个线程会独立搜索自己所负责的段,互不干扰
- 当所有线程都搜索完成后,Elasticsearch会将这10个结果按照一定的规则合并,得到最终的搜索结果
- 这种按段搜索的方式,使得搜索可以并行进行,大大提高了搜索效率
Lucene 提交点(Commit Point)
提交点在Lucene中起到了版本控制和索引状态管理的作用
当索引发生变化时,例如添加、删除或更新了文档,Lucene会写入新的文件来反映这些变化
- 在Lucene中,索引数据是存储在多个不同的文件中的
- 假设你在初始状态下有三个索引文件:file1, file2, file3
- 然后你添加了一些文档,Lucene生成了新的文件file4
- commit后,Lucene生成新的提交点,这个提交点文件列表就是[file1, file2, file3, file4]
- 如果此后你再删除了一些文档,Lucene可能会生成新的文件file5
- 然后你又执行了commit操作,新的提交点的文件列表就可能是[file1, file2, file3, file4, file5]
提交点也被用来支持索引的版本控制
- Lucene可以保存多个提交点,每个提交点代表了索引在不同时间点的状态
- 这样,就可以回滚到旧的提交点,也就是说,可以恢复到旧的索引状态
# 2、ES写入流程
1)写入内存
- 数据先写入内存buffer,在写入buffer的同时将数据写入translog日志文件
- 注意:此时数据还没有被成功es索引记录,因此无法搜索到对应数据
2)写入磁盘
- refresh(写入到os cache)
- 如果buffer快满了或者到一定时间,es就会将buffer数据refresh到一个新的segment file中
- 但是此时数据不是直接进入segment file的磁盘文件,而是先进入os cache的(这个过程就是refresh)
- 一旦buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了
- 写入磁盘
- 每隔1秒钟,es将buffer中的数据写入一个新的segment file
- 这个segment file中就存储最近1秒内buffer中写入的数据
- 只要数据被输入os cache中,buffer就会被清空
- 并且数据在translog日志文件里面持久化到磁盘了一份
- 此时就可以让这个segment file的数据对外提供搜索了
- refresh(写入到os cache)
3)commit操作
重复1~2步骤
新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去
每次refresh完,buffer就会被清空,同时translog保留一份日志数据
随着这个过程推进,translog文件会不断变大
当translog文件达到一定程度时,就会执行commit操作
commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer
4)commit完成
将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file
同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去
将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成
translog日志文件的作用是什么?
在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中
无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了
因此需要将数据对应的操作写入一个专门的日志文件,也就是translog日志文件
一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去
综上可以看出
- es是准实时的,因此数据写入1秒后才可以搜索到
- 如果translog是异步写入的话,es可能会丢失数据
# 3、ES名称解释
translog
写入ES的数据首先会被写入translog文件,该文件持久化到磁盘
保证服务器宕机的时候数据不会丢失,由于顺序写磁盘,速度也会很快
同步写入
- 每次写入请求执行的时候,translog在fsync到磁盘之后,才会给客户端返回成功
异步写入
- 写入请求缓存在内存中,每经过固定时间之后才会fsync到磁盘,写入量很大
- 对于数据的完整性要求又不是非常严格的情况下,可以开启异步写入
refresh
- 经过固定的时间,或者手动触发之后,将内存中的数据构建索引生成segment,写入文件系统缓冲区
commit/flush
超过固定的时间,或者translog文件过大之后,触发flush操作
内存的buffer被清空,相当于进行一次refresh
文件系统缓冲区中所有segment刷写到磁盘
将一个包含所有段列表的新的提交点写入磁盘
启动或重新打开一个索引的过程中使用这个提交点来判断哪些segment隶属于当前分片
删除旧的translog,开启新的translog
merge
每次refresh时,会在文件系统缓冲区中生成一个segment,后续flush触发的时候持久化到磁盘
随着数据的写入,尤其是refresh的时间设置的很短的时候,磁盘中会生成越来越多的segment
segment数目太多,每一个segment都会消耗文件句柄、内存和cpu运行周期
更重要的是,每个搜索请求都必须轮流检查每个segment,所以segment越多,搜索也就越慢
merge的过程大致描述如下
磁盘上两个小segment:A和B,内存中又生成了一个小segment:C
A,B被读取到内存中,与内存中的C进行merge,生成了新的更大的segment:D
触发commit操作,D被fsync到磁盘
创建新的提交点,删除A和B,新增D
# 4、文档更新与删除
删除
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新
磁盘上的每个segment都有一个.del文件与它相关联
当发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除
此文档可能仍然能被搜索到,但会从结果中过滤掉
当segment合并时,在.del文件中标记为已删除的文档不会被包括在新的segment中
也就是说merge的时候会真正删除被删除的文档
更新
- 创建新文档时,Elasticsearch将为该文档分配一个版本号
- 对文档的每次更改都会产生一个新的版本号
- 当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的segment中写入索引
- 旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉
# 5、并发控制
# 1、ES乐观锁
- Elasticsearch主要使用乐观并发控制,每个文档都有一个版本号,当文档被修改时,版本号会自动递增
- 我们可以在更新文档时指定版本号,如果指定的版本号和服务器上的版本号不匹配,那么更新操作就会失败
- 这种机制相当于实现了乐观锁,假设冲突不会经常发生,只在发生冲突时才处理
# 2、ES悲观锁(无)
- Elasticsearch的设计是无锁的,它并不支持传统意义上的悲观锁
- 也就是说,它不会在数据被访问时就加锁
- 但是,通过使用特定的API和字段(如
_seq_no
和_primary_term
) - 可以实现类似悲观锁的效果,这需要在应用层面进行处理
# 6、批量操作
bulk API 允许在单个步骤中进行多次 create 、 index 、 update 或 delete 请求
如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次
bulk 请求不是原子的: 不能用它来实现事务控制
每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求
整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少
批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降
但是最佳值不是一个固定的值
它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况
# 7、ES数据类型
Elasticsearch 支持如下简单域类型
字符串:
string
整数 :
byte
,short
,integer
,long
浮点数:
float
,double
布尔型:
boolean
日期:
date
映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串
Elasticsearch 需要知道每个域中数据的类型,这个信息包含在映射中
索引中每个文档都有 类型 ,每种类型都有它自己的 映射 ,或者 模式定义
映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域
映射也用于配置与类型有关的元数据。
当你索引一个包含新域的文档(之前未曾出现),Elasticsearch 会使用 动态映射
通过JSON中基本数据类型,尝试猜测域类型
使用如下规则
# 02.ES新增Doc过程介绍
# 1、ES增加Doc过程(新增一条数据)
第一步:插入一条数据
- 插入一行数据Doc,Doc会先被搜索集到内存中的Buffer内(这个时候还不能搜索到)
- 因为当前数据还是在内存的Buffer中,还没有写进去
第二步:创建新段,写入磁盘
- 创建一个新段(Segment),作为一个追加的倒排索引,写入到磁盘(文件系统缓存)
- 将包含新段提交点(Commit Point)写入磁盘(文件系统缓存)
- 每隔一段时间将buffer提交,在flush磁盘后打开新段是的搜索可见
第三步:默认间隔1秒频繁refresh
- 默认间隔1秒频繁refresh,创建新的段并打开他们
- 可以使文档在没有完全刷入硬盘的状态下就能对搜索可见
- 这是ES被称为近实时搜索的原因(而且是一个开销小的操作)
第四:避免两次commit操作间隔发生异常Doc丢失
- 为了避免两次commit操作间隔发生异常Doc丢失(两次refresh更新)
- ES中采用了一个translog(事务日志)的概念
- 记录每次对ES操作的事务日志,还记录了尚未被flush到磁盘的操作
- 默认每5秒或者是每次请求增删改查完成后,就会写入translog并被fsysc到磁盘(在主分配和副分片都会)
# 2、ES删除Doc过程(删除、合并、更新)
ES Doc删除
- 删除一个ES文档不会立即从磁盘中删除,它只是被标记成已删除
- 因为段是不可变的,所以文档既不能从久文档中移除,旧的段也不能更新以反映文档的版本
ES Doc的合并
- 通过每秒自动刷新创建新的段,很快的数量就越来越多,每个段消费大量资源
- 每次搜索请求都需要依次检查每个段,段越多查询越慢
- ES利用段合并在后台选择一些小的段合并成大的段
- 合并后新的段可以被搜索,就的段被删除
ES Doc的更新
- 当一个文档被更新,旧版的文档被标记为删除,新版本的文档在新的段中索引
- 该文档的不同版本都会匹配一个查询,但是较旧的版本会从结果中删除