Lucene解析 - 基本概念

前言

Apache Lucene是一個開源的高性能、可擴展的信息檢索引擎,提供了強大的數據檢索能力。Lucene已經發展了不少年,其功能愈來愈強大,架構也愈來愈精細。它目前不只僅能支持全文索引,也可以提供多種其餘類型的索引方式,來知足不一樣類型的查詢需求。數據庫

基於Lucene的開源項目有不少,最知名的要屬Elasticsearch和Solr,若是說Elasticsearch和Solr是一輛設計精美、性能卓越的跑車,那Lucene就是爲其提供強大動力的引擎。爲了駕馭這輛跑車讓它跑的更快更穩定,咱們須要對它的引擎研究透徹。數據結構

在此以前咱們在專欄已經發表了多篇文章來剖析Elasticsearch的數據模型、讀寫路徑、分佈式架構以及Data/Meta一致性等問題,這篇文章以後咱們會陸續發表一系列的關於Lucene的原理和源碼解讀,來全面解析Lucene的數據模型和數據讀寫路徑。架構

Lucene官方對本身的優點總結爲幾點:
Scalable, High-Performance Indexing
Powerful, Accurate and Efficient Search Algorithms
但願經過咱們的系列文章,可以讓讀者理解Lucene是如何達到這些目標的。elasticsearch

整個分析會基於Lucene 7.2.1版本,在讀這篇文章以前,須要有必定的知識基礎,例如瞭解基本的搜索和索引原理,知道什麼是倒排、分詞、相關性等基本概念,瞭解Lucene的基本使用,例如Directory、IndexWriter、IndexSearcher等。分佈式

基本概念

在深刻解讀Lucene以前,先了解下Lucene的幾個基本概念,以及這幾個概念背後隱藏的一些東西。性能

clipboard.png

如圖是一個Index內的基本組成,Segment內數據只是一個抽象表示,不表明其內部真實數據結構。優化

Index(索引)
相似數據庫的表的概念,可是與傳統表的概念會有很大的不一樣。傳統關係型數據庫或者NoSQL數據庫的表,在建立時至少要定義表的Scheme,定義表的主鍵或列等,會有一些明肯定義的約束。而Lucene的Index,則徹底沒有約束。Lucene的Index能夠理解爲一個文檔收納箱,你能夠往內部塞入新的文檔,或者從裏面拿出文檔,但若是你要修改裏面的某個文檔,則必須先拿出來修改後再塞回去。這個收納箱能夠塞入各類類型的文檔,文檔裏的內容能夠任意定義,Lucene都能對其進行索引。ui

Document(文檔)
相似數據庫內的行或者文檔數據庫內的文檔的概念,一個Index內會包含多個Document。寫入Index的Document會被分配一個惟一的ID,即Sequence Number(更多被叫作DocId),關於Sequence Number後面會再細說。this

Field(字段)
一個Document會由一個或多個Field組成,Field是Lucene中數據索引的最小定義單位。Lucene提供多種不一樣類型的Field,例如StringField、TextField、LongFiled或NumericDocValuesField等,Lucene根據Field的類型(FieldType)來判斷該數據要採用哪一種類型的索引方式(Invert Index、Store Field、DocValues或N-dimensional等),關於Field和FieldType後面會再細說。編碼

Term和Term Dictionary
Lucene中索引和搜索的最小單位,一個Field會由一個或多個Term組成,Term是由Field通過Analyzer(分詞)產生。Term Dictionary即Term詞典,是根據條件查找Term的基本索引。

Segment
一個Index會由一個或多個sub-index構成,sub-index被稱爲Segment。Lucene的Segment設計思想,與LSM相似但又有些不一樣,繼承了LSM中數據寫入的優勢,可是在查詢上只能提供近實時而非實時查詢。

