周 登朋
(zhoudengpeng@yahoo.com.cn), 軟件工程師, 上海交通大學
2006 年 9 月 06 日?
在本篇文章中,你會學習到如何利用 Lucene 實現高級搜索功能以及如何利用 Lucene 來創建 Web 搜索應用程序。通過這些學習,你就可以利用 Lucene 來創建自己的搜索應用程序。
架構概覽
通常一個 Web 搜索引擎的架構分為前端和后端兩部分,就像圖一中所示。在前端流程中,用戶在搜索引擎提供的界面中輸入要搜索的關鍵詞,這里提到的用戶界面一般是一個帶有輸入框的 Web 頁面,然后應用程序將搜索的關鍵詞解析成搜索引擎可以理解的形式,并在索引文件上進行搜索操作。在排序后,搜索引擎返回搜索結果給用戶。在后端流程中,網絡爬蟲或者機器人從因特網上獲取 Web 頁面,然后索引子系統解析這些 Web 頁面并存入索引文件中。如果你想利用 Lucene 來創建一個 Web 搜索應用程序,那么它的架構也和上面所描述的類似,就如圖一中所示。
Figure 1. Web 搜索引擎架構
利用 Lucene 實現高級搜索
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 顯示了如果利用布爾操作符進行查詢的過程。
清單1:使用布爾操作符
//Test boolean operatorpublic 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)上進行。例如,如果索引的文檔包含兩個域,Title
和 Content
,你就可以使用查詢 “Title: Lucene AND Content: Java” 來返回所有在 Title 域上包含 Lucene 并且在 Content 域上包含 Java 的文檔。清單 2 顯示了如何利用 Lucene 的 API 來實現域搜索。
清單2:實現域搜索
//Test field searchpublic 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 支持兩種通配符:問號(?)和星號(*)。你可以使用問號(?)來進行單字符的通配符查詢,或者利用星號(*)進行多字符的通配符查詢。例如,如果你想搜索 tiny 或者 tony,你就可以使用查詢語句 “t?ny”;如果你想查詢 Teach, Teacher 和 Teaching,你就可以使用查詢語句 “Teach*”。清單3 顯示了通配符查詢的過程。
清單3:進行通配符查詢
//Test wildcard searchpublic 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 進行模糊查詢的代碼。
清單4:實現模糊查詢
//Test fuzzy searchpublic 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)
范圍搜索匹配某個域上的值在一定范圍的文檔。例如,查詢 “age:[18 TO 35]” 返回所有 age 域上的值在 18 到 35 之間的文檔。清單5顯示了利用 Lucene 的 API 進行返回搜索的過程。
清單5:測試范圍搜索
//Test range searchpublic 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
接下來我們開發一個 Web 應用程序利用 Lucene 來檢索存放在文件服務器上的 HTML 文檔。在開始之前,需要準備如下環境:
- Eclipse 集成開發環境
- Tomcat 5.0
- Lucene Library
- JDK 1.5
這個例子使用 Eclipse 進行 Web 應用程序的開發,最終這個 Web 應用程序跑在 Tomcat 5.0 上面。在準備好開發所必需的環境之后,我們接下來進行 Web 應用程序的開發。
1、創建一個動態 Web 項目
- 在 Eclipse 里面,選擇 File > New > Project,然后再彈出的窗口中選擇動態 Web 項目,如圖二所示。
圖二:創建動態Web項目
- 在創建好動態 Web 項目之后,你會看到創建好的項目的結構,如圖三所示,項目的名稱為 sample.dw.paper.lucene。
圖三:動態 Web 項目的結構
2. 設計 Web 項目的架構
在我們的設計中,把該系統分成如下四個子系統:
-
用戶接口: 這個子系統提供用戶界面使用戶可以向 Web 應用程序服務器提交搜索請求,然后搜索結果通過用戶接口來顯示出來。我們用一個名為 search.jsp 的頁面來實現該子系統。
-
請求管理器: 這個子系統管理從客戶端發送過來的搜索請求并把搜索請求分發到搜索子系統中。最后搜索結果從搜索子系統返回并最終發送到用戶接口子系統。我們使用一個 Servlet 來實現這個子系統。
-
搜索子系統: 這個子系統負責在索引文件上進行搜索并把搜索結構傳遞給請求管理器。我們使用 Lucene 提供的 API 來實現該子系統。
-
索引子系統: 這個子系統用來為 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 頁面進行解析。
圖四:項目的架構設計
3. 子系統的實現
在分析了系統的架構設計之后,我們接下來看系統實現的詳細信息。
-
用戶接口: 這個子系統有一個名為 search.jsp 的 JSP 文件來實現,這個 JSP 頁面包含兩個部分。第一部分提供了一個用戶接口去向 Web 應用程序服務器提交搜索請求,如圖5所示。注意到這里的搜索請求發送到了一個名為 SearchController 的 Servlet 上面。Servlet 的名字和具體實現的類的對應關系在 web.xml 里面指定。
圖5:向Web服務器提交搜索請求
這個JSP的第二部分負責顯示搜索結果給用戶,如圖6所示:
圖6:顯示搜索結果
-
請求管理器: 一個名為
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 會被調用。最后搜索結果被返回到客戶端。
-
搜索子系統: 在這個子系統中,我們定義了兩個類:
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 文件的標題。
-
索引子系統: 類
IndexManager
用來實現這個子系統。清單8 給出了這個類的源代碼。
清單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
和 indexDir
。dataDir
代表存放等待進行索引的 HTML 頁面的路徑,indexDir
代表了存放 Lucene 索引文件的路徑。類 IndexManager
提供了三個方法,分別是 createIndex
, addDocument
和 ifIndexExist
。如果索引不存在的話,你可以使用方法 createIndex
去創建一個新的索引,用方法 addDocument
去向一個索引上添加文檔。在我們的場景中,一個文檔就是一個 HTML 頁面。方法 addDocument
會調用由類 HTMLDocParser
提供的方法對 HTML 文檔進行解析。你可以使用最后一個方法 ifIndexExist
來判斷 Lucene 的索引是否已經存在。
現在我們來看一下放在包 sample.dw.paper.lucene.util
里面的類 HTMLDocParser
。這個類用來從 HTML 文件中提取出文本信息。這個類包含三個方法,分別是 getContent
,getTitle
和 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; }}
|
5.在 Tomcat 5.0 上運行應用程序
現在我們可以在 Tomcat 5.0 上運行開發好的應用程序。
- 右鍵單擊 search.jsp,然后選擇 Run as > Run on Server,如圖7所示。
圖7:配置 Tomcat 5.0
- 在彈出的窗口中,選擇 Tomcat v5.0 Server 作為目標 Web 應用程序服務器,然后點擊 Next,如圖8 所示:
圖8:選擇 Tomcat 5.0
- 現在需要指定用來運行 Web 應用程序的 Apache Tomcat 5.0 以及 JRE 的路徑。這里你所選擇的 JRE 的版本必須和你用來編譯 Java 文件的 JRE 的版本一致。配置好之后,點擊 Finish。如 圖9 所示。
圖9:完成Tomcat 5.0的配置
- 配置好之后,Tomcat 會自動運行,并且會對 search.jsp 進行編譯并顯示給用戶。如 圖10 所示。
圖10:用戶界面
- 在輸入框中輸入關鍵詞 “information” 然后單擊 Search 按鈕。然后這個頁面上會顯示出搜索結果來,如 圖11 所示。
圖11:搜索結果
- 單擊搜索結果的第一個鏈接,頁面上就會顯示出所鏈接到的頁面的內容。如 圖12 所示.
圖12:詳細信息
現在我們已經成功的完成了示例項目的開發,并成功的用Lucene實現了搜索和索引功能。你可以下載這個項目的源代碼。
總結
Lucene 提供了靈活的接口使我們更加方便的設計我們的 Web 搜索應用程序。如果你想在你的應用程序中加入搜索功能,那么 Lucene 是一個很好的選擇。在設計你的下一個帶有搜索功能的應用程序的時候可以考慮使用 Lucene 來提供搜索功能。
下載
描述 |
名字 |
大小 |
下載方法 |
Lucene Web 應用程序示例 |
wa-lucene2_source_code.zip |