仇 寅 (qiuyin04@software.nju.edu.cn)南京大學(xué)
2007 年 11 月 27 日
為應(yīng)用程序添加搜索能力經(jīng)常是一個(gè)常見(jiàn)的需求。本文介紹了一個(gè)框架,開(kāi)發(fā)者可以使用它以最小的付出實(shí)現(xiàn)搜索引擎功能,理想情況下只需要一個(gè)配置文件。該框架基于若干開(kāi)源的庫(kù)和工具,如 Apache Lucene,Spring 框架,cpdetector 等。它支持多種資源。其中兩個(gè)典型的例子是數(shù)據(jù)庫(kù)資源和文件系統(tǒng)資源。Indexer 對(duì)配置的資源進(jìn)行索引并傳輸?shù)街醒敕?wù)器,之后這些索引可以通過(guò) API 進(jìn)行搜索。Spring 風(fēng)格的配置文件允許清晰靈活的自定義和調(diào)整。核心 API 也提供了可擴(kuò)展的接口。
引言
為應(yīng)用程序添加搜索能力經(jīng)常是一個(gè)常見(jiàn)的需求。盡管已經(jīng)有若干程序庫(kù)提供了對(duì)搜索基礎(chǔ)設(shè)施的支持,然而對(duì)于很多人而言,使用它們從頭開(kāi)始建立一個(gè)搜索引擎將是一個(gè)付出不小而且可能乏味的過(guò)程。另一方面,很多的小型應(yīng)用對(duì)于搜索功能的需求和應(yīng)用場(chǎng)景具有很大的相似性。本文試圖以對(duì)多數(shù)小型應(yīng)用的適用性為出發(fā)點(diǎn),用 Java 語(yǔ)言構(gòu)建一個(gè)靈活的搜索引擎框架。使用這個(gè)框架,多數(shù)情形下可以以最小的付出建立起一個(gè)搜索引擎。最理想的情況下,甚至只需要一個(gè)配置文件。特殊的情形下,可以通過(guò)靈活地對(duì)框架進(jìn)行擴(kuò)展?jié)M足需求。當(dāng)然,如題所述,這都是借助開(kāi)源工具的力量。
基礎(chǔ)知識(shí)
Apache Lucene 是開(kāi)發(fā)搜索類(lèi)應(yīng)用程序時(shí)最常用的 Java 類(lèi)庫(kù),我們的框架也將基于它。為了下文更好的描述,我們需要先了解一些有關(guān) Lucene 和搜索的基礎(chǔ)知識(shí)。注意,本文不關(guān)注索引的文件格式、分詞技術(shù)等話(huà)題。
什么是搜索和索引
從用戶(hù)的角度來(lái)看,搜索的過(guò)程是通過(guò)關(guān)鍵字在某種資源中尋找特定的內(nèi)容的過(guò)程。而從計(jì)算機(jī)的角度來(lái)看,實(shí)現(xiàn)這個(gè)過(guò)程可以有兩種辦法。一是對(duì)所有資源逐個(gè)與關(guān)鍵字匹配,返回所有滿(mǎn)足匹配的內(nèi)容;二是如同字典一樣事先建立一個(gè)對(duì)應(yīng)表,把關(guān)鍵字與資源的內(nèi)容對(duì)應(yīng)起來(lái),搜索時(shí)直接查找這個(gè)表即可。顯而易見(jiàn),第二個(gè)辦法效率要高得多。建立這個(gè)對(duì)應(yīng)表事實(shí)上就是建立逆向索引(inverted index)的過(guò)程。
Lucene 基本概念
Lucene 是 Doug Cutting 用 Java 開(kāi)發(fā)的用于全文搜索的工具庫(kù)。在這里,我假設(shè)讀者對(duì)其已有基本的了解,我們只對(duì)一些重要的概念簡(jiǎn)要介紹。要深入了解可以參考 參考資源 中列出的相關(guān)文章和圖書(shū)。下面這些是 Lucene 里比較重要的類(lèi)。
- DE>DocumentDE>:索引包含多個(gè) DE>DocumentDE>。而每個(gè) DE>DocumentDE> 則包含多個(gè) DE>FieldDE> 對(duì)象。DE>DocumentDE> 可以是從數(shù)據(jù)庫(kù)表里取出的一堆數(shù)據(jù),可以是一個(gè)文件,也可以是一個(gè)網(wǎng)頁(yè)等。注意,它不等同于文件系統(tǒng)中的文件。
- DE>FieldDE>:一個(gè) DE>FieldDE> 有一個(gè)名稱(chēng),它對(duì)應(yīng) DE>DocumentDE>的一部分?jǐn)?shù)據(jù),表示文檔的內(nèi)容或者文檔的元數(shù)據(jù)(與下文中提到的資源元數(shù)據(jù)不是一個(gè)概念)。一個(gè) DE>FieldDE> 對(duì)象有兩個(gè)重要屬性:Store ( 可以有 YES, NO, COMPACT 三種取值 ) 和 Index ( 可以有 TOKENIZED, UN_TOKENIZED, NO, NO_NORMS 四種取值 )
- DE>QueryDE>:抽象了搜索時(shí)使用的語(yǔ)句。
- DE>IndexSearcherDE>:提供 DE>QueryDE> 對(duì)象給它,它利用已有的索引進(jìn)行搜索并返回搜索結(jié)果。
- DE>HitsDE>:一個(gè)容器,包含了指向一部分搜索結(jié)果的指針。
使用 Lucene 來(lái)進(jìn)行編制索引的過(guò)程大致為:將輸入的數(shù)據(jù)源統(tǒng)一為字符串或者文本流的形式,然后從數(shù)據(jù)源提取數(shù)據(jù),創(chuàng)建合適的 DE>FieldDE> 添加到對(duì)應(yīng)數(shù)據(jù)源的 DE>DocumentDE> 對(duì)象之中。
系統(tǒng)概覽
要建立一個(gè)通用的框架,必須對(duì)不同情況的共性進(jìn)行抽象。反映到設(shè)計(jì)需要注意兩點(diǎn)。一是要提供擴(kuò)展接口;二是要盡量降低模塊之間的耦合程度。我們的框架很簡(jiǎn)單地分為兩個(gè)模塊:索引模塊和搜索模塊。索引模塊在不同的機(jī)器上各自進(jìn)行對(duì)資源的索引,并把索引文件(事實(shí)上,下面我們會(huì)說(shuō)到,還有元數(shù)據(jù))統(tǒng)一傳輸?shù)酵粋€(gè)地方(可以是在遠(yuǎn)程服務(wù)器上,也可以是在本地)。搜索模塊則利用這些從多個(gè)索引模塊收集到的數(shù)據(jù)完成用戶(hù)的搜索請(qǐng)求。
圖 1 展現(xiàn)了整體的框架。可以看到,兩個(gè)模塊之間相對(duì)是獨(dú)立的,它們之間的關(guān)聯(lián)不是通過(guò)代碼,而是通過(guò)索引和元數(shù)據(jù)。在下文中,我們將會(huì)詳細(xì)介紹如何基于開(kāi)源工具設(shè)計(jì)和實(shí)現(xiàn)這兩個(gè)模塊。
圖 1. 系統(tǒng)架構(gòu)圖
建立索引
可以進(jìn)行索引的對(duì)象有很多,如文件、網(wǎng)頁(yè)、RSS Feed 等。在我們的框架中,我們定義可以進(jìn)行索引的一類(lèi)對(duì)象為資源。從實(shí)現(xiàn)細(xì)節(jié)上來(lái)說(shuō),從一個(gè)資源中可以提取出多個(gè) DE>DocumentDE> 對(duì)象。文件系統(tǒng)資源和數(shù)據(jù)庫(kù)結(jié)果集資源都是資源的代表性例子。
前面提到,從資源中收集到的索引被統(tǒng)一傳送到同一個(gè)地方,以被搜索模塊所用。顯然除了索引之外,搜索模塊需要對(duì)資源有更多的了解,如資源的名稱(chēng)、搜索該資源后搜索結(jié)果的呈現(xiàn)格式等。這些額外的附加信息稱(chēng)為資源的元數(shù)據(jù)。元數(shù)據(jù)和索引數(shù)據(jù)一同被收集起來(lái),放置到某個(gè)特定的位置。
簡(jiǎn)要地介紹過(guò)資源的概念之后,我們首先為其定義一個(gè) DE>ResourceDE> 接口。這個(gè)接口的聲明如下。
清單 1. Resource 接口
public interface Resource {
// RequestProcessor 對(duì)象被動(dòng)地從資源中提取 Document,并返回提取的數(shù)量
public int extractDocuments(ResourceProcessor processor);
// 添加的 DocumentListener 將在每一個(gè) Document 對(duì)象被提取出時(shí)被調(diào)用
public void addDocumentListener(DocumentListener l);
// 返回資源的元數(shù)據(jù)
public ResourceMetaData getMetaData();
}
|
其中元數(shù)據(jù)包含的字段見(jiàn)下表。在下文中,我們還會(huì)對(duì)元數(shù)據(jù)的用途做更多的介紹。
表 1. 資源元數(shù)據(jù)包含的字段
而 DE>DocumentListenerDE> 的代碼如下。
清單 2. DocumentListener 接口
public interface DocumentListener extends EventListener {
public void documentExtracted(Document doc);
}
|
為了讓索引模塊能夠知道所有需要被索引的資源,我們?cè)谶@里使用 Spring 風(fēng)格的 XML 文件配置索引模塊中的所有組件,尤其是所有資源。您可以在 下載部分 查看一個(gè)示例配置文件。
 |
為什么選擇使用 Spring 風(fēng)格的配置文件?
這主要有兩個(gè)好處:
- 僅依賴(lài)于 Spring Core 和 Spring Beans 便免去了定義配置機(jī)制和解析配置文件的負(fù)擔(dān);
- Spring 的 IoC 機(jī)制降低了框架的耦合性,并使擴(kuò)展框架變得簡(jiǎn)單;
|
|
基于以上內(nèi)容,我們可以大致描述出索引模塊工作的過(guò)程:
- 首先在 XML 配置的 bean 中找出所有 DE>ResourceDE> 對(duì)象;
- 對(duì)每一個(gè)調(diào)用其 DE>extractDocuments()DE> 方法,這一步除了完成對(duì)資源的索引外,還會(huì)在每次提取出一個(gè) DE>DocumentDE> 對(duì)象之后,通知注冊(cè)在該資源上的所有 DE>DocumentListenerDE>;
- 接著處理資源的元數(shù)據(jù)(DE>getMetaData()DE> 的返回值);
- 將緩存里的數(shù)據(jù)寫(xiě)入到本地磁盤(pán)或者傳送給遠(yuǎn)程服務(wù)器;
在這個(gè)過(guò)程中,有兩個(gè)地方值得注意。
第一,對(duì)資源可以注冊(cè) DE>DocumentListenerDE> 使得我們可以在運(yùn)行時(shí)刻對(duì)索引過(guò)程有更為動(dòng)態(tài)的控制。舉一個(gè)簡(jiǎn)單例子,對(duì)某個(gè)文章發(fā)布站點(diǎn)的文章進(jìn)行索引時(shí),一個(gè)很正常的要求便是發(fā)布時(shí)間更靠近當(dāng)前時(shí)間的文章需要在搜索結(jié)果中排在靠前的位置。每篇文章顯然對(duì)應(yīng)一個(gè) DE>DocumentDE> 對(duì)象,在 Lucene 中我們可以通過(guò)設(shè)置 DE>DocumentDE> 的 DE>boostDE> 值來(lái)對(duì)其進(jìn)行加權(quán)。假設(shè)其中文章發(fā)布時(shí)間的 DE>FieldDE> 的名稱(chēng)為 DE>PUB_TIMEDE>,那么我們可以為資源注冊(cè)一個(gè) DE>DocumentListenerDE>,當(dāng)它被通知時(shí),則檢測(cè) DE>PUB_TIMEDE> 的值,根據(jù)距離當(dāng)前時(shí)間的遠(yuǎn)近進(jìn)行加權(quán)。
第二點(diǎn)很顯然,在這個(gè)過(guò)程中,DE>extractDocuments()DE> 方法的實(shí)現(xiàn)依不同類(lèi)型的資源而各異。下面我們主要討論兩種類(lèi)型的資源:文件系統(tǒng)資源和數(shù)據(jù)庫(kù)結(jié)果集資源。這兩個(gè)類(lèi)都實(shí)現(xiàn)了上面的 DE>接口DE>。
文件系統(tǒng)資源
對(duì)文件系統(tǒng)資源的索引通常從一個(gè)基目錄開(kāi)始,遞歸處理每個(gè)需要進(jìn)行索引的文件。該資源有一個(gè)字符串?dāng)?shù)組類(lèi)型的 DE>excludedFilesDE> 屬性,表示在處理文件時(shí)需要排除的文件絕對(duì)路徑的正則表達(dá)式。在遞歸遍歷文件系統(tǒng)樹(shù)的同時(shí),絕對(duì)路徑匹配 DE>excludedFilesDE> 中任意一項(xiàng)的文件將不會(huì)被處理。這主要是考慮到一般我們只需要對(duì)一部分文件夾(比如排除可能存在的備份目錄)中的一部分文件(如 doc, ppt 文件等)進(jìn)行索引。
除了所有文件共有的文件名、文件路徑、文件大小和修改時(shí)間等 Field,不同類(lèi)型的文件需要有不同的處理方法。為了保留靈活性,我們使用 Strategy 模式封裝對(duì)不同類(lèi)型文件的處理方式。為此我們抽象出一個(gè) DE>DocumentBuilderDE> 的接口,該接口僅定義了一個(gè)方法如下:
清單 3. DocumentBuilder 接口
public interface DocumentBuilder {
Document buildDocument(InputStream is);
}
|
 |
什么是 Strategy 模式?
根據(jù) Design patterns: Elements of reusable object orientated software 一書(shū):Strategy 模式“定義一系列的算法,把它們分別封裝起來(lái),并且使它們相互可以替換。這個(gè)模式使得算法可以獨(dú)立于使用它的客戶(hù)而變化。”
|
|
不同的 DE>DocumentBuilderDE>(Strategy) 用于從一個(gè)輸入流中讀取數(shù)據(jù),處理不同類(lèi)型的文件。對(duì)于常見(jiàn)的文件格式來(lái)說(shuō),都有合適的開(kāi)源工具幫助進(jìn)行解析。在下表中我們列舉一些常見(jiàn)文件類(lèi)型的解析辦法。
文件類(lèi)型 |
常用擴(kuò)展名 |
可以使用的解析辦法 |
純文本文檔 |
txt |
無(wú)需類(lèi)庫(kù)解析 |
RTF 文檔 |
rtf |
使用 DE>javax.swing.text.rtf.RTFEditorKitDE> 類(lèi) |
Word 文檔(非 OOXML 格式) |
doc |
Apache POI (可配合使用 POI Scratchpad) |
PowerPoint 演示文稿(非 OOXML 格式) |
xls |
Apache POI (可配合使用 POI Scratchpad) |
PDF 文檔 |
pdf |
PDFBox(可能中文支持欠佳) |
HTML 文檔 |
htm, html |
JTidy, Cobra |
這里以 Word 文件為例,給出一個(gè)簡(jiǎn)單的參考實(shí)現(xiàn)。
清單 4. 解析純文本內(nèi)容的實(shí)現(xiàn)
// WordDocument 是 Apache POI Scratchpad 中的一個(gè)類(lèi)
Document buildDocument(InputStream is) {
String bodyText = null;
try {
WordDocument wordDoc = new WordDocument(is);
StringWriter sw = new StringWriter();
wordDoc.writeAllText(sw);
sw.close();
bodyText = sw.toString();
} catch (Exception e) {
throw new DocumentHandlerException("Cannot extract text from a Word document", e);
}
if ((bodyText != null) && (bodyText.trim().length() > 0)) {
Document doc = new Document();
doc.add(new Field("body", bodyText, Field.Store.YES, Field.Index.TOKENIZED));
return doc;
}
return null;
}
|
那么如何選擇合適的 Strategy 來(lái)處理文件呢?UNIX 系統(tǒng)下的 file(1) 工具提供了從 magicnumber 獲取文件類(lèi)型的功能,我們可以使用 DE>Runtime.exec()DE> 方法調(diào)用這一命令。但這需要在有 file(1) 命令的情況下,而且并不能識(shí)別出所有文件類(lèi)型。在一般的情況下我們可以簡(jiǎn)單地根據(jù)擴(kuò)展名來(lái)使用合適的類(lèi)處理文件。擴(kuò)展名和類(lèi)的映射關(guān)系寫(xiě)在 properties 文件中。當(dāng)需要添加對(duì)新的文件類(lèi)型的支持時(shí),我們只需添加一個(gè)新的實(shí)現(xiàn) DE>DocumentBuilderDE> 接口的類(lèi),并在映射文件中添加一個(gè)映射關(guān)系即可。
數(shù)據(jù)庫(kù)結(jié)果集資源
大多數(shù)應(yīng)用使用數(shù)據(jù)庫(kù)作為永久存儲(chǔ),對(duì)數(shù)據(jù)庫(kù)查詢(xún)結(jié)果集索引是一個(gè)常見(jiàn)需求。
生成一個(gè)數(shù)據(jù)庫(kù)結(jié)果集資源的實(shí)例需要先提供一個(gè)查詢(xún)語(yǔ)句,然后執(zhí)行查詢(xún),得到一個(gè)結(jié)果集。這個(gè)結(jié)果集中的內(nèi)容便是我們需要進(jìn)行索引的對(duì)象。DE>extractDocumentsDE> 的實(shí)現(xiàn)便是為結(jié)果集中的每一行創(chuàng)建一個(gè) DE>DocumentDE> 對(duì)象。和文件系統(tǒng)資源不同的是,數(shù)據(jù)庫(kù)資源需要放入 DE>DocumentDE> 中的 DE>FieldDE> 一般都存在在查詢(xún)結(jié)果集之中。比如一個(gè)簡(jiǎn)單的文章發(fā)布站點(diǎn),對(duì)其后臺(tái)數(shù)據(jù)庫(kù)執(zhí)行查詢(xún) DE>SELECT ID, TITLE, CONTENT FROM ARTICLEDE> 返回一個(gè)有三列的結(jié)果集。對(duì)結(jié)果集的每一行都會(huì)被提取出一個(gè) DE>DocumentDE> 對(duì)象,其中包含三個(gè) DE>FieldDE>,分別對(duì)應(yīng)這三列。
然而不同 DE>FieldDE> 的類(lèi)型是不同的。比如 DE>IDDE> 字段一般對(duì)應(yīng) DE>Store.YESDE> 和 DE>Index.NODE> 的 DE>FieldDE>;而 DE>TITLEDE> 字段則一般對(duì)應(yīng) DE>Store.YESDE> 和 DE>Index.TOKENIZEDDE> 的 DE>FieldDE>。為了解決這個(gè)問(wèn)題,我們?cè)跀?shù)據(jù)庫(kù)結(jié)果集資源的實(shí)現(xiàn)中提供一個(gè)類(lèi)型為 DE>PropertiesDE> 的 DE>fieldTypeMappingsDE> 屬性,用于設(shè)置數(shù)據(jù)庫(kù)字段所對(duì)應(yīng)的 DE>FieldDE> 的類(lèi)型。對(duì)于前面的情況來(lái)說(shuō),這個(gè)屬性可能會(huì)被配置成類(lèi)似這樣的形式:
ID = YES, NO
TITLE = YES, TOKENIZED
CONTENT = NO, TOKENIZED
|
配合這個(gè)映射,我們便可以生成合適類(lèi)型的 DE>FieldDE>,完成對(duì)結(jié)果集索引的工作。
收集索引
完成對(duì)資源的索引之后,還需要讓索引為搜索模塊所用。前面我們已經(jīng)說(shuō)過(guò)這里介紹的框架主要用于小型應(yīng)用,考慮到復(fù)雜性,我們采取簡(jiǎn)單地將分布在各個(gè)機(jī)器上的索引匯總到一個(gè)地方的策略。
匯總索引的傳輸方式可以有很多方案,比如使用 FTP、HTTP、rsync 等。甚至索引模塊和搜索模塊可以位于同一臺(tái)機(jī)器上,這種情況下只需要將索引進(jìn)行本地拷貝即可。同前面類(lèi)似,我們定義一個(gè) DE>TransporterDE> 接口。
清單 5. Transporter 接口
public interface Transporter {
public void transport(File file);
}
|
以 FTP 方式傳輸為例,我們使用 Commons Net 完成傳輸?shù)牟僮鳌?/p>
public void transport(File file) throws TransportException {
FTPClient client = new FTPClient();
client.connect(host);
client.login(username, password);
client.changeWorkingDirectory(remotePath);
transportRecursive(client, file);
client.disconnect();
}
public void transportRecursive(FTPClient client, File file) {
if (file.isFile() && file.canRead()) {
client.storeFile(file.getName(), new FileInputStream(file));
} else if (file.isDirectory()) {
client.makeDirectory(file.getName());
client.changeWorkingDirectory(file.getName());
File[] fileList = file.listFiles();
for (File f : fileList) {
transportRecursive(client, f);
}
}
}
|
對(duì)其他傳輸方案也有各自的方案進(jìn)行處理,具體使用哪個(gè) DE>TransporterDE> 的實(shí)現(xiàn)被配置在 Spring 風(fēng)格的索引模塊配置文件中。傳輸?shù)姆绞绞庆`活的。比如當(dāng)需要強(qiáng)調(diào)安全性時(shí),我們可以換用基于 SSL 的 FTP 進(jìn)行傳輸。所需要做的只是開(kāi)發(fā)一個(gè)使用 FTP over SSL 的 DE>TransporterDE> 實(shí)現(xiàn),并在配置文件中更改 DE>TransporterDE> 的實(shí)現(xiàn)即可。
進(jìn)行搜索
在做了這么多之后,我們開(kāi)始接觸和用戶(hù)關(guān)聯(lián)最為緊密的搜索模塊。注意,我們的框架不包括一個(gè)搜索的 Web 前端界面。但是類(lèi)似這樣的界面可以在搜索模塊的基礎(chǔ)上方便地開(kāi)發(fā)出來(lái)。基于已經(jīng)收集好的索引進(jìn)行搜索是個(gè)很簡(jiǎn)單的過(guò)程。Lucene 已經(jīng)提供了功能強(qiáng)大的 DE>IndexSearcherDE> 及其子類(lèi)。在這個(gè)部分,我們不會(huì)再介紹如何使用這些類(lèi),而是關(guān)注在前文提到過(guò)的資源元數(shù)據(jù)上。元數(shù)據(jù)從各個(gè)資源所在的文件夾中讀取得到,它在搜索模塊中扮演重要的角色。
構(gòu)建一個(gè)查詢(xún)
對(duì)不同資源進(jìn)行搜索的查詢(xún)方法并不一樣。例如搜索一個(gè)論壇里的所有留言時(shí),我們關(guān)注的一般是留言的標(biāo)題、作者和內(nèi)容;而當(dāng)搜索一個(gè) FTP 站點(diǎn)時(shí),我們更多關(guān)注的是文件名和文件內(nèi)容。另一方面,我們有時(shí)可能會(huì)使用一個(gè)查詢(xún)?nèi)ニ阉鞫鄠€(gè)資源的結(jié)果。這正是之前我們?cè)谇懊嫠岬降脑獢?shù)據(jù)中 DE>searchableFieldsDE> 和 DE>resourceNameDE> 屬性的作用。前者指出一個(gè)資源中哪些字段是參與搜索的;后者則用于在搜索時(shí)確定使用哪個(gè)或者哪些索引。從技術(shù)細(xì)節(jié)來(lái)說(shuō),只有有了這些信息,我們才可以構(gòu)造出可用的 DE>QueryDE> 對(duì)象。
呈現(xiàn)搜索結(jié)果
當(dāng)從 DE>IndexSearcherDE> 對(duì)象得到搜索結(jié)果(DE>HitsDE>)之后,當(dāng)然我們可以直接從中獲取需要的值,再格式化予以輸出。但一來(lái)格式化輸出搜索結(jié)果(尤其在 Web 應(yīng)用中)是個(gè)很常見(jiàn)的需求,可能會(huì)經(jīng)常變更;二來(lái)結(jié)果的呈現(xiàn)格式應(yīng)該是由分散的資源各自定義,而不是交由搜索模塊來(lái)定義。基于上面兩個(gè)原因,我們的框架將使用在資源收集端配置結(jié)果輸出格式的方式。這個(gè)格式由資源元數(shù)據(jù)中的 DE>hitTextPatternDE> 屬性定義。該屬性是一個(gè)字符串類(lèi)型的值,支持兩種語(yǔ)法
- 形如 DE>${field_name}DE> 的子字符串都會(huì)被動(dòng)態(tài)替換成查詢(xún)結(jié)果中各個(gè) DE>DocumentDE> 內(nèi) DE>FieldDE> 的值。
- 形如 DE>$function(...) DE>的被解釋為函數(shù),括號(hào)內(nèi)以逗號(hào)隔開(kāi)的符號(hào)都被解釋成參數(shù),函數(shù)可以嵌套。
例如搜索“具體”返回的搜索結(jié)果中包含一個(gè) DE>DocumentDE> 對(duì)象,其 DE>FieldDE> 如下表:
Field 名稱(chēng) |
Field 內(nèi)容 |
url |
http://example.org/article/1.html |
title |
示例標(biāo)題 |
content |
這里是具體的內(nèi)容。 |
那么如果 DE>hitTextPattenDE> 被設(shè)置為“DE><a href="${url}">${title}</a><br/>$highlight(${content}, 5, "<b>", "</b>")DE>”,返回的結(jié)果經(jīng)瀏覽器解釋后可能的顯示結(jié)果如下(這只是個(gè)演示鏈接,請(qǐng)不要點(diǎn)擊):
示例標(biāo)題
這里是具體...
上面提到的 DE>$highlight()DE> 函數(shù)用于在搜索結(jié)果中取得最匹配的一段文本,并高亮顯示搜索時(shí)使用的短語(yǔ),其第一個(gè)參數(shù)是高亮顯示的文本,第二個(gè)參數(shù)是顯示的文本長(zhǎng)度,第三和第四個(gè)參數(shù)是高亮文本時(shí)使用的前綴和后綴。
可以使用正則表達(dá)式和文本解析來(lái)實(shí)現(xiàn)前面所提到的語(yǔ)法。我們也可以使用 JavaCC 定義 DE>hitTextPatternDE> 的文法,進(jìn)而生成詞法分析器和語(yǔ)法解析器。這是更為系統(tǒng)并且相對(duì)而言不易出錯(cuò)的方法。對(duì) JavaCC 的介紹不是本文的重點(diǎn),您可以在下面的 閱讀資源 中找到學(xué)習(xí)資料。
相關(guān)產(chǎn)品
下面列出的是一些與我們所提出的框架所相關(guān)或者類(lèi)似的產(chǎn)品,您可以在 學(xué)習(xí)資料 中更多地了解他們。
IBM®OmniFind?Family
OmniFind 是 IBM 公司推出的企業(yè)級(jí)搜索解決方案。基于 UIMA (Unstructured Information Management Architecture) 技術(shù),它提供了強(qiáng)大的索引和獲取信息功能,支持巨大數(shù)量、多種類(lèi)型的文檔資源(無(wú)論是結(jié)構(gòu)化還是非結(jié)構(gòu)化),并為 Lotus®Domino®和 WebSphere®Portal 專(zhuān)門(mén)進(jìn)行了優(yōu)化。
Apache Solr
Solr 是 Apache 的一個(gè)企業(yè)級(jí)的全文檢索項(xiàng)目,實(shí)現(xiàn)了一個(gè)基于 HTTP 的搜索服務(wù)器,支持多種資源和 Web 界面管理,它同樣建立在 Lucene 之上,并對(duì) Lucene 做了很多擴(kuò)展,例如支持動(dòng)態(tài)字段及唯一鍵,對(duì)查詢(xún)結(jié)果進(jìn)行動(dòng)態(tài)分組和過(guò)濾等。
Google SiteSearch
使用 Google 的站點(diǎn)搜索功能可以方便而快捷地建立一個(gè)站內(nèi)搜索引擎。但是 Google 的站點(diǎn)搜索基于 Google 的網(wǎng)絡(luò)爬蟲(chóng),所以無(wú)法訪(fǎng)問(wèn)受保護(hù)的站點(diǎn)內(nèi)容或者 Intranet 上的資源。另外,Google 所支持的資源類(lèi)型也是有限的,我們無(wú)法對(duì)其進(jìn)行擴(kuò)展。
SearchBlox?
SearchBlox 是一個(gè)商業(yè)的搜索引擎構(gòu)建框架。它本身是一個(gè) J2EE 組件,和我們的框架類(lèi)似,也支持對(duì)網(wǎng)頁(yè)和文件系統(tǒng)等資源進(jìn)行索引,進(jìn)而進(jìn)行搜索。
還需考慮的問(wèn)題
本文介紹的思想試圖利用開(kāi)源的工具解決中小型應(yīng)用中的常見(jiàn)問(wèn)題。當(dāng)然,作為一個(gè)框架,它還有很多不足,下面列舉出一些可以進(jìn)行改進(jìn)的地方。
性能考慮
當(dāng)需要進(jìn)行索引的資源數(shù)目不多時(shí),隔一定的時(shí)間進(jìn)行一次完全索引不會(huì)占用很長(zhǎng)時(shí)間。使用一臺(tái) 2G 內(nèi)存,Xeon 2.66G 處理器的服務(wù)器進(jìn)行實(shí)際測(cè)試,發(fā)現(xiàn)對(duì)數(shù)據(jù)庫(kù)資源的索引占用的時(shí)間很少,一千多條記錄花費(fèi)的時(shí)間在 1 秒到 2 秒之內(nèi)。而對(duì) 1400 多個(gè)文件進(jìn)行索引耗時(shí)大約十幾秒。但在大型應(yīng)用中,資源的容量是巨大的,如果每次都進(jìn)行完整的索引,耗費(fèi)的時(shí)間會(huì)很驚人。我們可以通過(guò)跳過(guò)已經(jīng)索引的資源內(nèi)容,刪除已不存在的資源內(nèi)容的索引,并進(jìn)行增量索引來(lái)解決這個(gè)問(wèn)題。這可能會(huì)涉及文件校驗(yàn)和索引刪除等。
另一方面,框架可以提供查詢(xún)緩存來(lái)提高查詢(xún)效率。框架可以在內(nèi)存中建立一級(jí)緩存,并使用如 OSCache 或 EHCache 實(shí)現(xiàn)磁盤(pán)上的二級(jí)緩存。當(dāng)索引的內(nèi)容變化不頻繁時(shí),使用查詢(xún)緩存更會(huì)明顯地提高查詢(xún)速度、降低資源消耗。
分布式索引
我們的框架可以將索引分布在多臺(tái)機(jī)器上。搜索資源時(shí),查詢(xún)被 flood 到各個(gè)機(jī)器上從而獲得搜索結(jié)果。這樣可以免去傳輸索引到某一臺(tái)中央服務(wù)器的過(guò)程。當(dāng)然也可以基于實(shí)現(xiàn)了分布式哈希表 (DHT)的結(jié)構(gòu)化 P2P 網(wǎng)絡(luò),配合索引復(fù)制 (Replication),使得應(yīng)用程序更為安全,可靠,有伸縮性。在 閱讀資料 中給出了 一篇關(guān)于構(gòu)建分布式環(huán)境下全文搜索的可行性的論文。
安全性
目前我們的框架并沒(méi)有涉及到安全性。除了依賴(lài)資源本身的訪(fǎng)問(wèn)控制(如受保護(hù)的網(wǎng)頁(yè)和文件系統(tǒng)等)之外,我們還可以從兩方面增強(qiáng)框架本身的安全性:
- 考慮到一個(gè)組織的搜索功能對(duì)不同用戶(hù)的權(quán)限設(shè)置不一定一樣,可以支持對(duì)用戶(hù)角色的定義,實(shí)行對(duì)搜索模塊的訪(fǎng)問(wèn)控制。
- 在資源索引模塊中實(shí)現(xiàn)一種機(jī)制,讓資源可以限制自己暴露的內(nèi)容,從而縮小索引模塊的索引范圍。這可以類(lèi)比 robots 文件可以規(guī)定搜索引擎爬蟲(chóng)的行為。
總結(jié)
通過(guò)上文的介紹,我們認(rèn)識(shí)了一個(gè)可擴(kuò)展的框架,由索引模塊和搜索模塊兩部分組成。它可以靈活地適應(yīng)不同的應(yīng)用場(chǎng)景。如果需要更獨(dú)特的需求,框架本身預(yù)留了可以擴(kuò)展的接口,我們可以通過(guò)實(shí)現(xiàn)這些接口完成功能的定制。更重要的是這一切都是建立在開(kāi)源軟件的基礎(chǔ)之上。希望本文能為您揭示開(kāi)源的力量,體驗(yàn)用開(kāi)源工具組裝您自己的解決方案所帶來(lái)的莫大快樂(lè)。