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

你会发现,其实系统中就有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支持的核心数据类型

参考官网 //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官网分词器模块 //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中包含了诸如propertiesmatadata(_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

_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进行检索

详情见官网 //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

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

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

//img2018.cnblogs.com/blog/1496926/201911/1496926-20191104182531120-1172752624.png

如上图为初始状态图

假如图上的第一个节点是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上(因为它们的数据是一样的)

就像下图这样:

节点对等的架构

路由算法,揭开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通过自己设置超时时间来缩短超时时间默认的超时时间。