Elasticsearch 进阶
核心概念
索引(Index)
一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。
能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。
Elasticsearch 索引的精髓:一切设计都是为了提高搜索的性能。
类型(Type)
在一个索引中,你可以定义一种或多种类型。
一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化。
版本 | Type |
5.x | 支持多种 type |
6.x | 只能有一种 type |
7.x | 默认不再支持自定义索引类型(默认类型为:_doc) |
文档(Document)
一个文档是一个可被索引的基础信息单元,也就是一条数据比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。
在一个 index/type 里面,你可以存储任意多的文档。
字段(Field)
相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
映射(Mapping)
mapping 是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,其它就是处理ES 里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。
分片(Shards)
一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档数据的索引占据 1TB 的磁盘空间,而任一节点都可能没有这样大的磁盘空间。或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点
分片很重要,主要有两方面的原因:
- 允许你水平分割/扩展你的内容容量。
- 允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量,至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。
- 被混淆的概念是,一个Lucene索引我们在Elasticsearch称作分片。一个Elasticsearch 索引是分片的集合。当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集。
副本(Replicas)
在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elastigsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
复制分片之所以重要,有两个主要原因:
- 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(originalprimary)分片置于同一节点上是非常重要的
- 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
分配(Allocation)
将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由master 节点完成的
分布式集群
单节点集群
我们在包含一个空节点的集群内创建名为users 的索引,为了演示目的,我们将分配3个主分片和一份副本(每个主分片拥有一个副本分片)
{
"settings": {
//主分片
"number_of_shards": 3,
//副本分片
"number_of_replicas" : 1
}
}
表示当前集群的全部主分片都正常运,但是副本分片没有全部处在正常状态 | |
3个主分片正常 | |
3个副本分片都是Unassigned -- 它们都没有被分配到任何节点。在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。 |
故障转移
当集群中只有一个节点在运行时,意味着会有一个单点故障问题--没有冗余。幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 custer.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。
如果启动了第二个节点,我们的集群将会拥有两个节点的集群:所有主分片和副本分片已被分配。
表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行。 | |
3个主分片正常 | |
当第二个节点加入到集群后,3个副本分片将会分配到这个节点上--每个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。 |
水平扩容
怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群:为了分散负载而对分片进行重新分配。
启动node3,通过elasticsearch-head插件查看状态
表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行。 | |
Node1和 Node2 上各有一个分片被迁移到了新的 Node3 节点,现在每个节点上都拥有2个分片而不是之前的3个。 这表示每个节点的硬件资源(CPU,RAM,IO)将被更少的分片所共享,每个分片的性能将会得到提升。分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。 |
但是如果我们想要扩容超过6个节点怎么办呢?
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作--搜索和返回数据--可以同时被主分片或副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的1增加到2。
{
"number_of_replicas" : 2
}
应对故障
我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群。
我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点:Node3。在我们关闭 Node1的同时也失去了主节点地位。
重启Node1,这时集群的状态为:恢复三个节点,主节点从Node1转移到Node3。
路由计算 &分片控制
ES存储数据只会存储在主分片,副本分片只是作为主分片的备份。ES存储、查询数据遵守路由计算公式(路由计算公式:hash(id)%主分片数量= 【0,1,2】)。用户查询数据时去哪个节点查询遵守分片控制(分片控制:用户可以访问任何一个节点获取数据,这个节点被称之为协调节点,由于负载问题协调节点具体是谁一般是所有节点轮询担任)。
数据写流程
-
客户端访问集群节点(任意节点)-协调节点。
-
协调节点通过计算将请求转发到指定节点
-
主分片将数据存储
-
主分片将数据发送到副本分片
-
副本分片保存后,进行反馈
-
主分片反馈
-
客户端反馈
consistency | consistency,即一致性。在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求 必须要有规定数量(quonum)(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行写操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(networkpartition)的时候进行“写”操作,进而导致数据不一致。”规定数量“即:int( (primary + number of replicas)/2 )+ 1 consistency 参数的值可以设为: one(只要主分片状态ok就允许执行作); all(必须要主分片和所有副本分片的状态没问题才允许执行写操作); quorum(默认),即大多数的分片副本状态没问题就允许执行 “写”操作。 |
timeout | 如果没有足够的副本分片会发生什么?Elasticsearch 会等待,希望更多的分片出现。默认情况下,它最多等待1分钟。如果你需要,你可以使用timeout 参数使它更早终止:100100毫秒,30s是30秒。 |
NOTE新索引默认有 1个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of replicas 大于1的时候,规定数量才会执行。
用户读流程
-
客户端发送查询请求到协调节点
-
协调节点计算出数据所在的分片以及全部副本位置
-
为了负载均衡,轮询所有节点
-
将请求转发具体节点
-
节点反馈查询结果,并将结果反馈至客户端
更新流程
-
客户端向协调协调发送更新请求。它将请求转发到主分片所在的节点。
-
节点从主分片检索文档,修改source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤3,超过retny_on_conflict 次后放弃。
-
如果 节点成功地更新文档,它将新版本的文档并行转发到其他节点上的副本分片,重新建立索引。一旦所有副本分片都返回成功,成功向协调节点也返回成功,协调节点向客户端返回成功。
多文档操作流程
mget 和 buk API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成每个分片的多文档请求,并且将这些请求并行转发到每个参与节点。
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端。
分片原理
分片是 Elasticsearch 最小的工作单元。但是究竟什么是一个分片,它是如何工作的?
传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
倒排索引
Elasticsearch 使用一种称为倒排索引的结构,它适用于快速的全文搜索。
见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forwardindex),反向索引(imnverted index)更熟悉的名字是倒排索引。
所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID 和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数
词条 | 索引中最小存储和查询单元 |
词典 | 字典,词条的集合,Btree,HashMap |
倒排表 | 关键词出现的位置、频率等 |
查询流程:
es收到查询请求,根据请求内容查找词典,不存在反馈null,存在根据倒排表反馈ID,再根据ID对应数据。
文档搜索
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是不可改变的:它永远不会修改。
不变性有重要的价值:
-
不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
-
一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
-
其它缓存(像 filter 缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
-
写入单个大的倒排索引允许数据被压缩,减少磁盘 IO 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的!你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
动态更新索引
如何在保留不变性的前提下实现倒排索引的更新?
答案是:用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
Elasticsearch 基于Lucene,这个java库引入了按段搜索的概念。每一 段 本身都是一个倒排索引,但索引在Lucene 中除表示所有段的集合外,还增加了提交点的概念--个列出了所有己知段的文件。
按段搜索会以如下流程执行:
-
新文档被收集到内存索引缓存
-
不时地,缓存被提交
-
一个新的段- 一个追加的倒排索引——被写入磁盘
-
一个新的包含新段名字的-提交点——被写入磁盘
-
磁盘进行同步-所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件
-
-
新的段被开启,让它包含的文档可见以被搜索
-
内存缓存被清空,等待接收新的文档
当一个查询被触发,所有己知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。取而代之的是,每个提交点会包含一个.del 文件,文件中会列出这些被删除文档的段信息。
当一个文档被“删除”时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
近实时搜索
随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commmiting)一个新的段到磁盘需要一个fsync来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync操作代价很大;如果每次索引一个文档都去执行次的话会造成很大的性能问题。
我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着要从整个过程中fsync被移除。在 Elasticsearch 和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。但是这里新段会被先写入到文件系统缓存——这一步代价会比较低,稍后再被刷新到磁盘——这一步代价比较高。不过只要文件已经在缓存中就可以像其它文件一样被打开和读取了。
文档分析
分析包含下面的过程:
-
将一块文本分成适合于倒排索引的独立的 词条
-
将这些词条统一化为标准格式以提高它们的“可搜索性”,或者recall
分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:
-
(字符过滤器)首先,字符串按顺序通过每个字符过滤器。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将&转化成 and。
-
(分词器)其次,字符串被 分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
-
(Token 过滤器)最后,词条按顺序通过每个token 过滤器。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如,像 a, and, the 等无用词),或者增加词条(例如,像 jump和 leap 这种同义词)。
内置分析器
Elasticsearch 还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:
"Set the shape to semi-transparent by calling set _trans(5)"
- 标准分析器
标准分析器是 Elasticsearch 默认使用的分析器。它是分析各种语言文本最常用的选择它根据 Unicode 联盟 定义的单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生:
set, the, shape, to, semi, transparent, by, calling, set trans, 5 - 空恪分析器
空格分析器在空格的地方划分文本。它会产生:
Set, the, shape, to, semi-transparent, by, calling, set trans(5) - 语言分析器
特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语 分析器附带了一组英语无用词(常用单词,例如and 或者the,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的词干。
英语分词器会产生下面的词条:
set, shape, semi, transpar, call, set tran, 5注意看transparent、calling和 set trans 已经变为词根格式
分析器使用场景
当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域搜索的时候,我们需要将查询字符串通过 相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。
测试分析器
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本。
GET http://127.0.0.1:9201/_analyze
{
"analyzer": "standard",
"text": "Text to analyzer"
}
结果中每一个元素代表一个词条。(token 是实际存储到索引中的词条。position 指明词条在原始文本中出现的位置。start ofset 和end oset 指明字符在原始字符串中的位置。)
指定分析器
当Elasticsearch在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文 字符串 域,使用 标准分析器对它进行分析。你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域——不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。要做到这一点,我们必须手动指定这些域的映射。
IK 分词器
首先我们通过 Postan 发送 GET请求查询分词效果
# GET http://localhost:9200/_analyze
{
"text":"测试单词"
}
IK 分词器中文版
将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用我们这次加入新的查询参数"analyzer":"ik_max word"
ik_max word:会将文本做最细粒度的拆分
ik_smart:会将文本做最粗粒度的拆分
自定义分析器(非重要)
虽然 Elasticsearch 带有一些现成的分析器,然而在分析器上 Elasticsearch 真正的强大之处在于,你可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。在分析与分析器 我们说过,一个 分析器 就是在一个包里面组合了三种函数的一个包装器,三种函数按照顺序被执行:
- 字符过滤器
字符过滤器 用来 整理 一个尚未被分词的字符串。例如,如果我们的文本是 HTM, 格式的,它会包含像 <p> 或者 <div> 这样的 HTML 标签,这些标签是我们不想索引的。我们可以使用 html清除 字符过滤器 来移除掉所有的 HTM 标签,并且像把 Á转换为相对应的 Unicode 字符 Á 这样,转换 HTML, 实体。一个分析器可能有0个或者多个字符过滤器。 - 分词器
一个分析器必须有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元。标准分析器里使用的标准分词器把一个字符串根据单词边界分解成单个词条,并且移除掉大部分的标点符号,然而还有其他不同行为的分词器存在。例如, 关键词 分词器 完整地输出 接收到的同样的字符串,并不做任何分词。 空格 分词器只根据空格分割文本。正则分词器根据匹配正则表达式来分割文本。 - 词单元过滤器
经过分词,作为结果的 词单元流会按照指定的顺序通过指定的词单元过滤器词.单元过滤器可以修改、添加或者移除词单元。我们已经提到过 lowercase 和 stop 词过滤器,但是在 Elasticsearch 里面还有很多可供选择的词单元过滤器。词干过滤器 把单词 遏经过分词,作为结果的词单元流会按照指定的顺序通过指定的词单元过滤器词单元过滤器可以修改、添加或者移除词单元。我们已经提到过 lowercase和 stop 词过滤器,但是在 Elasticsearch 里面还有很多可供选择的词单元过滤器。词干过滤器 把单词遏制为词干。ascii folding 过滤器移除变音符,把一个像"très"这样的词转换为"tres"ngram 和 edge_ngram 词单元过滤器 可以产生 适合用于部分匹配或者自动补全的词单元。
文档冲突
当我们使用indexAPI 更新文档 ,可以一次性读取原始文档,做我们的修改,然后重新索引整个文档。最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。如果其他人同时更改这个文档,他们的更改将丢失。
很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到 Elasticsearch中并使其可被搜索。 也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。但有时丢失了一个变更就是非常严重的 。
在数据库领域中,有两种方法(锁)通常被用来确保并发更新时变更不会丢失:
-
悲观并发控制(悲观锁)
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。 -
乐观并发控制(乐观锁)
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
乐观并发控制
Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
当我们之前讨论 index ,GET 和 delete 请求时,我们指出每个文档都有一个 version(版本)号,当文档被修改时版本号递增。Elasticsearch 使用这个 version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。我们可以利用 version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败(老的版本 es 使用 version,但是新版本不支持了,会报下面的错误,提示我们用 if seq no和if primary term)。
外部系统版本控制
一个常见的设置是使用其它数据库作为主要的数据存储,使用Elasticsearch 做数据检索,这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch ,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。
如果你的主数据库已经有了版本号一或一个能作为版本号的字段值比如 timestamp那么你就可以在 Elasticsearch 中通过增加 versibn type=extemmal 到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数,且小于9.2E+18-一个Java 中 long类型的正值。
Kibana
Kibama,是一个免费且开放的用户界面,能够让你对Elasticsearch 数据进行可视化,并让你在 Elastic Stack 中进行导航。你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。
下载地址Kibana 7.8.0 | Elastic
解压缩下载的 zip 文件
修改 config/kibana.yml 文件
#默认端口
server.port:5601
#ES服务器的地址
elasticsearch.hosts:["http://localhost:9200"]
#索引名
kibana.index:".kibana"
#支持中文
i18n.1ocale:zhCN