深入理解 Elasticsearch

Elasticsearch 是一个基于 Lucene 的 Java 开发的企业级搜索引擎,是一个分布式、可扩展、实时的搜索与数据分析引擎,它能从项目一开始就赋予你的数据以搜索、分析和探索的能力,基于 RESTful web 接口。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。本篇主要讲解 Elasticsearch 的相关原理。

1.倒排索引与分词

1.倒排索引

正排索引,即文档 id 到文档内容、单词的关联关系:

文档 id文档内容
10001elasticsearch是最流行的搜索引擎
10002深入jvm运行原理
10003搜索引擎的发展历史

倒排索引,单词到文档 id 的关联关系:

单词文档 id
elasticsearch10001
流行10001
搜索引擎10001,10003
深入10002
jvm10002
运行10002
原理10002
发展10003
历史10003

那倒排索引是如何产生的呢?是通过将正排索引进行了分词。

倒排索引的查询流程:

  1. 通过倒排索引获得单词对应的一个或多个文档 id;
  2. 通过正排索引查询文档 id 的完整内容;
  3. 返回用户最终结果;

倒排索引是搜索引擎的核心,主要包含两部分:

-说明
单词词表(Term Dictionary)单词字典的实现一般是用 B+ Tree(查询和插入性能是非常高的,能充分的利用磁盘和内存的映射机制)
倒排列表(Posting List)记录了单词对应的文档集合,由倒排索引项(Posting)组成
2.分词

分词(Analysis)是指将文本转换成一系列单词(term or token)的过程,也可以叫做文本分析。分词器(Analyzer)是 ES 中专门处理分词的组件,它的组成如下:

-说明
Character Filters针对原始文本进行处理(比如去除 html 特殊标记符)
Tokenizer将原始文本按照一定规则切分为单词
Token Filters针对 Tokenizer 处理的单词进行再加工,比如转小写(比如将英文单词都转为小写,这样不管输入大小写都能匹配到)、删除(比如删除中文里的 “的”、“着”、“那” 等 stop word)或新增(比如新增近义词、同义词)等处理

分词器这三个部分的调用顺序是:Character Filters -> Tokenizer -> Token Filters

3.Match Query的流程

Match Query(全文匹配 )的流程:
在这里插入图片描述

4.相关性算分

相关性算分(relevance)是指文档与查询语句间的相关度。

通过倒排索引可以获取与查询语句相匹配的文档列表,那么如何将最符合用户查询需求的文档放在前列呢?本质上是一个排序问题,排序的依据是相关性算分。相关性算分的几个重要概念如下:

概念说明
Term Frequency(TF,词频)单词在该文档中出现的次数,TF 越高,相关度越高
Document Frequency(DF,文档频率)单词出现的文档数
Inverse Document Frequency(IDF,逆向文档频率)与 DF 相反,简单理解为 1/DF,即单词出现的文档数越少,相关度越高
Field-length Norm文档越短,相关度越高

ES 目前主要有两个相关性算分模型:

概念说明
TF/IDF 模型Lucene 的经典模型
BM25 模型ES 5.x 版本之后的默认模型,BM指Best Match,25指迭代了25次才计算方法,是针对TF/IDF的一个优化。BM25相比TF/IDF的一大优化是降低了TF在过大时的权重

我们在进行查询时,可以通过 explain 参数来查看具体的计算方法,但要注意:

  • ES 的算分是按照 shard (分片)进行的,即 shard 的分数计算是相互独立的,所以在使用 explain 的时候注意分片数。可以通过设置自索引的分片数为 1(这样所有文档就存在一个 shard 上了)来避免这个问题。

比如可以使用 Kibana DevTools 输入命令:

POST /book/_search
{
    "explain":true,
	"query":{
		"match": {"title": "细说kibana"}
	}
}

2.分布式特性

ES 支持集群模式,是一个分布式系统,每个 ES 实例本质上是一个 JVM 进程。

这里推荐一款 ES 开源监控工具:cerebro

一些概念:

概念说明
Cluster StateES集群相关的数据称为Cluster State,主要记录节点信息(节点名称、连接地址),索引信息(索引名称、配置)等。
master节点可以修改cluster state的节点,一个集群只能有一个;cluster state存储在每个节点上,master维护最新版本并同步给其他节点;master节点是通过集群中所有节点选举产生的,可以被选举的节点称为master-eligible节点(配置node.master:true,默认true)
Coordinating Node处理请求的节点称为Coordinating Node,该节点为所有节点的默认角色,不能取消。Coordinating的作用就是路由请求到正确的节点处理,比如创建索引的请求到master节点
Data Node存储数据的节点称为data节点(配置node.data:true,默认true)
1.副本与分片

在这里插入图片描述

  • 分片存储了部分数据,可以分布于任意节点上
  • 分片数在索引创建时指定且后续不允许再更改,默认为5个
  • 分片有主分片和副本分片之分,以实现数据的高可用
  • 副本分片的数据由主分片同步,可以有多个,可以动态调整,从而提高读取的吞吐量

创建索引时我们指定了 3 个分片和 1 个副本(总共会是 6 个分片):

PUT /book
{
	"settings": {
		"number_of_shards": "3",
		"number_of_replicas": "1"
	}
}

在这里插入图片描述
其中,node-1 是 master 节点,P0、P1、P2 是主分片,R1、R2、R0 是副本分片。

2.集群状态

ES 可以通过如下命令查看集群的健康状态:

GET _cluster/health

  • green:健康状态,指所有主副分片都正常分配
  • yellow:指所有主分片都正常分配,但是有副本分片未正常分配
  • red:有主分片未分配
2.故障转移

故障转移指当集群其中有节点发生问题的时候,集群如何自动的去修复这些问题。

在这里插入图片描述
问题1:如果 node-1(master 节点)所在机器宕机导致服务终止,此时集群会如何处理?

答:node-2 和 node-3 会定时去 ping node-1 的,发现 node-1 无法响应一段时间后会发起 master 选举:

在这里插入图片描述
比如这里选举出 node-2 为新的 master 节点。此时由于主分片 P0 下线,集群状态变为 Red:

在这里插入图片描述
node-2 发现主分片 P0 未分配,将 R0 提升为 主分片,此时由于所有主分片都正常分配,集群状态变为 Yellow:
在这里插入图片描述
然后 node-2 会为 P0 和 P1 生成新的副本,集群状态变为绿色:
在这里插入图片描述

3.文档分布式存储

文档最终会存储在分片上。
在这里插入图片描述
我们假设文档 doc1 最终存储在分片 P1 上,在 P1 和 R1 副本都会有这个文档。

问题1:doc1 是如何存储到分片 P1 的?选择 P2 的依据是什么?

答:其实是有一个文档到分片的映射算法,这个算法使得文档均匀分布在所有分片上,以充分利用资源。ES 是通过如下的公式计算文档对应的分片

shard = hash(routing) % number_of_primary_shards

hash 算法可以保证可以将数据均匀地分散在分片中,routing 是一个关键参数,默认是文档 id,也可以自行指定,number_of_primary_shards 是主分片数。该算法与主分片数相关,这也是分片数一旦确定后便不能更改的原因。

一个完整的文档创建流程如下:

  1. client 向 node-3 发起创建文档的请求;
  2. node-3 通过 routing 计算该文档应该存储在 P1 上,查询 cluster state 后确认主分片 P1 在 node-2 上,然后转发创建文档的请求到 node-2;
  3. P1 接收并执行创建文档请求后,将同样的请求发送到副本分片 R1;
  4. R1 接收并执行创建文档请求后,通知 P1 成功的结果;
  5. P1 接收副本分片结果后,通知 node-3 创建成功;
  6. node-3 返回结果到 client。

