Week02总结

1. 文档学习

本周文档阅读内容大模块为 Search APIIndices API,重点内容包含以下模块

  • doc_valueexplaincollaspaceindex boost

  • 特殊查询rescore query、script_fields、inner_hits 以及 post_filter

  • 分页查询 from+sizescroll、search_after

  • search_typequery_then_fetchdfs_query_then_fetch

  • sortsource_filter 过滤;

  • search_templateexplainprofile_API

  • 索引操作 sharink、split、rollover、open/close(不常用)

  • 别名 index aliases 与索引模板 index template (疑问,templateversion 具体什么场景下使用?)
  • synced_flushforce_merge

2. Search API总结

2.1 index、doc_values、store 的区别

2.2 rescore query

重新查询,默认与 query中的得分进行加法计算,使用multi_rescore 时,后者会看到前一个 rescore 算分后的结果集,所以通常会先给定一个大的 window_size,第二个 rescore 再缩小 window_size 的方式。例如:

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 twitter/_search
{
"query" : {
"match" : {
"message" : {
"operator" : "or",
"query" : "the quick brown"
}
}
},
"rescore" : [ {
"window_size" : 100,
"query" : {
"rescore_query" : {
"match_phrase" : {
"message" : {
"query" : "the quick brown",
"slop" : 2
}
}
},
"query_weight" : 0.7,
"rescore_query_weight" : 1.2
}
}, {
"window_size" : 10,
"query" : {
"score_mode": "multiply",
"rescore_query" : {
"function_score" : {
"script_score": {
"script": {
"source": "Math.log10(doc.likes.value + 2)"
}
}
}
}
}
} ]
}

2.3 script_fields的取值方式

script_fields 有两种取值方式 ,doc['my_field'].value的方式会通过 term 加载整个字段到缓存中,使得 script 的执行更快,但占用的内存也更多,同时这种写法也只支持简单的值字段,无法取到 JSON Object 对象。官方的推荐做法仍旧是使用 doc[...],因为params['_source']['my_field'] 的方式在每次处理时都会去加载 _source,执行会慢很多。

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
POST test/_doc
{
"request":{
"uri":"http://www.baidu.com",
"param":{
"id":"1",
"name":"测试"
}
}
}
//查询会失败,无法获取
GET test/_search
{
"query": {
"match_all": {}
},
"script_fields": {
"test1": {
"script": {
"lang": "painless",
//查询会失败,无法取到request对象
"source": "doc['request'].value"
//可以使用如下方式, value 可以不需要,text字段需要指定fielddata或keyword
//"source": "doc['request.uri.keyword']"
}
}
}
}

//方式二可以取到 json object
GET test/_search
{
"query": {
"match_all": {}
},
"script_fields": {
"test2": {
"script": {
"lang": "painless",
"source": "params['_source']['request']"
}
}
}
}

2.4 post_filter

应用场景:

  • 结果中再次搜索(不推荐,会导致 filter_query 的缓存效果失效)
  • 聚合后过滤返回的 hits 的结果集
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
// 索引定义、数据初始化
DELETE shirts
PUT shirts
{
"mappings": {
"properties": {
"brand": {
"type": "keyword"
},
"color": {
"type": "keyword"
},
"model": {
"type": "keyword"
}
}
}
}

POST _bulk
{"index":{"_index":"shirts","_id":"1"}}
{"brand":"gucci","color":"red","model":"slim"}
{"index":{"_index":"shirts","_id":"2"}}
{"brand":"gucci","color":"black","model":"slim"}
{"index":{"_index":"shirts","_id":"3"}}
{"brand":"gucci","color":"black","model":"ntrms"}

结果中再次搜索 post_fiter

用户第一次搜索 gucci、基于第一次的结果中,再次搜索颜色为 red,此时的查询 dsl 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /shirts/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"brand": "gucci"
}
}
]
}
},
"post_filter": {
"term": {
"color": "red"
}
}
}

post_filter 先聚合,再过滤 hits

以下 dsl 的查询逻辑为:先筛选所有 brandgucci 的数据,然后按照 model 进行聚合。子聚合在父聚合的基础上,筛选颜色只为 red 的数据并进行二次聚合;由于此时返回的 hits 中包含的是父聚合中的所有数据,于是通过 post_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
GET shirts/_search
{
"query": {
"bool": {
"filter": {
"term": {
"brand": "gucci"
}
}
}
},
"aggs": {
"models": {
"terms": {
"field": "model"
}
},
"color_red": {
"filter": {
"term": {
"color": "red"
}
},
"aggs": {
"models": {
"terms": {
"field": "model"
}
}
}
}
},
"post_filter": {
"bool": {
"should": [
{
"term": {
"color": {
"value": "red"
}
}
}
]
}
}
}

