1. 程式人生 > >用Lucene加速Web站點搜尋應用程式的開發

用Lucene加速Web站點搜尋應用程式的開發

在本篇文章中,你會學習到如何利用 Lucene 實現高階搜尋功能以及如何利用 Lucene 來建立 Web 搜尋應用程式。通過這些學習,你就可以利用 Lucene 來建立自己的搜尋應用程式。

架構概覽

通常一個 Web 搜尋引擎的架構分為前端和後端兩部分,就像圖一中所示。在前端流程中,使用者在搜尋引擎提供的介面中輸入要搜尋的關鍵詞,這裡提到的使用者介面一般是一個帶有輸入框的 Web 頁面,然後應用程式將搜尋的關鍵詞解析成搜尋引擎可以理解的形式,並在索引檔案上進行搜尋操作。在排序後,搜尋引擎返回搜尋結果給使用者。在後端流程中,網路爬蟲或者機器人從因特網上獲取 Web 頁面,然後索引子系統解析這些 Web 頁面並存入索引檔案中。如果你想利用 Lucene 來建立一個 Web 搜尋應用程式,那麼它的架構也和上面所描述的類似,就如圖一中所示。

Lucene 支援多種形式的高階搜尋,我們在這一部分中會進行探討,然後我會使用 Lucene 的 API 來演示如何實現這些高階搜尋功能。

布林操作符

大多數的搜尋引擎都會提供布林操作符讓使用者可以組合查詢,典型的布林操作符有 AND, OR, NOT。Lucene 支援 5 種布林操作符,分別是 AND, OR, NOT, 加(+), 減(-)。接下來我會講述每個操作符的用法。

  • OR: 如果你要搜尋含有字元 A 或者 B 的文件,那麼就需要使用 OR 操作符。需要記住的是,如果你只是簡單的用空格將兩個關鍵詞分割開,其實在搜尋的時候搜尋引擎會自動在兩個關鍵詞之間加上 OR 操作符。例如,“Java OR Lucene” 和 “Java Lucene” 都是搜尋含有 Java 或者含有 Lucene 的文件。
  • AND: 如果你需要搜尋包含一個以上關鍵詞的文件,那麼就需要使用 AND 操作符。例如,“Java AND Lucene” 返回所有既包含 Java 又包含 Lucene 的文件。
  • NOT: Not 操作符使得包含緊跟在 NOT 後面的關鍵詞的文件不會被返回。例如,如果你想搜尋所有含有 Java 但不含有 Lucene 的文件,你可以使用查詢語句 “Java NOT Lucene”。但是你不能只對一個搜尋詞使用這個操作符,比如,查詢語句 “NOT Java” 不會返回任何結果。
  • 加號(+): 這個操作符的作用和 AND 差不多,但它只對緊跟著它的一個搜尋詞起作用。例如,如果你想搜尋一定包含 Java,但不一定包含 Lucene 的文件,就可以使用查詢語句“+Java Lucene”。
  • 減號(-): 這個操作符的功能和 NOT 一樣,查詢語句 “Java -Lucene” 返回所有包含 Java 但不包含 Lucene 的文件。

接下來我們看一下如何利用 Lucene 提供的 API 來實現布林查詢。清單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]);
   }
}

Lucene 支援域搜尋,你可以指定一次查詢是在哪些域(Field)上進行。例如,如果索引的文件包含兩個域,Title 和Content,你就可以使用查詢 “Title: Lucene AND Content: Java” 來返回所有在 Title 域上包含 Lucene 並且在 Content 域上包含 Java 的文件清單 2顯示瞭如何利用 Lucene 的 API 來實現域搜尋。

//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);
}

Lucene 支援兩種萬用字元:問號(?)和星號(*)。你可以使用問號(?)來進行單字元的萬用字元查詢,或者利用星號(*)進行多字元的萬用字元查詢。例如,如果你想搜尋 tiny 或者 tony,你就可以使用查詢語句 “t?ny”;如果你想查詢 Teach, Teacher 和 Teaching,你就可以使用查詢語句 “Teach*”。清單3顯示了萬用字元查詢的過程。

//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)。你可以在搜尋詞的尾部加上字元 ~ 來進行模糊查詢。例如,查詢語句 “think~” 返回所有包含和 think 類似的關鍵詞的文件。清單 4 顯示瞭如果利用 Lucene 的 API 進行模糊查詢的程式碼。

//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]);
   }
}

