在关系数据库中,子表使用外键引用父表,这种关系称为 join。 设计通常涉及 规范化数据
Elasticsearch 不是关系数据库,它全与搜索效率而不是存储效率有关。 存储的数据已被 去规范化 并且几乎是平坦的。 这意味着 join不能跨索引,Elasticsearch 的重点在于速度,而传统 join 的运行速度太慢。 因此,子文档和父文档都必须位于相同的索引和相同的分片中。

父-子关系例子

让我们考虑下图1所示的家谱。该树有 3 个父母和 9 个孩子。 每个角色都有 “gender” 和 “isAlive” 状态。

在上面的示例中,我们探索以下场景:

  • 每个父母有多个孩子
  • 多个层次的亲子关系
  • 创建 family_tree 索引

    以下代码有为上述关系创建索引:

    PUT family_tree
      "settings": {
        "index": {
          "number_of_shards": 1,
          "number_of_replicas": 1
      "mappings": {
        "properties": {
          "firstName": {
            "type": "text"
          "lastName": {
            "type": "text"
          "gender": {
            "type": "text"
          "isAlive": {
            "type": "boolean"
          "relation_type": {
            "type": "join",
            "eager_global_ordinals": true,
            "relations": {
              "parent": "child"
    
  • relation_type: 是 join 数据类型的名字
  • eager_global_ordinals: 亲子使用 Global Ordinals 加快导入速度
  • relations: 定义了一组可能的关系,每个关系都是 parent 名称和 child 名称。在这个例子里,为了方便,我们使用了 “parent” 及 “child” 作为父子关系的名称。在实际的例子中,我们可以使用你们所喜欢的名字
  • 插入 parent 数据

    在运行脚本以插入上图1所示的其他父级数据之前,让我们逐步了解一个父级插入的代码。

    PUT family_tree/_doc/1?routing=Darren
      "firstName": "Darren",
      "lastName": "Ford",
      "gender": "Male",
      "isAlive": false,
      "relation_type": {
        "name": "parent"
    

    上面的代码为 Darren Ford 创建了一个新文档,并使用 related_type 字段将其标记为父文档。 将值 “parent” 分配给关系的名称。 除了关系之外,它还添加了所需的字段,例如 “firstName”,“lastName”,“gender” 和 “isAlive”。
    这里要注意的一件事是 routing 查询参数。 每个父对象都为其参数分配自己的名称。 路由字段可帮助我们控制文档将在哪个分片上建立索引。 分片使用以下公式标识:

    shard = hash(routing_value) % number_of_primary_shards
    

    我们通过如果的方法来插入余下的 parent 文档:

    PUT family_tree/_doc/1?routing=Darren
      "firstName": "Darren",
      "lastName": "Ford",
      "gender": "Male",
      "isAlive": false,
      "relation_type": {
        "name": "parent"
    PUT family_tree/_doc/2?routing=Sienna
      "firstName": "Sienna",
      "lastName": "Evans",
      "gender": "Female",
      "isAlive": false,
      "relation_type": {
        "name": "parent"
    PUT family_tree/_doc/3?routing=Ryan
      "firstName": "Ryan",
      "lastName": "Turner",
      "gender": "Male",
      "isAlive": false,
      "relation_type": {
        "name": "parent"
    

    插入 child 数据

    同样,让我们先遍历一个子插件,然后运行上图所示的9个子插件的批量插入。

    PUT family_tree/_doc/5?routing=Darren
      "firstName": "Pearl",
      "lastName": "Ford",
      "gender": "Female",
      "isAlive": true,
      "relation_type": {
        "name": "child",
        "parent": "1"
    

    在我们的示例中,“Pearl Ford” 是 “Darren Ford” 的子代,请注意,我们使用与创建 Darren 记录相同的 routing 查询参数。 这是因为子文档和父文档必须位于同一分片上的限制。
    该记录与 Darren 的记录之间的关联是由 related_type 字段进行的,在该字段中,我们将关系的名称添加为 “child”,从而使 Pearl Ford 成为 ID 为“1”的 parent 的子代(我们创建父代 Darren 的同一个 ID 与)。

    我们可以运用如下的代码来创建如下的子文档:

    PUT family_tree/_bulk?routing=Darren
    {"index": {"_id": "4"}}
    {"firstName":"Otis", "lastName":"Ford", "gender":"Male", "isAlive":false, "relation_type":{ "name":"child", "parent":"1" }}
    {"index": {"_id": "5"}}
    {"firstName":"Pearl", "lastName":"Ford", "gender":"Female", "isAlive":true, "relation_type": { "name":"child", "parent":"1" }}
    {"index": {"_id": "6"}}
    {"firstName":"Ava", "lastName":"Ford", "gender":"Female", "isAlive":true, "relation_type": { "name":"child", "parent":"1" }}
    {"index": {"_id": "7"}}
    {"firstName":"Tyler", "lastName":"Ford", "gender":"Male", "isAlive":true, "relation_type": { "name":"child", "parent":"1" }}
    {"index": {"_id": "8"}}
    {"firstName":"Xavier", "lastName":"Ford", "gender":"Male", "isAlive":true, "relation_type": { "name":"child", "parent":"1"}}
    PUT family_tree/_bulk?routing=Sienna
    {"index": {"_id": "9"}}
    {"firstName":"Ralph", "lastName":"Evans", "gender":"Male", "isAlive":true, "relation_type":{ "name":"child", "parent":"2" }}
    PUT family_tree/_bulk?routing=Ryan
    {"index": {"_id": "10"}}
    {"firstName":"Fred", "lastName":"Turner", "gender":"Male", "isAlive":true, "relation_type":{ "name":"child", "parent":"3" }}
    {"index": {"_id": "11"}}
    {"firstName":"Scarlet", "lastName":"Turner", "gender":"Female", "isAlive":false, "relation_type":{ "name":"child", "parent":"3" }}
    {"index": {"_id": "12"}}
    {"firstName":"Wayne", "lastName":"Turner", "gender":"Male", "isAlive":true, "relation_type":{ "name":"child", "parent":"3" }}
    

    我们使用上面 bulk API 来把我们的 child 数据导入到 Elasticsearch 中。

    现在是执行和理解的有趣部分,我们可以在刚刚创建的关系上运行查询。

    搜索和过滤特定的 parent

  • 得到 Sienna Evans 的 child:parent_id 查询可用于查找属于特定 parent 的 child 文档
  • GET family_tree/_search
      "query": {
        "parent_id": {
          "type": "child",
          "id": "2"
    

    返回的结果是:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "9",
            "_score" : 1.7917595,
            "_routing" : "Sienna",
            "_source" : {
              "firstName" : "Ralph",
              "lastName" : "Evans",
              "gender" : "Male",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "2"
    

    上查询出 parent_id 为2的所有 child 文档。

  • 获取所有 isAlive 为 true 的所有 Darren Ford 的孩子:bool 和 must 关键字可用于获取记录。
  • GET family_tree/_search
      "query": {
        "bool": {
          "filter": {
            "term": {
              "isAlive": true
          "must": {
            "parent_id": {
              "type": "child",
              "id": "1"
    

    返回的结果为:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "5",
            "_score" : 0.6931472,
            "_routing" : "Darren",
            "_source" : {
              "firstName" : "Pearl",
              "lastName" : "Ford",
              "gender" : "Female",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "1"
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "6",
            "_score" : 0.6931472,
            "_routing" : "Darren",
            "_source" : {
              "firstName" : "Ava",
              "lastName" : "Ford",
              "gender" : "Female",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "1"
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "7",
            "_score" : 0.6931472,
            "_routing" : "Darren",
            "_source" : {
              "firstName" : "Tyler",
              "lastName" : "Ford",
              "gender" : "Male",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "1"
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "8",
            "_score" : 0.6931472,
            "_routing" : "Darren",
            "_source" : {
              "firstName" : "Xavier",
              "lastName" : "Ford",
              "gender" : "Male",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "1"
    

    执行上面的查询将获取 “Pearl”,“ Ava”,“ Tyler” 和 “Xavier” Ford 的记录。

    查询拥有 child 及 parent 查询

    查询关键字 has_child 和 has_parent,有助于查询具有父子关系的数据。

    获取所有育有女儿并且女儿已死的父母:has_child,关键字可帮助我们获取所有父母记录,其中孩子有过滤器。

    GET family_tree/_search
      "query": {
        "has_child": {
          "type": "child",
          "query": {
            "bool": {
              "must": [
                  "match": {
                    "gender": "Female"
                  "match": {
                    "isAlive": false
    

    查询的返回结果是:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "3",
            "_score" : 1.0,
            "_routing" : "Ryan",
            "_source" : {
              "firstName" : "Ryan",
              "lastName" : "Turner",
              "gender" : "Male",
              "isAlive" : false,
              "relation_type" : {
                "name" : "parent"
    

    执行上面的查询,将获得 “Ryan Turner” 的记录,Ryan Turner 是唯一一个死去的女儿 “Scarlet Turner” 的父母。

  • 获取所有父母性别为 “female” 的 child:has_parent 关键字可帮助我们获取所有有父母过滤条件的孩子记录。
  • GET family_tree/_search
      "query": {
        "has_parent": {
          "parent_type": "parent",
          "query": {
            "match": {
              "gender": "Female"
    

    返回的结果:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "9",
            "_score" : 1.0,
            "_routing" : "Sienna",
            "_source" : {
              "firstName" : "Ralph",
              "lastName" : "Evans",
              "gender" : "Male",
              "isAlive" : true,
              "relation_type" : {
                "name" : "child",
                "parent" : "2"
    

    执行上述查询,获取 “Ralph Evans” 的记录,其父母是 “Sienna Evans”,所有其他父母均为 Male。

    Parent 拥有多个children

    让我们将 “Melissa Ford” 作为 wife 添加到 “Darren Ford”中,如下图所示。“Darren” 现在已附加了 “children 和 wife” 文件。

    可以使用以下代码更改索引:

    PUT family_tree/_mapping
      "properties": {
        "relation_type": {
          "type": "join",
          "eager_global_ordinals": true,
          "relations": {
            "parent": [
              "child",
              "wife"
    

    我们修改了上面的 mapping,从而使得我们的 relation_type 具有一个 “parent” 及两个 children: “child” 及 “wife”。

    插入 “Melissa Ford” 文档与我们之前创建的子记录类似,这将使用与父路径 “Darren” 相同的路径参数,并使用 “wife” 作为 relationship_type 名称。

    PUT family_tree/_doc/13?routing=Darren
      "firstName": "Melissa",
      "lastName": "Ford",
      "gender": "Female",
      "isAlive": false,
      "relation_type": {
        "name": "wife",
        "parent": "1"
    

    查询 wife 数据

    获取有 wife 的 “parent” (请注意这里的 parent 不是指的是妻子的父母,而是我们的数据关系的父母,也就是 Daren。我们可以从上面的图中可以看出来):查询使用 has_child 关键字并按 “wife” 类型进行过滤。

    GET family_tree/_search
      "query": {
        "has_child": {
          "type": "wife",
          "query": {
            "match_all": {}
    

    返回数据:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 1.0,
            "_routing" : "Darren",
            "_source" : {
              "firstName" : "Darren",
              "lastName" : "Ford",
              "gender" : "Male",
              "isAlive" : false,
              "relation_type" : {
                "name" : "parent"
    

    执行以上查询,获取 “Darren Ford” 的记录。

    多级关系(孙子关系)

    让我们将 Grand Children 添加到家谱中,如下图所示:

    索引需要在这里重新创建! 这是由于另一个限制,只有在元素已经是父元素的情况下,才可以在现有元素中添加子元素。 由于在较早创建索引时 “child” 类型不是父类型,因此我们需要删除较早的索引,使用下面的代码创建一个新的索引,然后重新插入所有数据。

    我们首先重新修改 family_tree 的 mapping:

    DELETE family_tree
    PUT family_tree
      "settings": {
        "index": {
          "number_of_shards": 1,
          "number_of_replicas": 1
      "mappings": {
        "properties": {
          "firstName": {
            "type": "text"
          "lastName": {
            "type": "text"
          "gender": {
            "type": "text"
          "isAlive": {
            "type": "boolean"
          "relation_type": {
            "type": "join",
            "eager_global_ordinals": true,
            "relations": {
              "parent": [
                "child",
                "wife"
              "child": "grandchild"
    

    在这里,“child” 也成为 “grandchild” 类型的父母。 这使我们具有关系 parent→child→grandchild。

    我们先按照上面的顺序,把我们之前的数据导入到 family_tree 中。然后再导入我们的 grandchild 数据。插入 “grandchild” 文档与插入 “child” 记录非常相似。

    PUT family_tree/_doc/14?routing=Darren
      "firstName": "Douglas",
      "lastName": "Ford",
      "gender": "Male",
      "isAlive": true,
      "relation_type": {
        "name": "grandchild",
        "parent": "5"
    

    在我们的示例中,“Douglas Ford” 是 “Pearl Ford” 的子代,也是 “Darren Ford” 的孙代,请注意,我们使用与创建 Darren 记录相同的路由查询参数。 这样可以确保与超级父代 “Darren” 关联的所有子代都在同一分片上建立索引。

    此记录与 “Pearl Ford” 之间的 join 由 relation_type 字段进行,在该字段中,我们将关系的名称添加为 “grandchild”,使 “Douglas Ford” 成为其 ID 为“5”的父代的孙代(同一个 ID  我们创建了 Pearl Ford)。

    我们可以使用如下的代码把其它的 grandchild 的文档写入到 Elasticsearch 中:

    PUT family_tree/_bulk?routing=Darren
    {"index": {"_id": "14"}}
    {"firstName":"Douglas", "lastName":"Ford", "gender":"Male", "isAlive":true, "relation_type":{ "name":"grandchild", "parent":"5" }}
    PUT family_tree/_bulk?routing=Ryan
    {"index": {"_id": "15"}}
    {"firstName":"Frederick", "lastName":"Turner", "gender":"Male", "isAlive":false, "relation_type":{ "name":"grandchild", "parent":"11" }}
    {"index": {"_id": "16"}}
    {"firstName":"Eleanor", "lastName":"Turner", "gender":"Female", "isAlive":false, "relation_type":{ "name":"grandchild", "parent":"11" }}
    {"index": {"_id": "17"}}
    {"firstName":"Troy", "lastName":"Turner", "gender":"Male", "isAlive":false, "relation_type":{ "name":"grandchild", "parent":"11" }}
    

    请求 grandparent 数据

    获取所有拥有孙女的祖父母:

    GET family_tree/_search
      "query": {
        "has_child": {
          "type": "child",
          "query": {
            "has_child": {
              "type": "grandchild",
              "query": {
                "match": {
                  "gender": "Female"
    

    返回的结果:

        "hits" : [
            "_index" : "family_tree",
            "_type" : "_doc",
            "_id" : "3",
            "_score" : 1.0,
            "_routing" : "Ryan",
            "_source" : {
              "firstName" : "Ryan",
              "lastName" : "Turner",
              "gender" : "Male",
              "isAlive" : false,
              "relation_type" : {
                "name" : "parent"
    

    执行此查询将获得 “Ryan Turner” 记录,因为他是唯一拥有孙女 “Eleanor Turner” 的祖父母,如上面的图所示。

    在这里我们必须指出的是:

    不建议使用多个级别的关系来复制关系模型。 每个关系级别都会在查询时增加内存和计算方面的开销。 如果您关心性能,则应该对数据进行非规范化。 — elastic.co

    Elasticsearch 中的 join 限制

    现在,我们已经看到了加入功能的作用,让我们回顾一下上面注意到的限制。

  • 父文档和子文档必须在同一分片上建立索引。
  • 每个索引仅允许一个 join 字段映射。
  • 一个元素可以有多个子级,但只能有一个父级。
  • 可以向现有 join 字段添加新关系。
  • 也可以将子元素添加到现有元素中,但前提是该元素已经是父元素。
  • 当索引时间性能比搜索时间性能更重要时,父子联接可能是管理关系的有用技术,但代价是很高的。 必须意识到折衷方案,例如父子文档的物理存储约束和增加的复杂性。 另一个预防措施是避免多层父子关系,因为这将消耗更多的内存和计算量。

    分类:
    后端
    标签: