Elasticsearch: Join數據類型

在Elasticsearch中,Join可讓咱們建立parent/child關係。Elasticsearch不是一個RDMS。一般join數據類型儘可能不要使用,除非不得已。那麼Elasticsearch爲何須要Join數據類型呢?html

在Elasticsearch中,更新一個object須要root object一個完整的reindex:數據庫

  • 即便是一個field的一個字符的改變
  • 即使是nested object也須要完整的reindex才能夠實現搜索

一般狀況下,這是徹底OK的,可是在有些場合下,若是咱們有頻繁的更新操做,這樣可能對性能帶來很大的影響。app

若是你的數據須要頻繁的更新,並帶來性能上的影響,這個時候,join數據類型多是你的一個解決方案。elasticsearch

join數據類型能夠徹底地把兩個object分開,可是仍是保持這二者以前的關係。ide

  1. parent及child是徹底分開的兩個文檔
  2. parent能夠單獨更新而不須要從新reindex child
  3. children能夠任意被添加/串改/刪除而不影響parent及其它的children

與 nested類型相似,父子關係也容許您將不一樣的實體關聯在一塊兒,但它們在實現和行爲上有所不一樣。 與nested文檔不一樣,它們不在同一文檔中,而parent/child文檔是徹底獨立的文檔。 它們遵循一對多關係原則,容許您將一種類型定義爲parent類型,將一種或多種類型定義爲child類型性能

即使join數據類型給咱們帶來了方便,可是,它也在搜索時給我帶來額外的內存及計算方便的開銷。ui

注意:目前Kibana對nested及join數據類型有比較少的支持。若是你想使用Kibana來在dashboard裏展現數據,這個方面的你須要考慮。在將來,這種狀況可能會發生改變。spa

**join數據類型是一個特殊字段,用於在同一索引的文檔中建立父/子關係。 關係部分定義文檔中的一組可能關係,每一個關係是父(parent)名稱和子(child)名稱。 **code

一個例子:htm

PUT my_index
    {
      "mappings": {
        "properties": {
          "my_join_field": { 
            "type": "join",
            "relations": {
              "question": "answer" 
            }
          }
        }
      }
    }

在這裏咱們定義了一個叫作my_index的索引。在這個索引中,咱們定義了一個field,它的名字是my_join_field。它的類型是join數據類型。同時咱們定義了單個關係:question是answer的parent。

要使用join來index文檔,必須在source中提供關係的name和文檔的可選parent。 例如,如下示例在question上下文中建立兩個parent文檔:

PUT my_index/_doc/1?refresh
    {
      "text": "This is a question",
      "my_join_field": {
        "name": "question" 
      }
    }
     
    PUT my_index/_doc/2?refresh
    {
      "text": "This is another question",
      "my_join_field": {
        "name": "question"
      }
    }

這裏採用refresh來強制進行索引,以便接下來的搜索。在這裏name標識question,說明這個文檔時一個question文檔。

索引parent文檔時,您能夠選擇僅將關係的名稱指定爲快捷方式,而不是將其封裝在普通對象表示法中:

PUT my_index/_doc/1?refresh
    {
      "text": "This is a question",
      "my_join_field": "question" 
    }
     
    PUT my_index/_doc/2?refresh
    {
      "text": "This is another question",
      "my_join_field": "question"
    }

這種方法和前面的是同樣的,只是這裏咱們只使用了question, 而不是一個像第一種方法那樣,使用以下的一個對象來表達:

"my_join_field": {
        "name": "question"
      }

在實際的使用中,你能夠根據本身的喜愛來使用。

索引child項時,必須在_source中添加關係的名稱以及文檔的parent id。

注意:須要在同一分片中索引父級的譜系,必須使用其parent的id來確保這個child和parent是在一個shard中。每一個文檔分配在那個shard之中在默認的狀況下是按照文檔的id進行一些hash來分配的,固然也能夠經過routing來進行。針對child,咱們使用其parent的id,這樣就能夠保證。不然在咱們join數據的時候,跨shard是很是大的一個消費。

例如,如下示例顯示如何索引兩個child文檔:

PUT my_index/_doc/3?routing=1?refresh  (1)
    {
      "text": "This is an answer",
      "my_join_field": {
        "name": "answer",   (2)
        "parent": "1"       (3)
      }
    }
     
    PUT my_index/_doc/4?routing=1?refresh
    {
      "text": "This is another answer",
      "my_join_field": {
        "name": "answer",
        "parent": "1"
      }
    }

在上面的(1)處,咱們必須使用routing,這樣能確保parent和child是在同一個shard裏。咱們這裏routing爲1,這是由於parent的id 爲1,在(3)處定義。(2) 處定義了該文檔join的名稱。

parent-join及其性能

join字段不該像關係數據庫中的鏈接同樣使用。 在Elasticsearch中,良好性能的關鍵是將數據去規範化爲文檔。 每一個鏈接字段has_child或has_parent查詢都會對查詢性能產生重大影響。

join字段有意義的惟一狀況是,若是您的數據包含一對多關係,其中一個實體明顯超過另外一個實體。 這種狀況的一個例子是產品的用例和這些產品的報價。 若是提供的產品數量明顯多於產品數量,則將產品建模爲父文檔並將產品建模爲子文檔是有意義的。

parent-join的限制

  • 對於每一個index來講,只能有一個join字段
  • parent及child文檔,必須是在一個shard裏創建索引。這也意味着,一樣的routing值必須應用於getting, deleting或updating一個child文檔。
  • 一個元素能夠有多個children,可是隻能有一個parent.
  • 能夠對已有的join項添加新的關係
  • 也能夠將child添加到現有元素,但僅當元素已是parent時才能夠。

