管理 Elasticsearch 中的關係

本文翻譯自: Managing Relations Inside Elasticsearch | Elastic

在現實世界中,數據不多是簡單的,一般狀況都存在着混亂的交錯關係。數據庫

咱們該如何在 Elasticsearch 中表現文檔( 數據 )間的關係呢?這裏有一些機制,可以爲咱們提供文檔間關係的支持。這些機制有着各自的優點和劣勢,請務必根據不一樣的場景妥善使用。緩存

Inner Objects

最簡單的機制被稱爲「內部對象」。下面是一個嵌入到父級對象中的 JSON 對象:架構

{
    "name":"Zach",
    "car":{
        "make":"Saturn",
        "model":"SL"
    }
}

很簡單吧。car 字段是一個擁有 makemodel 兩個屬性的內部對象。當根對象和內部對象間屬於一對一關係時,這種內部對象的映射關係是有效的。好比:每一個人最多含有一個 carapp

可是,當 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 從根本上就是扁平處理的,因此文檔在內部都會被當作扁平的字段。性能

Nested

做爲內部對象的另外一個選擇,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.nameB.age 同時做爲過濾條件進行查詢。可行的作法是使用 include_in_root,這會高效的將嵌套文檔拷貝到根中,但如此一來,問題又回到了 Inner Objects 的狀況。

Parent/Child

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 父文檔創建了關聯,這使得咱們可使用以下的查詢:

  • Has Parent 過濾 / Has Parent 查詢,可在父文檔中起做用,並返回子文檔
  • Has Child 過濾 / Has Child 查詢,可在子文檔中起做用,並返回父文檔
  • Top Children 查詢,可以返回匹配的前 X 個文檔

因爲子元素或父元素都是第一等類型,咱們能夠像一般狀況同樣單獨請求它們( 只是不能使用關係值 )。

Nested 的最大問題在於其存儲:同一元素的全部內容軍備存儲在同一個 Lucene 塊中。Parent/Child 方式經過分離兩者並使其鬆耦合在一塊兒移除了這一限制。這種方式有利有弊。鬆耦合方式使得咱們能夠自由的更新或刪除父文檔,由於這些操做並不會對父文檔或其餘子文檔產生影響。

Parent/Child 的缺點在於,其表現性能比 Nested 稍差。子文檔被定位到與父文檔相同的 Shard,於是它們仍能得益於分片級的緩存和內存過濾。但由於它們沒有被放在同一個 Lucene 塊中,於是比起 Nested 方式,Parent/Child 方式仍會稍慢。此外,這種方式還會增長必定的內存負載,由於 Elasticsearch 須要在內存中保存管理關係的「join table」。

最後,咱們會發現排序和評分計算是相對困難的。例如,咱們很可貴到到底是哪一個文檔匹配了 Has_child 過濾條件,而僅可以獲得一個父文檔符合條件的文檔。在某些狀況下,這個問題會至關棘手。

逆規範化

有時,最好的作法是在合適的時候簡單地進行數據的逆規範化。Elasticsearch 的確提供了在特定狀況下有效的關係結構支持,但這並不意味着咱們可以使用相似關係型數據庫管理系統所提供的強關係特性。

Elasticsearch 在本質上是扁平的數據架構,於是嘗試去使用關係型數據是有風險的。在部分狀況下,選擇將數據進行逆規範化( 反範式 ),並採用二次查詢的方式得到數據,是最爲明智的選擇。逆規範化能夠說是最爲強大和靈活的。

固然,這會帶來管理成本。咱們須要手動管理數據間的關係,並使用必要的查詢或過濾條件去關聯多樣的類型。

結論和回顧

本文內容相對冗長,如下是簡短的回顧:

Inner Object

  • 簡單、快速、高性能
  • 僅對一對一關係起做用
  • 無需額外的查詢

Nested

  • Nested 文檔被存儲在同一個 Lucene 塊中,於是在同等條件下,Nested 的讀取性能要高於 Parent/Child 類型
  • 對 Nested 文檔中的根元素或子元素進行更新會致使 ES 對整個文檔的更新。對於內容較多的文檔而言,這一過程的代價很大
  • 沒法使用「交叉引用」
  • 對不常常變動的文檔很是適用

Parent/Child

  • 子文檔與父文檔單獨存儲,但仍在同一個分片中。於是其查詢效率比 Nested 稍差
  • 因爲 ES 須要在內存中管理 「join」列表,於是會增長一些內存負載
  • 更新子文檔不會影響父文檔或其餘子文檔,這會在大文檔中潛在地節省大量的索引消耗
  • 由於 Has Child/Has Parent 操做有時是不可見的,於是排序和評分操做時難以進行的

逆規範化

  • 須要本身管理全部的關係
  • 更爲靈活,但也會有最大的管理成本
  • 基於具體的設置,性能會或多或少的有所損耗
相關文章
相關標籤/搜索