一、Elasticsearch技術簡介
Elastic本身也是一個分佈式存儲系統,如同其他分佈式系統一樣,我們經常關注的一些特性如下。
- 數據可靠性:通過分片副本和事務日誌機制保障數據安全
- 服務可用性:在可用性和一致性的取捨方面,默認情況下Elastic更傾向於可用性,只要主分片可用即可執行寫入操作
- 一致性:弱一致性。只要主分片寫成功,數據就可能被讀取。因此讀取操作在主分片和副本分片上可能會得到不同的結果
- 原子性:索引的讀寫、別名更新是原子操作,不會出現中間狀態。但Bulk不是原子操作。不能用來實現事務
- 擴展性:主副本分片都可以承擔讀請求,分擔系統負載
1.1 Elasticsearch與MySQL的關係
RDBMS | Elasticsearch |
---|---|
Table | Index(Type) |
Row | Document |
Column | Field |
Schema | Mapping |
SQL | DSL |
1.1.1 Mapping
索引結構
1.1.2 DSL
查詢語句
1.1.3 倒排索引
正排索引: 文檔ID -> 文檔內容
倒排索引:文檔內容 -> 文檔ID
文檔ID | 文檔內容 |
---|---|
1 | Mastering Elasticsearch |
2 | Elasticsearch Server |
3 | Elasticsearch Essentials |
Term | Count | DocumentId:Position |
---|---|---|
Elasticsearch | 3 | 1:1,2:0,3:0 |
Mastering | 1 | 1:0 |
Server | 1 | 2:1 |
Essentials | 1 | 3:1 |
倒排索引的核心組成
- 倒排索引包含兩個部分
- 單詞詞典(Term Dictionary),記錄所有文檔的單詞,記錄單詞到倒排列表的關聯關係
- 單詞詞典一般比較大,可以通過B+樹或哈希拉鍊法實現,以滿足高性能的插入與查詢
- 倒排列表(Posting List)記錄了單詞對應的文檔結合,由倒排索引項組成
- 倒排索引項(Posting)
- 文檔ID
- 詞頻TF - 該單詞在文檔中出現的次數,用於相關性評分
- 位置(Position)- 單詞在文檔中分詞的位置。用於語句搜索(phrase query)
- 便宜(Offset)- 記錄單詞的開始結束位置,實現高亮顯示
- 倒排索引項(Posting)
- 單詞詞典(Term Dictionary),記錄所有文檔的單詞,記錄單詞到倒排列表的關聯關係
文檔ID | 文檔內容 |
---|---|
1 | Mastering Elasticsearch |
2 | Elasticsearch Server |
3 | Elasticsearch Essentials |
Posting List
DocId | TF | Position | Offset |
---|---|---|---|
1 | 1 | 1 | <10,23> |
2 | 1 | 0 | <0,13> |
3 | 1 | 0 | <0,13> |
- Elasticsearch的JSON文檔中的每個字段,都有自己的倒排索引
- 可以指定對某些字段不做索引
- 優點:節省存儲空間
- 缺點:字段無法被搜索
1.1.4 Lucene字典數據結構FST
常見的詞典數據結構:
名稱 | 特點 |
---|---|
排序列表Array/List | 使用二分法查找,不平衡 |
HashMap/TreeMap | 性能高,內存消耗大,幾乎是原始數組的三倍 |
Skip List | 跳躍表,可快速查找詞語,在Lucene、Redis、HBase等均有實現。相對於TreeMap等結構,特別適合高併發場景 |
Trie | 適合英文詞典,如果系統中存在大量字符串且這些字符串基本沒有公共前綴,則相應的trie樹將非常消耗內存 |
Double Array Trie | 適合做中文詞典,內存佔用小,很多分詞工具均採用此算法 |
Ternary Search Tree | 三叉樹,每一個node有3個節點,兼具省空間和查詢快的優點 |
Finite State Transducers(FST) | 一種有限狀態機,Luncene 4有開源實現,並大量使用 |
FST數據結構:
插入單詞“cat”、”deep“、”do“、”dog“、”dogs“
1.2 Elasticsearch基本概念
高可用高擴展的分佈式搜索引擎——Elasticsearch
1.2.1 節點
節點即一個Elasticsearch的實例,本質上就是一個Java進程,一臺機器上可以運行多個Elasticsearch進程,但是生產環境一般建議一臺機器上只運行一個Elasticsearch實例
Master-eligible節點和Master節點
- 每個節點啓動後默認就是一個Master-eligible節點,該類型節點可以參加選主流程,成爲Master節點。
- 當第一個節點啓動的時候,它會將自己選舉成Master節點
- 每個節點都保存了集羣的狀態,但是隻有Master節點可以修改集羣的狀態信息(如所有節點的信息、所有的索引以及其相關的Mapping、Setting信息、分片路由信息等)
Data節點和Coordinating節點
-
Data節點
- 可以保存數據的節點,叫做Data Node。負責保存分片數據。在數據擴展上起到了至關重要的作用。
-
Coordinating節點
- 負責接收Client的請求,將請求分發到合適的節點,最終把結果彙集到一起
- 每個節點默認都起到了Coordinating Node的職責
-
Ingest 節點
- 數據前置處理轉換節點,支持pipeline管道設置
- 可以使用ingest節點對數據進行過濾、轉換等操作
- 每個節點默認都起到了該職責,即在文檔進入索引前做預處理
Hot節點和Warm節點
不同硬件配置的Data Node,用來實現Hot & Warm 架構,降低集羣部署成本
分片
又稱爲主分片,用以解決數據水平擴展問題。通過主分片,可以將數據分佈到集羣內的所有節點之上。
- 一個分片是一個運行Lucene的實例
- 主分片數在索引創建時指定,後續不允許修改,除非Reindex
副本
用以解決數據高可用問題,是主分片的拷貝。
- 副本分片數可以動態調整
- 增加副本數,可以在一定程度上提高服務的可用性(讀取的吞吐)
1.2.2 水平擴展
1.2.3 寫入流程
(1)客戶端向NODE1發送寫請求。
(2)NODE1使用文檔ID來確定文檔屬於分片0,通過集羣狀態中的內容路由表信息獲知分片0的主分片位於NODE3,因此請求被轉發到NODE3上。
(3)NODE3上的主分片執行寫操作。如果寫入成功,則它將請求並行轉發到 NODE1和NODE2的副分片上,等待返回結果。當所有的副分片都報告成功,NODE3將向協調節點報告成功,協調節點再向客戶端報告成功。
在客戶端收到成功響應時,意味着寫操作已經在主分片和所有副分片都執行完成。
1.2.4 查詢流程
(1)客戶端向NODE1發送讀請求。
(2)NODE1使用文檔ID來確定文檔屬於分片0,通過集羣狀態中的內容路由表信息獲知分片0有三個副本數據,位於所有的三個節點中,此時它可以將請求發送到任意節點,這裏它將請求轉發到NODE2。
(3)NODE2將文檔返回給 NODE1,NODE1將文檔返回給客戶端。
1.2.5 搜索流程
(1)客戶端發送search請求到NODE 3。
(2)NODE 3將查詢請求轉發到索引的每個主分片或副分片中。
(3)每個分片在本地執行查詢,並使用本地的Term/Document Frequency信息進行打分,添加結果到大小爲from + size的本地有序優先隊列中。
(4)每個分片返回各自優先隊列中所有文檔的ID和排序值給協調節點,協調節點合併這些值到自己的優先隊列中,產生一個全局排序後的列表。
1.2.6 動態索引
- 新增加字段
- Dynamic 設置爲true時,一旦有新增字段的文檔寫入,Mapping也同時被更新
- Dynamic 設置爲false,Mapping不會被更新,新增字段的數據無法被索引,但是信息會出現在_source中
- Dynamic 設置爲Strict,文檔寫入失敗
- 對已有字段,一旦已經有數據寫入,就不再支持修改字段定義
- Lucene實現的倒排索引,一旦生成後,就不允許修改
- 如果希望改變字段類型,使用Reindex API,重建索引
- 因爲如果修改了字段的數據類型,會導致已被索引的數據無法被搜索
- 如果是增加新的字段,就不會有這樣的影響
1.2.7 數據建模
1.2.7.1 Elasticsearch中處理關聯關係
- 對象類型
- 嵌套對象(Nested Object)
- 父子關聯關係(Parent / Child)
- 應用端關聯
對象類型
因爲Elasticsearch會把JSON打平(扁平式鍵值對結構),所以能夠搜索到名稱爲”John“,年齡爲31的文檔,因爲這些數據都能夠被搜索到。(對象之間沒有界限)
Nested Data Type
- Nested數據類型:允許對象數組中的對象被獨立索引
- 使用nested和properties關鍵字,將所有actors索引到多個分隔的文檔
- 在內部,Nested文檔會被保存在兩個Lucene文檔中,在查詢時做Join處理
如下對對象設置了“nested”類型,則不再能夠搜索到”不正確的數據”了。
父子關聯關係
- 對象和Nested對象的侷限性
- 每次更新,需要重新索引整個對象(包括根對象和嵌套對象)
- ES提供了類似關係型數據庫中Join的實現。使用Join數據類型實現,可以通過維護Parent/Child的關係,從而分離兩個對象
- 父文檔和子文檔是兩個獨立的文檔
- 更新父文檔無需重新索引子文檔。子文檔被添加,更新或者刪除也不會影響到父文檔和其他的子文檔
- 父文檔和子文檔必須在相同的分片上 -> 確保查詢join的性能
- 當指定子文檔的時候,必須指定它的父文檔Id -> 使用route參數保證分配到相同分片上
Nested Object | Parent / Child | |
---|---|---|
優點 | 文檔存儲在一起,讀取性能高 | 父子文檔可以獨立更新 |
缺點 | 更新嵌套的子文檔時,需要更新整個文檔 | 需要額外的內存維護關係。讀取性能相對差 |
適用場景 | 子文檔偶爾更新,以查詢爲主 | 子文檔更新頻繁 |
2.4 優化手段
2.4.1 深度分頁
2.4.1.1 FROM+SIZE
這種分頁方式,當FROM+SIZE > 10000的時候,Elasticsearch會報錯,因爲這裏它有個默認分頁窗口設置(當然也可以修改,一般不建議修改)。
- ES天生就是分佈式的。查詢信息的時候需要從多個分片(多臺機器)上拉取數據,並且ES天生就需要滿足排序的需要(按照相關性算分)
- 當一個查詢: From = 990, Size = 10
- 會在每個分片上都獲取1000個文檔。然後,通過Coordinating Node聚合所有結果。最後再通過排序選取前1000個文檔
- 頁數越深,佔用內存越多。爲了避免深度分頁帶來的內存開銷。ES有一個設定,默認限定到10000個文檔。
- Index.max.result.window
特別注意:如果你的查詢沒有指定from,size的話ES默認會限制爲from,size=0,10。
提示:from是指偏移量,不是第幾頁,與MySQL的limit後的兩個參數一樣。(我就腦瓜子疼了很久,剛開始一直把from當頁碼。。)
2.4.1.2 SearchAfter
- 避免深度分頁的性能問題,可以實時獲取下一頁文檔信息
- 不支持指定偏移量
- 只能繼續向後偏移翻頁
- 第一步搜索需要指定sort,並且保證值是唯一的(可以通過加入_id保證唯一性)
- 然後使用上一次查詢的結果集中,最後一個文檔的sort值繼續進行查詢
關鍵點:根據提供的排序屬性排序後的sort值爲依據,向後繼續翻頁。類似於MySQL中,LIMIT 10000,30。我拿到了第9999條數據的id值,然後SELECT * FROM a WHERE id > 9999 LIMIT 30。
特別注意:SearchAfter這種特性,很顯然不支持跳頁,但是它也是能夠實時向後翻頁的,而接下來介紹的Scoll翻頁方式就不支持實時。
2.4.1.3 ScollAPI
- 創建一個快照,有新的數據寫入以後,無法被查到
- 每次查詢後,輸入上一次的Scoll Id
我理解和SearchAfter類似,一個是通過傳遞上一次的排序值,一個是通過傳遞上一次的Scoll值。不同的是,SearchAfter是實時的,而Scoll方式對翻頁過程中有數據變更是無感知的。
分頁總結
一般不建議深度分頁,儘可能讓業務增加時間範圍,減少搜索範圍,或者說直接使用另外兩種,滾動分頁即可。需要注意的是後兩者對數據變化的感知是不一樣的,具體需要根據場景來選擇分頁方式。
思考:Scoll是快照,即那一瞬間的快照,所以翻頁是在快照中自己玩,對數據的變化無感知了,那麼如果數據量很大,會不會把內存玩脫。
代碼片段
PUT nested_index
{
"mappings": {
"properties": {
"actors" : {
"type" : "nested",
"properties": {
"first_name" : {"type" : "keyword"},
"last_name" : {"type" : "keyword"}
}
},
"title": {
"type" : "text",
"fields" : {"keyword": {"type" : "keyword","ignore_above":256}}
}
}
}
}
PUT nested_index/_doc/1
{
"title": "Speed",
"actors": [
{"first_name": "Keanu","last_name": "Reeves"},
{"first_name": "Dennis","last_name": "Hopper"}
]
}
POST nested_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "Speed"
}
},
{
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{"match": {"actors.first_name": "Keanu"}},
{"match": {"actors.last_name": "Hopper"}}
]
}
}
}
}
]
}
}
}
#指定父子關係,父親爲博客“blog”,子爲評論“comment”
PUT parent_child_index
{
"mappings": {
"properties": {
"blog_comments_relation": {
"type" : "join",
"relations": { "blog": "comment"}
},
"content": {"type":"text"},
"title":{"type": "keyword"}
}
}
}
#索引父文檔,確認自身身份爲“blog”->父文檔
PUT parent_child_index/_doc/blog1
{
"title" : "Learning Elasticsearch",
"content": "hello Elasticsearch",
"blog_comments_relation": {"name":"blog"}
}
#索引子文檔,引用父文檔。
PUT parent_child_index/_doc/comment1?routing=blog1
{
"comment": "I am learning ELK",
"username": "Jack",
"blog_comments_relation": {"name":"comment","parent":"blog1"}
}
PUT parent_child_index/_doc/blog2
{
"title" : "Learning Elasticsearch",
"content": "hello Elasticsearch",
"blog_comments_relation": {"name":"blog"}
}
PUT parent_child_index/_doc/comment2?routing=blog2
{
"comment": "I am learning ELK too",
"username": "Bob",
"blog_comments_relation": {"name":"comment","parent":"blog2"}
}
#查詢所有文檔
POST parent_child_index/_search
{}
#根據Parent Id查詢
POST parent_child_index/_search
{
"query": {
"parent_id":{
"type": "comment",
"id": "blog2"
}
}
}
# Has Child查詢,返回父文檔
POST parent_child_index/_search
{
"query": {
"has_child": {
"type": "comment",
"query": {
"match": {
"username": "Jack"
}
}
}
}
}
# Has Parent查詢,返回相關子文檔
POST parent_child_index/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query": {
"match": {
"title": "Learning Elasticsearch"
}
}
}
}
}