本文翻譯自: 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 在本質上是扁平的數據架構,於是嘗試去使用關係型數據是有風險的。在部分狀況下,選擇將數據進行逆規範化( 反範式 ),並採用二次查詢的方式得到數據,是最爲明智的選擇。逆規範化能夠說是最爲強大和靈活的。
固然,這會帶來管理成本。咱們須要手動管理數據間的關係,並使用必要的查詢或過濾條件去關聯多樣的類型。
本文內容相對冗長,如下是簡短的回顧: