淺談 Elasticsearch 的入門與實踐
本文主要圍繞 ES 核心特性:分佈式存儲特性和分析檢索能力,介紹了概念、原理與實踐案例,希望讓讀者快速理解 ES 的核心特性與應用場景。
Elasticsearch 入門
Elasticsearch(ES) 是一種基於分佈式存儲的搜索和分析引擎,目前在許多場景得到了廣泛使用,比如維基百科和 github 的檢索,使用的就是 ES。ES 中不乏紛繁冗餘的細節,而本文將關注其核心特性:分佈式存儲特性和分析檢索能力。圍繞這兩大核心特性,本文將介紹其中的概念、原理與實踐案例,希望讓讀者快速理解 ES 的核心特性與應用場景。
核心概念
分佈式存儲特性相關概念:
節點(Node)
節點就是單個的 Elasticsearch 實例,該實例運行的載體可以是物理服務器或虛擬機器。
集羣(Cluster)
集羣是節點的集合。
集羣是一組運行在不同載體(物理或虛擬機器)上的一個或多個 Elasticsearch 節點的集合。在集羣內,節點間相互合作,對數據進行存儲和管理。
分片(Shards)
分片是索引的片段。
受制於單個 ES 節點的性能上限(內存、磁盤 IO 速度),如果數據以整塊形式進行存儲與管理,則無法足夠快速地響應客戶端的請求,因此 ES 將索引拆分爲更小塊的分片,以便分佈式存儲和並行處理數據。
副本(Replicas)
副本是分片的拷貝。
每個分片可以有零個或者多個副本,副本與分片對外都可以提供數據查詢服務。副本的存在可以增加整個 ES 系統的高可用性,高併發性,因爲分片能做到的事情,副本也能做到。計算機世界裏沒有銀彈,副本的開銷主要體現在數據同步成本的增加(每次數據更新時,都需要把分片上的數據變更同步到其他副本中)。
數據模型相關概念
索引(Index)
索引由一個或多個分片組成。索引是 Elasticsearch 中的頂層數據容器,對應關係型數據庫中的數據庫模型。
類別(Type)
類別在較早的 Elasticsearch 版本中,一個索引可以包含多個類別,每個類別用於存儲不同種類的文檔。對應關係型數據庫中的數據表。然而,在 Elasticsearch 7.0 以後,類別逐漸被棄用。原因是同一索引下,不同 type 的數據存儲其他 type 的 field,包含大量空值,造成資源浪費。
文檔(Document)
索引中的每一條數據叫作一個文檔,它是一個 JSON 格式的數據對象。對應關係型數據庫中的數據行。這一點與非關係型數據庫 MongoDB 很類似,MongoDB 屬於文檔數據庫,每條數據也是一個 BSON 文檔(BinaryJSON)。非關係型數據庫的文檔相比關係型數據庫的數據行,優勢在於提供了更高的自由度,文檔中可以方便地新增減字段,多個文檔間也不要求字段完全一致。同時,文檔也保留了一部分結構化存儲的特性,對存儲的數據進行了一定的結構化封裝,而沒有像 K-V 非關係型數據庫那樣完全拋棄數據的結構化。
分析檢索能力相關概念
倒排索引(Inverted Index)
倒排索引是 Elasticsearch 中用於高效檢索文檔的關鍵數據結構。它是將文檔中的每個單詞映射到包含它的文檔上。這種數據結構使得 Elasticsearch 能夠高效地處理文本信息這類非結構化數據,相比傳統關係型數據庫的正排索引遍歷整個數據表,它能夠高效地進行文本檢索與分析。
正排索引是從文檔到關鍵字的映射(已知文檔求關鍵字),倒排索引是從關鍵字到文檔的映射(已知關鍵字求文檔)。倒排索引由兩個主要部分組成:詞彙表(Vocabulary)和倒排列表(Inverted List)。詞彙表存儲了所有不同的單詞,而倒排列表存儲了每個單詞文檔中的分佈情況。
分析器(Analyzer)
分析器是 Elasticsearch 用於進行文本預處理的組件。它的主要作用是將文本轉化爲可被倒排索引的單詞(term)。分析通常由以下幾個步驟組成:
-
分詞(Tokenization):將文本拆分成單詞,對於英文,以空格爲分界線來拆分單詞。
-
標準化(Normalization):對單詞進行規範化,通常包括大小轉小寫、去除停用詞等。
-
過濾(Filtering):過濾掉特殊字符,例如移除特定字符、刪除數字替換等。
在阿里雲 DTS 數據同步工具中,可以選擇一系列 ES 內置的分析器,但是 Elasticsearch 的內置分析器對於中文的支持較差,採取了暴力拆分每個中文單字的策略。如果希望對中文進行合適的分詞,可以選擇第三方分詞器,比如 jieba 分詞器。
主要查詢類型
基於以上的核心概念,Elasticsearch 通過分佈式存儲結構和分析檢索能力,支持並提供了多種不同類型的查詢能力,用於滿足各種檢索需求。以下是 ES 中主要的查詢類型:
單詞級別查詢
萬丈高樓平地起,優秀的全文索引能力是由基礎的單詞查詢能力支撐的。
-
Term Query(精確)
-
最基礎的 ES 查詢,把輸入字符串全部看作一個完整的單詞,然後去倒排索引表裏面找。
-
Fuzzy Query(模糊)
-
帶編輯距離的 term 查詢。具體實現:給定一個模糊度(編輯距離),ES 會根據這個編輯距離,對原始的單詞進行拓展,生成一系列候選的新單詞。對每一個編輯距離內的新單詞,做 term 查詢。
全文級別查詢
像使用 match 和 match_phrase 這樣的高層查詢都屬於全文級別查詢,全文級別查詢是對多個 / 多種單詞級別查詢的封裝。
-
match
-
match 是自適應的:
-
如果給定了模糊度參數 fuzziness,match 在單詞級別查詢上會調用 fuzzy querry;如果未給定此參數,則 match 在單詞級別上會走 term query;
-
如果 analyzed,match 會對輸入進行分詞,把輸入 "service_123456" 看成 "service" 和 "123456";如果 not_analyzed,match 走完全匹配,把輸入 "service_123456" 看成 "service_123456"。
-
match 查詢的主要步驟:
i. 檢查字段類型,查看字段是 analyzed 還是 not_analyzed;
-
如果 analyzed,說明該字段已經被分析器處理過,match 會對輸入進行分詞;
-
如果 not_analyzed,說明該字段未被分析器處理過,match 走完全匹配;
ii. 分析查詢字符串,將輸入字符串進行分詞,對分出來的每個單詞,根據是否設置了模糊度參數 fuzziness,選擇走 term query 或者 fuzzy query;
iii. 文檔評分計算。
-
match_phrase
-
在 match 查詢的基礎上,保證輸入的單詞之間的順序不變纔會命中,性能相比 match 會差一些。
Bool 查詢
用於實現複雜的組合查詢邏輯,具體有四種:
-
should:或
-
must:且
-
must _not:非
-
filter:可以用於作爲查詢中的前置過濾條件,must 類似,好處是它不會參與計算相關性分數。
邏輯完備性:足夠數量的或且非,可以實現任何邏輯。
Term Query 的文檔相關度得分計算方式
利用倒排索引,對於輸入的單詞,考慮每個文檔的以下指標:
-
TFIDF
-
目的:用文檔中的一個單詞,在一堆文檔中區分出該文檔;
-
TFIDF = TF * IDF;
-
TF(term frequency):詞頻。表示單詞在該文本中出現的頻率(單詞在該文本中出現的多不多);
-
IDF(inverse document frequency):反向文檔頻率。 表示單詞在整個文本集合中出現的頻率(有多少文本包含了這個詞)的倒數,IDF 越大表示該詞的重要性越高,反映了單詞是否具有 distinguish 其所在文本的能力。
-
字段的長度
-
字段越短相關度越高;
綜合這兩個指標得出每個文檔的相關度評分_score。
Elasticsearch 實踐
Elasticsearch 實現 nextToken 分頁
在服務端開發的實踐中,由於數據量大,不可能一次請求一次查詢就返回全部數據。因此,對數據進行分頁查詢是一種常見的工程實踐。而由於 ES 方便處理非結構化字段的能力,常常被用作搜索框 API 中的主力分頁查詢。
在 ES 中,內置的分頁機制爲 sort+Search After 分頁。它會對每次請求生成一個遊標字段,這就相當於標記了上一頁的結束位置,因此下次請求只要從上一次的遊標字段開始,就能夠方便地查找下一頁。這實際上是 ES 官方提供的一種 nextToken 分頁實現,它省略掉了構建遊標這一過程,只需要使用者在查詢條件中給定排序字段:
GET /service_version_index/service_version_type/_search
{
"size": 100,
"sort": [
{"gmt_modified": "desc"},
{"score": "desc"},
{"id": "desc"}
],
...
}
sort+Search After 就能在查詢結果中方便地生成每一頁在特定排序上的遊標。
{
"sort" : [
1614561419000,
"6FxZJXgBE6QbUWetnarH"
]
}
下次查詢帶上這個遊標,就可以快速定位到上一頁的結束位置,開始下一頁的查詢:
GET /service_version_index/service_version_type/_search
{
"size": 100,
"sort": [
{"gmt_modified": "desc"},
{"score": "desc"},
{"id": "desc"}
],
"query": {
...
...
},
"search_after": [
1614561419000,
"6FxZJXgBE6QbUWetnarH"
]
}
我們也可以手動構建查詢條件,手動實現 nextToken 分頁條件。即使你不熟悉 nextToken 分頁和 ES 查詢的具體用法,你也應該能做出以下判斷:你一定可以用 ES 的查詢條件實現任意的 nextToken 分頁邏輯。理由是 ES 的 Bool 查詢具有邏輯完備性。
具體地,一個簡單的 if-else 邏輯,可以用 ES 的基礎查詢來複現:
if A then B else C => (A must B) should (must not A must C)
類似地,任意複雜的查詢條件,都可以實現了。
這樣做的好處是,如果項目中涉及到多種數據庫的分頁,則後端代碼的分頁邏輯可以共用,只需要在不同的數據庫中實現相同的 nextToken 條件:
在上述項目中,一個 API 服務使用到了兩條查詢鏈路:一條純 MySQL 的查詢鏈路,另一條 ES 分頁 + MySQL 補字段的查詢鏈路。由於在 ES 分頁中,我們已經手動復現了 MySQL 中的 nextToken 分頁條件。因此,在查詢結束後,封裝 nextToken 分頁請求的這部分後端邏輯就可以實現複用。如果 MySQL 基於自實現的 nextToken 分頁而 ES 使用官方推薦的 Sort 分頁,則複用性較差,需要兩套分頁邏輯。
Elasticsearch 關聯查詢與數據同步
關聯查詢方案:
ES 與非關係型的文檔數據庫類似,基於文檔存儲數據,沒有固定的表結構。關係型數據庫以二維表結構的形式來組織數據,並擅長提供對數據表間關係的管理。而 ES 以文檔爲數據的組織形式,進行扁平化存儲,它不擅長進行關係管理而擅長對扁平化的文檔進行文本檢索。
在 ES 中,由於其分佈式存儲特性和非關係性數據模型,類似關係型數據庫中 JOIN 聯表查詢這樣的操作將非常不便。ES 內置了類似 MySQL 的 JOIN 的關聯查詢實現:父子文檔,但它存在功能和性能上的限制:父子文檔需要在在同一個分片中,額外實現關係管理需要的成本。ES 官方通常不建議使用這種方式。
在 ES 中,如果要實現關聯查詢,最佳實踐一般爲構建寬表或採取服務端 JOIN 這種折中方案。
服務端 JOIN
ES 中分兩個索引來存儲數據,查詢時在服務端的業務代碼內進行兩次查詢,將第一次查詢的結果作爲第二次查詢的條件。
好處:實現容易,數據量少時用戶體驗好。
壞處:數據量大時,兩次查詢會帶來額外的開銷,因爲每次查詢都需要建立連接、發送請求......
拓展:如果涉及不同數據庫之間的關聯查詢,也可以採用此方案,比如用 ES 處理有限的文本字段,查得一個 id 列表,然後把這個 id 列表給 MySQL 的完整查詢作爲條件,補齊剩下的字段。
寬表冗餘存儲:
寬表:通俗地講就是字段很多的數據庫表。指的是把特定的查詢業務需求所需要的全部字段都關聯在一起的一張數據庫表。由於關聯了大量冗餘字段,寬表已經不符合數據庫設計的三範式,而因此獲得的好處就是查詢性能的提高與關聯查詢的簡化(避開了查詢時 JOIN)。這是一種典型的空間換時間的優化思路。但寬表不便擴展,如果業務需求有變化,哪怕是需要新增一個字段,都需要變更寬表。
窄表:嚴格按照數據庫設計三範式設計的數據表。這種表的設計形式減少了數據冗餘,但是實現一個複雜查詢要使用很多張表,涉及多表 JOIN 問題,可能會影響性能。其特點是方便擴展,多個窄表可以組合並適應多種業務場景,無論有多少不同的場景,都不用修改原本的表結構。但在查詢邏輯和代碼邏輯上需要進行封裝。
如果對查詢速度性能要求較高,建議選擇寬表。
數據同步
由於 ES 擅長檢索而不是存儲,業務場景中很少會以 ES 作爲主力做數據存儲,而是使用關係型數據庫進行存儲,在需要 ES 時再構建需要的數據並進行同步。具體來說,有手動寫入和數據同步工具等方案。
手動寫入
在已有的業務邏輯中,同步或異步地增加對 ES 的增刪改查。實現簡單,但不利於擴展,耦合性較強。
數據同步工具
阿里雲 DTS:
是阿里雲提供的一種雲服務產品,基於 binLog 模擬主從複製實現數據同步。一對一數據同步方便高效,但對多表 JOIN 場景無法支持。如果表結構出現變更,則需要手動刪除目標 ES 庫,重建同步任務。
如果我們搭建一箇中間層,將多表 JOIN 結果先寫入一個冗餘的 MySQL 寬表,再同步到 ES,則數據同步可以使用簡單高效的 DTS。代價是整個同步鏈路環節增多,不穩定性增加了。
ES 套件 Logstash:
是 ES 官方套件系列中的數據同步工具。支持在 config 配置文件中寫入需要的 SQL 邏輯並存儲爲視圖,並通過視圖來寫入多表查詢的結果到 ES。這種方式自由度較高,能夠方便地構建所需要的數據。但是數據同步的性能略差(秒級別)。
CREATE VIEW my_view AS
SELECT sv.*, s.score, sc.category
FROM service_version sv
JOIN service s ON sv.service_id = s.service_id
JOIN service_category sc ON s.service_id = sc.service_id;
其他數據同步工具選型指南:
https://help.aliyun.com/zh/es/use-cases/select-a-synchronization-method
參考:
[1] 網易基於 Elasticsearch 構建通用搜索系統的實踐 - 分享 - Elastic 中文社區:https://elasticsearch.cn/slides/243#page=20
[2] RDSMySQL 同步方案_檢索分析服務 Elasticsearch 版 - 阿里雲幫助中心:https://help.aliyun.com/zh/es/use-cases/select-a-synchronization-method
[3] Elasticsearch Guide [8.9] | Elastic:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wlh2AHpNLrz9dHxPw9UrkQ