当前位置:   article > 正文

ElasticSearch文档数据关联关系处理

ElasticSearch文档数据关联关系处理

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" } }
      ]
    }
  }
}
  • 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



案例二 不适用场景

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" } }
      ]
    }
  }
}
  • 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

在这里插入图片描述



造成这种情况的原因是

ES在存储文档数据时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对结构。当对多个字段进行查询时,导致了意外结果。

"content":"speed"
"user".first_name: ["zhang","li"]
"user".last_name: ["san","si"]
  • 1
  • 2
  • 3

可以使用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" } }
          ]
        }
      }
    }
  }
}
  • 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
  • 59

在这里插入图片描述



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"
          }
        }
      }
    }
  }
}
  • 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

在这里插入图片描述

在这里插入图片描述



父子关联关系

使用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"
      }
    }
  }
}
  • 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

在这里插入图片描述



索引父文档

# 索引两个父文档 ,指定文档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"
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在这里插入图片描述



索引子文档

创建子文档时,必须通过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"
  }
}
  • 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

在这里插入图片描述



测试查询

# 查询所有文档,就是正常的查询,能查询到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"
        }
      }
    }
  }
}
  • 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

在这里插入图片描述

在这里插入图片描述



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"
    }
}
  • 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



嵌套文档 VS 父子关系

Nested ObjectParent / Child
优点文档存储在一起,读取性能高父子文档可以独立更新
缺点更新嵌套的子文档时,需要更新整个文档需要额外的内存维护关系。读取性能相对差
适用场景子文档偶尔更新,以查询为主子文档更新频繁
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/987756
推荐阅读
相关标签
  

闽ICP备14008679号