Elasticsearch 数据建模

数据建模是创建数据模型的过程,通过抽象的实体及实体之间的联系的形式去描述业务规则,从而实现对现实世界的映射。

mapping 字段的相关设置

enable 设置为false时,仅存储,不做搜索或聚合分析
index 是否构建倒排索引
index_options 存储倒排索引的哪些信息(docs/freqs/positions/offsets)
norms 是否存储归一化相关参数,如果字段仅用于过滤和聚合分析,可关闭
doc_values 是否启用doc_values,用于排序和聚合分析
field_data 是否为text类型启用field data,实现排序和聚合分析
store 是否存储该字段值
coerse 是否开启自动数据类型转换功能,比如字符串转为数字、浮点型转为整型
multifields 灵活使用多字段特性来解决多样的业务需求
dynamic 控制mapping自动更新
date_detection 是否自动识别日期类型

mapping 字段属性的设定流程

是何种类型? -> 是否需要检索? -> 是否需要排序和聚合分析? -> 是否需要另行存储?

是何种类型

字符串类型 需要分词则设定为text类型,否则设置为keyword类型
枚举类型 基于性能考虑将其设定为keyword类型,即便该数据为整形
数值类型 尽量选择贴近的类型,比如byte即可表示所有数值时,即选用byte,不要用long
其他类型 比如布尔类型、日期、地理位置数据等

是否需要检索

  • 完全不需要检索、排序、聚合分析,enable设置为false

  • 不需要检索的字段,index设置为false

  • 需要检索的字段,可以通过index_options、norms来设置的存储粒度

是否需要排序和聚合分析

不需要排序或者聚合分析功能,设置doc_values、fielddata为false。

是否需要另行存储

是否需要专门存储当前字段的数据,store设定为true,即可存储该字段的原始内容(与字段存储在_source中不相关),一般结合_source的enableed设定为false时使用。

实例

创建一个blog文章的index,如下的字段:标题 title、发布日期 publish_date、作者 author、摘要 abstract、内容 content、url

如果把content存储在_source,content的内容可能非常大,所以每次检索文档时,都会返回content的内容,这会导致es的性能下降。这时设置mapping的时候可以把_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
PUT /blog_index
{
"mappings": {
"doc": {
"_source": {
"enabled": false
},
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"store": true
},
"publish_date": {
"type": "date",
"store": true
},
"author": {
"type": "keyword",
"store": true
},
"abstract": {
"type": "text",
"store": true
},
"content": {
"type": "text",
"store": true
},
"url": {
"type": "keyword",
"doc_values": false,
"norms": false,
"store": true
}
}
}
}
}

如下是查询content中包含word的文档,通过stored_fields设定需要返回的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /blog_index/_search
{
"stored_fields": ["title", "publish_date", "author", "abstract", "url"],
"query": {
"match": {
"content": "word"
}
},
"highlight": {
"fields": {
"content": {}
}
}
}

关联关系处理

es不擅长处理关系型数据库中的关联关系,比如blog文章和评论comment,在关系型数据库中可以通过blog_id关联。在es中可以通过如下两种手段变相解决:

  • nested object

  • parent/child

nested object

如下把comments的type设置为nested:

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
PUT /blog_index_nested
{
"mappings": {
"doc":{
"properties": {
"title":{
"type": "text",
"fields": {
"keyword":{
"type":"keyword",
"ignore_above": 100
}
}
},
"publish_date":{
"type":"date"
},
"author":{
"type":"keyword",
"ignore_above": 100
},
"abstract":{
"type": "text"
},
"url":{
"enabled":false
},
"comments":{
"type":"nested",
"properties": {
"username":{
"type":"keyword",
"ignore_above":100
},
"date":{
"type":"date"
},
"content":{
"type":"text"
}
}
}
}
}
}
}

创建如下文档,在文章中包含两条comment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /blog_index_nested/doc/1
{
"title": "blog number one",
"author": "alfred",
"comments": [
{
"username": "lee",
"date": "2017-01-02",
"content": "awesome article!"
},
{
"username": "fax",
"date": "2017-04-02",
"content": "thanks!"
}
]
}