Lucene中的數據寫入會先寫內存的一個Buffer(相似LSM的MemTable,可是不可讀),當Buffer內數據到必定量後會被flush成一個Segment,每一個Segment有本身獨立的索引,可獨立被查詢,但數據永遠不能被更改。這種模式避免了隨機寫,數據寫入都是Batch和Append,能達到很高的吞吐量。Segment中寫入的文檔不可被修改,但可被刪除,刪除的方式也不是在文件內部原地更改,而是會由另一個文件保存須要被刪除的文檔的DocID,保證數據文件不可被修改。Index的查詢須要對多個Segment進行查詢並對結果進行合併,還須要處理被刪除的文檔,爲了對查詢進行優化,Lucene會有策略對多個Segment進行合併,這點與LSM對SSTable的Merge相似。

Segment在被flush或commit以前,數據保存在內存中,是不可被搜索的,這也就是爲何Lucene被稱爲提供近實時而非實時查詢的緣由。讀了它的代碼後,發現它並非不能實現數據寫入便可查,只是實現起來比較複雜。緣由是Lucene中數據搜索依賴構建的索引(例如倒排依賴Term Dictionary),Lucene中對數據索引的構建會在Segment flush時,而非實時構建,目的是爲了構建最高效索引。固然它可引入另一套索引機制,在數據實時寫入時即構建,但這套索引實現會與當前Segment內索引不一樣,須要引入額外的寫入時索引以及另一套查詢機制,有必定複雜度。

Sequence Number
Sequence Number(後面統一叫DocId)是Lucene中一個很重要的概念,數據庫內經過主鍵來惟一標識一行,而Lucene的Index經過DocId來惟一標識一個Doc。不過有幾點要特別注意:
DocId實際上並不在Index內惟一,而是Segment內惟一,Lucene這麼作主要是爲了作寫入和壓縮優化。那既然在Segment內才惟一,又是怎麼作到在Index級別來惟一標識一個Doc呢?方案很簡單,Segment之間是有順序的,舉個簡單的例子,一個Index內有兩個Segment,每一個Segment內分別有100個Doc,在Segment內DocId都是0-100,轉換到Index級的DocId,須要將第二個Segment的DocId範圍轉換爲100-200。
DocId在Segment內惟一,取值從0開始遞增。但不表明DocId取值必定是連續的,若是有Doc被刪除,那可能會存在空洞。
一個文檔對應的DocId可能會發生變化,主要是發生在Segment合併時。

Lucene內最核心的倒排索引,本質上就是Term到全部包含該Term的文檔的DocId列表的映射。因此Lucene內部在搜索的時候會是一個兩階段的查詢,第一階段是經過給定的Term的條件找到全部Doc的DocId列表,第二階段是根據DocId查找Doc。Lucene提供基於Term的搜索功能,也提供基於DocId的查詢功能。

DocId採用一個從0開始底層的Int32值,是一個比較大的優化,同時體如今數據壓縮和查詢效率上。例如數據壓縮上的Delta策略、ZigZag編碼,以及倒排列表上採用的SkipList等,這些優化後續會詳述。

索引類型

Lucene中支持豐富的字段類型,每種字段類型肯定了支持的數據類型以及索引方式,目前支持的字段類型包括LongPoint、TextField、StringField、NumericDocValuesField等。

clipboard.png

