Elasticsearch 数据建模

数据建模

  • 英文为Data Modeling ,为创建数据模型的过程
  • 数据模型( Data Model )

对现实世界进行抽象描述的一种工具和方法
通过抽象的实体及实体之间联系的形式去描述业务规则,从而实现对现实世界的映射

数据建模的过程

概念模型

  • 确定系统的核心需求和范围边界,设计实体和实体间的关系
    逻辑模型
  • 进一步梳理业务需求,确定每个实体的属性、关系和约束等
    物理模型
  • 结合具体的数据库产品,在满足业务读写性能等需求的前提下确定最终的定义
  • mysql,MongoDB,elasticsearch等
  • 第三范式

ES数据建模配置

ES是基于Lucene以倒排索引为基础实现的存储体系,不遵循关系型数据库中的范式约定

Mapping字段的相关设置

enabled

  • true | false
  • 仅存储,不做搜索或聚合分析

index

  • true | false
  • 是否构建倒排索引

index_options

  • docs | freqs I positions | offsets
  • 存储倒排索引的哪些信息

norms

  • true | false
  • 是否存储归一化相关参数,如果字段仅用于过滤和聚合分析,可关闭

doc values

  • true | false
  • 是否启用doc values ,用于排序和聚合分析

field data

  • false I true
  • 是否为text类型启用fielddata ,实现排序和聚合分析

store

  • false | true
  • 是否存储该字段值

coerce

  • true l false
  • 是否开启自动数据类型转换功能,比如字符串转为数字、浮点转为整型等

multifields多字段

  • 灵活使用多字段特性来解决多样的业务需求

dynamic

  • true | false | strict
  • 控制mapping自动更新

date_detection

  • true | false
  • 是否自动识别日期类型

Mapping字段属性的设定流程

  1. 是何种类型
  2. 是否需要检索
  3. 是否需要另行存储
  4. 是否需要排序和聚合分析
是何种类型?

字符串类型

  • 需要分词则设定为text类型,否则设置为keyword类型

枚举类型

  • 基于性能考虑将其设定为keyword类型,即便该数据为整型

数值类型

  • 尽量选择贴近的类型,比如byte即可表示所有数值时,即选用byte ,不要用long

其他类型

  • 比如布尔类型、日期、地理位置数据等
是否需要检索

完全不需要检索、排序、聚合分析的字段

  • enabled设置为false

不需要检索的字段

  • index设置为false

需要检索的字段,可以通过如下配置设定需要的存储粒度

  • index-options结合需要设定
  • norms不需要归一化数据时关闭即可
是否需要排序和聚合分析

不需要排序或者聚合分析功能

  • doc values设定为false
  • fielddata设定为false
是否需要另行存储?

是否需要专门存储当前字段的数据?

  • store设定为true ,即可存储该字段的原始内容(与_source中的不相关)
  • 一般结合_source的enabled设定为false时使用

数据建模示例

博客文章 blog_index

  • 标题title
  • 发布日期publish-date
  • 作者author
  • 摘要abstract
  • 网络地址url
  • 内容content

blog_index的mapping设置如下:

request
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 blog_index
{
"mappings": {
"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
},
"content": {
"type": "text"
}
}
}
}

content内容非常大的时候,_source每次返回会很耗费es性能

blog_index的mapping设置如下(都加了store:true专门存储每个字段原始值;不存入_source中了):

request
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
PUT blog_index
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 100
}
},
"store": true
},
"publish_date": {
"type": "date",
"store": true
},
"author": {
"type": "keyword",
"ignore_above": 100,
"store": true
},
"abstract": {
"type": "text",
"store": true
},
"content": {
"type": "text",
"store": true
},
"url": {
"type": "keyword",
"doc_values": false,
"norms": false,
"ignore_above": 100,
"store": true
}
}
}
}

新增一条数据:

request
1
2
3
4
5
PUT blog_index/_doc/1
{
"title":"Blog Number One",
"author":"alfred"
}

request
1
GET blog_index/_search

此时查询不返回_source,如果想查看文档内容:

request
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": {
"title": "blog"
}
},
"highlight": {
"fields":{
"title": {}
}
}
}
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
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "blog_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"fields" : {
"author" : [
"alfred"
],
"title" : [
"Blog Number One"
]
},
"highlight" : {
"title" : [
"<em>Blog</em> Number One"
]
}
}
]
}
}

Nested_Object

关联关系处理

ES不擅长处理关系型数据库中的关联关系,比如文章表blog与评论表comment之间通过blogid关联,在ES中可以通过如下两种手段变相解决

  • Nested Object
    0 Parent/Child

评论Comment

  • 文章Id blog-id
  • 评论人username
  • 评论日期date
  • 评论内容content

插入两条数据

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

查询username包含lee并且content包含thanks

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET blog_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"comments.username": "lee"
}
},
{
"match": {
"comments.content": "thanks"
}
}
]
}
}
}

数据返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"_index" : "blog_index",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.5753642,
"_source" : {
"title" : "Blog Number One",
"author" : "alfred",
"comments" : [
{
"username" : "lee",
"date" : "2017-01-02",
"content" : "awesome article!"
},
{
"username" : "fax",
"date" : "2017-04-02",
"content" : "thanks!"
}
]
}
}