範圍搜尋匹配某個域上的值在一定範圍的文件。例如,查詢 “age:[18 TO 35]” 返回所有 age 域上的值在 18 到 35 之間的文件。清單5顯示了利用 Lucene 的 API 進行返回搜尋的過程。

//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 應用程式利用 Lucene 來檢索存放在檔案伺服器上的 HTML 文件。在開始之前,需要準備如下環境:

  1. Eclipse 整合開發環境
  2. Tomcat 5.0
  3. Lucene Library
  4. JDK 1.5

這個例子使用 Eclipse 進行 Web 應用程式的開發,最終這個 Web 應用程式跑在 Tomcat 5.0 上面。在準備好開發所必需的環境之後,我們接下來進行 Web 應用程式的開發。

  1. 在 Eclipse 裡面,選擇 File > New > Project,然後再彈出的視窗中選擇動態 Web 專案,如圖二所示。

圖二:建立動態Web專案
建立動態Web專案 
  1. 在建立好動態 Web 專案之後,你會看到建立好的專案的結構,如圖三所示,專案的名稱為 sample.dw.paper.lucene。

圖三:動態 Web 專案的結構
動態 Web 專案的結構 

在我們的設計中,把該系統分成如下四個子系統:

  1. 使用者介面: 這個子系統提供使用者介面使使用者可以向 Web 應用程式伺服器提交搜尋請求,然後搜尋結果通過使用者介面來顯示出來。我們用一個名為 search.jsp 的頁面來實現該子系統。
  2. 請求管理器: 這個子系統管理從客戶端傳送過來的搜尋請求並把搜尋請求分發到搜尋子系統中。最後搜尋結果從搜尋子系統返回並最終傳送到使用者介面子系統。我們使用一個 Servlet 來實現這個子系統。
  3. 搜尋子系統: 這個子系統負責在索引檔案上進行搜尋並把搜尋結構傳遞給請求管理器。我們使用 Lucene 提供的 API 來實現該子系統。
  4. 索引子系統: 這個子系統用來為 HTML 頁面來建立索引。我們使用 Lucene 的 API 以及 Lucene 提供的一個 HTML 解析器來建立該子系統。

圖4 顯示了我們設計的詳細資訊,我們將使用者介面子系統放到 webContent 目錄下面。你會看到一個名為 search.jsp 的頁面在這個資料夾裡面。請求管理子系統在包 sample.dw.paper.lucene.servlet 下面,類 SearchController 負責功能的實現。搜尋子系統放在包 sample.dw.paper.lucene.search 當中,它包含了兩個類,SearchManager 和 SearchResultBean,第一個類用來實現搜尋功能,第二個類用來描述搜尋結果的結構。索引子系統放在包 sample.dw.paper.lucene.index 當中。類IndexManager 負責為 HTML 檔案建立索引。該子系統利用包 sample.dw.paper.lucene.util 裡面的類 HTMLDocParser 提供的方法 getTitle 和 getContent 來對 HTML 頁面進行解析。


圖四:專案的架構設計
專案的架構設計 

在分析了系統的架構設計之後,我們接下來看系統實現的詳細資訊。

  1. 使用者介面: 這個子系統有一個名為 search.jsp 的 JSP 檔案來實現,這個 JSP 頁面包含兩個部分。第一部分提供了一個使用者介面去向 Web 應用程式伺服器提交搜尋請求,如圖5所示。注意到這裡的搜尋請求傳送到了一個名為 SearchController 的 Servlet 上面。Servlet 的名字和具體實現的類的對應關係在 web.xml 裡面指定。

圖5:向Web伺服器提交搜尋請求
向Web伺服器提交搜尋請求 

這個JSP的第二部分負責顯示搜尋結果給使用者,如圖6所示:


圖6:顯示搜尋結果
顯示搜尋結果 
  1. 請求管理器: 一個名為 SearchController 的 servlet 用來實現該子系統清單6給出了這個類的原始碼。

清單6:請求管理器的實現
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 方法從客戶端獲取搜尋詞並建立類 SearchManager 的一個例項,其中類 SearchManager 在搜尋子系統中進行了定義。然後,SearchManager 的方法 search 會被呼叫。最後搜尋結果被返回到客戶端。

  1. 搜尋子系統: 在這個子系統中,我們定義了兩個類:SearchManager 和 SearchResultBean。第一個類用來實現搜尋功能,第二個類是個JavaBean,用來描述搜尋結果的結構。清單7給出了類 SearchManager 的原始碼。

