《极客时间教程 - Elasticsearch 核心技术与实战》笔记二

《极客时间教程 - Elasticsearch 核心技术与实战》笔记二

第四章:深入搜索

基于词项和基于全文的搜索

基于词项的查询

Term 是表达语意的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理 Term

Term 级别查询:Term / Range / Exists / Prefix / Wildcard

在 ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。

可以通过 Constant Score 将查询转换成一个 Filtering,避免算法,并利用缓存,提高性能。

基于文本的查询

文本查询:match、match_phrase、query_string

索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表。

查询时,会先对输入的查询进行分词,然后每个词项柱哥进行底层的查询,最终将结果进行合并,并为每个文档计算一个相关度分值。

【示例】

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
DELETE products
PUT products
{
"settings": {
"number_of_shards": 1
}
}

POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }

GET /products

POST /products/_search
{
"query": {
"term": {
"desc": {
//"value": "iPhone"
"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
//"value": "iPhone"
//"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
//"explain": true,
"query": {
"term": {
"productID.keyword": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}

}
}
}

#设置 position_increment_gap
DELETE groups
PUT groups
{
"mappings": {
"properties": {
"names":{
"type": "text",
"position_increment_gap": 0
}
}
}
}

GET groups/_mapping

POST groups/_doc
{
"names": [ "John Water", "Water Smith"]
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": {
"query": "Water Water",
"slop": 100
}
}
}
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": "Water Smith"
}
}
}

结构化搜索

结构化搜索是指对结构化数据的搜索。

日期、布尔、和数字类型都是结构化的。它们都有精确的格式,可以根据这些格式进行逻辑操作,如:比较范围、判定大小。

文本也可以是结构化的。结构化的文本可以精确匹配(term)或部分匹配(prefix)

结构化结果只有是或否两个选项。

【示例】

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#结构化搜索,精确匹配
DELETE products
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

GET products/_mapping

#对布尔值 match 查询,有算分
POST products/_search
{
"profile": "true",
"query": {
"term": {
"avaliable": true
}
}
}

#对布尔值,通过 constant score 转成 filtering,没有算分
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": true
}
}
}
}
}

#数字类型 Term
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": 30
}
}
}

#数字类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"price": [
"20",
"30"
]
}
}
}
}
}

#数字 Range 查询
GET products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lte" : 30
}
}
}
}
}
}

# 日期 range
POST products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"date" : {
"gte" : "now-1y"
}
}
}
}
}
}

#exists 查询
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "date"
}
}
}
}
}

#处理多值字段
POST /movies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy"}
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"] }

#处理多值字段,term 查询是包含,而不是等于
POST movies/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"genre.keyword": "Comedy"
}
}
}
}
}

#字符类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"productID.keyword": [
"QQPX-R-3956-#aD8",
"JODL-X-1937-#pV7"
]
}
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": 30
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"date": "2019-01-01"
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"date": "2019-01-01"
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}

#对布尔数值
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": "false"
}
}
}
}
}

POST products/_search
{
"query": {
"term": {
"avaliable": {
"value": "false"
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": {
"value": "20"
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": "20"
}
}

POST products/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"must_not": {
"exists": {
"field": "date"
}
}
}
}
}
}
}

搜索的相关性算分

搜索的相关性打分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分(_score)。

ES5 之前,默认的相关性算法是 TD-IDF;ES5 后,采用 BM25。

词频(Term Frequency,TF) - 检索词在一篇文档中出现的频率

逆文档频率(Inverse Document Frequency,IDF) - log(全部文档数/检索词出现过的文档总数),用以表示检索词在所有文档中出现的频率。

Stop Word - 词项出现频率岁高,但对相关度几乎没有用户,例如:的、the、a 之类的词。

TF-IDF 本质上就是将 TF 求和变成了加权求和。

和 TF-IDF 相比,当 TF 无限增加时, BM 25 分支会趋于一个平稳值。

Boosting 是控制相关度的一种手段。

  • boost > 1,打分的权重提升;
  • 0 < boost < 1,打分的权重降低
  • boost < 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
PUT testscore
{
"settings": {
"number_of_shards": 1
},
"mappings": {
"properties": {
"content": {
"type": "text"
}
}
}
}

PUT testscore/_bulk
{ "index": { "_id": 1 }}
{ "content":"we use Elasticsearch to power the search" }
{ "index": { "_id": 2 }}
{ "content":"we like elasticsearch" }
{ "index": { "_id": 3 }}
{ "content":"The scoring of documents is caculated by the scoring formula" }
{ "index": { "_id": 4 }}
{ "content":"you know, for search" }

POST /testscore/_search
{
//"explain": true,
"query": {
"match": {
"content":"you"
//"content": "elasticsearch"
//"content":"the"
//"content": "the elasticsearch"
}
}
}

POST testscore/_search
{
"query": {
"boosting" : {
"positive" : {
"term" : {
"content" : "elasticsearch"
}
},
"negative" : {
"term" : {
"content" : "like"
}
},
"negative_boost" : 0.2
}
}
}

POST tmdb/_search
{
"_source": [
"title",
"overview"
],
"query": {
"more_like_this": {
"fields": [
"title^10",
"overview"
],
"like": [
{
"_id": "14191"
}
],
"min_term_freq": 1,
"max_query_terms": 12
}
}
}

Query & Filtering 实现多字符串多字段查询

ES 中,有 Query 和 Filter 两种不同的 Context

  • Query - 有相关性计算
  • 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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

#基本语法
POST /products/_search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :1
}
}
}

#改变数据模型,增加字段。解决数组包含而不是精确匹配的问题
POST /newmovies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy","genre_count":1 }
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"],"genre_count":2 }

#must,有算分
POST /newmovies/_search
{
"query": {
"bool": {
"must": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}

]
}
}
}

#Filter。不参与算分,结果的 score 是 0
POST /newmovies/_search
{
"query": {
"bool": {
"filter": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}
]

}
}
}

#Filtering Context
POST _search
{
"query": {
"bool" : {

"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
}
}
}
}

#Query Context
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

POST /products/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"productID.keyword": {
"value": "JODL-X-1937-#pV7"}}
},
{"term": {"avaliable": {"value": true}}
}
]
}
}
}

#嵌套,实现了 should not 逻辑
POST /products/_search
{
"query": {
"bool": {
"must": {
"term": {
"price": "30"
}
},
"should": [
{
"bool": {
"must_not": {
"term": {
"avaliable": "false"
}
}
}
}
],
"minimum_should_match": 1
}
}
}

#Controll the Precision
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :2
}
}
}

POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "brown" }},
{ "term": { "text": "red" }},
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }}
]
}
}
}

POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }},
{
"bool":{
"should":[
{ "term": { "text": "brown" }},
{ "term": { "text": "brown" }},
]
}

}
]
}
}
}

DELETE blogs
POST /blogs/_bulk
{ "index": { "_id": 1 }}
{"title":"Apple iPad", "content":"Apple iPad,Apple iPad" }
{ "index": { "_id": 2 }}
{"title":"Apple iPad,Apple iPad", "content":"Apple iPad" }

POST blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": {
"query": "apple,ipad",
"boost": 1.1
}
}},

{"match": {
"content": {
"query": "apple,ipad",
"boost":
}
}}
]
}
}
}

DELETE news
POST /news/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple iPad" }
{ "index": { "_id": 3 }}
{ "content":"Apple employee like Apple Pie and Apple Juice" }

POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
}
}
}
}

POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
},
"must_not": {
"match":{"content":"pie"}
}
}
}
}

POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}

单字符串多字段查询 - DisMaxQuery

Disjunction Max Query - 将评分最高的字符评分作为结果返回,满足多个字段是竞争关系的场景

对最佳字段查询进行调优:通过控制 tie_breaker 参数,引入其他字段对计算的一些影响

【示例】

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
PUT /blogs/_doc/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}

POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}

POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}

POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "Quick pets"
}
},
{
"match": {
"body": "Quick pets"
}
}
],
"tie_breaker": 0.2
}
}
}

单字符串多字段查询 - Multi Match

场景:最佳字段、多数字段、混合字段

multi_match

best_fields 是默认类型,可以不指定

minimum_should_match 等参数可以传递到生成的 query 中

【示例】

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.2
}
}
}

POST blogs/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "Quick pets",
"fields": ["title","body"],
"tie_breaker": 0.2,
"minimum_should_match": "20%"
}
}
}

POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}

POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ]
}
}

DELETE /titles
PUT /titles
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "string",
"analyzer": "english",
"fields": {
"std": {
"type": "string",
"analyzer": "standard"
}
}
}
}
}
}
}

PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english"
}
}
}
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

GET titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}