2.5 from+size、scroll、search_after对比

  • from + size 适用于普通的分页查询,也不需要深度分页,支持随机翻页,数据是实时的;
  • scroll 属于索引快照数据,通过不停地滚动查询来遍历所有数据,适合深度分页,但不具有实时性,当滚动开始后直到结束也不会发生变化;
  • search_after,通过具备唯一性的属性来排序,按照排序值来实现翻页效果,适合深度分页,并且数据实时的。但不能进行随机翻页,只能一页一页的展示。

需要注意的是 : search_after 需要指定唯一字段为排序依据,否则同分值的会被过滤掉。

如下所示的search_after 是通过 taxful_total_size 排序后的数值进行翻页,当出现金额相同的情况时,两条数据都会被过滤掉,所以应该在 sortsearch_after 中都加上类似 id 的唯一字段作区分。

2.6 Nested、Join、Object类型的区别

创建映射mapping,两个字段分别为 nested 和 object

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
PUT nested_object_test
{
"mappings": {
"properties": {
"my_object": {
"type": "object",
"properties": {
"username": {
"type": "text"
},
"age": {
"type": "keyword"
}
}
},
"my_nested": {
"type": "nested",
"properties": {
"username": {
"type": "text"
},
"age": {
"type": "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
//采用数组格式,分别放两个字段存入一条数据
POST nested_object_test/_doc/1
{
"my_object": [
{
"username": "Tom smith",
"age": 20
},
{
"username": "William smith",
"age": 30
}
],
"my_nested":[
{
"username": "Tom smith",
"age": 20
},
{
"username": "William smith",
"age": 30
}
]
}

搜索对比

检索同时满足 username = Tom ,age=30 条件的数据,正常情况下是没有结果的,而 object 字段的检索会返回结果,其本质原因是 object 类型在存储时会进行扁平化处理,即上述测试数据在存储时被处理为了:

username: [“Tom smith” , “William smith”], age: [20,30]

使用 Nested 嵌套对象时则不会出现该情况,Nested 数据类型允许对象数组中的对象被独立索引,存储时会分为多条 lucene 文档 (即,上述的测试数据会分为两个隐藏文档 ,每一个都存储着对应的 usernameage ,最后召回时进行 join,所以效率也相对更低)

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
//错误结果
GET nested_object_test/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"my_object.username": "Tom"
}
},
{
"term": {
"my_object.age": 30
}
}
]
}
}
}
//预期结果
GET nested_object_test/_search
{
"query": {
"nested": {
"path": "my_nested",
"query": {
"bool": {
"must": [
{
"match": {
"my_nested.username": "Tom"
}
},
{
"term": {
"my_nested.age": 30
}
}
]
}
}
}
}
}

Join类型 的性能不太好,nestedobject 相比性能会低几倍,joinnested基础上会再低几倍。

对比 Object Nested Join
优点 速度快,性能好 文档存储在一起,性能较好 父子文档独立更新,互不影响
缺点 不支持数组字段的组合检索 更新时需要整个文档一起更新 内存占用多,读取性能差
场景 不需要对 object 数组查询、聚合 查询多,子文档偶尔更新 子文档更新频繁

nested 的查询。聚合都需要使用 nested_query;

Join 需要通过 parent_idhas_parenthas_child 查询,且父子文档需要在同一分片上(使用routing

需要注意的是,has_child 查询筛选的是子文档内容,然后返回父文档,通过 inner_hits 可以同时返回父子文档。has_child 查询的相关性算分,是由命中的 child 影响的。同时排序方式也和常规的 sort 不同,需要通过 score_mode 来影响最终的父文档排序。( inner_hits 中的排序可以直接使用常规 sort

  • none ,默认
  • avg、max、min、sum ;分别表示根据子文档得分的平均值、最大值、最小值、求和来排序

Join 的语法案例:

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
// 创建索引
PUT /my_index
{
"mappings": {
"properties" : {
"my-join-field" : {
"type" : "join",
"relations": {
"parent": "child"
}
}
}
}
}
// 查询
GET /_search
{
"query": {
"has_child" : {
"type" : "child",
"query" : {
"match_all" : {}
},
"inner_hits": {},
"max_children": 10,
"min_children": 2,
"score_mode" : "min"
}
}
}

官方文档: has_child 的排序问题(has_parent也类似)

2.7 search_template 与 render template

search_template 的功能主要是用于工程代码和搜索解耦,维护人员定好 search_template 的基础结构和参数后,程序即可编写,当涉及到搜索调整,在参数没有更改的情况下,只需要调整 template 即可。

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
POST _bulk
{"index":{"_index":"shirts","_id":"1"}}
{"brand":"gucci","color":"red","model":"slim"}
{"index":{"_index":"test","_id":"2"}}
{"brand":"gucci","color":"black","model":"slim"}
{"index":{"_index":"test","_id":"3"}}
{"brand":"gucci","color":"black","model":"ntrms"}
{"index":{"_index":"test","_id":"4"}}
{"brand":"gucci","color":"black","model":"ntrms"}

# 0.直接使用search_template
GET shirts/_search/template
{
"source":{
"query":{
"match":{
"{{my_field}}":"{{my_value}}"
}
}
},
"params": {
"my_field":"color",
"my_value":"black",
"my_size": 1
}
}

DELETE _scripts/shirts_template

#1.script存储 search_template,使用 mustache 模板语法
POST _scripts/shirts_template
{
"script": {
"lang": "mustache",
"source": {
"query":{
"match":{
"{{my_field}}":"{{my_value}}"
}
},
"size":"{{my_size}}"
}
}
}

#2.使用模板(id只能在dsl里声明)
GET _search/template
{
"id":"shirts_template",
"explain": true,
"params": {
"my_size": "1",
"my_field":"color",
"my_value":"black"
}
}
# render template 模板渲染测试
GET _render/template
{
"source": {
"query": {
"match": {
"{{my_field}}": "{{my_value}}"
}
}
},
"params": {
"my_field": "color",
"my_value": "black",
"my_size": 1
}
}
#或者带上模板id渲染
GET _render/template/shirts_template
{
"params": {
"my_field": "color",
"my_value": "black",
"my_size": 1
}
}
# 整体渲染
GET _render/template
{
"source": "{\"query\":{\"bool\":{\"must\": {{#toJson}}clauses{{/toJson}} }}}",
"params": {
"clauses": [
{ "term": { "user" : "foo" } },
{ "term": { "user" : "bar" } }
]
}
}

3. indices API 总结

open/closeshrinksplitrollover 等API 不在考试范围内,暂且跳过。

3.1 索引别名 index aliases

别名概念可以类比数据库的字段别名,起到一个昵称的作用。

  • index aliases 可以指定多个索引,所以也可以通过别名来检索多个 index
  • index 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
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
// add 表示增加别名,remove 表示移除别名,remove_index 则等同于 delete_index
POST /_aliases
{
"actions" : [
{ "add" : { "index" : "test1", "alias" : "alias1" } },
{ "remove" : { "index" : "test1", "alias" : "alias1" } },
{ "remove_index": { "index": "test" } }
]
}
// 针对一部分数据添加别名
POST /_aliases
{
"actions" : [
{
"add" : {
"index" : "test1",
"alias" : "alias2",
"filter" : { "term" : { "user" : "kimchy" } }
}
}
]
}
// routing
POST /_aliases
{
"actions" : [
{
"add" : {
"index" : "test",
"alias" : "alias1",
"routing" : "1"
}
}
]
}
// is_write_index,当一个别名对应了多个索引后,就无法使用别名来进行 index 操作了
// 因为 es 不知道数据应该写到哪个索引下,使用 `is_write_index:true` 则可以指定。
// 常规场景还是推荐通过index_name来进行 index操作
POST /_aliases
{
"actions" : [
{
"add" : {
"index" : "test",
"alias" : "alias1",
"is_write_index" : true
}
},
{
"add" : {
"index" : "test2",
"alias" : "alias1"
}
}
]
}
// 在索引创建时声明别名
PUT /logs_20162801
{
"mappings" : {
"properties" : {
"year" : {"type" : "integer"}
}
},
"aliases" : {
"current_day" : {},
"2016" : {
"filter" : {
"term" : {"year" : 2016 }
}
}
}
}
// 直接删除别名
DELETE /logs_20162801/_alias/current_day
// 通过 put 请求直接添加
PUT /{index}/_alias/{name}
// 查看别名
GET /{index}/_alias

3.2 索引模板 index template

索引模板可以用于数据量非常大,需要进行索引生命周期管理,按日期划分索引的场景,或者想要提取多个索引之间的共同字段

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
// 索引模板样例
PUT _template/template_1
{
"index_patterns": [
"te*",
"bar*"
],
"aliases": {
"alias1": {}
},
"settings": {
"number_of_shards": 1
},
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"host_name": {
"type": "keyword"
},
"created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z yyyy"
}
}
}
}
// 增
PUT _template/template_1
// 删
DELETE /_template/template_1
// 改,执行新增操作,同名称的模板会覆盖原来的模板,新模板只对新创建的索引起效。对已经建好的历史索引不起作用
...
// 查
GET /_template/template_1

// 索引模板的顺序问题,这里创建了两个模板。而b的order顺序更大,那么创建logstash-b-tomcat-1时,生效的是B模板。
PUT _template/b-template
{
"order":1,
"index_patterns":["logstash-b-tomcat-*","logstash-b1-tomcat-*","logstash-b2-server-*","logstash-b3-tomcat-*"],
"settings":{
"number_of_shards":"5",
"number_of_replicas":"1",
"refresh_interval":"30s",
"translog.durability":"async"
}
}

PUT logstash-b-tomcat-1

PUT _template/a-template
{
"order":0,
"index_patterns":["*"],
"settings":{
"number_of_shards":"5",
"number_of_replicas":"0",
"refresh_interval":"30s",
"translog.durability":"async"
}
}

索引模板其实是可以使用变量的!,以下案例中展示了动态添加别名。{index} 会在索引创建时替换为实际的索引名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT _template/template_1
{
"index_patterns" : ["te*"],
"settings" : {
"number_of_shards" : 1
},
"aliases" : {
"alias1" : {},
"alias2" : {
"filter" : {
"term" : {"user" : "kimchy" }
},
"routing" : "kimchy"
},
"{index}-alias" : {}
}
}

3.3 synced flush

sync flushElasticsearch 提供的一种机制,当一个分片在 5 分钟内没有收到任何 indexing 操作时,会被标记为 inactive 非活跃状态,然后启用 synced flush,给每一个分配标记一个唯一的 sync_id,这个 id 在集群恢复(或者重启时)可以快速检测两个分配是否相同,在这种情况下,可以无需复制 segment 段文件,以加快恢复速度。synced_flush说明

对于无需更新,或者很少更新的数据会很有用

1
2
3
4
5
6
7
8
//检查是否有syncid 标记
GET index_name/_stats?filter_path=**.commit&level=shards

//手动执行synced flush,无需等待5分钟
POST _flush/synced

//指定索引名称
POST kimchy,elasticsearch/_flush/synced

手动执行 synced flush 后会返回成功数量、失败数量

3.4 forcemerge 段合并

Elasticsearch 的存储内容进行逐层拆解后是这样的:

index—>多shard—>多lucene—–> 多segment

默认情况下每一个 refresh_interval 产生一个 segment。后台也会有进程自动进行 merge,如下的指令表示强制进行 merge,max_num_segments 指定了合并后的段个数

only_expunge_deletes 参数表示把标记为 delete 的数据进行 merge (可以理解为真正删除,默认会等 60s

1
POST index_name/_forcemerge?max_num_segments=1&flush=true&only_expunge_deletes=true

index.merge.scheduler.max_thread_count: 限制 merge 的并发线程数量,根据 cpu 核数修改。

index.merge.policy.expunge_deletes_allowed: 可以限制段合并的阈值,当删除的比例低于该阈值时,段合并不会生效。

4. tips

  • es 7.x 为了提高性能,在命中数据大于 1000 时,不会返回准确的命中总数,并指出数据总量是 gte 10000的,设置 track_total_hits:true 可以解决该问题,也可以将该参数设置为一个固定的数值,ES 会返回 gte 该数值的说明;

  • 返回 version,可以设置 version:true

  • sort 可以对数组类型进行排序,例如:返回数组的平均值。或最小值;

  • 副本虽然影响写入速度,但是可以增加查询速度和数据安全性,因为查询可以在任一副本上获取到结果;

  • query_then_fetchdfs_query_then_fetch 都是分两阶段,从不同的分片查询 X 条数据,最后汇总取 top X 区别在于 dfs 方式会在第一阶段计算分布式的词频,以得到更准确的相关性得分;

  • explain API 用于查看相关性算分的详情,而 profile API 则是用于查看每个步骤的执行耗时。

  • 默认不需要执行,特殊情况下可以手动执行的 dsl 语句:

    1
    2
    3
    4
    // 1.清除缓存
    POST /twitter/_cache/clear
    // 2. flush
    POST /twitter/_flush

    `,


Alt

0%