wyl232

          2008年9月19日 #

          用 Lucene 加速 Web 搜索應(yīng)用程序的開發(fā)

          用 Lucene 加速 Web 搜索應(yīng)用程序的開發(fā)

          級(jí)別: 中級(jí)

          周 登朋 (zhoudengpeng@yahoo.com.cn), 軟件工程師, 上海交通大學(xué)

          2006 年 9 月 06 日

          Lucene 是基于 Java 的全文信息檢索包,它目前是 Apache Jakarta 家族下面的一個(gè)開源項(xiàng)目。在這篇文章中,我們首先來看如何利用 Lucene 實(shí)現(xiàn)高級(jí)搜索功能,然后學(xué)習(xí)如何利用 Lucene 來創(chuàng)建一個(gè)健壯的 Web 搜索應(yīng)用程序。

          在本篇文章中,你會(huì)學(xué)習(xí)到如何利用 Lucene 實(shí)現(xiàn)高級(jí)搜索功能以及如何利用 Lucene 來創(chuàng)建 Web 搜索應(yīng)用程序。通過這些學(xué)習(xí),你就可以利用 Lucene 來創(chuàng)建自己的搜索應(yīng)用程序。

          架構(gòu)概覽

          通常一個(gè) Web 搜索引擎的架構(gòu)分為前端和后端兩部分,就像圖一中所示。在前端流程中,用戶在搜索引擎提供的界面中輸入要搜索的關(guān)鍵詞,這里提到的用戶界面一般是一個(gè)帶有輸入框的 Web 頁面,然后應(yīng)用程序?qū)⑺阉鞯年P(guān)鍵詞解析成搜索引擎可以理解的形式,并在索引文件上進(jìn)行搜索操作。在排序后,搜索引擎返回搜索結(jié)果給用戶。在后端流程中,網(wǎng)絡(luò)爬蟲或者機(jī)器人從因特網(wǎng)上獲取 Web 頁面,然后索引子系統(tǒng)解析這些 Web 頁面并存入索引文件中。如果你想利用 Lucene 來創(chuàng)建一個(gè) Web 搜索應(yīng)用程序,那么它的架構(gòu)也和上面所描述的類似,就如圖一中所示。


          Figure 1. Web 搜索引擎架構(gòu)
          Web搜索引擎架構(gòu)

          利用 Lucene 實(shí)現(xiàn)高級(jí)搜索

          Lucene 支持多種形式的高級(jí)搜索,我們?cè)谶@一部分中會(huì)進(jìn)行探討,然后我會(huì)使用 Lucene 的 API 來演示如何實(shí)現(xiàn)這些高級(jí)搜索功能。

          布爾操作符

          大多數(shù)的搜索引擎都會(huì)提供布爾操作符讓用戶可以組合查詢,典型的布爾操作符有 AND, OR, NOT。Lucene 支持 5 種布爾操作符,分別是 AND, OR, NOT, 加(+), 減(-)。接下來我會(huì)講述每個(gè)操作符的用法。

          • OR: 如果你要搜索含有字符 A 或者 B 的文檔,那么就需要使用 OR 操作符。需要記住的是,如果你只是簡(jiǎn)單的用空格將兩個(gè)關(guān)鍵詞分割開,其實(shí)在搜索的時(shí)候搜索引擎會(huì)自動(dòng)在兩個(gè)關(guān)鍵詞之間加上 OR 操作符。例如,“Java OR Lucene” 和 “Java Lucene” 都是搜索含有 Java 或者含有 Lucene 的文檔。
          • AND: 如果你需要搜索包含一個(gè)以上關(guān)鍵詞的文檔,那么就需要使用 AND 操作符。例如,“Java AND Lucene” 返回所有既包含 Java 又包含 Lucene 的文檔。
          • NOT: Not 操作符使得包含緊跟在 NOT 后面的關(guān)鍵詞的文檔不會(huì)被返回。例如,如果你想搜索所有含有 Java 但不含有 Lucene 的文檔,你可以使用查詢語句 “Java NOT Lucene”。但是你不能只對(duì)一個(gè)搜索詞使用這個(gè)操作符,比如,查詢語句 “NOT Java” 不會(huì)返回任何結(jié)果。
          • 加號(hào)(+): 這個(gè)操作符的作用和 AND 差不多,但它只對(duì)緊跟著它的一個(gè)搜索詞起作用。例如,如果你想搜索一定包含 Java,但不一定包含 Lucene 的文檔,就可以使用查詢語句“+Java Lucene”。
          • 減號(hào)(-): 這個(gè)操作符的功能和 NOT 一樣,查詢語句 “Java -Lucene” 返回所有包含 Java 但不包含 Lucene 的文檔。

          接下來我們看一下如何利用 Lucene 提供的 API 來實(shí)現(xiàn)布爾查詢。清單1 顯示了如果利用布爾操作符進(jìn)行查詢的過程。


          清單1:使用布爾操作符
            //Test boolean operator
                      public void testOperator(String indexDirectory) throws Exception{
                      Directory dir = FSDirectory.getDirectory(indexDirectory,false);
                      IndexSearcher indexSearcher = new IndexSearcher(dir);
                      String[] searchWords = {"Java AND Lucene", "Java NOT Lucene", "Java OR Lucene",
                      "+Java +Lucene", "+Java -Lucene"};
                      Analyzer language = new StandardAnalyzer();
                      Query query;
                      for(int i = 0; i < searchWords.length; i++){
                      query = QueryParser.parse(searchWords[i], "title", language);
                      Hits results = indexSearcher.search(query);
                      System.out.println(results.length() + "search results for query " + searchWords[i]);
                      }
                      }
                      

          域搜索(Field Search)

          Lucene 支持域搜索,你可以指定一次查詢是在哪些域(Field)上進(jìn)行。例如,如果索引的文檔包含兩個(gè)域,TitleContent,你就可以使用查詢 “Title: Lucene AND Content: Java” 來返回所有在 Title 域上包含 Lucene 并且在 Content 域上包含 Java 的文檔。清單 2 顯示了如何利用 Lucene 的 API 來實(shí)現(xiàn)域搜索。


          清單2:實(shí)現(xiàn)域搜索
          //Test field search
                      public void testFieldSearch(String indexDirectory) throws Exception{
                      Directory dir = FSDirectory.getDirectory(indexDirectory,false);
                      IndexSearcher indexSearcher = new IndexSearcher(dir);
                      String searchWords = "title:Lucene AND content:Java";
                      Analyzer language = new StandardAnalyzer();
                      Query query = QueryParser.parse(searchWords, "title", language);
                      Hits results = indexSearcher.search(query);
                      System.out.println(results.length() + "search results for query " + searchWords);
                      }
                      

          通配符搜索(Wildcard Search)

          Lucene 支持兩種通配符:?jiǎn)柼?hào)(?)和星號(hào)(*)。你可以使用問號(hào)(?)來進(jìn)行單字符的通配符查詢,或者利用星號(hào)(*)進(jìn)行多字符的通配符查詢。例如,如果你想搜索 tiny 或者 tony,你就可以使用查詢語句 “t?ny”;如果你想查詢 Teach, Teacher 和 Teaching,你就可以使用查詢語句 “Teach*”。清單3 顯示了通配符查詢的過程。


          清單3:進(jìn)行通配符查詢
          //Test wildcard search
                      public void testWildcardSearch(String indexDirectory)throws Exception{
                      Directory dir = FSDirectory.getDirectory(indexDirectory,false);
                      IndexSearcher indexSearcher = new IndexSearcher(dir);
                      String[] searchWords = {"tex*", "tex?", "?ex*"};
                      Query query;
                      for(int i = 0; i < searchWords.length; i++){
                      query = new WildcardQuery(new Term("title",searchWords[i]));
                      Hits results = indexSearcher.search(query);
                      System.out.println(results.length() + "search results for query " + searchWords[i]);
                      }
                      }
                      

          模糊查詢

          Lucene 提供的模糊查詢基于編輯距離算法(Edit distance algorithm)。你可以在搜索詞的尾部加上字符 ~ 來進(jìn)行模糊查詢。例如,查詢語句 “think~” 返回所有包含和 think 類似的關(guān)鍵詞的文檔。清單 4 顯示了如果利用 Lucene 的 API 進(jìn)行模糊查詢的代碼。


          清單4:實(shí)現(xiàn)模糊查詢
          //Test fuzzy search
                      public void testFuzzySearch(String indexDirectory)throws Exception{
                      Directory dir = FSDirectory.getDirectory(indexDirectory,false);
                      IndexSearcher indexSearcher = new IndexSearcher(dir);
                      String[] searchWords = {"text", "funny"};
                      Query query;
                      for(int i = 0; i < searchWords.length; i++){
                      query = new FuzzyQuery(new Term("title",searchWords[i]));
                      Hits results = indexSearcher.search(query);
                      System.out.println(results.length() + "search results for query " + searchWords[i]);
                      }
                      }
                      

          范圍搜索(Range Search)

          范圍搜索匹配某個(gè)域上的值在一定范圍的文檔。例如,查詢 “age:[18 TO 35]” 返回所有 age 域上的值在 18 到 35 之間的文檔。清單5顯示了利用 Lucene 的 API 進(jìn)行返回搜索的過程。


          清單5:測(cè)試范圍搜索
          //Test range search
                      public void testRangeSearch(String indexDirectory)throws Exception{
                      Directory dir = FSDirectory.getDirectory(indexDirectory,false);
                      IndexSearcher indexSearcher = new IndexSearcher(dir);
                      Term begin = new Term("birthDay","20000101");
                      Term end   = new Term("birthDay","20060606");
                      Query query = new RangeQuery(begin,end,true);
                      Hits results = indexSearcher.search(query);
                      System.out.println(results.length() + "search results is returned");
                      }
                      





          回頁首


          在 Web 應(yīng)用程序中集成 Lucene

          接下來我們開發(fā)一個(gè) Web 應(yīng)用程序利用 Lucene 來檢索存放在文件服務(wù)器上的 HTML 文檔。在開始之前,需要準(zhǔn)備如下環(huán)境:

          1. Eclipse 集成開發(fā)環(huán)境
          2. Tomcat 5.0
          3. Lucene Library
          4. JDK 1.5

          這個(gè)例子使用 Eclipse 進(jìn)行 Web 應(yīng)用程序的開發(fā),最終這個(gè) Web 應(yīng)用程序跑在 Tomcat 5.0 上面。在準(zhǔn)備好開發(fā)所必需的環(huán)境之后,我們接下來進(jìn)行 Web 應(yīng)用程序的開發(fā)。

          1、創(chuàng)建一個(gè)動(dòng)態(tài) Web 項(xiàng)目

          1. 在 Eclipse 里面,選擇 File > New > Project,然后再彈出的窗口中選擇動(dòng)態(tài) Web 項(xiàng)目,如圖二所示。

          圖二:創(chuàng)建動(dòng)態(tài)Web項(xiàng)目
          創(chuàng)建動(dòng)態(tài)Web項(xiàng)目
          1. 在創(chuàng)建好動(dòng)態(tài) Web 項(xiàng)目之后,你會(huì)看到創(chuàng)建好的項(xiàng)目的結(jié)構(gòu),如圖三所示,項(xiàng)目的名稱為 sample.dw.paper.lucene。

          圖三:動(dòng)態(tài) Web 項(xiàng)目的結(jié)構(gòu)
          動(dòng)態(tài) Web 項(xiàng)目的結(jié)構(gòu)

          2. 設(shè)計(jì) Web 項(xiàng)目的架構(gòu)

          在我們的設(shè)計(jì)中,把該系統(tǒng)分成如下四個(gè)子系統(tǒng):

          1. 用戶接口: 這個(gè)子系統(tǒng)提供用戶界面使用戶可以向 Web 應(yīng)用程序服務(wù)器提交搜索請(qǐng)求,然后搜索結(jié)果通過用戶接口來顯示出來。我們用一個(gè)名為 search.jsp 的頁面來實(shí)現(xiàn)該子系統(tǒng)。
          2. 請(qǐng)求管理器: 這個(gè)子系統(tǒng)管理從客戶端發(fā)送過來的搜索請(qǐng)求并把搜索請(qǐng)求分發(fā)到搜索子系統(tǒng)中。最后搜索結(jié)果從搜索子系統(tǒng)返回并最終發(fā)送到用戶接口子系統(tǒng)。我們使用一個(gè) Servlet 來實(shí)現(xiàn)這個(gè)子系統(tǒng)。
          3. 搜索子系統(tǒng): 這個(gè)子系統(tǒng)負(fù)責(zé)在索引文件上進(jìn)行搜索并把搜索結(jié)構(gòu)傳遞給請(qǐng)求管理器。我們使用 Lucene 提供的 API 來實(shí)現(xiàn)該子系統(tǒng)。
          4. 索引子系統(tǒng): 這個(gè)子系統(tǒng)用來為 HTML 頁面來創(chuàng)建索引。我們使用 Lucene 的 API 以及 Lucene 提供的一個(gè) HTML 解析器來創(chuàng)建該子系統(tǒng)。

          圖4 顯示了我們?cè)O(shè)計(jì)的詳細(xì)信息,我們將用戶接口子系統(tǒng)放到 webContent 目錄下面。你會(huì)看到一個(gè)名為 search.jsp 的頁面在這個(gè)文件夾里面。請(qǐng)求管理子系統(tǒng)在包 sample.dw.paper.lucene.servlet 下面,類 SearchController 負(fù)責(zé)功能的實(shí)現(xiàn)。搜索子系統(tǒng)放在包 sample.dw.paper.lucene.search 當(dāng)中,它包含了兩個(gè)類,SearchManagerSearchResultBean,第一個(gè)類用來實(shí)現(xiàn)搜索功能,第二個(gè)類用來描述搜索結(jié)果的結(jié)構(gòu)。索引子系統(tǒng)放在包 sample.dw.paper.lucene.index 當(dāng)中。類 IndexManager 負(fù)責(zé)為 HTML 文件創(chuàng)建索引。該子系統(tǒng)利用包 sample.dw.paper.lucene.util 里面的類 HTMLDocParser 提供的方法 getTitlegetContent 來對(duì) HTML 頁面進(jìn)行解析。


          圖四:項(xiàng)目的架構(gòu)設(shè)計(jì)
          項(xiàng)目的架構(gòu)設(shè)計(jì)

          3. 子系統(tǒng)的實(shí)現(xiàn)

          在分析了系統(tǒng)的架構(gòu)設(shè)計(jì)之后,我們接下來看系統(tǒng)實(shí)現(xiàn)的詳細(xì)信息。

          1. 用戶接口: 這個(gè)子系統(tǒng)有一個(gè)名為 search.jsp 的 JSP 文件來實(shí)現(xiàn),這個(gè) JSP 頁面包含兩個(gè)部分。第一部分提供了一個(gè)用戶接口去向 Web 應(yīng)用程序服務(wù)器提交搜索請(qǐng)求,如圖5所示。注意到這里的搜索請(qǐng)求發(fā)送到了一個(gè)名為 SearchController 的 Servlet 上面。Servlet 的名字和具體實(shí)現(xiàn)的類的對(duì)應(yīng)關(guān)系在 web.xml 里面指定。

          圖5:向Web服務(wù)器提交搜索請(qǐng)求
          向Web服務(wù)器提交搜索請(qǐng)求

          這個(gè)JSP的第二部分負(fù)責(zé)顯示搜索結(jié)果給用戶,如圖6所示:


          圖6:顯示搜索結(jié)果
          顯示搜索結(jié)果
          1. 請(qǐng)求管理器: 一個(gè)名為 SearchController 的 servlet 用來實(shí)現(xiàn)該子系統(tǒng)。清單6給出了這個(gè)類的源代碼。

          清單6:請(qǐng)求管理器的實(shí)現(xiàn)
          package sample.dw.paper.lucene.servlet;
                      import java.io.IOException;
                      import java.util.List;
                      import javax.servlet.RequestDispatcher;
                      import javax.servlet.ServletException;
                      import javax.servlet.http.HttpServlet;
                      import javax.servlet.http.HttpServletRequest;
                      import javax.servlet.http.HttpServletResponse;
                      import sample.dw.paper.lucene.search.SearchManager;
                      /**
                      * This servlet is used to deal with the search request
                      * and return the search results to the client
                      */
                      public class SearchController extends HttpServlet{
                      private static final long serialVersionUID = 1L;
                      public void doPost(HttpServletRequest request, HttpServletResponse response)
                      throws IOException, ServletException{
                      String searchWord = request.getParameter("searchWord");
                      SearchManager searchManager = new SearchManager(searchWord);
                      List searchResult = null;
                      searchResult = searchManager.search();
                      RequestDispatcher dispatcher = request.getRequestDispatcher("search.jsp");
                      request.setAttribute("searchResult",searchResult);
                      dispatcher.forward(request, response);
                      }
                      public void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws IOException, ServletException{
                      doPost(request, response);
                      }
                      }
                      

          清單6中,doPost 方法從客戶端獲取搜索詞并創(chuàng)建類 SearchManager 的一個(gè)實(shí)例,其中類 SearchManager 在搜索子系統(tǒng)中進(jìn)行了定義。然后,SearchManager 的方法 search 會(huì)被調(diào)用。最后搜索結(jié)果被返回到客戶端。

          1. 搜索子系統(tǒng): 在這個(gè)子系統(tǒng)中,我們定義了兩個(gè)類:SearchManagerSearchResultBean。第一個(gè)類用來實(shí)現(xiàn)搜索功能,第二個(gè)類是個(gè)JavaBean,用來描述搜索結(jié)果的結(jié)構(gòu)。清單7給出了類 SearchManager 的源代碼。

          清單7:搜索功能的實(shí)現(xiàn)
          package sample.dw.paper.lucene.search;
                      import java.io.IOException;
                      import java.util.ArrayList;
                      import java.util.List;
                      import org.apache.lucene.analysis.Analyzer;
                      import org.apache.lucene.analysis.standard.StandardAnalyzer;
                      import org.apache.lucene.queryParser.ParseException;
                      import org.apache.lucene.queryParser.QueryParser;
                      import org.apache.lucene.search.Hits;
                      import org.apache.lucene.search.IndexSearcher;
                      import org.apache.lucene.search.Query;
                      import sample.dw.paper.lucene.index.IndexManager;
                      /**
                      * This class is used to search the
                      * Lucene index and return search results
                      */
                      public class SearchManager {
                      private String searchWord;
                      private IndexManager indexManager;
                      private Analyzer analyzer;
                      public SearchManager(String searchWord){
                      this.searchWord   =  searchWord;
                      this.indexManager =  new IndexManager();
                      this.analyzer     =  new StandardAnalyzer();
                      }
                      /**
                      * do search
                      */
                      public List search(){
                      List searchResult = new ArrayList();
                      if(false == indexManager.ifIndexExist()){
                      try {
                      if(false == indexManager.createIndex()){
                      return searchResult;
                      }
                      } catch (IOException e) {
                      e.printStackTrace();
                      return searchResult;
                      }
                      }
                      IndexSearcher indexSearcher = null;
                      try{
                      indexSearcher = new IndexSearcher(indexManager.getIndexDir());
                      }catch(IOException ioe){
                      ioe.printStackTrace();
                      }
                      QueryParser queryParser = new QueryParser("content",analyzer);
                      Query query = null;
                      try {
                      query = queryParser.parse(searchWord);
                      } catch (ParseException e) {
                      e.printStackTrace();
                      }
                      if(null != query >> null != indexSearcher){
                      try {
                      Hits hits = indexSearcher.search(query);
                      for(int i = 0; i < hits.length(); i ++){
                      SearchResultBean resultBean = new SearchResultBean();
                      resultBean.setHtmlPath(hits.doc(i).get("path"));
                      resultBean.setHtmlTitle(hits.doc(i).get("title"));
                      searchResult.add(resultBean);
                      }
                      } catch (IOException e) {
                      e.printStackTrace();
                      }
                      }
                      return searchResult;
                      }
                      }
                      

          清單7中,注意到在這個(gè)類里面有三個(gè)私有屬性。第一個(gè)是 searchWord,代表了來自客戶端的搜索詞。第二個(gè)是 indexManager,代表了在索引子系統(tǒng)中定義的類 IndexManager 的一個(gè)實(shí)例。第三個(gè)是 analyzer,代表了用來解析搜索詞的解析器。現(xiàn)在我們把注意力放在方法 search 上面。這個(gè)方法首先檢查索引文件是否已經(jīng)存在,如果已經(jīng)存在,那么就在已經(jīng)存在的索引上進(jìn)行檢索,如果不存在,那么首先調(diào)用類 IndexManager 提供的方法來創(chuàng)建索引,然后在新創(chuàng)建的索引上進(jìn)行檢索。搜索結(jié)果返回后,這個(gè)方法從搜索結(jié)果中提取出需要的屬性并為每個(gè)搜索結(jié)果生成類 SearchResultBean 的一個(gè)實(shí)例。最后這些 SearchResultBean 的實(shí)例被放到一個(gè)列表里面并返回給請(qǐng)求管理器。

          在類 SearchResultBean 中,含有兩個(gè)屬性,分別是 htmlPathhtmlTitle,以及這個(gè)兩個(gè)屬性的 get 和 set 方法。這也意味著我們的搜索結(jié)果包含兩個(gè)屬性:htmlPathhtmlTitle,其中 htmlPath 代表了 HTML 文件的路徑,htmlTitle 代表了 HTML 文件的標(biāo)題。

          1. 索引子系統(tǒng): 類 IndexManager 用來實(shí)現(xiàn)這個(gè)子系統(tǒng)。清單8 給出了這個(gè)類的源代碼。

          清單8:索引子系統(tǒng)的實(shí)現(xiàn)
          package sample.dw.paper.lucene.index;
                      import java.io.File;
                      import java.io.IOException;
                      import java.io.Reader;
                      import org.apache.lucene.analysis.Analyzer;
                      import org.apache.lucene.analysis.standard.StandardAnalyzer;
                      import org.apache.lucene.document.Document;
                      import org.apache.lucene.document.Field;
                      import org.apache.lucene.index.IndexWriter;
                      import org.apache.lucene.store.Directory;
                      import org.apache.lucene.store.FSDirectory;
                      import sample.dw.paper.lucene.util.HTMLDocParser;
                      /**
                      * This class is used to create an index for HTML files
                      *
                      */
                      public class IndexManager {
                      //the directory that stores HTML files
                      private final String dataDir  = "c:\\dataDir";
                      //the directory that is used to store a Lucene index
                      private final String indexDir = "c:\\indexDir";
                      /**
                      * create index
                      */
                      public boolean createIndex() throws IOException{
                      if(true == ifIndexExist()){
                      return true;
                      }
                      File dir = new File(dataDir);
                      if(!dir.exists()){
                      return false;
                      }
                      File[] htmls = dir.listFiles();
                      Directory fsDirectory = FSDirectory.getDirectory(indexDir, true);
                      Analyzer  analyzer    = new StandardAnalyzer();
                      IndexWriter indexWriter = new IndexWriter(fsDirectory, analyzer, true);
                      for(int i = 0; i < htmls.length; i++){
                      String htmlPath = htmls[i].getAbsolutePath();
                      if(htmlPath.endsWith(".html") || htmlPath.endsWith(".htm")){
                      addDocument(htmlPath, indexWriter);
                      }
                      }
                      indexWriter.optimize();
                      indexWriter.close();
                      return true;
                      }
                      /**
                      * Add one document to the Lucene index
                      */
                      public void addDocument(String htmlPath, IndexWriter indexWriter){
                      HTMLDocParser htmlParser = new HTMLDocParser(htmlPath);
                      String path    = htmlParser.getPath();
                      String title   = htmlParser.getTitle();
                      Reader content = htmlParser.getContent();
                      Document document = new Document();
                      document.add(new Field("path",path,Field.Store.YES,Field.Index.NO));
                      document.add(new Field("title",title,Field.Store.YES,Field.Index.TOKENIZED));
                      document.add(new Field("content",content));
                      try {
                      indexWriter.addDocument(document);
                      } catch (IOException e) {
                      e.printStackTrace();
                      }
                      }
                      /**
                      * judge if the index exists already
                      */
                      public boolean ifIndexExist(){
                      File directory = new File(indexDir);
                      if(0 < directory.listFiles().length){
                      return true;
                      }else{
                      return false;
                      }
                      }
                      public String getDataDir(){
                      return this.dataDir;
                      }
                      public String getIndexDir(){
                      return this.indexDir;
                      }
                      }
                      

          這個(gè)類包含兩個(gè)私有屬性,分別是 dataDirindexDirdataDir 代表存放等待進(jìn)行索引的 HTML 頁面的路徑,indexDir 代表了存放 Lucene 索引文件的路徑。類 IndexManager 提供了三個(gè)方法,分別是 createIndex, addDocumentifIndexExist。如果索引不存在的話,你可以使用方法 createIndex 去創(chuàng)建一個(gè)新的索引,用方法 addDocument 去向一個(gè)索引上添加文檔。在我們的場(chǎng)景中,一個(gè)文檔就是一個(gè) HTML 頁面。方法 addDocument 會(huì)調(diào)用由類 HTMLDocParser 提供的方法對(duì) HTML 文檔進(jìn)行解析。你可以使用最后一個(gè)方法 ifIndexExist 來判斷 Lucene 的索引是否已經(jīng)存在。

          現(xiàn)在我們來看一下放在包 sample.dw.paper.lucene.util 里面的類 HTMLDocParser。這個(gè)類用來從 HTML 文件中提取出文本信息。這個(gè)類包含三個(gè)方法,分別是 getContentgetTitlegetPath。第一個(gè)方法返回去除了 HTML 標(biāo)記的文本內(nèi)容,第二個(gè)方法返回 HTML 文件的標(biāo)題,最后一個(gè)方法返回 HTML 文件的路徑。清單9 給出了這個(gè)類的源代碼。


          清單9:HTML 解析器
          package sample.dw.paper.lucene.util;
                      import java.io.FileInputStream;
                      import java.io.FileNotFoundException;
                      import java.io.IOException;
                      import java.io.InputStream;
                      import java.io.InputStreamReader;
                      import java.io.Reader;
                      import java.io.UnsupportedEncodingException;
                      import org.apache.lucene.demo.html.HTMLParser;
                      public class HTMLDocParser {
                      private String htmlPath;
                      private HTMLParser htmlParser;
                      public HTMLDocParser(String htmlPath){
                      this.htmlPath = htmlPath;
                      initHtmlParser();
                      }
                      private void initHtmlParser(){
                      InputStream inputStream = null;
                      try {
                      inputStream = new FileInputStream(htmlPath);
                      } catch (FileNotFoundException e) {
                      e.printStackTrace();
                      }
                      if(null != inputStream){
                      try {
                      htmlParser = new HTMLParser(new InputStreamReader(inputStream, "utf-8"));
                      } catch (UnsupportedEncodingException e) {
                      e.printStackTrace();
                      }
                      }
                      }
                      public String getTitle(){
                      if(null != htmlParser){
                      try {
                      return htmlParser.getTitle();
                      } catch (IOException e) {
                      e.printStackTrace();
                      } catch (InterruptedException e) {
                      e.printStackTrace();
                      }
                      }
                      return "";
                      }
                      public Reader getContent(){
                      if(null != htmlParser){
                      try {
                      return htmlParser.getReader();
                      } catch (IOException e) {
                      e.printStackTrace();
                      }
                      }
                      return null;
                      }
                      public String getPath(){
                      return this.htmlPath;
                      }
                      }
                      

          5.在 Tomcat 5.0 上運(yùn)行應(yīng)用程序

          現(xiàn)在我們可以在 Tomcat 5.0 上運(yùn)行開發(fā)好的應(yīng)用程序。

          1. 右鍵單擊 search.jsp,然后選擇 Run as > Run on Server,如圖7所示。

          圖7:配置 Tomcat 5.0
          配置 Tomcat 5.0
          1. 在彈出的窗口中,選擇 Tomcat v5.0 Server 作為目標(biāo) Web 應(yīng)用程序服務(wù)器,然后點(diǎn)擊 Next,如圖8 所示:

          圖8:選擇 Tomcat 5.0
          選擇 Tomcat 5.0
          1. 現(xiàn)在需要指定用來運(yùn)行 Web 應(yīng)用程序的 Apache Tomcat 5.0 以及 JRE 的路徑。這里你所選擇的 JRE 的版本必須和你用來編譯 Java 文件的 JRE 的版本一致。配置好之后,點(diǎn)擊 Finish。如 圖9 所示。

          圖9:完成Tomcat 5.0的配置
          完成Tomcat 5.0的配置
          1. 配置好之后,Tomcat 會(huì)自動(dòng)運(yùn)行,并且會(huì)對(duì) search.jsp 進(jìn)行編譯并顯示給用戶。如 圖10 所示。

          圖10:用戶界面
          用戶界面
          1. 在輸入框中輸入關(guān)鍵詞 “information” 然后單擊 Search 按鈕。然后這個(gè)頁面上會(huì)顯示出搜索結(jié)果來,如 圖11 所示。

          圖11:搜索結(jié)果
          搜索結(jié)果
          1. 單擊搜索結(jié)果的第一個(gè)鏈接,頁面上就會(huì)顯示出所鏈接到的頁面的內(nèi)容。如 圖12 所示.

          圖12:詳細(xì)信息
          詳細(xì)信息

          現(xiàn)在我們已經(jīng)成功的完成了示例項(xiàng)目的開發(fā),并成功的用Lucene實(shí)現(xiàn)了搜索和索引功能。你可以下載這個(gè)項(xiàng)目的源代碼(下載)。





          回頁首


          總結(jié)

          Lucene 提供了靈活的接口使我們更加方便的設(shè)計(jì)我們的 Web 搜索應(yīng)用程序。如果你想在你的應(yīng)用程序中加入搜索功能,那么 Lucene 是一個(gè)很好的選擇。在設(shè)計(jì)你的下一個(gè)帶有搜索功能的應(yīng)用程序的時(shí)候可以考慮使用 Lucene 來提供搜索功能。

          posted @ 2008-09-19 11:19 wyl 閱讀(80) | 評(píng)論 (0)編輯 收藏

          實(shí)戰(zhàn) Lucene,第 1 部分: 初識(shí) Lucene

          級(jí)別: 初級(jí)

          朋 周登 (zhoudengpeng@yahoo.com.cn), 軟件工程師

          2006 年 4 月 20 日

          本文首先介紹了Lucene的一些基本概念,然后開發(fā)了一個(gè)應(yīng)用程序演示了利用Lucene建立索引并在該索引上進(jìn)行搜索的過程。

          Lucene 簡(jiǎn)介

          Lucene 是一個(gè)基于 Java 的全文信息檢索工具包,它不是一個(gè)完整的搜索應(yīng)用程序,而是為你的應(yīng)用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta 家族中的一個(gè)開源項(xiàng)目。也是目前最為流行的基于 Java 開源全文檢索工具包。

          目前已經(jīng)有很多應(yīng)用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的幫助系統(tǒng)的搜索功能。Lucene 能夠?yàn)槲谋绢愋偷臄?shù)據(jù)建立索引,所以你只要能把你要索引的數(shù)據(jù)格式轉(zhuǎn)化的文本的,Lucene 就能對(duì)你的文檔進(jìn)行索引和搜索。比如你要對(duì)一些 HTML 文檔,PDF 文檔進(jìn)行索引的話你就首先需要把 HTML 文檔和 PDF 文檔轉(zhuǎn)化成文本格式的,然后將轉(zhuǎn)化后的內(nèi)容交給 Lucene 進(jìn)行索引,然后把創(chuàng)建好的索引文件保存到磁盤或者內(nèi)存中,最后根據(jù)用戶輸入的查詢條件在索引文件上進(jìn)行查詢。不指定要索引的文檔的格式也使 Lucene 能夠幾乎適用于所有的搜索應(yīng)用程序。

          圖 1 表示了搜索應(yīng)用程序和 Lucene 之間的關(guān)系,也反映了利用 Lucene 構(gòu)建搜索應(yīng)用程序的流程:


          圖1. 搜索應(yīng)用程序和 Lucene 之間的關(guān)系
          圖1. 搜索應(yīng)用程序和 Lucene 之間的關(guān)系




          回頁首


          索引和搜索

          索引是現(xiàn)代搜索引擎的核心,建立索引的過程就是把源數(shù)據(jù)處理成非常方便查詢的索引文件的過程。為什么索引這么重要呢,試想你現(xiàn)在要在大量的文檔中搜索含有某個(gè)關(guān)鍵詞的文檔,那么如果不建立索引的話你就需要把這些文檔順序的讀入內(nèi)存,然后檢查這個(gè)文章中是不是含有要查找的關(guān)鍵詞,這樣的話就會(huì)耗費(fèi)非常多的時(shí)間,想想搜索引擎可是在毫秒級(jí)的時(shí)間內(nèi)查找出要搜索的結(jié)果的。這就是由于建立了索引的原因,你可以把索引想象成這樣一種數(shù)據(jù)結(jié)構(gòu),他能夠使你快速的隨機(jī)訪問存儲(chǔ)在索引中的關(guān)鍵詞,進(jìn)而找到該關(guān)鍵詞所關(guān)聯(lián)的文檔。Lucene 采用的是一種稱為反向索引(inverted index)的機(jī)制。反向索引就是說我們維護(hù)了一個(gè)詞/短語表,對(duì)于這個(gè)表中的每個(gè)詞/短語,都有一個(gè)鏈表描述了有哪些文檔包含了這個(gè)詞/短語。這樣在用戶輸入查詢條件的時(shí)候,就能非常快的得到搜索結(jié)果。我們將在本系列文章的第二部分詳細(xì)介紹 Lucene 的索引機(jī)制,由于 Lucene 提供了簡(jiǎn)單易用的 API,所以即使讀者剛開始對(duì)全文本進(jìn)行索引的機(jī)制并不太了解,也可以非常容易的使用 Lucene 對(duì)你的文檔實(shí)現(xiàn)索引。

          對(duì)文檔建立好索引后,就可以在這些索引上面進(jìn)行搜索了。搜索引擎首先會(huì)對(duì)搜索的關(guān)鍵詞進(jìn)行解析,然后再在建立好的索引上面進(jìn)行查找,最終返回和用戶輸入的關(guān)鍵詞相關(guān)聯(lián)的文檔。





          回頁首


          Lucene 軟件包分析

          Lucene 軟件包的發(fā)布形式是一個(gè) JAR 文件,下面我們分析一下這個(gè) JAR 文件里面的主要的 JAVA 包,使讀者對(duì)之有個(gè)初步的了解。

          Package: org.apache.lucene.document

          這個(gè)包提供了一些為封裝要索引的文檔所需要的類,比如 Document, Field。這樣,每一個(gè)文檔最終被封裝成了一個(gè) Document 對(duì)象。

          Package: org.apache.lucene.analysis

          這個(gè)包主要功能是對(duì)文檔進(jìn)行分詞,因?yàn)槲臋n在建立索引之前必須要進(jìn)行分詞,所以這個(gè)包的作用可以看成是為建立索引做準(zhǔn)備工作。

          Package: org.apache.lucene.index

          這個(gè)包提供了一些類來協(xié)助創(chuàng)建索引以及對(duì)創(chuàng)建好的索引進(jìn)行更新。這里面有兩個(gè)基礎(chǔ)的類:IndexWriter 和 IndexReader,其中 IndexWriter 是用來創(chuàng)建索引并添加文檔到索引中的,IndexReader 是用來刪除索引中的文檔的。

          Package: org.apache.lucene.search

          這個(gè)包提供了對(duì)在建立好的索引上進(jìn)行搜索所需要的類。比如 IndexSearcher 和 Hits, IndexSearcher 定義了在指定的索引上進(jìn)行搜索的方法,Hits 用來保存搜索得到的結(jié)果。





          回頁首


          一個(gè)簡(jiǎn)單的搜索應(yīng)用程序

          假設(shè)我們的電腦的目錄中含有很多文本文檔,我們需要查找哪些文檔含有某個(gè)關(guān)鍵詞。為了實(shí)現(xiàn)這種功能,我們首先利用 Lucene 對(duì)這個(gè)目錄中的文檔建立索引,然后在建立好的索引中搜索我們所要查找的文檔。通過這個(gè)例子讀者會(huì)對(duì)如何利用 Lucene 構(gòu)建自己的搜索應(yīng)用程序有個(gè)比較清楚的認(rèn)識(shí)。





          回頁首


          建立索引

          為了對(duì)文檔進(jìn)行索引,Lucene 提供了五個(gè)基礎(chǔ)的類,他們分別是 Document, Field, IndexWriter, Analyzer, Directory。下面我們分別介紹一下這五個(gè)類的用途:

          Document

          Document 是用來描述文檔的,這里的文檔可以指一個(gè) HTML 頁面,一封電子郵件,或者是一個(gè)文本文件。一個(gè) Document 對(duì)象由多個(gè) Field 對(duì)象組成的。可以把一個(gè) Document 對(duì)象想象成數(shù)據(jù)庫中的一個(gè)記錄,而每個(gè) Field 對(duì)象就是記錄的一個(gè)字段。

          Field

          Field 對(duì)象是用來描述一個(gè)文檔的某個(gè)屬性的,比如一封電子郵件的標(biāo)題和內(nèi)容可以用兩個(gè) Field 對(duì)象分別描述。

          Analyzer

          在一個(gè)文檔被索引之前,首先需要對(duì)文檔內(nèi)容進(jìn)行分詞處理,這部分工作就是由 Analyzer 來做的。Analyzer 類是一個(gè)抽象類,它有多個(gè)實(shí)現(xiàn)。針對(duì)不同的語言和應(yīng)用需要選擇適合的 Analyzer。Analyzer 把分詞后的內(nèi)容交給 IndexWriter 來建立索引。

          IndexWriter

          IndexWriter 是 Lucene 用來創(chuàng)建索引的一個(gè)核心的類,他的作用是把一個(gè)個(gè)的 Document 對(duì)象加到索引中來。

          Directory

          這個(gè)類代表了 Lucene 的索引的存儲(chǔ)的位置,這是一個(gè)抽象類,它目前有兩個(gè)實(shí)現(xiàn),第一個(gè)是 FSDirectory,它表示一個(gè)存儲(chǔ)在文件系統(tǒng)中的索引的位置。第二個(gè)是 RAMDirectory,它表示一個(gè)存儲(chǔ)在內(nèi)存當(dāng)中的索引的位置。

          熟悉了建立索引所需要的這些類后,我們就開始對(duì)某個(gè)目錄下面的文本文件建立索引了,清單1給出了對(duì)某個(gè)目錄下的文本文件建立索引的源代碼。


          清單 1. 對(duì)文本文件建立索引
          package TestLucene;
                      import java.io.File;
                      import java.io.FileReader;
                      import java.io.Reader;
                      import java.util.Date;
                      import org.apache.lucene.analysis.Analyzer;
                      import org.apache.lucene.analysis.standard.StandardAnalyzer;
                      import org.apache.lucene.document.Document;
                      import org.apache.lucene.document.Field;
                      import org.apache.lucene.index.IndexWriter;
                      /**
                      * This class demonstrate the process of creating index with Lucene
                      * for text files
                      */
                      public class TxtFileIndexer {
                      public static void main(String[] args) throws Exception{
                      //indexDir is the directory that hosts Lucene's index files
                      File   indexDir = new File("D:\\luceneIndex");
                      //dataDir is the directory that hosts the text files that to be indexed
                      File   dataDir  = new File("D:\\luceneData");
                      Analyzer luceneAnalyzer = new StandardAnalyzer();
                      File[] dataFiles  = dataDir.listFiles();
                      IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
                      long startTime = new Date().getTime();
                      for(int i = 0; i < dataFiles.length; i++){
                      if(dataFiles[i].isFile() && dataFiles[i].getName().endsWith(".txt")){
                      System.out.println("Indexing file " + dataFiles[i].getCanonicalPath());
                              		Document document = new Document();
                      Reader txtReader = new FileReader(dataFiles[i]);
                      document.add(Field.Text("path",dataFiles[i].getCanonicalPath()));
                      document.add(Field.Text("contents",txtReader));
                      indexWriter.addDocument(document);
                      }
                      }
                      indexWriter.optimize();
                      indexWriter.close();
                      long endTime = new Date().getTime();
                      System.out.println("It takes " + (endTime - startTime)
                      + " milliseconds to create index for the files in directory "
                      + dataDir.getPath());
                      }
                      }
                      

          在清單1中,我們注意到類 IndexWriter 的構(gòu)造函數(shù)需要三個(gè)參數(shù),第一個(gè)參數(shù)指定了所創(chuàng)建的索引要存放的位置,他可以是一個(gè) File 對(duì)象,也可以是一個(gè) FSDirectory 對(duì)象或者 RAMDirectory 對(duì)象。第二個(gè)參數(shù)指定了 Analyzer 類的一個(gè)實(shí)現(xiàn),也就是指定這個(gè)索引是用哪個(gè)分詞器對(duì)文擋內(nèi)容進(jìn)行分詞。第三個(gè)參數(shù)是一個(gè)布爾型的變量,如果為 true 的話就代表創(chuàng)建一個(gè)新的索引,為 false 的話就代表在原來索引的基礎(chǔ)上進(jìn)行操作。接著程序遍歷了目錄下面的所有文本文檔,并為每一個(gè)文本文檔創(chuàng)建了一個(gè) Document 對(duì)象。然后把文本文檔的兩個(gè)屬性:路徑和內(nèi)容加入到了兩個(gè) Field 對(duì)象中,接著在把這兩個(gè) Field 對(duì)象加入到 Document 對(duì)象中,最后把這個(gè)文檔用 IndexWriter 類的 add 方法加入到索引中去。這樣我們便完成了索引的創(chuàng)建。接下來我們進(jìn)入在建立好的索引上進(jìn)行搜索的部分。





          回頁首


          搜索文檔

          利用Lucene進(jìn)行搜索就像建立索引一樣也是非常方便的。在上面一部分中,我們已經(jīng)為一個(gè)目錄下的文本文檔建立好了索引,現(xiàn)在我們就要在這個(gè)索引上進(jìn)行搜索以找到包含某個(gè)關(guān)鍵詞或短語的文檔。Lucene提供了幾個(gè)基礎(chǔ)的類來完成這個(gè)過程,它們分別是呢IndexSearcher, Term, Query, TermQuery, Hits. 下面我們分別介紹這幾個(gè)類的功能。

          Query

          這是一個(gè)抽象類,他有多個(gè)實(shí)現(xiàn),比如TermQuery, BooleanQuery, PrefixQuery. 這個(gè)類的目的是把用戶輸入的查詢字符串封裝成Lucene能夠識(shí)別的Query。

          Term

          Term是搜索的基本單位,一個(gè)Term對(duì)象有兩個(gè)String類型的域組成。生成一個(gè)Term對(duì)象可以有如下一條語句來完成:Term term = new Term(“fieldName”,”queryWord”); 其中第一個(gè)參數(shù)代表了要在文檔的哪一個(gè)Field上進(jìn)行查找,第二個(gè)參數(shù)代表了要查詢的關(guān)鍵詞。

          TermQuery

          TermQuery是抽象類Query的一個(gè)子類,它同時(shí)也是Lucene支持的最為基本的一個(gè)查詢類。生成一個(gè)TermQuery對(duì)象由如下語句完成: TermQuery termQuery = new TermQuery(new Term(“fieldName”,”queryWord”)); 它的構(gòu)造函數(shù)只接受一個(gè)參數(shù),那就是一個(gè)Term對(duì)象。

          IndexSearcher

          IndexSearcher是用來在建立好的索引上進(jìn)行搜索的。它只能以只讀的方式打開一個(gè)索引,所以可以有多個(gè)IndexSearcher的實(shí)例在一個(gè)索引上進(jìn)行操作。

          Hits

          Hits是用來保存搜索的結(jié)果的。

          介紹完這些搜索所必須的類之后,我們就開始在之前所建立的索引上進(jìn)行搜索了,清單2給出了完成搜索功能所需要的代碼。


          清單2 :在建立好的索引上進(jìn)行搜索
          package TestLucene;
                      import java.io.File;
                      import org.apache.lucene.document.Document;
                      import org.apache.lucene.index.Term;
                      import org.apache.lucene.search.Hits;
                      import org.apache.lucene.search.IndexSearcher;
                      import org.apache.lucene.search.TermQuery;
                      import org.apache.lucene.store.FSDirectory;
                      /**
                      * This class is used to demonstrate the
                      * process of searching on an existing
                      * Lucene index
                      *
                      */
                      public class TxtFileSearcher {
                      public static void main(String[] args) throws Exception{
                      String queryStr = "lucene";
                      //This is the directory that hosts the Lucene index
                      File indexDir = new File("D:\\luceneIndex");
                      FSDirectory directory = FSDirectory.getDirectory(indexDir,false);
                      IndexSearcher searcher = new IndexSearcher(directory);
                      if(!indexDir.exists()){
                      System.out.println("The Lucene index is not exist");
                      return;
                      }
                      Term term = new Term("contents",queryStr.toLowerCase());
                      TermQuery luceneQuery = new TermQuery(term);
                      Hits hits = searcher.search(luceneQuery);
                      for(int i = 0; i < hits.length(); i++){
                      Document document = hits.doc(i);
                      System.out.println("File: " + document.get("path"));
                      }
                      }
                      }
                      

          在清單2中,類IndexSearcher的構(gòu)造函數(shù)接受一個(gè)類型為Directory的對(duì)象,Directory是一個(gè)抽象類,它目前有兩個(gè)子類:FSDirctory和RAMDirectory. 我們的程序中傳入了一個(gè)FSDirctory對(duì)象作為其參數(shù),代表了一個(gè)存儲(chǔ)在磁盤上的索引的位置。構(gòu)造函數(shù)執(zhí)行完成后,代表了這個(gè)IndexSearcher以只讀的方式打開了一個(gè)索引。然后我們程序構(gòu)造了一個(gè)Term對(duì)象,通過這個(gè)Term對(duì)象,我們指定了要在文檔的內(nèi)容中搜索包含關(guān)鍵詞”lucene”的文檔。接著利用這個(gè)Term對(duì)象構(gòu)造出TermQuery對(duì)象并把這個(gè)TermQuery對(duì)象傳入到IndexSearcher的search方法中進(jìn)行查詢,返回的結(jié)果保存在Hits對(duì)象中。最后我們用了一個(gè)循環(huán)語句把搜索到的文檔的路徑都打印了出來。好了,我們的搜索應(yīng)用程序已經(jīng)開發(fā)完畢,怎么樣,利用Lucene開發(fā)搜索應(yīng)用程序是不是很簡(jiǎn)單。





          回頁首


          總結(jié)

          本文首先介紹了 Lucene 的一些基本概念,然后開發(fā)了一個(gè)應(yīng)用程序演示了利用 Lucene 建立索引并在該索引上進(jìn)行搜索的過程。希望本文能夠?yàn)閷W(xué)習(xí) Lucene 的讀者提供幫助。



          關(guān)于作者

           

          周登朋,軟件工程師,上海交通大學(xué)研究生,對(duì) Java 技術(shù)以及信息檢索技術(shù)很感興趣。您可以通過 zhoudengpeng@yahoo.com.cn 與他聯(lián)系。

          posted @ 2008-09-19 11:16 wyl 閱讀(104) | 評(píng)論 (0)編輯 收藏

          僅列出標(biāo)題  
          主站蜘蛛池模板: 儋州市| 台山市| 长乐市| 马龙县| 威远县| 郑州市| 库车县| 齐河县| 墨玉县| 阿拉善右旗| 灵山县| 乐东| 林西县| 弋阳县| 栖霞市| 乾安县| 成安县| 杂多县| 屏东市| 同仁县| 遂宁市| 昌乐县| 喀什市| 重庆市| 连江县| 遵化市| 嘉义市| 铜鼓县| 静宁县| 乌拉特后旗| 越西县| 阳城县| 紫阳县| 福清市| 汝阳县| 申扎县| 杂多县| 昌平区| 平顶山市| 西峡县| 天峨县|