DELETE /titles
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {"std": {"type": "text","analyzer": "standard"}}
}
}
}
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}

GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [
"title^10",
"title.std"
]
}
}
}

多语言及中文分词与检索

自然语言与查询 recall

处理人类自然语言时,有些情况下,尽管搜索和原文不完全匹配,但希望搜到一些内容。

可采取的优化:

  • 归一化词元
  • 抽取词根
  • 包含同义词
  • 拼写错误

混合语言、中文的分词都存在一些挑战

  • 词干提取
  • 不正确的文档频率
  • 语言识别
  • 歧义

中文分析器

  • elasticsearch-analysis-hanlp
  • elasticsearch-analysis-ik
  • elasticsearch-analysis-pinyin

SpaceJam 一个全文搜索的实例

使用 SearchTemplate 和 IndexAlias 进行查询

综合排序:Function Score Query 优化算分

ES 默认会以文档的相关度算分进行排序

可以指定一个或多个字段进行排序

使用相关度算分排序,不能满足某些特定条件

function_score 可以在查询结束后,对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。提供了几种默认的计算分值的函数:

  • weight - 为每一个文档设置一个简单而不被规范化的权重
  • field_value_factor - 使用该数值来修改 _score
  • random_score - 为每一个用户使用一个不同的,随机算分结果
  • 衰减函数 - 以某个字段的值为标准,距离某个值越近,得分越高
  • script_score - 自定义脚本完全控制所需逻辑

Boost Mode

  • multiply
  • sum
  • min / max
  • replace

Max Boost 可以将算分控制在一个最大值

【示例】

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
DELETE blogs
PUT /blogs/_doc/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 0
}

PUT /blogs/_doc/2
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 100
}

PUT /blogs/_doc/3
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 1000000
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
},
"boost_mode": "sum",
"max_boost": 3
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"random_score": {
"seed": 911119
}
}
}
}

Term&PhraseSuggester

自动补全与基于上下文的提示

Completion Suggester,对性能要求比较苛刻。采用了不同的数据结构,并非通过倒排索引来完成。而是将 Analyze 的数据编码成 FST 和索引一起存放。FST 会被 ES 整个加载进内存,速度很快。

精准度:completion > Phrase > Term

召回率:Term > Phrase > Completion

性能:Completion > Phrase > Term

跨集群搜索

早期版本,通过 Tribe Node 可以实现多集群访问的需求,但是还存在一定的问题,现已废弃。

ES 5.3 引入了跨集群搜索的功能。

  • 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理
  • 不需要以 Client Node 形式加入其他集群

第五章:分布式特性及分布式搜索的机制

集群分布式模型及选主与脑裂问题

分布式特性:高可用、易扩展(水平扩展,支持 PB 级数据)

ES 集群名称可以通过配置或 -E cluster.name=xxx 来设定。

ES 节点本质上就是一个 JAVA 进程。一台机器上可以运行多个 ES 进程。

每个 ES 节点都有名字,可以通过配置文件或 -E node.name=xxx 来设定。

每个 ES 节点在启动后,会分片一个 UID,保存在 data 目录下。

  • Coordinating Node - 处理请求的节点,叫 Coordinating Node(协调节点),每个节点默认都是协调节点。
  • Data Node - 保存分片数据的节点。默认就是 data node,可以设置 node.data: false 禁止成为数据节点。
  • Master Node - 负责处理创建、删除索引等请求;决定分片被分配到哪个节点;负责索引的创建与删除;维护并更新集群状态。
    • 节点启动后,默认为主节点的候选节点,可以在必要时参与选主,成为 master node。可以设置 node.master: false 禁止成为主节点候选节点。
  • 集群状态
    • 所有的节点信息
    • 所有的索引和其相关的 mapping、setting
    • 分片的路由信息
    • 每个节点上都保存了集群的状态信息
    • 只有 master 节点才能修改集群的状态信息,并负责同步给其他节点

选主过程

集群中的节点互 ping,node id 最小的会成为被选举的节点。

其他节点会加入集群,但是不承担 master 节点的角色,一旦发现被选中的主节点丢失,就会选举出新的 master。

避免脑裂

7.0 之前,minimum_master_nodes 设为 N / 2 + 1

7.0 开始,ES 自动选择以形成仲裁的节点。

分片与集群的故障转移

主分片 - 水平扩展

副本分片 - 高可用:冗余、故障转移