一个完整的文档读取流程如下:

  1. client 向 node-3 发起获取文档 doc1 的请求;
  2. node-3 通过 routing 计算该文档在 P1 上,查询 cluster state 后获取 P1 的主副分片列表,然后以轮询的机制获取一个分片,比如这里获取到了 R1,然后转发读取文档的请求到 node-1;
  3. R1 接收并执行读取文档请求后,将结果返回 node-3;
  4. node-3 返回结果给 client。

文档批量创建的流程如下:

  1. client 向 node-3 发起批量创建文档的请求 (bulk);
  2. node-3 通过 routing 计算所有文档对应的分片,然后按照主分片分配对应执行的操作,同时发送请求到涉及的主分片,比如这里 3 个主分片都需要参数;
  3. 主分片接收并执行请求后,将同样的请求同步到对应的副本分片;
  4. 副本分片执行结果后返回结果到主分片,主分片再返回 node-3;
  5. node-3 整合结果后返回 client。

文档批量读取的流程如下:

  1. client 向 node-3 发起批量获取文档的请求 (mget);
  2. node-3 通过 routing 计算所有文档对应的分片,然后以轮询的机制获取要参与的分片,按照分片构建 mget 请求,同时发送请求到涉及的分片,比如这里有 2 个分片需要参与;
  3. R1、R2 返回文档结果;
  4. node-3 返回结果给 client。
4.脑裂问题

脑裂问题(split-brain)是分布式系统中的经典网络问题,如下图所示,3 个节点组成的集群,突然 node-1 的网络和其他两个节点中断:
在这里插入图片描述
这个时候,node-2 和 node-3 会重新选举 master,比如 node-2 成为了新 master,此时会更新 cluster state,node-1 自己组成集群后,也会更新 cluster state。这就出现了一个问题,同一个集群有两个 master,而且维护不同的 cluster state,网络恢复后无法选择正确的 master。

脑裂问题解决方案:

仅在可选举 master-eligible 节点数大于等于 quorum 时才可以进行 master 选举。

  • quorum = master - eligible 节点数 / 2 + 1,例如 3 个 master-eligible 节点时,quorum 为 2;
  • 设定 discovery.zen.minimum_master_nodes 为 quorum 即可避免脑裂。

发生网络隔离后,node-1 可选举数未达到 2,不选举,node-2 和 node-3 可选举数为 2,重新选举 master。待网络恢复后,node-1 可正常加入集群

5.倒排索引的不可变更

倒排索引一旦生成,不能更改,其好处如下:

  • 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题;
  • 由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,对该文件的读取都会从内存读取,性能高;
  • 利于生成缓存数据;
  • 利用对文件进行压缩存储,节省磁盘和内存存储空间。

坏处:

  • 需要写入新文档时,必须重新构建倒排索引文件(全部文档),然后替换老文件后,新文档才能被检索,导致文档实时性差。
6.文档搜索实时性

写入新文档时,必须重新构建倒排索引文件(全部文档),导致文档实时性差。

这个问题是有解决方案的:

新文档直接生成新的倒排索引文件(只是新文档),查询的同时查询所有的倒排文件,然后做结果的汇总计算即可。

  • Lucene 便是采用了这种方案,它构建的单个倒排索引称为 segment,合在一起称为 Index,与 ES 中的 Index 概念不同。ES 中的一个分片对应一个 Lucene Index。
  • Lucene 会有一个专门的文件来记录所有的 segment 信息,称为 commint point。
1.refresh
  • segment 写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将 segment 在缓存中创建并开放查询来进一步提升实时性,该过程在 ES 中被称为 refresh;
  • 在 refresh 之前文档会先存储在一个 buffer 中,refresh 时将 buffer 中的所有文档清空并生成 segment;
  • ES 默认每一秒执行一次 refresh,因此文档的实时性被提高到 1 秒,这也是 ES 被称为近实时 (Near Real Time) 的原因。

refresh 发生的时机主要有如下几种情况:

  • 间隔时间达到时,通过 index.settings.refresh_interval 来设定,默认是1秒;
  • index.buffer 占满时,其大小通过 indices.memory.index_buffer_size 设置,默认为 jvm heap 的 10%,所有分片共享;
  • flush 发生时也会发生 refresh。
