ElasticSearch相关见解资料摘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
搜索线程池(search):

作用:用于执行搜索请求。
示例参数:thread_pool.search.size 控制线程池的大小,默认为 CPU 核心数 * 3。
索引线程池(index):

作用:用于执行索引请求,包括索引文档、更新文档等操作。
示例参数:thread_pool.index.size 控制线程池的大小,默认为 CPU 核心数 * 3。
刷新线程池(refresh):

作用:用于执行索引的刷新操作,将内存中的变更写入磁盘。
示例参数:thread_pool.refresh.size 控制线程池的大小,默认为 1。
合并线程池(merge):

作用:用于执行索引段的合并操作,以减少磁盘碎片。
示例参数:thread_pool.merge.size 控制线程池的大小,默认为 1。
监听线程池(listener):

作用:用于监听集群状态的变化。
示例参数:thread_pool.listener.size 控制线程池的大小,默认为 1。
写线程池(write):

作用:用于处理写请求,包括索引、更新和删除操作。
示例参数:thread_pool.write.size 控制线程池的大小,默认为 CPU 核心数 * 2。
管理线程池(management):

作用:用于执行集群管理任务,如节点发现、配置更改等。
示例参数:thread_pool.management.size 控制线程池的大小,默认为 1。
复制线程池(get):

作用:用于处理文档的复制请求。
示例参数:thread_pool.get.size 控制线程池的大小,默认为 CPU 核心数 * 2。
搜索远程线程池(search_remote):

作用:用于执行远程搜索请求。
示例参数:thread_pool.search_remote.size 控制线程池的大小,默认为 CPU 核心数。

Es使用层面

Es的基本概念

  • 集群(Cluster):集群由一个唯一的名字标识,默认为Elasticsearch。集群名称非常重要,具有相同集群名称的节点才会组成一个集群。集群名称可以在配置文件中指定。需要注意的是,Elasticsearch中一个节点也被称为集群。
  • 节点(Node):用于存储集群的数据,参与集群的索引和搜索功能。像集群有名字一样,节点也有自己的名字,默认在启动时会以一个随机的UUID的前7个字符作为节点的名字,用户可以为其指定任意的名字。多个节点通过同一个集群名在网络中发现同伴组成一个集群。一个节点也可以是集群。
  • 索引(Index):索引是一个文档数据的集合。每个索引都有唯一的名称,用户通过这个名称来操作它。一个集群中可以有任意多个索引。
  • 类型(Type):在一个索引中,可以存放不同类型的文档,如用户数据、订单数据等。一个索引中只存放一类数据。
  • 文档(Document):用JSON格式来表示,存储在索引库中的一条数据。
  • 分片(Shard):在创建索引时可以指定分成多少个分片来存储。每个分片本身也是一个功能完善且独立的“索引”,可以被放置在集群的任意节点上,从而实现负载均衡。合理的分片数量可以提高Elasticsearch服务的性能。
  • 复制(Replication):一个分片可以有多个副本,以防止数据丢失和避免数据丢失后服务不可用。

在关系型数据库中,用户会创建数据库;在Elasticsearch中对应的是创建索引,俗称索引库。
关系型数据库中的表在Elasticsearch中已经没有对应的项。
对于关系型数据库中的行,在Elasticsearch中称为文档。
而关系型数据库的列在Elasticsearch中是由字段体现的,表结构使用映射体现。
因此Elasticsearch的大体架构就是创建索引库,也可以给索引库指定映射和字段类型,在Elasticsearch索引库中存储的基本单位就是文档数据。

Elasticsearch映射

映射(Mapping)是定义文档及其包含的字段如何存储在索引库中的过程。
每个文档都是一个字段的集合,每个字段都有各自的数据类型。
映射数据时,可以创建一个映射的定义,其中包含与文档相关字段的列表。
映射定义还包括元数据字段(如_source字段),它自定义如何处理文档关联的元数据,而Elasticsearch支持动态映射和显式映射。
每种方法根据不同的使用情况有不同的好处。

如果要明确定义映射或者更好地控制创建字段和字段类型,则可以利用Elasticsearch的显式映射。
动态映射和显式映射的详细说明如下:

动态映射

动态映射是Elasticsearch非常重要的功能之一,使用户不需要关注存储结果,能够在业务中尽快使用数据进行搜索。
使用动态映射在写入索引文档数据时,不需要先创建索引和定义字段。
索引中的字段和字段类型都将自动创建

1
2
3
4
PUT datadb/_doc/1
{
"count":10
}

以上语句创建了一个名为datadb的索引库,然后为此索引库添加count字段,该字段的数据类型是long,而long类型都是动态映射自动生成的。

动态映射模板

动态映射模板使得在默认的动态字段映射规则之外能够更好地控制Elasticsearch如何映射数据。
用户可以通过将dynamic参数设置为true来启用动态映射模式,然后使用动态模板来定义自定义映射,这些映射可以根据匹配条件应用于动态添加的字段。

相关参数说明如下:

  • match_mapping_type:表示对Elasticsearch检测到的数据类型进行操作。
  • match和unmatch:表示使用模式匹配来匹配字段名称。

需要注意的是,如果动态映射模板未定义match_mapping_type或者match参数,则不会匹配任何字段。




Elasticsearch字段类型

  • Elasticsearch中的别名字段的详解和范例
  • Elasticsearch中的数组和二进制类型的详解和范例
  • Elasticsearch中的数字、布尔、日期类型的详解和范例
  • Elasticsearch中的嵌套、范围、排名类型的详解和范例
  • Elasticsearch中的对象、地理位置类型的详解和范例
  • Elasticsearch中的字符串类型text的详解和范例
  • Elasticsearch中的字符串类型keyword的详解和范例

Elasticsearch中的字段类型分为如下几类

  • 常用类型binary:表示可以存储编码为Base64的字符串或者二进制值。
    • boolean:表示可以存储true和false的布尔值。
    • keyword:该字段类型的数据在存储时不会进行分词处理,适合进行统计分析,不能进行全文搜索。
    • numbers:用于表示数字类型,例如long和double。
    • date:表示可以存储日期类型的数据。
    • alias:表示为现有字段定义别名。
    • text:该字段类型的数据在存储时会进行分词并建立索引,适合进行全文搜索,不能进行统计分析。
  • 对象和关系类型
    • object:表示一个JSON对象。
    • nested:嵌套类型,对象中可以嵌套对象。
    • array:在Elasticsearch中,数组不需要专用的字段类型。默认情况下,任何字段都可以包含0个或多个值,但是数组中的所有值必须具有相同的字段类型。
  • 其余类型range:表示范围类型。
    • rank_feature:表示排名类型。
    • token_count:表示令牌计数类型。
    • ip:用于IP地址的存储和查询的类型。
    • geo_point,geo_shape:用于地理位置和空间位置的存储与搜索的类型。




内置分词器和IK分词器

  • simple分词器详解
  • simple_pattern分词器
  • 详解simple_pattern_split分词器详解
  • text类型和keyword类型的区别

simple分词器

simple分词器是对字母文本进行分词拆分,并将分词后的内容转换成小写格式。

simple_pattern分词器

Elasticsearch还提供了根据正则表达式进行分词的分词器(simple_pattern分词器)

设置正则规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT myindex-simple-pattern
{
"settings":{
"analysis":{
"my_analyzer":{
"tokenizer":"my_tokenizer"
}
},
"tokenizer":{
"my_tokenizer":{
"type":"simple_pattern",
"pattern":"[0123456789]{#3}"# 如果有三个数字一起则当做一个单词
}
}
}
}

fd-123-4567-890-xxd9-689-x987

1
[123, 456, 890, 689, 987]

simple_pattern_split分词器

simple_pattern_split(指定分词符号)分词器比simple_pattern分词器功能更有限,但是分词效率较高。
默认模式下它的分词匹配符号是空字符串。
需要注意的是使用此分词器应该根据业务进行配置,而不是简单地使用默认匹配模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT myindex-simple-pattern_split
{
"settings":{
"analysis":{
"my_analyzer":{
"tokenizer":"my_tokenizer"
}
},
"tokenizer":{
"my_tokenizer":{
"type":"simple_pattern_split",
"pattern":"-"#使用"-"符号进行分词
}
}
}
}
1
2
3
4
5
POST myindex-simple-pattern_split/_analyze
{
"analyzer":"my_analyzer",
"text":"fd-123-123-123-123"
}

standard分词器

standard(标准)分词器是Elasticsearch中默认的分词器,它是基于Unicode文本分割算法进行分词的。

standard分词器还提供了两种参数。

  • max_token_length: 分词单词最大长度
  • stopwords: 表示停用词

自定义个与strandard类似的分词器,原定义配置参数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT custom_standard_analyzer_index
{
"settings":{
"analysis":{
"analyzer":{
"rebuild_analyzer":{
"type":"keyword",
"tokenizer":"standard",
"filter":["lowercase"] #单词全部转成小写
}
}
}
}
}




keyword类型和text类型的区别

  • text字段类型会进行分词处理,然后根据分词后的单词建立倒排索引(反向索引),因而不支持聚合计算。
  • keyword字段类型不会进行分词处理,直接根据字符串的内容建立倒排索引(反向索引),支持聚合计算和排序操作。

案例展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
## 索引映射
PUT /clayindex
{
"mappings":{
"properties":{
"name":{
"type":"keyword",
},
"address":{
"type":"text"
},
"age":{
"type":"integer"
}
}
}
}

POST /clayindex/_doc
{
"name":"曹操",
"address":"魏国",
"age":18
}
POST /clayindex/_doc
{
"name":"关羽",
"address":"蜀国",
"age":18
}
POST /clayindex/_doc
{
"name":"周瑜",
"address":["吴国","蜀国"],
"age":18
}

搜索

1
2
GET clayindex/_doc/_search?q=name:曹操
## 数据准确展示
1
2
GET clayindex/_doc/_search?q=adress:魏国
## 竟然搜出了蜀国

address值等于”魏国”的全文搜索竟然搜索出了”蜀国”等文档数据。
导致出现这种情况的原因是,name字段为keyword类型,存储时没有被分词,搜索的时候也被当作一个完整的词去匹配;
而address字段是text类型,存储时会进行分词处理,搜索的时候先对搜索的内容进行分词,再和文档数据(文档数据也会进行分词)进行匹配。

再举例

将”做一朵向日葵,面朝太阳,心纳阳光”进行分词
最后分成了”[做,一,朵,向,日,葵,面,朝,太,阳,心,纳,阳,光]”单独的14个词。
搜索条件也会进行分词……


IK分词器

前面的范例创建索引、搜索数据时都是使用默认的分词器,因为存储的都是中文,所以分词效果不太理想,会把text的字段分成一个个汉字,为了更好地对中文内容进行分词,需要更加智能的IK分词器。

案例展示

未使用IK分词器范例

1
2
3
4
5
#对内容"内心没有分别心,就是真正的苦行"利用默认分词器分析
POST _analyze
{
"text":"内心没有分别心,就是真正的苦行"
}

分词的结果会把中文内容分词成单独的汉字。下面来看使用IK分词器的范例

使用IK分词器范例

1
2
3
4
5
POST _analyze
{
"analyzer":"ik_max_word",
"text":"内心没有分别心,就是真正的苦行"
}

利用IK分词器,以上内容被分词为”[内心,没有,分别,心,就是,真正,的,苦行]”。

IK分词模式

IK分词器有以下两种分词模型

  • ik_max_word:对文本进行最细粒度的拆分。
  • ik_smart:对文本进行最粗粒度的拆分。

ik_max_word

1
2
3
4
5
POST _analyzer
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}

语句使用ik_max_word模式将”中华人民共和国国歌”拆分为”[中华人民共和国,中华人民,中华,华人,人民共和国,人民,共和国,共和,国,国歌]”,产生了各种可能的组合,即不同的词。

ik_smart

对内容”中华人民共和国国歌”使用ik_smart分词模式

1
2
3
4
5
POS _analyzer
{
"analyzer":"ik_smart",
"text":"中华人民共和国国歌"
}

以上语句使用ik_smart模式将”中华人民共和国国歌”拆分为”[中华人民共和国,国歌]”,产生了尽可能少的组合。

创建使用IK分词器的索引映射

一般在创建索引时会明确指定分词的模式,总共有两种操作:一种是让所有text类型的字段都使用分词模式,另一种是给每一种text类型的字段指定分词模式。

下面的范例是所有text类型的字段都使用同一种分词模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## 创建索引模板,所有text类型的字段都是用IK分词器的"ik_max_word"模式
PUT myindex_ik
{
"settings":{
"analysis":{
"analyzer":{
"ik":{
"tokenizer":"ik_max_word"
}
}
}
},
"mappings":{
"properties":{
"field1":{
"type":"text"
},
"field2":{
"type":"integer"
},
"field3":{
"type":"text"
},
"field4":{
"type":"text"
},
}
}
}

下面的范例为每一个text类型字段分别指定特定的分词模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PUT myindex_ik_01
{
"mappings":{
"properties":{
"field1":{
"type":"text",
"analyzer":"ik_max_word",
"search_analyzer":"ik_max_word",
},
"field2":{
"type":"integer"
},
"field3":{
"type":"text",
"analyzer":"standard",
"search_analyzer":"standard",
},
"field1":{
"type":"text",
"analyzer":"ik_max_word",
"search_analyzer":"ik_smart",
}
}
}
}

由以上语句可知,field1字段使用ik_max_word分词模式,field3字段使用standard分词器的默认模式,field4字段使用IK分词器的ik_smart模式。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
PUT /clayindex_ik
{
"mappings":{
"properties":{},
"name":{
"type":"keyword"
},
"address":{
"text":"text",
"analyzer":"ik_max_word",
"search_analyzer":"ik_smart"
},
"age":{
"type":"integer"
}
}
}

POST /clayindex_ik/_doc
{
"name":"曹操",
"address":"魏国",
"age":18
}
POST /clayindex/_doc
{
"name":"关羽",
"address":"蜀国",
"age":18
}
POST /clayindex/_doc
{
"name":"周瑜",
"address":["吴国","蜀国"],
"age":18
}

搜索

1
2
3
4
5
6
7
8
9
10
11
POST /clayindex_ik/_doc/_search
{
"query":{
"match":{
"address":{
"query":"魏国",
"analyzer":"ik_smart" #这句可以不写,默认也是ik_smart模式
}
}
}
}

在正式项目的使用中也推荐这种做法,存储时选择尽量细的分词规则,这样在搜索时可以指定符合具体项目要求的分词模式。






Elasticsearch基础查询详解

  • 批量数据插入操作
  • 分页查询和排序查询操作
  • 控制返回字段操作
  • 精准查询操作
  • 条件查询和多条件查询操作
  • 简单的聚合查询操作

主要讲解Elasticsearch中经常使用的基础查询操作,比如数据批量插入、分页、排序和简单的聚合(最大值计算、最小值计算、平均值计算、求和计算等)。
将通过具体的范例帮助读者了解Elasticsearch的基础操作。

批量插入

1
2
3
4
5
POST /userinfo/_doc/_bulk
{"index":{}}
{"name":"张三","address":"中国","age":18}
{"index":{}}
{"name":"李四","address":"中国","age":20}

查询所有数据

1
2
3
4
GET /userinfo/_search
{
"query":{"match_all":{}}
}

以上语句返回了userinfo索引库中所有的文档数据。
以下字段为返回信息中的重要信息

  • took:表示查询所花费的时间,以毫秒为单位。
  • timed_out:表示查询请求是否超时。
  • shards:表示总共查询了多少个分片,以及是否成功。
  • max_score:表示查询到的相关文档的分数,分数越高,匹配度就越高。
  • hits.total.value:表示找到了多少个匹配的文档。
  • hits._score:表示当前文档和查询内容的匹配度,使用match_all时,此字段的值没有任何意义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "userinfo",
"_type" : "_doc",
"_id" : "FhDqW3oBiBO67ReogO86",
"_score" : 1.0,
"_source" : {
"name" : "张三",
"address" : "中国",
"age" : 18
}
},
{
"_index" : "userinfo",
"_type" : "_doc",
"_id" : "FxDqW3oBiBO67ReogO86",
"_score" : 1.0,
"_source" : {
"name" : "李四",
"address" : "中国",
"age" : 19
}
},
{
"_index" : "userinfo",
"_type" : "_doc",
"_id" : "GBDqW3oBiBO67ReogO86",
"_score" : 1.0,
"_source" : {
"name" : "王五",
"address" : "美国",
"age" : 20
}
},
{
"_index" : "userinfo",
"_type" : "_doc",
"_id" : "GRDqW3oBiBO67ReogO86",
"_score" : 1.0,
"_source" : {
"name" : "赵六",
"address" : "美国",
"age" : 21
}
}
]
}
}

排序查询

1
2
3
4
5
6
7
8
#查询userinfo索引库中所有的文档数据,根据age字段进行倒序输出
GET /userinfo/_search
{
"query": { "match_all": {} },
"sort":[
{"age":{"order":"desc"}}
]
}

如果需要根据多个字段排序,则可以使用下面的语句
以上语句表示先根据field1字段进行倒序排序,再根据field2字段进行顺序排序。
要注意的是,排序字段不可以是text等特殊类型,一般是整数类型和keyword类型。

1
2
3
4
5
6
7
8
GET /索引名称/_search
{
"query": { "match_all": {} },
"sort":[
{"field1":{"order":"desc"}},
{"field2":{"order":"asc"}}
]
}

根据需求返回相应的字段

1
2
3
4
5
6
GET /索引名称/_search
{
"query": { 查询语句 },
"_source":["字段1","字段2",…]

}

范例

1
2
3
4
5
6
7
8
9
#查询userino索引库中所有的文档数据,根据age字段进行顺序排序,只需要返回name和age字段内容
GET /userinfo/_search
{
"query": { "match_all": {} },
"_source":["name","age"],
"sort":[
{"age":{"order":"asc"}}
]
}

分页查询

1
2
3
4
5
6
7
8
GET /索引库名称/_search
{
"query": { 查询语句 },
"_source":["字段1","字段2",…],
"from":0,
"size":10

}

范例

1
2
3
4
5
6
7
8
9
10
GET /userinfo/_search
{
"query": { "match_all": {} },
"_source":["name","age"],
"sort":[
{"age":{"order":"asc"}}
],
"from":0,
"size":2
}

以上语句将查询userinfo索引库的所有文档数据,并根据age字段按顺序排序,返回从第0条到第2条的文档数据,只包含name和age字段的内容

查询指定字段内的特定字词

如果想要在Elasticsearch中查询指定字段内的特定字词,可使用match进行查询

1
2
3
4
5
6
7
8
9
10
GET /索引库名称/_search
{
"query": {
"match":
{
"address": "查询内容"
}

}
}

范例

1
2
3
4
5
6
7
8
9
10
GET /userinfo/_search
{
"query": {
"match":
{
"address": "中国,美国"
}

}
}

可以看到返回的结果数据符合预期,文档中address字段内容不是包含”中国”就是包含”美国”。

段落匹配查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#批量插入数据
POST /userinfo_002/_doc/_bulk
{ "index": {}}
{"name":"张三","address":"中国 上海","age":18}
{ "index": {}}
{"name":"李四","address":"中国 上海","age":19}
{ "index": {}}
{"name":"王五","address":"中国 杭州","age":20}
{ "index": {}}
{"name":"大刀王五","address":"中国 杭州","age":21}

#查询索引库中address字段内容是"中国 杭州"的文档数据
GET /userinfo_002/_search
{
"query": {
"match":
{
"address": "中国 上海"
}

}
}

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 1.5970153,
"hits" : [
{
"_index" : "userinfo_002",
"_type" : "_doc",
"_id" : "LhAiXHoBiBO67Reo6u9y",
"_score" : 1.5970153,
"_source" : {
"name" : "张三",
"address" : "中国 上海",
"age" : 18
}
},
{
"_index" : "userinfo_002",
"_type" : "_doc",
"_id" : "LxAiXHoBiBO67Reo6u9y",
"_score" : 1.5970153,
"_source" : {
"name" : "李四",
"address" : "中国 上海",
"age" : 19
}
},
{
"_index" : "userinfo_002",
"_type" : "_doc",
"_id" : "MBAiXHoBiBO67Reo6u9y",
"_score" : 0.21072102,
"_source" : {
"name" : "王五",
"address" : "中国 杭州",
"age" : 20
}
},
{
"_index" : "userinfo_002",
"_type" : "_doc",
"_id" : "MRAiXHoBiBO67Reo6u9y",
"_score" : 0.21072102,
"_source" : {
"name" : "大刀王五",
"address" : "中国 杭州",
"age" : 21
}
}
]
}
}

