Elasticsearch 中 Parent-Child 關系

Neil Zhu,簡書ID Not_GOD姜性,University AI 創(chuàng)始人 & Chief Scientist猪叙,致力于推進世界人工智能化進程。制定并實施 UAI 中長期增長戰(zhàn)略和目標夹囚,帶領團隊快速成長為人工智能領域最專業(yè)的力量纵刘。
作為行業(yè)領導者,他和UAI一起在2014年創(chuàng)建了TASA(中國最早的人工智能社團), DL Center(深度學習知識中心全球價值網(wǎng)絡)荸哟,AI growth(行業(yè)智庫培訓)等假哎,為中國的人工智能人才建設輸送了大量的血液和養(yǎng)分。此外鞍历,他還參與或者舉辦過各類國際性的人工智能峰會和活動舵抹,產(chǎn)生了巨大的影響力,書寫了60萬字的人工智能精品技術內(nèi)容劣砍,生產(chǎn)翻譯了全球第一本深度學習入門書《神經(jīng)網(wǎng)絡與深度學習》惧蛹,生產(chǎn)的內(nèi)容被大量的專業(yè)垂直公眾號和媒體轉(zhuǎn)載與連載。曾經(jīng)受邀為國內(nèi)頂尖大學制定人工智能學習規(guī)劃和教授人工智能前沿課程秆剪,均受學生和老師好評赊淑。

parent-child 關系

類似于 nested model:可以關聯(lián)兩個實體。不同在于仅讽,nested object 中所有的實體必須存在同一個文檔中陶缺,而在 parent-child 中,parent 和 children 可以是完全分開的文檔洁灵。

parent-child 功能讓我們可以將一種文檔類型以一對多的關系關聯(lián)到另一個上饱岸。相比 nested object 的好處在于:

  1. parent 文檔可以不需要重新索引 children 進行更新。
  2. child 文檔可以被添加徽千、修改或者刪除苫费,而不影響 parent 或者其他 children。這在 child 文檔很多和增改頻率很高的時候尤其有用双抽。
  3. child 文檔可以被作為搜索請求的結(jié)果返回百框。

Elasticsearch 維護了一個 parent 到 children 的映射。所以在查詢時刻的連接(join)會很快牍汹,但是這樣也給 parent-child 關系帶來了限制:parent 和所有 children 必須處在一個分片上铐维。

parent-child ID 映射作為字段數(shù)據(jù)存放在內(nèi)存中柬泽。后期會有計劃將這個默認設置改成使用 doc values。

parent-child 映射

為了建立 parent-child 關系的需求是指定哪種文檔類型應該是 child 類型的 parent嫁蛇。這個必須在?索引創(chuàng)建時刻?指定锨并,或者使用 update-mapping API 在 child 類型被創(chuàng)建前指定。

假設睬棚,我們一家公司在很多城市都有自己的分部第煮。我們希望將員工和他們工作的地址關聯(lián)。我們需要搜索分部抑党、員工個人包警,和為特定分部工作的員工,所以 nested 模型就沒有作用了新荤。當然揽趾,我們是可以使用 application-side-join 或者 data denormalization台汇,但這里我們試試 parent-child苛骨。

現(xiàn)在我們必須要做的是告訴 Elasticsearch employee 類型將 branch 文檔類型作為其 _parent,這個我們可以在創(chuàng)建索引的時候指定:

PUT /company
{
  "mappings": {
    "branch": {},
    "employee": {
      "_parent": {
        "type": "branch"  ...1
      }
    }
  }
}
  1. 類型為 employee 的文檔是 類型 branch 的 children苟呐。

索引 parents 和 children

索引 parent 文檔跟以前一樣痒芝。parents 不需要知道任何關于其 children 的信息:

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" }

在索引 children 文檔時,你必須指定關聯(lián)的 parent 文檔的 ID:

PUT /company/employee/1?parent=london  ...1
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}
  1. employee 文檔是 london 分部的 child

parent ID 有兩個作用:創(chuàng)建了 parent 和 child 之間的關聯(lián)牵素,確保 child 文檔存在同一個分片上严衬。在 Routing a Document to a Shard 中,我們解釋了 Elasticsearch 如何使用一個路由值笆呆,默認是文檔的 _id 來確定文檔應該屬于哪個分片请琳。路由值插入到下面的公式中:

