Lucene是一個基於Java的全文索引工具包。html
另外,若是是在選擇全文引擎,如今也許是試試Sphinx的時候了:相比Lucene速度更快,有中文分詞的支持,並且內置了對簡單的分佈式檢索的支持;前端
Lucene不是一個完整的全文索引應用,而是是一個用Java寫的全文索引引擎工具包,它能夠方便的嵌入到各類應用中實現針對應用的全文索引/檢索功能。web
Lucene的做者:Lucene的貢獻者DougCutting是一位資深全文索引/檢索專家,曾經是V-Twin搜索引擎(Apple的Copland操做系統的成就之一)的主要開發者,後在Excite擔任高級系統架構設計師,目前從事於一些INTERNET底層架構的研究。他貢獻出的Lucene的目標是爲各類中小型應用程序加入全文檢索功能。算法
Lucene的發展歷程:早先發布在做者本身的www.lucene.com,後來發佈在SourceForge,2001年年末成爲APACHE基金會jakarta的一個子項目:http://jakarta.apache.org/lucene/數據庫
已經有不少Java項目都使用了Lucene做爲其後臺的全文索引引擎,比較著名的有:apache
· Eyebrows:郵件列表HTML歸檔/瀏覽/查詢系統,本文的主要參考文檔「TheLucene search engine: Powerful, flexible, and free」做者就是EyeBrows系統的主要開發者之一,而EyeBrows已經成爲目前APACHE項目的主要郵件列表歸檔系統。數據結構
· Cocoon:基於XML的web發佈框架,全文檢索部分使用了Lucene架構
· Eclipse:基於Java的開放開發平臺,幫助部分的全文索引使用了Lucene
對於中文用戶來講,最關心的問題是其是否支持中文的全文檢索。但經過後面對於Lucene的結構的介紹,你會了解到因爲Lucene良好架構設計,對中文的支持只需對其語言詞法分析接口進行擴展就能實現對中文檢索的支持。
Lucene的API接口設計的比較通用,輸入輸出結構都很像數據庫的表==>記錄==>字段,因此不少傳統的應用的文件、數據庫等均可以比較方便的映射到Lucene的存儲結構/接口中。整體上看:能夠先把Lucene當成一個支持全文索引的數據庫系統。
比較一下Lucene和數據庫:
Lucene |
數據庫 |
索引數據源:doc(field1,field2...) doc(field1,field2...) |
索引數據源:record(field1,field2...) record(field1..) |
Document:一個須要進行索引的「單元」 |
Record:記錄,包含多個字段 |
Field:字段 |
Field:字段 |
Hits:查詢結果集,由匹配的Document組成 |
RecordSet:查詢結果集,由多個Record組成 |
全文檢索 ≠ like "%keyword%"
一般比較厚的書籍後面經常附關鍵詞索引表(好比:北京:12, 34頁,上海:3,77頁……),它可以幫助讀者比較快地找到相關內容的頁碼。而數據庫索引可以大大提升查詢的速度原理也是同樣,想像一下經過書後面的索引查找的速度要比一頁一頁地翻內容高多少倍……而索引之因此效率高,另一個緣由是它是排好序的。對於檢索系統來講核心是一個排序問題。
因爲數據庫索引不是爲全文索引設計的,所以,使用like "%keyword%"時,數據庫索引是不起做用的,在使用like查詢時,搜索過程又變成相似於一頁頁翻書的遍歷過程了,因此對於含有模糊查詢的數據庫服務來講,LIKE對性能的危害是極大的。若是是須要對多個關鍵詞進行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
因此創建一個高效檢索系統的關鍵是創建一個相似於科技索引同樣的反向索引機制,將數據源(好比多篇文章)排序順序存儲的同時,有另一個排好序的關鍵詞列表,用於存儲關鍵詞==>文章映射關係,利用這樣的映射關係索引:[關鍵詞==>出現關鍵詞的文章編號,出現次數(甚至包括位置:起始偏移量,結束偏移量),出現頻率],檢索過程就是把模糊查詢變成多個能夠利用索引的精確查詢的邏輯組合的過程。從而大大提升了多關鍵詞查詢的效率,因此,全文檢索問題歸結到最後是一個排序問題。
由此能夠看出模糊查詢相對數據庫的精確查詢是一個很是不肯定的問題,這也是大部分數據庫對全文檢索支持有限的緣由。Lucene最核心的特徵是經過特殊的索引結構實現了傳統數據庫不擅長的全文索引機制,並提供了擴展接口,以方便針對不一樣應用的定製。
能夠經過一下表格對比一下數據庫的模糊查詢:
Lucene全文索引引擎 |
數據庫 |
|
索引 |
將數據源中的數據都經過全文索引一一創建反向索引 |
對於LIKE查詢來講,數據傳統的索引是根本用不上的。數據須要逐個便利記錄進行GREP式的模糊匹配,比有索引的搜索速度要有多個數量級的降低。 |
匹配效果 |
經過詞元(term)進行匹配,經過語言分析接口的實現,能夠實現對中文等非英語的支持。 |
使用:like "%net%"會把netherlands也匹配出來, |
匹配度 |
有匹配度算法,將匹配程度(類似度)比較高的結果排在前面。 |
沒有匹配程度的控制:好比有記錄中net出現5詞和出現1次的,結果是同樣的。 |
結果輸出 |
經過特別的算法,將最匹配度最高的頭100條結果輸出,結果集是緩衝式的小批量讀取的。 |
返回全部的結果集,在匹配條目很是多的時候(好比上萬條)須要大量的內存存放這些臨時結果集。 |
可定製性 |
經過不一樣的語言分析接口實現,能夠方便的定製出符合應用須要的索引規則(包括對中文的支持) |
沒有接口或接口複雜,沒法定製 |
結論 |
高負載的模糊查詢應用,須要負責的模糊查詢的規則,索引的資料量比較大 |
使用率低,模糊匹配規則簡單或者須要模糊查詢的資料量少 |
全文檢索和數據庫應用最大的不一樣在於:讓最相關的頭100條結果知足98%以上用戶的需求
Lucene的創新之處:
大部分的搜索(數據庫)引擎都是用B樹結構來維護索引,索引的更新會致使大量的IO操做,Lucene在實現中,對此稍微有所改進:不是維護一個索引文件,而是在擴展索引的時候不斷建立新的索引文件,而後按期的把這些新的小索引文件合併到原先的大索引中(針對不一樣的更新策略,批次的大小能夠調整),這樣在不影響檢索的效率的前提下,提升了索引的效率。
Lucene和其餘一些全文檢索系統/應用的比較:
Lucene |
其餘開源全文檢索系統 |
|
增量索引和批量索引 |
能夠進行增量的索引(Append),能夠對於大量數據進行批量索引,而且接口設計用於優化批量索引和小批量的增量索引。 |
不少系統只支持批量的索引,有時數據源有一點增長也須要重建索引。 |
數據源 |
Lucene沒有定義具體的數據源,而是一個文檔的結構,所以能夠很是靈活的適應各類應用(只要前端有合適的轉換器把數據源轉換成相應結構), |
不少系統只針對網頁,缺少其餘格式文檔的靈活性。 |
索引內容抓取 |
Lucene的文檔是由多個字段組成的,甚至能夠控制那些字段須要進行索引,那些字段不須要索引,近一步索引的字段也分爲須要分詞和不須要分詞的類型: |
缺少通用性,每每將文檔整個索引了 |
語言分析 |
經過語言分析器的不一樣擴展實現: |
缺少通用接口實現 |
查詢分析 |
經過查詢分析接口的實現,能夠定製本身的查詢語法規則: |
|
併發訪問 |
可以支持多用戶的使用 |
對於中文來講,全文索引首先還要解決一個語言分析的問題,對於英文來講,語句中單詞之間是自然經過空格分開的,但亞洲語言的中日韓文語句中的字是一個字挨一個,全部,首先要把語句中按「詞」進行索引的話,這個詞如何切分出來就是一個很大的問題。
首先,確定不能用單個字符做(si-gram)爲索引單元,不然查「上海」時,不能讓含有「海上」也匹配。
但一句話:「北京天安門」,計算機如何按照中文的語言習慣進行切分呢?
「北京天安門」仍是「北京天安門」?讓計算機可以按照語言習慣進行切分,每每須要機器有一個比較豐富的詞庫纔可以比較準確的識別出語句中的單詞。
另一個解決的辦法是採用自動切分算法:將單詞按照2元語法(bigram)方式切分出來,好比:
"北京天安門" ==> "北京京天天安安門"。
這樣,在查詢的時候,不管是查詢"北京"仍是查詢"天安門",將查詢詞組按一樣的規則進行切分:"北京","天安安門",多個關鍵詞之間按與"and"的關係組合,一樣可以正確地映射到相應的索引中。這種方式對於其餘亞洲語言:韓文,日文都是通用的。
基於自動切分的最大優勢是沒有詞表維護成本,實現簡單,缺點是索引效率低,但對於中小型應用來講,基於2元語法的切分仍是夠用的。基於2元切分後的索引通常大小和源文件差很少,而對於英文,索引文件通常只有原文件的30%-40%不一樣,
自動切分 |
詞表切分 |
|
實現 |
實現很是簡單 |
實現複雜 |
查詢 |
增長了查詢分析的複雜程度, |
適於實現比較複雜的查詢語法規則 |
存儲效率 |
索引冗餘大,索引幾乎和原文同樣大 |
索引效率高,爲原文大小的30%左右 |
維護成本 |
無詞表維護成本 |
詞表維護成本很是高:中日韓等語言須要分別維護。 |
適用領域 |
嵌入式系統:運行環境資源有限 |
對查詢和存儲效率要求高的專業搜索引擎 |
目前比較大的搜索引擎的語言分析算法通常是基於以上2個機制的結合。關於中文的語言分析算法,你們能夠在Google查關鍵詞"wordsegment search"能找到更多相關的資料。
下載:http://jakarta.apache.org/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的使用方法:
索引過程:從命令行讀取文件名(多個),將文件分路徑(path字段)和內容(body字段)2個字段進行存儲,並對內容進行全文索引:索引的單位是Document對象,每一個Document對象包含多個字段Field對象,針對不一樣的字段屬性和數據輸出的需求,對字段還能夠選擇不一樣的索引/存儲字段規則,列表以下:
方法 |
切詞 |
索引 |
存儲 |
用途 |
Field.Text(String name, String value) |
Yes |
Yes |
Yes |
切分詞索引並存儲,好比:標題,內容字段 |
Field.Text(String name, Reader value) |
Yes |
Yes |
No |
切分詞索引不存儲,好比:META信息, |
Field.Keyword(String name, String value) |
No |
Yes |
Yes |
不切分索引並存儲,好比:日期字段 |
Field.UnIndexed(String name, String value) |
No |
No |
Yes |
不索引,只存儲,好比:文件路徑 |
Field.UnStored(String name, String value) |
Yes |
Yes |
No |
只全文索引,不存儲 |
public classIndexFiles { //使用方法:: IndexFiles [索引輸出目錄] [索引的文件列表] ... public static void main(String[] args)throws Exception { String indexPath = args[0]; IndexWriter writer; //用指定的語言分析器構造一個新的寫索引器(第3個參數表示是否爲追加索引) writer = new IndexWriter(indexPath,new SimpleAnalyzer(), false); for (int i=1; i<args.length; i++){ System.out.println("Indexingfile " + args[i]); InputStream is = new FileInputStream(args[i]); //構造包含2個字段Field的Document對象 //一個是路徑path字段,不索引,只存儲 //一個是內容body字段,進行全文索引,並存儲 Document doc = new Document(); doc.add(Field.UnIndexed("path", args[i])); doc.add(Field.Text("body", (Reader) new InputStreamReader(is))); //將文檔寫入索引 writer.addDocument(doc); is.close(); }; //關閉寫索引器 writer.close(); } }
索引過程當中能夠看到:
· 語言分析器提供了抽象的接口,所以語言分析(Analyser)是能夠定製的,雖然lucene缺省提供了2個比較通用的分析器SimpleAnalyser和StandardAnalyser,這2個分析器缺省都不支持中文,因此要加入對中文語言的切分規則,須要修改這2個分析器。
· Lucene並無規定數據源的格式,而只提供了一個通用的結構(Document對象)來接受索引的輸入,所以輸入的數據源能夠是:數據庫,WORD文檔,PDF文檔,HTML文檔……只要可以設計相應的解析轉換器將數據源構形成成Docuement對象便可進行索引。
· 對於大批量的數據索引,還能夠經過調整IndexerWrite的文件合併頻率屬性(mergeFactor)來提升批量索引的效率。
檢索過程和結果顯示:
搜索結果返回的是Hits對象,能夠經過它再訪問Document==>Field中的內容。
假設根據body字段進行全文檢索,能夠將查詢結果的path字段和相應查詢的匹配度(score)打印出來,
public class Search{ public static void main(String[] args)throws Exception { String indexPath = args[0],queryString = args[1]; //指向索引目錄的搜索器 Searcher searcher = newIndexSearcher(indexPath); //查詢解析器:使用和索引一樣的語言分析器 Query query =QueryParser.parse(queryString, "body", newSimpleAnalyzer()); //搜索結果使用Hits存儲 Hits hits = searcher.search(query); //經過hits能夠訪問到相應字段的數據和查詢的匹配度 for (int i=0; i<hits.length();i++) { System.out.println(hits.doc(i).get("path") + "; Score:" + hits.score(i)); }; } }
在整個檢索過程當中,語言分析器,查詢分析器,甚至搜索器(Searcher)都是提供了抽象的接口,能夠根據須要進行定製。
簡化的查詢分析器
我的感受lucene成爲JAKARTA項目後,畫在了太多的時間用於調試日趨複雜QueryParser,而其中大部分是大多數用戶並不很熟悉的,目前LUCENE支持的語法:
Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] (<TERM> | "(" Query ")")
中間的邏輯包括:and or + - &&||等符號,並且還有"短語查詢"和針對西文的前綴/模糊查詢等,我的感受對於通常應用來講,這些功能有一些華而不實,其實可以實現目前相似於Google的查詢語句分析功能其實對於大多數用戶來講已經夠了。因此,Lucene早期版本的QueryParser還是比較好的選擇。
添加修改刪除指定記錄(Document)
Lucene提供了索引的擴展機制,所以索引的動態擴展應該是沒有問題的,而指定記錄的修改也彷佛只能經過記錄的刪除,而後從新加入實現。如何刪除指定的記錄呢?刪除的方法也很簡單,只是須要在索引時根據數據源中的記錄ID專門另建索引,而後利用IndexReader.delete(Termterm)方法經過這個記錄ID刪除相應的Document。
根據某個字段值的排序功能
lucene缺省是按照本身的相關度算法(score)進行結果排序的,但可以根據其餘字段進行結果排序是一個在LUCENE的開發郵件列表中常常提到的問題,不少原先基於數據庫應用都須要除了基於匹配度(score)之外的排序功能。而從全文檢索的原理咱們能夠了解到,任何不基於索引的搜索過程效率都會致使效率很是的低,若是基於其餘字段的排序須要在搜索過程當中訪問存儲字段,速度回大大下降,所以很是是不可取的。
但這裏也有一個折中的解決方法:在搜索過程當中可以影響排序結果的只有索引中已經存儲的docID和score這2個參數,因此,基於score之外的排序,其實能夠經過將數據源預先排好序,而後根據docID進行排序來實現。這樣就避免了在LUCENE搜索結果外對結果再次進行排序和在搜索過程當中訪問不在索引中的某個字段值。
這裏須要修改的是IndexSearcher中的HitCollector過程:
...
scorer.score(newHitCollector() { private float minScore = 0.0f; public final void collect(int doc,float score) { if (score > 0.0f && // ignore zeroed buckets (bits==null || bits.get(doc))) { // skip docs not in bits totalHits[0]++; if (score >= minScore) { /* 原先:Lucene將docID和相應的匹配度score例入結果命中列表中: *hq.put(new ScoreDoc(doc, score)); // update hit queue * 若是用doc 或 1/doc 代替 score,就實現了根據docID順排或逆排 * 假設數據源索引時已經按照某個字段排好了序,而結果根據docID排序也就實現了 * 針對某個字段的排序,甚至能夠實現更復雜的score和docID的擬合。 */ hq.put(new ScoreDoc(doc, (float) 1/doc )); if (hq.size() > nDocs) { // if hit queue overfull hq.pop(); // remove lowest in hit queue minScore =((ScoreDoc)hq.top()).score; // reset minScore } } } } }, reader.maxDoc());
更通用的輸入輸出接口
雖然lucene沒有定義一個肯定的輸入文檔格式,但愈來愈多的人想到使用一個標準的中間格式做爲Lucene的數據導入接口,而後其餘數據,好比PDF只須要經過解析器轉換成標準的中間格式就能夠進行數據索引了。這個中間格式主要以XML爲主,相似實現已經不下4,5個:
數據源: WORD PDF HTML DB other
\ | | | /
XML中間格式
|
Lucene INDEX
目前尚未針對MSWord文檔的解析器,由於Word文檔和基於ASCII的RTF文檔不一樣,須要使用COM對象機制解析。這個是我在Google上查的相關資料:http://www.intrinsyc.com/products/enterprise_applications.asp
另一個辦法就是把Word文檔轉換成text:http://www.winfield.demon.nl/index.html
索引過程優化
索引通常分2種狀況,一種是小批量的索引擴展,一種是大批量的索引重建。在索引過程當中,並非每次新的DOC加入進去索引都從新進行一次索引文件的寫入操做(文件I/O是一件很是消耗資源的事情)。
Lucene先在內存中進行索引操做,並根據必定的批量進行文件的寫入。這個批次的間隔越大,文件的寫入次數越少,但佔用內存會不少。反之佔用內存少,但文件IO操做頻繁,索引速度會很慢。在IndexWriter中有一個MERGE_FACTOR參數能夠幫助你在構造索引器後根據應用環境的狀況充分利用內存減小文件的操做。根據個人使用經驗:缺省Indexer是每20條記錄索引後寫入一次,每將MERGE_FACTOR增長50倍,索引速度能夠提升1倍左右。
搜索過程優化
lucene支持內存索引:這樣的搜索比基於文件的I/O有數量級的速度提高。
http://www.onjava.com/lpt/a/3273
而儘量減小IndexSearcher的建立和對搜索結果的前臺的緩存也是必要的。
Lucene面向全文檢索的優化在於首次索引檢索後,並不把全部的記錄(Document)具體內容讀取出來,而起只將全部結果中匹配度最高的頭100條結果(TopDocs)的ID放到結果集緩存中並返回,這裏能夠比較一下數據庫檢索:若是是一個10,000條的數據庫檢索結果集,數據庫是必定要把全部記錄內容都取得之後再開始返回給應用結果集的。因此即便檢索匹配總數不少,Lucene的結果集佔用的內存空間也不會不少。對於通常的模糊檢索應用是用不到這麼多的結果的,頭100條已經能夠知足90%以上的檢索需求。
若是首批緩存結果數用完後還要讀取更後面的結果時Searcher會再次檢索並生成一個上次的搜索緩存數大1倍的緩存,並再從新向後抓取。因此若是構造一個Searcher去查1-120條結果,Searcher實際上是進行了2次搜索過程:頭100條取完後,緩存結果用完,Searcher從新檢索再構造一個200條的結果緩存,依此類推,400條緩存,800條緩存。因爲每次Searcher對象消失後,這些緩存也訪問那不到了,你有可能想將結果記錄緩存下來,緩存數儘可能保證在100如下以充分利用首次的結果緩存,不讓Lucene浪費屢次檢索,並且能夠分級進行結果緩存。
Lucene的另一個特色是在收集結果的過程當中將匹配度低的結果自動過濾掉了。這也是和數據庫應用須要將搜索的結果所有返回不一樣之處。
· 支持中文的Tokenizer:這裏有2個版本,一個是經過JavaCC生成的,對CJK部分按一個字符一個TOKEN索引,另一個是從SimpleTokenizer改寫的,對英文支持數字和字母TOKEN,對中文按迭代索引。
· 基於XML數據源的索引器:XMLIndexer,所以全部數據源只要可以按照DTD轉換成指定的XML,就能夠用XMLIndxer進行索引了。
· 根據某個字段排序:按記錄索引順序排序結果的搜索器:IndexOrderSearcher,所以若是須要讓搜索結果根據某個字段排序,可讓數據源先按某個字段排好序(好比:PriceField),這樣索引後,而後在利用這個按記錄的ID順序檢索的搜索器,結果就是至關因而那個字段排序的結果了。
Luene的確是一個面對對象設計的典範
· 全部的問題都經過一個額外抽象層來方便之後的擴展和重用:你能夠經過從新實現來達到本身的目的,而對其餘模塊而不須要;
· 簡單的應用入口Searcher, Indexer,並調用底層一系列組件協同的完成搜索任務;
· 全部的對象的任務都很是專注:好比搜索過程:QueryParser分析將查詢語句轉換成一系列的精確查詢的組合(Query),經過底層的索引讀取結構IndexReader進行索引的讀取,並用相應的打分器給搜索結果進行打分/排序等。全部的功能模塊原子化程度很是高,所以能夠經過從新實現而不須要修改其餘模塊。
· 除了靈活的應用接口設計,Lucene還提供了一些適合大多數應用的語言分析器實現(SimpleAnalyser,StandardAnalyser),這也是新用戶可以很快上手的重要緣由之一。
這些優勢都是很是值得在之後的開發中學習借鑑的。做爲一個通用工具包,Lunece的確給予了須要將全文檢索功能嵌入到應用中的開發者不少的便利。
此外,經過對Lucene的學習和使用,我也更深入地理解了爲何不少數據庫優化設計中要求,好比:
· 儘量對字段進行索引來提升查詢速度,但過多的索引會對數據庫表的更新操做變慢,而對結果過多的排序條件,實際上每每也是性能的殺手之一。
· 不少商業數據庫對大批量的數據插入操做會提供一些優化參數,這個做用和索引器的merge_factor的做用是相似的,
· 20%/80%原則:查的結果多並不等於質量好,尤爲對於返回結果集很大,如何優化這頭幾十條結果的質量每每纔是最重要的。
· 儘量讓應用從數據庫中得到比較小的結果集,由於即便對於大型數據庫,對結果集的隨機訪問也是一個很是消耗資源的操做。