針對parent-join的搜索

parent-join建立一個字段來索引文檔中關係的名稱(my_parent,my_child,...)。

它還爲每一個parent/child關係建立一個字段。 此字段的名稱是join字段的名稱,後跟和關係中parent的名稱。 所以,例如對於my_parent⇒[my_child,another_child]關係,join字段會建立一個名爲my_join_field#my_parent的附加字段。

若是文檔是子文件(my_child或another_child),則此字段包含文檔連接到的parent_id,若是文檔是parent文件(my_parent),則包含文檔的_id。

搜索包含join字段的索引時,始終在搜索響應中返回這兩個字段:

上面的描述比較繞口,咱們仍是以一個例子來講說明吧:

GET my_index/_search
    {
      "query": {
        "match_all": {}
      },
      "sort": ["_id"]
    }

這裏咱們搜索全部的文檔,並以_id進行排序:

{
      "took" : 2,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 4,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : null,
            "_source" : {
              "text" : "This is a question",
              "my_join_field" : "question" (1)
            },
            "sort" : [
              "1"
            ]
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "2",
            "_score" : null,
            "_source" : {
              "text" : "This is another question",
              "my_join_field" : "question" (2)
            },
            "sort" : [
              "2"
            ]
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "3",
            "_score" : null,
            "_routing" : "1",
            "_source" : {
              "text" : "This is an answer",
              "my_join_field" : {
                "name" : "answer", (3)
                "parent" : "1"     (4)
              }
            },
            "sort" : [
              "3"
            ]
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "4",
            "_score" : null,
            "_routing" : "1",
            "_source" : {
              "text" : "This is another answer",
              "my_join_field" : {
                "name" : "answer",
                "parent" : "1"
              }
            },
            "sort" : [
              "4"
            ]
          }
        ]
      }
    }

在這裏,咱們能夠看到4個文檔:

(1)代表這個文檔是一個question join (2)代表這個文檔是一個question join (3)代表這個文檔是一個answer join (4)代表這個文檔的parent是id爲1的文檔

Parent-join 查詢及aggregation

能夠在aggregation和script中訪問join字段的值,並可使用parent_id查詢進行查詢:

GET my_index/_search
    {
      "query": {
        "parent_id": { 
          "type": "answer",
          "id": "1"
        }
      }
    }

咱們經過查詢parent_id,返回全部parent_id爲1的全部answer類型的文檔:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.35667494,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.35667494,
        "_routing" : "1",
        "_source" : {
          "text" : "This is another answer",
          "my_join_field" : {
            "name" : "answer",
            "parent" : "1"
          }
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.35667494,
        "_routing" : "1",
        "_source" : {
          "text" : "This is an answer",
          "my_join_field" : {
            "name" : "answer",
            "parent" : "1"
          }
        }
      }
    ]
  }
}

在這裏,咱們能夠看到返回id爲3和4的文檔。咱們也能夠對這些文檔進行aggregation:

GET my_index/_search
    {
      "query": {
        "parent_id": {
          "type": "answer",
          "id": "1"
        }
      },
      "aggs": {
        "parents": {
          "terms": {
            "field": "my_join_field#question",
            "size": 10
          }
        }
      },
      "script_fields": {
        "parent": {
          "script": {
            "source": "doc['my_join_field#question']"
          }
        }
      }
    }

就像咱們在上一節中介紹的那樣, 在咱們的應用實例中,在index時,它也建立一個額外的一個字段,雖然在source裏咱們看不到。這個字段就是my_join_filed#question,這個字段含有parent _id。在上面的查詢中,咱們首先查詢全部的parent_id爲1的全部的answer類型的文檔。接下來對全部的文檔以parent_id進行聚合:

{
      "took" : 0,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 2,
          "relation" : "eq"
        },
        "max_score" : 0.35667494,
        "hits" : [
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "4",
            "_score" : 0.35667494,
            "_routing" : "1",
            "fields" : {
              "parent" : [
                "1"
              ]
            }
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "3",
            "_score" : 0.35667494,
            "_routing" : "1",
            "fields" : {
              "parent" : [
                "1"
              ]
            }
          }
        ]
      },
      "aggregations" : {
        "parents" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [
            {
              "key" : "1",
              "doc_count" : 2
            }
          ]
        }
      }
    }

一個parent對應多個child

對於一個parent來講,咱們能夠定義多個child,好比:

PUT my_index
    {
      "mappings": {
        "properties": {
          "my_join_field": {
            "type": "join",
            "relations": {
              "question": ["answer", "comment"]  
            }
          }
        }
      }
    }

在這裏,question是answer及comment的parent。

多層的parent join

雖然這個不建議,這樣作可能會可能在query時帶來更多的內存及計算方面的開銷:

PUT my_index
    {
      "mappings": {
        "properties": {
          "my_join_field": {
            "type": "join",
            "relations": {
              "question": ["answer", "comment"],  
              "answer": "vote" 
            }
          }
        }
      }
    }

這裏question是answer及comment的parent,同時answer也是vote的parent。它代表了以下的關係:

索引grandchild文檔需routing值等於grand-parent(譜系裏的更大parent):

PUT my_index/_doc/3?routing=1&refresh 
    {
      "text": "This is a vote",
      "my_join_field": {
        "name": "vote",
        "parent": "2" 
      }
    }

這個child文檔必須是和他的grand-parent在一個shard裏。在這裏它使用了1,也即question的id。同時,對於vote來講,它的parent必須是它的parent,也即answer的id。

更多參考:https://www.elastic.co/guide/en/elasticsearch/reference/7.3/parent-join.html

相關文章
相關標籤/搜索