從一個實戰(zhàn)問題再談 Elasticsearch 數(shù)據(jù)建模
1、上問題
請問在一張訂單表里,用戶購買的產(chǎn)品是一條數(shù)據(jù),我現(xiàn)在想查既購買了 A 又買了 B 的用戶,這種需求能做嗎?
在表里存在一個用戶購買了多種產(chǎn)品和一個產(chǎn)品被多個人購買的情況,每個用戶購買的產(chǎn)品是一條單獨的數(shù)據(jù)。
假如現(xiàn)在的表已經(jīng)是我上邊說的那種情況了,能寫出符合我查詢要求的DSL嗎?
球友提問
2、問題細(xì)化
注意,類似的問題是業(yè)務(wù)問題,如果要實際落地分析,需要進一步核實確認(rèn)當(dāng)前的數(shù)據(jù)建模。
本質(zhì)一句話:數(shù)據(jù)的建模決定了數(shù)據(jù)的存儲,數(shù)據(jù)的存儲決定了數(shù)據(jù)的檢索實現(xiàn)。
經(jīng)反復(fù)討論,敲定了當(dāng)前的簡化的數(shù)據(jù)建模如下:
PUT?products
{
??"mappings":?{
????"properties":?{
??????"uid":{
????????"type":"keyword"
??????},
??????"tag_name":{
????????"type":"keyword"
??????}
????}
??}
}
POST?products/_bulk
{"index":{"_id":1}}
{"tag_name":["陽光保險-2016"],?"uid":"1111_2222"}
{"index":{"_id":2}}
{"tag_name":"太平洋保險-2020",?"uid":"1111_2222"}
{"index":{"_id":3}}
{"tag_name":"平安保險-2019",?"uid":"333333"}
兩個字段給大家簡單解讀一下:
uid,用戶id "tag_name,用戶購買的產(chǎn)品
如上的建模就和問題描述建立起一一對應(yīng)的關(guān)系了。
用戶id為:1111_2222 的用戶,購買了 2 個產(chǎn)品:陽光保險-2016 和 太平洋保險-2020。
現(xiàn)在問題轉(zhuǎn)嫁為:查找購買了“陽光保險-2016” 和 “太平洋保險-2020” 的用戶?
到了這里,問題基本上無二義,雙方理解基本一致。
ps:這也是咱們公司內(nèi)部溝通拆解問題的思路,由于問題比較簡單,不再啰嗦。
3、問題剖析
怎么做?
看到這里,大家可以想一下?暫停幾秒,再往后看... ...
這時候,腦海里想一下,檢索或者聚合能否實現(xiàn)類似需求?
注意:購買了 “陽光保險-2016” 和 “太平洋保險-2020” ,是與的關(guān)系。首先想到的是:bool 和 must 結(jié)合。
很快啊,答案寫在了下面
POST?products/_search
{
??"query":?{
????"bool":?{
??????"must":?[
????????{
??????????"term":?{
????????????"tag_name":?"陽光保險-2016"
??????????}
????????},
????????{
??????????"term":?{
????????????"tag_name":?"太平洋保險-2020"
??????????}
????????}
??????]
????}
??}
}
但,放到 Kibana里面執(zhí)行一下,發(fā)現(xiàn)怎么返回結(jié)果為空?
大意了嗎?!再仔細(xì)審題.....
恍然大悟,本質(zhì)錯誤原因在于:一對一的字段映射關(guān)系,怎么能得到兩個或者多個都匹配的結(jié)果呢?
這才意識到哪里出了問題?!——不是數(shù)據(jù)檢索,而是數(shù)據(jù)建模!
4、問題解答
問題的本質(zhì)再細(xì)化抽象:
這已經(jīng)不是簡單的 Mysql 中的一對一的數(shù)據(jù)關(guān)系,所謂一對一代表 —— 一個用戶 id 對應(yīng)一個產(chǎn)品名。
如下圖所示:多個 1 對 1 表示不同的doc。

而是:一對多的數(shù)據(jù)關(guān)系。