由以上返回结果可知,因为查询的时候两个内容之间有空格,所以被当作分隔符处理,查询内容被分词
如果想要查询的内容不被分词,可使用match_phrase查询,语句如下:

1
2
3
4
5
6
7
8
9
10
GET /userinfo_002/_search
{
"query": {
"match_phrase":
{
"address": "中国 上海"
}

}
}

term精准查询

在Elasticsearch中使用term和前面使用match_phrase的效果类似。
如下范例是创建一个索引模板,其address字段在存储的时候使用ik_max_word模式进行分词存储
而查询的时候根据standard分词器的默认模式(中文语句会被拆成一个个汉字)进行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
PUT /myindex_term
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"address":{
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "standard"
},
"age":{
"type": "integer"
}
}
}
}
#批量新增数据
POST /myindex_term/_doc/_bulk
{ "index": {}}
{"name":"张三","address":"魏国","age":18}
{ "index": {}}
{"name":"李四","address":"吴国","age":18}
{ "index": {}}
{"name":"王五","address":"蜀国","age":20}
#利用"match"进行查询
GET /myindex_term/_doc/_search
{
"query": {
"match":
{
"address": "魏国"
}

}
}

可以看到,返回结果中没有匹配的文档数据,这是因为在存储的时候,”吴国”、”魏国”、”蜀国”经过ik_max_word模式进行分词,分词之后的内容依旧是”吴国”、”魏国”、”蜀国”。

可以看到“魏国”被分词后还是“魏国”。
而我们在进行查询的时候,是根据standard分词器默认进行分词的,会把单个汉字作为一个词term

1
2
3
4
5
6
7
GET _analyze
{
"analyzer":"standard",
"text":"魏国"
}

返回的是一个"魏",一个"国"

可以看到”魏国”被分成单独的两个汉字”魏”和”国”。
在查询时,就是在”魏国”,”吴国”,”蜀国”三个词中去匹配是否有等于”魏”或者”国”的数据,所以这就导致没有查询出任何结果。
对于这样的情况,我们可以使用term进行查询,把”魏国”当作一个单独的词不进行分词,然后进行查询。

1
2
3
4
5
6
7
8
9
10
GET /myindex_term/_doc/_search
{
"query": {
"term":
{
"address": "魏国"
}

}
}

bool多条件查询

在使用Elasticsearch查询时,如果想要构造更复杂的查询(即搜索),可以使用”bool”来组合多个查询条件。

使用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /索引库名称/_search
{
"query": {
"bool": {
"must": [
{ "match": { "字段1": "数据1" } }
],
"must_not": [
{ "match": { "字段2": "数据2" } }
]
}
}
}

范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#批量插入数据
POST /userinfo_003/_doc/_bulk
{ "index": {}}
{"name":"张三","address":"中国上海","age":18}
{ "index": {}}
{"name":"李四","address":"中国杭州","age":18}
{ "index": {}}
{"name":"王五","address":"中国杭州","age":20}
{ "index": {}}
{"name":"大刀王五","address":"中国上海","age":21}
#搜索索引库中age字段内容等于18并且address字段内容中不包含"中国上海"的数据
GET /userinfo_003/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "18" } }
],
"must_not": [
{ "match_phrase": { "address": "中国上海" } }
]
}
}
}

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "userinfo_003",
"_type" : "_doc",
"_id" : "MtREXHoBFXh0Ma_ypCIn",
"_score" : 1.0,
"_source" : {
"name" : "李四",
"address" : "中国杭州",
"age" : 18
}
}
]
}
}

bool和filter组合查询

下面的范例使用bool和filter组合查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#搜索address字段内容中包含"杭州"并且满足age大于等于10、小于等于20的文档数据
GET userinfo_003/_doc/_search
{

"query":{

"bool": {
"must": [
{
"match": {
"address": "杭州"

}
}
],
"filter": {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
}
}

}
}

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.3862942,
"hits" : [
{
"_index" : "userinfo_003",
"_type" : "_doc",
"_id" : "MxCnX3oBiBO67Reoqu-z",
"_score" : 1.3862942,
"_source" : {
"name" : "李四",
"address" : "中国杭州",
"age" : 18
}
},
{
"_index" : "userinfo_003",
"_type" : "_doc",
"_id" : "NBCnX3oBiBO67Reoqu-z",
"_score" : 1.3862942,
"_source" : {
"name" : "王五",
"address" : "中国杭州",
"age" : 20
}
}
]
}
}

简单的聚合查询

我们知道在SQL语句中有group by,而在Elasticsearch中把它叫Aggregation,即聚合运算。
下面通过范例来理解和学习聚合查询。

1
2
3
4
5
6
7
8
9
10
#批量考试成绩数据,并使用程序自动生成索引映射
POST /myindex_aggs/_doc/_bulk
{ "index": {}}
{"name":"张三","address":"上海","age":18,"score":60}
{ "index": {}}
{"name":"李四","address":"杭州","age":18,"score":70}
{ "index": {}}
{"name":"王五","address":"杭州","age":20,"score":80}
{ "index": {}}
{"name":"大刀王五","address":"上海","age":21,"score":90}

分组统计各组的总条数

以下范例是按地址进行分组,统计每个地址的考试人数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword"
}
}
}
}


{
"took" : 2,
"timed_out" : false,

"hits" : {
"total" : {
"value" : 4, // 统计总条数
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 2
},
{
"key" : "杭州",
"doc_count" : 2
}
]
}
}
}

分组统计每组的平均值

以下范例是按地址进行分组,统计每个地址中考生的平均分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword"
},
"aggs": {
"score_avg": {
"avg": {
"field": "score"
}
}
}
}
}
}



{
"took" : 6,

"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 2,
"score_avg" : {
"value" : 75.0
}
},
{
"key" : "杭州",
"doc_count" : 2,
"score_avg" : {
"value" : 75.0
}
}
]
}
}
}

分组统计每组的最大值

以下范例是按地址进行分组,统计每个地址考试成绩中的最高分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword"
},
"aggs": {
"score_max": {
"max": {
"field": "score"
}
}
}
}
}
}



{
"took" : 2,

"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 2,
"score_max" : {
"value" : 90.0
}
},
{
"key" : "杭州",
"doc_count" : 2,
"score_max" : {
"value" : 80.0
}
}
]
}
}
}

分组统计每组的最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword"
},
"aggs": {
"score_min": {
"min": {
"field": "score"
}
}
}
}
}
}


{
"took" : 9,

"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 2,
"score_min" : {
"value" : 60.0
}
},
{
"key" : "杭州",
"doc_count" : 2,
"score_min" : {
"value" : 70.0
}
}
]
}
}
}

分组统计每组的总和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword"
},
"aggs": {
"score_sum": {
"sum": {
"field": "score"
}
}
}
}
}
}



{
"took" : 2,

"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 2,
"score_sum" : {
"value" : 150.0
}
},
{
"key" : "杭州",
"doc_count" : 2,
"score_sum" : {
"value" : 150.0
}
}
]
}
}
}

分组统计每组的最小值并按统计结果排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
GET /myindex_aggs/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"terms": {
"field": "address.keyword",
"order": {
"score_min": "desc"
}
},
"aggs": {
"score_min": {
"min": {
"field": "score"
}
}
}
}
}
}


{
"took" : 2,

"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_address" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "杭州",
"doc_count" : 2,
"score_min" : {
"value" : 70.0
}
},
{
"key" : "上海",
"doc_count" : 2,
"score_min" : {
"value" : 60.0
}
}
]
}
}
}




Elasticsearch的组合查询和全文搜索详解

  • 布尔查询详解
  • 提高评分查询详解
  • 固定评分查询详解
  • 最佳匹配查询详解
  • 使用函数查询详解
  • 全文搜索的多种查询方式详解

接下来将通过大量的范例讲解Elasticsearch的各种组合查询,比如布尔查询、提高评分查询以及非常重要的全文索引查询(即全文索引搜索)。
全文索引查询包含高亮查询、自定义高亮查询、顺序匹配查询等。

组合查询的布尔查询

布尔查询是组合查询中常用的查询方式,它将多个查询条件组合在一起,并且将查询的结果和结果的评分组合在一起。
当查询条件是多个表达式的组合时,布尔查询将变得非常有用。

实际上布尔查询是把多个子查询组合成一个布尔表达式,所有子查询之间的逻辑关系是and(“与”逻辑,即“并且”的意思),只有当一个文档满足布尔查询中的所有子查询条件时,
Elasticsearch引擎才认为该文档满足查询条件。

布尔查询有以下两个特点:

  • 子查询可以以任意顺序出现。
  • 查询语句中可以嵌套多个查询条件,其中包括布尔查询。布尔查询包含以下4种操作符,并且它们均是一种数组,数组中是对应的判断条件。
    • must:必须匹配(返回结果中评分字段的结果有意义)。
    • must_not:过滤子句,必须不能匹配(返回结果中评分字段的结果无意义)。
    • should:选择性匹配,至少满足一条(返回结果中评分字段的结果有意义)。
    • filter:过滤子句,必须匹配(返回结果中评分字段的结果无意义)。

范例一:搜索同时满足must、must_not和should子句条件的数据

以下语句表示查询索引库中name字段的值必须等于”张三”,tags字段内容包含production,tags1字段内容要么包含env1要么包含deployed,且满足age在10和20之间的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "username" : "张三" }
},
"filter": {
"term" : { "tags" : "production" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tags1" : "env1" } },
{ "term" : { "tags1" : "deployed" } }
],
"minimum_should_match" : 2, #表示命中两个term文档才会被返回
"boost" : 1.0
}
}
}

搜索同时满足must和must_not子句并且满足should子句中任何一个条件的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#数据准备
POST /myindex_bool/_doc/_bulk
{ "index": {}}
{"name":"张三","address":"china shanghai","age":18,"score":60,"tags":"emp manager love"}
{ "index": {}}
{"name":"李四","address":"china hangzhou","age":18,"score":70,"tags":"emp love"}
{ "index": {}}
{"name":"王五","address":"china hangzhou","age":20,"score":80,"tags":"emp manager "}
{ "index": {}}
{"name":"大刀王五","address":"china shanghai","age":21,"score":90,"tags":" manager love"}
#查询address字段中包含china单词,tags字段中包含emp单词,age不在20和30之间,且满足tags字段中包含manager或者love中的一项的字段
POST /myindex_bool/_search
{
"query": {
"bool" : {
"must" : {
"term" : { "address" : "china" }
},
"filter": {
"term" : { "tags" : "emp" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 20, "lte" : 30 }
}
},
"should" : [
{ "term" : { "tags" : "manager" } },
{ "term" : { "tags" : "love" } }
],
"minimum_should_match" : 1, #表示上面should中的条件最少命中1个选项才被返回
"boost" : 1.0
}
}
}



{
"took" : 4,

"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.73310846,
"hits" : [
{
"_index" : "myindex_bool",
"_type" : "_doc",
"_id" : "RhDvX3oBiBO67ReoIe_r",
"_score" : 0.73310846,
"_source" : {
"name" : "张三",
"address" : "china shanghai",
"age" : 18,
"score" : 60,
"tags" : "emp manager love"
}
},
{
"_index" : "myindex_bool",
"_type" : "_doc",
"_id" : "RxDvX3oBiBO67ReoIe_r",
"_score" : 0.47901997,
"_source" : {
"name" : "李四",
"address" : "china hangzhou",
"age" : 18,
"score" : 70,
"tags" : "emp love"
}
}
]
}
}

以上结果符合我们的查询要求。
minimum_should_match选项需要特别注意,表示should选项条件中最少命中的分词数量。
如下语句把minimum_should_match选项设置为2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
POST /myindex_bool/_search
{
"query": {
"bool" : {
"must" : {
"term" : { "address" : "china" }
},
"filter": {
"term" : { "tags" : "emp" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 20, "lte" : 30 }
}
},
"should" : [
{ "term" : { "tags" : "manager" } },
{ "term" : { "tags" : "love" } }
],
"minimum_should_match" : 2,
"boost" : 1.0
}
}
}
#以上语句要求tags字段内容必须包含manager和love


{
"took" : 2,

"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.73310846,
"hits" : [
{
"_index" : "myindex_bool",
"_type" : "_doc",
"_id" : "RhDvX3oBiBO67ReoIe_r",
"_score" : 0.73310846,
"_source" : {
"name" : "张三",
"address" : "china shanghai",
"age" : 18,
"score" : 60,
"tags" : "emp manager love"
}
}
]
}
}




组合查询的提高评分查询

提高评分(boosting)查询不同于布尔查询,在布尔查询中只要一个子查询的条件不匹配,那么此文档数据就不符合要求。
提高评分查询其实是降低文档评分,比如查询条件是”name = ‘clay’ and address =’china’”,对于只满足部分条件的文档数据,不是不返回,而是降低显示的优先级(也就是评分字段的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#数据准备
POST /myindex-boosting/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple Fruit" }
{ "index": { "_id": 3 }}
{ "content":"Apple and Pie " }
#查询content字段中是否包含apple,并对包含pie的文档数据做降级处理(降低评分)
GET /myindex-boosting/_search
{
"query": {
"boosting": {
"positive": { ## 积极的
"term": {
"content": "apple"
}
},
"negative": { ## 消极的
"term": {
"content": "pie"
}
},
"negative_boost": 0.5#此值小于1表示降低评分,大于1表示提高评分
}
}
}




组合查询的固定评分查询

当根据某个条件查询时,如果需要返回固定评分(constant_score),只需要使用filter和constant_score进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#数据准备
POST /myindex-constant/_bulk
{ "index": { "_id": 1 }}
{ "content":"huawei Mac" }
{ "index": { "_id": 2 }}
{ "content":"huawei Mac" }

#固定分数查询
GET /myindex-constant/_search
{
"query": {
"constant_score": {
"filter": {
"term": { "content": "huawei" }
},
"boost": 1.8 // 不写,则默认为1
}
}
}



{
"took" : 1,

"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.8,
"hits" : [
{
"_index" : "myindex-constant",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.8,
"_source" : {
"content" : "huawei Mac"
}
},
{
"_index" : "myindex-constant",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.8,
"_source" : {
"content" : "huawei Mac"
}
}
]
}
}




组合查询的最佳匹配查询

dis_max指的是在文档匹配评分中,只将最佳匹配的评分作为查询的评分结果返回。

1
2
3
4
5
6
#数据准备
POST /myindex-dis-max/_bulk
{ "index": { "_id": 1 }}
{"title": "Quick brown rabbits","body": "Brown rabbits are commonly seen."}
{ "index": { "_id": 2 }}
{"title": "Keeping pets healthy","body": "My quick brown fox eats rabbits on a regular basis."}

如果用户要在title和body字段中查询包含brown fox的文档,那么很有可能只是想搜索与”brown fox”相关的词。根据以上两条文档信息,我们可以用肉眼判断出_id等于2的文档匹配度更高一些,因为”brown fox”在此文档的内容中是连续的。

我们先通过下面的语句进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
GET /myindex-dis-max/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "brown fox" }},
{ "match": { "body": "brown fox" }}
]
}
}
}


{
"took" : 1,

"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.90425634,
"hits" : [
{
"_index" : "myindex-dis-max",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.90425634,
"_source" : {
"title" : "Quick brown rabbits",
"body" : "Brown rabbits are commonly seen."
}
},
{
"_index" : "myindex-dis-max",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.77041256,
"_source" : {
"title" : "Keeping pets healthy",
"body" : "My quick brown fox eats rabbits on a regular basis."
}
}
]
}
}

从上面的返回结果可以看出,_id等于1的文档评分是0.90425634,_id等于2的文档评分是0.77041256。
这和我们预期的结果不一致。
面对这样的问题,可以使用dis_max进行查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
GET /myindex-dis-max/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "brown fox" }},
{ "match": { "body": "brown fox" }}
],
"tie_breaker": 0
}
}
}


{
"took" : 2,

"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.77041256,
"hits" : [
{
"_index" : "myindex-dis-max",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.77041256,
"_source" : {
"title" : "Keeping pets healthy",
"body" : "My quick brown fox eats rabbits on a regular basis."
}
},
{
"_index" : "myindex-dis-max",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.6931471,
"_source" : {
"title" : "Quick brown rabbits",
"body" : "Brown rabbits are commonly seen."
}
}
]
}
}




组合查询的使用函数查询

Elasticsearch中使用function_score进行查询可以让用户修改文档的评分。要使用function_score进行查询,用户必须为查询定义一个或多个函数,这些函数将为查询返回的每个文档计算一个新的评分(也就是修改返回结果中_score字段的值)。

Elasticsearch为用户预定义了如下函数

  • weight:表示为每个文档应用一个简单的权重:当weight为2时,最终结果为2 * _score。
  • field_value_factor:表示使用这个值来修改_score。
  • random_score:表示为每个用户都使用一个不同的随机评分。衰减函数:包括linear、exp、gauss等函数。
  • script_score:表示如果需求超出以上几种函数的范围,用户可以使用自定义脚本控制评分的计算。

使用random_score函数的范例

1
2
3
4
5
6
7
8
9
10
11
GET  /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"random_score": {},
"boost_mode": "multiply"
}
}
}

使用random_score函数的复杂查询范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"functions": [
{
"filter": { "match": { "test": "bar" } },
"random_score": {},
"weight": 23
},
{
"filter": { "match": { "test": "cat" } },
"weight": 42
}
],
"max_boost": 42,
"score_mode": "max",
"boost_mode": "multiply",
"min_score": 42
}
}
}

在以上语句中,首先每一个文档会根据定义的函数计算出一个分值,然后根据参数score_mode指定的规则计算最后的评分。

脚本分数

script_score函数允许用户嵌套一个查询条件,并使用脚本表达式计算评分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /_search
{
"query": {
"function_score": {
"query": {
"match": { "message": "Elasticsearch" }
},
"script_score": {
"script": {
"source": "Math.log(3 + doc['my-int'].value)"
}
}
}
}
}

需要注意的是在Elasticsearch中,所有文档分数都是正的32位浮点数。
如果script_score函数计算出来更精确的评分,它将被转换为最接近的32位浮点数。
而且必须保证评分是非负的,否则Elasticsearch将会返回一个错误信息。

权重

在Elasticsearch中,使用权重计算评分非常简单,只需要在script_score函数中加入如下语句

1
"weight" : number

范例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"functions": [
{
"filter": { "match": { "test": "bar" } },
"weight": 1
},
{
"filter": { "match": { "test": "cat" } },
"weight": 2
}
]
}
}
}
field_value_factor函数的使用

用户可以调用field_value_factor函数通过文档中的某一个字段来计算评分。如下范例通过文档的my-int字段来计算评分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /_search

{
"query": {
"function_score": {
"field_value_factor": {
"field": "my-int",
"factor": 1.2,
"modifier": "sqrt",
"missing": 1
}
}
}
}

以上语句中的函数将转化为以下公式来计算评分:

1
sqrt(1.2 * doc['my-int'].value)

field_value_factor函数提供的所有选项说明

函数查询案例

假如有一个网站可以让用户发布博客并且可以让用户为自己喜欢的博客点赞,我们希望将更受欢迎的博客放在搜索结果列表中相对靠前的位置,可以简单地通过存储每个博客的点赞数来实现这个业务。

首先通过如下语句自动创建功能的索引映射。

1
2
3
4
5
6
PUT /blogposts/post/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 6
}

在搜索时,可以将function_score与field_value_factor结合使用,也就是将点赞数与全文相关度结合起来计算文档匹配评分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}


