赞
踩
在关系数据库中,子表使用外键引用父表,这种关系称为 join。 设计通常涉及 规范化数据。Elasticsearch 不是关系数据库,它全与搜索效率而不是存储效率有关。 存储的数据已被 去规范化 并且几乎是平坦的。 这意味着 join不能跨索引,Elasticsearch 的重点在于速度,而传统 join 的运行速度太慢。 因此,子文档和父文档都必须位于相同的索引和相同的分片中。
让我们考虑下图1所示的家谱。该树有 3 个父母和 9 个孩子。 每个角色都有 “gender” 和 “isAlive” 状态。
在上面的示例中,我们探索以下场景:
以下代码有为上述关系创建索引:
- 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"
- }
- }
- }
- }
- }

在上面:
在运行脚本以插入上图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"
- }
- }

同样,让我们先遍历一个子插件,然后运行上图所示的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 中。
现在是执行和理解的有趣部分,我们可以在刚刚创建的关系上运行查询。
- 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 文档。
- 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 的记录。
查询关键字 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” 的父母。
- 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。
让我们将 “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 的 “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" }}
获取所有拥有孙女的祖父母:
- 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
现在,我们已经看到了加入功能的作用,让我们回顾一下上面注意到的限制。
当索引时间性能比搜索时间性能更重要时,父子联接可能是管理关系的有用技术,但代价是很高的。 必须意识到折衷方案,例如父子文档的物理存储约束和增加的复杂性。 另一个预防措施是避免多层父子关系,因为这将消耗更多的内存和计算量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。