為什么?多個一對一是不能解決:查找購買了“陽光保險-2016” 和 “太平洋保險-2020” 的用戶的需求的?
那怎么實現(xiàn)呢?幾乎沒有更好的方法,除了:數(shù)據(jù)重新建模。
再建模的時候,要以上面的一對多的圖示作為依據(jù)。
這時候,腦海里要浮現(xiàn)出 ES 支持哪些所謂“多表關(guān)聯(lián)”操作?
至少應(yīng)該想到:
Array 數(shù)組類型 Object 對象類型 Nested 嵌套對象類型 Join 父子關(guān)聯(lián)類型
我們先拿 Array 數(shù)組類型試驗,提到數(shù)組類型,里面要進一步映射出 Elasticsearch 關(guān)于數(shù)組的定義:
在Elasticsearch中,沒有專用的數(shù)組數(shù)據(jù)類型。
默認(rèn)情況下,任何字段都可以包含零個或多個值。
數(shù)組中的所有值必須具有相同的數(shù)據(jù)類型。
強調(diào)一下:根據(jù)數(shù)組的定義,之前定義的 Mapping 是不需要修改的。
實踐一把:
POST?products/_bulk
{"index":{"_id":1}}
{"tag_name":["陽光保險-2016",?"太平洋保險-2020"],?"uid":"1111_2222"}
{"index":{"_id":2}}
{"tag_name":"太平洋保險-2020",?"uid":"1111_2222"}
{"index":{"_id":3}}
{"tag_name":"平安保險-2019",?"uid":"333333"}
如此,一對一的數(shù)據(jù)建模變成了一對多的數(shù)據(jù)建模。
驗證一把結(jié)果是否達(dá)到預(yù)期呢?
POST?products/_search
{
??"query":?{
????"bool":?{
??????"must":?[
????????{
??????????"term":?{
????????????"tag_name":?"陽光保險-2016"
??????????}
????????},
????????{
??????????"term":?{
????????????"tag_name":?"太平洋保險-2020"
??????????}
????????}
??????]
????}
??}
}
返回結(jié)果(僅貼出了 hits 部分):
?"hits"?:?[
??????{
????????"_index"?:?"products",
????????"_type"?:?"_doc",
????????"_id"?:?"1",
????????"_score"?:?1.6161176,
????????"_source"?:?{
??????????"tag_name"?:?[
????????????"陽光保險-2016",
????????????"太平洋保險-2020"
??????????],
??????????"uid"?:?"1111_2222"
????????}
??????}
????]
很簡單的就實現(xiàn)了需求。
注意:Object,nested 等也可以實現(xiàn),要根據(jù)具體業(yè)務(wù)所需,不再展開講。
5、再談數(shù)據(jù)建模
為什么是再談,是因為我們強調(diào)過:干貨 | 論Elasticsearch數(shù)據(jù)建模的重要性。
尤其基礎(chǔ)數(shù)據(jù)源來自關(guān)系型數(shù)據(jù)庫(Mysql、Oracle 等)的業(yè)務(wù)數(shù)據(jù),切記不要將數(shù)據(jù)同步到ES就完事大吉。
同步之前需要討論:
數(shù)據(jù)在 Elasticsearch 怎么建模? Mapping 如何設(shè)置? 哪些字段需要全文檢索?需要分詞?哪些不需要? 哪些字段需要建索引? 哪些字段不需要存儲? 類型選擇:integer 還是 keyword? 哪些需要做多表關(guān)聯(lián)?使用:寬表冗余存儲?還是 Array,Object,Nested,Join ?都需要深入考慮。 哪些字段需要預(yù)處理? 預(yù)處理是通過logstash filter 環(huán)節(jié)實現(xiàn)?還是 Mysql 視圖實現(xiàn)?還是 ingest 管道實現(xiàn)?還是其他? ......? 類似延伸出幾十個問題都不在話下。
這里想的越充分,后面越省事!
關(guān)于多表關(guān)聯(lián)問題,推薦閱讀:Elasticsearch多表關(guān)聯(lián)設(shè)計指南
6、小結(jié)
數(shù)據(jù)建模的培養(yǎng)沒有太好的速成方法,需要結(jié)合項目實踐、反饋、再實踐、再反饋總結(jié)。形成知識積累。
