关系型数据库的范式与反范式
Elasticsearch 处理关联关系
Elasticsearch 并不擅长处理关联关系,有以下四种⽅法处理关联关系:
- 对象类型,json中的数组,JSON 格式会被处理成扁平式键值对的结构,搜索时会有问题;
- 嵌套对象(Nested Object),允许对象数组中的对象被独立索引
- 父子文档(Parent/Child),通过 join 字段来表明文档之间的关系
- 应用端关联,es不维护关联关系,由调用es的程序来维护关联关系
对象类型
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 。
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"
}
]
}
在内部, 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 的关系,从⽽而分离两个对象。⽗父⽂文档和⼦子⽂文档是两个独⽴立的⽂文档。更更新⽗父⽂文档⽆无需重新索引⼦子⽂文档。⼦子⽂文档被添加,更更新或者删除也不不会影响到⽗父⽂文档和其他的⼦子⽂文档
如何定义父子父子关系
- 设置索引的 Mapping
- 索引⽗父⽂文档
- 索引⼦子⽂文档
- 按需查询⽂文档
设置 Mapping
# 设定 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"
}
}
}
}
索引父文档
#索引父文档
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/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 查询
#根据父文档ID查看
GET my_blogs/_doc/blog2
# Parent Id 查询
POST my_blogs/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": "blog2"
}
}
}
Has Child 查询
# Has Child 查询,返回父文档
POST my_blogs/_search
{
"query": {
"has_child": {
"type": "comment",
"query" : {
"match": {
"username" : "Jack"
}
}
}
}
}
Has Parent 查询
# Has Parent 查询,返回相关的子文档
POST my_blogs/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query" : {
"match": {
"title" : "Learning Hadoop"
}
}
}
}
}
按需查询文档
# 通过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
如何设置字段类型
字段类型
-
字符串类型:需要分词则设置为 text 类型,否则设置为 keyword 类型;
-
Text
- 用于全⽂文本字段,文本会被 Analyzer 分词
- 默认不不⽀支持聚合分析及排序。需要设置 fielddata 为 true
-
Keyword
- 用于 id,枚举及不不需要分词的⽂文本。eg.电话号码,email地址,性别等
- 适用于 Filter(精确匹配),Sorting 和 Aggregations
-
设置多字段类型
- 默认会为文本类型设置成 text,并且设置一个 keyword 的子字段
- 在处理理人类语⾔言时,通过增加“英文”,“拼音”和“标准”分词器,提高搜索结构
-
- 数值类型:尽量选择贴近的类型,例如可以用 byte,就不要用 long
- 枚举类型:基于性能考虑将其设置为 keyword 类型,即便该数据是数字;
- 其他类型:比如布尔类型、日期、地理位置数据等;
是否要搜索及分词
-
完全不需要检索、排序、聚合分析的字段
- 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 进行数据分析,在数据建模时仍需对嵌套和⽗子关联类型作出取舍
避免过多字段
-
一个文档中,避免⼤量的字段
- 过多的字段数不不容易易维护
- Mapping 信息保存在 Cluster State 中,数据量量过⼤大,对集群性能会有影响
- 删除或者修改数据需要 reindex
- 默认大字段数是 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 的版本?每个主要版本号所关联的文档?
解决⽅案:将字符串转换为对象
搜索过滤
避免空值引起的聚合不准
使用 Null_Value 解决空值的问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。