{
"took" : 5,

"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.7260926,
"hits" : [
{
"_index" : "blogposts",
"_type" : "post",
"_id" : "1",
"_score" : 1.7260926,
"_source" : {
"title" : "About popularity",
"content" : "In this post we will talk about...",
"votes" : 6
}
}
]
}
}

以上程序的返回结果中,每个文档的最终评分将通过如下公式计算:new_score = old_score * number_of_votes;




全文搜索的match类型查询

在Elasticsearch中进行全文搜索时,如果要给字段指定要查询的特定字词,可以使用match类型的查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#数据准备
POST /myindex-test-match/_bulk
{ "index": { "_id": 1 }}
{ "title": "The flower and the dog" }
{ "index": { "_id": 2 }}
{ "title": "The flower and the dog are beautiful" }
{ "index": { "_id": 3 }}
{ "title": "the dog are beautiful" }
#使用match类型的查询
GET /myindex-test-match/_search
{
"query": {
"match": {
"title": "flower"
}
}
}

以上语句执行match查询的步骤如下

  • 检查字段类型。title字段是text类型(内容会被分词),说明此字段在存储时和查询时都会进行分词,而且在存储时会建立倒排索引。
  • 分析查询字符串。将查询的字符串”flower”传入标准分词器中,输出的结果是单词”flower”。因为只有一个单词,所以match查询执行的是单个底层term查询。
  • 查找匹配的文档。用term查询在倒排索引中查找”flower”,然后获取一组包含该单词的文档数据。
  • 为每个文档评分。用term查询计算出每个文档的评分。

使用match查询时,返回结果中文档的评分是和该文档中字段的内容长度有关的,即字段内容越短,评分就越高,




全文搜索的match多个词查询

采用上一节准备的数据,通过下面的范例来深入理解使用match设置多个词的查询。

1
2
3
4
5
6
7
8
9
#使用match类型的查询
GET /myindex-test-match/_search
{
"query": {
"match": {
"title": "flower dog"
}
}
}

因为match查询必须查找两个单词(”flower”和”dog”),它在内部实际上先执行两次term查询,然后将两次查询的结果合并起来作为最终的查询结果。
为了做到这点,它将两个term查询嵌入到一个布尔查询中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /myindex-test-match/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"title": "flower"
}
},
{
"term": {
"title": "dog"
}
}
]
}
}
}




全文搜索的控制match的匹配精度

如果用户给定3个查询单词,想查找只包含其中两个的文档,那么我们将逻辑运算符设置成and或者or都不合适。
而match查询支持minimum_should_match(最小匹配参数)选项,我们可以将其设置为某个具体数字。

1
2
3
4
5
6
7
8
9
10
11
GET /myindex-test-match/_search
{
"query": {
"match": {
"title": {
"query":"flower dog the",
"minimum_should_match": 3
}
}
}
}

返回结果符合我们查询的要求,文档内容必须满足匹配到3个单词。
需要注意的是,实际应用中更常用的做法是将其设置为一个百分数,因为我们无法控制用户查询时输入的单词数量。

1
2
3
4
5
6
7
8
9
10
11
GET /myindex-test-match/_search
{
"query": {
"match": {
"title": {
"query":"flower dog the",
"minimum_should_match": "80%"
}
}
}
}




全文搜索的query_string查询

query_string查询是根据运算符(AND/OR)来解析和拆分要搜索的字符串。

1
2
3
4
5
6
7
8
9
GET /myindex-test-match/_search
{
"query": {
"query_string": {
"query": "(dog and) AND (beautiful)",
"default_field": "title"
}
}
}

在以上语句中,查询索引库的title字段,必须匹配到beautiful单词,并且必须匹配dog和and其中任何一个单词的字段。
可以看到,返回结果符合预期。同理,我们也可以使用OR运算符来控制条件查询。

1
2
3
4
5
6
7
8
9
GET /myindex-test-match/_search
{
"query": {
"query_string": {
"query": "(dog and) OR (beautiful)",
"default_field": "title"
}
}
}

以上查询语句中,查询索引库title字段,只要文档内容匹配到beautiful、dog和and其中任何一个单词,就会有查询到的结果被输出。




全文搜索的simple_query_string查询

simple_query_string查询是一种使用简单的语法来解析要查询的字符串,并将其拆分为基于特殊运算符的查询方式。
其语法比query_string查询更受限制,但simple_query_string查询在遭遇无效语法事不会返回错误提示信息。

1
2
3
4
5
6
7
8
9
10
GET /myindex-test-match/_search
{
"query": {
"simple_query_string" : {
"query": "\"the dog\" + (flower | and) + beautiful",
"fields": ["title"],
"default_operator": "and"
}
}
}

可以看到,尽管查询的字符串中存在不正确的语法,simple_query_string查询依旧忽略了这些错误语法,只返回了正确的匹配文档数据。




全文搜索的顺序匹配查询

intervals是时间间隔的意思,在Elasticsearch中它本质上是将多个规则按照顺序匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GET /myindex-test-match/_search
{
"query": {
"intervals" : {
"title" : {
"all_of" : {
"ordered" : true,
"intervals" : [
{
"match" : {
"query" : "flower",#文档内容先匹配"flower"单词
"max_gaps" : 0,
"ordered" : true
}
},
{
"any_of" : {
"intervals" : [ #文档内容再匹配"beautiful"和"dog"两个单词中的其中一个
{ "match" : { "query" : "beautiful" } },
{ "match" : { "query" : "dog" } }
]
}
}
]
}
}
}
}
}






Elasticsearch的term level查询详解

  • term level的exists和ids查询详解
  • term level的前缀和分词查询详解
  • 使用通配符和范围查询详解
  • 使用模糊匹配查询详解
  • 使用正则表达式查询详解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#创建索引映射
PUT /myindex-term-level
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"programming_languages": {
"type": "keyword"
},
"required_matches": {
"type": "long"
}
}
}
}
#批量新增数据
POST /myindex-term-level/_bulk
{ "index": { "_id": 1 }}
{"name": "张三", "programming_languages": [ "c++", "java","dotnet" ], "required_matches": 2}
{ "index": { "_id": 2 }}
{"name": "李四", "programming_languages": [ "java", "php","dotnet" ], "required_matches": 2}
{ "index": { "_id": 3 }}
{"name": "王五", "programming_languages": [ "java", "c++", "dotnet" ], "required_matches": 3, "remarks": "hello world"}
{ "index": { "_id": 4 }}
{"name": "赵六", "programming_languages": [ "java", "c++", "dotnet" ], "required_matches": 3, "remarks": "powerful"}




term level的exists查询

在Elasticsearch中可以使用exists进行查询,以判断文档中是否存在对应的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#查询索引库中存在remarks字段的文档数据
GET /myindex-term-level/_search
{
"query": {
"exists":
{
"field": "remarks"
}
}
}


{
"took" : 2,

"max_score" : 1.0,
"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "王五",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "hello world"
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
}
]
}
}

只返回了_id等于3和_id等于4的文档数据,因为只有这两个文档中存在remarks字段




term level的ids查询

ids就是通过id进行批量查询。我们在写SQL的时候,可能会经常这样写:"select * from table where id in(1,2,3)",通过id匹配,一次性返回多行数据。
而在Elasticsearch中,可以使用ids查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#返回_id等于1和_id等于3的文档数据
GET /myindex-term-level/_search
{
"query": {
"ids":
{
"values": [1,3]
}
}
}



{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "张三",
"programming_languages" : [
"c++",
"java",
"dotnet"
],
"required_matches" : 2
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "王五",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "hello world"
}
}
]
}
}

以上结果只返回了_id等于1和_id等于3的文档数据。需要注意的是,返回结果的顺序和我们查找时设置的顺序没有任何关系。




term level的prefix查询

1
2
3
4
5
6
7
8
9
10
11
#查询索引库中name字段的内容中前缀是"张"的所有文档信息
GET /myindex-term-level/_search
{
"query": {
"prefix": {
"name": {
"value": "张"
}
}
}
}




term level的term单个单词查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#查询索引库中"programming_languages"字段内容中包含"dotnet"的文档数据
GET /myindex-term-level/_search
{
"query": {
"term": {
"programming_languages": "dotnet"
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.1448707,
"_source" : {
"name" : "张三",
"programming_languages" : [
"c++",
"java",
"dotnet"
],
"required_matches" : 2
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.1448707,
"_source" : {
"name" : "李四",
"programming_languages" : [
"java",
"php",
"dotnet"
],
"required_matches" : 2
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "3",
"_score" : 0.1448707,
"_source" : {
"name" : "王五",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "hello world"
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 0.1448707,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
}
]
}
}




term level的terms多个单词查询

1
2
3
4
5
6
7
8
9
#查询索引库中programming_languages字段的内容,包含php和java任何一个单词的文档都会被返回
GET /myindex-term-level/_search
{
"query": {
"terms": {
"programming_languages": ["php","java"]
}
}
}




term level的动态匹配到单词的个数

使用terms_set关键字查询可以统计文档中动态匹配到单词的个数。

范例一:使用terms_set动态设置匹配单词的个数1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#索引库中programming_languages字段的内容匹配到dotnet和php,而最少匹配的单词数量存储在当前文档中的required_matches字段中
GET /myindex-term-level/_search
{
"query": {
"terms_set": {
"programming_languages": {
"terms": [ "dotnet", "php"],
"minimum_should_match_field": "required_matches"
}
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.945204,
"_source" : {
"name" : "李四",
"programming_languages" : [
"java",
"php",
"dotnet"
],
"required_matches" : 2
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2897414,
"_source" : {
"name" : "张三",
"programming_languages" : [
"c++",
"java",
"dotnet"
],
"required_matches" : 2
}
}
]
}
}

范例二:使用terms_set动态设置匹配单词的个数2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#在索引库中programming_languages字段的内容中需要查询dotnet、java和c++这3个单词,最少匹配到的单词数量存储在当前文档对应的required_matches字段中
GET /myindex-term-level/_search
{
"query": {
"terms_set": {
"programming_languages": {
"terms": [ "dotnet", "java","c++"],
"minimum_should_match_field": "required_matches"
}
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.7801694,
"_source" : {
"name" : "张三",
"programming_languages" : [
"c++",
"java",
"dotnet"
],
"required_matches" : 2
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "3",
"_score" : 0.7801694,
"_source" : {
"name" : "王五",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "hello world"
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 0.7801694,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.2897414,
"_source" : {
"name" : "李四",
"programming_languages" : [
"java",
"php",
"dotnet"
],
"required_matches" : 2
}
}
]
}
}

在以上结果中,_id等于1和_id等于2的文档中required_matches字段中的值都等于2,而且programming_languages字段中的内容也匹配到我们要查询的两个单词
_id分别等于3和4的文档中required_matches字段的值都等于3,且programming_languages字段中的内容也匹配到我们要查询的3个单词。
所以查询结果符合查询要求。




通配符查询

在Elasticsearch中,如果需要通过通配符进行查询,可使用wildcard来进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#在索引中programming_languages字段的内容中查询匹配"p*p"和"*"(可以表示任何内容)的文档数据
GET /myindex-term-level/_search
{
"query": {
"wildcard": {
"programming_languages": {
"value": "p*p"
}
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"name" : "李四",
"programming_languages" : [
"java",
"php",
"dotnet"
],
"required_matches" : 2
}
}
]
}
}

由以上结果可知,文档匹配到了php,返回结果符合查询要求。需要注意的是,"*"可以表示多个字母。

这个查询条件表示p后面可以匹配任何字符,而且用户可以指定评分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
GET /myindex-term-level/_search
{
"query": {
"wildcard": {
"name": {
"value": "*三",
"boost": 3.0,
"rewrite": "constant_score"
}
}
}
}

{
"took" : 1,

"max_score" : 3.0,
"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "1",
"_score" : 3.0,
"_source" : {
"name" : "张三",
"programming_languages" : [
"c++",
"java",
"dotnet"
],
"required_matches" : 2
}
}
]
}
}




范围查询

在编写SQL查询语句时,会经常对数字或者日期进行范围查询。
在Elasticsearch中,range通常被用于数字或者日期范围的查询中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#根据范围查询required_matches字段的值大于等于3且小于等于4的文档数据
GET /myindex-term-level/_search
{
"query": {
"range": {
"required_matches": {
"gte": 3,
"lte": 4
}
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "王五",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "hello world"
}
},
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
}
]
}
}




模糊匹配查询

模糊匹配表示返回包含与搜索词相似的词的文档(将一个词转换为另一个词)。这些更改可以包括

  • 更改字符(例如box匹配到fox)。
  • 删除字符(例如black匹配到lack)。
  • 插入字符(例如sic匹配到sick)。
  • 倒置两个相邻字符(例如act匹配到cat)。

以下范例是对remarks字段中的内容模糊匹配powerf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
GET /myindex-term-level/_search
{
"query": {
"fuzzy": {
"remarks": {
"value": "powerf"
}
}
}
}


{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 0.68793553,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
}
]
}
}




正则表达式查询

正则表达式是一种使用占位符字符匹配数据的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#查询remarks字段内容匹配po开头的文档数据
GET /myindex-term-level/_search
{
"query": {
"regexp": {
"remarks": {
"value": "po.*",
"case_insensitive": true
}
}
}
}

{

"hits" : [
{
"_index" : "myindex-term-level",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"name" : "赵六",
"programming_languages" : [
"java",
"c++",
"dotnet"
],
"required_matches" : 3,
"remarks" : "powerful"
}
}
]
}
}






Elasticsearch的聚合桶全面解析

  • 聚合桶概述
  • 简单聚合和多个聚合的使用
  • 动态脚本聚合的使用
  • 过滤聚合的使用
  • 日期范围和数值范围聚合的使用

在Elasticsearch中,有很大一部分业务需要聚合统计。
本章将讲解Elasticsearch中的各种聚合查询,比如过滤聚合、动态脚本聚合以及日期范围和数值范围聚合。

聚合桶概述

聚合桶概述除了查询之外,Elasticsearch最常用的功能就是聚合统计。
Elasticsearch提供了聚合桶(Bucket Aggregation)功能。
Elasticsearch中的桶在概念上类似于SQL的分组(GROUP BY),而聚合桶中的指标则类似于COUNT()、SUM()、MAX()等统计方法。

数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#批量新增数据,数据是关于汽车交易的信息:制造商、售价、出售时间
POST /myindex-aggtest/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "soldtime" : "2021-11-21" }
{ "index": {}}
{ "price" : 15000, "color" : "red", "make" : "honda", "soldtime" : "2021-9-06" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "soldtime" : "2021-06-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "soldtime" : "2021-08-02" }
{ "index": {}}
{ "price" : 16000, "color" : "green", "make" : "toyota", "soldtime" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "soldtime" : "2021-11-08" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "soldtime" : "2021-01-03" }
{ "index": {}}

简单的聚合

如果想知道哪个颜色的汽车销量最好

1
2
3
4
5
6
7
8
9
10
11
GET /myindex-aggtest/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color.keyword"
}
}
}
}

以上语句中的关键字解析说明如下:

  • size:0:表示只要统计后的结果,原始数据不需要返回,如果是大于0的,则会返回对应数量的文档数据。
  • aggs:固定语法,聚合分析都要声明aggs或者aggregations。
  • song_qty_by_language:聚合的名称,可以随便命名,但建议规范命名。
  • terms:表示按哪个字段进行分组。
  • field:表示具体的字段名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{

"aggregations" : {
"popular_colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "red",
"doc_count" : 3
},
{
"key" : "green",
"doc_count" : 2
},
{
"key" : "blue",
"doc_count" : 1
}
]
}
}
}

多个聚合

如果想要知道哪个颜色和哪个厂商的汽车销量最好,可以对颜色和厂商两个字段进行聚合计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
GET /myindex-aggtest/_search
{
"size" : 0,
"aggs" : {
"colors" : {
"terms" : {
"field" : "color.keyword"
}
},
"make" : {
"terms" : {
"field" : "make.keyword"
}
}
}
}


{

"aggregations" : {
"make" : { // 厂商字段聚合统计结果
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "honda",
"doc_count" : 2
},
{
"key" : "toyota",
"doc_count" : 2
},
{
"key" : "bmw",
"doc_count" : 1
},
{
"key" : "ford",
"doc_count" : 1
}
]
},
"colors" : { // 颜色聚合结果
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "red",
"doc_count" : 3
},
{
"key" : "green",
"doc_count" : 2
},
{
"key" : "blue",
"doc_count" : 1
}
]
}
}
}

动态脚本聚合

Elasticsearch还支持一些基于脚本(生成运行时的字段)的复杂动态聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#根据索引库中厂商名称的长度进行分组,计算每种长度的文档数量
GET /myindex-aggtest/_search
{
"runtime_mappings": {
"make_content_length": {
"type": "long",
"script": "emit(doc['make.keyword'].value.length())" #运行时脚本计算make字段内容的长度
}
},
"size" : 0,
"aggs": {
"make_length": {
"histogram": {
"interval": 1,
"field": "make_content_length" #根据运行时make字段的内容长度进行聚合统计
}
}
}
}


{

"aggregations" : {
"make_length" : {
"buckets" : [
{
"key" : 3.0, // 表示厂商名称的长度
"doc_count" : 1 // 厂商名称长度等于3的文档数量
},
{
"key" : 4.0, // 表示厂商名称的长度
"doc_count" : 1
},
{
"key" : 5.0, // 表示厂商名称的长度
"doc_count" : 2
},
{
"key" : 6.0, // 表示厂商名称的长度
"doc_count" : 2
}
]
}
}
}

过滤聚合

在SQL中,我们经常会先进行筛选,再进行聚合统计。Elasticsearch中也支持先筛选再聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#聚合计算厂商toyota出售车的平均价格和出售的总数量
GET /myindex-aggtest/_search
{
"size": 0,
"aggs": {
"make_by": {
"filter": { "term": { "make": "toyota" } },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}


{

"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"make_by" : {
"doc_count" : 2,
"avg_price" : {
"value" : 15500.0
}
}
}
}

可以看到,厂商toyota出售车的平均价格是15500,出售的总数量是2。

filter分组聚合

如果想要知道出售车辆中红色(red)、绿色(green)以及其他颜色的占比,则可以使用如下语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
GET /myindex-aggtest/_search
{
"size": 0,
"aggs" : {
"messages" : {
"filters" : {
"other_bucket_key": "other_color",
"filters" : {
"reds" : { "match" : { "color" : "red" }},
"greens" : { "match" : { "color" : "green" }}
}
}
}
}
}


{

"aggregations" : {
"messages" : {
"buckets" : {
"greens" : {
"doc_count" : 2 // 绿色车辆的数量
},
"reds" : {
"doc_count" : 3 // 红色车辆的颜色
},
"other_color" : {
"doc_count" : 1 // 其他车辆的颜色
}
}
}
}
}

数值范围聚合

Elasticsearch提供了基于多桶值源的聚合方式。通过这种方式可以定义一组范围,每个范围代表一个桶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
GET /myindex-aggtest/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [#如下是一组范围条件
{ "to": 10000 },
{ "from": 10000, "to": 20000 },
{ "from": 20000, "to": 30000 },
{ "from": 40000 }
]
}
}
}
}

{

"aggregations" : {
"price_ranges" : {
"buckets" : [
{
"key" : "*-10000.0", // 0~10000的聚合结果
"to" : 10000.0,
"doc_count" : 0
},
{
"key" : "10000.0-20000.0", // 10000~20000的聚合结果

"from" : 10000.0,
"to" : 20000.0,
"doc_count" : 3
},
{
"key" : "20000.0-30000.0", // 20000~30000的聚合结果

"from" : 20000.0,
"to" : 30000.0,
"doc_count" : 1
},
{
"key" : "40000.0-*", // 40000以上的聚合结果
"from" : 40000.0,
"doc_count" : 1
}
]
}
}
}

指定范围间隔聚合

