目录

公众号首发、欢迎关注

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_分词器

一、导读

本篇是白日梦的第三篇ES笔记,前面已经跟大家分享过两篇ES笔记了,分别是:

​ES基础篇--快速上手ES​

​ES进阶篇--50个检索、聚合案例​


其实这个专题相对来说质量还是比较不错的,看过前面两篇文章之后基本上大家可以上手使用ES了,包括对一些花里花哨的查询相关的写法也有所了解。然后这一篇文章会和大家调过头来重新巩固一下基础概念上的扫盲。


二、彩蛋福利:账号借用


三、ES的Index、Shard及扩容机制

首先你看下这个表格(ES6):

Elasticsearch

关系型数据库

Document


type(ES7中被取消)


index

Database

在ES中的Index的地位相当于是MySQL中的database。所以你让ES帮你存储数据你总得先创建一个Index吧,如果你手动的定制创建Index,你还可以为Index指定shard。

那什么是shard呢?下文马上说。

下面是对Index操作的Case:

# 创建索引
PUT my_index
{
# 设置index的shard信息
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
# 设置index中各个字段的类型,属性(下文细讲)
"mapping":{
...
}
}

# 修改索引
PUT /my_index/_settings
{
# 只能改number_of_replicas,不能改number_of_shards
"number_of_replicas":3
}

# 删除索引
DELETE /my_index
DELETE /my_index1,my_index2
DELETE /my_*
DELETE /_all # 删掉所有索引

# 如果不想让ES可以一下子删除所有索引,可以通过配置文件设置
elasticsearch.yml
action.destructive_requires_name:true

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实例时,架构其实是下面这样:

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_分词器_02

你会发现,其实系统中就有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,使数据平均分散在不同的节点中。

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_分词器_03

举个例子:假设你真的又启动了一个Node,这个Node会自动的加入到上面那个ES中去,自动组成一个有两个Node的集群,如果你依然使用的默认配置即:​​number_of_shards = 5 和 number_of_replicas = 1​​。这时ES会自动将系统rebalance成下图这样:

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_分词器_04

此时你再去看集群的状态,会发现为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个实例组建集群。就像下图这样:

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_倒排索引_05

这时每个shard都独享操作系统的所有资源,性能自然会最好。

四、ES支持的核心数据类型

参考官网 ​​https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html​

4.1、数字类型

long、integer、short、byte、double、float、half_float、scaled_float

示例:

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"number_of_bytes": {
"type": "integer"
}
"time_in_seconds": {
"type": "float"
}
"price": {
"type": "scaled_float"
"scaling_factor": 100
}
}
}
}
}

4.2、日期类型

date

示例:

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"birthday ": {
"type": "date"
}
}
}
}
}

PUT my_index/_doc/1
{ "date": "2015-01-01" }

4.3、boolean类型

string类型的字符串可以被ES解释成boolean。

boolean

示例:

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"is_published": {
"type": "boolean"
}
}
}
}
}

4.4、二进制类型

binary

示例

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"name": {
"type": "text"
}
"blob": {
"type": "binary"
}
}
}
}
}

PUT my_index/_doc/1
{
"name": "Some binary blob"
"blob": "U29tZSBiaW5hcnkgYmxvYg=="
}

4.5、范围

integer_range、float_range、long_range、double_range、date_range

示例

PUT range_index
{
"settings": {
"number_of_shards": 2
}
"mappings": {
"_doc": {
"properties": {
"expected_attendees": {
"type": "integer_range"
}
"time_frame": {
"type": "date_range"
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
}

PUT range_index/_doc/1?refresh
{
"expected_attendees" : {
"gte" : 10
"lte" : 20
}
"time_frame" : {
"gte" : "2015-10-31 12:00:00"
"lte" : "2015-11-01"
}
}

4.6、复杂数据类型

对象类型,嵌套对象类型

示例:

PUT my_index/_doc/1
{
"region": "US"
"manager": {
"age": 30
"name": {
"first": "John"
"last": "Smith"
}
}
}

在ES内部这些值被转换成这种样式

{
"region": "US"
"manager.age": 30
"manager.name.first": "John"
"manager.name.last": "Smith"
}

4.7、Geo-type

ES支持地理上的定位点。

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
}

PUT my_index/_doc/1
{
"text": "Geo-point as an object"
"location": {
"lat": 41.12
"lon": -71.34
}
}

PUT my_index/_doc/4
{
"text": "Geo-point as an array"
"location": [ -71.34 41.12 ]
}

五、精确匹配与全文检索

精确匹配和全文检索是ES提供的两种检索方式,都不难理解。

5.1、精确匹配:exact value

搜索时输入的value必须和目标完全一致才算作命中。

"query": {
# match_phrase 短语精确匹配的关键字
# 只有name字段 完全等于 “白日梦”的doc 才算命中然后返回
"match_phrase": {
"name": "白日梦"
}
}

5.2、全文检索:full text

全文检索时存在各种优化处理如下:

  • 缩写转换: cn == china
  • 格式转换 liked == like == likes
  • 大小写转换 Tom == tom
  • 同义词转换 like == love

示例

GET /_search
{
"query": {
# match是全文检索的关键字
# 白日梦可以被分词器分成:白、白日、白日梦
# 所以当你使用:白、白日、白日梦、我是白日梦、白日梦是我 等等词条检索,都可以检索出结果
"match" : {
"name" : "白日梦"
}
}
}

六、倒排索引 & 正排索引

6.1、倒排索引 inverted index

其实正排索引和倒排索引都是人们取的名字而已。主要是你理解它是什么东西就好了。

正排索引:以doc为维度,记录doc中出现了哪些词。

倒排索引:以把doc打碎成一个个的词条,以词语为维度。记录它在哪些doc中出现过。

倒排索引要做的事就是将一篇文章通过分词器打散成很多词,然后记录各个词分别在哪篇doc中出现过。用户在使用的时候输入一串搜索串,这串字符串同样会使用一样的分词器打散成很多词。再拿着这些词去方才建立的倒排索引中匹配。同时结合相关性得分找到。

假设我们存在这样两句话。

doc1 : hello world you and me
doc2 : hi world how are you

建立倒排索引就是这样

词条

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​​,不创建正排索引。

PUT /index
{
"mappings":{
"my_type":{
"properties":{
"my_field":{
"type":"text"
"doc_values":false # 禁用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使用的分词器

PUT /my_index
{
"settings":{
"analysis":{
"analyzer":{
"es_std":{
# 指定分词器的类型是:standard
"type":"standard",
# 指定分词器的停用词:_english_
"stopwords":"_english_"
}
}
}
}
}

九、mapping

9.1、认识mapping

看到这里你肯定知道了,我们想往ES中写数据是需要一个index的。其实我们在往ES中PUT数据之前是可以手动创建Mapping,这里的mapping其实好比你搞一个java类,做一次对数据结构的抽象,比如name 的类型是String,age的类型是Integer。

就好比下面这样:

PUT my_index
{
# 指定index的primary shard数量以及 replicas的数量
“settings”:{
"number_of_shards":1,
"number_of_repicas":0
},
# 关键字,我们手动自定my_index中的mapping
"mappings": {
"my_index": { # index的名称
"properties": { # 关键字,mapping的属性,字段
"my_field1": { # 相当于Java中的 String my_field1
"type": "text",
"analyzer":"english"# 指定分词器,说明这个字段需要分词建立倒排索引
}
"my_field2": { # 相当于Golang中的 var my_field2 float
"type": "float",
# 指定是否要分词。analyzed表示要,not_analyzed表示不要
"index":"not_analyzed"
}
"my_field3": {
"type": "scaled_float"
"scaling_factor": 100
}
}
}
}
}

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

PUT my_index/_doc/1
{
"title": "This is a document"
}

9.2、查看mapping

# 查看某个index下的某个type的mapping
GET /index/_mapping/type

# 查看某个index的mapping
GET /index/_mapping

9.3、dynamic mapping (动态mapping)

就像下面这样,我们直接往ES中PUT数据,ES在为我们创建index时就会自动生成dynamic mapping。其实用大白话讲就是ES自动推断你往它里面存的json串的类型。比如下面的"first_name"会被dynamic mapping成string 类型的。

PUT my_index/_doc/1
{
"first_name": "John"
}

ES使用​​_type​​来描述doc字段的类型,原来我们直接往ES中存储数据,并没有指定字段的类型,原因是ES存在动态类型推断(ES支持的类型上文中我们也一起看过了,如果不记得阔以再去看一下哈)。默认的mapping中定义了每个field对应的数据类型以及如何进行分词。

null          --> no field add
true flase --> boolean
123 --> long
123.123 --> double
1999-11-11 --> date
"hello world" --> string
Object --> object

9.4、定制dynamic mapping 策略

  • ture: 语法陌生字段就进行dynamic mapping。
  • false: 遇到陌生字段就忽略。
  • strict: 遇到默认字段就报错。

示例

PUT /my_index/
{
"mappings":{
"dynamic":"strict"
}
}
  • 禁用ES的日期探测的Demo
# 创建mapping并制定:禁用ES的日期探测
PUT my_index
{
"mappings": {
"_doc": {
"date_detection": false
}
}
}

# 添加一条doc
PUT my_index/_doc/1
{
"create": "1985/12/22"
}

# 查看doc,结果如下
GET my_index/_doc/1
{
"_index": "my_index",
"_type": "_doc",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"create": "1985/12/22"
}
}

# 查看mapping
GET my_index/_mapping
# 结果如下:
{
"my_index": {
"mappings": {
"_doc": {
"date_detection": false,
"properties": {
"create": {
# 被任务是text类型
"type": "text",
# ES会自动帮你创建的下面的field部分
# create是text类型,create.ketword是keyword类型
# keyword类型不会分词,默认保留前256字符
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
  • 定制日期发现规则
PUT my_index
{
"mappings": {
"_doc": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}
}

PUT my_index/_doc/1
{
"create_date": "09/25/2015"
}
  • 定制数字类型的探测规则
PUT my_index
{
"mappings": {
"_doc": {
"numeric_detection": true
}
}
}

PUT my_index/_doc/1
{
"my_float": "1.0",
"my_integer": "1"
}

定制type field

ES中type相当于MySQL的数据表嘛,ES中可以给现存的type添加field。但是不能修改,否则就会报错。

type在高版本的ES7中被废弃了,Index的概念依然保留着。

# 创建index:twitter
PUT twitter
{
"mappings": {
# user为type
"user": {
"properties": {
"name": {
# 会被全部检索
"type": "text" ,
# 指定当前field使用 english分词器
"analyzer":"english"
}
"user_name": { "type": "keyword" },
"email": { "type": "keyword" }
}
}
"tweet": {
"properties": {
"content": { "type": "text" },
"user_name": { "type": "keyword" },
# "tweeted_at": { "type": "date" },
"tweeted_at": {
"type": "date"
# 通过index设置为当前field tweeted_at不能被分词
"index": "not_analyzeed"
}
}
}
}
}

9.5、mapping复杂数据类型在底层的存储格式

Object类型

# object类型的json
{
"address":{
"province":"shandong",
"city":"qingdao"
}
"name":"bairimeng",
"age":"12"
}

# ES会将上面的json转换成如下的格式存储
{
"name" : [bairimeng],
"age" : [12],
"address.province" : [shandong]
"address.city" : [qingdao]
}

Object数组类型

# Object数组类型
{
"address":[
{"age":"12""name":"张三"},
{"age":"12""name":"张三"},
{"age":"12""name":"张三"}
]
}

# ES会将上面的json转换成如下的格式存储
{
"address.age" : [12,12,12],
"address.name" : [张三,张三,张三]
}

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

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_倒排索引_06

_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​

PUT tweets
{
"mappings": {
"_doc": {
"_source": {
"enabled": false
}
}
}
}

_all

首先它也是一个元数据,当我们往ES中插入一条document时。ES会自动的将这个doc中的多个field的值串联成一个字符串,然后用这个作为​​_all​​​字段的值并建立索引。当用户发起检索却没有指定从哪个字段查询时,默认就会在这个​​_all​​中进行匹配。

_field_names

举个例子说明这个属性怎么用:

首先往index=my_index的索引下灌两条数据

# Example documents
PUT my_index/_doc/1
{
"title": "This is a document"
}

PUT my_index/_doc/2?refresh=true
{
"title": "This is another document",
"body": "This document has a body"
}

然后像下面这样使用​​_field_names​​检索,并且指定了字段=“title”。此时ES会将所有包含title字段,且title字段值不为空的doc检索出来。

GET my_index/_search
{
"query": {
"terms": {
"_field_names": [ "title" ]
}
}
}

禁用​​_field_names​​:

PUT tweets
{
"mappings": {
"_doc": {
"_field_names": {
"enabled": false
}
}
}
}

_routing

下面路由导航中细说。

_uid

在ES6.0中被弃用。

9.8、copy_to

在上一篇文章中跟大家介绍过可以像下面这样跨越多个字段搜索

# dis_max
GET /your_index/your_type/_search
{
# 基于 tie_breaker 优化dis_max
# tie_breaker可以使dis_max考虑其它field的得分影响
"query": {
# 直接取下面多个query中得分最高的query当成最终得分
# 这也是best field策略
"dis_max": {
"queries":[
{"match":{"name":"关注"}},
{"match":{"content":"白日梦"}}
],
"tie_breaker":0.4
}
}
}

# best_field
# 使用multi_match query简化写法如下:
GET /your_index/your_type/_search
{
"query": {
"multi_match":{
"query":"关注 白日梦",
# 指定检索的策略 best_fields(因为dis_max就是best field策略)
"type":"best_fields",
# content^2 表示增加权重,相当于:boost2
"fields":["name","content^2"],
"tie_breaker":0.4,
"minimum_should_match":3
}
}
}

# most_field
GET /your_index/your_type/_search
{
# most_fields策略、优先返回命中更多关键词的doc
# 如下从title、name、content中搜索包含“赐我白日梦”的doc
"query": {
"multi_match":{
"query":"赐我白日梦",
# 指定检索的策略most_fields
"type":"most_fields",
"fields":["title","name","content"]
}
}
}

针对跨越多个字段的检索除了上面的most_field和best_field之外,还可以使用copy_to预处理。

这个copy_to实际上是在允许我们自定义一个_all字段, ES会将多个字段的值复制到一个_all中,然后再次检索时目标字段就使用我们通过copy_to创建出来的_all新字段中。

示例:

PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"first_name": {
"type": "text",
# 把当前的first_name copy进full_name字段中
"copy_to": "full_name"
}
"last_name": {
"type": "text",
# 把当前的last_name copy进full_name字段中
"copy_to": "full_name"
}
"full_name": {
"type": "text"
}
}
}
}
}

PUT my_index/_doc/1
{
"first_name": "John",
"last_name": "Smith"
}

GET my_index/_search
{
"query": {
"match": {
"full_name": {
"query": "John Smith",
"operator": "and"
}
}
}
}

9.9、Arrays 和 Multi-field

更多内容参见官网 ​​https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html​

十、图解: master的选举、容错以及数据的恢复。

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_倒排索引_07

如上图为初始状态图

假如图上的第一个节点是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之间同步数据,这就意味着多个修改请求其实是乱序的不一定按照先后顺序执行。

相关语法:

PUT /index/type/2?version=1{
"name":"XXX"
}

上面的命令中URL中的存在​​?version=1​​,此时,如果存在其他客户端将id=2的这条记录修改过,导致id=2的版本号不等于1了,那么这条PUT语句将会失败并有相应的错误提示。这样也就规避了并发修改异常。


拓展:

ES也允许你使用自己的维护的版本号来进行并发控制,用法如下:

PUT /index/type/2?version=1&version_type=external

对比两者的不同:

  • 使用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上(因为它们的数据是一样的)

就像下图这样:

白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲_全文检索_08

路由算法,揭开primary_shard数量不可变的面纱

shard = hash(routing) % number_of_primary_shards

公式不复杂,可以将上面的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上,

# 添加一个doc,并制定routing
PUT my_index/_doc/1?routing=user1&refresh=true
{
"title": "This is a document"
}

# 通过id+routing获取你想要的doc
GET my_index/_doc/1?routing=user1

十三、写一致性及原理

我们在发送任何一个增删改查时,都可以带上一个 consistency 参数,指明我们想要的写一致性是什么,如下

PUT /index/type/id?consistency=quorum

有哪些可选参数呢?

  • 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​​通过自己设置超时时间来缩短超时时间默认的超时时间。