分片数过小:无法通过增加节点实现扩展;分片数过大:使得单个分片容量很小,导致一个节点上有过多分片,影响性能。

文档分布式存储

文档到分配的路由

1
shard = hash(_routing) % number_of_primary_shards

hash 算法确保离散

默认的 _routing 值是文档 id,可以定制 routing 数值

这也是设置 setting 中主分片数后,不能随意修改的根本原因。

分片及其生命周期

分片是 ES 中的最小工作单元。分片是一个 Lucene 的索引。

倒排索引不可变性

无需考虑并发写文件的问题,避免了锁机制带来的性能问题

一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能

如果需要让一个新的文档可以被搜索,需要重建整个索引。

Lucene Index

在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包闭的,不可变更的。多个 Segment 汇总在一起,称为 Lucene 的 Index,其对应的就是 ES 中的 shard

当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segment,并且对结果汇总。Lucene 中有一个文件,用来记录所有 Segment 信息,叫做 Commit Point。

删除的文档信息,保存在 .del 文件中。

什么是 Refresh

将 Index buffer 写入 Segment 的过程叫 refresh。refresh 不执行 fsync 操作。

refresh 默认 1 秒发生一次,refresh 后,数据就可以被搜索到了。

如果系统有大量的数据写入,就会产生很多的 Segment

index buffer 被占满时,会触发 refresh,默认是 JVM 的 10%

什么是事务日志

segment 写入磁盘的过程相对耗时,借助文件系统缓存,refresh 时,现将 segment 写入缓存以开放查询

为了保证数据不丢失,所以在 index 文档时,同时写事务日志,高版本开始,事务日志默认落盘。每个分片有一个事务日志。

在 ES refresh 时,index buffer 被清空,事务日志不会清空

什么是 flush

调用 refresh,index buffer 清空并 refresh

调用 fsync,将缓存中的 segments 写入磁盘

清空事务日志

默认 30 分钟调用一次

事务日志满(512MB)

Merge

Segment 很多,需要被定期合并

ES 和 Lucene 会自动进行 Merge 操作

剖析分布式查询及相关性评分

ES 搜索分为两阶段:

  1. Query
  2. Fetch

Query 阶段

用户发出搜索请求到 ES 节点。节点收到请求后,会以协调节点的身份,在所有主副本分片中随机选择主分片数个分片,发送查询请求。

被选中的分片执行查询,进行排序。然后,每个分片都会返回 from +size 个排序后的文档 id 和排序值给协调节点。

Fetch 阶段

协调节点会将 Query 阶段从每个分片获取的排序后的文档 id 列表,进行重排序,选取 from 到 from +size 个文档的 id

以 multi get 请求的方式,到相应的分片获取详细的文档数据

潜在问题

性能问题

  • 每个分片上需要查的文档数 = from + size
  • 最终协调节点需要处理 = 主分片数 * ( from + size)
  • 深度分页

相关性算分

每个分片都要基于自己分片上的数据进行相关度计算。这会导致打分偏离的情况,尤其是数据量很少时。当文档总数很少的情况下,主分片数越多,相关性计算会越不准。

解决算分不准的方法

将主分片数设为 1;

使用 _search?search_type=dfs_query_then_fetch,消耗更多 CPU 和内存,执行性能低下

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
DELETE message
PUT message
{
"settings": {
"number_of_shards": 20
}
}

GET message

POST message/_doc?routing=1
{
"content":"good"
}

POST message/_doc?routing=2
{
"content":"good morning"
}

POST message/_doc?routing=3
{
"content":"good morning everyone"
}

POST message/_search
{
"explain": true,
"query": {
"match_all": {}
}
}

POST message/_search
{
"explain": true,
"query": {
"term": {
"content": {
"value": "good"
}
}
}
}

POST message/_search?search_type=dfs_query_then_fetch
{

"query": {
"term": {
"content": {
"value": "good"
}
}
}
}

排序及 DocValues&Fielddata

默认采用相关性算分对结果进行降序排序

可以通过设定 sorting 参数,自行设定排序

如果不指定 _score,算分为 null

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#单字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

#多字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}

GET kibana_sample_data_ecommerce/_mapping

#对 text 字段进行排序。默认会报错,需打开 fielddata
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"customer_full_name": {"order": "desc"}}
]
}

#打开 text 的 fielddata
PUT kibana_sample_data_ecommerce/_mapping
{
"properties": {
"customer_full_name" : {
"type" : "text",
"fielddata": true,
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}

#关闭 keyword 的 doc values
PUT test_keyword
PUT test_keyword/_mapping
{
"properties": {
"user_name":{
"type": "keyword",
"doc_values":false
}
}
}

DELETE test_keyword

PUT test_text
PUT test_text/_mapping
{
"properties": {
"intro":{
"type": "text",
"doc_values":true
}
}
}

DELETE test_text

DELETE temp_users
PUT temp_users
PUT temp_users/_mapping
{
"properties": {
"name":{"type": "text","fielddata": true},
"desc":{"type": "text","fielddata": true}
}
}

Post temp_users/_doc
{"name":"Jack","desc":"Jack is a good boy!","age":10}

#打开 fielddata 后,查看 docvalue_fields 数据
POST temp_users/_search
{
"docvalue_fields": [
"name","desc"
]
}

#查看整型字段的 docvalues
POST temp_users/_search
{
"docvalue_fields": [
"age"
]
}

分页与遍历-FromSize&SearchAfter&ScrollAPI

from + size

当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。

页数越深,占用内存越多。为了避免深分页问题,ES 默认限定到 10000 个文档。

search after

实时获取下一页文档信息,不支持指定页数,只能向下翻页。

需要指定 sort,并保证值是唯一的

然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询

scroll

创建一个快照,有新的数据写入以后,无法被查到。

每次持续后,输入上一次的 scroll id

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
82
83
84
85
86
POST tmdb/_search
{
"from": 10000,
"size": 1,
"query": {
"match_all": {

}
}
}

#Scroll API
DELETE users

POST users/_doc
{"name":"user1","age":10}

POST users/_doc
{"name":"user2","age":11}

POST users/_doc
{"name":"user2","age":12}

POST users/_doc
{"name":"user2","age":13}

POST users/_count

POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}

POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after":
[
10,
"ZQ0vYGsBrR8X3IP75QqX"],
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}

#Scroll API
DELETE users
POST users/_doc
{"name":"user1","age":10}

POST users/_doc
{"name":"user2","age":20}

POST users/_doc
{"name":"user3","age":30}

POST users/_doc
{"name":"user4","age":40}

POST /users/_search?scroll=5m
{
"size": 1,
"query": {
"match_all" : {
}
}
}

POST users/_doc
{"name":"user5","age":50}
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}

处理并发读写

采用乐观锁机制

内部版本控制:_seq_no + primary_term

外部版本控制:version + version_type=external

第六章:深入聚合分析

Bucket&Metric聚合分析及嵌套聚合

Metric(统计) - 统计计算

Bucket(分组) - 按一定规则,将文档分配到不同的桶中。

Metric 聚合

  • 单值聚合 - 只输出一个分析结果
    • min、max、avg、sum、cardinality
  • 多值聚合 - 输出多个分析结果
    • stats、extended_stats、percentile、percentile_rank、top_hits

Pipeline聚合分析

Pipeline聚合支持对聚合分析的结果,进行再次聚合分析。

Pipeline 聚合的分析结果会输出到原结果中,根据位置的不同,分为两类:

聚合的作用范围及排序

ES 聚合分析的默认作用范围是 query 的查询结果集。

同时 ES 还支持以下方式改变聚合的作用范围:

  • filter
  • post_filter
  • global

指定 order,按照 _count_key 进行排序。

聚合分析的原理及精准度问题

ES 在进行聚合分析时,协调节点会在每个分片的主分片、副分片中选一个,然后在不同分片上分别进行聚合计算,然后将每个分片的聚合结果进行汇总,返回最终结果。

由于,并非基于全量数据进行计算,所以聚合结果并非完全准确。

要解决聚合准确性问题,有两个解决方案:

  • 解决方案 1:当数据量不大时,设置 Primary Shard 为 1,这意味着在数据全集上进行聚合。
  • 解决方案 2:设置 shard_size 参数,将计算数据范围变大,进而使得 ES 的整体性能变低,精准度变高。shard_size 值的默认值是 size * 1.5 + 10

第七章:数据建模(略)

第八章:保护你的数据(略)

第九章:水平扩展 Elasticsearch 集群

第十章:生产环境中的集群运维(略)

第十一章:索引生命周期管理(略)

第十二章:用Logstash和Beats构建数据管道(略)

第十三章:用Kibana进行数据可视化分析(略)

第十四章:探索X-Pack套件(略)

参考资料