2.translog

如果在内存中的 segment 还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?

  • ES 引入 translog 机制。写入文档到 buffer 时,同时将该操作写入 translog;
  • translog 文件会即时写入磁盘 (fsync),ES 6.x 版本默认每个请求都会落盘,可以修改为每 5 秒写一次,这样风险便是丢失 5 秒内的数据,相关配置为 index.translog.*;
  • ES 启动时会检查 translog 文件,并从中恢复数据。
3.flush

flush 负责将内存中的 segment 写入磁盘,主要做如下的工作:

  • 将 translog 写入磁盘;
  • 将 index buffer 清空,其中的文档生成一个新的 segment,相当于一个 refresh 操作
  • 更新 commit point 并写入磁盘;
  • 执行 fsync 操作,将内存中的 segment 写入磁盘
  • 删除旧的 translog 文件。

flush 发生的时机主要有如下几种情况:

  • 间隔时间达到时,默认是30分钟,ES 5.x 之后版本无法修改;
  • translog 占满时,其大小可以通过 index.translog.flush_threshold_size 控制,默认是 512MB,每个 index 有自己的 translog。
4.删除与更新文档

segment 一旦生成就不能更改,那么如果你要删除文档该如何操作?

  • Lucene 专门维护一个 .del 的文件,记录所有已经删除的文档,注意 .del 上记录的是文档在 Lucene 内部的 id;
  • 在查询结果返回前会过滤 .del 中的所有文档。

更新文档如何进行呢?

  • 首先删除文档,然后再创建新文档。
5.Segment Merging
  • 随着 segment 的增多,由于一次查询的 segment 数增多,查询速度会变慢;
  • ES 会定时在后台进行 segment merge的操作,减少 segment 的数量;
  • 通过 force_merge api 可以手动强制做 segment merge 的操作。

3.search运行机制

1.Query-Then-Fetch

search 执行的时候实际分两个步骤运作的:

  • Query 阶段;
  • Fetch 阶段。

在这里插入图片描述
node-3 在接收到用户的 search 请求后,会先进行 Query 阶段(此时是 Coordinating Node 角色):

  1. node-3 在 6 个主副分片中随机选择 3 个分片(涵盖了 Index 完整的数据),发送 search request;
  2. 被选中的 3 个分片会分别执行查询并排序,返回 from + size 个文档 id 和排序值给 node-3;
  3. node-3 整合 3 个分片返回的 form + size 个文档 id,根据排序值排序后选取 from 到 from + size 的文档 id;

Fetch 阶段,node-3 根据 Query 阶段获取的文档 id 列表去对应的 shard 上获取文档详情数据;

  1. node-3 向相关的分片发送 multi_get 请求;
  2. 3 个分片返回文档详细数据;
  3. node-3 拼接返回的结果并返回给客户。
2.相关性算分
  • 相关性算分在 shard 与 shard 间是相互独立的,也就意味着同一个 Term 的 IDF 等值在不同 shard 上是不同的。文档的相关性算分和它所处的 shard 相关;
  • 在文档数量不多时,会导致相关性算分严重不准的情况发生。

解决思路有两个:

  • 一是设置分片数为 1,从根本上排除问题,在文档数量不多的时候可以考虑该方案,比如百万到千万级别的文档数量;
  • 二是使用 DFS Query-Then-Fetch 查询方式。

DFS Query-Then-Fetch 是在拿到所有文档后再重新完整的计算一次相关性算分,耗费更多的 cpu 和内存,执行性能也比较低下,一般不建议使用。使用方式如下:

POST /book/_search?search_type=dfs_query_then_fetch
{
	"query":{
		"match": {"title": "细说kibana"}
	}
}

3.排序

