Lucene底層原理和最佳化經驗分享(1)-Lucene簡介和索引原理
基於
Lucene
檢索引擎我們開發了自己的全文檢索系統,承擔起後臺
PB
級、萬億條資料記錄的檢索工作,這裡向大家分享下
Lucene
底層原理研究和一些最佳化經驗。
從兩個方面介紹:
1. Lucene
簡介和索引原理
2. Lucene
最佳化經驗總結
1. Lucene 簡介和索引原理
該部分從三方面展開: Lucene 簡介、索引原理、 Lucene 索引實現。
1.1 Lucene 簡介
Lucene
最初由鼎鼎大名
Doug Cutting
開發,
2000
年開源,現在也是開源全文檢索方案的不二選擇,它的特點概述起來就是:全
Java
實現、開源、高效能、功能完整、易擴充,功能完整體現在對分詞的支援、各種查詢方式(字首、模糊、正則等)、打分高亮、列式儲存(
DocValues
)等等。
而且
Lucene
雖已發展
10
餘年,但仍保持著一個活躍的開發度,以適應著日益增長的資料分析需求,最新的
6.0
版本里引入
block k-d trees
,全面提升了數字型別和地理位置資訊的檢索效能,另基於
Lucene
的
Solr
和
ElasticSearch
分散式檢索分析系統也發展地如火如荼,
ElasticSearch
也在我們專案中有所應用。
結合程式碼說明一下四個步驟:
Document doc=new Document( new StringField("name", "Donald Trump", Field.Store.YES)); //構建索引文件
iw.addDocument(doc); //做索引庫
IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(index)));
IndexSearcher searcher = new IndexSearcher(reader); //開啟索引
Query query = parser.parse("name:trump");//解析查詢
TopDocs results =searcher.search(query, 100);//檢索並取回前100個文件號
for(ScoreDoc hit:results.hits)
{
Document doc=searcher .doc(hit.doc)//真正取文件
}
使用起來很簡單,但只有知道這背後的原理,才能更好地用好 Lucene ,後面將介紹通用檢索原理和 Lucene 的實現細節。
1.2 索引原理
全文檢索技術由來已久,絕大多數都基於倒排索引來做,曾經也有過一些其他方案如檔案指紋。倒排索引,顧名思義,它相反於一篇文章包含了哪些詞,它從詞出發,記載了這個詞在哪些文件中出現過,由兩部分組成 —— 詞典和倒排表。
其中詞典結構尤為重要,有很多種詞典結構,各有各的優缺點,最簡單如排序陣列,透過二分查詢來檢索資料,更快的有雜湊表,磁碟查詢有 B 樹、 B+ 樹,但一個能支援 TB 級資料的倒排索引結構需要在時間和空間上有個平衡,下圖列了一些常見詞典的優缺點:
其中可用的有:
B+
樹、跳躍表、
FST
B+
樹:
mysql
的
InnoDB B+
數結構
優點:外存索引、可更新
缺點:空間大、速度不夠快
跳躍表:
優點:結構簡單、跳躍間隔、級數可控,Lucene3.0之前使用的也是跳躍表結構,後換成了FST,但跳躍表在Lucene其他地方還有應用如倒排表合併和文件號索引。
缺點:模糊查詢支援不好
FST
Lucene
現在使用的索引結構
優點:記憶體佔用率低,壓縮率一般在3倍~20倍之間、模糊查詢支援好、查詢快
缺點:結構複雜、輸入要求有序、更新不易
Lucene裡有個FST的實現,從對外介面上看,它跟Map結構很相似,有查詢,有迭代:
String inputs={"abc","abd","acf","acg"}; //keys
long outputs={1,3,5,7}; //values
FST<Long> fst=new FST<>();
for(int i=0;i<inputs.length;i++)
{
fst.add(inputs[i],outputs[i])
}
//get
Long value=fst.get("abd"); //得到3
//迭代
BytesRefFSTEnum<Long> iterator=new BytesRefFSTEnum<>(fst);
while(iterator.next!=null){...}
100萬資料效能測試:
· 1
資料結構 |
HashMap |
TreeMap |
FST |
構建時間 (ms) |
185 |
500 |
1512 |
查詢所有 key(ms) |
106 |
218 |
890 |
可以看出,
FST
效能基本跟
HaspMap
差距不大,但
FST
有個不可比擬的優勢就是佔用記憶體小,只有
HashMap10
分之一左右,這對大資料規模檢索是至關重要的,畢竟速度再快放不進記憶體也是沒用的。
因此一個合格的詞典結構要求有:
1.
查詢速度。
2.
記憶體佔用。
3.
記憶體
+
磁碟結合。
後面我們將解析
Lucene
索引結構,重點從
Lucene
的
FST
實現特點來闡述這三點。
1.3 Lucene 索引實現
索引結構
Lucene
現在採用的資料結構為
FST
,它的特點就是:
1
、詞查詢複雜度為
O(len(str))
2
、共享字首、節省空間
3
、記憶體存放字首索引、磁碟存放字尾詞塊
這跟我們前面說到的詞典結構三要素是一致的:
1.
查詢速度。
2.
記憶體佔用。
3.
記憶體
+
磁碟結合。我們往索引庫裡插入四個單詞
abd
、
abe
、
acf
、
acg,
看看它的索引檔案內容。
tip
部分,每列一個
FST
索引,所以會有多個
FST
,每個
FST
存放字首和字尾塊指標,這裡字首就為
a
、
ab
、
ac
。
tim
裡
面存放字尾塊和詞的其他
資訊如倒排表指標
、
TFDF
等,
doc
檔案裡就為每個單詞的倒排表。
所以它的檢索過程分為三個步驟:
1.
記憶體載入
tip
檔案,透過
FST
匹配字首找到字尾詞塊位置。
2.
根據詞塊位置,讀取磁碟中
tim
檔案中字尾塊並找到字尾和相應的倒排表位置資訊。
3.
根據倒排表位置去
doc
檔案中載入倒排表。
這裡就會有兩個問題,第一就是字首如何計算,第二就是字尾如何寫磁碟並透過
FST
定位,下面將描述下
Lucene
構建
FST
過程
:
已知
FST
要求輸入有序,所以
Lucene
會將解析出來的文件單詞預先排序,然後構建
FST
,我們假設輸入為
abd,abd,acf,acg
,那麼整個構建過程如下:
2. 插入abe時,計算出字首ab,但此時不知道後續還不會有其他以ab為字首的詞,所以此時無輸出。
3. 插入acf時,因為是有序的,知道不會再有ab字首的詞了,這時就可以寫tip和tim了,tim中寫入字尾詞塊d、e和它們的倒排表位置ip_d,ip_e,tip中寫入a,b和以ab為字首的字尾詞塊位置(真實情況下會寫入更多資訊如詞頻等)。
4. 插入acg時,計算出和acf共享字首ac,這時輸入已經結束,所有資料寫入磁碟。tim中寫入字尾詞塊f、g和相對應的倒排表位置,tip中寫入c和以ac為字首的字尾詞塊位置。
以上是一個簡化過程, Lucene 的 FST 實現的主要最佳化策略有:
2. 字首計算基於byte,而不是char,這樣可以減少字尾數,防止字尾數太多,影響效能。如對宇(e9 b8 a2)、守(e9 b8 a3)、安(e9 b8 a4)這三個漢字,FST構建出來,不是隻有根節點,三個漢字為字尾,而是從unicode碼出發,以e9、b8為字首,a2、a3、a4為字尾,
倒排表結構
倒排表就是文件號集合,但怎麼存,怎麼取也有很多講究,
Lucene
現使用的倒排表結構叫
Frame of reference,
它主要有兩個特點:
1.
資料壓縮,可以看下圖怎麼將
6
個數字從原先的
24bytes
壓縮到
7bytes
。
2.
跳躍表加速合併,因為布林查詢時,
and
和
or
操作都需要合併倒排表,這時就需要快速定位相同文件號,所以利用跳躍表來進行相同文件號查詢。
這部分可參考
ElasticSearch
的一篇部落格,裡面有一些效能測試:
ElasticSearch
倒排表
正向檔案
正向檔案指的就是原始文件,
Lucene
對原始文件也提供了儲存功能,它儲存特點就是分塊
+
壓縮,
fdt
檔案就是存放原始文件的檔案,它佔了索引庫
90%
的磁碟空間,
fdx
檔案為索引檔案,透過文件號(自增數字)快速得到文件位置,它們的檔案結構如下:
fnm
中為元資訊存放了各列型別、列名、儲存方式等資訊。
fdt
為文件值,裡面一個
chunk
就是一個塊,
Lucene
索引文件時,先快取文件,快取大於
16KB
時,就會把文件壓縮儲存。一個
chunk
包含了該
chunk
起始文件、多少個文件、壓縮後的文件內容。
fdx
為文件號索引,倒排表存放的時文件號,透過
fdx
才能快速定位到文件位置即
chunk
位置,它的索引結構比較簡單,就是跳躍表結構,首先它會把
1024
個
chunk
歸為一個
block,
每個
block
記載了起始文件值,
block
就相當於一級跳錶。
所以查詢文件,就分為三步:
第一步二分查詢
block
,定位屬於哪個
block
。
第二步就是根據從
block
里根據每個
chunk
的起始文件號,找到屬於哪個
chunk
和
chunk
位置。
第三步就是去載入
fdt
的
chunk
,找到文件。這裡還有一個細節就是存放
chunk
起始文件值和
chunk
位置不是簡單的陣列,而是採用了平均值壓縮法。所以第
N
個
chunk
的起始文件值由
DocBase + AvgChunkDocs * n + DocBaseDeltas[n]
恢復而來,而第
N
個
chunk
再
fdt
中的位置由
StartPointerBase + AvgChunkSize * n + StartPointerDeltas[n]
恢復而來。
從上面分析可以看出,
lucene
對原始檔案的存放是行是儲存,並且為了提高空間利用率,是多文件一起壓縮,因此取文件時需要讀入和解壓額外文件,因此取文件過程非常依賴隨機
IO
,以及
lucene
雖然提供了取特定列,但從儲存結構可以看出,並不會減少取文件時間。
列式儲存DocValues
我們知道倒排索引能夠解決從詞到文件的快速對映,但當我們需要對檢索結果進行分類、排序、數學計算等聚合操作時需要文件號到值的快速對映,而原先不管是倒排索引還是行式儲存的文件都無法滿足要求。
原先
4.0
版本之前,
Lucene
實現這種需求是透過
FieldCache
,它的原理是透過按列逆轉倒排表將(
field value ->doc
)對映變成(
doc -> field value
)對映
,
但這種實現方法有著兩大顯著問題:
1.
構建時間長。
2.
記憶體佔用大,易
OutOfMemory
,且影響垃圾回收。
因此
4.0
版本後
Lucene
推出了
DocValues
來解決這一問題,它和
FieldCache
一樣,都為列式儲存,但它有如下優點:
1.
預先構建,寫入檔案。
2.
基於對映檔案來做,脫離
JVM
堆記憶體,系統排程缺頁。
DocValues
這種實現方法只比記憶體
FieldCache
慢大概
10~25%
,但穩定性卻得到了極大提升。
Lucene
目前有五種型別的
DocValues
:
NUMERIC
、
BINARY
、
SORTED
、
SORTED_SET
、
SORTED_NUMERIC
,針對每種型別
Lucene
都有特定的壓縮方法。
如對
NUMERIC
型別即數字型別,數字型別壓縮方法很多,如:增量、表壓縮、最大公約數,根據資料特徵選取不同壓縮方法。
SORTED
型別即字串型別,壓縮方法就是表壓縮:預先對字串字典排序分配數字
ID
,儲存時只需儲存字串對映表,和數字陣列即可,而這數字陣列又可以採用
NUMERIC
壓縮方法再壓縮,圖示如下:
這樣就將原先的字串陣列變成數字陣列,一是減少了空間,檔案對映更有效率,二是原先變成訪問方式變成固長訪問。
對
DocValues
的應用,
ElasticSearch
功能實現地更系統、更完整,即
ElasticSearch
的
Aggregations——
聚合功能,它的聚合功能分為三類:
1. Metric ->
統計
典型功能:
sum
、
min
、
max
、
avg
、
cardinality
、
percent
等
2. Bucket ->
分組
典型功能:日期直方圖,分組,地理位置分割槽
3. Pipline ->
基於聚合再聚合
典型功能:基於各分組的平均值求最大值。
基於這些聚合功能,
ElasticSearch
不再侷限與檢索,而能夠回答如下
SQL
的問題
銷售部門男女人數、平均年齡是多少
我們看下
ElasticSearch
如何基於倒排索引和
DocValues
實現上述
SQL
的。
1.
從倒排索引中找出銷售部門的倒排表。
2.
根據倒排表去性別的
DocValues
裡取出每個人對應的性別,並分組到
Female
和
Male
裡。
3.
根據分組情況和年齡
DocValues
,計算各分組人數和平均年齡
4.
因為
ElasticSearch
是分割槽的,所以對每個分割槽的返回結果進行合併就是最終的結果。
上面就是
ElasticSearch
進行聚合的整體流程,也可以看出
ElasticSearch
做聚合的一個瓶頸就是最後一步的聚合只能單機聚合,也因此一些統計會有誤差,比如
count(*) group by producet limit 5,
最終總數不是精確的。因為單點記憶體聚合,所以每個分割槽不可能返回所有分組統計資訊,只能返回部分,彙總時就會導致最終結果不正確,具體如下:
原始資料:
Shard 1 |
Shard 2 |
Shard 3 |
Product A (25) |
Product A (30) |
Product A (45) |
Product B (18) |
Product B (25) |
Product C (44) |
Product C (6) |
Product F (17) |
Product Z (36) |
Product D (3) |
Product Z (16) |
Product G (30) |
Product E (2) |
Product G (15) |
Product E (29) |
Product F (2) |
Product H (14) |
Product H (28) |
Product G (2) |
Product I (10) |
Product Q (2) |
Product H (2) |
Product Q (6) |
Product D (1) |
Product I (1) |
Product J (8) |
|
Product J (1) |
Product C (4) |
|
count(*) group by producet limit 5 ,每個節點返回的資料如下:
Shard 1 |
Shard 2 |
Shard 3 |
Product A (25) |
Product A (30) |
Product A (45) |
Product B (18) |
Product B (25) |
Product C (44) |
Product C (6) |
Product F (17) |
Product Z (36) |
Product D (3) |
Product Z (16) |
Product G (30) |
Product E (2) |
Product G (15) |
Product E (29) |
合併後:
Merged |
Product A (100) |
Product Z (52) |
Product C (50) |
Product G (45) |
Product B (43) |
商品 A 的總數是對的,因為每個節點都返回了,但商品 C 在節點 2 因為排不到前 5 所以沒有返回,因此總數是錯的。
總結
以上就是 Lucene 簡介和底層原理分析,側重於 Lucene 實現策略與特點,下一篇將介紹我們如何從這些底層原理出發來最佳化我們的全文檢索系統。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69992957/viewspace-2754878/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Lucene查詢原理
- 深度解析 Lucene 輕量級全文索引實現原理索引
- 從根上理解elasticsearch(lucene)查詢原理(2)-lucene常見查詢型別原理分析Elasticsearch型別
- Lucene建立索引流程索引
- InnoDB索引與底層原理索引
- Lucene介紹及簡單應用
- 從根上理解elasticsearch(lucene)查詢原理(1)-lucece查詢邏輯介紹Elasticsearch
- MySQL索引底層實現原理MySql索引
- runtime的底層原理和使用
- 基於Lucene查詢原理分析Elasticsearch的效能Elasticsearch
- Elasticsearch Lucene 資料寫入原理 | ES 核心篇Elasticsearch
- 深入理解 MySQL 索引底層原理MySql索引
- MySQL底層概述—6.索引原理MySql索引
- hadoop異構儲存+lucene索引Hadoop索引
- PHP 底層原理之類和物件PHP物件
- MySQL原理簡介—9.MySQL索引原理MySql索引
- KVO的使用和底層實現原理
- flutter底層原理和embedder的隱憂Flutter
- HashMap原理(一) 概念和底層架構HashMap架構
- 簡單瞭解InnoDB底層原理
- Lucene--專案記錄(1)
- ArrayList和LinkedList底層原理的區別和使用場景
- ConcurrentHashMap底層原理HashMap
- synchronized底層原理synchronized
- 面試必備之MYSQL索引底層原理分析面試MySql索引
- 索引原理和優勢索引
- 後端技術雜談3:Lucene基礎原理與實踐後端
- iptables基礎原理和使用簡介
- Go之NSQ簡介,原理和使用Go
- IOS 底層原理 物件的本質--(1)iOS物件
- HashMap原理詳解,包括底層原理HashMap
- MySQL全面瓦解22:索引的介紹和原理分析MySql索引
- OC底層探索(十六) KVO底層原理
- 在JAVA中將Elasticsearch索引載入到Lucene APIJavaElasticsearch索引API
- Dubbo底層原理分析和分散式實際應用分散式
- Spring Cloud底層原理SpringCloud
- RunLoop底層原理探究OOP
- iOS底層原理-CategoryiOSGo