在Elasticsearch中,Join可讓咱們建立parent/child關係。Elasticsearch不是一個RDMS。一般join數據類型儘可能不要使用,除非不得已。那麼Elasticsearch爲何須要Join數據類型呢?html
在Elasticsearch中,更新一個object須要root object一個完整的reindex:數據庫
一般狀況下,這是徹底OK的,可是在有些場合下,若是咱們有頻繁的更新操做,這樣可能對性能帶來很大的影響。app
若是你的數據須要頻繁的更新,並帶來性能上的影響,這個時候,join數據類型多是你的一個解決方案。elasticsearch
join數據類型能夠徹底地把兩個object分開,可是仍是保持這二者以前的關係。ide
與 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的名稱。
join字段不該像關係數據庫中的鏈接同樣使用。 在Elasticsearch中,良好性能的關鍵是將數據去規範化爲文檔。 每一個鏈接字段has_child或has_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的文檔
能夠在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,好比:
PUT my_index { "mappings": { "properties": { "my_join_field": { "type": "join", "relations": { "question": ["answer", "comment"] } } } } }
在這裏,question是answer及comment的parent。
雖然這個不建議,這樣作可能會可能在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