shard = hash(routing) % number_of_primary_shards

然而,如果 parent ID 指定了赠幕,路由值就是 parent ID 而不再是 _id 了俄精。換言之,parent 和 child 使用了同樣的路由值——parent 的_id —— 所以他們會同樣存在一個分片上榕堰。

在使用 GET 請求檢索 child 文檔竖慧,或者索引、更新或者刪除 child 文檔時parent ID 需要根據(jù)所有單個文檔請求指定逆屡。不像搜索請求圾旨,會被轉(zhuǎn)發(fā)給一個索引中的所有分片,這些單個文檔的請求只會轉(zhuǎn)發(fā)給那個包含對應文檔的分片——如果 parent ID 沒有指定魏蔗,這些請求可能就會被轉(zhuǎn)發(fā)到錯誤的分片上砍的。

parent ID 在使用 bulk API 時也應該指定:

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" }

如果你想改變 child 文檔的 parent 值,僅僅重新索引或者更新 child 文檔是不夠的——新的 parent 文檔可能會在不同的分片上莺治。所以廓鞠,你必須刪除舊的 child 文檔味混,然后索引新的 child。

通過 children 找到 parents

has_child 查詢和過濾器可以用來根據(jù) children 的內(nèi)容找到 parent 文檔诫惭。例如翁锡,我們可以找到所有包含出生在 1980 后的員工的分部:

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

如同 nested query,has_child 查詢可以匹配多個 child 文檔夕土,每個都有相應的相關分數(shù)馆衔。這些分數(shù)如何化歸為針對 parent 文檔的單獨分數(shù)取決于 score_mode 參數(shù)。默認設置是 none怨绣,這會忽視 child 分數(shù)并給 parents 分配了 1.0 的分值角溃,不過這里也可以使用 avgmin篮撑,maxsum减细。

下面的查詢將會返回 londonliverpool,但是 london 會有更高的分數(shù)赢笨,因為 Alice SmithBarry Smith 更好地匹配:

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

默認 score_modenone未蝌,該設置明顯快于其他模式,因為 Elasticsearch 不需要計算每個 child 文檔的分值茧妒。只有在你在乎分值的時候才需要根據(jù)需要設置模式萧吠。

min_children 和 max_children

has_child 查詢和過濾器都接受 min_childrenmax_children 參數(shù),僅當匹配 children 的數(shù)量在指定的范圍內(nèi)會返回 parent 文檔桐筏。

這個查詢將會匹配有至少兩位員工的分部:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2,  ...1
      "query": {
        "match_all": {}
      }
    }
  }
}
  1. 分部必須有至少兩位員工才能匹配

min_childrenmax_children 參數(shù)的 has_child 查詢或者過濾器的性能和啟用計分的 has_child 查詢相同纸型。

has_child 過濾器


has_child 過濾器和 has_child 查詢工作機制差不多,只是這里不會支持 score_mode 參數(shù)梅忌。就和其他的過濾器類似:包含或者不包含狰腌,并不計分。
has_child 過濾器的結(jié)果并不緩存牧氮,通常的緩存規(guī)則應用在 has_child 過濾器內(nèi)部的 filter 上琼腔。

通過 parents 尋找 children

nested 查詢只會返回根文檔作為結(jié)果,parent-child 文檔本身是獨立的蹋笼,每個可以獨立地進行查詢展姐。has_child 查詢允許我們返回基于在其 children 的數(shù)據(jù)上 parents 文檔,has_parent 查詢則是基于 parents 的數(shù)據(jù)返回 children剖毯。

看起來和 has_child 很像圾笨。這個例子返回了在 UK 工作的員工:

GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch", 
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}
  1. 返回有類型為 branch 的 children

has_children 查詢也支持 score_mode,但是僅僅會接受兩個設置:none(默認)和 score逊谋。每個 child 僅僅有 1 個 parent擂达,所以沒有必要去將多個分數(shù)化歸為單個的分數(shù)。選擇就是 scorenone 這兩者胶滋。

has_parent 過濾器