清單7:搜尋功能的實現
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中,注意到在這個類裡面有三個私有屬性。第一個是 searchWord,代表了來自客戶端的搜尋詞。第二個是indexManager,代表了在索引子系統中定義的類 IndexManager 的一個例項。第三個是 analyzer,代表了用來解析搜尋詞的解析器。現在我們把注意力放在方法 search 上面。這個方法首先檢查索引檔案是否已經存在,如果已經存在,那麼就在已經存在的索引上進行檢索,如果不存在,那麼首先呼叫類 IndexManager 提供的方法來建立索引,然後在新建立的索引上進行檢索。搜尋結果返回後,這個方法從搜尋結果中提取出需要的屬性併為每個搜尋結果生成類 SearchResultBean 的一個例項。最後這些 SearchResultBean 的例項被放到一個列表裡面並返回給請求管理器。

在類 SearchResultBean 中,含有兩個屬性,分別是 htmlPath 和 htmlTitle,以及這個兩個屬性的 get 和 set 方法。這也意味著我們的搜尋結果包含兩個屬性:htmlPath 和 htmlTitle,其中 htmlPath 代表了 HTML 檔案的路徑,htmlTitle 代表了 HTML 檔案的標題。

  1. 索引子系統: 類 IndexManager 用來實現這個子系統。清單8 給出了這個類的原始碼。

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;
    }

}

這個類包含兩個私有屬性,分別是 dataDir 和 indexDirdataDir 代表存放等待進行索引的 HTML 頁面的路徑,indexDir 代表了存放 Lucene 索引檔案的路徑。類 IndexManager 提供了三個方法,分別是 createIndexaddDocument 和ifIndexExist。如果索引不存在的話,你可以使用方法 createIndex 去建立一個新的索引,用方法 addDocument 去向一個索引上新增文件。在我們的場景中,一個文件就是一個 HTML 頁面。方法 addDocument 會呼叫由類 HTMLDocParser 提供的方法對 HTML 文件進行解析。你可以使用最後一個方法 ifIndexExist 來判斷 Lucene 的索引是否已經存在。

現在我們來看一下放在包 sample.dw.paper.lucene.util 裡面的類 HTMLDocParser。這個類用來從 HTML 檔案中提取出文字資訊。這個類包含三個方法,分別是 getContentgetTitle 和 getPath。第一個方法返回去除了 HTML 標記的文字內容,第二個方法返回 HTML 檔案的標題,最後一個方法返回 HTML 檔案的路徑清單9 給出了這個類的原始碼。


清單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;  
    }
}

現在我們可以在 Tomcat 5.0 上執行開發好的應用程式。

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

圖7:配置 Tomcat 5.0
配置 Tomcat 5.0 
  1. 在彈出的視窗中,選擇 Tomcat v5.0 Server 作為目標 Web 應用程式伺服器,然後點選 Next,如圖8 所示:

圖8:選擇 Tomcat 5.0
選擇 Tomcat 5.0 
  1. 現在需要指定用來執行 Web 應用程式的 Apache Tomcat 5.0 以及 JRE 的路徑。這裡你所選擇的 JRE 的版本必須和你用來編譯 Java 檔案的 JRE 的版本一致。配置好之後,點選 Finish。如 圖9 所示。

圖9:完成Tomcat 5.0的配置
完成Tomcat 5.0的配置 
  1. 配置好之後,Tomcat 會自動執行,並且會對 search.jsp 進行編譯並顯示給使用者。如 圖10所示。

圖10:使用者介面
使用者介面 
  1. 在輸入框中輸入關鍵詞 “information” 然後單擊 Search 按鈕。然後這個頁面上會顯示出搜尋結果來,如圖11 所示。

圖11:搜尋結果
搜尋結果 
  1. 單擊搜尋結果的第一個連結,頁面上就會顯示出所連結到的頁面的內容。如 圖12 所示.

圖12:詳細資訊
詳細資訊 

現在我們已經成功的完成了示例專案的開發,併成功的用Lucene實現了搜尋和索引功能。

總結

Lucene 提供了靈活的介面使我們更加方便的設計我們的 Web 搜尋應用程式。如果你想在你的應用程式中加入搜尋功能,那麼 Lucene 是一個很好的選擇。在設計你的下一個帶有搜尋功能的應用程式的時候可以考慮使用 Lucene 來提供搜尋功能。