Lucene.Net學習心得
一、Lucene點滴
(發音為['lusen]),我經常就讀鹿神,是頭活蹦亂跳的好鹿,研究它吧,保證感覺它很神!Lucene是一個非常優秀的開源的全文搜索引擎,我們可以在它的上面開發出各種全文搜索的應用來。Lucene在國外有很高的知名度,現在已經是Apache的頂級項目。
二、倒排索引原理簡述
Lucene是一個高性能的java全文檢索工具包,它使用的是倒排文件索引結構。具體解釋算法理論就不講了,直接用例子來說明吧,如果你認真仔細的讀懂例子,真正領會了其中的思想,你肯定就明白了Lucene索引的基本原理!記住:理解!把例子用你自己的語言表述出來,就是翻譯成你自己的東西,以后你想自己寫,也就是換成計算機語言再翻譯一次!!
Lucene結構及相應的生成算法如下:
我們設有兩篇文章1和2
文章1的內容為:Tom lives in Guangzhou,I live in Guangzhou too.
文章2的內容為:He once lived in Shanghai.
1)全文分析:
由于Lucene是基于關鍵詞索引和查詢的,首先我們要取得這兩篇文章的關鍵詞,通常我們需要如下處理措施
a. 我們現在有的是文章內容,即一大串字符串,我們先要找出全文字符串中的所有單詞,即分詞。英文單詞由于用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。(中文通常用詞典的方式比較準確)
b. 文章中的”in”, “once” “too”等詞沒有什么實際意義,中文中的“的”“是”等字通常也無具體含義,這些不代表概念的詞可以過濾掉,即分詞解析中的過濾
c. 用戶通常希望查“He”時能把含“he”,“HE”的文章也找出來,所以所有單詞需要統一大小寫。即解析過程中的額外處理(用戶可以根據自己需要增加多重處理)。
d. 用戶通常希望查“live”時能把含“lives”,“lived”的文章也找出來,所以需要把“lives”,“lived”還原成“live”即進一步優化處理(可以更人性更友好)
e. 文章中的標點符號通常不表示某種概念,也可以過濾掉
所有上面描述的步驟在lucene中都由Analyzer類完成!你了解了過程,你就可以根據自己的興趣或者需要去關心特點的步驟相關代碼,或者自己進一步添磚加瓦擴展功能。
經過上面處理后
文章1的所有關鍵詞為:[tom] [live] [guangzhou] [live] [guangzhou]
文章2的所有關鍵詞為:[he] [live] [shanghai]
2) 倒排索引:
有了關鍵詞后,我們就可以建立倒排索引了。
上面經過解析后的對應關系是:“文章號”對“文章中所有關鍵詞”。
這種對應比較符合我們正常的思維習慣,反過來對應的話,就要顛倒思維,所以這種算法叫做倒排! 倒排索引把這個關系倒過來,變成:“關鍵詞”對“擁有該關鍵詞的所有文章號”。文章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 |
以第4行的 live 詞語索引記錄作實例我們說明一下該結構:
live在文章1中出現了2次,文章2中出現了一次,它的出現位置為“2,5,2”這表示什么呢?我們需要結合文章號和出現頻率來分析,文章1中出現了2次,那么“2,5”就表示live在文章1中出現的兩個位置,文章2中出現了一次,剩下的“2”就表示live是文章2中第 2個關鍵字。
以上就是lucene索引結構中最核心的部分。是不是描述起來很簡單,沒錯,其實也就是這么簡單!算法都是人想出來的,毛主席說過辦法總比困難多,計算機算法也不例外,你何必恐懼它呢,本來就是很容易的事情!!!
【進一步深入探索,如果你不想深入,完全可以略過不看】
如果你對計算機算法和數據結構稍有研究,你就很容易發現,我們上面生成的倒排索引關鍵字是按字母字符順序排列的(Lucene沒有使用B樹結構),因此Lucene可以用二元搜索算法快速定位關鍵詞。
具體實現時Lucene將上面三列分別作為詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件(positions)保存。其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,通過指針可以找到該關鍵字的頻率信息和位置信息。
Lucene中使用了field的概念,用于表達信息所在位置(如標題中,文章中,url中),在建索引中,該field信息也記錄在詞典文件中,每個關鍵詞都有一個field信息(因為每個關鍵字一定屬于一個或多個field)。
為了減小索引文件的大小,Lucene對索引還使用了壓縮技術。首先,對詞典文件中的關鍵詞進行了壓縮,關鍵詞壓縮為<前綴長度,后綴>,例如:當前詞為“阿拉伯語”,上一個詞為“阿拉伯”,那么“阿拉伯語”壓縮為<3,語>。其次大量用到的是對數字的壓縮,數字只保存與上一個值的差值(這樣可以減小數字的長度,進而減少保存該數字需要的字節數)。例如當前文章號是16389(不壓縮要用3個字節保存),上一文章號是16382,壓縮后保存7(只用一個字節)。注意是“上一個詞”。由于詞典是按順序排列的,這種壓縮方法的效果會非常顯著。
三、全文檢索框架的實現機制:
Lucene的API接口設計的比較通用,輸入輸出結構都很像數據庫的表==>記錄==>字段,所以很多傳統的應用的文件、數據庫等都可以比較方便的映射到Lucene的存儲結構/接口中。總體上看:可以先把Lucene當成一個支持全文索引的數據庫系統。
比較一下Lucene數據存儲和傳統的關系數據庫之間的區別:
Lucene | 數據庫 |
索引數據源:doc(field1,field2...) doc(field1,field2...) \ indexer / _____________ | Lucene Index | -------------- / searcher \ 結果輸出:Hits(doc(field1,field2) doc(field1...)) |
索引數據源:record(field1,field2...) record(field1..) \ SQL: insert/ _____________ | DB Index | ------------- / SQL: select \ 結果輸出:results(record(field1,field2..) record(field1...)) |
Document:一個需要進行索引的“單元,一個Document由多個字段組成 | Record:記錄,包含多個字段 |
Field:字段 | Field:字段 |
Hits:查詢結果集,由匹配的Document組成 | RecordSet:查詢結果集,由多個Record組成 |
請一定記住:全文檢索 ≠ 數據庫SQL語句中的 like "%keyword%"
由于數據庫索引不是為全文索引設計的,因此,使用like"%keyword%"時,數據庫索引是不起作用的,在使用like查詢時,搜索過程又變成類似于一頁頁翻書的遍歷過程了,所以對于含有模糊查詢的數據庫服務來說,LIKE對性能的危害是極大的。如果是需要對多個關鍵詞進行模糊匹配:like"%keyword1%" and like"%keyword2%" ...其效率也就可想而知了。
通常比較厚的書籍后面常常附關鍵詞索引表(比如:北京:12,34頁,上海:3,77頁……),它能夠幫助讀者比較快地找到相關內容的頁碼。而數據庫索引能夠大大提高查詢的速度原理也是一樣,想像一下通過書后面的索引查找的速度要比一頁一頁地翻內容高多少倍……而索引之所以效率高,另外一個原因是它是排好序的。對于檢索系統來說核心是一個排序問題。
所以建立一個高效檢索系統的關鍵是建立一個類似于科技索引一樣的反向索引機制,將數據源(比如多篇文章)排序順序存儲的同時,有另外一個排好序的關鍵詞列表,用于存儲關鍵詞==>文章映射關系,利用這樣的映射關系索引:[關鍵詞==>出現關鍵詞的文章編號,出現次數(甚至包括位置:起始偏移量,結束偏移量),出現頻率],檢索過程就是把模糊查詢變成多個可以利用索引的精確查詢的邏輯組合的過程。從而大大提高了多關鍵詞查詢的效率,所以,全文檢索問題歸結到最后是一個排序問題。
由此可以看出模糊查詢相對數據庫的精確查詢是一個非常不確定的問題,這也是大部分數據庫對全文檢索支持有限的原因。
Lucene最核心的特征是通過特殊的索引結構實現了傳統數據庫不擅長的全文索引機制,并提供了擴展接口,以方便針對不同應用的定制。
我們對比一下Lucene全文索引和關系數據庫模糊查詢 算法區別:
Lucene全文索引引擎 | 數據庫 | |
索引 | 將數據源中的數據都通過全文索引一一建立倒排索引 | 對于LIKE查詢來說,數據傳統的索引是根本用不上的。數據需要逐個便利記錄進行GREP式的模糊匹配,比有索引的搜索速度要有多個數量級的下降。 |
匹配效果 | 通過詞元(term)進行匹配,通過語言分析接口的實現,可以實現對中文等非英語的支持。 | 使用:like "%net%" 會把netherlands也匹配出來, 多個關鍵詞的模糊匹配:使用like "%com%net%":就不能匹配詞序顛倒的xxx.net..xxx.com |
匹配度 | 有匹配度算法,將匹配程度(相似度)比較高的結果排在前面。 | 沒有匹配程度的控制:比如有記錄中net出現5詞和出現1次的,結果是一樣的 |
結果輸出 | 通過特別的算法,將最匹配度最高的頭100條結果輸出,結果集是緩沖式的小批量讀取的。 | 返回所有的結果集,在匹配條目非常多的時候(比如上萬條)需要大量的內存存放這些臨時結果集。 |
可定制性 | 通過不同的語言分析接口實現,可以方便的定制出符合應用需要的索引規則(包括對中文的支持) | 沒有接口或接口復雜,無法定制 |
結論 | 高負載的模糊查詢應用,需要負責的模糊查詢的規則,索引的資料量比較大 | 使用率低,模糊匹配規則簡單或者需要模糊查詢的資料量少 |
全文檢索和數據庫應用最大的不同在于:讓最相關的頭100條結果滿足98%以上用戶的需求。
Lucene的創新之處:
大部分的搜索(數據庫)引擎都是用B樹結構來維護索引,索引的更新會導致大量的IO操作,Lucene在實現中,對此稍微有所改進:不是維護一個索引文件,而是在擴展索引的時候不斷創建新的索引文件,然后定期的把這些新的小索引文件合并到原先的大索引中(針對不同的更新策略,批次的大小可以調整),這樣在不影響檢索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文檢索系統/應用的比較:
Lucene | 其他開源全文檢索系統 | |
增量索引和批量索引 | 可以進行增量的索引(Append),可以對于大量數據進行批量索引,并且接口設計用于優化批量索引和小批量的增量索引。 | 很多系統只支持批量的索引,有時數據源有一點增加也需要重建索引。 |
數據源 | Lucene沒有定義具體的數據源,而是一個文檔的結構,因此可以非常靈活的適應各種應用(只要前端有合適的轉換器把數據源轉換成相應結構)。 | 很多系統只針對網頁,缺乏其他格式文檔的靈活性。 |
索引內容抓取 | Lucene的文檔是由多個字段組成的,甚至可以控制那些字段需要進行索引,那些字段不需要索引,近一步索引的字段也分為需要分詞和不需要分詞的類型: 需要進行分詞的索引,比如:標題,文章內容字段 不需要進行分詞的索引,比如:作者/日期字段 |
缺乏通用性,往往將文檔整個索引了 |
語言分析 | 通過語言分析器的不同擴展實現: 可以過濾掉不需要的詞:an the of 等, 西文語法分析:將jumps jumped jumper都歸結成jump進行索引/檢索 非英文支持:對亞洲語言,阿拉伯語言的索引支持 |
缺乏通用接口實現 |
查詢分析 | 通過查詢分析接口的實現,可以定制自己的查詢語法規則: 比如: 多個關鍵詞之間的 + - and or關系等 |
功能較強大 |
并發訪問 | 能夠支持多用戶的使用 | 功能較強大 |
四、關于亞洲語言的的切分詞問題(Word Segment)
對于中文來說,全文索引首先還要解決一個語言分析的問題,對于英文來說,語句中單詞之間是天然通過空格分開的,但亞洲語言的中日韓文語句中的字是一個字挨一個,所有,首先要把語句中按“詞”進行索引的話,這個詞如何切分出來就是一個很大的問題。
首先,肯定不能用單個字符作(si-gram)為索引單元,否則查“上海”時,不能讓含有“海上”也匹配。但一句話:“北京天安門”,計算機如何按照中文的語言習慣進行切分呢?“北京 天安門” 還是“北 京 天安門”?讓計算機能夠按照語言習慣進行切分,往往需要機器有一個比較豐富的詞庫才能夠比較準確的識別出語句中的單詞。
另外一個解決的辦法是采用自動切分算法:將單詞按照2元語法(bigram)方式切分出來,比如:
"北京天安門" ==> "北京 京天 天安 安門"。
這樣,在查詢的時候,無論是查詢"北京" 還是查詢"天安門",將查詢詞組按同樣的規則進行切分:"北京","天安安門",多個關鍵詞之間按與"and"的關系組合,同樣能夠正確地映射到相應的索引中。這種方式對于其他亞洲語言:韓文,日文都是通用的。
基于自動切分的最大優點是沒有詞表維護成本,實現簡單,缺點是索引效率低,但對于中小型應用來說,基于2元語法的切分還是夠用的。基于2元切分后的索引一般大小和源文件差不多,而對于英文,索引文件一般只有原文件的30%-40%不同。
自動切分 | 詞表切分 | |
實現 | 實現非常簡單 | 實現復雜 |
查詢 | 增加了查詢分析的復雜程度 | 適于實現比較復雜的查詢語法規則 |
存儲效率 | 索引冗余大,索引幾乎和原文一樣大 | 索引效率高,為原文大小的30%左右 |
維護成本 | 無詞表維護成本 | 詞表維護成本非常高:中日韓等語言需要分別維護。 還需要包括詞頻統計等內容 |
適用領域 | 嵌入式系統:運行環境資源有限 分布式系統:無詞表同步問題 多語言環境:無詞表維護成本 |
對查詢和存儲效率要求高的專業搜索引擎 |
目前比較大的搜索引擎的語言分析算法一般是基于以上2個機制的結合。
五、Lucene的結構框架:
注意:Lucene中的一些比較復雜的詞法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,純Java的詞法分析生成器),所以如果從源代碼編譯或需要修改其中的QueryParser、定制自己的詞法分析器,還需要從https://javacc.dev.java.net/下載javacc。
Lucene的組成結構:對于外部應用來說索引模塊(index)和檢索模塊(search)是主要的外部應用入口。
org.apache.Lucene.search/ | 搜索入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 語言分析器 |
org.apache.Lucene.queryParser/ | 查詢分析器 |
org.apache.Lucene.document/ | 存儲結構 |
org.apache.Lucene.store/ | 底層IO/存儲結構 |
org.apache.Lucene.util/ | 一些公用的數據結構 |
六、從Lucene學到更多:
Luene的確是一個面對對象設計的典范。如果你要學習面向對象編程,你要學習java,建議你學習Lucene吧,玩一個實實在在的項目,而且是有名的基礎項目,在無形之中你既可以學習這個項目的使用,又可以學習java語法,又可以學習大師們的代碼技巧,還可以以后為Lucene做點什么,真的你可以一舉多得!!!何樂不為呢?!
- 所有的問題都通過一個額外抽象層來方便以后的擴展和重用:你可以通過重新實現來達到自己的目的,而對其他模塊而不需要;
- 簡單的應用入口Searcher, Indexer,并調用底層一系列組件協同的完成搜索任務;
- 所有的對象的任務都非常專一:比如搜索過程:QueryParser分析將查詢語句轉換成一系列的精確查詢的組合(Query),通過底層的索引讀取結構IndexReader進行索引的讀取,并用相應的打分器給搜索結果進行打分/排序等。所有的功能模塊原子化程度非常高,因此可以通過重新實現而不需要修改其他模塊。
- 除了靈活的應用接口設計,Lucene還提供了一些適合大多數應用的語言分析器實現(SimpleAnalyser,StandardAnalyser),這也是新用戶能夠很快上手的重要原因之一。