不符合想要的结果

重新设置mapping

request
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
PUT blog_index_nested
{
"mappings": {
"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"
}
}
}
}
}
}

数据插入:

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

request
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数据类型实现

mapping:

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
DELETE blog_index_parent_child
PUT blog_index_parent_child
{
"mappings": {
"properties": {
"join": {
"type": "join",
"relations": {
"blog": "comment" # blog为父类型,comment为子类型
}
}
}
}
}

关联关系处理之Parent/Child

创建父类型文档

request
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"
}

创建子类型文档:

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


PUT blog_index_parent_child/_doc/comment-2?routing=2
{
"comment":"comment hello",
"join":{
"name":"comment", #指明子类型
"parent":2 #父文档id
}
}

routing值的作用是为了确保父子文档在一个shard上,一般值为父文档id

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

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

parent_id返回id=2的父文档的子文档

request
1
2
3
4
5
6
7
8
9
GET blog_index_parent_child/_search
{
"query":{
"parent_id":{
"type":"comment",
"id":"2"
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "blog_index_parent_child",
"_type" : "_doc",
"_id" : "comment-2",
"_score" : 0.87546873,
"_routing" : "2",
"_source" : {
"comment" : "comment hello",
"join" : {
"name" : "comment",
"parent" : 2
}
}
}

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

request
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
GET blog_index_parent_child/_search
{
"query":{
"has_child": {
"type": "comment",
"query": {
"match": {
"comment": "world"
}
}
}
}
}

GET blog_index_parent_child/_search
{
"query":{
"has_child": {
"type": "comment",
"query": {
"match": {
"_id": "comment-1"
}
}
}
}
}

关键词has_child,type为子文档名称,match里面只能接一个查询条件

返回结果:

1
2
3
4
5
6
7
8
9
10
{
"_index" : "blog_index_parent_child",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "blog",
"join" : "blog"
}
}

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

request
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
GET blog_index_parent_child/_search
{
"query":{
"has_parent": {
"parent_type": "blog",
"query": {
"match": {
"title": "blog"
}
}
}
}
}

GET blog_index_parent_child/_search
{
"query":{
"has_parent": {
"parent_type": "blog",
"query": {
"match": {
"_id": "1"
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "blog_index_parent_child",
"_type" : "_doc",
"_id" : "comment-1",
"_score" : 1.0,
"_routing" : "1",
"_source" : {
"comment" : "comment world",
"join" : {
"name" : "comment",
"parent" : 1
}
}
}

nested vs parent_child

对吧 nested object parent_child
优点 文档存储在一起,读取性能更高 父子文档独立更新,互不影响
缺点 更新父文档或子文档时需要更新整个文档 为了维护join的关系,读取时需要占用部分内存,性能较差
场景 读多写少 写少读多

reindex

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

  • mapping设置变更,比如字段类型变化、分词器字典更新等
  • index设置变更,比如分片数更改等
  • 迁移数据

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

  • update-by.query在现有索引上重建
  • _reindex在其他索引上重建

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html

request
1
POST blog_index/_update_by_query?conflicts=proceed

索引重建,如果遇到版本冲突覆盖并继续

Reindex-_reindex

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

其他建议

防止字段过多
字段过多主要有如下的坏处:

  • 难于维护,当字段成百上千时,基本很难有人能明确知道每个字段的含义
  • mapping的信息存储在cluster state里面,过多的字段会导致mapping过大,最终导致更新变慢
    通过设置index.mapping.total_fields.limit可以限定索引中最大字段数,默认是1000
    可以通过key/value的方式解决字段过多的问题,但并不完美
request
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
PUT demo_common
{
"mappings": {
"doc":{
"properties": {
"url":{
"type": "keyword"
},
"@timestamp":{
"type": "date"
},
"cookies":{
"properties": {
"username":{
"type":"keyword"
},
"startTime":{
"type":"date"
},
"age":{
"type":"integer"
}
}
}
}
}
}
}

上面的设置可以转换为:

request
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 demo_key_value
{
"mappings": {
"doc": {
"properties": {
"url": {
"type": "keyword"
},
"@timestamp": {
"type": "date"
},
"cookies": {
"type": "nested",
"properties": {
"cookieName":{
"type":"keyword"
},
"cookieValueKeyword":{
"type":"keyword"
},
"cookieValueInteger":{
"type":"integer"
},
"cookieValueDate":{
"type":"date"
}
}
}
}
}
}
}

虽然通过这种方式可以极大地减少Field数目,但也有一些明显的坏处

  • query语句复杂度飙升,且有一些可能无法实现,比如聚合分析相关的
  • 不利于在Kibana中做可视化分析

查询对比

request
1
2
3
4
5
6
7
8
9
10
11
GET demo_common/_search
{
"query":{
"range": {
"cookies.age": {
"gte": 10,
"lte": 20
}
}
}
}

request
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
GET demo_key_value/_search
{
"query": {
"nested": {
"path": "cookies",
"query": {
"bool": {
"filter": [
{
"term": {
"cookies.cookieName": "age"
}
},
{
"range": {
"cookies.cookieValueInteger": {
"gte": 15,
"lte": 20
}
}
}
]
}
}
}
}
}
-------------本文结束感谢您的阅读-------------