Elasticsearch 數據建模指南
0、題記
我在做 Elasticsearch 相關諮詢和培訓過程中,發現大家普遍更關注實戰中涉及的問題,下面我選取幾個常見且典型的問題,和大家一起分析一下。
-
訂單表、賬單表父子文檔可以實現類似 SQL 的左連接嗎?通過 canal 同步到 ES 中,能否實現類似左連接的效果?具體應該如何建模?
-
一個人管理 1000 家連鎖門店,如何更高效地查詢自己管轄的商品類目?企微 一個人維護了 1000 個員工,如何快速查詢自己管轄的員工信息?
-
隨着業務的增長,一個索引的字段數據不斷膨脹(商品場景變化,業務一直加字段),有什麼解決方法?
-
一個索引字段個數設置爲 1500 個,超出這個限制,會不會消耗 CPU 資源和造成寫入堆積?
-
日誌診斷用於機器學習基線,需要將 message 分離出來,怎麼在寫入前搞定?
如果我們對上述實戰問題進行歸類,就都可以歸結爲 Elasticsearch 數據建模
問題。
本文將以實戰問題爲基準,手把手帶你實踐 Elasticsearch 數據建模全流程,重點解析基於業務角度、數據量角度、Setting 、Mapping ,以及複雜索引關聯,這五個層面中涉及的數據建模實戰問題,讓你學完即可應用到工作中。
1、爲什麼要做數據建模?
我們選型傳統的數據庫,這裏以 MySQL 爲例,做數據存儲前需要考慮的問題如下:
-
數據庫要不要做讀寫分離?
-
分幾張表存儲?
-
每個表的名是什麼?
-
每個表是按照業務劃分嗎?
-
單表數據大了怎麼搞?分庫分表還是其他?
-
每個表要有哪些字段?每個字段設置什麼類型?如何設計合理的字段類型,才能保證節省存儲?
-
哪些字段需要建索引?
-
哪些字段需要設置外鍵?
-
表之間要不要建立關聯?如何實現關聯聯動查詢?
-
關聯查詢可能會很慢?如何設計階段優化建模才能提高響應速度?
以上這些疑問也均是數據建模問題。在 MySQL 中我們往往認爲建模非常有必要,但反觀 Elasticsearch ,“上手快” 這類先入爲主的觀念已根植在很多同學心中,使得大家忽略了 Elasticsearch 數據建模的重要性。
接下來,我們基於 MySQL 做數據存儲需要考慮的問題,重新審視數據建模的定義,內容如下。
-
數據模型是對描述數據、數據聯繫、數據語義和一致性約束進行標準化的抽象模型。
-
數據建模是爲存儲在數據庫中的資源創建和分析數據模型的過程。
-
數據建模主要目的是表示系統內的數據類型、對象之間的關係及其屬性。
-
數據模型有助於瞭解需要哪些數據以及應如何組織數據。
到這裏,相信你已經初步明晰了數據建模的重要性。但我還想提醒你的是,“一把梭用法,上來就是幹” 並不是捷徑,尤其到了項目中後期,極易暴露出問題。經歷的項目越多,你會發現建模的時間不能省。
下面我們具體分析一下爲什麼要數據建模?
相比於 MySQL,Elasticsearch 有非常快捷的優勢
:
Elasticsearch 支持動態類型檢查和匹配。也就是說,當我們寫入索引數據的時候,可以不提前指定數據類型,直接插入數據。
以類似天眼查、企查查的工商實戰數據爲例(已做脫敏處理),如果利用以下語句直接創建索引和寫入一條數據,豈不是很快?
PUT company_index/_doc/1
{
"regist_id": 1XX1600000000012,
"company_name": "北京XX長江創業投資有限公司",
"regist_id_new": "191XX160066933968XC",
"legal_representative": "徐X武",
"scope_bussiness": "創業投資業務;代理其他創業投資企業等機構或個人的創業投資業務;創業投資諮詢業務;爲創業企業提供管理服務業務;參與設立創業投資企業與企業投資管理顧問機(依法須經批准的項目,經相關部門批准後方可開展經營活)",
"registration_status": "在營(開業)企業",
"approval_date": "201X年04月13日",
"registration_number": "191XX160066933968XC",
"establishment_time": "200X年12月03日",
"address": "北京市黃河XX路西首育青小區",
"register_capital": 3000,
"business_starttime": "20XX年12月03日",
"registration_authority": "北XX工商行政管理局",
"company_type": "其他有限責任公司",
"enttype": 1190,
"enttypename": "法定代表人:",
"pripid": "1XXX102201305305801X",
"uniscid": "1XXX160066933968XC"
}
相比於 MySQL 中一個字段一個字段地敲定,這樣操作確實節省了很多時間。但隨着後續數據量激增,副作用便會很快顯現出來。該處理方式的弊端:
-
首先是極大地浪費了存儲空間,所有字符串類型數據都存儲爲 text + keyword 組合類型,這種很多業務字段都是非必須的;
-
其次字符串類型默認分詞 standard,無法滿足中文精細化分詞檢索的需求。
接下來,結合我自己工作中早期系統的一個案例,我們做進一步分析。
5 個數據節點集羣(5 個分片,1 個副本),微博數據每日增量 5000W+(增量存儲 150GB),核心數據磁盤 10TB 左右,很明顯該系統面臨存儲上限問題。
我們當時就上述業務數據規劃了一個大索引,比如微博數據一個索引,微信數據一個索引。但微博索引最多隻能存儲 20 天左右的數據,然後就得走刪除索引數據的操作。由於 1 個索引只能通過 delete_by_query 刪除部分數據,而 delete_by_query 的特點是版本號更新的邏輯刪除,實際效果是越刪數據量越大,磁盤佔用率激增。加上是線上環境,壓力之大,處理難度之大,經歷過你就知道有多苦。
這也是很多大廠在面試候選人的時候,尤其偏愛數據建模能力強的工程師的主要原因之一。
比如下圖是美團對大數據開發高級工程師的崗位要求,第一條就是 “深入理解業務,對業務服務流程進行合理的抽象和建模。”
從以上兩個反例,以及這條招聘信息中便可以窺探出數據建模的重要性。下面我們具體說說如何做數據建模。
2、Elasticsearch 如何數據建模?
在做數據建模之前,會先進行架構設計,架構環節涉及選型、集羣規劃、節點角色劃分。
本文涉及的建模傾向於索引層面、數據層面的建模。爲了讓你學完即可應用到工作中,我會結合項目實戰進行講解。
2.1 基於業務角度建模
Elasticsearch 適用範圍非常廣,包括電商、快遞、日誌等各行各業。涉及索引層面的設計,和業務貼合緊密。
其一:業務一定要細分。
分成哪幾類數據,每類數據歸結爲一個索引還是多個索引,這是產品經理、架構師、項目經理要討論敲定的問題。比如大數據類的數據,可以按照業務數據分爲微博索引、微信索引、Twiiter 索引、Facebook 索引等。
其二:多個業務類型需不需要跨索引檢索?
跨索引檢索的痛點是字段不統一、不一致,需要寫非常複雜的 bool 組合查詢語句來實現。爲了避免這種情況,最好的方式就是提前建模。每一類業務數據的相同或者相似字段,採取統一建模的方式。
下面我們舉一個實際的例子加以分析。微博、微信、Twitter、Facebook 都有的字段,可以設計如下:
這樣設計的好處是:字段統一,寫查詢 DSL 無需特殊處理,非常快捷方便。所以,在設計階段,多個業務索引數據要儘可能地 “求同存異”。具體來說:
-
求同指的是相同或者相近含義字段,一定要統一字段名、統一字段類型;
-
存異指的則是特定業務數據特有字段類型,可以獨立設計字段名稱和類型。
比如微博信息來源字段有手機 App 或者網頁等,別的業務索引如果沒有,獨立建模就可以。
類似這些建模信息可以統一 Excel 存儲,統一 git 多人協作管理。
多索引管理一般優先推薦使用模板(template)和 別名(alias)結合的方式。
2.2 基於數據量角度建模
如本文前面所述,我是喫過單索引激增的虧,所以對於時序性數據(日誌數據、大數據類數據)等,我強烈建議你基於時間切分索引,具體如下圖所示。
當然,其他可用的方案非常多,這裏我列舉如下,供你選型參考。
由此可見,時序管理數據的優點非常明顯。
-
其一是靈活。基於時間切分索引非常方便,刪除數據屬於物理刪除。
-
其二則是快速。特定業務數據配合冷熱集羣架構,確保高配機器對應熱數據,提升檢索效率和用戶體驗。
2.3 基於 Setting 層面建模
Setting 層面又分爲靜態 Setting 和動態 Setting 兩種。
一種是靜態 Settings,一旦設置後,後續不可修改。如 number_of_shards
。
另一種是動態 Setting,索引創建後,後面隨時可以更新。如 number_of_replicas
, max_result_window
, refresh_interval
。
僅就建模階段最核心的問題,拆解如下。
- 問題一:索引設置多少個分片?多少個副本?
這裏有個認知前提,就是主分片數一旦設置後就不可以修改,副本分片數可以靈活動態調整。
主分片設計一般會考量總體數據量、集羣節點規模,這點在集羣規劃層面會着重強調。一般主分片數要考慮集羣未來動態擴展,通常設置爲數據節點的 1 倍或者 1~3 倍之間的值。
副本分片是保證集羣的高可用性,普通業務場景建議至少設置一個副本。
- 問題二:refresh_interval 一般設置多大?
默認值 1s,這意味着在寫入階段,每秒都會生成一個分段。
refresh_interval
的目的是:數據由 index buffer
的堆內存緩存區刷新到堆外內存區域,形成 segment
,以使得搜索可見。
在實際業務場景裏,如果寫入的數據不需要近實時搜索可見,可以適當地在模板、索引層面調大這個值,當然也可以動態調整,比如調整爲 30s 或者 60s。
- 問題三:max_result_window 要不要修改默認值?
這裏同樣有個認知前提,就是對於深度翻頁的 from + size 實現,越往後翻頁越慢。其實你對比看主流搜索引擎,比如 Google、百度、360、Bing 均不支持一下跳轉到最後一頁,這就是最大翻頁上限限制。
其實在基本業務層面也很好理解,按照相關度返回結果,前面幾頁是最相關的,越往後相關度越低。比如默認值 10000,也就是說如果每頁顯示 10 條數據,可以翻 1000 頁。基本業務場景已經足夠了。因此不建議調大該值。
如果需要向後翻頁查詢,推薦 search_after 查詢方式。如果需要全量遍歷或者全量導出數據,推薦 scroll 查詢方式。
- 問題四:管道預處理怎麼用?
管道預處理的好處很多,雖然 5.X 版本就有了這個功能,但實戰環境用起來還不多。
管道 ingest pipeline
就相當於大數據的 ETL 抽取、轉換、加載的環節,或者類似 logstash filter
處理環節。一些數據打標籤、字段類型切分、加默認字段、加默認值等的預處理操作都可以藉助 ingest pipelie
實現。
這裏給出索引層面 Setting
設置的簡單模板,供你進一步學習參考,如下定義了 indexed_at 缺省的管道,同時在索引 my_index_0001 指定了該缺省管道,這樣做的好處,是每個新增的數據都會加了插入時刻的時間戳:indexed_at 字段,無需我們在業務層面手動處理,非常靈活和方便。
更多設置,推薦閱讀官方文檔,地址如下:
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings
PUT _ingest/pipeline/indexed_at
{
"description": "Adds indexed_at timestamp to documents",
"processors": [
{
"set": {
"field": "_source.indexed_at",
"value": "{{_ingest.timestamp}}"
}
}
]
}
PUT my_index_0001
{
"settings": {
"number_of_replicas": 1,
"number_of_shards": 3,
"refresh_interval": "30s",
"index": {
"default_pipeline": "indexed_at"
}
},
"mappings": {
"properties": {
"cont": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
2.4 基於 Mapping 層面建模
Mapping 層面核心是字段名稱、字段類型、分詞器選型、多字段 multi_fields 選型,以及字段細節(是否索引、是否存儲等)的敲定。
(1)字段命名要規範
索引名稱不允許用大寫,字段名稱官方沒有限制,但是可以參考 Java 編碼規範。我還真見過學員用中文或者拼音命名的,非常不專業,大家一定要避免。
(2)字段類型要合理
要結合業務類型選擇合適的字段類型。比如 integer 能搞定的,就不要用 long、float 或 double。
注意,字符串類型在 5.X 版本之後分爲兩種類型:
-
一種是 keyword,適合精準匹配、排序和聚合操作;
-
另一種是 text,適合全文檢索。默認值 text & keyword 組合不見得是最優的,選型時候要結合業務選擇。比如優先選擇 keyword 類型,keyword 走倒排索引更快。
再舉個例子,實戰中情感值介於 0~100 之間,50 代表中性,0~50 代表負面,50~100 代表正面。如果使用 integer 查詢的時候要 range query,而實際存儲可以增加字段:0~50 設置爲 -1,50 設置爲 0,50~100 設置爲 1,三種都是 keyword 類型,檢索時直接走 term 檢索會非常快。
(3)分詞器要靈活
實戰中中文分詞器用得比較多,中文分詞又分爲 ansj,結巴,IK 等。以 IK 舉例,可以細分爲 ik_smart 粗粒度分詞、ik_max_word 細粒度分詞。
在工作中,要結合業務選擇合適的分詞器,分詞器一旦設定是不可以修改的,除非 reindex。
分詞器選型後,都會有動態詞典的更新問題。更新的前提是不要僅使用開源插件原生詞典,而是要在平時業務中自己多積累特定業務數據詞典、詞庫。
如果要動態更新:一般推薦第三方更新插件藉助數據庫更新實現。如果普通分詞都不能滿足業務需要,可以考慮 ngram
自定義分詞方式實現更細粒度分詞。
(4)multi_fields 適機使用
同一個字段根據需要可以設置多種類型。實戰業務中,對用特定中文詞明明存在,卻無法召回的情況,採用字詞混合索引的方式得以滿足。
所謂字詞混合,實際就是 standard 分詞器實現單字拆解,以及 ik_max_word 實現中文切詞結合的方式。檢索的時候 bool 對兩種分詞器結合,就可以實現相對精準的召回效果。
PUT mix_index
{
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"standard": {
"type": "text",
"analyzer": "standard"
},
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
POST mix_index/_search
{
"query": {
"bool": {
"should": [
{
"match_phrase": {
"content": "佟大"
}
},
{
"match_phrase": {
"content.standard": "佟大"
}
}
]
}
}
}
爲了方便你記憶和使用,這裏我把字段細節總結在如下這張表格中。
我們再來分析一下數據建模的流程,如下圖所示。
數據建模的流程圖
首先,根據業務選擇合適的數據類型。
注意字符串類型分爲兩種 text 和 keyword 類型;儘量選擇貼近實際大小的數據類型;nested 和 join 複雜類型需根據業務特點選型,具體會在下一部分詳細闡述。
其次,判定是否需要檢索,如果不需要,index 設置爲 false 即可。
然後,判定是否需要排序和聚合操作,如果不需要可以設置 doc_values 爲 false。
最後,考慮一下是否需要另行存儲,會結合使用 store 和 _source 字段。
Mapping 層面要強調的是:儘量不要使用默認的 dynamic 動態字段類型,強烈建議 strict 嚴格控制字段,避免字段 “暴漲” 導致不可預知的風險,比如字段數超過默認 1000 個的上限、磁盤大於預期的激增等。
2.5 基於複雜索引關聯建模
要摒棄 MySQL 的多表關聯建模思想,因爲 MySQL 中的範式思想都不再適用於 Elasticsearch。回顧文章開頭的幾個多表關聯問題,Elasticsearch 能提供的核心解決方案如下。
(1) 寬表方案
這是空間換時間的方案,就是允許部分字段冗餘存儲的存儲方式。實戰舉例如下。
用戶索引:user。
博客索引:blogpost。
一個用戶可以發表多篇博客。按照傳統的 MySQL 建表思想:兩個表建立個用戶外鍵,即可搞定一切。而對於 Elasticsearch,我們更願意在每篇博文後面都加上用戶信息(這就是寬表存儲的方案),看似存儲量大了,但是一次檢索就能搞定搜索結果。
PUT user/_doc/1
{
"name": "John Smith",
"email": "john@smith.com",
"dob": "1970/10/24"
}
PUT blogpost/_doc/2
{
"title": "Relationships",
"body": "It's complicated...",
"user": {
"id": 1,
"name": "John Smith"
}
}
GET /blogpost/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "relationships"
}
},
{
"match": {
"user.name": "John"
}
}
]
}
}
}
(2) nested 方案
- 適用場景:1 對少量,子文檔偶爾更新、查詢頻繁的場景。
如果需要索引對象數組並保持數組中每個對象的獨立性,則應使用嵌套 Nested 數據類型而不是對象 Oject 數據類型。
nested 文檔的優點是可以將父子關係的兩部分數據(如博客 + 評論)關聯起來,我們可以基於 nested 類型做任何的查詢。但缺點是查詢速度相對較慢,更新子文檔需要更新整篇文檔。
(3) join 父子文檔方案
- 適用場景:子文檔數據量要明顯多於父文檔的數據量,存在 1 對多量的關係;子文檔更新頻繁的場景。
比如 1 個產品和供應商之間就是 1 對 N 的關聯關係。當使用父子文檔時,使用 has_child 或者 has_parent 做父子關聯查詢。優點是父子文檔可獨立更新,但維護 Join 關係需要佔據部分內存,查詢較 Nested 更耗資源。
注意:5.X 之前版本叫父子文檔(多 type 實現),6.X 之後高版本是 join 類型(單 type 類型)。
(4) 業務層面實現關聯
需通過多次檢索獲取所需的關鍵字段,業務層面自己寫代碼實現。
這裏小結一下,以上四種方式便是 Elasticsearch 能實現的全量多表關聯方案。實戰建模階段,一定要結合自己的業務場景,儘量往上靠,先通過 kibana dev tool 模擬實現,找到契合自己業務的多表關聯方案。
此外我還要強調的是:多表關聯都會有性能問題,數據量極大且檢索性能要求高的場景需要慎用。這裏我摘取了官方文檔對應的描述如下,供你參考。
尤其應該避免多表關聯。Nested 嵌套可以使查詢慢幾倍,而 Join 父子關係可以使查詢慢數百倍。
3、總結
最後,我們再來總結一下建模其他核心考量因素。
-
儘量空間換時間:能多個字段解決的不要用腳本實現。
-
儘量前期數據預處理,不要後期腳本。優先選擇 ingest process 數據預處理實現,儘量不要留到後面 script 腳本實現。
-
能指定路由的提前指定路由。寫入的時候指定路由,檢索的時候也同樣適用路由。
-
能前置的儘量前置,讓後面檢索聚合更加清爽。比如 index sorting 前置索引字段排序是非常好的方式。
數據建模是 Elasticsearch 開發實戰中非常重要的一環,也是項目管理角度中的設計環節的重中之重,你一定要重視!千萬不要着急寫業務代碼,以 “代碼之前,設計先行” 作爲行動準繩。
感謝你的時間!有 Elasticsearch 建模問題歡迎留言交流。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vSh6w3eL_oQvU1mxnxsArA