在前面的范例中,我们可以指定具体的范围来进行聚合,但是当范围很多时,可以直接指定范围间隔(Histrogram)来进行聚合统计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#聚合统计,根据出售价格范围进行分组,每一组之间价格相差40000
GET /myindex-aggtest/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price",
"interval": 40000
}
}
}
}

{

},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"price" : {
"buckets" : [
{
"key" : 0.0, // 0~40000出售价格的聚合
"doc_count" : 5
},
{
"key" : 40000.0, // 40000~80000出售价格的聚合
"doc_count" : 0
},
{
"key" : 80000.0, // 80000以上出售价格的聚合
"doc_count" : 1
}
]
}
}
}

如上结果符合预期效果。
如果还想知道每一种价格区间的最高出售价格、最低出售价格和平均出售价格,可以使用如下范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
GET /myindex-aggtest/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price",
"interval": 40000
} ,
"aggs": {
"stats": {
"extended_stats": {
"field": "price"
}
}
}
}
}
}


{

"aggregations" : {
"price" : {
"buckets" : [
{
"key" : 0.0,
"doc_count" : 5,
"stats" : {
"count" : 5,
"min" : 10000.0,
"max" : 30000.0,
"avg" : 18200.0,
"sum" : 91000.0,
"sum_of_squares" : 1.881E9,
"variance" : 4.496E7,
"variance_population" : 4.496E7,
"variance_sampling" : 5.62E7,

}
},

}
},
{
"key" : 80000.0,
"doc_count" : 1,
"stats" : {
"count" : 1,
"min" : 80000.0,
"max" : 80000.0,
"avg" : 80000.0,
"sum" : 80000.0,

}
}

以上统计结果不仅统计出了每个区间的出售数量,还统计出了每个范围区间的价格最大值、最小值和平均值。
如果不关心最大值、最小值、平均值等指标,则不建议这样统计,因为这样统计会损失一部分的程序运行性能。

日期范围聚合

Elasticsearch不仅支持数值范围聚合,还支持日期范围聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#根据出售车的各个时间段来聚合
GET /myindex-aggtest/_search
{
"size": 0,
"aggs": {
"range": {
"date_range": {
"field": "soldtime",
"format": "yyyy-MM-dd",
"ranges": [
{ "from": "2021-06-30" },
{ "to": "2021-07-01" }
]
}
}
}
}

{

},
"aggregations" : {
"range" : {
"buckets" : [
{
"key" : "*-2021-07-01",
"to" : 1.6250976E12,
"to_as_string" : "2021-07-01",
"doc_count" : 3
},
{
"key" : "2021-06-30-*",
"from" : 1.6250112E12,
"from_as_string" : "2021-06-30",
"doc_count" : 3
}
]
}
}
}

由以上统计结果可知,程序根据统计范围的分解条件聚合统计出了两个时间段之间的出售车辆的总数量,而且日期范围统计还支持根据”Date Math”表达式进行范围分解统计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#计算5个月之前和之后的销售车辆数量
GET /myindex-aggtest/_search
{
"size": 0,
"aggs": {
"range": {
"date_range": {
"field": "soldtime",
"format": "yyyy-MM-dd",
"ranges": [
{ "from": "now-5M/M" },
{ "to": "now-5M/M" }
]
}
}
}
}

{

"aggregations" : {
"range" : {
"buckets" : [
{
"key" : "*-2021-02-01", // 2021-02-01之前的销售数量
"to" : 1.6121376E12,
"to_as_string" : "2021-02-01",
"doc_count" : 2
},
{
"key" : "2021-02-01-*", // 2021-02-01之后的销售数量

"from" : 1.6121376E12,
"from_as_string" : "2021-02-01",
"doc_count" : 4
}
]
}
}
}

以上结果分别统计出了从当前日期算起,5个月前后出售的车辆数量。统计结果符合预期。






Elasticsearch高可用集群

  • 多节点集群搭建
  • 生产环境中的集群推荐方案
  • 节点发现详解
  • 节点故障详解
  • 集群状态更新和选举方式详解
  • 脑裂问题
  • 分片和段合并详解

生产环境集群推荐方案

在一个生产环境集群中,我们可以对节点的职责进行划分,建议集群中设置3个以上的节点作为主节点,当现有的主节点出现故障时,这些节点可以被选举成为主节点,从而维护整个集群的状态。

然后可以根据数据量设置一批数据节点,这些节点只负责存储数据,如果集群中索引数量较大,推荐在集群中再设置一批客户端节点(在配置文件中设置node.master: false和node.data: false),这些节点只负责处理用户请求,实现请求转发、负载均衡等功能。

节点发现

当Elasticsearch集群中的节点启动后,它们会利用多播或者单播(如果用户更改了配置)寻找集群中的其他节点,并与之建立连接。

当新节点加入后,此节点会通过多播寻找集群中其他的节点并和发现的节点建立连接。

需要注意的是,从用户的角度来看,实际上用户并不需要知道哪个节点是主节点,所有的操作请求可以分发到任意的节点上,而Elasticsearch内部会完成这些让用户感到“不明觉历”的工作。

在必要的情况下,任何节点都可以并发地把查询语句分发到其他的节点上,然后合并各个节点返回的查询结果。
最后返回给用户一个完整的数据集。
而所有的这些工作都不需要经过主节点进行转发。

节点故障

在正常工作时,主节点会监控所有的节点,查看各个节点是否正常工作。
如果在指定的时间某个节点无法被访问,该节点就会被视为出故障了

当主节点发现超过3次Ping不通某一个节点,就会认为此节点宕机。

节点故障配置

集群状态更新

,无论是集群中新添加节点,还是节点有异常,都需要维护集群状态。
而主节点是唯一一个能够更新集群状态的节点。
一个时间段内主节点只能更新一个集群状态,并且将更改的集群状态发送给其他节点,当其他节点接收到状态时,先确认收到消息,但是此时并不更新到最新状态。
如果主节点在规定时间(discovery.zen.commit_timeout默认30秒)内没有收到大多数节点(大多数节点数量=discovery.zen.minimum_master_nodes参数的值)的确认,那么集群状态更新请求不会被通过。

一旦足够的节点响应了更新的消息,新的集群状态才会被提交,并且会发送一条消息给所有的节点。
随后这些节点开始在内部更新至新的集群状态

当集群状态有了新的变化,都是主节点主动通知其他节点来同步集群状态的信息。

主节点选举

在集群运行过程中,主节点有可能会出现异常

当主节点出现宕机或者网络异常时,如果造成其他节点超过3次都没有Ping通主节点,则认为当前主节点出现了异常,集群会重新选举新的主节点。
如果一些节点的node.master参数事先被配置为true,则表示这种节点在主节点发生故障的时候会参加选举。
在多个节点选举的过程中,节点唯一标识值越小,则越有可能被选举为主节点

上述代码片段是主节点进行选举的一部分代码
通过上述代码我们会发现,对于有资格被选举为主节点的各个节点
将它们的节点唯一标识进行排序,然后优先选择标识值小的节点作为主节点。

“脑裂”问题不再成为问题

一个正常的Elasticsearch集群中只有一个主节点,主节点负责管理整个集群。集群的所有节点都会选择同一个节点作为主节点,所以无论访问哪个节点,都可以查看集群的状态信息。
而“脑裂”问题的出现就是因为从节点在选择主节点时出现了分歧,导致一个集群中出现多个主节点,使得集群处于异常状态。

正常的集群状态如图

当前集群中有6个节点,左边3个节点和右边3个节点不在同一个网络分区
如果两个网络分区之间出现异常,就会导致左边的3个节点重新选举一个新的主节点,如图

可以看到,集群中出现了两个主节点,这种情况就是“脑裂”。
造成“脑裂”的原因有如下几种:

  • 网络原因。内网一般不会出现此问题,因为可以监控内网流量状态。外网出现网络问题的可能性更大一些,会导致其他节点认为主节点宕机了,从而重新选择主节点。
  • 节点负载。主节点既要负责管理集群又要存储数据,当访问量大的时候,可能会导致Elasticsearch实例反应不过来而停止响应,此时其他节点在向主节点发送消息时得不到主节点的响应,就会认为主节点宕机了,从而重新选择主节点。
  • 内存回收。大规模回收内存也会导致Elasticsearch集群失去响应,进而导致其他节点认为主节点宕机了,从而重新选择主节点。

解决脑裂问题的方案如下

只需要在配置文件中新增下面的配置即可:discovery.zen.minimum_master_nodes:(n/2)+1
该参数的意思是,当具备成为主节点的从节点的个数满足这个数值且都认为主节点“挂”了时,则会选举产生新的主节点。
例如Elasticsearch集群有3个从节点有资格成为主节点,如果这3个节点都认为主节点宕机了,则会进行选举
此时如果配置了这个参数并且参数的值是4,则不会进行选举。
用户可以适当地把这个值改大,减少出现“脑裂”的概率,官方给出的建议是(n/2)+1,n表示为有资格成为主节点的节点数(即配置了node.master=true的节点数量)。

值得庆幸的是,在Elasticsearch 7.13版本中,Elasticsearch内部创建了一个可插拔的集群协调系统,其默认实现称为Zen Discovery。
它对集群协调层进行了彻底的重建,并表示不会出现“脑裂”问题。
对于Zen Discovery官网没有深入地介绍,后续有机会我们可能会深入探讨此模型。

分片

Elasticsearch中所有的数据都均衡地存储在集群中各个节点的分片中。
分片就是Elasticsearch中所有数据的文件块,也是数据的最小单元块,整个Elasticsearch集群的核心就是对所有分片的分布、索引、负载、路由等达到惊人的速度。
假设IndexA索引库有两个分片,我们向IndexA索引库中插入10条数据,那么这10条数据会尽可能平均地分为5条存储在一个分片中,剩下的5条会存储在另一个分片中。
当我们查询的时候,会并发地让两个不同的节点同时查询两个分片数据,这样就提高了查询效率。

分片设置

如果用户需要设置分片数量,则可以使用如下语法创建分片:

1
2
3
4
5
6
#分片设置如下
PUT /索引库名称/_settings
{
"number_of_shards": 1,
"number_of_replicas": 4
}

范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT /clayindex
{
"settings": {
"number_of_shards": 2, #主分片数量是2
"number_of_replicas": 1 #副本分片数量是1
},
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"address":{
"type": "text"
},
"age":{
"type": "integer"
}
}
}
}

由以上语句可知:在创建索引映射的时候,可以在settings参数中指定分片数量。
需要注意的是如果有索引已经建立,那么修改的时候只能修改副本分片的数量,主分片数量不可以修改。

1
2
3
4
PUT /索引库名称/_settings
{
"number_of_replicas": 4
}

多少个分片数量才合理

对于“多少个分片数量才合理?”这个问题,我们通过下面几个方面来进行解答。

  • 避免分片过大

因为这样会对集群“从故障中恢复”造成不利的影响。尽管并没有关于分片大小的固定限值,但是开发和运维人员通常将50GB作为分片上限,而且这一限值在各种用例中都已得到了验证。

  • 分片不可过小

分片过小会导致“段”(Segment,Elasticsearch中存储数据的空间块)过小,进而导致开销增加。用户要尽量将分片的平均大小控制在几GB到几十GB。对于时序型数据而言,分片大小通常介于20GB~40GB。

  • 文档归并

由于单个分片的开销取决于“段”的数量和“段”的大小,因此通过文档归并(forcemerge)操作强制将较小的“段”合并为较大的“段”。这样做能够减少开销并改善查询的性能。理想状况下,应当在索引内无数据写入的时候完成此操作。但是需要注意的是,文档归并操作是一个极其耗费资源的操作,所以应该在非高峰时段进行。

  • 分片数量限制

每个节点上可以存储的分片数量与可用的堆内存大小成正比,但是Elasticsearch并未强制规定固定限值。推荐将单个分片存储索引数据的大小控制在20GB左右,绝对不要超过50GB,分片的数量可以根据如下公式计算:分片数量=数据总量 / 20GB

数据存储分片机制应对扩容和分片故障的方式

集群中有3个主分片,3个副本分片,每个主分片对应一个副本分片,而且主分片和副本分片分布在不同的节点。
所以当集群中内任何一个节点出现问题的时候,我们的数据都完好无损。
当我们写入文档时,这些文档都会保存在主分片上,然后被并行地复制到对应的副本分片上。
这样就保证了既可以从主分片上获得文档数据,又可以从副本分片上获得文档数据,也提高了查询效率。

当新加入Node3节点对集群进行扩容时,Node1和Node2上各有一个分片被迁移到了新的Node 3节点上。此时每个节点上都拥有两个分片,而不是之前的3个分片。这样每个节点的硬件资源将被更少的分片所使用,每个分片的性能将会得到提升

我们发现,Node1和Node2组合后的分片是(R0,R1,P1,P2),Node1和Node3组合后的分片是(R2,P0,P1,P2),Node2和Node3组合后的分片是(R0,R1,P0,R2)。

所以当这3个节点中有任何一个节点出现异常时,其余两个节点中的分片都完整地保留了分片0、分片1、分片2的数据。
也就是说,当出现任何一个节点宕机时,也不会导致服务异常,只不过新的主节点会把副本分片提升为主分片。
但是当出现两个节点同时出现异常时,则此集群状态就不能正确提供服务,因为集群数据不完整。

数据存储段合并详情

当客户端写入数据时,同时把数据写入translog日志(防止服务重启导致内存数据库丢失)和索引缓存(index buffer)中。
在用户读取数据的时候,读取的是“段”中的数据。
在Elasticsearch中,写入和打开一个新段(Segment)的过程叫作刷新(Refresh)。

在默认情况下,每个分片每秒会自动刷新一次。这就是我们说Elasticsearch是近实时搜索引擎的原因(文档的变化并不是立即对搜索可见,但会在一秒之内变为可见)。每个数据分片是由很多段组成的,每个段都需要占用内存等资源。

为了提高性能,Elasticsearch会定期将小的段进行合并,生成较大的段。由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内段的数量暴增。而段的数量太多会带来比较大的麻烦,因为每一个段都会消耗IO、内存和CPU等资源。更重要的是,每个搜索请求都必须轮流检查每个段,所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段里面,然后这些大的段再被合并到更大的段里面。
当段合并的时候,会将那些旧的已删除的文档从文件系统中清除。
旧的文档(或被更新文档的旧版本)不会被复制到新的大段中去。

以下是段相关的一些操作:

  • 1)当写入索引数据的时候,刷新操作会创建新的段并且将段的状态设置为打开以供搜索使用。
  • 2)段合并的时候会选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。
  • 3)段合并结束时,旧的段中的数据会被删除。

刷新操作并不需要每秒进行一次。
如果有业务正在使用Elasticsearch索引中大量的日志文件,此时用户想要优化数据写入性能,可以通过设置refresh_interval参数进行优化,以降低索引的刷新频率,提高数据写入的性能。

语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#设置时间时间为30秒,表示每30秒进行一次刷新
PUT /索引库名称
{
"settings": {
"refresh_interval": "30s"
}
}


#关闭自动刷新操作
PUT /索引库名称/_settings
{ "refresh_interval": -1 }


#开启刷新且刷新时间为1秒
PUT /索引库名称/_settings
{ "refresh_interval": "1s" }






高级操作和性能调优

  • 索引别名使用详情
  • 索引模板别名使用详情
  • 实现滚动查询
  • 实现跨集群查询
  • 实现SQL操作
  • 性能优化详解

本章主要学习Elasticsearch中的高级操作和性能优化,高级操作包含索引别名使用、索引模板别名使用、滚动查询、跨集群查询以及使用SQL语句操作Elasticsearch。
对于Elasticsearch的性能优化,主要针对数据结构、查询优化、索引优化和硬件配置这4方面提出详细的优化策略。

索引别名

在现实中,我们的业务需求是不断变化迭代的,也许之前编写的某个业务逻辑在下个版本就发生变化了,为了满足新的业务需求,可能需要修改原来的设计,例如修改现有数据库的名称。

这样的业务改动对于其他数据库而言实现起来比较麻烦(需要重新创建数据库,修改程序代码)。针对这种需求,利用Elasticsearch中的索引别名就可以轻松解决。

假设有一个学生的原始索引student_index_v1,我们给它起一个别名student_index,程序中也是用别名student_index进行查询
当业务需求发生改变,需要修改索引库名称的时候,重新创建一个索引库student_index_v2,将别名student_index指向新的索引库student_index_v2,同时将student_index_v1索引库的数据迁移到新的student_index_v2索引库中
这样我们就可以做到在零停机下从旧索引库切换到新索引库。

索引别名就像一个快捷方式或者软链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#给索引库test1创建别名alias1
POST /_aliases
{
"actions" : [
{ "add" : { "index" : "test1", "alias" : "alias1" } }
]
}
#删除索引库对应的别名
POST /_aliases
{
"actions" : [
{ "remove" : { "index" : "test1", "alias" : "alias1" } }
]
}
#索引别名指向新的索引库
POST /_aliases
{
"actions" : [
{ "remove" : { "index" : "test1", "alias" : "alias1" } },
{ "add" : { "index" : "test2", "alias" : "alias1" } }
]
}

以上语句先进行索引别名移除,然后创建索引别名。
这个操作是具有原子性的,因此不需要担心短时间内别名不指向一个索引库。

1
2
3
4
5
6
7
#一个别名关联多个索引库
POST /_aliases
{
"actions" : [
{ "add" : { "indices" : ["test1", "test2"], "alias" : "alias1" } }
]
}

索引模板别名

在Elasticsearch中,如果想要根据索引模板为索引库指定别名,需要使用aliases关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#创建索引模板并指定别名
PUT _template/templateds
{
"template": "log*",
"order": 0,
"settings": {
"number_of_shards":2
},
"aliases": {
"log-query":{} #别名名称
},
"mappings": {

"properties": {
"name": {
"type": "keyword"
},
"id": {
"type": "long"
}
}

}
}

以上语句创建了一个名为templateds的索引模板名,索引模板的匹配值是"log*"
根据这个模板创建的索引,它们的别名都是log-query。
也就是说当我们执行下面两个语句时,会自动为log1索引库和log2索引库创建相同的索引映射信息,而且log1和log2索引库的别名都是log-query:

1
2
3
4
5
6
7
8
9
10
POST /log1/_doc/1
{
"person_name": "张三",
"id": 1
}
POST /log2/_doc/1
{
"person_name": "李四",
"id": 2
}

在以上语句中,当我们给log1和log2索引库写入数据时,log1和log2匹配到了log*
所以系统会根据模板自动生成log1和log2索引映射。
我们需要同时查询两个索引库的数据时,可以使用别名通过一条语句来查询

1
2
3
4
POST /log-query/_search
{
"size": 10
}

以上语句会同时查询log1和log2索引库的数据。

滚动查询

当索引库中数据量特别大的时候,深度分页的操作会给Elasticsearch集群的性能带来极大损耗。
为了解决这个问题,Elasticsearch提供了滚动查询来避免这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#创建索引库并指定映射关系
PUT scrolldb
{
"mappings" : {
"properties" : {
"name" : {
"type" : "keyword"
},
"age" : {
"type" : "integer"
}
}
}
}
#批量新增数据
POST _bulk
{ "index" : { "_index" : "scrolldb", "_id" : "1" } }
{ "name" : "张三", "age": 12}
{ "index" : { "_index" : "scrolldb", "_id" : "2" } }
{ "name" : "李四", "age": 10 }
{ "index" : { "_index" : "scrolldb", "_id" : "3" } }
{ "name" : "王五", "age": 11 }
#滚动查询,每次查询返回2条文档,并且数据量不能超过1MB,也就是说当一条文档信息超过1MB的时候,只会返回1条文档信息
POST /scrolldb/_search?scroll=1m
{
"size": 2,
"query": {
"match_all": {

}
}
}

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
#下次进行滚动查询需要带的标识
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFjZnWUJ1RTJYUmZhTzhCMVNUemNIX2cAAAAAAAFmQxZ5UWdMX25LNlJYR1JMeHltWlhkcVVR",
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "scrolldb",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "张三",
"age" : 12
}
},
{
"_index" : "scrolldb",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"name" : "李四",
"age" : 10
}
}
]
}
}

