在关系数据库中,子表使用外键引用父表,这种关系称为 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 字段添加新关系。
也可以将子元素添加到现有元素中,但前提是该元素已经是父元素。
当索引时间性能比搜索时间性能更重要时,父子联接可能是管理关系的有用技术,但代价是很高的。 必须意识到折衷方案,例如父子文档的物理存储约束和增加的复杂性。 另一个预防措施是避免多层父子关系,因为这将消耗更多的内存和计算量。