ES5_1数据建模

文档关联

范式化

关系数据库一般会考虑Normalize数据,而在Elasticsearch中,往往考虑Denormalize。

  • 查询性能好
  • 无需表连接
  • 无需行锁

关联

ES不擅长处理关联关系,一般采用以下4种方法处理关联:

  • 对象类型
  • 嵌套对象(Nested Object)
  • 父子关联关系(Parent / Child)
  • 应用端关联
Nested Object Parent / Child
优点 文档存储在一起,读取性能高 父子文档可以独立更新
缺点 更新嵌套的子文档时,需要更新整个文档 需要额外的内存维护关系,读取性能相对较差
适用场景 子文档偶尔更新,以查询为主 子文档更新频繁

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
# 插入两条数据
PUT blog/doc/1
{
"content": "i love you",
"time": "2021-12-12T12:12:12",
"user": {
"userId": 1,
"userName": "Jack",
"city": "shanghai"
}
}
PUT blog/doc/2
{
"content": "i dont love you",
"time": "2021-12-12T12:12:12",
"user": [{
"userId": 2,
"userName": "Jack Mike",
"city": "hebei"
},
{
"userId": 3,
"userName": "Joe",
"city": "beijing"
}]
}

// 搜索
POST blog/_search
{
"query": {
"bool": {
"must": [
{"match": {"content": "you"}},
{"match": {"user.userName": "Jack"}},
{"match": {"user.city": "beijing"}}
]
}
}
}

上面的搜索初看没什么问题,但是实际上查出了我们不需要的数据。

  • **Jack Mike来自hebei,Joe来自beijing,并不符合查询条件的“来自beijing的Jack”。

出现这个问题的原因是:

  • ES存储时,Nested对象的边界没有被考虑在内,JSON格式被处理成了扁平式的键值对结构

Nested Data Type可以解决这个问题:

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
// 先指定user是nested域
PUT blog
{
"mappings": {
"doc": {
"properties": {
"content": {
"type": "text"
},
"time": {
"type": "date"
},
"user": {
"type": "nested",
"properties": {
"userId": {
"type": "long"
},
"userName": {
"type": "text"
}
}
}
}
}
}
}
// 查询
POST blog/_search
{
"query": {
"bool": {
"must": [
{"match": {"content": "you"}},
{
"nested": { // 指定对嵌套对象的查询
"path": "user", // 指定路径,就是嵌套对象是哪个域
"query": {
"bool": {
"must": [
{"match": {"user.userName": "Jack"}},
{"match": {"user.city": "beijing"}}
]
}
}
}
}
]
}
}
}

像这么定义索引的话相同搜索条件就查不出来了,当然查询条件的city改成”hebei”的话就能重新查出来了。

  • 其中,Nested数据结构,允许对象数组中的对象被独立索引
  • 用nested和properties关键字将所有actors索引到了多个分隔的文档
  • 在内部,Nexted文档会被存到两个Lucene对象中,

Parent / Child

Nested方式关联的局限性:

  • 每次更新,需要重新索引整个对象(包括根对象和嵌套对象)

ES中Parent / Child的关系是类似关系数据库中的Join查询。

  • 父文档和子文档是两个独立的文档;
  • 更新父文档无需重新索引子文档,子文档被添加、更新或删除也不会影响到父文档和其他的子文档。

创建索引,设置mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PUT my_blogs
{
"mappings": {
"doc": {
"properties": {
"blog_comments_relation": {
"type": "join", // 声明join类型
"relations": { // 声明parent和child关系
"blog": "comment" // parent名称和child名称
}
}
"content": {
"type": "text"
},
"title": {
"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
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
#索引父文档
PUT my_blogs/_doc/blog1
{
"title":"Learning Elasticsearch",
"content":"learning ELK @ geektime",
"blog_comments_relation":{
"name":"blog"
}
}

#索引父文档
PUT my_blogs/_doc/blog2
{
"title":"Learning Hadoop",
"content":"learning Hadoop",
"blog_comments_relation":{
"name":"blog"
}
}

#索引子文档
PUT my_blogs/_doc/comment1?routing=blog1
{
"comment":"I am learning ELK",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog1"
}
}

#索引子文档
PUT my_blogs/_doc/comment2?routing=blog2
{
"comment":"I like Hadoop!!!!!",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}

#索引子文档
PUT my_blogs/_doc/comment3?routing=blog2
{
"comment":"Hello Hadoop",
"username":"Bob",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
  • 索引子文档时设置routing=父文档,保证父文档和子文档在一个分片上
  • 同时在parent中设置其父文档的

根据需要查询:

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
#根据父文档ID查看
GET my_blogs/_doc/blog2

# Parent Id 查询
POST my_blogs/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": "blog2"
}
}
}

# Has Child 查询,返回父文档
POST my_blogs/_search
{
"query": {
"has_child": {
"type": "comment",
"query" : {
"match": {
"username" : "Jack"
}
}
}
}
}

# Has Parent 查询,返回相关的子文档
POST my_blogs/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query" : {
"match": {
"title" : "Learning Hadoop"
}
}
}
}
}

