elasticSearch核心知识

153 阅读7分钟

什么是倒排索引

  • 文本 也就是document,一段文本,每个文本都赋予一个文本id
  • 分词 比如对 文本“i like you” 可以拆分为 “i”、“like”、“you”
  • 词项 也就是term,比如拆分后的 “i” 称为一个词项
  • 词典 也就是Term Dictionary,分词后的所有词项经过排序后 称为词典
  • Posting List 每个词项对应的 一组【文本id、文本中偏移量、词频】的集合

倒排索引 就是词典 + Posting List 的映射关系

1719060549588.jpeg

Term Index 是什么

我们可以发现,词项和词项之间,有些前缀是一致的,比如 follow 和 forward 前面的 fo 是一致的,如果我们将部分 term 前缀提取出来,像下图一样,就可以用更少的空间表达更多的 term

基于这个原理,我们可以将 Term Dictionary 的部分词项提取出来,用这些 词项 的前缀信息,构建出一个精简的目录树。目录树的节点中存放这些词项在磁盘中的偏移量,也就是指向磁盘中的位置。这个目录树结构,体积小,适合放内存中,它就是所谓的 Term Index。可以用它来加速搜索。

1719060664024.jpeg

倒排索引是对文本的索引,而为了节省空间 Term Index 是对词项的索引目录。

当我们需要查找某个词项的时候,只需要搜索 Term Index,就能快速获得词项在 Term Dictionary 中的大概位置。再跳转到 Term Dictionary,通过少量的检索,定位到词项内容。

Stored Fields

查询过程 Term index -> Term Dicttionary -> Term -> Post List,到这里已经找到文档id及相对文档内容的偏移量和词频信息,Stored Fields 是存放完整文档内容的行式存储

1719060687244.jpeg

Doc Values

查询到内容后有时需要对 文档中某些Term进行排序,用户经常需要根据某个字段排序文档,比如按时间排序或商品价格排序。但问题就来了,这些字段散落在文档里。也就是说,我们需要先获取 Stored Fields 里的文档,再提取出内部字段进行排序。
也不是说不行。但其实有更高效的做法。

我们可以用空间换时间的思路,再构造一个列式存储结构,将散落在各个文档的某个字段,集中存放,当我们想对某个字段排序的时候,就只需要将这些集中存放的字段一次性读取出来,就能做到针对性地进行排序。这个列式存储结构,就是所谓的 Doc Values

1719060757527.jpeg

segment

在上面,我们介绍了四种关键的结构:倒排索引用于搜索,Term Index 用于加速搜索,Stored Fields 用于存放文档的原始信息,以及 Doc Values 用于排序和聚合。这些结构共同组成了一个复合文件,也就是所谓的"segment", 它是一个具备完整搜索功能的最小单元

1719060773326.jpeg

lucene

我们可以用多个文档生成一份 segment,如果新增文档时,还是写入到这份 segment,那就得同时更新 segment 内部的多个数据结构,这样并发读写时性能肯定会受影响。
那怎么办呢?
我们定个规矩。segment 一旦生成,则不能再被修改。如果还有新的文档要写入,那就生成新的 segment。
这样老的 segment 只需要负责读,写则生成新的 segment。同时保证了读和写的性能。 但 segment 变多了,我们怎么知道要搜索的数据在哪个 segment 里呢?
问题不大,并发同时读多个 segment 就好了。 1719060793882.jpeg 但这也引入了新问题,随着数据量增大,segment 文件越写越多,文件句柄被耗尽那是指日可待啊。
是的,但这个也好解决,我们可以不定期合并多个小 segment,变成一个大 segment,也就是段合并(segment merging)。这样文件数量就可控了。

1719060806214.jpeg

高性能

ucene 作为一个搜索库,可以写入大量数据,并对外提供搜索能力。
多个调用方同时读写同一个 lucene 必然导致争抢计算资源。抢不到资源的一方就得等待,这不纯纯浪费时间吗!
有解决方案吗?有!
首先是对写入 lucene 的数据进行分类,比如体育新闻和八卦新闻数据算两类,每一类是一个 Index Name,然后根据 Index Name 新增 lucene 的数量,将不同类数据写入到不同的 lucene 中,读取数据时,根据需要搜索不同的 Index Name 。这就大大降低了单个 lucene 的压力。

