hi,我是蛋挞,一个初出茅庐的后端开发,希望可以和大家共同努力、共同进步!
开启掘金成长之旅!这是我参与「掘金日新计划 · 4 月更文挑战」的第 4 天,点击查看活动详情
- 起始标记->数据建模(7讲):「49 | 对象及Nested对象」
- 结尾标记->数据建模(7讲):「50 | 文档的父子关系」
对象及Nested对象
关系型数据库的范式化设计
| 1NF一消除非主属性对键的部分函数依赖 |
|---|
| 2NF-消除非主要属性对键的传递函数依赖 |
| 3NF一消除主属性对键的传递函数依赖 |
| BCNF一主属性不依赖于主属性 |
- 范式化设计(Normalization)的主要目标是“减少不必要的更新”
- 副作用:一个完全范式化设计的数据库会经常面临“查询缓慢”的问题
- 数据库越范式化,就需要 Join 越多的表
- 范式化节省了存储空间,但是存储空间却越来越便宜
- 范式化简化了更新,但是数据“读”取操作可能多
Denormalization
- 反范式化设计
- 数据“Flattening”,不使用关联关系,而是在文档中保存几余的数据拷贝
- 优点: 无需处理 Joins 操作,数据读取性能好
- Elasticsearch 通过压缩 source 字段,I减少磁盘空间的开销
- 缺点**:不适合在数据频繁修改的场景**
- 一条数据 (用户名)的改动,可能会引起很多数据的更新
在Elasticsearch 中处理关联关系
- 关系型数据库,一般会考虑 Normalize 数据;在 Elasticsearch,往往考虑 Denormalize 数据
- Denormalize 的好处: 读的速度变快 / 无需表连接 /无需行锁
- Elasticsearch 并不擅长处理关联关系。我们一般采用以下四种方法处理关联
- 对象类型
- 嵌套对象(Nested Obiect)0
- 父子关联关系(Parent / Child )
- 应用端关联
为什么会搜到不需要的结果?
- 存储时,内部对象的边界并没有考虑在内,JSON 格式被处理成扁平式键值对的结构
- 当对多个字段进行查询时,导致了意外的搜索结果
- 可以用 Nested Data Type 解决这个问题
什么是 Nested Data Type
- Nested 数据类型:允许对象数组中的对象被独立索引
- 使用 nested 和 properties 关键字,将所有 actors 索引到多个分隔的文档
- 在内部,Nested 文档会被保存在两个Lucene 文档中,在查询时做 Join 处理
嵌套查询
- 在内部,Nested 文档会被保存在两个 Lucene文档中,会在查询时做 Join 处理
CodeDemo
DELETE blog
# 设置blog的 Mapping
PUT /blog
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"time": {
"type": "date"
},
"user": {
"properties": {
"city": {
"type": "text"
},
"userid": {
"type": "long"
},
"username": {
"type": "keyword"
}
}
}
}
}
}
# 插入一条 Blog 信息
PUT blog/_doc/1
{
"content":"I like Elasticsearch",
"time":"2019-01-01T00:00:00",
"user":{
"userid":1,
"username":"Jack",
"city":"Shanghai"
}
}
# 查询 Blog 信息
POST blog/_search
{
"query": {
"bool": {
"must": [
{"match": {"content": "Elasticsearch"}},
{"match": {"user.username": "Jack"}}
]
}
}
}
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"}}
]
}
}
}
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 查询
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
}
}
}
}
相关阅读
本节知识总结
在Elasticsearch当中我们通常会使用Denormalization来进行建模,但数据中如果包含了一个数组对象的时候如果对这个数组对象进行查询可能会查到我们不想要查到的结果,在这个时候可以引入嵌套对象的方式来解决这个问题。
文档的父子关系
Parent/Child
- 对象和 Nested 对象的局限性
- 每次更新,需要重新索引整个对象 (包括根对象和嵌套对象)
- ES 提供了类似关系型数据库中 Join 的实现。使用 Jin 数据类型实现,可以通过维护 Parent/Child 的关系,从而分离两个对象
- 父文档和子文档是两个独立的文档
- 更新父文档无需重新索引子文档。子文档被添加,更新或者删除也不会影响到父文档和其他的子文档
父子关系
- 定义父子关系的几个步骤
- 设置索引的 Mapping
- 索引父文档
- 索引子文档
- 按需查询文档
设置 Mapping
索引父文档
索引子文档
- 父文档和子文档必须存在相同的分片上
- 确保查询join 的性能
- 当指定子文档时候,必须指定它的父文档ld
- 使用 route 参数来保证,分配到相同的分片
- 子文档的ID
- 指定 routing,确保和父文档索引到相同的分片
- 父文档的ID
嵌套对象 v.s父子文档
CodeDemo
DELETE my_blogs
# 设定 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/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"
}
}
# 查询所有文档
POST my_blogs/_search
{
}
#根据父文档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 ,访问子文档
GET my_blogs/_doc/comment3
#通过ID和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"
}
}
相关阅读
- www.elastic.co/guide/en/el…
- www.elastic.co/guide/en/el…
- www.elastic.co/guide/en/el…
- www.elastic.co/guide/en/el…
本节知识总结
学习了父子文档关系,当父文档和子文档更新比较频繁的时候考虑使用这种对象关系。
此文章为4月Day4学习笔记,内容来源于极客时间《Elasticsearch 核心技术与实战》