#通过ID ,访问子文档,不会返回_source
GET my_blogs/_doc/comment3
#通过ID和routing ,访问子文档,能返回_source
GET my_blogs/_doc/comment3?routing=blog2

#更新子文档
PUT my_blogs/_doc/comment3?routing=blog2
{
"comment": "Hello Hadoop??",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}

查询子文档:

  • has_parent

根据子文档查父文档:

  • parent_id
  • has_child

数据建模

过程:

  1. 概念模型
  2. 逻辑模型
    实体属性
    实体之间的关系
    搜索相关的配置
  3. 数据模型
    索引分片数
    mapping字段配置、关系处理

字段建模

字段类型

text:

  • 用于全文本字段,文本会被Analyzer分词
  • 默认不支持聚合分析及排序,需要设置fielddata为true

keyword:

  • 用于id、枚举及不需要分词的文本
  • 适用于Filter(精确匹配)、sorting、aggregation

设置多字段类型:

  • 默认会为文本类型设置成text,并且设置一个keyword的子字段
  • 在处理人类语言时,通过增加“英文”、”拼音”、”标准”分词器,提高搜索体验

结构化数据:

  • 精确数据类型,可以用byte的情况下就不要用long
  • 枚举类型设置为keyword,即便是数字也应该设置成keyword,获取更好的性能

搜索和分词

  • 如果不需要搜索、排序和聚合分析,则设置enable为false
    不需要检索的话,将index设置为false
  • 对需要搜索的字段,设置存储粒度
    index_options / norms
    不需要归一化数据时,也可以关闭,节约磁盘存储

聚合及排序

  • 如果不需要聚合或排序
    设置Doc_values / fielddata为false
  • 对更新频繁、聚合查询频繁的keyword类型的字段
    设置eager_global_ordinals为true,利用缓存提高聚合性能

额外存储

_source设置enabled为false可以节约磁盘空间
但是一般不会把_source关掉,而是优先考虑增加压缩比,因为关掉后无法再看到_source字段,且无法做Reindex和Update

数据建模最佳实践

关联关系

  1. 优先考虑Denormalization
  2. 当数据包含多数值对象,同时有查询需求时,使用Nested Object
  3. 关联文档更新非常频繁时,使用Parent / Child

避免过多字段

过多字段带来的问题:

  1. 不容易维护
  2. Mapping信息保存在Cluster State中,数据量过大的话会对集群性能造成影响(Cluster State信息需要和所有节点同步)
  3. 删除或修改数据需要reindex

默认最大字段数是1000,可以设置index.mapping.total_fields.limit来修改

避免正则查询

正则查询存在的问题:

  • 正则、通配符查询、前缀查询属于Term查询,但是性能不够好
  • 特别是将通配符放在开头的话,性能极差

避免空值引起的聚合不准

比如下面插入两条文档,一条文档的rating值为null:

1
2
3
4
5
6
7
8
PUT ratings/doc/1
{
"rating":5
}
PUT ratings/doc/2
{
"rating":null
}

聚合分析结果中可以看到,total虽然是2,但是avg结果却是5:

1
2
3
4
5
6
7
8
9
10
11
POST ratings/_search
{
"size": 0,
"aggs": {
"avg": {
"avg": {
"field": "rating"
}
}
}
}

解决办法是给null取默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DELETE ratings
PUT ratings
{
"mappings": {
"doc": {
"properties": {
"rating": {
"type": "long",
"null_value": 1.0
}
}
}
}
}

为索引的Mapping加入Meta信息

1
2
3
4
5
6
7
8
PUT softwares
{
"mappings": {
"_meta": {
"software_version_mapping": "1.0"
}
}
}

Mapping的设置是一个迭代的过程:

  • 加入新的字段很容易(必要时需要update_by_query)
  • 更新删除字段不允许(需要Reindex重建数据)
  • 最好能对Mapping加入Meta信息,更好地进行版本管理
  • 可以考虑将Mapping文件上传git进行管理