#使用滚动查询返回信息的scroll_id继续查询,直到查不出为止,一般情况下当查询结果数量小于第一次查询执行的size时,结束查询
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : " FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFjZnWUJ1RTJYUmZhTzhCMVNUemNIX2cAAAAAAAFmQxZ5UWdMX25LNlJYR1JMeHltWlhkcVVR "
}

跨集群查询

Elasticsearch集群可以处理容量在PB以上的数据,集群的规模可以是成百上千个节点。
而跨集群查询允许用户查询多个集群中的数据,这个特性可以使企业更好地设计架构。

在分布式架构或微服务架构中,我们会根据业务把系统分为多个模块,模块A由一个团队负责,模块B由一个团队负责,模块C由一个团队负责,通常各个模块只查询各自模块的日志数据,但有些问题涉及多个模块时,需要查询多个模块的日志数据,这时跨集群查询刚好可以解决这类问题,对架构设计来说,我们将模块A、模块B、模块C的日志数据都存储在单独的集群里。

这样可以避免集群之间相互干扰,出现问题时也可以做到分而治之。但是,当我们需要在多个不同的集群中查询数据的时候,就需要跨集群查询高级特征。为了快速掌握这个特性,我们在本地环境搭建3个不相干的单节点集群。

远程集群配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUT _cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster_one": {
"seeds": [
"127.0.0.1:9300"
]
},
"cluster_two": {
"seeds": [
"127.0.0.1:9301"
]
},
"cluster_three": {
"seeds": [
"127.0.0.1:9302"
]
}
}
}
}
}

跨集群查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#筛选查询cluster_one集群中索引名称是clay1的数据
GET /cluster_one:clay1/_search
{
"query": {
"match": {
"name": "张三"
}
}
}


{

"_clusters" : {
"total" : 1,
"successful" : 1,
"skipped" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "cluster_one:clay1",
"_type" : "_doc",
"_id" : "FHGfXHoBMi5a2MutyHGA",
"_score" : 0.5753642,
"_source" : {
"name" : "张三11",
"address" : "中国1111",
"age" : 11
}
}
]
}
}

以上返回结果中包含当前集群的信息和文档数据。接下来同时从3个集群中查询数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
GET /cluster_one:clay1,cluster_two:clay1,cluster_three:clay1/_search
{
"query": {
"match": {
"name": "张三"
}
}
}


{

"_clusters" : {
"total" : 3,
"successful" : 3,
"skipped" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "cluster_three:clay1",//表示集群cluster_three中的clay1索引
"_type" : "_doc",
"_id" : "XZqgXHoBQa4W-gneJTjx",
"_score" : 0.5753642,
"_source" : {
"name" : "张三33",
"address" : "中国33",
"age" : 33
}
},
{
"_index" : "cluster_two:clay1", //表示集群cluster_two中的clay1索引
"_type" : "_doc",
"_id" : "KgegXHoBCz61Dl5JAMSj",
"_score" : 0.5753642,
"_source" : {
"name" : "张三22",
"address" : "中国22",
"age" : 22
}
},
{
"_index" : "cluster_one:clay1",//表示集群cluster_one中的clay1索引
"_type" : "_doc",
"_id" : "FHGfXHoBMi5a2MutyHGA",
"_score" : 0.5753642,
"_source" : {
"name" : "张三11",
"address" : "中国1111",
"age" : 11
}
}
]
}
}

由以上结果可知,同时查询并返回了多个集群中的数据,而且每一条文档信息中都包含当前文档数据的集群信息。

使用SQL语句操作Elasticsearch

Elasticsearch具有快速查询、高可扩展等特点,能够满足用户的各种业务需求,但是对于不是很熟悉Elasticsearch的开发人员而言,编写复杂的查询语句是相当困难的。
为了解决这个问题,Elasticsearch在特定的业务中可以使用SQL语句进行查询操作。
利用传统数据库的语法来解锁非传统数据库的性能,能在PB量级的数据中进行全文搜索,并实时获得结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#数据准备
POST /sqldbindex/_doc
{
"name":"clay",
"age":18,
"address":"江苏省",
"birth":"1998-01-0"
}
POST /sqldbindex/_doc
{
"name":"jick",
"age":17,
"address":"江苏省",
"birth":"1999-01-0"
}

#使用SQL操作Elasticsearch中的数据
POST /_xpack/sql?format=txt
{
"query":"select * from sqldbindex"

}

需要注意的是,SQL语句中的表名称就是Elasticsearch中索引库的名称。
只要懂SQL语句,就能很方便地操作Elasticsearch。

Elasticsearch性能优化详解

Elasticsearch作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能保证它的性能和稳定性。
如何根据实际情况提高Elasticsearch服务的性能,其实有很多技巧。

数据结构优化

  • 尽量减少不需要的字段
    如果Elasticsearch用于业务搜索服务,那么一些不需要用于搜索的字段最好不要存储到索引库中
    这样既节省空间同时在相同的数据量下也能提高搜索性能。
    同样控制字段的数量和字段的类型也是Elasticsearch性能优化的重中之重。

  • 尽量使用静态映射
    尽量避免使用动态映射,这样有可能会导致集群崩溃。
    此外动态映射有可能会带来不可控制的数据类型,进而导致在查询端出现相关异常影响业务。

查询优化

Elasticsearch用于业务搜索的近实时查询时,查询效率的优化显得尤为重要。

下面介绍查询优化需要注意的地方。

  • Cache的设置及使用

Elasticsearch在查询的时候,使用filter查询会用到querycache
如果业务场景中的过滤查询比较多,建议将querycache设置得大一些,以提高查询速度。
设置如下:

1
indices.queries.cache.size: 10%

以上设置是默认的,可以设置成百分比,也可以设置成具体的值,如256MB。

  • 合理使用深度翻页

在使用Elasticsearch的过程中,应尽量避免深度翻页的出现。
正常翻页查询都是从from开始size条数据,需要在每个分片中查询文档评分在前面的from+size条数据,然后协同节点收集每个分配的前from+size条数据,协同节点一共会收到N*(from+size)条数据然后进行排序,再将其中from到from+size条数据返回。

如果from或者size很大的话,会导致参加排序的数量过大,最终会导致CPU资源消耗增大。
对于这种深度翻页的业务,推荐使用Elasticsearch中的滚动查询来解决。

索引优化设置

  • 合理批量提交

当有大量数据需要提交时,建议采用批量提交(Bulk操作)的方式。
此外使用Bulk请求时,每个请求的数据量不宜过大,因为太大会导致内存使用过多,尽量控制在几十MB之内。

  • 增加刷新时间间隔

为了提高搜索性能,Elasticsearch在写入数据的时候采用延迟写入的策略,即数据先写到内存中,当超过默认的1秒(index.refresh_interval)后会进行一次写入操作,就是将内存中的段数据刷新到磁盘中,此时才能将数据搜索出来
所以这就是Elasticsearch提供的是近实时搜索功能,而不是实时搜索功能的原因。
如果系统对数据延迟要求不高的话,可以通过延长刷新时间间隔来有效地减少段合并的压力,提高索引的性能。
比如在做全链路日志跟踪的过程中,我们就将index.refresh_interval设置为30秒,减少刷新次数。
再如在进行数据导入时可以将“刷新”临时关闭,即把index.refresh_interval设置为设1,数据导入成功后再打开为正常模式。

  • 注意_id字段的使用

在创建索引或者写入文档数据时,应该尽可能避免使用自定义的_id,建议使用Elasticsearch的默认ID生成策略,这样对于Elasticsearch内部进行数据负载有很大作用。

  • 注意_all和_source字段的使用

_all_source字段的使用应该注意场景和需要,_all字段包含所有的索引字段,方便做全文搜索
如果无此需求可以禁用;_source存储了原始的文档内容,如果没有获取原始文档数据的需求,可通过设置includes、excludes属性来定义放入_source的字段。

硬件配置优化

  • 磁盘的选择
    硬盘对所有的集群都很重要,对大量写入数据的集群更是加倍重要。
    硬盘是服务器上最慢的子系统,这意味着那些写入量很大的集群很容易让硬盘饱和,使得它成为集群的瓶颈。在经济压力能承受的范围内,尽量使用固态硬盘(Solid State Drive,SSD)。
    固态硬盘相比任何旋转介质,无论是随机写还是顺序写,都会对IO有较大的提升。

  • 禁止swap
    在使用Elasticsearch的时候,推荐做法是禁用swap,一旦允许内存与磁盘交换,就会引起致命的性能问题。
    可以通过在config/elasticsearch.yml配置文件中修改bootstrap.memory_lock: true来保持JVM锁定内存,以保证Elasticsearch的性能。

  • 内存配置
    在Elasticsearch中,排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的。即使堆空间比较小,也能为操作系统文件缓存提供额外的内存。
    因为Lucene使用的许多数据结构都是基于磁盘的格式,Elasticsearch利用操作系统缓存对性能的提升能产生很大的效果。
    如果机器内存等于64GB,可以给Elasticsearch分配32GB,留下32GB给Lucene。
    当服务内存是32GB和16GB时,最少也需要给Elasticsearch分配8GB。






Es原理层面

Es基本概念

基本概念和原理

Elasticsearch是实时的分布式搜索分析引擎,内部使用Lucene做索引与搜索。

何谓实时?
新增到 ES 中的数据在1秒后就可以被检索到,这种新增数据对搜索的可见性称为“准实时搜索”。

分布式意味着可以动态调整集群规模,弹性扩容,而这一切操作起来都非常简便,用户甚至不必了解集群原理就可以实现。
按官方的描述,集群规模支持“上百”个节点,相比HDFS等上千台的集群,这个规模“小了点”。
因此目前我们认为ES适合中等数据量的业务,不适合存储海量数据。

Lucene是Java语言编写的全文搜索框架,用于处理纯文本的数据,但它只是一个库,提供建立索引、执行搜索等接口,但不包含分布式服务,这些正是 ES 做的。

什么是全文?
对全部的文本内容进行分析,建立索引,使之可以被搜索,因此称为全文。

基于ES,你可以很容易地搭建自己的搜索引擎,用于分析日志,或者配合开源爬虫建立某个垂直领域的搜索引擎。
ES易用的产品设计使得它很容易上手。
除了搜索,ES 还提供了大量的聚合功能,所以它不单单是一个搜索引擎,还可以进行数据分析、统计,生成指标数据。
而这些功能都在快速迭代,目前每2周左右就会发布新版本。

索引结构

ES是面向文档的。
各种文本内容以文档的形式存储到ES中,文档可以是一封邮件、一条日志,或者一个网页的内容。
一般使用 JSON 作为文档的序列化格式,
文档可以有很多字段,在创建索引的时候,我们需要描述文档中每个字段的数据类型,并且可能需要指定不同的分析器,就像在关系型数据中“CREATE TABLE”一样。

在存储结构上,由_index_type_id唯一标识一个文档。

  • _index:指向一个或多个物理分片的逻辑命名空间
  • _type:类型用于区分同一个集合中的不同细分
    在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同类型的数据。
    多个_type可以在相同的索引中存在,只要它们的字段不冲突即可(对于整个索引,映射在本质上被“扁平化”成一个单一的、全局的模式)。
  • _id文档标记符由系统自动生成或使用者提供。

在ES 6.x版本中,一个索引只允许存在一个_type,未来的7.x版本将完全删除_type的概念。

分片(shard)

在分布式系统中,单机无法存储规模巨大的数据,要依靠大规模集群处理和存储这些数据,一般通过增加机器数量来提高系统水平扩展能力。

因此需要将数据分成若干小块分配到各个机器上。
然后通过某种路由策略找到某个数据块所在的位置。
除了将数据分片以提高水平扩展能力,分布式存储中还会把数据复制成多个副本,放置到不同的机器中,这样一来可以增加系统可用性,同时数据副本还可以使读操作并发执行,分担集群压力。
但是多数据副本也带来了一致性的问题:部分副本写成功,部分副本写失败。我们随后讨论。
为了应对并发更新问题,ES将数据副本分为主从两部分,即主分片(primary shard)和副分片(replica shard)。

主数据作为权威数据,写过程中先写主分片,成功后再写副分片,恢复阶段以主分片为准。
数据分片和数据副本的关系如下图

分片(shard)是底层的基本读写单元,分片的目的是分割巨大索引,让读写可以并行操作,由多台机器共同完成。
读写请求最终落到某个分片上,分片可以独立执行读写工作。

ES利用分片将数据分发到集群内各处。
分片是数据的容器,文档保存在分片内,不会跨分片存储。
分片又被分配到集群内的各个节点里。
当集群规模扩大或缩小时,ES 会自动在各节点中迁移分片,使数据仍然均匀分布在集群里。
索引与分片的关系如下图

一个ES索引包含很多分片,一个分片是一个Lucene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。
Lucene索引又由很多分段组成,每个分段都是一个倒排索引。
ES每次“refresh”都会生成一个新的分段,其中包含若干文档的数据。
在每个分段内部,文档的不同字段被单独建立索引。每个字段的值由若干词(Term)组成,Term是原文本内容经过分词器处理和语言处理后的最终结果(例如去除标点符号和转换为词根)

可以查看Es和Lucene的关系细节

什么是倒排序?

原数据如下:

ID Name Age Sex
1 Kate 24 Female
2 John 24 Male
3 Bill 29 Male

ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:

Name:

Term Posting List
Kate 1
John 2
Bill 3

Age:

Term Posting List
24 [1,2]
29 3

Sex:

Term Posting List
Female 1
Male [2,3]

Elasticsearch分别为每个Field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。
倒排序就是:可以根据文档的信息,反向检索出所有符合条件的文档集合。比如:查找Male的信息,就会检索出2,3 两个人的信息。

索引建立的时候就需要确定好主分片数,在较老的版本中(5.x 版本之前),主分片数量不可以修改,副分片数可以随时修改。
现在(5.x~6.x 版本之后),ES 已经支持在一定条件的限制下,对某个索引的主分片进行拆分(Split)或缩小(Shrink)。
但是我们仍然需要在一开始就尽量规划好主分片数量:先依据硬件情况定好单个分片容量,然后依据业务场景预估数据量和增长量,再除以单个分片容量。
分片数不够时可以考虑新建索引,搜索1个有着50个分片的索引与搜索50个每个都有1个分片的索引完全等价,或者使用_split API来拆分索引(6.1版本开始支持)。

在实际应用中,我们不应该向单个索引持续写数据,直到它的分片巨大无比。
巨大的索引会在数据老化后难以删除,以_id 为单位删除文档不会立刻释放空间,删除的 doc 只在 Lucene分段合并时才会真正从磁盘中删除。
即使手工触发分段合并,仍然会引起较高的 I/O 压力,并且可能因为分段巨大导致在合并过程中磁盘空间不足(分段大小大于磁盘可用空间的一半)。
因此我们建议周期性地创建新索引。
例如每天创建一个。假如有一个索引website,可以将它命名为website_20180319。
然后创建一个名为website的索引别名来关联这些索引。
这样对于业务方来说,读取时使用的名称不变,当需要删除数据的时候,可以直接删除整个索引。

索引别名就像一个快捷方式或软链接,不同的是它可以指向一个或多个索引。
可以用于实现索引分组,或者索引间的无缝切换。

动态更新索引

为文档建立索引,使其每个字段都可以被搜索,通过关键词检索文档内容,会使用倒排索引的数据结构。

倒排索引一旦被写入文件后就具有不变性,不变性具有许多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。

那么索引如何更新,让新添加的文档可以被搜索到?

答案是使用更多的索引,新增内容并写到一个新的倒排索引中,查询时每个倒排索引都被轮流查询,查询完再对结果进行合并。
每次内存缓冲的数据被写入文件时,会产生一个新的Lucene段,每个段都是一个倒排索引。
在一个记录元信息的文件中描述了当前Lucene索引都含有哪些分段。
由于分段的不变性,更新、删除等操作实际上是将数据标记为删除,记录到单独的位置,这种方式称为标记删除。
因此删除部分数据不会释放磁盘空间。

近实时搜索

在写操作中,一般会先在内存中缓冲一段数据,再将这些数据写入硬盘,每次写入硬盘的这批数据称为一个分段,如同任何写操作一样。
一般情况下(direct方式除外),通过操作系统write接口写到磁盘的数据先到达系统缓存(内存),write函数返回成功时,数据未必被刷到磁盘。

通过手工调用flush,或者操作系统通过一定策略将系统缓存刷到磁盘。这种策略大幅提升了写入效率。
从write函数返回成功开始,无论数据有没有被刷到磁盘,该数据已经对读取可见。
ES正是利用这种特性实现了近实时搜索。
每秒产生一个新分段,新段先写入文件系统缓存,但稍后再执行flush刷盘操作,写操作很快会执行完,一旦写成功,就可以像其他文件一样被打开和读取了。
由于系统先缓冲一段数据才写,且新段不会立即刷入磁盘,这两个过程中如果出现某些意外情况(如主机断电),则会存在丢失数据的风险。

通用的做法是记录事务日志,每次对ES进行操作时均记录事务日志,当ES启动的时候,重放translog中所有在最后一次提交后发生的变更操作。
比如HBase等都有自己的事务日志。

段合并

在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为refresh,每次refresh会创建一个新的Lucene 段。

但是分段数量太多会带来较大的麻烦,每个段都会消耗文件句柄、内存。
每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并;所以段越多,搜索也就越慢。

因此需要通过一定的策略将这些较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。
在合并过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。

HBase、Cassandra等系统都有类似的分段机制,写过程中先在内存缓冲一批数据,不时地将这些数据写入文件作为一个分段,分段具有不变性,再通过一些策略合并分段。
分段合并过程中,新段的产生需要一定的磁盘空间,我们要保证系统有足够的剩余可用空间。

Cassandra系统在段合并过程中的一个问题就是,当持续地向一个表中写入数据,如果段文件大小没有上限,当巨大的段达到磁盘空间的一半时,剩余空间不足以进行新的段合并过程。
如果段文件设置一定上限不再合并,则对表中部分数据无法实现真正的物理删除。 ES存在同样的问题




集群内部原理

分布式系统的集群方式大致可以分为主从(Master-Slave)模式和无主模式。

ES、HDFS、HBase使用主从模式,Cassandra使用无主模式。
主从模式可以简化系统设计,Master作为权威节点,部分操作仅由Master执行,并负责维护集群元信息。
缺点是Master节点存在单点故障,需要解决灾备问题,并且集群规模会受限于Master节点的管理能力。
因此从集群节点角色的角度划分,至少存在主节点和数据节点,另外还有协调节点、预处理节点和部落节点

下面分别介绍各种类型节点的职能。

集群节点角色

主节点(Master node)

主节点负责集群层面的相关操作,管理集群变更。
通过配置node.master: true(默认)使节点具有被选举为Master的资格。
主节点是全局唯一的,将从有资格成为Master的节点中进行选举。

主节点也可以作为数据节点,但尽可能做少量的工作,因此生产环境应尽量分离主节点和数据节点

创建独立主节点的配置

1
2
node.master: true
node.data: false

为了防止数据丢失,每个主节点应该知道有资格成为主节点的数量,默认为1
为避免网络分区时出现多主的情况,配置discovery.zen.minimum_master_nodes
原则上最小值应该是:(master_eligible_nodes / 2)+ 1

数据节点(Data node)

负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。数据节点对CPU、内存、I/O要求较高。
一般情况下,数据读写流程只和数据节点交互,不会和主节点打交道(异常情况除外)。
通过配置node.data: true(默认)来使一个节点成为数据节点

也可以通过下面的配置创建一个数据节点

1
2
3
node.master: false
node.data: true
node.ingest: false

预处理节点(Ingest node)

这是从5.0版本开始引入的概念。
预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的processors(处理器)和pipeline(管道),对数据进行某种转换、富化。
processors和pipeline拦截bulk和index请求,在应用相关操作后将文档传回给index或bulk API。

默认情况下,在所有的节点上启用ingest,如果想在某个节点上禁用ingest,则可以添加配置node.ingest: false
也可以通过下面的配置创建一个仅用于预处理的节点

1
2
node.master: false
node.data: false

协调节点(Coordinating node)

客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。
协调节点将请求转发给保存数据的数据节点。
每个数据节点在本地执行请求,并将结果返回协调节点。协调节点收集完数据后,将每个数据节点的结果合并为单个全局结果。
对结果收集和排序的过程可能需要很多CPU和内存资源。

通过下面的配置创建一个仅用于协调的节点:

1
2
3
node.master: false
node.data: false
node.ingest: false

部落节点(Tribe node)

tribes(部落)功能允许部落节点在多个集群之间充当联合客户端。
在ES 5.0之前还有一个客户端节点(Node Client)的角色,客户端节点有以下属性

1
2
node.master: false
node.data: false

它不做主节点,也不做数据节点,仅用于路由请求,本质上是一个智能负载均衡器(从负载均衡器的定义来说,智能和非智能的区别在于是否知道访问的内容存在于哪个节点)
从5.0版本开始,这个角色被协调节点(Coordinating only node)取代。




集群健康状态

从数据完整性的角度划分,集群健康状态分为三种

  • Green,所有的主分片和副分片都正常运行。
  • Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。这意味着存在单点故障风险。
  • Red,有主分片没能正常运行。

每个索引也有上述三种状态,假设丢失了一个副分片,该分片所属的索引和整个集群变为Yellow状态,其他索引仍为Green。

集群扩容

当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。
分片副本实现了数据冗余,从而防止硬件故障导致的数据丢失。
下面演示了当集群只有一个节点,到变成两个节点、三个节点时的shard迁移过程示例。

起初在NODE1上有三个主分片,没有副分片

其中,P代表Primary shard;R代表Replica shard。以后出现的内容使用相同的简称。
添加第二个节点后,副分片被分配到NODE2,如下图

添加第三个节点后,索引的六个分片被平均分配到集群的三个节点,如下图

分片分配过程中除了让节点间均匀存储,还要保证不把主分片和副分片分配到同一节点,避免单个节点故障引起数据丢失。
分布式系统中难免出现故障,当节点异常时,ES会自动处理节点异常。
当主节点异常时,集群会重新选举主节点。当某个主分片异常时,会将副分片提升为主分片。




内部模块简介

Cluster

Cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息。
主要功能如下:

  • 管理集群状态,将新生成的集群状态发布到集群所有节点。
  • 调用allocation模块执行分片分配,决策哪些分片应该分配到哪个节点
  • 在集群各节点中直接迁移分片,保持数据平衡。

allocation

封装了分片分配相关的功能和策略,包括主分片的分配和副分片的分配,本模块由主节点调用。
创建新索引、集群完全重启都需要分片分配的过程。

Discovery

发现模块负责发现集群中的节点,以及选举主节点。当节点加入或退出集群时,主节点会采取相应的行动。从某种角度来说,发现模块起到类似ZooKeeper的作用,选主并管理集群拓扑。

gateway

负责对收到Master广播下来的集群状态(cluster state)数据的持久化存储,并在集群完全重启时恢复它们。

Indices

索引模块管理全局级的索引设置,不包括索引级的(索引设置分为全局级和每个索引级)。它还封装了索引数据恢复功能。集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块实现的。

HTTP

HTTP模块允许通过JSON over HTTP的方式访问ES的API,HTTP模块本质上是完全异步的,这意味着没有阻塞线程等待响应。
使用异步通信进行 HTTP 的好处是解决了 C10k 问题(10k量级的并发连接)。
在部分场景下,可考虑使用HTTP keepalive以提升性能。
注意:不要在客户端使用HTTP chunking。

Transport

传输模块用于集群内节点之间的内部通信。
从一个节点到另一个节点的每个请求都使用传输模块。如同HTTP模块,传输模块本质上也是完全异步的。
传输模块使用 TCP 通信,每个节点都与其他节点维持若干 TCP 长连接。
内部节点间的所有通信都是本模块承载的。

Engine

Engine模块封装了对Lucene的操作及translog的调用,它是对一个分片读写操作的最终提供者。
ES使用Guice框架进行模块化管理。
Guice是Google开发的轻量级依赖注入框架(IoC)。
软件设计中经常说要依赖于抽象而不是具象,IoC 就是这种理念的实现方式,并且在内部实现了对象的创建和管理。


数据模型

PacificA算法

ES的数据副本模型基于主从模式(或称主备模式,HDFS和Cassandra为对等模式),在实现过程中参考了微软的PacificA算法(借鉴了其中部分思想,并非完全按照这个模型实现)。

我们先看一下PacificA算法的几个特点:

  • 设计了一个通用的、抽象的框架,而不是具体的、特定的算法。模型的正确性容易验证。
  • 配置管理和数据副本分离,Paxos负责管理配置,数据副本策略采取主从模式。
  • 将错误检测和配置更新放在数据副本的交互里实现,去中心化。

该算法涉及的几个术语如下。

  • Replica Group:一个互为副本的数据集合称为副本组。其中只有一个副本是主数据(Primary),其他为从数据(Secondary)。
  • Configuration:配置信息中描述了一个副本组都有哪些副本,Primary是谁,以及它们位于哪个节点。
  • Configuration Version:配置信息的版本号,每次发生变更时递增。
  • Serial Number:代表每个写操作的顺序,每次写操作时递增,简称SN。每个主副本维护自己的递增SN。
  • Prepared List:写操作的准备序列。存储来自外部请求的列表,将请求按照 SN 排序,向列表中插入的序列号必须大于列表中最大的SN。每个副本上有自己的Prepared List。
  • Committed List:写操作的提交序列。

设计前提与假设

  • 节点可以失效,对消息延迟的上限不做假设。
  • 消息可以丢失、乱序,但不能被篡改,即不存在拜占庭问题。
  • 网络分区可以发生,系统时钟可以不同步,但漂移是有限度的。

整个系统框架主要由两部分组成:存储管理和配置管理。

  • 存储管理:负责数据的读取和更新,使用多副本方式保证数据的可靠性和可用性;
  • 配置管理:对配置信息进行管理,维护所有配置信息的一致性。

数据副本策略

分片副本使用主从模式。
多个副本中存在一个主副本Primary和多个从副本Secondary。
所有的数据写入操作都进入主副本,当主副本出现故障无法访问时,系统从其他从副本中选择合适的副本作为新的主副本。

数据写入的流程如下:

  • 写请求进入主副本节点,节点为该操作分配SN,使用该SN创建UpdateRequest结构。然后将该UpdateRequest插入自己的prepare list。
  • 主副本节点将携带 SN 的 UpdateRequest 发往从副本节点,从节点收到后同样插入prepare list,完成后给主副本节点回复一个ACK。
  • 一旦主副本节点收到所有从副本节点的响应,确定该数据已经被正确写入所有的从副本节点,此时认为可以提交了,将此UpdateRequest放入committed list,committed list向前移动。
  • 主副本节点回复客户端更新成功完成。对每一个Prepare消息,主副本节点向从副本节点发送一个commit通知,告诉它们自己的committed point位置,从副本节点收到通知后根据指示移动committed point到相同的位置。

因为主副本只有在所有从副本将请求添加进 prepared list 之后才可以通过移动 committed point的方式将该请求插入committed list中,因此主副本的committed list是任何一个从副本的prepared list的前缀(或者称为子集)。

例如从副本prepared list中SN为1、2、3、4;主副本committed point中SN一定不会大于4,如1、2、3。
同时因为一个从副本只有在主副本将一个请求添加进committed list后才会把同样的请求添加进committed list中,因此一个从副本上的committed list是主副本上committed list的前缀,此不变式称为Commit Invariant。
令P为主副本,R为从副本,以下不变式成立:committed_R ⊆ committed_P ⊆ prepared_R。

错误检测

分布式系统经常存在网络分区、节点离线等异常。
全局的配置管理器维护权威配置信息,但其他各节点上的配置信息不一定同步,我们必须处理旧的主副本和新的主副本同时存在的情况—旧的主副本可能没有意识到重新分配了一个新的主副本,从而违反了强一致性。

PacificA使用了租约(lease)机制来解决这个问题。

主副本定期向其他从副本获取租约。这个过程中可能产生两种情况

  • 如果主副本节点在一定时间内(lease period)未收到从副本节点的租约回复,则主副本节点认为从副本节点异常,向配置管理器汇报,将该异常从副本从副本组中移除,同时它也将自己降级,不再作为主副本节点。
  • 如果从副本节点在一定时间内(grace period)未收到主副本节点的租约请求,则认为主副本异常,向配置管理器汇报,将主副本从副本组中移除,同时将自己提升为新的主。如果存在多个从副本,则哪个从副本先执行成功,哪个从副本就被提升为新主。

假设没有时钟漂移,只要grace period≥lease period,则租约机制就可以保证主副本会比任意从副本先感知到租约失效。同时任何一个从副本只有在它租约失效时才会争取去当新的主副本,因此保证了新主副本产生之前,旧的主分片已降级,不会产生两个主副本。

PacificA算法的这些概念对应在ES中:

  • Master负责维护索引元信息,类似配置管理器维护配置信息。
  • 集群状态中的routing_table存储了所有索引、索引有哪些shard、各自的主分片,以及位于哪个节点等信息,类似副本组。
  • SequenceNumber和Checkpoint类似PacificA算法中的Serial Number和Committed Point。


ES的数据副本模型

ES 中的每个索引都会被拆分为多个分片,并且每个分片都有多个副本。这些副本称为replication group(副本组,与PacificA中的副本组概念一致),并且在删除或添加文档的时候,各个副本必须同步。
否则从不同副本中读取的数据会不一致。我们把保持分片副本之间的同步,以及从中读取的过程称为数据副本模型(data replication model)。

ES的数据副本模型基于主备模式(primary-backup model),主分片是所有索引操作的入口,它负责验证索引操作是否有效。一旦主分片接受一个索引操作,主分片的副分片也会接受该操作。

下面讨论数据副本模型在写操作和读操作时如何交互。

基本写入模型

每个索引操作首先会使用routing参数解析到副本组,通常基于文档ID。一旦确定副本组,就会内部转发该操作到分片组的主分片中。
主分片负责验证操作和转发它到其他副分片。
ES维护一个可以接收该操作的分片的副本列表。这个列表叫作同步副本列表(in-sync copies),并由Master节点维护。
正如它的名字,这个“好”分片副本列表中的分片,都会保证已成功处理所有的索引和删除操作,并给用户返回 ACK。
主分片负责维护不变性(各个副本保持一致),因此必须复制这些操作到这个列表中的每个副本。

写入过程遵循以下基本流程

  • 请求到达协调节点,协调节点先验证操作,如果有错就拒绝该操作。然后根据当前集群状态,请求被路由到主分片所在节点。
  • 该操作在主分片上本地执行,例如,索引、更新或删除文档。这也会验证字段的内容,如果未通过就拒绝操作(例如,字段串的长度超出Lucene定义的长度)。
  • 操作成功执行后,转发该操作到当前in-sync 副本组的所有副分片。如果有多个副分片,则会并行转发。
  • 一旦所有的副分片成功执行操作并回复主分片,主分片会把请求执行成功的信息返回给协调节点,协调节点返回给客户端。

写故障处理

写入期间可能会发生很多错误—硬盘损坏、节点离线,或者某些配置错误,这些错误都可能导致无法在副分片上执行某个操作,虽然这比较少见,但是主分片必须汇报这些错误信息。

对于主分片自身错误的情况,它所在的节点会发送一个消息到Master节点。
这个索引操作会等待(默认为最多一分钟)Master节点提升一个副分片为主分片。这个操作会被转发给新的主分片。
注意Master同样会监控节点的健康,并且可能会主动降级主分片。这通常发生在主分片所在的节点离线的时候。

在主分片上执行的操作成功后,该主分片必须处理在副分片上潜在发生的错误。错误发生的原因可能是在副分片上执行操作时发生的错误,也可能是因为网络阻塞,导致主分片无法转发操作到副分片,或者副分片无法返回结果给主分片。
这些错误都会导致相同的结果:in-sync replica set中的一个分片丢失一个即将要向用户确认的操作。
为了避免出现不一致,主分片会发送一条消息到Master节点,要求它把有问题的分片从in-sync replica set中移除。
一旦Master确认移除了该分片,主分片就会确认这次操作。
注意Master也会指导另一个节点建立一个新的分片副本,以便把系统恢复成健康状态。

在转发请求到副分片时,主分片会使用副分片来验证它是否仍是一个活跃的主分片。
如果主分片因为网络原因(或很长时间的 GC)被隔离,则在它意识到被降级之前可能会继续处理传入的索引操作。
来自陈旧的主分片的操作将会被副分片拒绝。
当它接收来自副分片的拒绝其请求的响应时,它将会访问一下主节点,然后就会知道自己已被替换。
最后将操作路由到新的主分片。

如果没有副分片呢?

出现这种场景可能是因为索引配置或所有副分片都发生故障。
在这种情况下,主分片处理的操作没有经过任何外部验证,可能会导致问题。
另一方面,主分片节点将副分片失效的消息告知主节点,主节点知道主分片是唯一可用的副本。
因此我们确保主节点不会提升任何其他分片副本(过时的)为主分片,并且索引到主分片上的任何操作都不会丢失。
当然由于只运行单个数据副本,当物理硬件出问题时可能会丢失数据。

可以使用 wait_for_active_shards 缓解此类问题。

基本读取模型

通过 ID 读取是非常轻量级的操作,而一个巨大的复杂的聚合查询请求需要消耗大量 CPU和内存资源。
主从模式的一个好处是保证所有的分片副本都是一致的(正在执行的操作例外)。
因此单个in-sync中的某个副本也可以提供服务。当一个读请求被协调节点接收,这个节点负责转发它到其他涉及相关分片的节点,并整理响应结果发送给客户端。
接收用户请求的这个节点称为协调节点。

基本流程如下:

  • 把读请求转发到相关分片。注意,因为大多数搜索都会发送到一个或多个索引,通常需要从多个分片中读取,每个分片都保存这些数据的一部分。
  • 从副本组中选择一个相关分片的活跃副本。它可以是主分片或副分片。默认情况下, ES会简单地循环遍历这些分片。
  • 发送分片级的读请求到被选中的副本。
  • 合并结果并给客户端返回响应。注意,针对通过ID查找的get请求,会跳过这个步骤,因为只有一个相关的分片。

读故障处理

当分片不能响应一个读请求时,协调节点会从副本组中选择另一个副本,将请求转发给它。
没有可用的分片副本会导致重复的错误。
在某些情况下,例如_search,ES 倾向于尽早响应,即使只有部分结果,也不等待问题被解决(可以在响应结果的_shards字段中检查本次结果是完整的还是部分的)。


Allocation IDs

ES从5.x版本开始引入Allocation IDs的概念,用于主分片选举策略。
每个分片有自己唯一的Allocation ID,同时集群元信息中有一个列表,记录了哪些分片拥有最新数据。
ES通过在集群中保留多个数据副本的方式提供故障转移功能,当出现网络分区或节点挂掉时,更改操作可能无法在所有副本上完成,此时我们希望把写失败的副本标记出来。

ES的数据副本模型会假定其中一个数据副本为权威副本,称之为主分片。
所有的索引操作写主分片,完成后,主分片所在节点会负责把更改转发到活跃的备份副本,称之为副分片。

如果当前主分片临时或永久地变为不可用状态,则另一个分片副本将被提升为主分片。
因为主分片是权威的数据副本,因此在这个模型中,只把含有最新数据的分片作为主分片是至关重要的。
如果将一个旧数据的分片作为主分片,则它将作为最终副本,从而导致这个副本之后的数据将会丢弃。


Sequence IDs

ES从6.0版本开始引入了Sequence IDs概念,使用唯一的ID来标记每个写操作。
通过这个ID我们有了索引操作的总排序。

写操作先到达主分片,主分片写完后转发到副分片,在转发到副分片之前,增加一个计数器,为每个操作分配一个序列号是很简单的。
但是由于节点离线随时可能发生,例如网络分区等,主分片可能被其他副分片取代,仅仅由主分片分配一个序列号无法保证全局唯一性和单调性。
因此我们把当前主分片做一个标记,放到每个操作中,这就是Primary Terms。
这样来自旧的主分片的迟到的操作就可以被检测到然后拒绝(虽然Allocation IDs可以让主分片分配在拥有最新数据的分片上,但仍然可能存在某些情况下主分片上的数据并非最新,例如手工分配主分片到有旧数据的副本)。

Primary Terms和Sequence Numbers

第一步是能够区分新旧两种主分片,我们必须找到一种方法来识别是来自较旧的主分片操作还是来自较新的主分片的操作。最重要的是,整个集群需要达成一致。为此,我们添加了Primary Terms。
它由主节点分配,当一个主分片被提升时,Primary Terms递增。
然后持久化到集群状态中,从而表示集群主分片所处的一个版本。
有了Primary Terms,操作历史中的任何冲突都可以通过查看操作的Primary Terms来解决。
新的Terms优先于旧Terms,拒绝过时的操作,避免混乱的情况。

一旦我们有了Primary Terms的保护,就可以添加一个简单的计数器,给每个操作分配一个Sequence Numbers(序列号)。
Sequence Numbers使我们能够理解发生在主分片节点上的索引操作的特定顺序,接下来讨论Sequence Numbers带来的各种好处。
可以在Response中看到分配的Sequence Numbers和Primary Terms:

我们再次整理一下这两个概念

  • Primary Terms由主节点分配给每个主分片,每次主分片发生变化时递增。这和Raft中的term,以及Zab中Viewstamped Replication的view-number概念很相似。
  • Sequence Numbers标记发生在某个分片上的写操作。由主分片分配,只对写操作分配。假设索引website有2个主分片和1个副分片,当分片website[0]的序列号增加到5时,它的主分片离线,副分片被提升为新的主分片,对于后续写操作,序列号从6开始递增。分片website[1]有自己独立的序列号计数器。

主分片每次向副分片转发写请求时,会带上这两个值。
为了实现将操作排序,当我们比较两个操作o1和o2时,如果o1 < o2,那么意味着:s1.seq# < s2.seq#或者(s1.seq# == s2.seq# and s1.term < s2.term)“等于”和以上“大于”以类似的方式定义。

本地及全局检查点

有了Primary Terms和Sequence Numbers,我们就有了在理论上能够检测出分片之间差异,并在主分片失效时,重新对齐它们的工具。
旧主分片就可以恢复为与拥有更高 Primary Terms值的新主分片一致:从旧主分片中删除新主分片操作历史中不存在的操作,并将缺少的操作索引到旧主分片。

主分片写入一条数据成功后,本地检查点向前推进

主分片将写请求转发到副分片,副分片本地处理成功后,将本地检查点向前推进

主分片收到所有副分片都处理成功的消息,根据汇报的各副本上的本地检查点更新全局检查点

在下一次索引操作时,主分片节点将全局检查点发送到所有分片副本

用于快速恢复(Recovery)

当ES恢复一个分片时,需要保证恢复之后与主分片一致。
对于冷数据来说,synced flush可以快速验证副分片与主分片是否相同,但对于热数据来说,恢复过程需要从主分片复制整个Lucene分段,如果分段很大,则是非常耗时的操作。

现在我们使用副本所知道的最后一个全局检查点,重放来自主分片事务日志(translog)中的相关更改。


_version

每个文档都有一个版本号(_version),当文档被修改时版本号递增。
ES 使用这个_version来确保变更以正确顺序执行。如果旧版本的文档在新版本之后到达,则它可以被简单地忽略。
例如索引recovery阶段就利用了这个特性。

版本号由主分片生成,在将请求转发给副本片时将携带此版本号。

版本号的另一个作用是实现乐观锁,如同其他数据库的乐观锁一样。我们在写请求中指定文档的版本号,如果文档的当前版本与请求中指定的版本号不同,则请求会失败。

在这个更新请求中,只有版本号设置为1,本次更新才能成功。否则ES会返回409 Conflict状态码。


写流程

