solr 倒排索引(转载)
原文地址:http://blog.csdn.net/chichengit/article/details/9235157
http://blog.csdn.net/njpjsoftdev/article/details/54015485
?
介紹:
所謂倒排索引的倒排,其實我感覺定義的不太貼切:正常的文檔索引是,描述一個文檔有哪些關鍵字,也就是文檔—關鍵字列表這種結構,但是倒排索引是關鍵字—文檔列表這種方式。
正排索引從文檔編號找詞:
倒排索引是從詞找文檔編號:
詳細索引內容
設有兩篇文章1和2
文章1的內容為:Tom lives in Guangzhou,I live in Guangzhou too
文章2的內容為:He once lived in Shanghai
獲取關鍵字
全文分析:由于lucene是基于關鍵詞索引和查詢的,首先我們要取得這兩篇文章的關鍵詞,
通常我們需要如下處理措施:
??? a.我們現在有的是文章內容,即一個字符串,我們先要找出字符串中的所有單詞,即分詞。
????? 英文單詞由于用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
??? b.文章中的"in", "once" "too"等詞沒有什么實際意義,中文中的"的""是"等字通常也無具體含義,
????? 這些不代表概念的詞可以過濾掉
??? c.用戶通常希望查"He"時能把含"he","HE"的文章也找出來,所以所有單詞需要統一大小寫。
??? d.用戶通常希望查"live"時能把含"lives","lived"的文章也找出來,所以需要把"lives","lived"還原成"live"
??? e.文章中的標點符號通常不表示某種概念,也可以過濾掉
??? 經過上面處理后:
文章1的所有關鍵詞為:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有關鍵詞為:[he] [live] [shanghai]
建立倒排索引
??? 有了關鍵詞后,我們就可以建立倒排索引了。
??? 上面的對應關系是:"文章號"對"文章中所有關鍵詞"。
??? 倒排索引把這個關系倒過來,變成:"關鍵詞"對"擁有該關鍵詞的所有文章號"。
??? 文章1,2經過倒排后變成:
??????? 關鍵詞? ??? ?文章號
guangzhou ?? ?1
he????? ??? ? ? ? ? ?? 2
i??????? ??? ??????????? 1
live???? ??? ????????? 1,2
shanghai ??? ?? 2
tom????? ??? ??????? 1
?? 通常僅知道關鍵詞在哪些文章中出現還不夠,我們還需要知道關鍵詞在文章中出現次數和出現的位置,
?? 通常有兩種位置:
?????? a)字符位置,即記錄該詞是文章中第幾個字符(優點是關鍵詞亮顯時定位快);
?????? b)關鍵詞位置,即記錄該詞是文章中第幾個關鍵詞(優點是節約索引空間、詞組(phase)查詢快),lucene中記錄的就是這種位置。
?加上"出現頻率"和"出現位置"信息后,我們的索引結構變為:
?關鍵詞?? ????? 文章號?? ? [出現頻率]?? ? 出現位置
?guangzhou?? ? 1?? ? ? ? ? ? ? ? [2]?? ???????????? 3,6
?he?? ? ? ? ? ? ? ? ? ? 2?? ? ? ? ? ? ? ? [1] ?? ??????????? 1
?i ?? ??????????????????? 1?? ?????????????? [1]?? ??????????? 4
?live ?? ?????????????? 1?? ?????????????? [2]?? ? ? ? ? ??? 2,5
??? ????????????????????? 2?? ? ? ? ? ? ? ?? [1]?? ? ? ? ? ? ? 2
?shanghai?? ???? 2?? ?????????????? [1] ?? ? ? ? ? ?? 3
?tom ?? ? ? ? ? ? ??? 1?? ?????????????? [1]?? ??????????? 1
?
以live這行為例我們說明一下該結構:
live在文章1中出現了2次,文章2中出現了一次,它的出現位置為"2,5,2"這表示什么呢?
我們需要結合文章號和出現頻率來分析,
文章1中出現了2次,那么"2,5"就表示live在文章1的關鍵詞中出現的兩個位置,
文章2中出現了1次,剩下的"2"就表示live是文章2的關鍵詞中第2個關鍵字。
?
以上就是lucene索引結構中最核心的部分。
我們注意到關鍵字是按字符順序排列的(lucene沒有使用B樹結構),
因此lucene可以用二元搜索算法(或叫二分查找/折半查找)快速定位關鍵詞。
簡單實現說明
實現時,lucene將上面三列分別作為:
詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。
其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,
通過指針可以找到該關鍵字的頻率信息和位置信息。
Lucene中使用了field的概念,用于表達信息所在位置(如標題中,文章中,url中),
在建索引中,該field信息也記錄在詞典文件中,每個關鍵詞都有一個field信息(因為每個關鍵字一定屬于一個或多個field)。
大概實現邏輯
全文檢索技術由來已久,絕大多數都基于倒排索引來做,曾經也有過一些其他方案如文件指紋。倒排索引,顧名思義,它相反于一篇文章包含了哪些詞,它從詞出發,記載了這個詞在哪些文檔中出現過,由兩部分組成——詞典和倒排表。
其中詞典結構尤為重要,有很多種詞典結構,各有各的優缺點,最簡單如排序數組,通過二分查找來檢索數據,更快的有哈希表,磁盤查找有B樹、B+樹,但一個能支持TB級數據的倒排索引結構需要在時間和空間上有個平衡,下圖列了一些常見詞典的優缺點:?
其中可用的有:B+樹、跳躍表、FST?
B+樹:?
mysql的InnoDB B+數結構?
理論基礎:平衡多路查找樹
優點:外存索引、可更新
缺點:空間大、速度不夠快
跳躍表:?
優點:結構簡單、跳躍間隔、級數可控,Lucene3.0之前使用的也是跳躍表結構,后換成了FST,但跳躍表在Lucene其他地方還有應用如倒排表合并和文檔號索引。
缺點:模糊查詢支持不好
FST?
Lucene現在使用的索引結構?
理論基礎: 《Direct construction of minimal acyclic subsequential transducers》,通過輸入有序字符串構建最小有向無環圖。
優點:內存占用率低,壓縮率一般在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萬數據性能測試:
|
數據結構 |
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的原理介紹都是基于4.10.3)*
Lucene經多年演進優化,現在的一個索引文件結構如圖所示,基本可以分為三個部分:詞典、倒排表、正向文件、列式存儲DocValues。
下面詳細介紹各部分結構:
索引結構
Lucene現在采用的數據結構為FST,它的特點就是:?
1、詞查找復雜度為O(len(str))?
2、共享前綴、節省空間?
3、內存存放前綴索引、磁盤存放后綴詞塊?
我的理解,比如單詞person,perl前綴索引可能是per,后綴塊中可能是son,l等。
這跟我們前面說到的詞典結構三要素是一致的: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,那么整個構建過程如下:?
1. 插入abd時,沒有輸出。
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實現的主要優化策略有:
1. 最小后綴數。Lucene對寫入tip的前綴有個最小后綴數要求,默認25,這時為了進一步減少內存使用。如果按照25的后綴數,那么就不存在ab、ac前綴,將只有一個跟節點,abd、abe、acf、acg將都作為后綴存在tim文件中。我們的10g的一個索引庫,索引內存消耗只占20M左右。
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的問題
select gender,count(*),avg(age) from employee where dept='sales' group by gender 銷售部門男女人數、平均年齡是多少
我們看下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所以沒有返回,因此總數是錯的。
?
?
?
總結
以上是生活随笔為你收集整理的solr 倒排索引(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 方差、标准差、均方差、均方误差
- 下一篇: 男人为什么会肾虚