has_parent 過濾器和 has_parent 查詢工作機制相同板鬓,除了它不支持 score_mode 參數(shù)悲敷。僅僅可以用在過濾器中。
has_parent 過濾器的結(jié)果并不緩存俭令,通常的緩存機制用在 has_parent 過濾器的內(nèi)部 filter 上后德。

children 聚合

parent-child 支持 children 聚合作為 nested 聚合直接的類似。parent 聚合不支持抄腔。

下面的例子展示了我們?nèi)绾胃鶕?jù)國家來確定員工最愛的興趣愛好:

GET /company/branch/_search?search_type=count
{
  "aggs": {
    "country": {
      "terms": { ...1
        "field": "country"
      },
      "aggs": {
        "employees": {
          "children": {  ...2
            "type": "employee"
          },
          "aggs": {
            "hobby": {
              "terms": {  ...3
                "field": "employee.hobby"
              }
            }
          }
        }
      }
    }
  }
}
  1. branch 文檔中的country 字段瓢湃。
  2. children 聚合聯(lián)結(jié)了 parent 文檔和相關聯(lián)的 children 類型 employee
  3. 來自 employee child 文檔的 hobby 字段赫蛇。

Grandparents 和 Grandchildren

parent-child 關系可以擴展超過一代——grandchildren 可以有 grandparents——但是需要額外步驟來確保來自所有代的文檔索引在同一個分片上绵患。
讓我們改變前面的例子來讓 country 類型是 branch 類型的 parent:

PUT /company
{
  "mappings": {
    "country": {},
    "branch": {
      "_parent": {
        "type": "country"  ...1
      }
    },
    "employee": {
      "_parent": {
        "type": "branch"  ...2
      }
    }
  }
}
  1. branchcountry 的 child
  2. employeebranch 的 child

國家和分部有一個簡單的 parent-child 關系,所以我們使用和之前同樣的過程:

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

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 ID 已經(jīng)確保了每個 branch 文檔被路由到和 parent country 文檔同樣的分片上悟耘。然而落蝙,看看使用同樣的技術在 employee grandchildren上:

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

這兒員工文檔的路由分片會被 parent ID London 確定,但是 london 文檔會根據(jù)其 parent ID ——uk 確定暂幼。很可能 grandchild 會得到和它 parent 和 grandparent 不同的分片筏勒,最終會導致 grandparent grandchild 關系失效。

于是我們重新設計粟誓,增加一個額外的 routing 參數(shù)奏寨,將這個設置為 grandparent ID 來保證所有三代都索引在同一個分片上。索引請求應該像這樣:

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

parent 值仍然會用來連接員工文檔和其parent鹰服,但是 routing 值需要對所有單個文檔請求設置。

查詢和聚合揽咕,只要你一步一步通過每一代文檔悲酷。例如,為了找到有員工喜歡滑雪的國家亲善,我們需要將國家和分部设易、分部和員工進行聯(lián)結(jié):

GET /company/country/_search
{
  "query": {
    "has_child": {
      "type": "branch",
      "query": {
        "has_child": {
          "type": "employee",
          "query": {
            "match": {
              "hobby": "hiking"
            }
          }
        }
      }
    }
  }
}

實戰(zhàn)建議

parent-child 連接是在管理關系時有用的技術,其前提是索引性能比搜索性能更加重要蛹头,也帶來了一個顯著的代價顿肺。parent-child 查詢時間可能是等價的 nested query 五到十倍。

內(nèi)存使用

parent-child ID 映射仍舊是存在內(nèi)存中的渣蜗。有計劃將這個映射使用 doc value 替代屠尊,這肯定是較大的內(nèi)存節(jié)約。在進行了這個更新前耕拷,你需要注意下面的事:每個 parent 文檔的字符串 _id 字段需要存放在內(nèi)存中讼昆,每個 child 文檔需要 8 字節(jié)(long value)的內(nèi)存。實際上骚烧,這個可以有壓縮技術的支持浸赫,但這是一個解決方向闰围。

你可以檢查使用 indices-stats API 來追蹤 parent-child 緩存使用了多少內(nèi)存,或者 node-stats API(在節(jié)點層的總結(jié)):

GET /_nodes/stats/indices/id_cache?human ...1
  1. 以比較友好的格式按節(jié)點返回內(nèi)存使用 ID 緩存的情況