分析ES写入单个和批量文档写请求的处理流程,仅限于ES内部实现,并不涉及Lucene内部处理。
在ES中,写入单个文档的请求称为Index请求,批量写入的请求称为Bulk请求。
写单个和多个文档使用相同的处理逻辑,请求被统一封装为BulkRequest。

文档操作的定义

  • INDEX:向索引中“put”一个文档的操作称为“索引”一个文档。此处“索引”为动词。
  • CREATE:put 请求可以通过 op_type 参数设置操作类型为 create,在这种操作下,如果文档已存在,则请求将失败。
  • UPDATE:默认情况下,“put”一个文档时,如果文档已存在,则更新它。
  • DELETE:删除文档。

Index/Bulk基本流程

新建、索引(这里的索引是动词,指写入操作,将文档添加到Lucene的过程称为索引一个文档)和删除请求都是写操作。
写操作必须先在主分片执行成功后才能复制到相关的副分片。

以下是写单个文档所需的步骤

  • 客户端向NODE1发送写请求。
  • NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0的主分片位于NODE3,因此请求被转发到NODE3上。
  • NODE3上的主分片执行写操作。如果写入成功,则它将请求并行转发到 NODE1和NODE2的副分片上,等待返回结果。当所有的副分片都报告成功,NODE3将向协调节点报告成功,协调节点再向客户端报告成功。

在客户端收到成功响应时,意味着写操作已经在主分片和所有副分片都执行完成。
写一致性的默认策略是quorum,即多数的分片(其中分片副本可以是主分片或副分片)在写入操作时处于可用状态。

quorum = int( (primary + number_of_replicas) / 2 ) + 1


Index/Bulk详细流程

以不同角色节点执行的任务整理流程如下图

协调节点流程

协调节点负责创建索引、转发请求到主分片节点、等待响应、回复客户端。
实现位于TransportBulkAction。执行本流程的线程池:http_server_worker。

参数检查

如同我们平常设计的任何一个对外服务的接口处理一样,收到用户请求后首先检测请求的合法性,把检查操作放在处理流程的第一步有问题就直接拒绝,对异常请求的处理代价是最小的。


处理pipeline请求

数据预处理(ingest)工作通过定义pipeline和processors实现。pipeline是一系列processors的定义,processors按照声明的顺序执行。

自动创建索引

如果配置为允许自动创建索引(默认允许),则计算请求中涉及的索引,可能有多个,其中有哪些索引是不存在的,然后创建它。如果部分索引创建失败,则涉及创建失败索引的请求被标记为失败。其他索引正常执行写流程。

创建索引请求被发送到Master节点,待收到全部创建请求的Response(无论成功还是失败的)之后,才进入下一个流程。Master节点什么时候返回Response?在Master节点执行完创建索引流程,将新的clusterState发布完毕才会返回。那什么才算发布完毕呢?默认情况下,Master发布clusterState的Request收到半数以上的节点Response,认为发布成功。负责写数据的节点会先执行一遍内容路由的过程以处理没有收到最新clusterState的情况。

对请求的预先处理

这里不同于对数据的预处理,对请求的预先处理只是检查参数、自动生成id、处理routing等。
由于上一步可能有创建索引操作,所以在此先获取最新集群状态信息。
然后遍历所有请求,从集群状态中获取对应索引的元信息,检查mapping、routing、id等信息。
如果id不存在,则生成一个UUID作为文档id。实现位于TransportBulkAction.BulkOperation#doRun。

检测集群状态

协调节点在开始处理时会先检测集群状态,若集群异常则取消写入。
例如Master节点不存在,会阻塞等待Master节点直至超时。

内容路由,构建基于shard的请求

将用户的 bulkRequest 重新组织为基于 shard 的请求列表。例如,原始用户请求可能有10个写操作,如果这些文档的主分片都属于同一个,则写请求被合并为1个。
所以这里本质上是合并请求的过程。

此处尚未确定主分片节点。
基于shard的请求结构如下:Map<ShardId, List<BulkItemRequest>> requestsByShard = new HashMap<>();
根据路由算法计算某文档属于哪个分片。
遍历所有的用户请求,重新封装后添加到上述map结构。

路由算法

路由算法就是根据routing和文档id计算目标shardid的过程。

一般情况下,路由计算方式为下面的公式:
shard_num = hash(_routing) % num_primary_shards

默认情况下,_routing值就是文档id。

ES使用随机id和Hash算法来确保文档均匀地分配给分片。
当使用自定义id或routing时,id 或routing 值可能不够随机,造成数据倾斜,部分分片过大。
在这种情况下,可以使用index.routing_partition_size配置来减少倾斜的风险。
routing_partition_size越大,数据的分布越均匀。

shard_num = (hash(_routing) + hash(_id) % routing_partition_size) %num_primary_shards

index.routing_partition_size取值应具有大于1且小于index.number_of_shards的值。

转发请求并等待响应

主要是根据集群状态中的内容路由表确定主分片所在节点,转发请求并等待响应。

遍历所有需要写的 shard,将位于某个 shard 的请求封装为 BulkShardRequest 类,调用TransportShardBulkAction#execute执行发送,在listener中等待响应,每个响应也是以shard为单位的。
如果某个shard的响应中部分doc写失败了,则将异常信息填充到Response中,整体请求做成功处理。
待收到所有响应后(无论成功还是失败的),回复给客户端。
转发请求的具体实现位于TransportReplicationAction.ReroutePhase#doRun。


主分片节点流程

主分片所在节点负责在本地写主分片,写成功后,转发写副本片请求,等待响应,回复协调节点。

检查请求

主分片所在节点收到协调节点发来的请求后也是先做了校验工作,主要检测要写的是否是主分片

是否延迟执行

判断请求是否需要延迟执行,如果需要延迟则放入队列,否则继续下面的流程。

判断主分片是否已经发生迁移

如果已经发生迁移,则转发请求到迁移的节点。

检测写一致性

在开始写之前,检测本次写操作涉及的shard,活跃shard数量是否足够,不足则不执行写入。
默认为1,只要主分片可用就执行写入。

写Lucene和事务日志

遍历请求,处理动态更新字段映射,然后调用InternalEngine#index逐条对doc进行索引。
Engine封装了Lucene和translog的调用,对外提供读写接口。
在写入Lucene之前,先生成Sequence Number和Version。这些都是在InternalEngine类中实现的。Sequence Number每次递增1,Version根据当前doc的最大版本加1。

索引过程为先写Lucene,后写translog。

flush translog

根据配置的translog flush策略进行刷盘控制,定时或立即刷盘

写副分片

现在已经为要写的副本shard准备了一个列表,循环处理每个shard,跳过unassigned状态的shard,向目标节点发送请求,等待响应。这个过程是异步并行的。

转发请求时会将SequenceID、PrimaryTerm、GlobalCheckPoint、version等传递给副分片。
replicasProxy.performOn(shard, replicaRequest, globalCheckpoint,...);

在等待Response的过程中,本节点发出了多少个Request,就要等待多少个Response。
无论这些Response是成功的还是失败的,直到超时。
收集到全部的Response后,执行finish()。给协调节点返回消息,告知其哪些成功、哪些失败了。


副分片节点流程

执行本流程的线程池:bulk。
执行与主分片基本相同的写doc过程,写完毕后回复主分片节点




GET流程

ES的读取分为GET和Search两种操作,这两种读取操作有较大的差异,GET/MGET必须指定三元组:_index_type_id
也就是说根据文档id从正排索引中获取内容。而Search不指定_id,根据关键词从倒排索引中获取内容。

GET基本流程

这个例子中的索引有一个主分片和两个副分片。

以下是从主分片或副分片中读取时的步骤

  • 客户端向NODE1发送读请求。
  • NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求转发到NODE2。
  • NODE2将文档返回给 NODE1,NODE1将文档返回给客户端。

NODE1作为协调节点,会将客户端请求轮询发送到集群的所有副本来实现负载均衡。在读取时,文档可能已经存在于主分片上,但还没有复制到副分片。
在这种情况下,读请求命中副分片时可能会报告文档不存在,但是命中主分片可能成功返回文档。一旦写请求成功返回给客户端,则意味着文档在主分片和副分片都是可用的。

GET详细分析

协调节点

执行本流程的线程池:http_server_worker。TransportSingleShardAction 类用来处理存在于一个单个(主或副)分片上的读请求。

将请求转发到目标节点,如果请求执行失败,则尝试转发到其他节点读取。在收到读请求后,处理过程如下。

  • 在TransportSingleShardAction.AsyncSingleAction构造函数中,准备集群状态、节点列表等信息。
  • 根据内容路由算法计算目标shardid,也就是文档应该落在哪个分片上。
  • 计算出目标shardid后,结合请求参数中指定的优先级和集群状态确定目标节点,由于分片可能存在多个副本,因此计算出的是一个列表。

具体的路由算法参考写流程分析

转发请求

作为协调节点,向目标节点转发请求,或者目标是本地节点直接读取数据。
发送函数声明了如何对Response进行处理:AsyncSingleAction类中声明对Response进行处理的函数。
无论请求在本节点处理还是发送到其他节点,均对Response执行相同的处理逻辑

发送的具体过程

  • 在TransportService::sendRequest中检查目标是否是本地node。
  • 如果是本地node,则进入TransportService#sendLocalRequest流程,sendLocalRequest不发送到网络,直接根据action获取注册的reg,执行processMessageReceived
  • 如果发送到网络,则请求被异步发送,“sendRequest”的时候注册 handle,等待处理Response,直到超时。
  • 等待数据节点的回复,如果数据节点处理成功,则返回给客户端;如果数据节点处理失败,则进行重试
数据节点

执行本流程的线程池:get。
数据节点接收协调节点请求的入口为:TransportSingleShardAction.ShardTransportHandler#messageReceived。

核心的数据读取实现在ShardGetService#innerGet()函数中

  • 通过 indexShard.get()获取 Engine.GetResult。Engine.GetResult 类与 innerGet 返回的GetResult是同名的类,但实现不同。indexShard.get()最终调用InternalEngine#get读取数据。
  • 调用ShardGetService#innerGetLoadFromStoredFields(),根据type、id、DocumentMapper等信息从刚刚获取的信息中获取数据,对指定的 field、source 进行过滤(source 过滤只支持对字段),把结果存于GetResult对象中。

InternalEngine的读取过程

InternalEngine#get过程会加读锁。处理realtime选项,如果为true,则先判断是否有数据可以刷盘,然后调用Searcher进行读取。Searcher是对IndexSearcher的封装。

MGET流程分析

主要流程如下

  • 遍历请求,计算出每个doc的路由信息,得到由shardid为key组成的request map。这个过程没有在TransportSingleShardAction中实现,是因为如果在那里实现,shardid就会重复,这也是合并为基于分片的请求的过程。
  • 循环处理组织好的每个 shard 级请求,调用处理 GET 请求时使用TransportSingleShardAction#AsyncSingleAction处理单个doc的流程。
  • 收集Response,全部Response返回后执行finishHim(),给客户端返回结果。回复的消息中文档顺序与请求的顺序一致。如果部分文档读取失败,则不影响其他结果,检索失败的doc会在回复信息中标出。




Search流程

GET操作只能对单个文档进行处理,由_index、_type和_id三元组来确定唯一文档。但搜索需要一种更复杂的模型,因为不知道查询会命中哪些文档。

找到匹配文档仅仅完成了搜索流程的一半,因为多分片中的结果必须组合成单个排序列表。集群的任意节点都可以接收搜索请求,接收客户端请求的节点称为协调节点。
在协调节点,搜索任务被执行成一个两阶段过程,即query then fetch。真正执行搜索任务的节点称为数据节点。

需要两个阶段才能完成搜索的原因是,在查询的时候不知道文档位于哪个分片,因此索引的所有分片(某个副本)都要参与搜索,然后协调节点将结果合并,再根据文档ID获取文档内容。
例如有5个分片,查询返回前10个匹配度最高的文档,那么每个分片都查询出当前分片的TOP 10,协调节点将5×10 = 50的结果再次排序,返回最终TOP 10的结果给客户端。

索引和搜索

ES中的数据可以分为两类:精确值和全文。

  • 精确值,比如日期和用户id、IP地址等。
  • 全文,指文本内容,比如一条日志,或者邮件的内容。

这两种类型的数据在查询时是不同的:对精确值的比较是二进制的,查询要么匹配,要么不匹配;
全文内容的查询无法给出“有”还是“没有”的结果,它只能找到结果是“看起来像”你要查询的东西,因此把查询结果按相似度排序,评分越高,相似度越大。

对数据建立索引和执行搜索的原理如下图

建立索引

如果是全文数据,则对文本内容进行分析,这项工作在 ES 中由分析器实现。分析器实现如下功能

  • 字符过滤器。主要是对字符串进行预处理,例如,去掉HTML,将&转换成and等。
  • 分词器(Tokenizer)。将字符串分割为单个词条,例如,根据空格和标点符号分割,输出的词条称为词元(Token)。
  • Token过滤器。根据停止词(Stop word)删除词元,例如,and、the等无用词,或者根据同义词表增加词条,例如,jump和leap。
  • 语言处理。对上一步得到的Token做一些和语言相关的处理,例如转为小写,以及将单词转换为词根的形式。语言处理组件输出的结果称为词(Term)。

分析完毕后,将分析器输出的词(Term)传递给索引组件,生成倒排和正排索引,再存储到文件系统中。

执行搜索

搜索调用Lucene完成,如果是全文检索,

  • 对检索字段使用建立索引时相同的分析器进行分析,产生Token列表
  • 根据查询语句的语法规则转换成一棵语法树
  • 查找符合语法树的文档
  • 对匹配到的文档列表进行相关性评分,评分策略一般使用TF/IDF
  • 根据评分结果进行排序。




分布式搜索过程

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。
假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。
也就是说一次搜索请求只会命中所有分片副本中的一个。







Lucene原理

Lucene自2000年发布第一个开源版本以来,在开源社区引起了很大的反响,为广大开发者提供了研发全文检索系统的利器。
Lucene作为Apache的顶级项目,有大量研发人员贡献源码,经过十几年的发展,目前Lucene已经十分成熟,可以说Lucene是当今最先进、最高效的全功能开源搜索引擎工具包。
但Lucene只是一个全文检索类库,Elasticsearch是一个建立在Lucene基础上的实时的分布式搜索引擎,2010年由Shay Bano发布。
相比于Lucene, Elasticsearch功能更加强大,使用更加方便。

信息检索模型

分词算法

分词算法概述

词是表达语义的最小单位。分词对搜索引擎的帮助很大,可以帮助搜索引擎程序自动识别语句的含义,从而使搜索结果的匹配度达到最高,因此分词的质量也就直接影响了搜索结果的精确度。
利用相同的分词器,把短语或者句子切分成相同的结果,才能保证检索过程顺利进行。
中文和英文的分词原理简介如下:

  • 英文分词的原理

基本的处理流程是:输入文本、词汇分割、词汇过滤(去除停留词)、词干提取(形态还原)、大写转为小写、结果输出。

  • 中文分词原理

中文分词比较复杂,并没有英文分词那么简单。这主要是因为中文的词与词之间并不像英文中那样用空格来隔开。中文分词主要有3种方法:基于词典匹配的分词方法、基于语义理解的分词、基于词频统计的分词。

词典匹配分词法

基于字典匹配的分词方法按照一定的匹配策略将输入的字符串与机器字典词条进行匹配,这种方法是最简单的也是最容易想到的分词办法,最早由北京航空航天大学的梁南元教授提出
字典匹配分词方法可以分为正向匹配、逆向匹配以及结合了两者的双向匹配算法;
按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;
按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与词性标注相结合的方法。

几种常用的词典分词方法如下

  • 正向最大匹配(由左到右的方向)
  • 逆向最大匹配(由右到左的方向)
  • 最少切分(是每一句中切除的词数最小)

语义理解分词法

一般结构中通常包括分词子系统、句法语义子系统、调度系统。在调度系统的协调下,分词子系统可以获得有关词、句子等的句法和语义信息,模拟人脑对句子的理解过程。
基于语义理解的分词方法需要使用大量的语言知识和信息。
目前国内外对汉语语言知识的理解和处理能力还没有达到语义层面,具体到语言信息很难组织成机器可直接读取、计算的形式,因此目前基于语义理解的分词系统还处在试验阶段。

词频统计分词法

这种做法基于人们对中文词语的直接感觉。通常词是稳定的字的组合,因此在中文文章的上下文中,相邻的字搭配出现的频率越多,就越有可能形成一个固定的词。
根据n元语法知识可以知道,字与字相邻同时出现的频率或概率能够较好地反映成词的可信度。实际的系统中,通过对精心准备的中文语料中相邻共现的各个字的组合的频度进行统计,计算不同字词的共现信息。
根据两个字的统计信息,计算两个汉字的相邻共现概率,统计出来的信息体现了中文环境下汉字之间结合的紧密程度。
当紧密程度高于某一个阈值时,便可认为此字组可能构成一个词。

倒排索引

索引是构成搜索引擎的核心技术之一,索引在日常生活中其实也是非常常见的,比如当我们看一本书的时候,我们首先会看书的目录,通过目录可以快速定位到某一章节的页码,加快对内容的查询速度。
倒排索引(Inverted index),也常被称为反向索引,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,它是文档检索系统中最常用的数据结构。


如果想查找包含关键词“美国”的文档,那么结果就是doc1和doc2。
这样从文档包含单词到单词所属文档的转换,就是倒排的由来。

布尔检索模型

在海量网页中与用户查询关键词相关的网页可能会有成千上万个,甚至更多。
那么信息检索系统是如何判断网页和查询关键词是相关的?内部的排序模型是怎样的?

布尔检索模型中主要有AND、OR、NOT三种逻辑运算,布尔逻辑运算符的作用是把检索词连接起来,构成一个逻辑检索式。

  • AND(或*):逻辑与,用来表示其所连接的两个检索项的交叉部分,即检索词的交集部分。例如检索同时含有关键词A和B的集合:A AND B
  • OR(或+):逻辑或,用于连接并列关系的检索词。表示查找含有检索词A和B之一,或同时包含检索词A和B的信息:A OR B
  • NOT(或-):逻辑非,排除不需要的和影响检索结果的概念。表示含有检索词A并且不含有检索词B的信息:A NOT B

运算符之间的优先级:NOT > AND > OR,如检索表达式:中国NOT日本AND歌曲OR小说,搜索结果为:名字包含中国但是不包含日本的歌曲或者小说。

如果想要查询包含“谷歌”“开源”但不包含“大会”的文档,构造布尔查询

1
谷歌 AND 开源 NOT 大会

分别取出“谷歌”“开源”以及“大会”对应的行向量,对“大会”对应的行向量取反算:

1
2
3
谷歌:    0   1   0   1
开源: 0 1 0 1
大会: 1 0 0 0(取反:0 1 1 1)

然后进行与运算

1
0101 AND  0101 AND 0111=0101

结果向量中第2和第4个元素为1,文档2和文档4是符合查询条件的结果。

布尔检索模型简单直观,有以下优点

  • 第一,与人们的思维习惯一致:用户可以通过布尔逻辑运算符“AND”“OR”“NOT”将用户的提问“翻译”成系统可接受的形式。
  • 第二,布尔逻辑式表达直观清晰。
  • 第三,方便用户进行扩检和缩检:用户可通过增加逻辑“与”进行缩小检索,增加逻辑“或”进行扩展检索。
  • 第四,易于计算机实现:由于布尔检索是以比较方式在集合中进行检索的,返回结果只有1和0,易于实现,这也是现在的各种检索系统中都提供布尔检索的重要原因。

布尔检索模型的缺点

  • 第一,它的检索策略只基于0和1二元判定标准。例如,一篇文档只有相关和不相关两种状态,缺乏文档分级(rank)的概念,不能进行关键词重要性排序,限制了检索功能。
  • 第二,没有反映概念之间内在的语义联系。所有的语义关系被简单的匹配代替,常常很难将用户的信息需求转换为准确的布尔表达式。
  • 第三,完全匹配会导致太少的结果文档被返回。没有加权的概念,容易出现漏检。

tf-idf权重计算

