Lucene入门与使用
本文主要面向具体使用,适用于已熟悉java编程的lucene初学者。
1. Lucene的简介
1.1 Lucene 历史
org.apache.lucene包是纯java语言的全文索引检索工具包。
Lucene的作者是资深的全文索引/检索专家,最开始发布在他本人的主页上,2001年10月贡献给APACHE,成为APACHE基金jakarta的一个子项目。
目前,lucene广泛用于全文索引/检索的项目中。
lucene也被翻译成C#版本,目前发展为Lucene.Net(不过最近好象有流产的消息)。
1.2 Lucene 原理
lucene的检索算法属于索引检索,即用空间来换取时间,对需要检索的文件、字符流进行全文索引,在检索的时候对索引进行快速的检索,得到检索位置,这个位置记录检索词出现的文件路径或者某个关键词。
在使用数据库的项目中,不使用数据库进行检索的原因主要是:数据库在非精确查询的时候使用查询语言“like %keyword%”,对数据库进行查询是对所有记录遍历,并对字段进行“%keyword%”匹配,在数据库的数据庞大以及某个字段存储的数据量庞大的时候,这种遍历是致命的,它需要对所有的记录进行匹配查询。因此,lucene主要适用于文档集的全文检索,以及海量数据库的模糊检索,特别是对数据库的xml或者大数据的字符类型。
2.Lucene的下载和配置
2.1 Lucene的下载
lucene在jakarta项目中的发布主页:http://jakarta.apache.org/lucene/docs/index.html。以下主要针对windows用户,其它用户请在上面的地址中查找相关下载。
lucene的.jar包的下载(包括.jar和一个范例demo):
http://apache.oregonstate.edu/jakarta/lucene/binaries/lucene-1.4-final.zip
lucene的源代码下载:
http://www.signal42.com/mirrors/apache/jakarta/lucene/source/lucene-1.4-final-src.zip
lucene的api地址:http://jakarta.apache.org/lucene/docs/api/index.html
本文使用lucene版本:lucene-1.4-final.jar。
2.2 lucene的配置
首先请确定你的机子已经进行了java使用环境的基本配置,即确保在某个平台下能够运行java源代码,否则请查阅相关文档进行配置。
接下来进入lucene的配置:
普通使用者:在环境变量的CLASSPATH中添加lucene的位置。比如:“D:/java /lucene-1.4-final/lucene-1.4-final.jar;”。
jbuilder使用者:在“Project”--“Project Properties”--“Required Libraries”进行添加。
Jsp使用者:也可以直接将lucene-1.4-final.jar文件放到/WEB-INF/classes下。
3. Lucene 的范例(Demo )
3.1 Demo说明
可以得到的Demo包括:lucene-demos-1.4-final、XMLIndexingDemo,lucene-demos-1.4-final中包括对普通文件和html文件的两种索引,XMLIndexingDemo针对xml文件的索引。他们的区别主要在于:对普通文件进行索引时只要对文件的全文进行索引,而针对html、xml文件时,对标签类型不能进行索引,在实现上:html、xml的索引需要额外的数据流分析器,以分析哪些内容有用哪些无用。因此,在后两者实现上,索引的时间额外开支,甚至超过索引本身时间,而检索时间没有区别。
以上Demo中,lucene-demos-1.4-final自带于lucene-1.4-final.zip中,XMLIndexingDemo的下载地址:
http://cvs.apache.org/viewcvs.cgi/jakarta-lucene-sandbox/contributions/XML-Indexing-Demo/
3.2 Demo的运行
首先将demo.jar的路径添加如环境变量的CLASSPATH中,例如:“D:/java/lucene-1.4-final/lucene-demos-1.4-final.jar;”,同时确保已经添加lucene-1.4-final.jar。
然后进行文件的全文索引,在dos控制台中,输入命令“java org.apache.lucene.demo.IndexFiles {full-path-to-lucene}/src”,后面的路径为所要进行索引的文件夹,例如:“java org.apache.lucene.demo.IndexFiles c:/test”。
接着对索引进行检索,敲入“java org.apache.lucene.demo.SearchFiles”,在提示“Query:”后输入检索词,程序将进行检索列出检索得到的结果(检索词出现的文件路径)。
其他Demo的运行请参考/docs/demo.html。
在运行Demo后请阅读Demo的源代码以便深入学习。
4. 利用Lucene进行索引
进行lucene的熟悉后,我们将学习如何使用Lucene。
一段索引的应用实例:
//需要捕捉IOException异常
//建立一个IndexWriter,索引保存目录为“index”
String[] stopStrs = {
"他奶奶的", "***"};
StandardAnalyzer analyzer = new StandardAnalyzer(stopStrs);
IndexWriter writer = new IndexWriter("index", analyzer, true);
//添加一条文档
Document doc = new Document();
doc.add(Field.UnIndexed("id", "1"));//“id”为字段名,“
1”
为字段值
doc.add(Field.Text("text", "***,他奶奶的,入门与使用"));
writer.addDocument(doc);
//索引完成后的处理
writer.optimize();
writer.close();
看完这段实例后,我们开始熟悉lucene的使用:
4.1 Lucene的索引接口
在学习索引的时候,首先需要熟悉几个接口:
4.1.1 分析器Analyzer
分析器主要工作是筛选,一段文档进来以后,经过它,出去的时候只剩下那些有用的部分,其他则剔除。而这个分析器也可以自己根据需要而编写。
org.apache.lucene.analysis.Analyzer:这是一个虚构类,以下两个借口均继承它而来。
org.apache.lucene.analysis.SimpleAnalyzer:分析器,支持最简单拉丁语言。
org.apache.lucene.analysis.standard.StandardAnalyzer:标准分析器,除了拉丁语言还支持亚洲语言,并在一些匹配功能上进行完善。在这个接口中还有一个很重要的构造函数:StandardAnalyzer(String[] stopWords),可以对分析器定义一些使用词语,这不仅可以免除检索一些无用信息,而且还可以在检索中定义禁止的政治性、非法性的检索关键词。
4.1.2 IndexWriter
IndexWriter的构造函数有三种接口,针对目录Directory、文件File、文件路径String三种情况。
例如IndexWriter(String path, Analyzer a, boolean create),path为文件路径,a为分析器,create标志是否重建索引(true:建立或者覆盖已存在的索引,false:扩展已存在的索引。)
一些重要的方法:
接口名 | 备注 |
addDocument(Document doc) | 索引添加一个文档 |
addIndexes(Directory[] dirs) | 将目录中已存在索引添加到这个索引 |
addIndexes(IndexReader[] readers) | 将提供的索引添加到这个索引 |
optimize() | 合并索引并优化 |
close() | 关闭 |
IndexWriter为了减少大量的io维护操作,在每得到一定量的索引后建立新的小索引文件(笔者测试索引批量的最小单位为10),然后再定期将它们整合到一个索引文件中,因此在索引结束时必须进行wirter. optimize(),以便将所有索引合并优化。
4.1.3 org.apache.lucene.document
以下介绍两种主要的类:
a)org.apache.lucene.document.Document:
Document文档类似数据库中的一条记录,可以由好几个字段(Field)组成,并且字段可以套用不同的类型(详细见b)。Document的几种接口:
接口名 | 备注 |
add(Field field) | 添加一个字段(Field)到Document中 |
String get(String name) | 从文档中获得一个字段对应的文本 |
Field getField(String name) | 由字段名获得字段值 |
Field[] getFields(String name) | 由字段名获得字段值的集 |
b)org.apache.lucene.document.Field
即上文所说的“字段”,它是Document的片段section。
Field的构造函数:
Field(String name, String string, boolean store, boolean index, boolean token)。
Indexed:如果字段是Indexed的,表示这个字段是可检索的。
Stored:如果字段是Stored的,表示这个字段的值可以从检索结果中得到。
Tokenized:如果一个字段是Tokenized的,表示它是有经过Analyzer转变后成为一个tokens序列,在这个转变过程tokenization中,Analyzer提取出需要进行索引的文本,而剔除一些冗余的词句(例如:a,the,they等,详见org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引时候的基本单元,代表一个被索引的词,例如一个英文单词,或者一个汉字。因此,所有包含中文的文本都必须是Tokenized的。
Field的几种接口:
Name | Stored | Indexed | Tokenized | use |
Keyword(String name, String value) | Y | Y | N | date,url |
Text(String name, Reader value) | N | Y | Y | short text fields: title,subject |
Text(String name, String value) | Y | Y | Y | longer text fields, like “body” |
UnIndexed(String name, String value) | Y | N | N |
|
UnStored(String name, String value) | N | Y | Y |
|
?
5. 利用Lucene进行检索
5.1 一段简单的检索代码
//需要捕捉IOException,ParseException异常
//处理检索条件
Query query = QueryParser.parse("入门", "text", analyzer);
//检索
Searcher searcher = new IndexSearcher("./index");//"index"指定索引文件位置
Hits hits = searcher.search(query);
//打印结果值集
for (int i = 0; i < hits.length(); i++) {
doc = hits.doc(i);
String id = doc.get("id");
System.out.println("found " + "入门" + " on the id:" + id);
}
5.2 利用Lucene的检索接口
5.2.1 Query与QueryParser
主要使用方法:
QueryParser .parse(String query, String field, Analyzer analyzer),例如:
Query query = QueryParser.parse("入门", "text", analyzer);
"入门"为检索词, "text"为检索的字段名, analyzer为分析器
5.2.2 Hits与Searcher
Hits的主要使用接口:
接口名 | 备注 |
Doc(int n) | 返回第n个的文档的所有字段 |
length() | 返回这个集中的可用个数 |
6. Lucene的其他使用
6.1 Lucene 的索引修改
下面给出一段修改索引的代码,请根据Lucene的API解读:
/**
* 对已有的索引添加新的一条索引
* @param idStr String:要修改的id
* @param doc Document:要修改的值
*/
public void addIndex(String idStr, String valueStr) {
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriter writer = null;
try {
writer = new IndexWriter(indexPath, analyzer, false);
writer.mergeFactor = 2; //修正lucene
1.4.2
bug,否则不能准确反映修改
Document doc = new Document();
doc.add(Field.UnIndexed("id", idStr));//“id”为字段名,“
1”
为字段值
doc.add(Field.Text("text", valueStr));
writer.addDocument(doc);
writer.optimize();
writer.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* 删除索引
*
* @param idStr String
*/
public void deleteIndex(String idStr) {
try {
Directory dirt = FSDirectory.getDirectory(indexPath, false);
IndexReader reader = IndexReader.open(dirt);
IndexXML.deleteIndex(idStr, reader);
reader.close();
dirt.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
6.2 Lucene 的检索结果排序
Lucene的排序主要是对org.apache.lucene.search.Sort的使用。Sort可以直接根据字段Field生成,也可以根据标准的SortField生成,但是作为Sort的字段,必须符合以下的条件:唯一值以及Indexed。可以对Integers, Floats, Strings三种类型排序。
对整数型的ID检索结果排序只要进行以下的简单操作:
Sort sort = new Sort("id");
Hits hits = searcher.search(query, sort);
用户还可以根据自己定义更加复杂的排序,详细请参考API。
7 总结
Lucene给java的全文索引检索带来了非常强大的力量,以上仅对Lucene进行简单的入门说明。
参考资料:
1. Overview (Lucene 1.4-final API)
2. 车东 《在应用中加入全文检索功能--基于JAVA的全文索引引擎Lucene简介》
3. http://www.mail-archive.com/lucene-user@jakarta.apache.org/index.html
Lucene原理
Lucene是一个高性能的java全文检索工具包,它使用的是倒排文件索引结构。该结构及相应的生成算法如下:
0)设有两篇文章1和2
文章1的内容为:Tom lives in
Guangzhou
,I live in
Guangzhou
too.
文章2的内容为:He once lived in
Shanghai
.
1)由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施
a.我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。
b.文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义,这些不代表概念的词可以过滤掉
c.用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。
d.用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live”
e.文章中的标点符号通常不表示某种概念,也可以过滤掉
在lucene中以上措施由Analyzer类完成
经过上面处理后
文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有关键词为:[he] [live] [shanghai]
2) 有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。文章1,2经过倒排后变成
关键词 文章号
guangzhou 1
he 2
i 1
live 1,2
shanghai 2
tom 1
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:a)字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);b)关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置。
加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
关键词 文章号[出现频率] 出现位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1
以live 这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,
2”
这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,
5”
就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“
2”
就表示live是文章2中第 2个关键字。
以上就是lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(lucene没有使用B树结构),因此lucene可以用二元搜索算法快速定位关键词。
实现时 lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。
Lucene中使用了field的概念,用于表达信息所在位置(如标题中,文章中,url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息(因为每个关键字一定属于一个或多个field)。
为了减小索引文件的大小,Lucene对索引还使用了压缩技术。首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减小数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)。
下面我们可以通过对该索引的查询来解释一下为什么要建立索引。
假设要查询单词 “live”,lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。
而用普通的顺序匹配算法,不建索引,而是对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。
Lucene讲座
5. IndexReader类与IndexWirter类··· 23
第一节 全文检索系统与Lucene简介
一、 什么是全文检索与全文检索系统?
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
全文检索的方法主要分为按字检索和按词检索两种。按字检索是指对于文章中的每一个字都建立索引,检索时将词分解为字的组合。对于各种不同的语言而言,字有不同的含义,比如英文中字与词实际上是合一的,而中文中字与词有很大分别。按词检索指对文章中的词,即语义单位建立索引,检索时按词检索,并且可以处理同义项等。英文等西方文字由于按照空白切分词,因此实现上与按字处理类似,添加同义处理也很容易。中文等东方文字则需要切分字词,以达到按词索引的目的,关于这方面的问题,是当前全文检索技术尤其是中文全文检索技术中的难点,在此不做详述。
全文检索系统是按照全文检索理论建立起来的用于提供全文检索服务的软件系统。一般来说,全文检索需要具备建立索引和提供查询的基本功能,此外现代的全文检索系统还需要具有方便的用户接口、面向WWW[1]的开发接口、二次应用开发接口等等。功能上,全文检索系统核心具有建立索引、处理查询返回结果集、增加索引、优化索引结构等等功能,外围则由各种不同应用具有的功能组成。结构上,全文检索系统核心具有索引引擎、查询引擎、文本分析引擎、对外接口等等,加上各种外围应用系统等等共同构成了全文检索系统。图1.1展示了上述全文检索系统的结构与功能。
在上图中,我们看到:全文检索系统中最为关键的部分是全文检索引擎,各种应用程序都需要建立在这个引擎之上。一个全文检索应用的优异程度,根本上由全文检索引擎来决定。因此提升全文检索引擎的效率即是我们提升全文检索应用的根本。另一个方面,一个优异的全文检索引擎,在做到效率优化的同时,还需要具有开放的体系结构,以方便程序员对整个系统进行优化改造,或者是添加原有系统没有的功能。比如在当今多语言处理的环境下,有时需要给全文检索系统添加处理某种语言或者文本格式的功能,比如在英文系统中添加中文处理功能,在纯文本系统中添加XML[2]或者HTML[3]格式的文本处理功能,系统的开放性和扩充性就十分的重要。
二、 什么是Lucene?
Lucene是apache软件基金会[4] jakarta项目组的一个子项目,是一个开放源代码[5]的全文检索引擎工具包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
Lucene的原作者是Doug Cutting,他是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎[6]的主要开发者,后在Excite[7]担任高级系统架构设计师,目前从事于一些Internet底层架构的研究。早先发布在作者自己的http://www.lucene.com/,后来发布在SourceForge[8],2001年年底成为apache软件基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/。
三、 Lucene的应用、特点及优势
作为一个开放源代码项目,Lucene从问世之后,引发了开放源代码社群的巨大反响,程序员们不仅使用它构建具体的全文检索应用,而且将之集成到各种系统软件中去,以及构建Web应用,甚至某些商业软件也采用了Lucene作为其内部全文检索子系统的核心。apache软件基金会的网站使用了Lucene作为全文检索的引擎,IBM的开源软件eclipse[9]的2.1版本中也采用了Lucene作为帮助子系统的全文索引引擎,相应的IBM的商业软件Web Sphere[10]中也采用了Lucene。Lucene以其开放源代码的特性、优异的索引结构、良好的系统架构获得了越来越多的应用。
Lucene作为一个全文检索引擎,其具有如下突出的优点:
(1)索引文件格式独立于应用平台。Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件。
(2)在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提升索引速度。然后通过与原有索引的合并,达到优化的目的。
(3)优秀的面向对象的系统架构,使得对于Lucene扩展的学习难度降低,方便扩充新功能。
(4)设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token流完成索引文件的创立,用户扩展新的语言和文件格式,只需要实现文本分析的接口。
(5)已经默认实现了一套强大的查询引擎,用户无需自己编写代码即使系统可获得强大的查询能力,Lucene的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search[11])、分组查询等等。
面对已经存在的商业全文检索引擎,Lucene也具有相当的优势。首先,它的开发源代码发行方式(遵守Apache Software License[12]),在此基础上程序员不仅仅可以充分的利用Lucene所提供的强大功能,而且可以深入细致的学习到全文检索引擎制作技术和面相对象编程的实践,进而在此基础上根据应用的实际情况编写出更好的更适合当前应用的全文检索引擎。在这一点上,商业软件的灵活性远远不及Lucene。其次,Lucene秉承了开放源代码一贯的架构优良的优势,设计了一个合理而极具扩充能力的面向对象架构,程序员可以在Lucene的基础上扩充各种功能,比如扩充中文处理能力,从文本扩充到HTML、PDF[13]等等文本格式的处理,编写这些扩展的功能不仅仅不复杂,而且由于Lucene恰当合理的对系统设备做了程序上的抽象,扩展的功能也能轻易的达到跨平台的能力。最后,转移到apache软件基金会后,借助于apache软件基金会的网络平台,程序员可以方便的和开发者、其它程序员交流,促成资源的共享,甚至直接获得已经编写完备的扩充功能。最后,虽然Lucene使用Java语言写成,但是开放源代码社区的程序员正在不懈的将之使用各种传统语言实现(例如.net framework[14]),在遵守Lucene索引文件格式的基础上,使得Lucene能够运行在各种各样的平台上,系统管理员可以根据当前的平台适合的语言来合理的选择。
四、 本文的重点问题与cLucene项目
作为中国人民大学信息学院99级本科生的一个毕业设计项目,我们对Lucene进行了深入的研究,包括系统的结构,索引文件结构,各个部分的实现等等。并且我们启动了cLucene项目,做为一个Lucene的C++语言的重新实现,以期望带来更快的速度和更加广泛的应用范围。我们先分析了系统结构,文件结构,然后在研究各个部分的具体实现的同时开始进行的cLucene实现。限于时间的限制,到本文完成为止,cLucene项目并没有完成,对于Lucene的具体实现部分也仅仅完成到了索引引擎部分。
接下来的部分,本文将对Lucene的系统结构、文件结构、索引引擎部分做一个彻底的分析。以期望提供对Lucene全文检索引擎的系统架构和部分程序实现的清晰的了解。cLucene项目则作为一个开放源代码的项目,继续进行的开发。
有关cLucene项目的一些信息:
n 开发语言:ISO C++[15],STLport 4.5.3 [16],OpenTop 1.1[17]
n 目标平台:Win32,POSIX
n 授权协议:GNU General Public License (GPL)[18]
第二节 Lucene系统结构分析
一、 系统结构组织
Lucene作为一个优秀的全文检索引擎,其系统结构具有强烈的面向对象特征。首先是定义了一个与平台无关的索引文件格式,其次通过抽象将系统的核心组成部分设计为抽象类,具体的平台实现部分设计为抽象类的实现,此外与具体平台相关的部分比如文件存储也封装为类,经过层层的面向对象式的处理,最终达成了一个低耦合高效率,容易二次开发的检索引擎系统。
以下将讨论Lucene系统的结构组织,并给出系统结构与源码组织图:
从图中我们清楚的看到,Lucene的系统由基础结构封装、索引核心、对外接口三大部分组成。其中直接操作索引文件的索引核心又是系统的重点。Lucene的将所有源码分为了7个模块(在java语言中以包即package来表示),各个模块所属的系统部分也如上图所示。需要说明的是org.apache.lucene.queryPaser是做为org.apache.lucene.search的语法解析器存在,不被系统之外实际调用,因此这里没有当作对外接口看待,而是将之独立出来。
从面象对象的观点来考察,Lucene应用了最基本的一条程序设计准则:引入额外的抽象层以降低耦合性。首先,引入对索引文件的操作org.apache.lucene.store的封装,然后将索引部分的实现建立在(org.apache.lucene.index)其之上,完成对索引核心的抽象。在索引核心的基础上开始设计对外的接口org.apache.lucene.search与org.apache.lucene.analysis。在每一个局部细节上,比如某些常用的数据结构与算法上,Lucene也充分的应用了这一条准则。在高度的面向对象理论的支撑下,使得Lucene的实现容易理解,易于扩展。
Lucene在系统结构上的另一个特点表现为其引入了传统的客户端服务器结构以外的的应用结构。Lucene可以作为一个运行库被包含进入应用本身中去,而不是做为一个单独的索引服务器存在。这自然和Lucene开放源代码的特征分不开,但是也体现了Lucene在编写上的本来意图:提供一个全文索引引擎的架构,而不是实现。
二、 数据流分析
理解Lucene系统结构的另一个方式是去探讨其中数据流的走向,并以此摸清楚Lucene系统内部的调用时序。在此基础上,我们能够更加深入的理解Lucene的系统结构组织,以方便以后在Lucene系统上的开发工作。这部分的分析,是深入Lucene系统的钥匙,也是进行重写的基础。
我们来看看在Lucene系统中的主要的数据流以及它们之间的关系图:
|
|
|
|
|
图2.2很好的表明了Lucene在内部的数据流组织情况,并且沿着数据流的方向我们也可以对与Lucene内部的执行时序有一个清楚的了解。现在将图中的涉及到的流的类型与各个逻辑对应系统的相关部分的关系说明一下。
图中共存在4种数据流,分别是文本流、token流、字节流与查询语句对象流。文本流表示了对于索引目标和交互控制的抽象,即用文本流表示了将要索引的文件,用文本流向用户输出信息;在实际的实现中,Lucene中的文本流采用了UCS-2[19]作为编码,以达到适应多种语言文字的处理的目的。Token流是Lucene内部所使用的概念,是对传统文字中的词的概念的抽象,也是Lucene在建立索引时直接处理的最小单位;简单的讲Token就是一个词和所在域值的组合,后面在叙述文件格式时也将继续涉及到token,这里不详细展开。字节流则是对文件抽象的直接操作的体现,通过固定长度的字节(Lucene定义为8比特位长,后面文件格式将详细叙述)流的处理,将文件操作解脱出来,也做到了与平台文件系统的无关性。查询语句对象流则是仅仅在查询语句解析时用到的概念,它对查询语句抽象,通过类的继承结构反映查询语句的结构,将之传送到查找逻辑来进行查找的操作。
图中的涉及到了多种逻辑,基本上直接对应于系统某一模块,但是也有跨模块调用的问题发生,这是因为Lucene的重用程度非常好,因此很多实现直接调用了以前的工作成果,这在某种程度上其实是加强了模块耦合性,但是也是为了避免系统的过于庞大和不必要的重复设计的一种折衷体现。词法分析逻辑对应于org.apache.lucene.analysis部分。查询语句语法分析逻辑对应于org.apache.lucene.queryParser部分,并且调用了org.apache.lucene.analysis的代码。查询结束之后向评分排序逻辑输出token流,继而由评分排序逻辑处理之后给出文本流的结果,这一部分的实现也包含在了org.apache.lucene.search中。索引构建逻辑对应于org.apache.lucene.index部分。索引查找逻辑则主要是org.apache.lucene.search,但是也大量的使用了org.apache.lucene.index部分的代码和接口定义。存储抽象对应于org.apache.lucene.store。没有提到的模块则是做为系统公共基础设施存在。
三、 基于Lucene的应用开发
通过以上的系统结构分析和数据流分析,我们已经很清楚的了解了Lucene的系统的结构特征。在此基础上,我们可以通过扩充Lucene系统来完成一个完备的全文检索引擎,紧接着还可以在全文检索引擎的基础上构建各种应用系统。鉴于本文的目的并不在此,以下我们只是略为叙述一下相关的步骤,从而给出应用开发的一些思路。
首先,我们需要的是按照目标语言的词法结构来构建相应的词法分析逻辑,实现Lucene在org.apache.lucene.analysis中定义的接口,为Lucene提供目标系统所使用的语言处理能力。Lucene默认的已经实现了英文和德文的简单词法分析逻辑(按照空格分词,并去除常用的语法词,如英语中的is,am,are等等)。在这里,主要需要参考实现的接口在org.apache.lucene.analysis中的Analyzer.java和Tokenizer.java中定义,Lucene提供了很多英文规范的实现样本,也可以做为实现时候的参考资料。其次,需要按照被索引的文件的格式来提供相应的文本分析逻辑,这里是指除开词法分析之外的部分,比如HTML文件,通常需要把其中的内容按照所属于域分门别类加入索引,这就需要从org.apache.lucene.document中定义的类document继承,定义自己的HTMLDocument类,然后就可以将之交给org.apache.lucene.index模块来写入索引文件。完成了这两步之后,Lucene全文检索引擎就基本上完备了。这个过程可以用下图表示:
当然,上面所示的仅仅只是对于Lucene的基本扩充过程,它将Lucene由不完备的变成完备的(尤其是对于非英语的语言检索)。除此之外我们还可以在很多方面对Lucene进行改造。第一个方面即为按照文档索引的域,比如标题,作者之类的信息对返回的查询结果排序,这即需要改造Lucene的评分排序逻辑。默认的,Lucene采用其内部的相关性方法来处理评分和排序,我们可以根据需要改变它。遗憾的是,这部分Lucene并没有做到如同扩充词法解析和文档类型那样的条理清晰,没有留下很好的接口,因此需要仔细的分析其源代码的实现,自行扩充等等。其他的方面,比如改进其索引的效率,改进其返回结果时候的缓冲机制等等,都是加强Lucene系统的方面,在此也不再叙述。
完成了Lucene系统,之后就可以开始考虑其上的应用系统开发。如果应用系统也使用java语言开发,那么Lucene系统能够方便的嵌入到整个系统中去,作为一个API集来调用。这个过程十分简单,以下便是一个示例程序,配合注释理解起来很容易。
|
或者,Lucene全文检索引擎也可作为服务器程序启动,但是这就需要用户自行扩充其他应用与Lucene的接口。这个可以通过传统的包装方式,比如客户服务器结构,或者采用现在流行的Web方式。诸如此类的应用方案,本文也不再继续叙述。参考Lucene的项目网站中的用户邮件列表能找到更多的信息。
第三节 Lucene索引文件格式分析
一、 Lucene源码实现分析的说明
通过以上对Lucene系统结构的分析,我们已经大致的清楚了Lucene系统的组成,以及在Lucene系统之上的开发步骤。接下来,我们试图来分析Lucene项目(采用Lucene 1.2版本)的源码实现,考察其实现的细节。这不仅仅是我们尝试用C++语言重新实现Lucene的必须工作,也是进一步做Lucene开发工作的必要准备。因此,这一部分所涉及到的内容,对于Lucene上的应用开发也是有价值的,尤其是本部分所做的文件格式分析。
由于本文建立在我们的毕设项目之上,且同时我们需要实现cLucene项目,因此很遗憾的我们并没有完全的完成Lucene的所有源码实现的分析工作。接下来的部分,我们将涉及的部分为Lucene文件格式分析,Lucene中的存储抽象模块分析,以及Lucene中的索引构建逻辑模块分析。这一部分,我们主要涉及到的是文件格式分析与存储抽象模块分析。
二、 Lucene索引文件格式
在Lucene的web站点上,有关于Lucene的文件格式的规范,其规定了Lucene的文件格式采取的存储单位、组织结构、命名规范等等内容,但是它仅仅是一个规范说明,并没有从实现者角度来衡量这个规范的实现。因此,我们以下的内容,结合了我们自己的分析与文件格式的定义规范,以期望给出一个更加清晰的文件格式说明。具体的文档规范可以参考后面的文献2。
首先在Lucene的文件格式中,以字节为基础,定义了如下的数据类型:
表 3.1 Lucene文件格式中定义的数据类型
数据类型 | 所占字节长度(字节) | 说明 | ||||||||||||||||||||||||||||||||||||||||||||
Byte | 1 | 基本数据类型,其他数据类型以此为基础定义 | ||||||||||||||||||||||||||||||||||||||||||||
UInt32 | 4 | 32位无符号整数,高位优先 | ||||||||||||||||||||||||||||||||||||||||||||
UInt64 | 8 | 64位无符号整数,高位优先 | ||||||||||||||||||||||||||||||||||||||||||||
VInt | 不定,最少1字节 | 动态长度整数,每字节的最高位表明还剩多少字节,每字节的低七位表明整数的值,高位优先。可以认为值可以为无限大。其示例如下
| ||||||||||||||||||||||||||||||||||||||||||||
Chars | 不定,最少1字节 | 采用UTF-8编码[20]的Unicode字符序列 | ||||||||||||||||||||||||||||||||||||||||||||
String | 不定,最少2字节 | 由VInt和Chars组成的字符串类型,VInt表示Chars的长度,Chars则表示了String的值 |
以上的数据类型就是Lucene索引文件格式中用到的全部数据类型,由于它们都以字节为基础定义而来,因此保证了是平台无关,这也是Lucene索引文件格式平台无关的主要原因。接下来我们看看Lucene索引文件的概念组成和结构组成。
以上就是Lucene的索引文件的概念结构。Lucene索引index由若干段(segment)组成,每一段由若干的文档(document)组成,每一个文档由若干的域(field)组成,每一个域由若干的项(term)组成。项是最小的索引概念单位,它直接代表了一个字符串以及其在文件中的位置、出现次数等信息。域是一个关联的元组,由一个域名和一个域值组成,域名是一个字串,域值是一个项,比如将“标题”和实际标题的项组成的域。文档是提取了某个文件中的所有信息之后的结果,这些组成了段,或者称为一个子索引。子索引可以组合为索引,也可以合并为一个新的包含了所有合并项内部元素的子索引。我们可以清楚的看出,Lucene的索引结构在概念上即为传统的倒排索引结构[21]。
从概念上映射到结构中,索引被处理为一个目录(文件夹),其中含有的所有文件即为其内容,这些文件按照所属的段不同分组存放,同组的文件拥有相同的文件名,不同的扩展名。此外还有三个文件,分别用来保存所有的段的记录、保存已删除文件的记录和控制读写的同步,它们分别是segments,deletable和lock文件,都没有扩展名。每个段包含一组文件,它们的文件扩展名不同,但是文件名均为记录在文件segments中段的名字。让我们看如下的结构图3.2。
|
|
|
|
|
|
|
|
|
|
|
关于图3.2中的各个文件具体的内部格式,在参考文献3中,均可以找到详细的说明。接下来我们从宏观关系上说明一下这些文件组成。在这些宏观上的关系理清楚之后,仔细阅读参考文献3,即可清楚的明白具体的Lucene文件格式。
每个段的文件中,主要记录了两大类的信息:域集合与项集合。这两个集合中所含有的文件在图3.2中均有表明。由于索引信息是静态存储的,域集合与项集合中的文件组采用了一种类似的存储办法:一个小型的索引文件,运行时载入内存;一个对应于索引文件的实际信息文件,可以按照索引中指示的偏移量随机访问;索引文件与信息文件在记录的排列顺序上存在隐式的对应关系,即索引文件中按照“索引项1、索引项2…”排列,则信息文件则也按照“信息项1、信息项2…”排列。比如在图3.2所示文件中,segment1.fdx与segment1.fdt之间,segment1.tii与segment1.tis、segment1.prx、segment1.frq之间,都存在这样的组织关系。而域集合与项集合之间则通过域的在域记录文件(比如segment1.fnm)中所记录的域记录号维持对应关系,在图3.2中segment1.fdx与segment1.tii中就是通过这种方式保持联系。这样,域集合和项集合不仅仅联系起来,而且其中的文件之间也相互联系起来。此外,标准化因子文件和被删除文档文件则提供了一些程序内部的辅助设施(标准化因子用在评分排序机制中,被删除文档是一种伪删除手段)。这样,整个段的索引信息就通过这些文档有机的组成。
以上所阐述的,就是Lucene所采用的索引文件格式。基本上而言,它是一个倒排索引,但是Lucene在文件的安排上做了一些努力,比如使用索引/信息文件的方式,从文件安排的形式上提高查找的效率。这是一种数据库之外的处理方法,其有其优点(格式平台独立、速度快),也有其缺点(独立性带来的共享访问接口问题等等),具体如何衡量两种方法之间的利弊,本文这里就不讨论了。
三、 一些公用的基础类
分析完索引文件格式,我们接下来应该着手对存储抽象也就是org.apache.lucenestore中的源码做一些分析。我们先不着急分析这部分,而是分析图2.1中基础结构封装那一部分,因为这是整个系统的基石,然后我们在下一部分再来分析存储抽象。
基础结构封装,或者基础类,由org.apache.lucene.util和org.apache.lucene.document两个包组成,前者定义了一些常量和优化过的常用的数据结构和算法,后者则是对于文档(document)和域(field)概念的一个类定义。以下我们用列表的方式来分析这些封装类,指出其要点。
表 3.2 基础类包org.apache.lucene.util
类 | 说明 |
Arrays | 一个关于数组的排序方法的静态类,提供了优化的基于快排序的排序方法sort |
BitVector | C/C++语言中位域的java实现品,但是加入了序列化能力 |
Constants | 常量静态类,定义了一些常量 |
PriorityQueue | 一个优先队列的抽象类,用于后面实现各种具体的优先队列,提供常数时间内的最小元素访问能力,内部实现机制是哈析表和堆排序算法 |
表 3.3 基础类包org.apache.lucene.document
类 | 说明 |
Document | 是文档概念的一个实现类,每个文档包含了一个域表(fieldList),并提供了一些实用的方法,比如多种添加域的方法、返回域表的迭代器的方法 |
Field | 是域概念的一个实现类,每个域包含了一个域名和一个值,以及一些相关的属性 |
DateField | 提供了一些辅助方法的静态类,这些方法将java中Date和Time数据类型和String相互转化 |
总的来说,这两个基础类包中含有的类都比较简单,通过阅读源代码,可以很容易的理解,因此这里不作过多的展开。
四、 存储抽象
有了上面的知识,我们接下来来分析存储抽象部分,也就是org.apache.lucene.store包。存储抽象是唯一能够直接对索引文件存取的包,因此其主要目的是抽象出和平台文件系统无关的存储抽象,提供诸如目录服务(增、删文件)、输入流和输出流。在分析其实现之前,首先我们看一下UML[22]图。
图 3.3 存储抽象实现UML图(一)
图 3.4 存储抽象实现UML图(二)
图 3.4 存储抽象实现UML图(三)
图3.2到3.4展示了整个org.apache.lucene.store中主要的继承体系。共有三个抽象类定义:Directory、InputStream和OutputStrem,构成了一个完整的基于抽象文件系统的存取体系结构,在此基础上,实作出了两个实现品:(FSDirectory,FSInputStream,FSOutputStream)和(RAMDirectory,RAMInputStream和RAMOutputStream)。前者是以实际的文件系统做为基础实现的,后者则是建立在内存中的虚拟文件系统。前者主要用来永久的保存索引文件,后者的作用则在于索引操作时是在内存中建立小的索引,然后一次性的输出合并到文件中去,这一点我们在后面的索引逻辑部分能够看到。此外,还定以了org.apache.lucene.store.lock和org.apache.lucene.store.with两个辅助内部实现的类用在实现Directory方法的makeLock的时候,以在锁定索引读写之前来让客户程序做一些准备工作。
(FSDirectory,FSInputStream,FSOutputStream)的内部实现依托于java语言中的io类库,只是简单的做了一个外部逻辑的包装。这当然要归功于java语言所提供的跨平台特性,同时也带了一些隐患:文件存取的效率提升需要依耐于文件类库的优化。如果需要继续优化文件存取的效率,应该还提供一个文件与目录的抽象,以根据各种文件系统或者文件类型来提供一个优化的机会。当然,这是应用开发者所不需要关系的问题。
(RAMDirectory,RAMInputStream和RAMOutputStream)的内部实现就比较直接了,直接采用了虚拟的文件RAMFile类(定义于文件RAMDirectory.java中)来表示文件,目录则看作一个String与RAMFile对应的关联数组。RAMFile中采用数组来表示文件的存储空间。在此的基础上,完成各项操作的实现,就形成了基于内存的虚拟文件系统。因为在实际使用时,并不会牵涉到很大字节数量的文件,因此这种设计是简单直接的,也是高效率的。
这部分的实现在理清楚继承体系后,相当的简单。因此接下来的部分,我们可以通过直接阅读源代码解决。接下来我们看看这个部分的源代码如何在实际中使用的。
一般来说,我们使用的是抽象类提供的接口而不是实际的实现类本身。在实现类中一般都含有几个静态函数,比如createFile,它能够返回一个OutputStream接口,或者openFile,它能够返回一个InputStream接口,利用这些接口之中的方法,比如writeString,writeByte等等,我们就能够在抽象的层次上处理Lucene定义的数据类型的读写。简单的说,Lucene中存储抽象这部分设计时采用了工厂模式(Factory parttern)[23]。我们利用静态类的方法也就是工厂来创建对象,返回接口,通过接口来执行操作。
五、 关于cLucene项目
这一部分详细的说明了Lucene系统中所采用的索引文件格式、一些基础类和存储抽象。接下来我们来叙述一下我们在项目cLucene中重新实现这些结构时候的一些考虑。
cLucene彻底的遵守了Lucene所定义的索引文件格式,这是Lucene对于各个兼容系统的基本要求。在此基础上,cLucene系统和Lucene系统才能够共享索引文件数据。或者说,cLucene生成的索引文件和Lucene生成的索引文件完全等价。
在基础类问题上,cLucene同样封装了类似的结构。我们同样列表描述,请和前面的表3.2与3.3对照比较。
表 3.4 基础类包cLucene::util
类 | 说明 |
Arrays | 没有实现,直接利用了STL库中的快排序算法实现 |
BitVector | C/C++语言版本的实现,与java实现版本类似 |
Constants | 常量静态类,定义了一些常量,但是与java版本不同的是,这里主要定义了一些宏 |
PriorityQueue | 这是一个类型定义,直接利用STL库中的std::priority_queue |
表 3.3 基础类包cLucene::document
类 | 说明 |
Document | C/C++语言版本的实现,与java实现版本类似 |
Field | C/C++语言版本的实现,与java实现版本类似 |
DateField | 没有实现,直接利用OpenTop库中的ot::StringUtil |
存储抽象的实现上,也同样是类似于java实现。由于我们采用了OpenTop库,因此同样得以借助其中对于文件系统抽象的ot::io包来解决文件系统问题。这部分问题与前面一样,存在优化的可能。在实现的类层次上、对外接口上,均与java版本的一样。
第四节 Lucene索引构建逻辑模块分析
一、 绪论
这一个部分,我们将分析Lucene中的索引构建逻辑模块。它与前面介绍的存储抽象一起构成了Lucene的索引核心部分。无论是对外接口中的查询,还是分析各种文本以进一步生成索引,都需要直接调用这部分来获得对索引文件的访问能力,因此,这部分在系统中至关重要。构建一个高效的、易使用的索引构建逻辑,即是Lucene在这一部分需要达到的目的。
从面向对象的经典思考方式出发来看,我们只需要使用继承体系来表达图3.1中的各个概念,就可以通过这个继承体系来控制索引文件的结构,然后设计合适的永久化方法,以及接受分析token流的操作,即可将索引构建逻辑完成。原理上就是这样的简单。由于两个关键的概念document和field都已经在org.apache.lucene.document中当作基础类定义过了,因此实际上Lucene在这部分需要完善的概念结构还有segment和term。在此基础上继续编写各个逻辑结构的永久化方法,然后提供一个进入的接口方法,即是宣告完成了这个过程。其中永久化的部分,Lucene使用了另外实现一个代理类的方式来实现,即对于某个类X,存在XWriter类和XReader类来负责写出和读入的功能;用作永久化功能的类是被永久化的类的友元。
在接下来的分析过程中,我们按照这样一个思路,以UML图和对象体系的描述来叙述这部分的设计和实现,然后通过内部的数据流理清楚调用时序。
二、 对象体系与UML图
1. 项(Term)
这部分主要是分析针对项(Term)这个概念所做的设计,包括概念所实际涉及的类、永久化类。首先,我们从图3.2和阅读参考文献3知道,项(Term)所表示的是一个字符串,它拥有域、频数和位置信息等等属性。因此,Lucene中设计了两个类来表示这个概念,如下图
图 4.1 UML图(-)
上图中,有意的突出了类Term和TermInfo中的数据成员,因为它反映了对于项(Term)这个概念的具体表示。同时上图中也同时列出了用于永久化项(Term)的代理类TermInfosWriter和TermInfosReader,它们完成永久化的功能,需要注意的是,TermInfosReader内部使用了数组indexTerms和indexInfos来存储一系列项;而TermInfosWriter则是一个类似于链表的结构,通过一个other指向下一个TermInfosWriter,每一个TermInfosWriter只负责本身那个lastTerm和lastTi的永久化工作。这是一个设计上的技巧,通过批量读取(或者称为缓冲的方式)来获得读入时候的效率优化;而通过一个链表式的、各负其责的方式,来获得写出时候的设计简化。
项(term)这部分的设计中,还有一些重要的接口和类,我们先介绍如下,同样我们也先展示UML图
图 4.2 UML图(二)
图4.2中,我们看到三个类:TermEnum、TermDocs与TermPositions,第一个是抽象类,后两个都是接口。TermEnum的设计主要用在后面Segment和Document等等的实现中,以提供枚举其中每一个项(Term)的能力。TermDocs是一个接口,用来继承以提供返回<document, frequency>值对的能力,通过这个接口就可以获得某个项(Term)在某个文档中出现的频数。TermPositions则是在TermDocs上的扩展,将项(Term)在文档中的位置信息也表示出来。TermDocs(TermPositions)接口的使用方式类似于java中的Enumration接口,即通过next方法跳转,通过doc,freq等方法获得当前的属性值。
2. 域(Field)
由于Field的基本概念在org.apache.lucene.document中已经做了定义,因此在这部分主要是针对项文件(.fnm文件、.fdx文件、.fdt文件)所需要的信息再来设计一些类。
图 4.3 UML图(三)
图 4.3中展示的,就是表示与域(Field)所关联的属性信息的类。其中isIndexed表示的这个域的值是否被索引过,即值是否被分词然后索引;另外两个属性所表示的意思则很明显:一个是域的名字,一个是域的编号。
接下来我们来看关于域表和存取逻辑的UML图。
图 4.4 UML图(四)
FieldInfos即为域表的概念表示,内部采用了冗余的方式以获取在通过域的编号访问或者通过域的名字来访问时候的高效率。FieldsReader与FieldsWriter则分别是写出和读入的代理类。在功能和实现上,这两个类都比较简单。至于FieldInfos中采用的冗余方式,则是基于域的数目相对比较少而做出的一种折衷处理。
3. 文档(document)
文档(document)同样也是在org.apache.lucene.document中定义过的结构。由于对于这部分比较重要,我们也来看看其UML图。
图 4.5 UML图(五)
在图4.5中我们看到,Document的设计基本上沿用了链表的处理方法。左边的Document类作为一个数据外包类,用来提供对于内部结构DocumentFieldList的增加删除访问操作等等。DocumentFieldList才是实际上的数据存储单位,它用了链表的处理方法,直接指向一个当前的Field对象和下一个DocumentFieldList对象,这个与前面的类似。为了能够逐个访问链表中的节点,还设计了DocumentFieldEnumeration枚举类。
图 4.6 UML图(六)
实际上定义于org.apache.lucene.index中的有关于Document的就是永久化的代理类。在图4.6中给出了其UML图。需要说明的是为什么没有出现读入的方法:这个方法已经隐含在图4.5中Document类中的add方法中了,结合图2.4中的程序代码段,我们就能够清楚的理解这种设计。
4. 段(segment)
段(Segment)这一部分设计的比较特殊,在实现简单的对象结构之上,还特意的设计了用于段之间合并的类。接下来,我们仍然采取对照UML分析的方式逐个叙述。接下来我们看Lucene中如何表示段这个概念。
图 4.7 UML图(七)
Lucene定义了一个类SegmentInfo用来表示每一个段(Segment)的信息,包括名字(name)、含有的文档的数目(docCount)和段所位于的目录的位置(dir)。根据索引文件中的段的意义,有了这三点,就能唯一确定一个段了。SegmentInfos这个类则是用来表示一个段的链表(从标准的java.util.Vector继承而来),实际上,也就是索引(index)的意思了。需要注意的是,这里并没有在SegmentInfo中安插一个文档(document)的链表。这样做的原因牵涉到Lucene内部对于文档(相当于一个被索引文件)的处理;Lucene内部采用了赋予文档编号,给域赋值的方式来处理文档,即加入的文档顺次编号,以后用文档号表示文档,而路径信息,文件名字等等在以后索引查找需要的属性,都作为域存储下来;因此SegmentInfo中并没有另外存储一个文档(document)的链表,对于这些的写出和读入,则交给了永久化的代理类来做。
图 4.8 UML图(八)
图4.8给出了负责段(segment)的读入操作的代理类,而负责段(segment)的写出操作也同样没有定义,这些操作都直接实现在了类IndexWriter类中(后面会详细分析)。段的操作同样采用了之前的数组或者说是缓冲的处理方式,相关的细节也不在这里详细叙述了。
然后,针对前面项(term)那部分定义的几个接口,段(segment)这部分也需要做相应的接口实现,因为提供直接遍历访问段中的各个项的能力对于检索来说,无疑是十分重要的。即这部分的设计,实际上都是在为了检索在服务。
图 4.9 UML图(九)
图 4.10 UML图(十)
图4.9和图4.10分别展示了前面项(term)那里定义的接口是如何在这里通过继承实现的。Lucene在处理这部分的时候,也是分成两部分(Segment与Segments开头的类)来实现,而且很合理的运用了数组的技法,以及注意了继承重用。但是细化到局部,终归是比较简单的按照语义来获得结果而已了,因此关于更多的也就不多做分析了,我们完全可以通过阅读源代码来解决。
接下来所介绍的,就是在Lucene的设计过程中比较特殊的一个部分:段合并类(SegmentMerger)。这首先需要介绍Lucene中的建立索引时的段合并策略。
Lucene为了兼顾建立索引时的效率和读取索引查找的速度,引入了分小段建立索引的方式,即每一次批量建立索引时,先在内存中的虚拟文件系统中为每一个文档单独建立一个段,然后在输出的时候将这些段合并之后输出成为索引文件,这时仅仅存在一个段。多次建立的索引后,如果想优化索引文件,也可采取合并段的方法,将索引中的段合并成为一个段。我们来看一下在IndexWriter类中相应的方法的实现,来了解一下这中建立索引的实现。
对于上面的代码,我们不做过多注释了,结合源码中的注解应该很容易理解。在最后那个mergeSegments函数中,将用到几个重要的类结构,它们记录了合并时候的一些重要信息,完成合并时候的工作。接下来,我们来看这几个类的UML图。
图 4.12 UML图(十一)
从图4.12中,我们看到Lucene设计一个类SegmentMergeInfo用来保存每一个被合并的段的信息,也保存能够访问其内部的接口句柄,也就是说合并时的操作使用这个类作为对被合并的段的操作代理。类SegmentMergeQueue则设计为org.apache.lucene.util.PriorityQueue的子类,做为SegmentMergeInfo的容器类,而且附带能够自动排序。SegmentMerger是主要进行操作的类,里面各个方法环环相扣,分别完成合并各个数据项的问题。
5. IndexReader类与IndexWirter类
最后剩下的,就是整个索引逻辑部分的使用接口类了。外界通过这两个类以及文档(document)类的构造函数调用之,比如图2.4中的代码示例所示。下面我们来看一下这部分最后两个类的UML图。
图 4.13 UML图(十二)
IndexWriter的设计与IndexReader的设计很不相同,前者是一个实现类,而后者是一个抽象类,带有没有实现的接口。IndexWriter的主要作用就是接收新加入的文档(document),然后在内部为之生成相应的小段,最后再合并并向索引文件中输出,图4.11中已经给出了一些实现的代码。由于Lucene在面向对象上封装的努力,通过各个构造函数就已经完成了对于各个概念的构造过程,剩下部分的代码主要是依据各个数组或者是链表中的信息,逐个逐个的将信息写出到相应的文件中去了。IndexReader部分则只是做了接口设计,没有具体的实现,这个和本部分所完成的主要功能有关:索引构建逻辑。设计这个抽象类的目的是,预先完成一些函数,为以后的检索(search)部分的各种形式的IndexReader铺平道路,也是利用了在同一个包内可以方便访问其它类的保护变量这个java语言的限制。
到此,在索引构建逻辑部分出现的类我们就分析完毕了,需要说明主要是做的一个宏观上的组成结构上的分析,并指出一些实现上的要点。具体的实现,由于Lucene的开放源码而显得并不是非常的重要,因为Lucene在做到良好的面相对象设计之后,实际带来的是局部复杂性的减小,因此某一些单独的函数或者实现就比较容易编写,也容易让人阅读。本文不再继续叙述这方面的细节,作为一个总结,下一个部分我们通过索引构建逻辑的数据流图的方式,再来理清楚一下索引构建逻辑这部分的调用时序。
三、 数据流逻辑
从宏观上明白一个系统的设计,理清楚其中的运行规律,最好的方式应该是通过数据流图。在分析了各个位于索引构建逻辑部分的类的设计之后,我们接下来就通过分析数据流图的方式来总结一下。但是由于之前提到的原因:索引读入部分在这一部分并没有完全实现,所以我们在数据流图中主要给出的是索引构建的数据流图。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对于图4.14中所描述的内容,结合Lucene源代码中的一些文件看,能够加深理解。准备阶段可以参考demo文件夹中的org.apache.lucene.demo.IndexFiles类和java文件夹中的org.apache.lucene.document文件包。索引构建阶段的主要源码位于java文件夹中org.apache.lucene.index.IndexWriter类,因此这部分可以结合这个类的实现来看。至于内存文件系统,比较复杂,但是这时的逻辑相对简单,因此也不难理解。
上面的数据流图十分清楚的勾画除了整个索引构建逻辑这部分的设计:通过层层嵌套的类结构,在构建时候即分步骤有计划的生成了索引结构,将之存储到内存中的文件系统中,然后通过对内存中的文件系统优化合并输出到实际的文件系统中。
四、 关于cLucene项目
前面的三个部分,已经完成了分析索引构建逻辑的任务,这里我们还是有针对性的谈谈我们这次的毕业设计项目cLucene在这一部分的情况。
在实现这部分的时候,为了将一些java语法中比较特殊的部分,比如内隐类、同步函数、同步对象等等,我们不得不采用了一些比较晦涩和艰深的C++语法,在OpenTop这个类库所提供的类似于java语言的设施上来实现。这个尤其体现在实现Segment相关类时,为了处理原来java源代码中用内隐类实现的Lock文件创建机制的时候,我们不得不定义了大量的cLucene::store::With的子类,并为之传入调用类的指针,设置它为调用类的友元,才得以精确的模拟了原有的语义。陷于我们这次的重写以移植为主,系统结构基本上没有大的变化,不得不产生这种重复而且大量的工作。如果需要改进这中状况,我们应该考虑按照C++语言的特点来设计索引构建部分的类库继承结构,但是很可惜在本文成文之前,时间不允许我们这样做。
来自java语法的特殊性只是我们解决问题的一个方面,我们还需要处理引用的调用方式。由于java语言拥有了垃圾收集机制,因此得以将一切的参数形式看作为引用,而不考虑其分配与消亡的问题。C++语言并不具备这种机制,它需要程序员自行管理分配空间与销毁对象的问题。在这里,我们使用的是来自OpenTop中所引入的计数指针RefPtr<>模板,它能够模拟指针的语义,并且计算指针被引用的次数,在引用次数为0时就自动释放资源:这是一种类似于java语言中引用的方式,不过它显得更加高效率。我们在cLucene的实现中大量的使用了计数指针模板。
除此之外,我们没有改变Lucene所定义的索引构建逻辑的结构和语义,我们实现的是一个完全和java版本Lucene兼容的版本。
Lucene的系统结构
本文主要讨论Lucene的系统结构,希望对其结构的初步分析,更深入的了解Lucene的运作机制,从而实现对Lucene的功能扩展。
1. Lucene的包结构
如上图所示,Lucene源码中共包括7个子包,每个包完成特定的功能:
Lucene包结构功能表 | |
包名 | 功能 |
org.apache.lucene.analysis | 语言分析器,主要用于的切词,支持中文主要是扩展此类 |
org.apache.lucene.document | 索引存储时的文档结构管理,类似于关系型数据库的表结构 |
org.apache.lucene.index | 索引管理,包括索引建立、删除等 |
org.apache.lucene.queryParser | 查询分析器,实现查询关键词间的运算,如与、或、非等 |
org.apache.lucene.search | 检索管理,根据查询条件,检索得到结果 |
org.apache.lucene.store | 数据存储管理,主要包括一些底层的I/O操作 |
org.apache.lucene.util | 一些公用类 |
1. 2. Lucene的主要逻辑图
Lucene功能强大,但从根本上说,主要包括两块:一是文本内容经切词后索引入库;二是根据查询条件返回结果。
以下是上述两大功能的逻辑图:
|
|
|
|
|
|
|
|
|
|
|
|
查询逻辑
按先后顺序,查询逻辑可分为如下几步:
1. 1. 查询者输入查询条件
条件之间可以通过特定运算符进行运算,比如查询希望查询到与“中国”和“北京”相关的记录,但不希望结果中包括“海淀区中关村”,于是输入条件为“中国+北京-海淀区中关村”;
2. 2. 查询条件被传达到查询分析器中,分析器将将对“中国+北京-海淀区中关村”进行分析,首先分析器解析字符串的连接符,即这里的加号和减号,然后对每个词进行切词,一般最小的词元是两个汉字,则中国和北京两个词不必再切分,但对海淀区中关村需要切分,假设根据切词算法,把该词切分为“海淀区”和“中关村”两部分,则最后得到的查询条件可以表示为:“中国” AND “北京” AND NOT(“海淀区” AND “中关村”)。
3. 3. 查询器根据这个条件遍历索引树,得到查询结果,并返回结果集,返回的结果集类似于JDBC中的ResultSet。
4. 4. 将返回的结果集显示在查询结果页面,当点击某一条内容时,可以链接到原始网页,也可以打开全文检索库中存储的网页内容。
这就是查询的逻辑过程,需要说明的是,Lucene默认只支持英文,为了便于说明问题,以上查询过程采用中文举例,事实上,当Lucene被扩充支持中文后就是这么一个查询过程。
入库逻辑
入库将把内容加载到全文检索库中,按顺序,入库逻辑包括如下过程:
1. 1. 入库者定义到库中文档的结构,比如需要把网站内容加载到全文检索库,让用户通过“站内检索”搜索到相关的网页内容。入库文档结构与关系型数据库中的表结构类似,每个入库的文档由多个字段构成,假设这里需要入库的网站内容包括如下字段:文章标题、作者、发布时间、原文链接、正文内容(一般作为网页快照)。
2. 2. 包含N个字段的文档(DOCUMENT)在真正入库前需要经过切词(或分词)索引,切词的规则由语言分析器(ANALYZER)完成。
3. 3. 切分后的“单词”被注册到索引树上,供查询时用,另外也需要也其它不需要索引的内容入库,所有这些是文件操作均由STORAGE完成。
以上就是记录加载流程,索引树是一种比较复杂的数据存储结构,将在后续章节陆续介绍,这里就不赘述了,需要说明的一点是,Lucene的索引树结构非常优秀,是Lucene的一大特色。
接下来将对Lucene的各个子包的结构进行讨论。
2. 3. 语言分析包org.apache.lucene.analysis
Analyzer是一个抽象类,司职对文本内容的切分词规则。
切分后返回一个TokenStream,TokenStream中有一个非常重要方法next(),即取到下一个词。简单点说,通过切词规则,把一篇文章从头到尾分成一个个的词,这就是org.apache.lucene.analysis的工作。
对英文而言,其分词规则很简单,因为每个单词间都有一个空格,按空格取单词即可,当然为了提高英文检索的准确度,也可以把一些短语作为一个整体,其间不切分,这就需要一个词库,对德文、俄文也是类似,稍有不同。
对中文而言,文字之间都是相连的,没有空格,但我们同样可以把字切分,即把每个汉字作为一个词切分,这就是所谓的“切字”,但切字方式方式的索引没有意义,准确率太低,要想提高准确度一般都是切词,这就需要一个词库,词库越大准确度将越高,但入库效率越低。
若要支持中文切词,则需要扩展Analyzer类,根据词库中的词把文章切分。
简单点说,org.apache.lucene.analysis就是完成将文章切分词的任务。
3. 4. 文档结构包org.apache.lucene.document
document包相对而言比较简单,该包下面就3个类,Document相对于关系型数据库的记录对象,主要负责字段的管理,字段分两种,一是Field,即文本型字段,另一个是日期型字段DateField。这个包中关键需要理解的是Field中字段存储方式的不同,这在上一篇中已列表提到,下面我们可以参见一下其详细的类图:
4. 5. 索引管理包org.apache.lucene.index
索引包是整个系统核心,全文检索的的根本就为每个切出来的词建索引,查询时就只需要遍历索引,而不需要去正文中遍历,从而极大的提高检索效率,索引建设的质量关键整个系统的质量。Lucene的索引树是非常优质高效的,具体的索引树细节,将在后续章节中重要探讨。
在这个包中,主要学习IndexWriter和IndexReader这个类。
通过上一篇的初步应用可知,全文检索库的初始化和记录加载均需要通过该类来完成。
初始化全文库的语句为:
IndexWriter indexWriter = new IndexWriter(“全文库的目录位置”,new StandardAnalyzer(),true);
记录加载的语句为:indexWriter.addDocument(doc);
IndexWriter主要用于写库,当需要读取库内容时,就需要用到IndexReader这个类了。
5. 6. 查询分析包org.apache.lucene.queryParser和检索包org.apache.lucene.search
通过查询分析器(queryParser)解析后,将返回一个查询对象(query),根据查询对象就可进行检索了。上图描述了query对象的生成,下图描述了查询结果集(Hits)的生成。
6. 7. 存储包org.apache.lucene.store
一些底层的文件I/O操作。
7. 8. 工具包org.apache.lucene.util
该包中包括4个工具类。
8. 9. 总结
通过对Lucene源码包的分析,我们可以初步认识到Lucene的核心类包主要有3个:
l l org.apache.lucene.analysis
l l org.apache.lucene.index
l l org.apache.lucene.search
其中org.apache.lucene.analysis 主要用于切分词,切分词的工作由Analyzer的扩展类来实现,Lucene自带了StandardAnalyzer类,我们可以参照该写出自己的切词分析器类,如中文分析器等。
org.apache.lucene.index主要提供库的读写接口,通过该包可以创建库、添加删除记录及读取记录等。
org.apache.lucene.search主要提供了检索接口,通过该包,我们可以输入条件,得到查询结果集,与org.apache.lucene.queryParser包配合还可以自定义的查询规则,像google一样支持查询条件间的与、或、非、属于等复合查询。
参考资料
1. 1. http://www-igm.univ-mlv.fr/~dr/XPOSE2003/lucene/node1.html
Lucene实践
Lucene 全文检索实践(1)
Lucene 是 Apache Jakarta 的一个子项目,是一个全文检索的搜索引擎库。其提供了简单实用的 API,通过这些 API,可以自行编写对文件(TEXT/XML/HTML等)、目录、数据库的全文检索程序。
Features:
* Very fast indexing, minimal RAM required
* Index compression to 30% of original text
* Indexes text and HTML, document classes available for XML, PDF and RTF
* Search supports phrase and Boolean queries, plus, minus and quote marks, and parentheses
* Allows single and multiple character wildcards anywhere in the search words, fuzzy search, proximity
* Will search for punctuation such as + or ?
* Field searches for title, author, etc., and date-range searching
* Supports most European languages
* Option to store and display full text of indexed documents
* Search results in relevance order
* APIs for file format conversion, languages and user interfaces
实践任务:
1) 编写 Java 程序 MyIndexer.java,使用 JDBC 取出 MySQL 数据表内容(以某一论坛数据做测试),然后通过 org.apache.lucene.index.IndexWriter 创建索引。
2) 编写 Java 程序 MySearcher.java,通过 org.apache.lucene.search.IndexSearcher 等查询索引。
3) 实现支持中文查询及检索关键字高亮显示。
4) 通过 PHP / Java Integration 实现对 MySearch.java 的调用。
5) 实现对 PHP 手册(简体中文) 的全文检索。
Lucene 全文检索实践(2)
Java 的程序基本编写完成,实现了对中文的支持。下一步是将其放到 WEB 上运行,首先想到的是使用 JSP,安装了Apache Tomcat/4.1.24,默认的发布端口是 8080。现在面临的一个问题是:Apache httpd 的端口是 80,并且我的机器对外只能通过 80 端口进行访问,如果将 Tomcat 的发布端口改成 80 的话,httpd 就没法对外了,而其上的 PHP 程序也将无法在 80 端口运行。
对于这个问题,我想到两种方案:
1、使用 PHP 直接调用 Java。需要做的工作是使用 --with-java 重新编译 PHP;
2、使用 mod_jk 做桥接的方式,将 servlet 引擎结合到 httpd 中。需要做的工作是编译 jakarta-tomcat-connectors-jk-1.2.5-src,生成 mod_jk.so 给 httpd 使用,然后按照 Howto 文档 进行 Tomcat、httpd 的配置。
对于第一个方案的尝试:使用 PHP 直接调用 Java
环境
* PHP 4.3.6 prefix=/usr
* Apache 1.3.27 prefix=/usr/local/apache
* j2sdk1.4.1_01 prefix=/usr/local/jdk
配置步骤
1) 安装 JDK,这个就不多说了,到 GOOGLE 可以搜索出这方面的大量文章。
2) 重新编译 PHP,我的 PHP 版本是 4.3.6:
cd php- 4.3.6
./configure --with-java=/usr/local/jdk
make
make install
完成之后,会在 PHP 的 lib 下(我的是在 /usr/lib/php)有个 php_java.jar,同时在扩展动态库存放的目录下(我的是在 /usr/lib/php/20020429)有个 java.so 文件。到这一步需要注意一个问题,有些 PHP 版本生成的是 libphp_java.so 文件,extension 的加载只认 libphp_java.so,直接加载 java.so 可能会出现如下错误:
PHP Fatal error: Unable to load Java Library /usr/local/jdk/jre/lib/i386/libjava.so, error: libjvm.so:
cannot open shared object file: No such file or directory in /home/nio/public_html/java.php on line 2
所以如果生成的是 java.so,需要创建一个符号连接:
ln -s java.so libphp_java.so
3) 修改 Apache Service 启动文件(我的这个文件为 /etc/init.d/httpd),在这个文件中加入:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/jdk/jre/lib/i386/server:/usr/local/jdk/jre/lib/i386
正如你所看到的,我的 JDK 装在 /usr/local/jdk 目录下,如果你的不是在此目录,请做相应改动(下同)。
4) 修改 PHP 配置文件 php.ini,找到 [Java] 部分进行修改:
[Java]
java.class.path = /usr/lib/php/php_java.jar
java.home = /usr/local/jdk
;java.library =
;java.library.path =
extension_dir=/usr/lib/php/20020429/
extension=java.so
我将 java.library 及 java.library.path 都注释掉了,PHP 会自动认为 java.library=/usr/local/jdk/jre/lib/i386/libjava.so。
5) 重新启动 Apache httpd 服务:
service httpd restart
测试
测试脚本 java.php 源代码:
getProperty('java.version').'<br />';
print 'Java vendor=' . $system->getProperty('java.vendor').'<br />';
print 'OS=' . $system->getProperty('os.name') . ' ' .
$system->getProperty('os.version') . ' on ' .
$system->getProperty('os.arch') . '<br />';
?>
总结
安装配置还算简单,但是在 PHP 运行 Java 的速度感觉较慢,所以下定决心开始实践第二个方案。(待续)
Lucene 全文检索实践(3)
今天总算有些空闲时间,正好说说第二种方案:使用 mod_jk 做桥接的方式,将 servlet 引擎结合到 httpd 中。
环境
* PH
P 4.3.6
prefix=/usr
* Apache 1.3.27 prefix=/usr/local/apache
* j2sdk1.4.1_01 prefix=/usr/local/jdk
* jakarta-tomcat-4.1.24 prefix=/usr/local/tomcat
* 另外需要下载 jakarta-tomcat-connectors-jk-1.2.5-src.tar.gz
配置步骤
1) 安装 JDK 与 Tomcat,这些安装步骤就不多说了。
2) 编译 jakarta-tomcat-connectors-jk-1.2.5-src,生成 mod_jk.so,并将其复制到 apache 的 modules 存放目录:
tar xzf jakarta-tomcat-connectors-jk- 1.2.5 -src.tar.gz
cd jakarta-tomcat-connectors-jk- 1.2.5 -src/jk/native
./configure --with-apxs=/usr/local/apache/bin/apxs
make
cp apache-1.3/mod_jk.so /usr/local/apache/libexec
3) 编辑 Apache 配置文件 /usr/local/apache/conf/httpd.conf,加入:
LoadModule jk_module libexec/mod_jk.so
AddModule mod_jk.c
这个 LoadModule 语句最好放在其他 LoadModule 语句后边。
同时在配置文件后边加入:
# workers.properties 文件所在路径,后边将对此文件进行讲解
JkWorkersFile /usr/local/apache/conf/workers.properties
# jk 的日志文件存放路径
JkLogFile /usr/local/apache/log/mod_jk.log
# 设置 jk 的日志级别 [debug/error/info]
JkLogLevel info
# 选择日志时间格式
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "
# JkOptions 选项设置
JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories
# JkRequestLogFormat 设置日志的请求格式
JkRequestLogFormat "%w %V %T"
# 映射 /examples/* 到 worker1,worker1 在 workers.properties 文件中定义
JkMount /examples/* worker1
4) 在 /usr/local/apache/conf/ 目录下创建 workers.properties 文件,其内容如下:
# 定义使用 ajp13 的 worker1
worker.list=worker1
# 设置 worker1 的属性(ajp13)
worker.worker1.type=ajp13
worker.worker1.host=localhost
worker.worker1.port=8009
worker.worker1.lbfactor=50
worker.worker1.cachesize=10
worker.worker1.cache_timeout=600
worker.worker1.socket_keepalive=1
worker.worker1.socket_timeout=300
5) 好了,启动 Tomcat,重启一下 Apache HTTPD Server,访问:http://localhost/examples/index.jsp,看看结果如何,和 http://localhost:8080/examples/index.jsp 是一样的。
提示:如果不想让别人通过 8080 端口访问到你的 Tomcat,可以将 /usr/lcoal/tomcat/conf/server.xml 配置文件中的如下代码加上注释:
<!--
<Connector className="org.apache.coyote.tomcat4.CoyoteConnector"
port="8080" minProcessors="5" maxProcessors="75"
enableLookups="false" redirectPort="8443"
acceptCount="100" debug="0" connectionTimeout="20000"
useURIValidationHack="false" disableUploadTimeout="true" />
-->
然后重新启动 Tomcat 即可。
总结
此方案安装配置稍微复杂些,但执行效率要比第一种方案要好很多。所以决定使用这种方案来完成我的 Lucene 全文检索实践任务。
Tomcat Service 脚本
在 Linux (我用的是 Redhat)中,如果经常需要启动/关闭 Tomcat 的话,还是创建一个 daemon 来得比较方便,创建步骤如下:
1) 在 /etc/init.d/ 目录下创建文件 tomcat,代码如下:
# chkconfig: 345 91 10
# description: Tomcat daemon.
#
# 包含函数库
. /etc/rc.d/init.d/functions
# 获取网络配置
. /etc/sysconfig/network
# 检测 NETWORKING 是否为 "yes"
[ "${NETWORKING}" = "no" ] && exit 0
# 设置变量
# $TOMCAT 指向 Tomcat 的安装目录
TOMCAT=/usr/local/tomcat
# $STARTUP 指向 Tomcat 的启动脚本
STARTUP=$TOMCAT/bin/startup.sh
# $SHUTDOWN 指向 Tomcat 的关闭脚本
SHUTDOWN=$TOMCAT/bin/shutdown.sh
# 设置 JAVA_HOME 环境变量,指向 JDK 安装目录
export JAVA_HOME=/usr/local/jdk
# 启动服务函数
start() {
echo -n $"Starting Tomcat service: "
$STARTUP
RETVAL=$?
echo
}
# 关闭服务函数
stop() {
action $"Stopping Tomcat service: " $SHUTDOWN
RETVAL=$?
echo
}
# 根据参数选择调用
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo $"Usage: $0 start|stop|restart"
exit 1
esac
exit 0
2) 修改 tomcat 文件的属性
chown a+x tomcat
3) 生成 service
chkconfig --add tomcat
好了,现在可以通过 service tomcat start 命令启动 Tomcat 了,关闭及重启服务的命令也类似,只是将 start 换成 stop 或 restart。
Lucene 全文检索实践(4)
在几天的研究中,了解了 Lucene 全文检索的一些原理,同时进行了实践,编写了一个论坛的全文检索创建索引程序及用于搜索的 JSP 程序,另外还写了一个 PHP 手册(简体中文)的全文检索,可以进行多关键字搜索。基本完成了最初定下的实践任务。
Lucene 全文检索实践(5)
对于 Lucene 的初步研究已经过去一段时间,自己感觉还不是很深入,但由于时间的关系,一直也没再拿起。应网友的要求,将自己实践中写的一些代码贴出来,希望能对大家有用。程序没有做进一步的优化,只是很简单的实现功能而已,仅供参考。
在实践中,我以将 PHP 中文手册中的 HTML 文件生成索引,然后通过一个 JSP 对其进行全文检索。
生成索引的 Java 代码:
/**
* PHPDocIndexer.java
* 用于对 PHPDoc 的 HTML 页面生成索引文件。
*/
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Date;
import java.text.DateFormat;
import java.lang.*;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.DateField;
class PHPDocIndexer
{
public static void main(String[] args) throws ClassNotFoundException, IOException
{
try {
Date start = new Date();
IndexWriter writer = new IndexWriter("/home/nio/indexes-phpdoc", new CJKAnalyzer(), true); //索引保存目录,必须存在
indexDocs(writer, new File("/home/nio/phpdoc-zh")); //HTML 文件保存目录
System.out.println("Optimizing ....");
writer.optimize();
writer.close();
Date end = new Date();
System.out.print("Total time: ");
System.out.println(end.getTime() - start.getTime());
} catch (Exception e) {
System.out.println("Class " + e.getClass() + " throws error!/n errmsg: " + e.getMessage());
} //end try
} //end main
public static void indexDocs(IndexWriter writer, File file) throws Exception
{
if (file.isDirectory()) {
String[] files = file.list();
for (int i = 0; i < files.length; i++) {
indexDocs(writer, new File(file, files[i]));
} //end for
} else if (file.getPath().endsWith(".html")) { //只对 HTML 文件做索引
System.out.print("Add file:" + file + " ....");
// Add html file ....
Document doc = new Document();
doc.add(Field.UnIndexed("file", file.getName())); //索引文件名
doc.add(Field.UnIndexed("modified", DateFormat.getDateTimeInstance().format(new Date(file.lastModified())))); //索引最后修改时间
String title = "";
String content = "";
String status = "start";
FileReader fReader = new FileReader(file);
BufferedReader bReader = new BufferedReader(fReader);
String line = bReader.readLine();
while (line != null) {
content += line;
//截取 HTML 标题 <title>
if ("start" == status && line.equalsIgnoreCase("><TITLE")) {
status = "match";
} else if ("match" == status) {
title = line.substring(1, line.length() - 7);
doc.add(Field.Text("title", title)); //索引标题
status = "end";
} //end if
line = bReader.readLine();
} //end while
bReader.close();
fReader.close();
doc.add(Field.Text("content", content.replaceAll("<[^<>]+>", ""))); //索引内容
writer.addDocument(doc);
System.out.println(" [OK]");
} //end if
}
} //end class
索引生成完之后,就需要一个检索页面,下边是搜索页面(search.jsp)的代码:
<%@ page language="java" import="javax.servlet.*, javax.servlet.http.*, java.io.*, java.util.Date, java.util.ArrayList, java.util.regex.*, org.apache.lucene.analysis.*, org.apache.lucene.document.*, org.apache.lucene.index.*, org.apache.lucene.search.*, org.apache.lucene.queryParser.*, org.apache.lucene.analysis.Token, org.apache.lucene.analysis.TokenStream, org.apache.lucene.analysis.cjk.CJKAnalyzer, org.apache.lucene.analysis.cjk.CJKTokenizer, com.chedong.weblucene.search.WebLuceneHighlighter" %>
<%@ page contentType="text/html;charset=GB2312" %>
<!DOCTYPE html PUBLIC "-//W 3C //DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>PHPDoc - PHP 简体中文手册全文检索</title>
<base target="main"><!-- 由于使用了 Frame,所以指定 target 到 main 窗口显示 -->
<style>
body {background-color: white; margin: 4px}
body, input, div {font-family: Tahoma; font-size: 9pt }
body, div {line-height: 18px}
u {color: red}
b {color: navy}
form {padding: 0px; margin: 0px}
.txt {border: 1px solid black}
.f {padding: 4px; margin-bottom: 16px; background-color: #E5ECF9; border-top: 1px solid #3366CC; border-bottom: 1px solid #3366CC; text-align: center;}
.d, .o {padding-left: 16px}
.d {color: gray}
.o {color: green}
.o a {color: #7777CC}
</style>
<script language="JavaScript">
function gotoPage(i)
{
document.frm.page.value = i;
document.frm.submit();
} //end function
</script>
</head>
<body>
<%
String keyVal = null;
String pageVal = null;
int offset = 0;
int curPage = 0;
int pages;
final int ROWS = 50;
//获取 GET 参数
try {
byte[] keyValByte = request.getParameter("key").getBytes("ISO8859_1"); //查找关键字
keyVal = new String(keyValByte);
pageVal = request.getParameter("page"); //页码
} catch (Exception e) {
//do nothing;
}
if (keyVal == null)
keyVal = new String("");
%>
<div class="f">
<form name="frm" action="./index.jsp" method="GET" onsubmit="this.page.value='0';return true;" target="_self">
<input type="text" name="key" class="txt" size="40" value="<%=keyVal%>" />
<input type="hidden" name="page" value="<%=pageVal%>" />
<input type="submit" value="搜 索" /><br />
<font color="green">提示:可使用多个关键字(使用空格隔开)提高搜索的准确率。</font>
</form>
<script language="JavaScript">
document.frm.key.focus();
</script>
</div>
<%
if (keyVal != null && keyVal.length() > 0) {
try {
curPage = Integer.parseInt(pageVal); //将当前页转换成整数
} catch (Exception e) {
//do nothing;
} //end try
try {
Date startTime = new Date();
keyVal = keyVal.toLowerCase().replaceAll("(or|and)", "").trim().replaceAll("//s+", " AND ");
Searcher searcher = new IndexSearcher("/home/nio/indexes-phpdoc"); //索引目录
Analyzer analyzer = new CJKAnalyzer();
String[] fields = {"title", "content"};
Query query = MultiFieldQueryParser.parse(keyVal, fields, analyzer);
Hits hits = searcher.search(query);
StringReader in = new StringReader(keyVal);
TokenStream tokenStream = analyzer.tokenStream("", in);
ArrayList al = new ArrayList();
for (Token token = tokenStream.next(); token != null; token = tokenStream.next()) {
al.add(token.termText());
} //end for
//总页数
pages = (new Integer(hits.length()).doubleValue() % ROWS != 0) ? (hits.length() / ROWS) + 1 : (hits.length() / ROWS);
//当前页码
if (curPage < 1)
curPage = 1;
else if (curPage > pages)
curPage = pages;
//起始、终止下标
offset = (curPage - 1) * ROWS;
int end = Math.min(hits.length(), offset + ROWS);
//循环输出查询结果
WebLuceneHighlighter hl = new WebLuceneHighlighter(al);
for (int i = offset; i < end; i++) {
Document doc = hits.doc(i);
%>
<div class="t"><a href="/~nio/phpdoc-zh/<%=doc.get("file")%>"><%=hl.highLight(doc.get("title"))%></a></div>
<div class="d"><%=hl.highLight(doc.get("content").replaceAll("/n", " "), 100)%> ……</div>
<div class="o">
/~nio/phpdoc-zh/<%=doc.get("file")%>
-
<%=doc.get("modified")%>
</div>
<br />
<%
} //end for
searcher.close();
Date endTime = new Date();
%>
<div class="f">
检索总共耗时 <b><%=((endTime.getTime() - startTime.getTime()) / 1000.0)%></b> 秒,约有 <b><%=hits.length()%></b> 项符合条件的记录,共 <b><%=pages%></b> 页
<%
if (curPage > 1 && pages > 1) {
%>
| <a href="javascript:gotoPage(<%=(curPage-1)%>);" target="_self">上一页</a>
<%
} //end if
if (curPage < pages && pages > 1) {
%>
| <a href="javascript:gotoPage(<%=(curPage+1)%>)" target="_self">下一页</a>
<%
} //end if
} catch (Exception e) {
%>
<!-- <%=e.getClass()%> 导致错误:<%=e.getMessage()%> -->
<%
} //end if
} //end if
%>
</body>
</html>
在线示例:PHP 手册(简体中文)。
在应用中加入全文检索功能
——基于Java的全文索引引擎Lucene简介
作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com
写于:2002/08 最后更新: 11/29/2006 17:23:30
Feed Back >> (Read this before you ask question)
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
http://www.chedong.com/tech/lucene.html
关键词:Lucene java full-text search engine Chinese word segment
内容摘要:
Lucene是一个基于Java的全文索引工具包。
- 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史
- 全文检索的实现:Luene全文索引和数据库索引的比较
- 中文切分词机制简介:基于词库和自动切分词算法的比较
- 具体的安装和使用简介:系统结构介绍和演示
- Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展
- 从Lucene我们还可以学到什么
Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。
Lucene的作者:Lucene的贡献者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。
Lucene的发展历程:早先发布在作者自己的www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/
已经有很多Java项目都使用了Lucene作为其后台的全文索引引擎,比较著名的有:
- Jive:WEB论坛系统;
- Eyebrows:邮件列表HTML归档/浏览/查询系统,本文的主要参考文档“TheLucene search engine: Powerful, flexible, and free”作者就是EyeBrows系统的主要开发者之一,而EyeBrows已经成为目前APACHE项目的主要邮件列表归档系统。
- Cocoon:基于XML的web发布框架,全文检索部分使用了Lucene
· Eclipse:基于Java的开放开发平台,帮助部分的全文索引使用了Lucene
对于中文用户来说,最关心的问题是其是否支持中文的全文检索。但通过后面对于Lucene的结构的介绍,你会了解到由于Lucene良好架构设计,对中文的支持只需对其语言词法分析接口进行扩展就能实现对中文检索的支持。
Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
比较一下Lucene和数据库:
Lucene | 数据库 |
索引数据源:doc(field1,field2...) doc(field1,field2...) | 索引数据源:record(field1,field2...) record(field1..) |
Document:一个需要进行索引的“单元” | Record:记录,包含多个字段 |
Field:字段 | Field:字段 |
Hits:查询结果集,由匹配的Document组成 | RecordSet:查询结果集,由多个Record组成 |
全文检索 ≠ like "%keyword%"
通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页,上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。
由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。
由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。
可以通过一下表格对比一下数据库的模糊查询:
| Lucene全文索引引擎 | 数据库 |
索引 | 将数据源中的数据都通过全文索引一一建立反向索引 | 对于LIKE查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。 |
匹配效果 | 通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 | 使用:like "%net%" 会把netherlands也匹配出来, |
匹配度 | 有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 | 没有匹配程度的控制:比如有记录中net出现5词和出现1次的,结果是一样的。 |
结果输出 | 通过特别的算法,将最匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 | 返回所有的结果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放这些临时结果集。 |
可定制性 | 通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) | 没有接口或接口复杂,无法定制 |
结论 | 高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 | 使用率低,模糊匹配规则简单或者需要模糊查询的资料量少 |
全文检索和数据库应用最大的不同在于:让最相关的头100条结果满足98%以上用户的需求
Lucene的创新之处:
大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文检索系统/应用的比较:
| Lucene | 其他开源全文检索系统 |
增量索引和批量索引 | 可以进行增量的索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。 | 很多系统只支持批量的索引,有时数据源有一点增加也需要重建索引。 |
数据源 | Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相应结构), | 很多系统只针对网页,缺乏其他格式文档的灵活性。 |
索引内容抓取 | Lucene的文档是由多个字段组成的,甚至可以控制那些字段需要进行索引,那些字段不需要索引,近一步索引的字段也分为需要分词和不需要分词的类型: | 缺乏通用性,往往将文档整个索引了 |
语言分析 | 通过语言分析器的不同扩展实现: | 缺乏通用接口实现 |
查询分析 | 通过查询分析接口的实现,可以定制自己的查询语法规则: |
|
并发访问 | 能够支持多用户的使用 |
|
对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。
首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。
但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢?
“北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。
另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:
"北京天安门" ==> "北京 京天 天安 安门"。
这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。
基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,
| 自动切分 | 词表切分 |
实现 | 实现非常简单 | 实现复杂 |
查询 | 增加了查询分析的复杂程度, | 适于实现比较复杂的查询语法规则 |
存储效率 | 索引冗余大,索引几乎和原文一样大 | 索引效率高,为原文大小的30%左右 |
维护成本 | 无词表维护成本 | 词表维护成本非常高:中日韩等语言需要分别维护。 |
适用领域 | 嵌入式系统:运行环境资源有限 | 对查询和存储效率要求高的专业搜索引擎 |
目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。
下载:http://jakarta.apache.org/lucene/
注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从https://javacc.dev.java.net/下载javacc。
lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
org.apache.Lucene.search/ | 搜索入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 语言分析器 |
org.apache.Lucene.queryParser/ | 查询分析器 |
org.apache.Lucene.document/ | 存储结构 |
org.apache.Lucene.store/ | 底层IO/存储结构 |
org.apache.Lucene.util/ | 一些公用的数据结构 |
简单的例子演示一下Lucene的使用方法:
索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,列表如下:
方法 | 切词 | 索引 | 存储 | 用途 |
Field.Text(String name, String value) | Yes | Yes | Yes | 切分词索引并存储,比如:标题,内容字段 |
Field.Text(String name, Reader value) | Yes | Yes | No | 切分词索引不存储,比如:META信息, |
Field.Keyword(String name, String value) | No | Yes | Yes | 不切分索引并存储,比如:日期字段 |
Field.UnIndexed(String name, String value) | No | No | Yes | 不索引,只存储,比如:文件路径 |
Field.UnStored(String name, String value) | Yes | Yes | No | 只全文索引,不存储 |
public class IndexFiles {
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc);
is.close();
};
//关闭写索引器
writer.close();
}
}
索引过程中可以看到:
- 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。
- Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
- 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。
检索过程和结果显示:
搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。
假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来,
public class Search {
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目录的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查询解析器:使用和索引同样的语言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索结果使用Hits存储
Hits hits = searcher.search(query);
//通过hits可以访问到相应字段的数据和查询的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。
简化的查询分析器
个人感觉lucene成为JAKARTA项目后,画在了太多的时间用于调试日趋复杂QueryParser,而其中大部分是大多数用户并不很熟悉的,目前LUCENE支持的语法:
Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")
中间的逻辑包括:and or + - &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,个人感觉对于一般应用来说,这些功能有一些华而不实,其实能够实现目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍是比较好的选择。
添加修改删除指定记录(Document)
Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。如何删除指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用IndexReader.delete(Termterm)方法通过这个记录ID删除相应的Document。
根据某个字段值的排序功能
lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常提到的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。而从全文检索的原理我们可以了解到,任何不基于索引的搜索过程效率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。
但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外的排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过程中访问不在索引中的某个字段值。
这里需要修改的是IndexSearcher中的HitCollector过程:
...
scorer.score(new HitCollector() {
private float minScore = 0.0f ;
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先:Lucene将docID和相应的匹配度score例入结果命中列表中:
* hq.put(new ScoreDoc(doc, score)); // update hit queue
* 如果用doc 或 1/doc 代替 score,就实现了根据docID顺排或逆排
* 假设数据源索引时已经按照某个字段排好了序,而结果根据docID排序也就实现了
* 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合。
*/
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) { // if hit queue overfull
hq.pop(); // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());
更通用的输入输出接口
虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个:
数据源: WORD PDF HTML DB other
/ | | | /
XML中间格式
|
Lucene INDEX
目前还没有针对MSWord文档的解析器,因为Word文档和基于ASCII的RTF文档不同,需要使用COM对象机制解析。这个是我在Google上查的相关资料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一个办法就是把Word文档转换成text:http://www.winfield.demon.nl/index.html
索引过程优化
索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。
Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索过程优化
lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。
http://www.onjava.com/lpt/a/3273
而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。
Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。
如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。
Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。
- 支持中文的Tokenizer:这里有2个版本,一个是通过JavaCC生成的,对CJK部分按一个字符一个TOKEN索引,另外一个是从SimpleTokenizer改写的,对英文支持数字和字母TOKEN,对中文按迭代索引。
- 基于XML数据源的索引器:XMLIndexer,因此所有数据源只要能够按照DTD转换成指定的XML,就可以用XMLIndxer进行索引了。
- 根据某个字段排序:按记录索引顺序排序结果的搜索器:IndexOrderSearcher,因此如果需要让搜索结果根据某个字段排序,可以让数据源先按某个字段排好序(比如:PriceField),这样索引后,然后在利用这个按记录的ID顺序检索的搜索器,结果就是相当于是那个字段排序的结果了。
Luene的确是一个面对对象设计的典范
- 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块而不需要;
- 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
- 所有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修改其他模块。
- 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。
这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。
此外,通过对Lucene的学习和使用,我也更深刻地理解了为什么很多数据库优化设计中要求,比如:
- 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
- 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
- 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
- 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的操作。
参考资料:
Apache: Lucene Project
http://jakarta.apache.org/lucene/
Lucene开发/用户邮件列表归档
Lucene-dev@jakarta.apache.org
Lucene-user@jakarta.apache.org
The Lucene search engine: Powerful, flexible, and free
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html
Lucene Tutorial
http://www.darksleep.com/puff/lucene/lucene.html
Notes on distributed searching with Lucene
http://home.clara.net/markharwood/lucene/
中文语言的切分词
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment
搜索引擎工具介绍
http://searchtools.com/
Lucene作者Cutting的几篇论文和专利
http://lucene.sourceforge.net/publications.html
Lucene的.NET实现:dotLucene
http://sourceforge.net/projects/dotlucene/
Lucene作者Cutting的另外一个项目:基于Java的搜索引擎Nutch
http://www.nutch.org/ http://sourceforge.net/projects/nutch/
关于基于词表和N-Gram的切分词比较
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
2005-01-08
Cutting在Pisa大学做的关于Lucene的讲座:非常详细的Lucene架构解说
特别感谢:
前网易CTO许良杰(Jack Xu)给我的指导:是您将我带入了搜索引擎这个行业。
原文出处:<ahref="http://www.chedong.com/tech/lucene.html">http://www.chedong.com/tech/lucene.html</a>
文章类别: 程序 技术 — SuperTaoer @ 2:40 am
一,weblucene的简介
Web Lunce是中国人车东在sourceforge里面的一个项目,详细的地址在http://www.chedong.com/tech/weblucene.html。这个项目很好的解决对中文的支持
二, 基本原理我就不用介绍了,参照作者的文档。
三,碰到的问题
1,编译的时候需要指定javacc的路径,并且只支持javacc2.0,由于发布的是linux版,所以需要修改build.property里面的值对应的路径。
2,生存index目录
发布的index的命令在dump目录中的index.sh,如果是windows的话就需要按照里面内容重新建立index.cmd文件,并且修改相关的环境变量和值
3,把生成的index目录拷贝到tomcat的webapps路径下面
4,在查询的时候会出现中文问题,产生的原因主要是由于字符集引起的,解决这个办法可以把提交的方法修改为post就可以了。需要修改几个地方
1),WebLuceneServlet.java增加doPost方法
2),修改index.html文件,把method中的get修改为post
3),修改var/searchbox.xsl文件,把method中的get修改为post
XML 与 Java 技术: 用 Castor 进行数据绑定 #
对于主要关心文档数据内容的应用程序,Java XML 数据绑定是一种代替 XML 文档模型的强大机制。本文中,企业 Java 专家 Dennis Sosnoski 介绍数据绑定,并讨论什么使它如此令人瞩目。然后,他向读者展示了如何利用 Java 数据绑定的开放源代码 Castor 框架来处理日益复杂的文档。如果您的应用程序更多的把 XML 作为数据而不是文档,您就会愿意了解这种处理 XML 和 Java 技术的简单有效的方法。
.LUNCE有几种语言分析器,其特点见另外一篇文章。import org.apache.lucene.analysis.standard.StandardAnalyzer;这个类可以进行中文分词,但是效果不是很好。