Elasticsearch內核解析 - 數據模型篇
Elasticsearch是一個實時的分散式搜索和分析引擎,它可以幫助我們用很快的速度去處理大規模數據,可以用於全文檢索、結構化檢索、推薦、分析以及統計聚合等多種場景。
Elasticsearch是一個建立在全文搜索引擎庫Apache Lucene 基礎上的分散式搜索引擎,Lucene最早的版本是2000年發布的,距今已經18年,是當今最先進,最高效的全功能開源搜索引擎框架,眾多搜索領域的系統都基於Lucene開發,比如Nutch,Solr和Elasticsearch等。Elasticsearch第一個版本發佈於2010年,發布後就以非常快的速度霸佔了開源搜索系統領域,成為目前搜索領域的首選,著名的維基百科,GitHub和Stack Overflow都在使用它。
既然有Lucene娥,為啥還會出現很火的Elasticsearch?回答這個問題之前, 我們先來簡單看一下Lucene中的一些數據模型:
Lucene數據模型
Lucene中包含了四種基本數據類型,分別是:
- Index:索引,由很多的Document組成。
- Document:由很多的Field組成,是Index和Search的最小單位。
- Field:由很多的Term組成,包括Field Name和Field Value。
- Term:由很多的位元組組成,可以分詞。
上述四種類型在Elasticsearch中同樣存在,意思也一樣。
Lucene中存儲的索引主要分為三種類型:
- Invert Index:倒排索引,或者簡稱Index,通過Term可以查詢到擁有該Term的文檔。可以配置為是否分詞,如果分詞可以配置不同的分詞器。索引存儲的時候有多種存儲類型,分別是:
- DOCS:只存儲DocID。
- DOCS_AND_FREQS:存儲DocID和詞頻(Term Freq)。
- DOCS_AND_FREQS_AND_POSITIONS:存儲DocID、詞頻(Term Freq)和位置。
- DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:存儲DocID、詞頻(Term Freq)、位置和偏移。
- DocValues:正排索引,採用列式存儲。通過DocID可以快速讀取到該Doc的特定欄位的值。由於是列式存儲,性能會比較好。一般用於sort,agg等需要高頻讀取Doc欄位值的場景。
- Store:欄位原始內容存儲,同一篇文章的多個Field的Store會存儲在一起,適用於一次讀取少量且多個欄位內存的場景,比如摘要等。
Lucene中提供索引和搜索的最小組織形式是Segment,Segment中按照索引類型不同,分成了Invert Index,Doc Values和Store這三大類(還有一些輔助類,這裡省略),每一類裡面都是按照Doc為最小單位存儲。Invert Index中存儲的Key是Term,Value是Doc ID的鏈表;Doc Value中Key 是Doc ID和Field Name,Value是Field Value;Store的Key是Doc ID,Value是Filed Name和Filed Value。
由於Lucene中沒有主鍵概念和更新邏輯,所有對Lucene的更新都是Append一個新Doc,類似於一個只能Append的隊列,所有Doc都被同等對等,同樣的處理方式。其中的Doc由眾多Field組成,沒有特殊Field,每個Field也都被同等對待,同樣的處理方式。
從上面介紹來看,Lucene只是提供了一個索引和查詢的最基本的功能,距離一個完全可用的完整搜索引擎還有一些距離:
Lucene的不足
- Lucene是一個單機的搜索庫,如何能以分散式形式支持海量數據?
- Lucene中沒有更新,每次都是Append一個新文檔,如何做部分欄位的更新?
- Lucene中沒有主鍵索引,如何處理同一個Doc的多次寫入?
- 在稀疏列數據中,如何判斷某些文檔是否存在特定欄位?
- Lucene中生成完整Segment後,該Segment就不能再被更改,此時該Segment才能被搜索,這種情況下,如何做實時搜索?
上述幾個問題,對於搜索而言都是至關重要的功能訴求,我們接下來看看Elasticsearch中是如何來解這些問題的。
Elasticsearch怎麼做
在Elasticsearch中,為了支持分散式,增加了一個系統欄位_routing(路由),通過_routing將Doc分發到不同的Shard,不同的Shard可以位於不同的機器上,這樣就能實現簡單的分散式了。
採用類似的方式,Elasticsearch增加了_id、_version、_source和_seq_no等等多個系統欄位,通過這些Elasticsearch中特有的系統欄位可以有效解決上述的幾個問題,新增的系統欄位主要是下列幾個:
下面我們逐個欄位的剖析下上述系統欄位的作用,先來看第一個_id欄位:
1. _id
Doc的主鍵,在寫入的時候,可以指定該Doc的ID值,如果不指定,則系統自動生成一個唯一的UUID值。
Lucene中沒有主鍵索引,要保證系統中同一個Doc不會重複,Elasticsearch引入了_id欄位來實現主鍵。每次寫入的時候都會先查詢id,如果有,則說明已經有相同Doc存在了。
通過_id值(ES內部轉換成_uid)可以唯一在Elasticsearch中確定一個Doc。
Elasticsearch中,_id只是一個用戶級別的虛擬欄位,在Elasticsearch中並不會映射到Lucene中,所以也就不會存儲該欄位的值。
_id的值可以由_uid解析而來(_uid =type + # + id),Elasticsearch中會存儲_uid。
2. _uid
_uid的格式是:type + # + id。
_uid會存儲在Lucene中,在Lucene中的映射關係如下:dex下可能存在多個id值相同的Doc,而6.0.0之後只支持單Type,同Index下id值是唯一的。
uid會存儲在Lucene中,在Lucene中的映射關係如下:
_uid 只是存儲了倒排Index和原文store:倒排Index的目的是可以通過_id快速查詢到文檔;原文store用來在返回的Response裡面填充完整的_id值。
在Lucene中存儲_uid,而不是_id的原因是,在6.0.0之前版本裡面,_uid可以比_id表示更多的信息,比如Type。在6.0.0版本之後,同一個Index只能有一個Type,這時候Type就沒多大意義了,後面Type應該會消失,那時候_id就會和_uid概念一樣,到時候兩者會合二為一,也能簡化大家的理解。
3. _version
Elasticsearch中每個Doc都會有一個Version,該Version可以由用戶指定,也可以由系統自動生成。如果是系統自動生成,那麼每次Version都是遞增1。
_version是實時的,不受搜索的近實時性影響,原因是可以通過_uid從內存中versionMap或者TransLog中讀取到。
Version在Lucene中也是映射為一個特殊的Field存在。
Elasticsearch中Version欄位的主要目的是通過doc_id讀取Version,所以Version只要存儲為DocValues就可以了,類似於KeyValue存儲。
Elasticsearch通過使用version來保證對文檔的變更能以正確的順序執行,避免亂序造成的數據丟失:
- 首次寫入Doc的時候,會為Doc分配一個初始的Version:V0,該值根據VersionType不同而不同。
- 再次寫入Doc的時候,如果Request中沒有指定Version,則會先加鎖,然後去讀取該Doc的最大版本V1,然後將V1+1後的新版本號寫入Lucene中。
- 再次寫入Doc的時候,如果Request中指定了Version:V2,則繼續會先加鎖,然後去讀該Doc的最大版本V2,判斷V1==V2,如果不相等,則發生版本衝突。否則版本吻合,繼續寫入Lucene。
- 當做部分更新的時候,會先通過GetRequest讀取當前id的完整Doc和V1,接著和當前Request中的Doc合併為一個完整Doc。然後執行一些邏輯後,加鎖,再次讀取該Doc的最大版本號V2,判斷V1==V2,如果不相等,則在剛才執行其他邏輯時被其他線程更改了當前文檔,需要報錯後重試。如果相等,則期間沒有其他線程修改當前文檔,繼續寫入Lucene中。這個過程就是一個典型的read-then-update事務。
4. _source
Elasticsearch中有一個重要的概念是source,存儲原始文檔,也可以通過過濾設置只存儲特定Field。
Source在Lucene中也是映射為了一個特殊的Field存在:
Elasticsearch中_source欄位的主要目的是通過doc_id讀取該文檔的原始內容,所以只需要存儲Store即可。
_source其實是將文檔中所有Field都打包到一個名為_source的虛擬Field,然後存儲為Store類型。
Elasticsearch中使用_source欄位可以實現以下功能:
- Update:部分更新時,需要從文檔讀取到保存在_source欄位中的原文,然後和請求中的部分欄位合併為一個完整文檔。如果沒有_source,則不能完成部分欄位的Update操作。
- Rebuild:最新的版本中新增了rebuild介面,可以通過Rebuild API完成索引重建,過程中不需要從其他系統導入全量數據,而是從當前文檔的_source中讀取。如果沒有_source,則不能使用Rebuild API。
- Script:不管是Index還是Search的Script,都可能用到存儲在Store中的原始內容,如果禁用了_source,則這部分功能不再可用。
- Summary:摘要信息也是來源於_source欄位。
5. _seq_no
嚴格遞增的順序號,每個文檔一個,Shard級別嚴格遞增,保證後寫入的Doc的_seq_no大於先寫入的Doc的_seq_no。
任何類型的寫操作,包括index、create、update和Delete,都會生成一個_seq_no。
_seq_no在Primary Node中由SequenceNumbersService生成,但其實真正產生這個值的是LocalCheckpointTracker,每次遞增1:
/** * The next available sequence number. */ private volatile long nextSeqNo; /** * Issue the next sequence number. * * @return the next assigned sequence number */ synchronized long generateSeqNo() { return nextSeqNo++; }
每個文檔在使用Lucene的document操作介面之前,會獲取到一個_seq_no,這個_seq_no會以系統保留Field的名義存儲到Lucene中,文檔寫入Lucene成功後,會標記該seq_no為完成狀態,這時候會使用當前seq_no更新local_checkpoint。
checkpoint分為local_checkpoint和global_checkpoint,主要是用於保證有序性,以及減少Shard恢復時數據拷貝的數據拷貝量,更詳細的介紹可以看這篇文章:Sequence IDs: Coming Soon to an Elasticsearch Cluster Near You。
_seq_no在Lucene中的映射:
Elasticsearch中_seq_no的作用有兩個,一是通過doc_id查詢到該文檔的seq_no,二是通過seq_no範圍查找相關文檔,所以也就需要存儲為Index和DocValues(或者Store)。由於是在衝突檢測時才需要讀取文檔的_seq_no,而且此時只需要讀取_seq_no,不需要其他欄位,這時候存儲為列式存儲的DocValues比Store在性能上更好一些。
_seq_no是嚴格遞增的,寫入Lucene的順序也是遞增的,所以DocValues存儲類型可以設置為Sorted。
另外,_seq_no的索引應該僅需要支持存儲DocId就可以了,不需要FREQS、POSITIONS和分詞。如果多存儲了這些,對功能也沒影響,就是多佔了一點資源而已。
6. _primary_term
_primary_term也和_seq_no一樣是一個整數,每當Primary Shard發生重新分配時,比如重啟,Primary選舉等,_primary_term會遞增1。
_primary_term主要是用來恢複數據時處理當多個文檔的_seq_no一樣時的衝突,避免Primary Shard上的寫入被覆蓋。
Elasticsearch中_primary_term只需要通過doc_id讀取到即可,所以只需要保存為DocValues就可以了.
7. _routing
路由規則,寫入和查詢的routing需要一致,否則會出現寫入的文檔沒法被查到情況。
在mapping中,或者Request中可以指定按某個欄位路由。默認是按照_Id值路由。
_routing在Lucene中映射為:
Elasticsearch中文檔級別的_routing主要有兩個目的,一是可以查詢到使用某種_routing的文檔有哪些,當發生_routing變化時,可以對歷史_routing的文檔重新讀取再Index,這個需要倒排Index。另一個是查詢到文檔後,在Response裡面展示該文檔使用的_routing規則,這裡需要存儲為Store。
8. _field_names
該欄位會索引某個Field的名稱,用來判斷某個Doc中是否存在某個Field,用於exists或者missing請求。
_field_names在Lucene中的映射:
Elasticsearch中_field_names的目的是查詢哪些Doc的這個Field是否存在,所以只需要倒排Index即可。
總結
在上面的介紹中,我們解釋了Elasticsearch是如何通過增加系統欄位來擴充Lucene的功能,開篇提出的Lucene的多個不足中,前四個都在文章中做了說明,最後一個沒法通過增加系統欄位實現,我們將會在下一篇《Elasticsearch寫流程簡介》中介紹如何通過其他方式來實現,下一篇見。
另外,我們招人:Elasticsearch和Lucene的開發,有興趣的可以私信聯繫我。
推薦閱讀:
※現在最成熟的開源nosql是什麼?分別有什麼優缺點?
※為什麼 Cassandra 的寫速度比 MySQL 快?
※Redis 應該如何節約使用內存?有什麼好的設計策略和好的方法?
※如何評價SequoiaDB巨杉資料庫?
※如何評價RethinkDB?和MongoDB,Redis有什麼區別?
TAG:Elasticsearch | NoSQL | Lucene |