连接字段类型编辑

join 数据类型是一种特殊字段,用于在同一索引的文档之间创建父子关系。relations 部分定义了文档中一组可能的关系,每个关系都是一个父名称和一个子名称。

我们不建议使用多级关系来复制关系模型。每一级关系都会在查询时增加内存和计算开销。为了获得更好的搜索性能,请对数据进行反规范化。

父子关系可以定义如下

response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_id: {
          type: 'keyword'
        },
        my_join_field: {
          type: 'join',
          relations: {
            question: 'answer'
          }
        }
      }
    }
  }
)
puts response
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_id": {
        "type": "keyword"
      },
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

字段的名称

定义一个关系,其中 questionanswer 的父级。

要使用连接索引文档,必须在 source 中提供关系的名称和文档的可选父级。例如,以下示例在 question 上下文中创建两个 parent 文档

response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    my_id: '1',
    text: 'This is a question',
    my_join_field: {
      name: 'question'
    }
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    my_id: '2',
    text: 'This is another question',
    my_join_field: {
      name: 'question'
    }
  }
)
puts response
PUT my-index-000001/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}

PUT my-index-000001/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}

此文档是一个 question 文档。

在索引父文档时,可以选择仅指定关系的名称作为快捷方式,而不是将其封装在正常的对象表示法中

PUT my-index-000001/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": "question" 
}

PUT my-index-000001/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": "question"
}

父文档的更简单表示法仅使用关系名称。

在索引子级时,必须在 _source 中添加关系的名称以及文档的父级 ID。

需要在同一个分片中索引父级的 lineage,因此您必须始终使用其更大的父级 ID 来路由子文档。

例如,以下示例显示了如何索引两个 child 文档

response = client.index(
  index: 'my-index-000001',
  id: 3,
  routing: 1,
  refresh: true,
  body: {
    my_id: '3',
    text: 'This is an answer',
    my_join_field: {
      name: 'answer',
      parent: '1'
    }
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 4,
  routing: 1,
  refresh: true,
  body: {
    my_id: '4',
    text: 'This is another answer',
    my_join_field: {
      name: 'answer',
      parent: '1'
    }
  }
)
puts response
PUT my-index-000001/_doc/3?routing=1&refresh 
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

PUT my-index-000001/_doc/4?routing=1&refresh
{
  "my_id": "4",
  "text": "This is another answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

路由值是必需的,因为父文档和子文档必须索引在同一个分片上

answer 是此文档的连接名称

此子文档的父级 ID

父子连接和性能编辑

连接字段不应像关系数据库中的连接那样使用。在 Elasticsearch 中,良好性能的关键是将数据反规范化为文档。每个连接字段、has_childhas_parent 查询都会增加查询性能的负担。它还可以触发构建 全局序号

连接字段唯一有意义的情况是,如果您的数据包含一对多关系,其中一个实体的数量远远超过另一个实体。这种情况下,一个例子是产品和这些产品的报价用例。如果报价的数量远远超过产品的数量,那么将产品建模为父文档并将报价建模为子文档是有意义的。

父子连接限制编辑

  • 每个索引只允许一个 join 字段映射。
  • 父文档和子文档必须索引在同一个分片上。这意味着在 获取删除更新 子文档时需要提供相同的 routing 值。
  • 一个元素可以有多个子元素,但只能有一个父元素。
  • 可以向现有的 join 字段添加新的关系。
  • 也可以向现有元素添加子元素,但前提是该元素已经是父元素。

使用父子连接进行搜索编辑

父子连接创建一个字段来索引文档中关系的名称(my_parentmy_child 等)。

它还为每个父子关系创建一个字段。此字段的名称是 join 字段的名称,后跟 # 和关系中父级的名称。因此,例如,对于 my_parent → [my_child, another_child] 关系,join 字段会创建一个名为 my_join_field#my_parent 的附加字段。

如果文档是子级(my_childanother_child),则此字段包含文档链接到的父级 _id;如果文档是父级(my_parent),则此字段包含文档的 _id

在搜索包含 join 字段的索引时,这两个字段始终在搜索响应中返回

GET my-index-000001/_search
{
  "query": {
    "match_all": {}
  },
  "sort": ["my_id"]
}

将返回

{
  ...,
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": null,
    "hits": [
      {
        "_index": "my-index-000001",
        "_id": "1",
        "_score": null,
        "_source": {
          "my_id": "1",
          "text": "This is a question",
          "my_join_field": "question"         
        },
        "sort": [
          "1"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "2",
        "_score": null,
        "_source": {
          "my_id": "2",
          "text": "This is another question",
          "my_join_field": "question"          
        },
        "sort": [
          "2"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "3",
        "_score": null,
        "_routing": "1",
        "_source": {
          "my_id": "3",
          "text": "This is an answer",
          "my_join_field": {
            "name": "answer",                 
            "parent": "1"                     
          }
        },
        "sort": [
          "3"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "4",
        "_score": null,
        "_routing": "1",
        "_source": {
          "my_id": "4",
          "text": "This is another answer",
          "my_join_field": {
            "name": "answer",
            "parent": "1"
          }
        },
        "sort": [
          "4"
        ]
      }
    ]
  }
}

此文档属于 question 连接

此文档属于 question 连接

此文档属于 answer 连接

子文档的链接父级 ID

父子连接查询和聚合编辑

有关更多信息,请参阅 has_childhas_parent 查询、children 聚合和 内部命中

join 字段的值可以在聚合和脚本中访问,并且可以使用 parent_id 查询 进行查询

GET my-index-000001/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }
  },
  "aggs": {
    "parents": {
      "terms": {
        "field": "my_join_field#question", 
        "size": 10
      }
    }
  },
  "runtime_mappings": {
    "parent": {
      "type": "long",
      "script": """
        emit(Integer.parseInt(doc['my_join_field#question'].value)) 
      """
    }
  },
  "fields": [
    { "field": "parent" }
  ]
}

查询 parent id 字段(另请参阅 has_parent 查询has_child 查询

parent id 字段进行聚合(另请参阅 children 聚合)

在脚本中访问 parent id 字段。

全局序号编辑

join 字段使用 全局序号 来加速连接。在对分片进行任何更改后,都需要重建全局序号。分片中存储的父级 ID 值越多,重建 join 字段的全局序号所需的时间就越长。

默认情况下,全局序号是急切构建的:如果索引已更改,则 join 字段的全局序号将在刷新过程中重建。这会增加刷新所需的时间。然而,大多数情况下,这是正确的权衡,否则全局序号会在第一次使用父子连接查询或聚合时重建。这可能会给您的用户带来明显的延迟峰值,而且通常情况下,当发生大量写入操作时,可能会在单个刷新间隔内尝试重建 join 字段的多个全局序号,这会使情况变得更糟。

当不经常使用 join 字段并且频繁发生写入操作时,禁用急切加载可能是有意义的

response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: 'answer'
          },
          eager_global_ordinals: false
        }
      }
    }
  }
)
puts response
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
           "question": "answer"
        },
        "eager_global_ordinals": false
      }
    }
  }
}

可以按父关系检查全局序号使用的堆内存量,如下所示

response = client.indices.stats(
  metric: 'fielddata',
  human: true,
  fields: 'my_join_field'
)
puts response

response = client.nodes.stats(
  metric: 'indices',
  index_metric: 'fielddata',
  human: true,
  fields: 'my_join_field'
)
puts response
# Per-index
GET _stats/fielddata?human&fields=my_join_field#question

# Per-node per-index
GET _nodes/stats/indices/fielddata?human&fields=my_join_field#question

每个父级多个子级编辑

也可以为单个父级定义多个子级

response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: [
              'answer',
              'comment'
            ]
          }
        }
      }
    }
  }
)
puts response
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"]  
        }
      }
    }
  }
}

questionanswercomment 的父级。

多级父子连接编辑

我们不建议使用多级关系来复制关系模型。每一级关系都会在查询时增加内存和计算开销。为了获得更好的搜索性能,请对数据进行反规范化。

多级父子关系

response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: [
              'answer',
              'comment'
            ],
            answer: 'vote'
          }
        }
      }
    }
  }
)
puts response
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"],  
          "answer": "vote" 
        }
      }
    }
  }
}

questionanswercomment 的父级

answervote 的父级

上面的映射表示以下树

   question
    /    \
   /      \
comment  answer
           |
           |
          vote

索引孙子文档需要一个等于祖父(lineage 中更大的父级)的 routing

response = client.index(
  index: 'my-index-000001',
  id: 3,
  routing: 1,
  refresh: true,
  body: {
    text: 'This is a vote',
    my_join_field: {
      name: 'vote',
      parent: '2'
    }
  }
)
puts response
PUT my-index-000001/_doc/3?routing=1&refresh 
{
  "text": "This is a vote",
  "my_join_field": {
    "name": "vote",
    "parent": "2" 
  }
}

此子文档必须与其祖父和父级位于同一个分片上

此文档的父级 ID(必须指向 answer 文档)