如圖是Lucene中對於不一樣類型Field定義的一個基本關係,全部字段類都會繼承自Field這個類,Field包含3個重要屬性:name(String)、fieldsData(BytesRef)和type(FieldType)。name即字段的名稱,fieldsData即字段值,全部類型的字段的值最終都會轉換爲二進制字節流來表示。type是字段類型,肯定了該字段被索引的方式。
FieldType是一個很重要的類,包含多個重要屬性,這些屬性的值決定了該字段被索引的方式。
Lucene提供的多種不一樣類型的Field,本質區別就兩個:一是不一樣類型值到fieldData定義了不一樣的轉換方式;二是定義了FieldType內不一樣屬性不一樣取值的組合。這種模式下,你也可以經過自定義數據以及組合FieldType內索引參數來達到定製類型的目的。
要理解Lucene可以提供哪些索引方式,只須要理解FieldType內每一個屬性的具體含義,咱們來一個一個看:
stored: 表明是否須要保存該字段,若是爲false,則lucene不會保存這個字段的值,而搜索結果中返回的文檔只會包含保存了的字段。
tokenized: 表明是否作分詞,在lucene中只有TextField這一個字段須要作分詞。
termVector: 這篇文章很好的解釋了term vector的概念,簡單來講,term vector保存了一個文檔內全部的term的相關信息,包括Term值、出現次數(frequencies)以及位置(positions)等,是一個per-document inverted index,提供了根據docid來查找該文檔內全部term信息的能力。對於長度較小的字段不建議開啓term verctor,由於只須要從新作一遍分詞便可拿到term信息,而針對長度較長或者分詞代價較大的字段,則建議開啓term vector。Term vector的用途主要有兩個,一是關鍵詞高亮,二是作文檔間的類似度匹配(more-like-this)。
omitNorms: Norms是normalization的縮寫,lucene容許每一個文檔的每一個字段都存儲一個normalization factor,是和搜索時的相關性計算有關的一個係數。Norms的存儲只佔一個字節,可是每一個文檔的每一個字段都會獨立存儲一份,且Norms數據會所有加載到內存。因此若開啓了Norms,會消耗額外的存儲空間和內存。但若關閉了Norms,則沒法作index-time boosting(elasticsearch官方建議使用query-time boosting來替代)以及length normalization。
indexOptions: Lucene提供倒排索引的5種可選參數(NONE、DOCS、DOCS_AND_FREQS、DOCS_AND_FREQS_AND_POSITIONS、DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS),用於選擇該字段是否須要被索引,以及索引哪些內容。
docValuesType: DocValue是Lucene 4.0引入的一個正向索引(docid到field的一個列存),大大優化了sorting、faceting或aggregation的效率。DocValues是一個強schema的存儲結構,開啓DocValues的字段必須擁有嚴格一致的類型,目前Lucene只提供NUMERIC、BINARY、SORTED、SORTED_NUMERIC和SORTED_SET五種類型。
dimension:Lucene支持多維數據的索引,採起特殊的索引來優化對多維數據的查詢,這類數據最典型的應用場景是地理位置索引,通常經緯度數據會採起這個索引方式。

來看下Lucene中對StringField的一個定義:

clipboard.png

StringFiled有兩種類型索引定義,TYPE_NOT_STORED和TYPE_STORED,惟一的區別是這個Field是否須要Store。從其餘的幾個屬性也能夠解讀出,StringFiled選擇omitNorms,須要進行倒排索引而且不須要被分詞。

Elasticsearch數據類型

Elasticsearch內對用戶輸入文檔內Field的索引,也是按照Lucene能提供的幾種模式來提供。除了用戶能自定義的Field,Elasticsearch還有本身預留的系統字段,用做一些特殊的目的。這些字段映射到Lucene本質上也是一個Field,與用戶自定義的Field無任何區別,只不過Elasticsearch根據這些系統字段不一樣的使用目的,定製有不一樣的索引方式。

clipboard.png

舉個例子,上圖​是Elasticsearch內兩個系統字段_version和_uid的FieldType定義,咱們來解讀下它們的索引方式。Elasticsearch經過_uid字段惟一標識一個文檔,經過_version字段來記錄該文檔當前的版本。從這兩個字段的FieldType定義上能夠看到,_uid字段會作倒排索引,不須要分詞,須要被Store。而_version字段則不須要被倒排索引,也不須要被Store,可是須要被正排索引。很好理解,由於_uid須要被搜索,而_version不須要。但_version須要經過docId來查詢,並且Elasticsearch內versionMap內須要經過docId作大量查詢且只須要查詢出_version字段,因此_version最合適的是被正排索引。

關於Elasticsearch內系統字段全面的解析,能夠看下這篇文章。

總結

這篇文章主要介紹了Lucene的一些基本概念以及提供的索引類型。後續咱們會有一系列文章來解析Lucene提供的IndexWriter的寫入流程,其In-Memory Buffer的結構以及持久化後的索引文件結構,來了解Lucene爲什麼能達到如此高效的數據索引性能。也會去解析IndexSearcher的查詢流程,以及一些特殊的查詢優化的數據結構,來了解爲什麼Lucene能提供如此高效的搜索和查詢。

相關文章
相關標籤/搜索