如何用Java开源工具建立搜索引擎
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
使用 Java 开源工具建立一个灵活的搜索引擎
揭示开源的力量
为应用程序添加搜索能力经常是一个常见的需求。本文介绍了一个框架,开发者可以使用它以最小的付出实现搜索引擎功能,理想情况下只需要一个配置文件。该框架基于若干开源的库和工具,如Apache Lucene,Spring 框架,cpdetector 等。它支持多种资源。
其中两个典型的例子是数据库资源和文件系统资源。Indexer 对配置的资源进行索引并传输到中央服务器,之后这些索引可以通过API 进行搜索。Spring 风格的配置文件允许清晰灵活的自定义和调整。核心API 也提供了可扩展的接口。
引言
为应用程序添加搜索能力经常是一个常见的需求。尽管已经有若干程序库提供了对搜索基础设施的支持,然而对于很多人而言,使用它们从头开始建立一个搜索引擎将是一个付出不小而且可能乏味的过程。另一方面,很多的小型应用对于搜索功能的需求和应用场景具有很大的相似性。本文试图以对多数小型应用的适用性为出发点,用Java 语言构建一个灵活的搜索引擎框架。使用这个框架,多数情形下可以以最小的付出建立起一个搜索引擎。最理想的情况下,甚至只需要一个配置文件。特殊的情形下,可以通过灵活地对框架进行扩展满足需求。当然,如题所述,这都是借助开源工具的力量。
基础知识
Apache Lucene 是开发搜索类应用程序时最常用的Java 类库,我们的框架也将基于它。为了下文更好的描述,我们需要先了解一些有关Lucene 和搜索的基础知识。注意,本文不关注索引的文件格式、分词技术等话题。
什么是搜索和索引
从用户的角度来看,搜索的过程是通过关键字在某种资源中寻找特定的内容的过程。而从计算机的角度来看,实现这个过程可以有两种办法。一是对所有资源逐个与关键字匹配,返回所有满足匹配的内容;二是如同字典一样事先建立一个对应表,把关键字与资源的内容对应起来,搜索时直接查找这个表即可。显而易见,第二个办法效率要高得多。建立这个对应表事实上就是建立逆向索引(inverted index)的过程。
Lucene 基本概念
Lucene 是Doug Cutting 用Java 开发的用于全文搜索的工具库。在这里,我假设读者对其已有基本的了解,我们只对一些重要的概念简要介绍。要深入了解可以参考参考资源中列出的相关文章和图书。下面这些是Lucene 里比较重要的类。
Document:索引包含多个Document。而每个Document则包含多个Field对象。Document 可以是从数据库表里取出的一堆数据,可以是一个文件,也可以是一个网页等。注意,它不等同于文件系统中的文件。
Field:一个Field有一个名称,它对应Document的一部分数据,表示文档的内容或者文档的元数据(与下文中提到的资源元数据不是一个概念)。一个Field对象有两个重要属性:Store ( 可以有YES, NO, COMPACT 三种取值) 和Index ( 可以有TOKENIZED, UN_TOKENIZED, NO, NO_NORMS 四种取值)
Query:抽象了搜索时使用的语句。
IndexSearcher:提供Query对象给它,它利用已有的索引进行搜索并返回搜索结果。Hits:一个容器,包含了指向一部分搜索结果的指针。
使用Lucene 来进行编制索引的过程大致为:将输入的数据源统一为字符串或者文本流的形式,然后从数据源提取数据,创建合适的Field添加到对应数据源的Document对象之中。
系统概览
要建立一个通用的框架,必须对不同情况的共性进行抽象。反映到设计需要注意两点。一是要提供扩展接口;二是要尽量降低模块之间的耦合程度。我们的框架很简单地分为两个模块:索引模块和搜索模块。索引模块在不同的机器上各自进行对资源的索引,并把索引文件(事实上,下面我们会说到,还有元数据)统一传输到同一个地方(可以是在远程服务器上,也可以是在本地)。搜索模块则利用这些从多个索引模块收集到的数据完成用户的搜索请求。图1展现了整体的框架。可以看到,两个模块之间相对是独立的,它们之间的关联不是通过代码,而是通过索引和元数据。在下文中,我们将会详细介绍如何基于开源工具设计和实现这两个模块。
图1. 系统架构图
图1. 系统架构图
建立索引
可以进行索引的对象有很多,如文件、网页、RSS Feed 等。在我们的框架中,我们定义可以进行索引的一类对象为资源。从实现细节上来说,从一个资源中可以提取出多个Document对象。文件系统资源和数据库结果集资源都是资源的代表性例子。
前面提到,从资源中收集到的索引被统一传送到同一个地方,以被搜索模块所用。显然除了索引之外,搜索模块需要对资源有更多的了解,如资源的名称、搜索该资源后搜索结果的呈现格式等。这些额外的附加信息称为资源的元数据。元数据和索引数据一同被收集起来,放置到某个特定的位置。简要地介绍过资源的概念之后,我们首先为其定义一个Resource 接口。这个接口的声明如下。
清单1. Resource 接口
public interface Resource {
// RequestProcessor 对象被动地从资源中提取Document,并返回提取的数量
public int extractDocuments(ResourceProcessor processor);
// 添加的DocumentListener 将在每一个Document 对象被提取出时被调用
public void addDocumentListener(DocumentListener l);
// 返回资源的元数据
public ResourceMetaData getMetaData();
}
其中元数据包含的字段见下表。在下文中,我们还会对元数据的用途做更多的介绍。
表1. 资源元数据包含的字段
属性类型含义
String 资源的唯一名称
resourceDescription String 资源的介绍性文字
String 当文档被搜索到时,这个pattern 规定了结果显示的格式
String[] 可以被搜索的字段名称
而DocumentListener的代码如下。
清单2. DocumentListener 接口
public interface DocumentListener extends EventListener {
public void documentExtracted(Document doc);
}
为了让索引模块能够知道所有需要被索引的资源,我们在这里使用Spring风格的XML 文件配置索引模块中的所有组件,尤其是所有资源。您可以在下载部分查看一个示例配置文件。
为什么选择使用Spring 风格的配置文件?
这主要有两个好处:
仅依赖于Spring Core 和Spring Beans 便免去了定义配置机制和解析配置文件的负担;Spring 的IoC 机制降低了框架的耦合性,并使扩展框架变得简单;基于以上内容,我们可以大致描述出索引模块工作的过程:首先在XML 配置的bean 中找出所有Resource对象;对每一个调用其extractDocuments()方法,这一步除了完成对资源的索引外,还会在每次提取出一个Document对象之后,通知注册在该资源上的所有DocumentListener;接着处理资源的元数据(getMetaData()的返回值);将缓存里的数据写入到本地磁盘或者传送给远程服务器;在这个过程中,有两个地方值得注意。
第一,对资源可以注册DocumentListener使得我们可以在运行时刻对索引过程有更为动态的控制。举一个简单例子,对某个文章发布站点的文章进行索引时,一个很正常的要求便是发布时间更靠近当前时间的文章需要在搜索结果中排在靠前的位置。每篇文章显然对应一个Document对象,在Lucene 中我们可以通过设置Document的boost值来对其进行加权。假设其中文章发布时间的Field的名称为PUB_TIME,那么我们可以为资源注册一个DocumentListener,当它被通知时,则检测PUB_TIME的值,根据距离当前时间的
远近进行加权。
第二点很显然,在这个过程中,extractDocuments()方法的实现依不同类型的资源而各异。下面我们主要讨论两种类型的资源:文件系统资源和数据库结果集资源。这两个类都实现了上面的接口。文件系统资源对文件系统资源的索引通常从一个基目录开始,递归处理每个需要进行索引的文件。该资源有一个字符串数组类型的excludedFiles属性,表示在处理文件时需要排除的文件绝对路径的正则表达式。在递归遍历文件系统树的同时,绝对路径匹配excludedFiles中任意一项的文件将不会被处理。这主要是考虑到一般我们只需要对一部分文件夹(比如排除可能存在的备份目录)中的一部分文件(如doc, ppt 文件等)进行索引。除了所有文件共有的文件名、文件路径、文件大小和修改时间等Field,不同类型的文件需要有不同的处理方法。为了保留灵活性,我们使用Strategy 模式封装对不同类型文件的处理方式。为此我们抽象出一个DocumentBuilder的接口,该接口仅定义了一个方法如下: