Elasticsearch 數據建模指南

0、題記

我在做 Elasticsearch 相關諮詢和培訓過程中,發現大家普遍更關注實戰中涉及的問題,下面我選取幾個常見且典型的問題,和大家一起分析一下。

如果我們對上述實戰問題進行歸類,就都可以歸結爲 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 中一個字段一個字段地敲定,這樣操作確實節省了很多時間。但隨着後續數據量激增,副作用便會很快顯現出來。該處理方式的弊端:

接下來,結合我自己工作中早期系統的一個案例,我們做進一步分析。

5 個數據節點集羣(5 個分片,1 個副本),微博數據每日增量 5000W+(增量存儲 150GB),核心數據磁盤 10TB 左右,很明顯該系統面臨存儲上限問題。

我們當時就上述業務數據規劃了一個大索引,比如微博數據一個索引,微信數據一個索引。但微博索引最多隻能存儲 20 天左右的數據,然後就得走刪除索引數據的操作。由於 1 個索引只能通過 delete_by_query 刪除部分數據,而 delete_by_query 的特點是版本號更新的邏輯刪除,實際效果是越刪數據量越大,磁盤佔用率激增。加上是線上環境,壓力之大,處理難度之大,經歷過你就知道有多苦。

這也是很多大廠在面試候選人的時候,尤其偏愛數據建模能力強的工程師的主要原因之一。

比如下圖是美團對大數據開發高級工程師的崗位要求,第一條就是 “深入理解業務,對業務服務流程進行合理的抽象和建模。”

從以上兩個反例,以及這條招聘信息中便可以窺探出數據建模的重要性。下面我們具體說說如何做數據建模。

2、Elasticsearch 如何數據建模?

在做數據建模之前,會先進行架構設計,架構環節涉及選型、集羣規劃、節點角色劃分。

本文涉及的建模傾向於索引層面、數據層面的建模。爲了讓你學完即可應用到工作中,我會結合項目實戰進行講解。

2.1 基於業務角度建模

Elasticsearch 適用範圍非常廣,包括電商、快遞、日誌等各行各業。涉及索引層面的設計,和業務貼合緊密。

其一:業務一定要細分。

分成哪幾類數據,每類數據歸結爲一個索引還是多個索引,這是產品經理、架構師、項目經理要討論敲定的問題。比如大數據類的數據,可以按照業務數據分爲微博索引、微信索引、Twiiter 索引、Facebook 索引等。

其二:多個業務類型需不需要跨索引檢索?

跨索引檢索的痛點是字段不統一、不一致,需要寫非常複雜的 bool 組合查詢語句來實現。爲了避免這種情況,最好的方式就是提前建模。每一類業務數據的相同或者相似字段,採取統一建模的方式。

下面我們舉一個實際的例子加以分析。微博、微信、Twitter、Facebook 都有的字段,可以設計如下:

2oaEmH

這樣設計的好處是:字段統一,寫查詢 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 倍之間的值。

副本分片是保證集羣的高可用性,普通業務場景建議至少設置一個副本。

默認值 1s,這意味着在寫入階段,每秒都會生成一個分段。

refresh_interval 的目的是:數據由 index buffer 的堆內存緩存區刷新到堆外內存區域,形成 segment,以使得搜索可見。

在實際業務場景裏,如果寫入的數據不需要近實時搜索可見,可以適當地在模板、索引層面調大這個值,當然也可以動態調整,比如調整爲 30s 或者  60s。

這裏同樣有個認知前提,就是對於深度翻頁的 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 版本之後分爲兩種類型:

再舉個例子,實戰中情感值介於 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""佟大"
          }
        }
      ]
    }
  }
}

爲了方便你記憶和使用,這裏我把字段細節總結在如下這張表格中。

oHuB78

我們再來分析一下數據建模的流程,如下圖所示。

數據建模的流程圖

首先,根據業務選擇合適的數據類型。

注意字符串類型分爲兩種 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 方案

如果需要索引對象數組並保持數組中每個對象的獨立性,則應使用嵌套 Nested 數據類型而不是對象 Oject 數據類型。

nested 文檔的優點是可以將父子關係的兩部分數據(如博客 + 評論)關聯起來,我們可以基於 nested 類型做任何的查詢。但缺點是查詢速度相對較慢,更新子文檔需要更新整篇文檔。

(3) join 父子文檔方案

比如 1 個產品和供應商之間就是 1 對 N 的關聯關係。當使用父子文檔時,使用 has_child 或者 has_parent 做父子關聯查詢。優點是父子文檔可獨立更新,但維護 Join 關係需要佔據部分內存,查詢較 Nested 更耗資源。

注意:5.X 之前版本叫父子文檔(多 type 實現),6.X 之後高版本是 join 類型(單 type 類型)。

(4) 業務層面實現關聯

需通過多次檢索獲取所需的關鍵字段,業務層面自己寫代碼實現。

這裏小結一下,以上四種方式便是 Elasticsearch 能實現的全量多表關聯方案。實戰建模階段,一定要結合自己的業務場景,儘量往上靠,先通過 kibana dev tool 模擬實現,找到契合自己業務的多表關聯方案。

此外我還要強調的是:多表關聯都會有性能問題,數據量極大且檢索性能要求高的場景需要慎用。這裏我摘取了官方文檔對應的描述如下,供你參考。

尤其應該避免多表關聯。Nested 嵌套可以使查詢慢幾倍,而 Join 父子關係可以使查詢慢數百倍。

3、總結

最後,我們再來總結一下建模其他核心考量因素。

數據建模是 Elasticsearch 開發實戰中非常重要的一環,也是項目管理角度中的設計環節的重中之重,你一定要重視!千萬不要着急寫業務代碼,以 “代碼之前,設計先行” 作爲行動準繩。

感謝你的時間!有 Elasticsearch 建模問題歡迎留言交流。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vSh6w3eL_oQvU1mxnxsArA