文章目录
- ES如何处理关联关系
- 对象类型
- 案例一 适用场景
- 案例二 不适用场景
- 嵌套对象nested object
- 父子关联关系
- 嵌套文档 VS 父子关系
ES如何处理关联关系
关系型数据库中的范式化:
- 减少了数据冗余,节省了磁盘空间
- 减少了不必要的更新操作,因为没有了数据冗余,我更新一个地方的数据就可以了,不用去更新冗余数据
- 单查询需要join更多的表,范式简化了更新,读取操作可能更多。
反范式化:
- 不使用join关联关系,而是在文档中保存冗余数据
- 读取的性能更好,并且ES通过压缩_source字段减少所占磁盘空间
- 不适合频繁更新的场景
ElasticSearch就是使用的反范式。ES并不擅长处理关联关系,如果出现了一般会采取以下几种方式:
- 对象类型
- 嵌套对象
- 父子关联关系
- 应用层面自己处理
对象类型
文档中使用一个字段,该字段保存的值value是一个object。这种方式不适用于 value为对象数组的场景,查询会不准确。
如下所示,案例一是适用场景,案例二为不适用场景
案例一 适用场景
博客作者信息变更
对象类型:
- 在每一博客的文档中都保留作者的信息
- 如果作者信息发生变化,需要修改相关的博客文档
DELETE /blog
# 创建一个blog索引,其中的user字段保存的是一个object
PUT /blog
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"create_time": {
"type": "date",
"format": ["yyyy-MM-dd HH:mm:ss"]
},
"user": { # user字段中保存的是一个对象,通过properties关键字定义对象中的字段
"properties": {
"userid": {
"type": "long"
},
"username": {
"type": "text"
},
"age": {
"type": "long"
}
}
}
}
}
}
# 插入一条 blog信息
PUT /blog/_doc/1
{
"content": "I like Elasticsearch",
"create_time": "2024-01-01 00:00:00",
"user": { # user字段中保存的是一个对象
"userid": 1,
"username": "hushang",
"age": 24
}
}
# 查询,可以使用使用user.field 对象内的字段查询
GET /blog/_search
{
"query": {
"bool": {
"must": [
{ "match": { "content": "Elasticsearch" } },
{ "match": { "user.username": "hushang" } }
]
}
}
}
案例二 不适用场景
user字段如果保存的是一个对象数组,在搜索时添加两个查询条件,数组中两个对象分别满足一个条件。
我使用bool must 关键字,表示两个查询条件都需要满足,才会显示文档。但是上方中数组中两个对象分别满足一个条件 这也查询出来了。
DELETE /blog
# 创建一个blog索引,其中的user字段保存的是一个object
PUT /blog
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"user": { # user字段保存 姓 和 名 两个字段
"properties": {
"first_name": {
"type": "text"
},
"last_name": {
"type": "text"
}
}
}
}
}
}
# 写入一条数据 张三和李四
PUT /blog/_doc/1
{
"content": "speed",
"user": [
{
"first_name":"zhang",
"last_name":"san"
},
{
"first_name":"li",
"last_name":"si"
}
]
}
# 查询,此时我的性查询的是zhang 名查询的是si 而且还是采用的bool must方式。我期望的结果是应该查询不到数据
# 但实际上此时能查询到数据
GET /blog/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first_name": "zhang" } },
{ "match": { "user.last_name": "si" } }
]
}
}
}
造成这种情况的原因是
ES在存储文档数据时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对结构。当对多个字段进行查询时,导致了意外结果。
"content":"speed"
"user".first_name: ["zhang","li"]
"user".last_name: ["san","si"]
可以使用nested data type
查询解决这个问题
嵌套对象nested object
-
nested数据类型,它允许对象数组中的对象被独立索引
-
使用nested和properties关键字,将上方案例中所有user索引到多个分隔的文档。
-
在内部,Nested文档会被保存在两个Lucene文档中,在查询时做join处理
DELETE /blog
# 创建一个blog索引,其中的user字段保存的是一个object
# 并且使用了type:nested
PUT /blog
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"user": {
"type": "nested", # 使用了type:nested
"properties": {
"first_name": {
"type": "keyword"
},
"last_name": {
"type": "keyword"
}
}
}
}
}
}
# 写入一条数据 张三和李四
PUT /blog/_doc/1
{
"content": "speed",
"user": [
{
"first_name":"zhang",
"last_name":"san"
},
{
"first_name":"li",
"last_name":"si"
}
]
}
# 查询
GET /blog/_search
{
"query": {
"nested": { # 使用nested关键字
"path": "user", # user字段是nested类型 这里指定nestred类型的字段,并且下面就是对这个字段进行查询
"query": { # 之后就是正常的查询query语句
"bool": {
"must": [
{ "match": { "user.first_name": "zhang" } },
{ "match": { "user.last_name": "si" } }
]
}
}
}
}
}
nested类型的字段,直接进行aggs聚合操作是没有数据的
# user字段是nested类型 直接聚合操作是没有数据的
GET /blog/_search
{
"size": 0,
"aggs": {
"hs_first_name": {
"terms": {
"field": "user.first_name"
}
}
}
}
# 需要添加nestred关键字,并指定user这个字段
GET /blog/_search
{
"size": 0,
"aggs": {
"hs_agg": {
"nested": { # 添加nestred关键字
"path": "user"
},
"aggs": { # 再进行聚合操作
"hs_first_name": {
"terms": {
"field": "user.first_name"
}
}
}
}
}
}
父子关联关系
使用ES时,大部分的场景都不会频繁更新操作,父子关联关系了解即可。
再更新操作时,对象类型和嵌套对象nested方式有一个问题,因为根对象和嵌套对象本质上它们还是存在一个文档中的,每次更新时就可以需要重新索引整个文档。
ES提供了父子关联关系,通过维护parent/child的关系,分离它们,使父文档和子文档是两个独立的文档,更新其中一个文档不会影响另一个文档。
设定 Parent/Child Mapping
# 设定 Parent/Child Mapping映射关系
# 指定我们定义的hs_blog_comments_relation字段类型为join
# 并在relations下指定两个关联的自定义字符串值 其中hs_blog为parent名称,hs_comment为child名称
PUT /my_blogs
{
"settings": {
"number_of_shards": 2
},
"mappings": {
"properties": {
"hs_blog_comments_relation": {
"type": "join",
"relations": {
"hs_blog": "hs_comment"
}
},
"content": {
"type": "text"
},
"title": {
"type": "keyword"
}
}
}
}
索引父文档
# 索引两个父文档 ,指定文档id为blog1 和 blog2
# 同时指定文档的类型为hs_blog父文档
PUT /my_blogs/_doc/blog1
{
"content": "learning ELK ",
"title": "Learning Elasticsearch",
"hs_blog_comments_relation": {
"name": "hs_blog"
}
}
PUT /my_blogs/_doc/blog2
{
"content": "learning Hadoop ",
"title": "Learning Hadoop",
"hs_blog_comments_relation": {
"name": "hs_blog"
}
}
索引子文档
创建子文档时,必须通过routing指定父文档id,保证父子文档在一个shard中,提高join查询性能。
当指定子文档时,必须指定父文档id
# 索引三个子文档,指定文档id、同时指定routing 让父子文档在相同的shard中
# 指定文档的类型为子文档,同时必须指定它的父文档id
PUT /my_blogs/_doc/comment1?routing=blog1
{
"comment":"I am learning ELK",
"username":"Jack",
"hs_blog_comments_relation": {
"name": "hs_comment",
"parent": "blog1"
}
}
PUT /my_blogs/_doc/comment2?routing=blog2
{
"comment":"I like Hadoop!!!!!",
"username":"Jali",
"hs_blog_comments_relation": {
"name": "hs_comment",
"parent": "blog2"
}
}
PUT /my_blogs/_doc/comment3?routing=blog2
{
"comment":"Hello Hadoop",
"username":"Bob",
"hs_blog_comments_relation": {
"name": "hs_comment",
"parent": "blog2"
}
}
测试查询
# 查询所有文档,就是正常的查询,能查询到5个文档,因为父子文档都是独立的文档
POST /my_blogs/_search
#根据父文档ID查看,也就是正常的查询
GET /my_blogs/_doc/blog2
# has_child 查询,返回这个子文档对应的父文档
GET /my_blogs/_search
{
"query": {
"has_child": {
"type": "hs_comment",
"query": {
"match": {
"username": "Jack"
}
}
}
}
}
# has_parent 查询,返回相关的子文档
GET /my_blogs/_search
{
"query": {
"has_parent": {
"parent_type": "hs_blog",
"query": {
"match": {
"content": "ELK"
}
}
}
}
}
GET /my_blogs/_search
#通过ID ,访问子文档,会发现查询不到数据,返回404
# 但是通过上方直接查询全部是能查询到comment3这个文档的
GET /my_blogs/_doc/comment3
# 通过query语句能查询到到这个子文档
GET /my_blogs/_search
{
"query": {
"match": {
"comment": "Hello"
}
}
}
#通过ID和routing ,这种方式也能查询到这个子文档
GET /my_blogs/_doc/comment3?routing=blog2
#更新子文档,因为使用的是PUT 全量更新,所以在文档中还是需要指定子文档类型和父文档id
PUT /my_blogs/_doc/comment3?routing=blog2
{
"comment": "Hello Hadoop??",
"blog_comments_relation": {
"name": "hs_comment",
"parent": "blog2"
}
}
嵌套文档 VS 父子关系
Nested Object | Parent / Child | |
---|---|---|
优点 | 文档存储在一起,读取性能高 | 父子文档可以独立更新 |
缺点 | 更新嵌套的子文档时,需要更新整个文档 | 需要额外的内存维护关系。读取性能相对差 |
适用场景 | 子文档偶尔更新,以查询为主 | 子文档更新频繁 |