ElasticSearch:处理数据的关联关系 nested 父子文档

    科技2022-07-16  113

    传统关系型数据库对关联关系的处理

    对于传统关系型数据库而言,处理数据的关联关系时比较正规的设计是范式化设计与非范式化设计。

    范式化 (Normalization)

    范式化设计的主要⽬标是“减少不必要的更新”,一般有三段范式,其实就是本着将两个关联数据模型之间通过主键的处理去划分属性字段,减少不必要的更新处理。

    关于范式化的概念可以自行去学习。

    反范式化 (Denormalization)

    范式化设计带来的一个弊端就是读操作可能会涉及很多表的处理,性能受影响,所以如果我们本来只需要关联对象的一个字段却还得去查询关联表一次,很不方便,于是便有了范式化设计,就是不使⽤关联关系,⽽是直接保存冗余的数据,减少join操作。

    关于反范式化的概念可以自行去学习。

    ElasticSearch 对关联关系的处理

    在 ElasticSearch 中,对数据的关联关系的处理,其实也有参考范式化与反范式化的设计,并且针对这两中设计理念都有对应的实现方式。

    嵌套对象(Nested Object)

    nested 是 ElasticSearch 处理关联关系时的一种范式化设计的数据模型,在索引时他们被存放到两个不同Lucene文档中,在查询的时候 join出来,将根父文档带出来。

    允许嵌套对象中的数据被独⽴索引分成两个文档去存储,类似数据库的分表存放关键词 “type”: “nested”

    指定 nested 数据类型

    要想使用 nested,需要我们在设置mapping的时候将这个对象的数据类型设置成为 nested。

    PUT blog { "mappings": { "properties": { "actors": { "type": "nested", "properties": { "name": { "type": "keyword" }, "sex": { "type": "keyword" }, "another": { "type": "nested", "properties": { "name": { "type": "keyword" } } } } } } } }

    如上这个mapping的设置语义,就是将actors作者定义成为nested存储类型。

    nested 查询API

    针对 nested 数据结构的查询,需要使用 ElasticSearch 提供的 nested 查询API。

    语法:POST 索引名/_search + 请求体(关键词:nested )

    path 要查询字段路径,以根路径开始,比如上方的actors只有一层,那么path 就是 actors,如果是another,那么path 就是 actors.another,加上父路径query 查询语法 POST blog/_search { "query": { "nested": { "path": "actors", "query": { "bool": { "must": [ { "match": { "actors.name": "Keanu" } }, { "match": { "actors.sex": "boy" } } ] } } } } }

    如上的查询语义是,查询actors 的那么等于Keanu以及sex等于boy的文档出来,可以看到结果是正常查询的。

    POST blog/_search { "query": { "nested": { "path": "actors.another", "query": { "bool": { "must": [ { "match": { "actors.another.name": "mrs" } } ] } } } } }

    如上的查询语义是查询another的name为mrs的,可以看到结果也可以正常展示。 这里需要注意的是match中的内容,要写全路径名称。

    更新文档

    nested 有个不好用的缺点就是,我们对嵌套文档进行操作的时候,本来是打算想要更新其中一个字段的数据,但是他会进行覆盖处理,对整个文档进行更新。 可以看到,我们的更新语句只是想更新title,结果将 actors直接覆盖掉了。

    父子文档关系

    因为 nested 更新的缺点,导致ElasticSearch 无法像传统关系型数据库那样保证各个表之间的数据互不干扰,所以 ElasticSearch 又提供了一种新的数据结构,就是父子文档。 父子文档的父文档与子文档的更新并不会干扰到对方,这与数据库的多表join已经很相似了,父子文档之间通过父文档的id关联,这有些类似数据库的外键。

    分成两个文档去存储,类似数据库的分表存放关键词 “type”: “join”

    指定父子文档的mapping

    要想使用父子文档,需要我们在设置mapping的时候将数据类型设置成为 join。

    “type” 类型为 “join”relations 指定区分父子文档的唯一标识,比如"parent": “child”,那么 parent 就是父文档的唯一标识,child 就是子文档的唯一标识,在文档索引的时候需要带在文档中。 PUT blogs { "mappings": { "properties": { "child_relation": { "type": "join", "relations": { "parent": "child" } }, "content": { "type": "text" }, "title": { "type": "keyword" } } } }

    这个文档mapping,语义是child_relation为父子文档类型的,并且如果 child_relation 的值为 parent,那么为父文档,如果 child_relation 的值为 child,那么为子文档,这个值是随你自己自定义的。

    索引文档,分别索引父文档与子文档

    父子文档是两个独立的文档,之间互不干扰,可以单独的去查询,并且需要单独的去索引。

    索引父文档

    在索引父子文档的时候,都需要显示的指定文档id,因为ElasticSearch需要根据这个id去建立关系,它自己无法知道,需要用户去告诉他。

    指定父文档id关联关系唯一标识设置为mapping 中父文档的标识 #索引父文档 PUT blogs/_doc/blog2 { "title":"Learning Hadoop", "content":"learning Hadoop is so easy", "child_relation":{ "name":"parent" } }

    这里的 child_relation 是我们在之前mapping中定义的父子文档之间的关联唯一标识字段,这里因为是父文档,所以这里需要将 child_relation 的 name 字段设置成为parent。

    索引子文档

    在索引子文档的时候,需要显示的指定子文档id,并且还需要指定父文档的id,告诉ElasticSearch 你这个文档的父文档是谁。

    指定子文档id指定父文档id,通过关键字 routing关联关系唯一标识设置为mapping 中子文档的标识 #索引子文档 PUT blogs/_doc/comment2?routing=blog2 { "comment":"I like Hadoop!!!!!", "username":"Jack", "child_relation":{ "name":"child", "parent":"blog2" } }

    首先指定子文档的id,然后通过 routing 指定父文档的id,最后需要在 child_relation 中指定子文档的唯一标识并且指定parent 关键字段的值为父文档的id。

    查询 API

    ElasticSearch 对于父子文档的查询提供了如下几种查询方式

    基本查询Parent Id 查询Has Child 查询Has Parent 查询

    基本查询

    父子文档也支持常用的查询语法。

    查询所有文档
    # 查询所有文档 POST blogs/_search { "query": { "match_all": {} } }
    根据文档id访问文档

    根据文档id去访问,只会访问到当前文档

    #根据父文档ID查看 GET blogs/_doc/blog2 #根据子文档ID查看 GET blogs/_doc/comment3

    Parent Id 查询

    Parent Id 查询是ElasticSearch 提供的一种专门查询父子文档的API,这种查询是为了满足类似将一个父文档下的所有的子文档全部查询出来的场景。

    语法:POST 索引名/_search + 查询体(关键词:“parent_id”)

    type 子文档的唯一标识id 父文档的id # Parent Id 查询 POST blogs/_search { "query": { "parent_id": { "type": "child", "id": "blog2" } } }

    这个语法就是查询 blog2 下的所有子文档,可以看到结果,都是符合条件的,注意这里不会将父文档带出来。

    Has Child 查询

    Has Child 查询是根据查询条件将符合条件的子文档查询出来然后再根据父文档id将所有的父文档查询出来。

    语法:POST 索引名/_search + 查询体(关键词:“has_child”)

    type 子文档的唯一标识query 查询语法,这里的查询条件是针对子文档的字段的。 # Has Child 查询,返回父文档 POST blogs/_search { "query": { "has_child": { "type": "child", "query" : { "match": { "username" : "Jack" } } } } }

    Has Parent 查询

    Has Parent 查询正好与子查询相反,会将父文档的查询条件找出父文档并且再去找出所有的子文档。

    语法:POST 索引名/_search + 查询体(关键词:“has_parent”)

    parent_type 子文档的唯一标识query 查询语法,这里的查询条件是针对父文档的字段的。 # Has Parent 查询,返回相关的子文档 POST blogs/_search { "query": { "has_parent": { "parent_type": "parent", "query" : { "match": { "title" : "Learning Hadoop" } } } } }

    Processed: 0.010, SQL: 8