tf-idf中文称为词频-逆文档频率,用以计算词项对于一个文档集或一个语料库中的一份文件的重要程度。
词项的重要性随着它在文档中出现的次数成正比增加,但同时会随着它在文档集中出现的频率成反比下降。
换句话说如果一个词项在一篇文档中出现的频率非常高,说明其重要性比较高,但是如果这个词项在文档集中的其他的文档中出现的频率也很高,那么说明这个词语有可能是比较通用比较常见的。

tf(term frequency)代表词项频率,要想计算一份文档中某个词的词频,统计该词在整篇文档中出现的次数即可。文档有长短之分

举个例子,一篇3000字的文章中词语“足球”出现了3次,我们很难断定这篇文章就是和足球相关的
但是一篇140字的微博中同样出现三次“足球”,基本可以断定微博内容和足球有关。

为了削弱文档长度的影响,需要将词频标准化,计算方法如下

词频标准化的方法不止一种,Lucene中采用了另外一种词频标准化方法

向量空间模型

向量空间模型(Vector Space Model, VSM)在上世纪70年代由信息检索领域奠基人Salton教授提出,并成功地应用于著名的SMART文本检索系统。
把对文本内容的处理简化为向量空间中的向量运算,它以空间上的相似度表达语义的相似度,直观易懂。
当文档被表示为文档空间的向量,就可以通过计算向量之间的相似性来度量文档间的相似性。

在同一个N维空间中存在两个非零向量A和B

  • 向量A记作A=( x1, x2, x3, x4……xn-2, xn)
  • 向量B记作B=(y1, y2, y3, y4…… yn-1, yn)
  • 向量A和B的夹角为θ,那么由夹角公式可得:

概率检索模型

概率检索模型从概率排序原理推导而来,是一种直接对用户需求相关性进行建模的方法,其基本思想是:给定一个查询,返回的文档能够按照查询和用户需求的相关性得分高低来排序。
目前最成功的概率检索模型是BM25(Best Match 25)模型,发展于1970年到1980年之间,目前很多商业搜索引擎使用的都是1994年在BM25的基础上进行改进的Okapi BM25模型。
下面从概率基础推导BM25的评分公式。

贝叶斯决策理论

概率检索模型的数学基础是贝叶斯决策理论,推导BM25模型的评分公式先从贝叶斯公式开始。
我们知道概率是对随机事件发生的可能性的度量,取值为0~1,比如抛一枚硬币,正面朝上的可能性和反面朝上的可能性都是0.5。
条件概率是指在某些前提条件下的概率问题,在事件A发生的前提下事件B发生的概率记为P(B|A)。
联合概率是指两个事件同时发生的概率,事件A和事件B相互独立,那么A和B同时发生的概率记为P(AB),显然P(AB)=P(BA)。
A和B同时发生的概率等于事件A发生的前提下事件B发生的概率

…略

二值独立模型

二值独立模型(Binary Independence Model,简称BIM)也是一种概率检索模型,通过做出一些假设估算文档或者查询的相似性概率。
二值独立模型中的二值是指文档和查询都表示成词项出现与否的布尔向量,词项出现记为1,词项不出现记为0。

独立是指假设词项在文档中的出现是相互独立的,通过对词项的独立性假设可以用数学的方法描述文本,把文档频率转换为词项概率的乘积




Lucene开发

  • Lucene简介
  • Lucene索引详解
  • Lucene特点和架构
  • Lucene查询详解
  • Lucene开发准备
  • Lucene搜索高亮
  • Lucene分词详解
  • Lucene新闻高频词提取案例

Lucene概述

维基百科用Lucene建立了一个站内的强大搜索功能,用以检索站内数以千万的词条。
IBM的商业软件Web Sphere也采用了Lucene作为全文索引引擎。

Lucene以其开放源代码的特性、优异的索引结构、良好的系统架构获得了越来越多的应用。
Lucene的优点主要有以下3点:

  • 稳定,索引性能高
    • 现代硬盘上每小时能够索引150GB以上的数据。
    • 对内存的要求小——只需要1MB的堆内存。
    • 增量索引和批量索引一样快。
    • 索引的大小约为索引文本大小的20%~30%。
  • 高效、准确、高性能的搜索算法
    • 搜索排名——最好的结果显示在最前面。
    • 许多强大的查询类型:短语查询、通配符查询、近似查询、范围查询等。
    • 对字段级别搜索(如标题,作者,内容)。
    • 可以对任意字段排序。
    • 支持搜索多个索引并合并搜索结果。
    • 支持更新操作和查询操作同时进行。
    • 灵活的切面、高亮、join和group by功能。
    • 速度快,内存效率高,容错性好。
    • 可选排序模型,包括向量空间模型和BM25模型。
    • 可配置存储引擎。
  • 跨平台解决方案
    • 作为Apache开源许可,在商业软件和开放程序中都可以使用Lucene。
    • 100%纯Java编写。
    • 对多种语言提供接口。

Lucene架构

先看上层应用,首先是信息采集的过程,文件系统、数据库、万维网以及手工输入的文件都可以作为信息采集的对象,也是要搜索的文档的来源,采集万维网上的信息一般使用网络爬虫。
完成信息采集之后到Lucene层面主要有两大任务:索引文档和搜索文档,索引文档的过程完成由原始文档到倒排索引的构建过程,搜索文档用以处理用户查询。
应用层的第三部分就是用户接口,用户输入查询关键词,Lucene完成文档搜索任务,经过分词、匹配、评分、排序等一系列过程之后返回用户想要的文档。

一次完整的搜索从用户输入要查询的关键词开始,比如想查找Lucene的相关学习资料,我们都会在Google或百度等搜索引擎中输入关键词,比如输入“Lucene,全文检索框架”,之后系统根据用户输入的关键词返回相关信息。一次检索大致可分为4步:

第一步:查询分析

正常情况下用户输入正确的查询,比如搜索“里约奥运会”这个关键词,用户输入正确完成一次搜索

第二步:分词技术

这一步利用自然语言处理技术将用户输入的查询语句进行分词
如标准分词会把“lucene,全文检索框架”分成:lucene|全|文|检|索|框|架|
空格分词会分成:lucene,|全文检索框架|
二分法会分成:lucene|全文|文检|检索|索框|框架|,还有简单分词等多种分词方法。

第三步:关键词检索

提交关键词后在倒排索引库中进行匹配,倒排索引就是关键词和文档之间的对应关系,就像给文档贴上标签。
比如在文档集中含有lucene关键词的有文档1、文档6、文档9,含有全文检索的有文档1、文档6,那么做与运算,同时含有lucene和全文检索的文档就是1和6,在实际的搜索中会有更复杂的文档匹配模型。

第四步:搜索排序

对多个相关文档进行相关度计算、排序,返回给用户检索结果。


Lucene分词详解

  • StopAnalyzer(停用词分词器)
    StopAnalyzer能过滤词汇中的特定字符串和词汇,并且完成大写转小写的功能。
  • StandardAnalyzer(标准分词器)
    StandardAnalyzer根据空格和符号来完成分词,还可以完成数字、字母、E-mail地址、IP地址以及中文字符的分析处理,还可以支持过滤词表,用来代替StopAnalyzer能够实现的过滤功能。
  • WhitespaceAnalyzer(空格分词)
    WhitespaceAnalyzer使用空格作为间隔符的词汇分割分词器。处理词汇单元的时候,以空格字符作为分割符号。
    分词器不做词汇过滤,也不进行小写字符转换。实际中可以用来支持特定环境下的西文符号的处理。
    由于不完成单词过滤和小写字符转换功能,也不需要过滤词库支持。
    词汇分割策略上简单使用非英文字符作为分割符,不需要分词词库支持。
  • SimpleAnalyzer(简单分词)
    SimpleAnalyzer具备基本西文字符词汇分析的分词器,处理词汇单元时,以非字母字符作为分割符号。分词器不能做词汇的过滤,只进行词汇的分析和分割。
    输出的词汇单元完成小写字符转换,去掉标点符号等分割符。在全文检索系统开发中,通常用来支持西文符号的处理,不支持中文。
    由于不完成单词过滤功能,所以不需要过滤词库支持。
    词汇分割策略上简单使用非英文字符作为分割符,不需要分词词库的支持。
  • CJKAnalyzer(二分法分词)
    内部调用CJKTokenizer分词器,对中文进行分词,同时使用StopFilter过滤器完成过滤功能,可以实现中文的多元切分和停用词过滤。
  • KeywordAnalyzer(关键词分词)
    把整个输入作为一个单独词汇单元,方便特殊类型的文本进行索引和检索。
    针对邮政编码、地址等文本信息使用关键词分词器进行索引项建立非常方便。

中文分词器对比

Lucene 6.0中自带的中文智能分词器SmartChineseAnalyzer和IK Analyzer
扩展停用词词典IK Analyzer默认的停用词词典为IKAnalyzer2012_u6/stopword.dic
这个停用词词典并不完整,只有30多个英文停用词,推荐使用扩展的停用词词表(下载地址:https://github.com/cseryp/stopwords)




Lucene索引详解

Lucene字段类型

文档是Lucene索引的基本单位,比文档更小的单位是字段,字段是文档的一部分,每个字段由3部分组成:名称(name)、类型(type)和取值(value)。
字段的取值一般为文本类型(字符串、字符流等)、二进制类型和数值类型。

Lucene中的字段类型主要有以下几种:

  • TextField
    TextField会把该字段的内容索引并词条化,但是不保存词向量。比如,包含整篇文档内容的body字段,常常使用TextField类型进行索引。
  • StringField
    StringField只会对该字段的内容索引,但是并不词条化,也不保存词向量。字符串的值会被索引为一个单独的词项。比如,有个字段是国家名称,字段名为“country”,以国家“阿尔吉利亚”为例,只索引不词条化是最合适的。
  • IntPoint
    IntPoint适合索引值为int类型的字段。IntPoint是为了快速过滤的,如果需要展示出来需要另存一个字段。比如,商品的数量用字段“productCount”存储,根据商品数量进行过滤操作时可以直接通过productCount字段获取结果,但是要想展示商品数量,需要另外再存储一个字段。
  • LongPoint
    用法和IntPoint类似,区别在LongPoint适合索引值为长整型long类型的字段。
  • DoublePoint
    用法和IntPoint类似,区别在DoublePoint适合索引值为double类型的字段。
  • SortedDocValuesField
    存储值为文本内容的DocValue字段,SortedDocValuesField适合索引字段值为文本内容并且需要按值进行排序的字段。
  • SortedSetDocValuesField
    存储多值域的DocValues字段,SortedSetDocValuesField适合索引字段值为文本内容并且需要按值进行分组、聚合等操作的字段。
  • NumericDocValuesField
    存储单个数值类型的DocValues字段,主要包括(int, long, float, double)。
  • SortedNumericDocValuesField
    存储数值类型的有序数组列表的DocValues字段。
  • StoredFieldS
    toredField适合索引只需要保存字段值不进行其他操作的字段。


Lucene查询详解

搜索入门

在Lucene中,处理用户输入的查询关键词其实就是构建Query对象的过程。
Lucene搜索文档需要实例化一个IndexSearcher对象,IndexSearcher对象的search()方法完成搜索过程,Query对象作为search()方法的对象。
搜索结果会保存在一个TopDocs类型的文档集合中,遍历TopDocs集合输出文档信息。

QueryParser实际上就是一个解析用户输入的工具,可以通过扫描用户输入的字符串生成Query对象。
当使用QueryParser构建用户Query时,要搜索的field和analyzer对象作为参数传入QueryParser类,告诉QueryParser在哪个字段内查找该关键字信息以及搜索时使用什么样的分词器。

1
2
3
4
QueryParser parser = new QueryParser(field, analyzer);
Query query = parser.parse("农村学生");
parser.setDefaultOperator(Operator.AND);
TopDocs topDocs = searcher.search(query, 10);

这里设置要查询的字段为title字段,查询所用的分词器为IK智能分词,查询关键词为“农村学生”,关键词经过分词器分成“农村”和“学生”两个词项,title中含有这两个词项中的任何一个的文档都是本次查询的匹配文档。
如果只想返回同时包含两个词项的文档,可以通过setDefaultOperator()方法把关键词理解为AND操作。

运行结果如下

1
2
3
4
5
6
7
8
加载扩展词典:ext.dic
加载扩展停止词典:stopword.dic
加载扩展停止词典:ext_stopword.dic
Query:+title:农村 +title:学生
DocID:1
id:2
title:北大迎4380名新生 农村学生700多人近年最多
文档评分:1.6022166

多域搜索(MultiFieldQueryParser)

通过MultiFieldQueryParser对象生成Query对象的代码如下

1
2
3
4
String[] fields = { "title", "content" };
Analyzer analyzer = new IKAnalyzer6x(true);
MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer);
Query multiFieldQuery = parser.parse("日本");

通过IndexSearcher搜索文档和打印结果的代码和2.4.1中的一样,这里省略,给出查询结果

1
2
3
4
5
6
7
title:日本content:日本
DocID:0
id:1
title:安倍晋三本周会晤特朗普 将强调日本对美国益处
content:日本首相安倍晋三计划2月10日在华盛顿与美国总统特朗普举行会晤时提出加
大日本在美国投资的设想
文档评分:2.0341508

词项搜索(TermQuery)

TermQuery是最简单也是最常用的Query。TermQuery可以理解成为“词项搜索”,在搜索引擎中最基本的搜索就是在索引中搜索某一词条,而TermQuery就是用来完成这项工作的。
在Lucene中词条是最基本的搜索单位,从本质上来讲一个词条其实就是一个key/value对。
只不过这个key是字段名,而value则表示字段中所包含的某个关键字。

要使用TermQuery进行搜索首先需要构造一个Term对象,示例代码如下:

1
2
Term term = new Term("title", "美国");
Query termQuery = new TermQuery(term);

运行结果如下

1
2
3
4
5
6
7
8
9
Query:title:美国
DocID:2
id:3
title:特朗普宣誓就任美国第45任总统
文档评分:0.53872687
DocID:0
id:1
title:安倍晋三本周会晤特朗普 将强调日本对美国益处
文档评分:0.38388318

布尔搜索(BooleanQuery)

BooleanQuery也是实际开发过程中经常使用的一种Query查询,它其实是一个组合的Query,在使用时可以把各种Query对象添加进去并标明它们之间的逻辑关系。
BooleanQuery本身来讲是一个布尔子句的容器,它提供了专门的API方法往其中添加子句,并标明它们之间的关系。
下面的代码中,创建了两个TermQuery, BooleanClause对象可以指定查询的包含关系,并作为参数通过BooleanQuery.Builder()构造布尔查询。

查询title字段中包含关键词“美国”并且“content”字段中不包含日本的文档,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Query query1 = new TermQuery(new Term("title","美国"));
Query query2 = new TermQuery(new Term("content","日本"));
BooleanClause bc1=new BooleanClause(query1,Occur.MUST);
BooleanClause bc2=new BooleanClause(query2,Occur.MUST_NOT);
BooleanQuery boolQuery=new BooleanQuery.Builder()
.add(bc1).add(bc2).build();




Query:+title:美国 -content:日本
DocID:2
id:1
title:特朗普宣誓就任美国第45任总统
content:当地时间1月20日,唐纳德·特朗普在美国国会宣誓就职,正式成为美国第45任总统。
文档评分:0.53872687

范围搜索(RangeQuery)

RangeQuery表示在某范围内的搜索条件,实现从一个开始词条到一个结束词条的搜索功能
在查询时“开始词条”和“结束词条”可以包含在内也可以不被包含在内。
查询新闻回复条数在500条到1000条之间的有哪些,构成Query对象的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Query rangeQuery=IntPoint.newRangeQuery("reply",500,1000);



Query:reply:[500 TO 1000]
DocID:0
id:1
title:安倍晋三本周会晤特朗普 将强调日本对美国益处
Reply:672
文档评分:1.0
DocID:1
id:2
title:北大迎4380名新生 农村学生700多人近年最多
Reply:995
文档评分:1.0

前缀搜索(PrefixQuery)

PrefixQuery就是使用前缀来进行查找的。通常情况下,首先定义一个词条Term。
该词条包含要查找的字段名以及关键字的前缀,然后通过该词条构造一个PrefixQuery对象,就可以进行前缀查找了。

查询包含以“学”开头的词项的文档,构造Query对象的代码如下

1
2
3
4
5
6
7
8
9
Term term = new Term("title", "学");
Query prefixQuery = new PrefixQuery(term);


Query:title:学*
DocID:1
id:2
title:北大迎4380名新生 农村学生700多人近年最多
文档评分:1.0

多关键字搜索(PhraseQuery)

用户在搜索引擎中进行搜索时,常常查找的并非是一个简单的单词,很有可能是几个不同的关键字。
这些关键字之间要么是紧密相连的,成为一个精确的短语,要么在这几个关键字之间还插有其他无关的内容。
PhraseQuery正是Lucene所提供的满足上述需求的一种Query对象。

它的add方法可以让用户向其内部添加关键字,在添加完毕后,用户还可以通过setSlop()方法来设定一个称之为“坡度”的变量来确定关键字之间是否允许或允许多少个无关词汇的存在。

1
2
3
4
5
6
PhraseQuery.Builder builder = new PhraseQuery.Builder();
builder.add(new Term("title", "日本"), 2);
builder.add(new Term("title", "美国"), 3);
PhraseQuery phraseQuery = builder.build();
打印phraseQuery对象:
Query:title:"? ?日本 美国"

模糊搜索(FuzzyQuery)

FuzzyQuery是一种模糊查询,它可以简单地识别两个相近的词语。
例如由于拼写错误把“Trump”拼成“Trmp”或者“Tramp”,使用FuzzyQuery仍可搜索到正确的结果

1
2
3
4
5
6
7
Term term = new Term("title", "Tramp");
FuzzyQuery fuzzyQuery = new FuzzyQuery(term);
Query:title:Tramp~2
DocID:2
id:3
title:特朗普宣誓(Donald Trump)就任美国第45任总统
文档评分:0.6056149

通配符搜索(WildcardQuery)

Lucene也提供了通配符的查询,就是使用WildcardQuery。

1
2
3
4
5
6
WildcardQuery wildcardQuery=new WildcardQuery(new Term(field, "学?"));
Query:title:学*
DocID:1
id:2
title:北大迎4380名新生 农村学生700多人近年最多
文档评分:1.0

运维

手动触发索引滚动

http://192.168.2.152:9200/app_hole_v3/_rollover/

1
2
3
4
5
6
7
8
9
10
11
{
"conditions": {
"max_age": "7d",
"max_docs": 5
},
"settings": {
"index.number_of_shards": 3,
"index.number_of_replicas": 1
},
"aliases":["app_hole_v3"]
}

Q2任务

公共

  • 开源软件、开源漏洞库数据清洗和采集, 核心竞争力
  • 国产信创软件搜集,信创操作系统识别

SCA

  • SCA Iac检测与修复
  • SCA K8s CIS检测
  • SCA 后台服务拆分与优化
  • SCA 数据在线升级服务
  • SCA 软件运行时资产分析
  • CVE漏洞代码关联技术研究与SCA漏洞可达性分析

Astp

  • Rasp功能增强商业化能力 【重点】(防御能力增强)
  • Astp后台重构与优化: core、proxy的服务版本同步新架构
  • 国产化信创支持: Agent兼容不同架构Cpu、操作系统; Agent支持国产数据库、中间件、架构、国产Jdk、国产类工具(Java、Arm环境、.netCore)
  • 组件CVE防御功能
  • 江苏农信需求
  • Iast未授权漏洞检测支持
  • Iast业务逻辑漏洞页面可配置及描述更新
  • Iast响应包敏感信息漏洞检测
  • Iast静态Api资产热加载后Api发现技术
  • CVE漏洞代码关联技术研究与Iast漏洞可达性分析

研究部分

  • CNAPP前端部分(一体化产品),一体化产品研究这个Q要出产品方案
  • 应用程序漏洞关联VAC与产品研究
  • SBOM产品或功能
  • SBOM标准与管理工具需求研究