如下查询的是comment中username包含lee和content包含awesome的文档:

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 /blog_index_nested/_search
{
"query": {
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.username": "lee"
}
},
{
"match": {
"comments.content": "awesome"
}
}
]
}
}
}
}
}

parent/child

es还提供了类似关系型数据库中join的实现方式,使用join(6.x之后的版本才支持)数据类型实现。

如下创建一个blog_index_parent_child文档,指明父文档为blog,子文档为comment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /blog_index_parent_child
{
"mappings": {
"doc": {
"properties": {
"join": {
"type": "join",
"relations": {
"blog": "comment"
}
}
}
}
}
}

如下用join指明创建的两个blog为父文档:

1
2
3
4
5
6
7
8
9
10
11
PUT /blog_index_parent_child/doc/1
{
"title": "blog",
"join": "blog"
}

PUT /blog_index_parent_child/doc/2
{
"title": "blog2",
"join": "blog"
}

如下创建两个子文档comment,用parent的值指明父文档的id,指明routing值确保父子文档在一个分片上,一般使用父文档id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /blog_index_parent_child/doc/comment-1?routing=1
{
"comment": "comment1",
"join": {
"name": "comment",
"parent": 1
}
}

PUT /blog_index_parent_child/doc/comment-2?routing=2
{
"comment": "comment2",
"join": {
"name": "comment",
"parent": 2
}
}
查询

parent/child 常见query语法包括如下几种:

  • parent_id: 返回某父文档的子文档

  • has_child: 返回包含某子文档的父文档

  • has_parent: 返回包含某父文档的子文档

parent_id

返回某父文档的子文档。

如下是查询的id为2的父文档中的comment的子文档:

1
2
3
4
5
6
7
8
9
GET /blog_index_parent_child/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": 2
}
}
}
has_child

返回包含某子文档的父文档。

如下是查询子文档comment中包含comment1的父文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /blog_index_parent_child/_search
{
"query": {
"has_child": {
"type": "comment",
"query": {
"match": {
"comment": "comment1"
}
}
}
}
}
has_parent

返回包含某父文档的子文档。

如下是查询父文档blog中title包含blog的子文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /blog_index_parent_child/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query": {
"match": {
"title": "blog"
}
}
}
}
}

nested object 与 parent/child 对比

对比 nested object parent/child
优点 文档存储在一起,因此读取性能高 父子文档可以独立更新,互不影响
缺点 更新父或子文档时需要更新整个文档 为了维护join的关系,需要占用部分内存,读取性能较差
场景 子文档偶尔更新,查询频繁 子文档更新频繁

建议尽量选择nested object来解决问题

reindex

reindex指重建所有数据的过程,一般发生在如下情况:

  • mapping设置变更,比如字段类型变化、分词器字段更新等

  • index设置变更,比如分片数更改等

  • 迁移数据

es提供了现成的API用于完成该工作:

  • _update_by_query在现有索引上重建

  • _reindex在其他索引上重建

_update_by_query

如下是对blog_index执行reindex,conflicts=proceed表示如果版本冲突时,覆盖并执行:

POST /blog_index/_update_by_query?conflicts=proceed

_reindex

将source的数据重建到dest中。

如下是将blog_index重建到blog_new_index:

1
2
3
4
5
6
7
8
9
POST /_reindex
{
"source": {
"index": "blog_index"
},
"dest": {
"index": "blog_new_index"
}
}

task

数据重建的时间受源索引文档规模的影响,当规模越大时,所需时间多,此时可以通过设定url参数wait_for_completion为false来异步执行,es已task来描述执行任务。

es提供了task API来查看任务的执行进度和相关数据

POST /blog_index/_update_by_query?conflicts=proceed&wait_for_completion=false

服务器将会返回对应的task的id:

1
2
3
{
"task" : "me8cxrESTm-MEu4_rWmwbA:7643"
}

通过服务器返回对应的task的id,可以查看task的详情:

GET /_tasks/me8cxrESTm-MEu4_rWmwbA:7375