ES 默认会采用相关性算分排序,用户可以通过设定 sorting 参数来自行设定排序规则。

  • 排序的过程实质上是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用,需要用到正排索引也就是通过文档 id 和字段可以快速得到字段原始内容;
  • ES 对此提供了两种实现方式:fielddata 默认禁用;doc values 除了text类型默认启用。
对比fielddatadoc values
创建时机搜索时即时创建索引时创建,与倒排索引创建时机一致
创建位置JVM Heap磁盘
优点不会占用额外的磁盘资源不会占用 Heap 内存
缺点文档过多时,即时会花过多时间,占用过多 Heap 内存减慢索引的速度,占用额外的磁盘空间

fielddata 默认是关闭的,可以通过如下 API 开启:

PUT /book/_mapping/doc
{
	"properties":{
		"title": {"type":"text", "fielddata":true}
	}
}

  • 此时字符串是按照分词后的 term 排序,往往结果很难符合预期;
  • 一般是在对分词做聚合分析的时候开启。

doc values 默认是启用的,可以在创建索引的时候关闭:

PUT /book
{
	"mappings":{
		"doc":{
		    "properties":{
		        "title":{"type":"text", "doc_values":false}
		    }
		}
	}
}

  • 如果后面要再开启 doc values,需要做 reindex 操作。

可以通过 docvalue fields 字段获取 fielddata 或 doc values 中存储的内容:

POST /book/_search
{
	"docvalue_fields":[
		"title.keyword",
		"desc.keyword",
		"country"
	]
}

4.分页与遍历

ES 提供了 3 种方式来解决分页与遍历的问题:

  • from/size
  • scroll
  • search_after
类型场景
from/size最常用的分页方案,需要实时获取顶部的部分文档,且需要自由翻页
scroll需要全部文档,如导出所有数据的功能
search_after需要全部文档,不需要自由翻页
1.from/size

from/size 是最常用的分页方案,from 表示开始位置,size 表示大小。

深度分页是一个经典的问题:在数据分片存储的情况下如何获取前 1000 个文档?

  • 获取从 990-1000 的文档时,会在每个分片上都先获取 1000 个文档,然后再由 Coordinating Node 聚合所有分片的结果后再排序选取前 1000 个文档;
  • 页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,ES 通过 index.max_result_window 限定最多到 10000 条数据。
2.scroll

scroll 是遍历文档的 API,以快照的方式来避免深度分页的问题。

  • 不能用来做实时搜索,因为数据不是实时的;
  • 尽量不要使用复杂的 sort 条件,使用 _doc 最高效;
  • 使用稍嫌复杂。

第一步需要发起 1 个 scroll search,如下所示:

  • ES 在收到该请求后会根据查询条件创建文档 id 合集的快照
POST /book/_search?scroll=5m    //scroll=5m 指该scroll快照的过期时间
{
	"size":1                    //指明每次scroll返回的文档数
}

ES 返回参数:

{
    "_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAyqNFjlHdVJROUFQUkN5MndRQ3NfRHpWaVEAAAAAAAJi2BYzM2VaaUt6N1RuQzF0UGFKUHlBNW9nAAAAAAAE8SwWZEpJeWpfcnJSWkNYcS10cXc5MTdXQQAAAAAAAmLZFjMzZVppS3o3VG5DMXRQYUpQeUE1b2cAAAAAAATxKxZkSkl5al9yclJaQ1hxLXRxdzkxN1dB",
    ...
}

_scroll_id 就是后续调用 scroll api 时的参数。

第二步调用 scroll search 的 api,获取文档集合,如下所示:

  • 不断迭代调用直到返回 hits.hits 数组为空时停止
POST /_search/scroll
{
	"scroll":"5m",             //快照的过期时间
	"scroll_id":"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAyqNFjlHdVJROUFQUkN5MndRQ3NfRHpWaVEAAAAAAAJi2BYzM2VaaUt6N1RuQzF0UGFKUHlBNW9nAAAAAAAE8SwWZEpJeWpfcnJSWkNYcS10cXc5MTdXQQAAAAAAAmLZFjMzZVppS3o3VG5DMXRQYUpQeUE1b2cAAAAAAATxKxZkSkl5al9yclJaQ1hxLXRxdzkxN1dB" //第一步返回的_scroll_id
}

ES 返回参数:

{
    "_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAyqNFjlHdVJROUFQUkN5MndRQ3NfRHpWaVEAAAAAAAJi2BYzM2VaaUt6N1RuQzF0UGFKUHlBNW9nAAAAAAAE8SwWZEpJeWpfcnJSWkNYcS10cXc5MTdXQQAAAAAAAmLZFjMzZVppS3o3VG5DMXRQYUpQeUE1b2cAAAAAAATxKxZkSkl5al9yclJaQ1hxLXRxdzkxN1dB",
    "hits" : {
    	"total" : 10,
    	"max_score" : 1.0,
   		"hits" : [
   			...
   		]
   	},
    ...
}

_scroll_id 下一次调用时使用到的 scroll_id。

过多的 scroll 调用会占用大量内存,可以通过 clear api 删除过多的 scroll 快照:

DELETE /_search/scroll
{
	"scroll_id":[
		"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAyqNFjlHdVJROUFQUkN5MndRQ3NfRHpWaVEAAAAAAAJi2BYzM2VaaUt6N1RuQzF0UGFKUHlBNW9nAAAAAAAE8SwWZEpJeWpfcnJSWkNYcS10cXc5MTdXQQAAAAAAAmLZFjMzZVppS3o3VG5DMXRQYUpQeUE1b2cAAAAAAATxKxZkSkl5al9yclJaQ1hxLXRxdzkxN1dB"
	]
}

DELETE /_search/scroll/_all

3.search_after

避免深度分页的性能问题,提供实时的下一页文档获取功能。

  • 缺点是不能使用 from 参数,即不能指定页数;
  • 只能下一页,不能上一页
  • 使用简单

第一步为正常的搜索,但要指定 sort 值,并保证值唯一:

POST /book/_search
{
	"size":1,
	"sort":{
		"word_count":"desc",       //要保障sort值唯一
		"_id":"desc"
	}
}

ES 返回参数:

{
  "hits" : {
    "hits" : [
      {
        "_id" : "100021",
        ...,
        "sort" : [
          20000,
          "100021"
        ]
      }
    ], ...
  }, ...
}

第二步使用上一步最后一个文档的 sort 值进行查询:

POST /book/_search
{
	"size":1,
	"search_after":[20000,"100021"],
	"sort":{
		"word_count":"desc",
		"_id":"desc"
	}
}

search_after 是如何避免深度分页问题的?

  • 通过唯一排序值定位将每次要处理的文档数都控制在 size 内。

常见问题

1. 使用场景和数据一致性?

典型应用场景:电商商品搜索

es本来就不是做精确检索的,数据肯定是冗余了一份的,所以保证最终一致性就好。

要最终一致性,肯定也不能直接就写到es,一定要有类似mq的机制做削峰,自然需要kafka之类的。这个你肯定没办法保证一定是实时的。

2. ES和Redis互为替代?

es和redis也不能互相替代,我理解你的场景是精确检索某个key,实际上es能做的比这个多得多,比如全文检索需要对词语做分词,用ik分词插件,可以讲词语做分词,这样才可以根据关键词,按相关度做搜索。

3. 数据同步(比如mysql和ES的数据同步)

具体步骤如下:

1) 读取mysql的binlog日志,获取指定表的日志信息;

2) 将读取的信息转为MQ;

3) 编写一个MQ消费程序;

4) 不断消费MQ,每消费完一条消息,将消息写入到ES中。

方案优点:

  • 没有代码侵入、没有硬编码;原有系统不需要任何变化,没有感知;性能高;业务解耦,不需要关注原来系统的业务逻辑。

方案缺点

  • 构建Binlog系统复杂;存在MQ延时的风险

参考链接:
基于 MySQL Binlog 的 Elasticsearch 数据同步实践

mysql数据实时同步到Elasticsearch

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值