程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Lucene:基於Java的全文檢索引擎簡介

Lucene:基於Java的全文檢索引擎簡介

編輯:關於JAVA

Lucene是一個基於Java的全文索引工具包。

基於Java的全文索引引擎Lucene簡介:關於作者和Lucene的歷史

全文檢索的實現:Luene全文索引和數據庫索引的比較

中文切分詞機制簡介:基於詞庫和自動切分詞算法的比較

具體的安裝和使用簡介:系統結構介紹和演示

Hacking Lucene:簡化的查詢分析器,刪除的實現,定制的排序,應用接口的擴展

從Lucene我們還可以學到什麼

基於Java的全文索引/檢索引擎——Lucene

Lucene不是一個完整的全文索引應用,而是是一個用Java寫的全文索引引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引/檢索功能。

Lucene的作者:Lucene的貢獻者Doug Cutting是一位資深全文索引/檢索專家,曾經是V-Twin搜索引擎(Apple的Copland操作系統的成就之一)的主要開發者,後在Excite擔任高級系統架構設計師,目前從事於一些INTERNET底層架構的研究。他貢獻出的Lucene的目標是為各種中小型應用程序加入全文檢索功能。

Lucene的發展歷程:早先發布在作者自己的www.lucene.com,後來發布在SourceForge,2001年年底成為APACHE基金會jakarta的一個子項目:http://jakarta.apache.org/lucene/

已經有很多Java項目都使用了Lucene作為其後台的全文索引引擎,比較著名的有:

Jive:WEB論壇系統;

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和數據庫:

全文檢索 ≠ like "%keyword%"

通常比較厚的書籍後面常常附關鍵詞索引表(比如:北京:12, 34頁,上海:3,77頁……),它能夠幫助讀者比較快地找到相關內容的頁碼。而數據庫索引能夠大大提高查詢的速度原理也是一樣,想像一下通過書後面的索引查找的速度要比一頁一頁地翻內容高多少倍……而索引之所以效率高,另外一個原因是它是排好序的。對於檢索系統來說核心是一個排序問題。

由於數據庫索引不是為全文索引設計的,因此,使用like "%keyword%"時,數據庫索引是不起作用的,在使用like查詢時,搜索過程又變成類似於一頁頁翻書的遍歷過程了,所以對於含有模糊查詢的數據庫服務來說,LIKE對性能的危害是極大的。如果是需要對多個關鍵詞進行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。

所以建立一個高效檢索系統的關鍵是建立一個類似於科技索引一樣的反向索引機制,將數據源(比如多篇文章)排序順序存儲的同時,有另外一個排好序的關鍵詞列表,用於存儲關鍵詞==>文章映射關系,利用這樣的映射關系索引:[關鍵詞==>出現關鍵詞的文章編號,出現次數(甚至包括位置:起始偏移量,結束偏移量),出現頻率],檢索過程就是把模糊查詢變成多個可以利用索引的精確查詢的邏輯組合的過程。從而大大提高了多關鍵詞查詢的效率,所以,全文檢索問題歸結到最後是一個排序問題。

由此可以看出模糊查詢相對數據庫的精確查詢是一個非常不確定的問題,這也是大部分數據庫對全文檢索支持有限的原因。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個機制的結合。關於中文的語言分析算法,大家可以在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 class IndexFiles {
 //使用方法:: 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("Indexing file " + 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 = new IndexSearcher(indexPath);
  //查詢解析器:使用和索引同樣的語言分析器
  Query query = QueryParser.parse(queryString, "body",
               new SimpleAnalyzer());
  //搜索結果使用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)都是提供了抽象的接口,可以根據需要進行定制。

Hacking Lucene

簡化的查詢分析器

個人感覺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(new HitCollector() {
  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順序檢索的搜索器,結果就是相當於是那個字段排序的結果了。

從Lucene學到更多

Luene的確是一個面對對象設計的典范

所有的問題都通過一個額外抽象層來方便以後的擴展和重用:你可以通過重新實現來達到自己的目的,而對其他模塊而不需要;

簡單的應用入口Searcher, Indexer,並調用底層一系列組件協同的完成搜索任務;

所有的對象的任務都非常專一:比如搜索過程:QueryParser分析將查詢語句轉換成一系列的精確查詢的組合(Query),通過底層的索引讀取結構IndexReader進行索引的讀取,並用相應的打分器給搜索結果進行打分/排序等。所有的功能模塊原子化程度非常高,因此可以通過重新實現而不需要修改其他模塊。 

除了靈活的應用接口設計,Lucene還提供了一些適合大多數應用的語言分析器實現(SimpleAnalyser,StandardAnalyser),這也是新用戶能夠很快上手的重要原因之一。

這些優點都是非常值得在以後的開發中學習借鑒的。作為一個通用工具包,Lunece的確給予了需要將全文檢索功能嵌入到應用中的開發者很多的便利。

此外,通過對Lucene的學習和使用,我也更深刻地理解了為什麼很多數據庫優化設計中要求,比如:

盡可能對字段進行索引來提高查詢速度,但過多的索引會對數據庫表的更新操作變慢,而對結果過多的排序條件,實際上往往也是性能的殺手之一。

很多商業數據庫對大批量的數據插入操作會提供一些優化參數,這個作用和索引器的merge_factor的作用是類似的,

20%/80%原則:查的結果多並不等於質量好,尤其對於返回結果集很大,如何優化這頭幾十條結果的質量往往才是最重要的。

盡可能讓應用從數據庫中獲得比較小的結果集,因為即使對於大型數據庫,對結果集的隨機訪問也是一個非常消耗資源的操作。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved