級別: 中級
周 登朋 ( zhoudengpeng@yahoo.com.cn ), 軟件工程師, 上海交通大學
2006 年 9 月 06 日
Lucene 是基于 Java 的全文信息檢索包,它目前是 Apache Jakarta 家族下面的一個開源項目。在這篇文章中,我們首先來看如何利用 Lucene 實現高級搜索功能,然后學習如何利用 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 文檔。在開始之前,需要準備如下環境:
- Eclipse 集成開發環境
- Tomcat 5.0
- Lucene Library
- JDK 1.5
這個例子使用 Eclipse 進行 Web 應用程序的開發,最終這個 Web 應用程序跑在 Tomcat 5.0 上面。在準備好開發所必需的環境之后,我們接下來進行 Web 應用程序的開發。
- 在 Eclipse 里面,選擇 File > New > Project ,然后再彈出的窗口中選擇 動態 Web 項目 ,如 圖二 所示。
- 在創建好動態 Web 項目之后,你會看到創建好的項目的結構,如 圖三 所示,項目的名稱為 sample.dw.paper.lucene。
在我們的設計中,把該系統分成如下四個子系統:
- 用戶接口 : 這個子系統提供用戶界面使用戶可以向 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 頁面進行解析。
在分析了系統的架構設計之后,我們接下來看系統實現的詳細信息。
- 用戶接口 : 這個子系統有一個名為 search.jsp 的 JSP 文件來實現,這個 JSP 頁面包含兩個部分。第一部分提供了一個用戶接口去向 Web 應用程序服務器提交搜索請求,如 圖5 所示。注意到這里的搜索請求發送到了一個名為 SearchController 的 Servlet 上面。Servlet 的名字和具體實現的類的對應關系在 web.xml 里面指定。
這個JSP的第二部分負責顯示搜索結果給用戶,如 圖6 所示:
-
請求管理器
: 一個名為
SearchController
的 servlet 用來實現該子系統。 清單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
的源代碼。
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 給出了這個類的源代碼。
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
給出了這個類的源代碼。
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 上運行開發好的應用程序。
- 右鍵單擊 search.jsp ,然后選擇 Run as > Run on Server ,如 圖7 所示。
- 在彈出的窗口中,選擇 Tomcat v5.0 Server 作為目標 Web 應用程序服務器,然后點擊 Next ,如 圖8 所示:
- 現在需要指定用來運行 Web 應用程序的 Apache Tomcat 5.0 以及 JRE 的路徑。這里你所選擇的 JRE 的版本必須和你用來編譯 Java 文件的 JRE 的版本一致。配置好之后,點擊 Finish 。如 圖9 所示。
- 配置好之后,Tomcat 會自動運行,并且會對 search.jsp 進行編譯并顯示給用戶。如 圖10 所示。
- 在輸入框中輸入關鍵詞 “information” 然后單擊 Search 按鈕。然后這個頁面上會顯示出搜索結果來,如 圖11 所示。
- 單擊搜索結果的第一個鏈接,頁面上就會顯示出所鏈接到的頁面的內容。如 圖12 所示.
現在我們已經成功的完成了示例項目的開發,并成功的用Lucene實現了搜索和索引功能。你可以下載這個項目的源代碼( 下載 )。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