global ordinals 和 延時

parent-child 使用 global ordinals 來加速聯(lián)結(jié)既峡。不管 parent-child 映射使用 in-memory 緩存或者磁盤 doc value羡榴,global ordinals 仍然需要在每次索引變動后進行重建。

在分片中的 parent 越多运敢,就需要更長的 global ordinals 來構(gòu)建炕矮。parent-child 是最適合對每個 parent 有很多 children的情況,而不是很多的 parent 少量的 children者冤。

global ordinals 默認是 lazily 構(gòu)建的:在每個刷新 refresh 后的第一個 parent-child 查詢或者聚合將會觸發(fā) global ordinal 的構(gòu)建肤视。這會引入一個明顯延遲增加。你可以使用 eager_global_ordinals 來從查詢時刻到刷新時刻的變動構(gòu)建 global ordinal 的代價涉枫,通過將 _parent 字段映射按照如下修改:

PUT /company
{
  "mappings": {
    "branch": {},
    "employee": {
      "_parent": {
        "type": "branch",
        "fielddata": {
          "loading": "eager_global_ordinals"  ...1
        }
      }
    }
  }
}
  1. _parent 字段的 Global ordinals 將會在新的 segment 在搜索可見前構(gòu)建邢滑。

有很多 parent 時,global ordinals 需要幾秒鐘進行構(gòu)建愿汰。這樣的話困后,增加 refresh_interval 來讓刷新減少并讓 global ordinals 留存更長就比較合理了。這會大幅降低每秒鐘都重建 global ordinals CPU 代價衬廷。

多代關系總結(jié)性思考

連接多代的能力看起來很誘人摇予,但是你會發(fā)現(xiàn)它帶來的代價:

  • 更多的連接,更差的性能
  • 每代 parents 需要讓他們的字符串 _id 字段存儲在內(nèi)存中吗跋,這樣也會消耗大量內(nèi)存

所以在你考慮需要處理的關系侧戴,考量 parent-child 是不是合適的選擇是,可以看看下面的一些建議:

  • 謹慎地使用 parent-child 關系跌宛,?僅僅在有更多的 children 時采用
  • 避免在一個單獨的查詢中使用多個 parent-child 聯(lián)結(jié)
  • 避免在使用 has_child 過濾器中使用計分酗宋,或者在 has_child 查詢中將 score_mode 設置為 none
  • 盡量讓 parent ID 短,以減少內(nèi)存使用量

綜上所述:在嘗試 parent-child 關系前考慮其他類型的關系技術

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疆拘,一起剝皮案震驚了整個濱河市蜕猫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哎迄,老刑警劉巖回右,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異漱挚,居然都是意外死亡翔烁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門棱烂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來租漂,“玉大人,你說我怎么就攤上這事×ㄖ危” “怎么了秃踩?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長业筏。 經(jīng)常有香客問我憔杨,道長,這世上最難降的妖魔是什么蒜胖? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任消别,我火速辦了婚禮,結(jié)果婚禮上台谢,老公的妹妹穿的比我還像新娘寻狂。我一直安慰自己,他們只是感情好朋沮,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布蛇券。 她就那樣靜靜地躺著,像睡著了一般樊拓。 火紅的嫁衣襯著肌膚如雪纠亚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天筋夏,我揣著相機與錄音蒂胞,去河邊找鬼。 笑死条篷,一個胖子當著我的面吹牛骗随,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拥娄,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蚊锹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稚瘾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤姚炕,失蹤者是張志新(化名)和其女友劉穎摊欠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柱宦,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡些椒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了掸刊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片免糕。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出石窑,到底是詐尸還是另有隱情牌芋,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布松逊,位于F島的核電站躺屁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏经宏。R本人自食惡果不足惜犀暑,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烁兰。 院中可真熱鬧耐亏,春花似錦、人聲如沸沪斟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽非凌。三九已至四啰,卻和暖如春铜涉,著一層夾襖步出監(jiān)牢的瞬間锌杀,已是汗流浹背广恢。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工实胸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叙甸,地道東北人史翘。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓枉长,卻偏偏與公主長得像,于是被迫代替她去往敵國和親琼讽。 傳聞我的和親對象是個殘疾皇子必峰,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容