1

Elasticseach数据建模

关系型数据库的范式与反范式

范式与反范式

Elasticsearch 处理关联关系

Elasticsearch 并不擅长处理关联关系,有以下四种⽅法处理关联关系:

  • 对象类型,json中的数组,JSON 格式会被处理成扁平式键值对的结构,搜索时会有问题;
  • 嵌套对象(Nested Object),允许对象数组中的对象被独立索引
  • 父子文档(Parent/Child),通过 join 字段来表明文档之间的关系
  • 应用端关联,es不维护关联关系,由调用es的程序来维护关联关系

对象类型

image-20200107113532391.png

DELETE my_movies

# 电影的Mapping信息
PUT my_movies
{
      "mappings" : {
      "properties" : {
        "actors" : {
          "properties" : {
            "first_name" : {
              "type" : "keyword"
            },
            "last_name" : {
              "type" : "keyword"
            }
          }
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
}

# 写入一条电影信息
POST my_movies/_doc/1
{
  "title":"Speed",
  "actors":[
    {
      "first_name":"Keanu",
      "last_name":"Reeves"
    },

    {
      "first_name":"Dennis",
      "last_name":"Hopper"
    }

  ]
}


# 查询电影信息
POST my_movies/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "actors.first_name": "Keanu"
          }
        },
        {
          "match": {
            "actors.last_name": "Hopper"
          }
        }
      ]
    }
  }
}

为什么会搜到不到需要的结果?

  • 存储时,内部对象的边界并没有考虑在内,JSON 格式被处理理成扁平式键值对的结构
  • 当对多个字段进行查询时,导致了意外的搜索结果
  • 可以用 Nested Data Type 解决这个问题
"title":"Speed" 
"actors.first_name":["Keanu","Dennis"] 
"actors.last_name":["Reeves","Hopper"]

嵌套对象(Nested Object)

当对象包含了了多值对象时,可以使⽤用嵌套对象(Nested Object)解决查询正确性的问题。将一个字段的类型设置为 "type": "nested",它就是一个 Nested Object 。

image-20200107114305417.png

Nested 数据类型:

  • 允许对象数组中的对象被独立索引
  • 使⽤用 nested 和 properties 关键字,将所有 actors 索引到多个分隔的文档
  • 在内部, Nested ⽂档会被保存在两个 Lucene ⽂档中,在查询时做 Join 处理

插入一组数据

DELETE my_movies

# 创建 Nested 对象 Mapping
PUT my_movies
{
      "mappings" : {
      "properties" : {
        "actors" : {
          "type": "nested",
          "properties" : {
            "first_name" : {"type" : "keyword"},
            "last_name" : {"type" : "keyword"}
          }},
        "title" : {
          "type" : "text",
          "fields" : {"keyword":{"type":"keyword","ignore_above":256}}
        }
      }
    }
}


POST my_movies/_doc/1
{
  "title":"Speed",
  "actors":[
    {
      "first_name":"Keanu",
      "last_name":"Reeves"
    },

    {
      "first_name":"Dennis",
      "last_name":"Hopper"
    }
  ]
}

image-20200107114718111.png

在内部, Nested ⽂文档会被保存在两个 Lucene ⽂文档中,会在查询时做 Join 处理。

# Nested 查询
POST my_movies/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Speed"}},
        {
          "nested": {
            "path": "actors",
            "query": {
              "bool": {
                "must": [
                  {"match": {
                    "actors.first_name": "Keanu"
                  }},

                  {"match": {
                    "actors.last_name": "Hopper"
                  }}
                ]
              }
            }
          }
        }
      ]
    }
  }
}


# Nested Aggregation
POST my_movies/_search
{
  "size": 0,
  "aggs": {
    "actors": {
      "nested": {
        "path": "actors"
      },
      "aggs": {
        "actor_name": {
          "terms": {
            "field": "actors.first_name",
            "size": 10
          }
        }
      }
    }
  }
}


# 普通 aggregation不工作
POST my_movies/_search
{
  "size": 0,
  "aggs": {
    "NAME": {
      "terms": {
        "field": "actors.first_name",
        "size": 10
      }
    }
  }
}

父子关联关系(Parent/Child)

对象和 Nested 对象的局限性:每次更更新,需要重新索引整个对象(包括根对象和嵌套对象)

ES 提供了了类似关系型数据库中 Join 的实现。使⽤用 Join 数据类型实现,可以通过维护 Parent / Child 的关系,从⽽而分离两个对象。⽗父⽂文档和⼦子⽂文档是两个独⽴立的⽂文档。更更新⽗父⽂文档⽆无需重新索引⼦子⽂文档。⼦子⽂文档被添加,更更新或者删除也不不会影响到⽗父⽂文档和其他的⼦子⽂文档

如何定义父子父子关系

  1. 设置索引的 Mapping
  2. 索引⽗父⽂文档
  3. 索引⼦子⽂文档
  4. 按需查询⽂文档

设置 Mapping

image-20200107115455156.png

# 设定 Parent/Child Mapping
PUT my_blogs
{
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "properties": {
      "blog_comments_relation": {
        "type": "join",
        "relations": {
          "blog": "comment"
        }
      },
      "content": {
        "type": "text"
      },
      "title": {
        "type": "keyword"
      }
    }
  }
}

索引父文档

image-20200107115722551.png

#索引父文档
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"
  }
}

索引子文档

image-20200107135531235.png

#索引子文档
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"
  }
}

Parent / Child 所⽀支持的查询

查询所有文档
POST my_blogs/_search
{

}
Parent Id 查询

image-20200107142040289.png

#根据父文档ID查看
GET my_blogs/_doc/blog2

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

image-20200107141908894.png

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

image-20200107142005926.png

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

image-20200107142323241.png

# 通过ID ,访问子文档
GET my_blogs/_doc/comment3

#通过ID和routing ,访问子文档
# 需指定⽗父⽂文档 routing 参数
GET my_blogs/_doc/comment3?routing=blog2

更新子文档,更新⼦文档不会影响到⽗文档。

PUT my_blogs/_doc/comment3?routing=blog2
{
    "comment": "Hello Hadoop??",
    "blog_comments_relation": {
      "name": "comment",
      "parent": "blog2"
    }
}

Nested Object VS Parent/Child

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

应用端关联

在es中用两个索引进行存储。程序查询两次:

  • 第一次查获获得结果 A
  • 第二次用结果 A 进行查询,获得结果 B

在把结果 A 和 B 组合返回给调用者。

适用于数据量少的业务场景。数据量大时,两次查询会耗费更多时间。

数据建模SOP

如何设置字段类型

image-20200107144330601.png

字段类型

  1. 字符串类型:需要分词则设置为 text 类型,否则设置为 keyword 类型;

    • Text

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

      • 用于 id,枚举及不不需要分词的⽂文本。eg.电话号码,email地址,性别等
      • 适用于 Filter(精确匹配),Sorting 和 Aggregations
    • 设置多字段类型

      • 默认会为文本类型设置成 text,并且设置一个 keyword 的子字段
      • 在处理理人类语⾔言时,通过增加“英文”,“拼音”和“标准”分词器,提高搜索结构
  2. 数值类型:尽量选择贴近的类型,例如可以用 byte,就不要用 long
  3. 枚举类型:基于性能考虑将其设置为 keyword 类型,即便该数据是数字;
  4. 其他类型:比如布尔类型、日期、地理位置数据等;

是否要搜索及分词

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

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

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

    • index_options 结合需要设定
    • norms 不需要归一化数据时,可以关闭

是否要聚合及排序

  • 如不需要检索,排序和聚合分析

    • enabled 设置为 false
  • 如不需要排序或者聚合分析功能

    • Doc_values / fielddata 设置成 false
  • 更新频繁,聚合查询频繁的 keyword 类型的字段

    • 推荐将 eager_global_ordinals 设置为 true

是否要额外存储

  • 是否需要专⻔门存储当前字段数据

    • Store 设置成 true,可以存储该字段的原始内容
    • 一般结合 _source 的 enabled 为 false 时候使⽤用
  • Disable _source:节约磁盘;适⽤用于指标型数据

    • 一般建议先考虑增加压缩比
    • 无法看到 _source字段,无法做 ReIndex,无法做 Update
    • Kibana 中无法做 discovery

Mapping 字段的相关设置

https://www.elastic.co/guide/...

名称 备注
Enabled 设置成 false,仅做存储,不不⽀支持搜索和聚合分析 (数据保存在 _source 中)
Index 是否构倒排索引。设置成 false,⽆无法被搜索,但还是⽀支持 aggregation,并出现在 _source 中
Norms 如果字段用来过滤和聚合分析,可以关闭,节约存储
Doc_values 是否启用 doc_values,用于排序和聚合分析
Field_data 如果要对 text 类型启⽤用排序和聚合分析, fielddata 需要设置成true
Store 默认不不存储,数据默认存储在 _source。
Coerce 默认开启,是否开启数据类型的⾃自动转换(例例如,字符串串转数字)
Multifields 多字段特性
Dynamic true / false / strict 控制 Mapping 的自动更更新

数据建模最佳实践

如何处理关联关系

如何处理关联关系

  • Kibana 目前暂不支持 nested 类型和 parent/child 类型 ,在未来有可能会支持
  • 如果需要使用 Kibana 进行数据分析,在数据建模时仍需对嵌套和⽗子关联类型作出取舍

避免过多字段

  1. 一个文档中,避免⼤量的字段

    1. 过多的字段数不不容易易维护
    2. Mapping 信息保存在 Cluster State 中,数据量量过⼤大,对集群性能会有影响
    3. 删除或者修改数据需要 reindex
  2. 默认大字段数是 1000,可以设置 index.mapping.total_fields.limt 限定大字段数。

Dynamic v.s Strict

Dynamic(生产环境中,尽量不要打开 Dynamic)

  • true - 未知字段会被自动加⼊
  • false - 新字段不会被索引。但是会保存在 _source
  • strict - 新增字段不不会被索引,⽂文档写⼊入失败

Strict

  • 可以控制到字段级别

避免正则查询

问题:

  • 正则,通配符查询,前查询属于 Term 查询,但是性能不不够好
  • 特别是将通配符放在开头,会导致性能的灾难

案例:

  • 文档中某个字段包含了 Elasticsearch 的版本信息,例如 version: “7.1.0”
  • 搜索所有是 bug fix 的版本?每个主要版本号所关联的文档?

解决⽅案:将字符串转换为对象

image.png

搜索过滤

image.png

避免空值引起的聚合不准

image.png

使用 Null_Value 解决空值的问题

image.png

为索引的 Mapping 加⼊ Meta 信息

image.png

相关文档


深页
105 声望3 粉丝