1719060861197.jpeg 但单个 Index Name 内数据依然可能过多,于是可以将单个 Index Name 的同类数据,拆成好几份,每份是一个 shard 分片每个 shard 分片本质上就是一个独立的 lucene 库
这样我们就可以将读写操作分摊到多个 分片 中去,大大降低了争抢,提升了系统性能。

1719060882499.jpeg

高扩展性

随着 分片 变多,如果 分片 都在同一台机器上的话,就会导致单机 cpu 和内存过高,影响整体系统性能。

于是我们可以申请更多的机器,将 分片 分散部署在多台机器上,这每一台机器,就是一个 Node。我们可以通过增加 Node 缓解机器 cpu 过高带来的性能问题。

1719060916626.jpeg

高可用

到这里,问题又又来了,如果其中一个 Node 挂了,那 Node 里所有分片 都无法对外提供服务了。我们需要保证服务的高可用。有解决方案吗?
有,我们可以给 分片 多加几个副本。将 分片 分为 Primary shard 和 Replica shard,也就是主分片和副本分片 。主分片会将数据同步给副本分片,副本分片既可以同时提供读操作,还能在主分片挂了的时候,升级成新的主分片让系统保持正常运行,提高性能的同时,还保证了系统的高可用

1719060935206.jpeg

ES 是什么?

lucene + 三高特性(数据分片、多节点、副本机制)的支持

1719060991824.jpeg

ES 和 Kafka 的架构差异

  • es 中用于分类消息的 Index Name,其实就是 kafka 的 topic
  • es 中用于对 Index Name 数据分片的 Shard,其实就是 kafka 中拆分 topic 数据的 Partition
  • es 中用于分散部署多个 shard 的 Node, 其实就相当于 kafka 中的 broker

es 的架构跟 kafka 以及我们上期聊过的 rocketMQ 都非常相似,其实对于目前三高的处理都大同小异。

ES的写入流程

request -> 协调节点 -> 主分片 -> 同步副本并获得ack -> response

1719061035966.jpeg

ES的搜索流程

查询阶段
  • 客户端应用发起搜索请求,请求会先发到集群中的协调节点
  • 协调节点根据 index name 的信息,可以了解到 index name 被分为了几个 分片,以及这些分片 分散哪个数据节点上,将请求转发到这些数据节点的 分片 上面。
  • 搜索请求到达分片后,分片 底层的 lucene 库会并发搜索多个 segment,利用每个 segment 内部的倒排索引获取到对应文档 id,并结合 doc values 获得排序信息。分片将结果聚合返回给协调节点
  • 协调节点对多个分片中拿到的数据进行一次排序聚合舍弃大部分不需要的数据。

1719061062084.jpeg

获取阶段
  • 协调节点再次拿着文档 id 请求数据节点里的 分片,分片 底层的 lucene 库会从 segment 内的 Stored Fields 中取出完整文档内容,并返回给协调节点。
  • 协调节点最终将数据结果返回给客户端。完成整个搜索过程。

1719061075092.jpeg

总结

  • lucene 是 es 底层的单机文本检索库,它由多个 segment 组成,每个 segment 其实是由倒排索引、Term Index、Stored Fields 和 Doc Values 组成的具备完整搜索功能的最小单元。
  • 将数据分类,存储在 es 内不同的 Index Name 中。
  • 为了防止 Index Name 内数据过多,引入了 Shard 的概念对数据进行分片。提升了性能。
  • 将多个 shard 分布在多个 Node 上,根据需要对 Node 进行扩容,提升扩展性。
  • 将 shard 分为主分片和副本分片,主分片挂了之后由副本分片顶上,提升系统的可用性。
  • 对 Node 进行角色分化,提高系统的性能和资源利用率,同时简化扩展和维护。
  • es 和 kafka 的架构非常像