本文翻译自: Managing Relations Inside Elasticsearch | Elastic
在现实世界中,数据不多是简单的,一般状况都存在着混乱的交错关系。数据库
咱们该如何在 Elasticsearch 中表现文档( 数据 )间的关系呢?这里有一些机制,可以为咱们提供文档间关系的支持。这些机制有着各自的优点和劣势,请务必根据不一样的场景妥善使用。缓存
最简单的机制被称为「内部对象」。下面是一个嵌入到父级对象中的 JSON 对象:架构
{ "name":"Zach", "car":{ "make":"Saturn", "model":"SL" } }
很简单吧。car
字段是一个拥有 make
和 model
两个属性的内部对象。当根对象和内部对象间属于一对一关系时,这种内部对象的映射关系是有效的。好比:每一个人最多含有一个 car
。app
可是,当 Zach
拥有两个 car
,而 Bob
只有一个 car
时呢?curl
{ "name" : "Zach", "car" : [ { "make" : "Saturn", "model" : "SL" }, { "make" : "Subaru", "model" : "Imprezza" } ] } { "name" : "Bob", "car" : [ { "make" : "Saturn", "model" : "Imprezza" } ] }
请忽略 Saturn 公司从未生产过 Imprezza 型汽车的问题,考虑一下,当咱们试图在 ES 中检索的时候会发生什么呢?因为只有 Bob 拥有 Saturn Imprezza
,因此咱们能够建立一个查询:elasticsearch
query: car.make=Saturn AND car.model=Imprezza
这样对吗?好吧,这样的查询结果并不会如咱们所愿。若是执行这条查询语句,咱们将会获得所有两条文档。这是因为 Elasticsearch 在内部将内部对象降维成了单个对象。于是 Zach
这条文档其实是这样的:ide
{ "name" : "Zach", "car.make" : ["Saturn", "Subaru"] "car.model" : ["SL", "Imprezza"] }
这就解释了为何上述查询会返回那样的结果。ELasticsearch 从根本上就是扁平处理的,因此文档在内部都会被当作扁平的字段。性能
做为内部对象的另外一个选择,Elasticsearch 提供了「嵌套类型」的概念。嵌套文档在文档层面和内部对象是相同的,可是它提供了内部对象没有的功能( 也包括一些限制 )。url
嵌套文档的例子以下:翻译
{ "name" : "Zach", "car" : [ { "make" : "Saturn", "model" : "SL" }, { "make" : "Subaru", "model" : "Imprezza" } ] }
在映射层面,嵌套类型必须显式的声明( 不一样于内部对象,能够自动检测 ):
{ "person":{ "properties":{ "name" : { "type" : "string" }, "car":{ "type" : "nested" } } } }
Inner Objects 的问题在于,每个嵌套的 JSON 对象并不会被认为是文档中的单独组件。相反的,它们会与其余 Inner Objects 合并,并共享相同的属性名。
而这一问题并不会在 Nested 文档中出现。每个 Nested 文档都会保持独立,于是咱们可使用 car.make=Saturn AND car.model=Imprezza
而不会遇到意外问题。
Elasticsearch 从根本上还是扁平的,但它在内部管理着 Nested 关系,使其可以表现嵌套层次。当咱们建立一个 Nested 文档时,Elasticsearch 实际上添加了两个独立的文档( 根对象和嵌套对象 ),而后再内部将其关联。上述两个文档均被存储在同一 Shard 上的同一个 Lucene 块中,于是读取性能仍旧很是迅速。
这种安排也同时带来了一些弊端。最明显的在于,咱们只能经过特殊的「嵌套查询」才能访问嵌套文档。另外一个问题会在咱们试图对文档的根对象或其子对象的更新操做时出现。
由于 Nested 文档被存储在同一个 Lucene 块中,而 Lucene 不容许在段上的随机写操做,因此对 Nested 文档中某个字段的鞥更新操做将会致使整个文档的索引重建。
索引重建的目标包括根及其嵌套的子对象,即便它们并无被修改。在内部,Elasticsearch 会将就文档标记为删除,更新字段,并将文档的所有内容重建索引值新的 Lucene 块中。若是 Nested 文档数据频繁更新的话,因索引重建而致使的性能消耗便不能被忽视。
此外,在 Nested 文档间使用交叉引用是不可行的。一个 Nested 对象的属性对另外一个 Nested 对象是不可见的。例如,咱们不能使用 A.name
和 B.age
同时做为过滤条件进行查询。可行的作法是使用 include_in_root
,这会高效的将嵌套文档拷贝到根中,但如此一来,问题又回到了 Inner Objects 的状况。
Elasticsearch 提供的最后一种方式是使用 Parent/Child 类型。这种模式相比 Nested 嵌套类,属于更为松散的耦合,而且给咱们提供了更多强大的查询方式。看咱们来看个例子,在这个例子中,一我的具备多个家庭( 在不一样的状况下 )。父元素像一般同样具备 mapping 以下:
{ "mappings":{ "person":{ "name":{ "type":"string" } } } }
子元素在父元素以外,有着本身的 mapping,且含有特殊的 _parent
属性集
{ "homes":{ "_parent":{ "type" : "person" }, "state" : { "type" : "string" } } }
_parent
字段向 Elasticsearch 声明了 Employers
类型文档是 Person
类型的自雷。咱们能够很是容易的向文档中加入此类型的数据。咱们能够像一般状况同样添加父类型文档:
$ curl -XPUT localhost:9200/test/person/zach/ -d' { "name" : "Zach" }
添加子类型文档与一般略有不一样,咱们须要在在请求参数中指定该子文档所属于的父文档( 在这个例子中是 zach
,这个值是咱们在上面添加父文档时所指定的文档 ID ):
$ curl -XPOST localhost:9200/homes?parent=zach -d' { "state" : "Ohio" } $ curl -XPOST localhost:9200/test/homes?parent=zach -d' { "state" : "South Carolina" }
上述两个文档如今都以与 zach
父文档创建了关联,这使得咱们可使用以下的查询:
因为子元素或父元素都是第一等类型,咱们能够像一般状况同样单独请求它们( 只是不能使用关系值 )。
Nested 的最大问题在于其存储:同一元素的全部内容军备存储在同一个 Lucene 块中。Parent/Child 方式经过分离两者并使其松耦合在一块儿移除了这一限制。这种方式有利有弊。松耦合方式使得咱们能够自由的更新或删除父文档,由于这些操做并不会对父文档或其余子文档产生影响。
Parent/Child 的缺点在于,其表现性能比 Nested 稍差。子文档被定位到与父文档相同的 Shard,于是它们仍能得益于分片级的缓存和内存过滤。但由于它们没有被放在同一个 Lucene 块中,于是比起 Nested 方式,Parent/Child 方式仍会稍慢。此外,这种方式还会增长必定的内存负载,由于 Elasticsearch 须要在内存中保存管理关系的「join table」。
最后,咱们会发现排序和评分计算是相对困难的。例如,咱们很可贵到到底是哪一个文档匹配了 Has_child
过滤条件,而仅可以获得一个父文档符合条件的文档。在某些状况下,这个问题会至关棘手。
有时,最好的作法是在合适的时候简单地进行数据的逆规范化。Elasticsearch 的确提供了在特定状况下有效的关系结构支持,但这并不意味着咱们可以使用相似关系型数据库管理系统所提供的强关系特性。
Elasticsearch 在本质上是扁平的数据架构,于是尝试去使用关系型数据是有风险的。在部分状况下,选择将数据进行逆规范化( 反范式 ),并采用二次查询的方式得到数据,是最为明智的选择。逆规范化能够说是最为强大和灵活的。
固然,这会带来管理成本。咱们须要手动管理数据间的关系,并使用必要的查询或过滤条件去关联多样的类型。
本文内容相对冗长,如下是简短的回顾: