一、前言

在日常开发中,常遇到父子关系的对象,它们通常是1:N的关系:

  • 一个parent可以有多个child
  • 一个child只能有一个parent

如果我们用关系型数据库(如MySQL),我们还是挺熟悉如何设计表的。一个主表,一个子表,两个表可以用joinleft joinright join做关联查询。

而在ES中,不能像MySQL那样设计表了,ES提供了2种表达父子关系的方案:

  • nested,子属于父的某一个nested字段里,子属于父的一部分,父和子在同一文档里。
  • join,子和父在同一index里,但不在同一文档里,父和子都是独立的文档。
  • join类型父子文档必须在同一分片上

nested类型,之前的博客Mapping字段类型之nested已经讲过了,本篇博客主要讲join类型。

二、join类型Mapping

join类型是一种用来创建父子关系的特殊类型,且父子文档都存放在同一index里。
父子关系,不能是多对多,也就是说一个父亲可以有多个儿子,但一个儿子只能有一个父亲。

下面我要创建《王者荣耀》里两个阵营(group)以及其英雄(hero)的信息,举例说明join类型。

  • 稷下学院阵营:老夫子、墨子
  • 长安阵营:李白、李元芳
  • 阵营(group)是父文档,英雄(hero)是子文档
  • 请牢牢记住这边的grouphero,后面的查询都少不了它们
IDname关系类型父ID
group1稷下学院group
group2长安group
hero1老夫子herogroup1
hero2墨子herogroup1
hero3李白herogroup2
hero4李元芳herogroup2
PUT pigg_test_join
{
  "mappings": {
    "properties": {
      "name": {			  	# 第1个字段叫name,是keyword类型
        "type": "keyword"
      },
      "join_field": {     	# 第2个字段叫join_field,是join类型
        "type": "join",
        "relations": {      # relations是固定关键字,表示父子关系
          "group": "hero"   # 冒号:的左边是父类型名称,右边是子类型名称
        }
      }
    }
  }
}

三、插入父和子文档

1 父文档

插入id=group1name=稷下学院的父文档:

PUT pigg_test_join/_doc/group1
{
  "name": "稷下学院",
  "join_field": {
    "name": "group"  # 指定文档为父类型group
  }
}

插入id=group2name=长安的父文档,指定父子类型可以用如下简化写法:

PUT pigg_test_join/_doc/group2
{
  "name": "长安",
  "join_field": "group"  # 简化写法,指定文档为父类型group
}

2 子文档

  • 父子文档必须在同一分片上,所以插入子文档时要指定routing为父ID
  • 创建、更新、删除、获取子文档时,都得加上routing参数
  • 子文档需要在关联字段里用parent指定父ID
  • 一个子只能有一个父亲

插入稷下学院阵营的2个子文档

PUT pigg_test_join/_doc/hero1?routing=group1  # 指定routing
{
  "name": "老夫子",
  "join_field": {
    "name": "hero",  	# 指定该文档为子文档
    "parent": "group1"  # 指定父ID
  }
}

PUT pigg_test_join/_doc/hero2?routing=group1
{
  "name": "墨子",
  "join_field": {
    "name": "hero", 
    "parent": "group1" 
  }
}

插入长安阵营的2个子文档

PUT pigg_test_join/_doc/hero3?routing=group2
{
  "name": "李白",
  "join_field": {
    "name": "hero", 
    "parent": "group2" 
  }
}

PUT pigg_test_join/_doc/hero4?routing=group2
{
  "name": "李元芳",
  "join_field": {
    "name": "hero", 
    "parent": "group2" 
  }
}

四、parent_id 根据父ID查询儿子

  • parent_id查询是根据父亲ID查询其儿子文档
  • parent_id.type是指定儿子文档的类型,因为子类型可以有多个,所以要指定下
GET pigg_test_join/_search
{
  "query": {
    "parent_id": {		# 关键字
      "type": "hero",   # 子文档的类型
      "id": "group1"    # 父ID
    }
  }
}

上面查询返回的是老夫子墨子这2个文档

五、has_parent 根据父查子

  • 查询条件是描述父亲,返回的是子文档
  • has_parent是固定关键字
  • has_parent.parent_type是指定父类型
  • query查询条件体应该是对对父亲的描述

比如查询name=长安的阵营有哪些英雄?

GET pigg_test_join/_search
{
  "query": {
    "has_parent": {
      "parent_type": "group",
      "query": {
        "term": {
          "name": "长安"
        }
      }
    }
  }
}

上面查询返回的是李元芳李白这两个文档

六、has_child 根据子查父

  • 查询条件是描述儿子,返回的是父文档
  • has_child.type指定子类型
  • query查询体里面是描述儿子

比如查询哪些阵营有姓李的英雄?

GET pigg_test_join/_search
{
  "query": {
    "has_child": {
      "type": "hero",
      "query": {
        "prefix": {
          "name": "李"
        }
      }
    }
  }
}

七、join VS nested

nestedjoin
优点父子文档存储一起,读取性能高父子文档可以独立更新
缺点更新嵌套的子文档时,更新nested字段的全部文档需要额外的内存维护关系,读取性能相对差
适用场景子文档偶尔更新,以查询为主子文档更新频繁

在我实际工作中用nested更多些,join类型设计的相当复杂,对人员的水平要求较高些(你得足够细心),而且在父子文档分别存储时,我们也可以使用nested类型来维护父子关系,而不一定用join
具体的可以参考我之前的博客:
ES 存储树形结构 整合Spring Data Elasticsearch
Elasticsearch工具类 支持树形结构

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