理解elasticsearch的parent-child關係

前面文章介紹了,在es裏面的幾種數據組織關係,包括array[object],nested,以及今天要說的Parent-Child。app

Parent-Child與Nested很是相似,均可以用來處理一對多的關係,若是多對多的關係,那就拆分紅一對多在處理。前面提到nested的缺點是對數據的更新須要reindex整個nested結構下的全部數據,因此註定了它的使用場景必定是查詢多更新少的場景,若是是更新多的場景,那麼nested的性能未必會很好,而Parent-Child就很是適合在更新多的場景,由於Parent-Child的數據存儲都是獨立的,只要求父子文檔都分佈在同一個shard裏面便可而nested模式下,不只要求在同一個shard下還必須是同一個sengment裏面的同一個block下,這種模式註定了nested查詢的性能要比Parent-Child好,可是更新性能就大大不如Parent-Child了,對比nested模式,Parent-Child主要有下面的幾個特色:elasticsearch

(1) 父文檔能夠被更新,而無須重建全部的子文檔性能

(2)子文檔的添加,修改,或者刪除不影響它的父文檔和其餘的子文檔,這尤爲是在子文檔數量巨大並且須要被添加和更新頻繁的場景下Parent-Child能獲取更好的性能測試

(3)子文檔能夠被返回在搜索結果裏面設計

ElasticSearch在內存裏面維護了一個父子關係的映射表,以便於可以加速查詢,這種映射使用的是doc-value,若是數據量巨大內存放不下,會自動的保存到磁盤中,固然此時性能也會降低。code

下面來看一個例子,首先咱們要定義mapping:排序

{
  "order": 0,
  "template": "pc_test*",
  "settings": {
    "index": {
      "number_of_replicas": "0",
      "number_of_shards": "3"
    }
  },
  "mappings": {
    "employee": {
      "_parent": {
        "type": "branch"
      }
    },
    "branch": {}
  },
  "aliases": {}
}

branch:表明一個分公司內存

employee:表明員工ci

關係: 一個公司能夠包含多個員工路由

下面開始插入數據,首先咱們先插入公司數據:

POST /company/branch/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster", "city": "London", "country": "UK" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central", "city": "Liverpool", "country": "UK" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Élysées", "city": "Paris", "country": "France" }

注意插入公司數據的type是branch,數據的id用的是city字段,

添加員工數據的時候,要指定的父文檔是屬於哪一個,這樣才能把父子數據給關聯到同一臺機器上。

PUT /company/employee/1?parent=london 
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

parent id字段有兩個用途:

(1)它建立了鏈接父子文檔的關係而且確保了子文檔必定和父文檔存在一個shard裏面

(2)默認狀況下es用的是文檔的id字段進行hash取模分片的,若是父文檔的id字段被指定,那麼路由字段就是id,而在子文檔中咱們指定parent的值也是父文檔的id字段,因此就必定確保了父子文檔都在一個shard裏面,在父子文檔的關係中,index,update,add,delete包括search在使用的時候都必須設置路由字段,不然查詢結果會出錯。

繼續插入子文檔:

POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "london" }}
{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }
{ "index": { "_id": 3, "parent": "liverpool" }}
{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }
{ "index": { "_id": 4, "parent": "paris" }}
{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }

注意:若是parent的值改變了,必須刪除這個parent下面的全部子文檔而後刪除自己,最後添加新的父文檔,再添加新的子文檔,不然parent值改變後,父文檔的parent改變了,子的沒改變會出現父子不在同一個shard裏面,從而致使查詢出錯。

下面來看下,如何查詢父子關係的數據,這裏面主要有兩個查詢方法:

(1)has_child

使用子文檔的字段當成查詢條件,查詢出符合條件的父文檔的數據

一個查詢例子以下:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "dob": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}

這裏面關於父文檔的score,是由全部子文檔的評分經過一個計算方法得來的,這裏能夠設置,有5種策略:

none:忽略評分 avg:全部子文檔的平均分 min:全部子文檔的最小分 max:全部子文檔的最大分 sum:全部子文檔的得分和

經過下面的查詢,能夠看出評分對排序的影響:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":       "employee",
      "score_mode": "max",
      "query": {
        "match": {
          "name": "Alice Smith"
        }
      }
    }
  }
}

得分設置爲none擁有更快的查詢性能,由於少了額外的計算

此外has_child查詢還能夠接受兩個限制參數min_children和max_children,在查詢的時候根據子文檔的個數作過濾,看下面的一個例子:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2, 
      "query": {
        "match_all": {}
      }
    }
  }
}

上面的查詢僅僅查詢最子文檔個數符合過濾條件的父文檔,has_child也可使用filter查詢。

(2)has_parent

has_parent查詢和has_child相反,經過查詢父文檔的字段,從而獲得子文檔的數據。

一個例子以下:

GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch", 
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}

has_parent也支持score_mode,有兩種設置一個none,一個score由於每一個child只有一個parent,因此不須要作聚合的評分。

最後看下parent-child的聚合,一個例子:

GET /company/branch/_search
{
  "size" : 0,
  "aggs": {
    "country": {
      "terms": { 
        "field": "country"
      },
      "aggs": {
        "employees": {
          "children": { 
            "type": "employee"
          },
          "aggs": {
            "hobby": {
              "terms": { 
                "field": "hobby"
              }
            }
          }
        }
      }
    }
  }
}

上面聚合的意思是:

按國家分組,而後算組內的員工再根據其愛好進行分組

最後,parent-child模式,支持多層的關係

一個對多對多,目前官網上給出了3層關係的例子,從社區上來看說是支持無限層級的關係映射,可是超過3層的映射,官網沒有給出使用例子,具體的使用還得使用者去測試,不過現實狀況包含3級以上的關係數據應該很是少了。

一個的3級例子的mapping:

PUT /company
{
  "mappings": {
    "country": {},
    "branch": {
      "_parent": {
        "type": "country" 
      }
    },
    "employee": {
      "_parent": {
        "type": "branch" 
      }
    }
  }
}

多了一級國家的映射,整體的關係是:

一個國家能夠有多個分公司,每一個分公司又能夠有多個員工

看下,數據例子:

(1)先插入國家數據

POST /company/country/_bulk
{ "index": { "_id": "uk" }}
{ "name": "UK" }
{ "index": { "_id": "france" }}
{ "name": "France" }

(2)在插入公司數據

POST /company/branch/_bulk
{ "index": { "_id": "london", "parent": "uk" }}
{ "name": "London Westmintster" }
{ "index": { "_id": "liverpool", "parent": "uk" }}
{ "name": "Liverpool Central" }
{ "index": { "_id": "paris", "parent": "france" }}
{ "name": "Champs Élysées" }

注意parent是父的,公司的route用的是city

(3)插入員工數據

PUT /company/employee/1?parent=london&routing=uk 
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

第三層的插入數據用了parent字段來確保和父文檔的關聯,又用了routding字段來確保和父文檔,祖父文檔位於同一個shard裏面。

注意若是超過3層,routing字段必定最頂層的文檔的路由值,而parent字段則是其真正的關聯的父文檔。超過3層的映射官網沒有給出例子,具體是不是那樣用的,有興趣的朋友能夠自行測試,多層的父子關係會消耗更多的內存,以及性能更糟糕因此設計上應該儘可能避免出現這種狀況,此外若是非得設計,注意parent id字段應該儘可能短的,從而在doc value中獲的更好的壓縮以減小使用的內存。

https://discuss.elastic.co/t/would-it-be-possible-the-relation-grate-grandparent-grate-grandchild-in-elasticsearch/26875/4

相關文章
相關標籤/搜索