目录
- 9.1、认识mapping
- 9.2、查看mapping
- 9.3、dynamic mapping (动态mapping)
- 9.4、定制dynamic mapping 策略
- 定制type field
- 9.5、mapping复杂数据类型在底层的存储格式
- 9.6、ES7中废弃了type的概念
- 9.7、认识一些mate-field(元数据字段)
- 9.8、copy_to
- 9.9、Arrays 和 Multi-field
公众号首发、欢迎关注
一、导读
本篇是白日梦的第三篇ES笔记,前面已经跟大家分享过两篇ES笔记了,分别是:
其实这个专题相对来说质量还是比较不错的,看过前面两篇文章之后基本上大家可以上手使用ES了,包括对一些花里花哨的查询相关的写法也有所了解。然后这一篇文章会和大家调过头来重新巩固一下基础概念上的扫盲。
二、彩蛋福利:账号借用
三、ES的Index、Shard及扩容机制
首先你看下这个表格(ES6):
Elasticsearch | 关系型数据库 |
Document | 行 |
type(ES7中被取消) | 表 |
index | Database |
在ES中的Index的地位相当于是MySQL中的database。所以你让ES帮你存储数据你总得先创建一个Index吧,如果你手动的定制创建Index,你还可以为Index指定shard。
那什么是shard呢?下文马上说。
下面是对Index操作的Case:
shard分为primary shard和replica shard ,其中的primary shard可以接受读/写请求,replica shard可以接受读请求,起到一个负载的作用。默认情况下我创建的索引都有: number_of_shards = 5 和number_of_replicas = 1
。表示一共有五个primary shard,并且每个primary 都有一个副本。也就是 5+5*1 =10个shard。
但是当你启动单台ES实例时,架构其实是下面这样:
你会发现,其实系统中就有5个shard。不存在上面计算的10个shard。原因是因为ES要求Primary Shard 和它的备份 replica shard不能同时存在于一个Node上。所以你单个Node启动后,就只有5个primary shard。并且这时你去看集群的状态,会发现整个集群处于yellow状态,表示集群整体可用,但是存在replica shard不可用的情况。
然后你会不会好奇,假设我有2个Node(两个ES实例)组成的ES集群,你怎样做,才能让系统中的Shard是如何负载均衡分布在两个Node上呢?
回答:其实你不用操心,ES自己会帮你完成的。当你增加或减少节点时,ES会自动的进行rebalance,使数据平均分散在不同的节点中。
举个例子:假设你真的又启动了一个Node,这个Node会自动的加入到上面那个ES中去,自动组成一个有两个Node的集群,如果你依然使用的默认配置即:number_of_shards = 5 和 number_of_replicas = 1
。这时ES会自动将系统rebalance成下图这样:
此时你再去看集群的状态,会发现为green。表示集群中所有shard都可用。
Node2中会存在5个replica shard,他们是Node1中的Primary的备份。每个shard相当于是一个luncene实例,拥有完整的检索数据、处理请求的能力。所以shard的数量越多,一定意义上意味着ES的吞吐量就越大。
但是你需要注意的是,primary shard的数量是不能改变的,但是它的副本的数量可以改变。
至于为什么primary shard的数量是不能改变的,下文中的路由原理会说的。
所以当你想对现在有的ES集群进行扩容的时,就存在两种选择:
1、纵向扩容:你不改变集群的总shard数,然后去买配置更高,存储更大的机器跑这些shard。
2、横向扩容:你扩大replica shard的数量,然后去多购置几个配置低的机器,你只需要写好配置文件,再启动Node,它自己会加入到现有的集群中。因为每个shard的都能对外提供服务嘛,所以你这样扩容系统的性能肯定有提升。
根据现在云服务器实例的市场行情来看,方案二会更省钱一些。
当然了如果你想让ES集群有最好的性能,还是使用默认的配置:number_of_shards = 5 和number_of_replicas = 1
,这时你需要10台机器。每个集群上都启动一个ES实例,让这10个实例组建集群。就像下图这样:
这时每个shard都独享操作系统的所有资源,性能自然会最好。
四、ES支持的核心数据类型
参考官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html
4.1、数字类型
示例:
4.2、日期类型
示例:
4.3、boolean类型
string类型的字符串可以被ES解释成boolean。
示例:
4.4、二进制类型
示例
4.5、范围
示例
4.6、复杂数据类型
示例:
在ES内部这些值被转换成这种样式
4.7、Geo-type
ES支持地理上的定位点。
五、精确匹配与全文检索
精确匹配和全文检索是ES提供的两种检索方式,都不难理解。
5.1、精确匹配:exact value
搜索时输入的value必须和目标完全一致才算作命中。
5.2、全文检索:full text
全文检索时存在各种优化处理如下:
- 缩写转换: cn == china
- 格式转换 liked == like == likes
- 大小写转换 Tom == tom
- 同义词转换 like == love
示例
六、倒排索引 & 正排索引
6.1、倒排索引 inverted index
其实正排索引和倒排索引都是人们取的名字而已。主要是你理解它是什么东西就好了。
正排索引:以doc为维度,记录doc中出现了哪些词。
倒排索引:以把doc打碎成一个个的词条,以词语为维度。记录它在哪些doc中出现过。
倒排索引要做的事就是将一篇文章通过分词器打散成很多词,然后记录各个词分别在哪篇doc中出现过。用户在使用的时候输入一串搜索串,这串字符串同样会使用一样的分词器打散成很多词。再拿着这些词去方才建立的倒排索引中匹配。同时结合相关性得分找到。
假设我们存在这样两句话。
建立倒排索引就是这样
词条 | doc1(*表示出现过) | doc2(-表示不曾出现过) |
hello | * | - |
world | * | * |
you | * | * |
and | * | - |
me | * | - |
hi | - | * |
how | - | * |
are | - | * |
这时,我们拿着hello world you 来检索,同样需要先经过分词器分词,然后可以得到分出来的三个单词:hello、world、you,然后拿着这三个单词去上面的倒排索引表中找,你可以看到:
- hello在doc1中出现过。
- world在doc1、doc2中出现过。
- you在doc1、doc2中出现过。
最终doc1、doc2都会被检索出,但是doc1命中了更多的词,因此doc1得分会更高,排名越靠前。
6.2、正排索引 doc value
doc value 是指所有不分词的document的field。
在建立索引的时候,一方面会建立倒排索引,以供搜索用。一方面会建立正排索引,也就是doc values,以供排序,聚合,过滤等操作使用。
正排索引大概长这样:
document | name | age |
doc1 | 张三 | 12 |
doc2 | 李四 | 34 |
os cache会缓存正排索引,以提高访问doc value
的速度。当OS Cache中内存大小不够存放整个正排索引时,doc value
中的值会被写入到磁盘中。
关于性能方面的问题:ES官方建议,大量使用OS Cache来进行缓存和提升性能。不建议使用jvm内存来缓存数据,那样会导致一定的gc开销,甚至可能导致oom问题。所以官方的建议给JVM更小的内存,给OS Cache更大的内存。假如我们的机器64g,只需要给JVM 16g即可。
6.3、禁用doc value
假设我们不使用聚合、排序等操作,为了节省空间,在创建mappings
时,可以选择禁用doc value
,不创建正排索引。
七、简述相关性评分
relevance score 相关度评分算法, 直白说就是算出一个索引中的文本和搜索文本之间的相似程度。
Elasticsearch使用的是 TF-IDF算法 (term-frequency / inverser document frequency)。
- term-frequency: 表示你搜索的词条在当前doc中出现的次数,出现的次数越多越相关。
- inverse document frequency : 表示搜索文本中的各个词条在整个index中所有的document中出现的次数,出现的次数越多越不相关。
- field-length: field长度越长,越不相关。
八、分词器
ES官网分词器模块 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/analysis.html
8.1、什么是分词器?
我们使用分词器可以将一段话拆分成一个一个的单词,甚至可以进一步对分出来的单词进行词性的转换、时态的转换、单复数的转换的操作。
为什么使用分词器呢?
你想一个doc那么长,成千上万字。为了对它进行特征的提取,分析。就得把它还原成组成它的词条。这样会提高检索时的召回率,让更多的doc被检索到。
8.2、分词器的组成
character filter:
在一段文本在分词前先进行预处理,比如过滤html标签, 将特殊符号转换成123..这种 阿拉伯数字等特殊符号的转换。
tokenizer:
进行分词、拆解句子、记录词条的位置(在当前doc中占第几个位置term position)及顺序。
token filter:
进行同义词的转换,去除同义词,单复数的转换等等。
ES内置的分词器:
- standard analyzer(默认)
- simple analyzer
- whitespace
- language analyzer(特定语言的分词器,English)
另外比较受欢迎的中文分词器为IK分词器,这个分词器的插件包、安装方式我都整理成文档了,公众号后台回复:es即可领取。
8.3、修改Index使用的分词器
九、mapping
9.1、认识mapping
看到这里你肯定知道了,我们想往ES中写数据是需要一个index的。其实我们在往ES中PUT数据之前是可以手动创建Mapping,这里的mapping其实好比你搞一个java类,做一次对数据结构的抽象,比如name 的类型是String,age的类型是Integer。
就好比下面这样:
1、mapping json中包含了诸如
properties
、matadata(_id,_source,_type)
、settings(analyzer)
以及其他的settings。2、我们把上面的json中的properties部分称为:root object
3、自己创建mapping一般是为了更好的控制各个字段的数据类型,包括使用到的分词器。
4、另外注意:field的mapping只能新增,不能修改。
你也可以在往ES中PUT数据之前不创建任何Mapping,ES会自动为我们生成mapping。就像下面这样,自动生成的mapping信息叫做dynamic mapping,下文中我们还会详细讲这个dynamic
9.2、查看mapping
9.3、dynamic mapping (动态mapping)
就像下面这样,我们直接往ES中PUT数据,ES在为我们创建index时就会自动生成dynamic mapping。其实用大白话讲就是ES自动推断你往它里面存的json串的类型。比如下面的"first_name"会被dynamic mapping成string 类型的。
ES使用_type
来描述doc字段的类型,原来我们直接往ES中存储数据,并没有指定字段的类型,原因是ES存在动态类型推断(ES支持的类型上文中我们也一起看过了,如果不记得阔以再去看一下哈)。默认的mapping中定义了每个field对应的数据类型以及如何进行分词。
9.4、定制dynamic mapping 策略
- ture: 语法陌生字段就进行dynamic mapping。
- false: 遇到陌生字段就忽略。
- strict: 遇到默认字段就报错。
示例
- 禁用ES的日期探测的Demo
- 定制日期发现规则
- 定制数字类型的探测规则
定制type field
ES中type相当于MySQL的数据表嘛,ES中可以给现存的type添加field。但是不能修改,否则就会报错。
type在高版本的ES7中被废弃了,Index的概念依然保留着。
9.5、mapping复杂数据类型在底层的存储格式
Object类型
Object数组类型
9.6、ES7中废弃了type的概念
在一开始我们将ElasticSearch的index比作MySQL中的database,将type比作table,其实这种类比是错误的。因为在MySQL中不同表之间的列在物理上是没有关系的,各自占有自己的空间。
但是在ES中不是这样,可能type=Student中的name列和type=Teacher中的name列会被lucene认为是同一个field。导致Lucene处理效率下降。
所以在ES7中直接就将type概念废弃了。
不过你也不用担心,大部分企业都倾向于使用低版本的ES,比好比你现在用的依然是java8 而不是JDK14。
9.7、认识一些mate-field(元数据字段)
这里说的元数据字段指定的是,当你检索doc时,除了返回的doc本身的数据之外,其他的出现在检索结果中的数据,我们是需要了解这些字段都是什么含义的。如下:
_index , _type , _id , _source , _version
_id
它是document的唯一标识信息。上图中我手动指定了id等于1。如果不指定的话,ES会自动为我们生成一个长20个字符的id,ES会保证集群中的生成的doc id不会发生冲突。 有这种场景,比如你的数据是从MySQL这种数据库中倒入进ES的,那其实完全可以使用MySQL中的数据行的ID作为doc id。
_index
你可以简单粗暴的将es的index的地位理解成MYSQL中的数据库。这里的元数据_index被用来标识当前的doc存在于哪个index中。index的命名规范,名称小写,不能用下划线开头,不能包含逗号。
ES支持跨域index进行检索
详情见官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-index-field.html
_type
这个字段用来标识doc的类型。但它其实是一个逻辑上的划分。
field中的value在顶层的lucene建立索引的时候,全部使用的opaque bytes类型,不区分类型的lucene是没有type概念的。
为了方便我们区分出不通doc的类型,于是在document中加了一个_type
属性。
ES会通过_type
进行type的过滤和筛选,一个index中是存放的多个type实际上是存放在一起的,因此一个index下,不可能存在多个重名的type。
_version `_version`是doc的版本号,可以用来做并发控制,当一个doc被创建时它的`_version`是1,之后对它的每一次修改,都会使这个版本号+1,哪怕是你将这个doc删除了,这个doc的版本号也会增加1。
_source
通过这个字段可以定制我们想要返回字段。比如说一个type = user类型的doc中存在100个字段,但是可能前端并不是真的需要这100个字段,于是我们使用_source去除一些字段,注意和filter是不一样的,因为filter不会影响相关性得分。
你可用像下面这样禁用_source
_all
首先它也是一个元数据,当我们往ES中插入一条document时。ES会自动的将这个doc中的多个field的值串联成一个字符串,然后用这个作为_all
字段的值并建立索引。当用户发起检索却没有指定从哪个字段查询时,默认就会在这个_all
中进行匹配。
_field_names
举个例子说明这个属性怎么用:
首先往index=my_index的索引下灌两条数据
然后像下面这样使用_field_names
检索,并且指定了字段=“title”。此时ES会将所有包含title字段,且title字段值不为空的doc检索出来。
禁用_field_names
:
_routing
下面路由导航中细说。
_uid
在ES6.0中被弃用。
9.8、copy_to
在上一篇文章中跟大家介绍过可以像下面这样跨越多个字段搜索
针对跨越多个字段的检索除了上面的most_field和best_field之外,还可以使用copy_to预处理。
这个copy_to实际上是在允许我们自定义一个_all字段, ES会将多个字段的值复制到一个_all中,然后再次检索时目标字段就使用我们通过copy_to创建出来的_all新字段中。
示例:
9.9、Arrays 和 Multi-field
更多内容参见官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html
十、图解: master的选举、容错以及数据的恢复。
如上图为初始状态图
假如图上的第一个节点是master节点,并且它挂掉了。那它挂掉后,整个cluster的status会变成red,表示存在数据丢失了集群不可用。然后集群会按照下面的步骤恢复:
第一步:完成master的选举,自动在剩下的节点中选出一个节点当成master节点。
第二步:选出master节点后,这个新的master节点会将P0在第三个节点中存在一个replica shard提升为primary shard,此时cluster 的 status = yellow,表示集群中的数据是可以被访问的但是存在部分replica shard不可用。
第三步:重新启动因为故障宕机的node,并且将右边两个节点中的数据拷贝到第一个节点中,进行数据的恢复。
十一、ES如何解决并发冲突
ES内部的多线程异步并发修改时,通过_version
版本号进行并发控制,每次创建一个document,它的_version
内部版本号都是1,以后对这个doc的修改,删除都会使这个版本号增1。
ES的内部需在Primary shard 和 replica shard之间同步数据,这就意味着多个修改请求其实是乱序的不一定按照先后顺序执行。
相关语法:
上面的命令中URL中的存在?version=1
,此时,如果存在其他客户端将id=2的这条记录修改过,导致id=2的版本号不等于1了,那么这条PUT语句将会失败并有相应的错误提示。这样也就规避了并发修改异常。
拓展:
ES也允许你使用自己的维护的版本号来进行并发控制,用法如下:
对比两者的不同:
- 使用es提供的_version进行版本控制的话,需要你的PUT命令中提供的version == es的维护的version。
- 添加参数
version_type=external
之后,假设当前ES中维护的doc版本号是1, 那么只有当用户提供的版本号大于1时,PUT才会成功。
十二、路由原理
什么是数据路由?
一个index被分成了多个shard,文档被随机的存在某一个分片 上。客户端一个请求随机打向index中的一个分片,但是请求的doc可能不存在于这个分片上,于是接受请求的shard会将请求路由到真正存储数据的shard上,这个过程叫做数据路由。
其中接受到客户端请求的节点称为coordinate node(协调节点),比如现在是客户端想修改服务端的一条消息,shard A接受到请求了,那么A就是 coordnate node协调节点。数据存储在B primary shard 上,那么协调节点就会将请求路由到B primary shard中,B处理完成后再向 B replica shard同步数据,数据同步完成后,B primary shard响应 coordinate node, 最后协调节点响应客户端结果。
假如说你每个primary shard有多个存活的replica shard,默认情况下coordinate node会将请求使用round-robin的方式分散到replica shard和这个primary shard上(因为它们的数据是一样的)
就像下图这样:
路由算法,揭开primary_shard数量不可变的面纱
公式不复杂,可以将上面的routing当成doc的id。无论是用户执行的还是自动生成的,反正肯定是唯一的。既然是唯一的,那每次hash得到的结果也是一样的, 这样一个唯一的值对主分片的数进行取余数,得到的结果就会在 0~最大分片数 之间。
你看看上面的路由公式中后半部分使用的是 number_of_primary_shards ,这也是为什么ES规定,primary shard的数量不能改变,但是replica shard 可以改变的原因。
除了上面说的路由方式,你还可以像下面这样定制路由规则:比如PUT /index/type/id?routing=user_id
,可以保证这类doc一定被路由到指定的shard上,而且后续进行应用级负载均衡时会批量提升读取的性能。
像下面这种用法,可以保证你的doc一定被路由到一个shard上,
十三、写一致性及原理
我们在发送任何一个增删改查时,都可以带上一个 consistency 参数,指明我们想要的写一致性是什么,如下
有哪些可选参数呢?
- one:当我们进行写操作时,只要存在一个primary_shard=active 就能写入成功。
- all:cluster中全部shard都为active时,可以写入成功。
- quorum(法定的):也是ES的默认值, 要求大部分的replica_shard存活时系统才可用。
quorum数量的计算公式: int((primary+number_of_replicas)/2)+1
算一算,假如我们的集群中存在三个node,replica=1,那么cluster中就存在3+3*1=6个shard。
int((3+1)/2)+1 = 3
看计算的结果,只有当quorum=3 即replica_shard=3时,集群才是可用的。
但是当我们的单机部署时,由于ES不允许同一个server的primary_shard和replica_shard共存,也就是说我们的replica数目为0,为什么ES依然可以用呢?这是因为ES提供了一种特殊的处理场景,也就是当number_of_replicas>1时,上述检查集群是否可用的机制才会生效。
quorum不全时 集群进入wait()状态。 默认1分钟。在等待期间,期望活跃的shard的数量可以增加,到最后都没有满足这个数量的话就会timeout。
我们在写入时也可以使用timeout参数, 比如: PUT /index/type/id?timeout=30
通过自己设置超时时间来缩短超时时间默认的超时时间。