第一章 接触
Lucene
本章包括
n
理解
Lucene
n
使用基本的索引
API
n
使用搜索
API
n
考虑可替换产品
Lucene流行和成功的一个关键因素是它的简单。
… …
1.1 信息组织和访问的演化
1.2 理解
Lucene
不同的人使用不同的方法解决相同的问题
—即信息超负荷问题。一些人使用新的用户接口来工作,一些使用智能代理,还有一些使用发展较为成熟的搜索工具如Lucene。本章稍后我们展示代码示例之前,我们将提供给你一张高层次的图来说明Lucene是什么,它不是什么和它以后会变得怎样。
1.2.1 Lucene是什么
Lucene是一个高性能、可伸缩的信息搜索
(IR)库。它使你可以为你的应用程序添加索引和搜索能力。Lucene是用java实现的成熟的、免费的开源项目,是著名的Apache Jakarta大家庭的一员,并且基于在Apache软件许可 [ASF, License]。同样,
Lucene是当前与近几年内非常流行的免费的Java信息搜索(IR)库。
注意 贯穿这本书,我们将使用术语
IR(Information Retrieval)来描述像Lucene这样的搜索工具。人们常常将IR库归诸于搜索引擎,但是一定不要将IR库与web搜索引擎混为一谈。
正如你马上就会发现的,
Lucene提供了一组简单却足够强大的核心API,只需要最小限度地理解全文索引和搜索。你只须学习它的几个类从而把Lucene集成到一个应用程序中。因为Lucene是一个Java库,它并不限定要索引和搜索的内容,这使得它比其它一些搜索程序更具有优势。
刚接触
Lucene的人经常把它误解为一个现成的程序,类似文件搜索程序或web网络爬行器或是一个网站的搜索引擎。那些都不是Lucene:Lucene是一个软件库,一个开发工具包(如果你想这样称呼),而不是一个具有完整特征的搜索应用程序。它本身只关注文本的索引和搜索,并且这些事它完成的非常好。Lucene使得你的应用程序只针对它的问题域来处理业务规则,而把复杂的索引和搜索实现隐藏在一组简单易用的API之后。你可以把Lucene认为成一层,应用程序位于它之上,如图1.5所示。
图
1.5 一个集成Lucene的典型应用
大量基于
Lucene的完整的搜索程序已经构建出来。如果你正在寻找预创建的东西或是一个抓取、文档处理和搜索的框架,请参考Lucene Wiki 的“powered by”页
(http://wiki.apache.org/jakarta-lucene/PoweredBy)以获得更多选择:Zilverling、SearchBlox、Nutch、LARM和jSearch,还有其它一部分的命名。Nutch和SearchBlox的案例研究包含在第10章。
1.2.2 Lucene能做什么
Lucene使你可以为你的应用程序添加索引和搜索能力
(这些功能将在1.3节中描述)。Lucene可以索引并能使得可以转换成文本格式的任何数据能够被搜索。在图1.5可以看出,Lucene并不关心数据的来源、格式甚至它的语言,只要你能将它转换为文本。这就意味着你可经索引并搜索存放于文件中的数据:在远程服务器上的web页面,存于本地文件系统的文档,简单的文本文件,微软Word文档,HTML或PDF文件或任何其它能够提取出文本信息的格式。
同样,利用
Lucene你可以索引存放于数据库中的数据,提供给用户很多数据库没有提供的全文搜索的能力。一旦你集成了Lucene,你的应用程序的用户就能够像这样来搜索:+George +Rice –eat –pudding, Apple –pie +Tiger, animal:monkey AND food:banana等等。利用Lucene,你可以索引和搜索
email邮件,邮件列表档案,即时聊天记录,你的Wiki页面……等等更多。
1.2.3 Lucene的历史
Lucene最初是由
Doug Cutting开发的,在SourceForge的网站上提供下载。在2001年9月做为高质量的开源Java产品加入到Apache软件基金会的Jakarta家族中。随着每个版本的发布,这个项目得到明显的增强,也吸线了更多的用户和开发人员。2004年7月,Lucene1.4版正式发布,10月的1.4.2版本做了一次bug修正。表1.1显示了Lucene的发布历史。
表1.1 Lucene的发布历史
版本
|
发布日期
|
里程碑
|
0.01
|
2000年3月
|
第一个开源版本(SourceForge)
|
1.0
|
2000年10月
|
|
1.01b
|
2001年7月
|
最后的SourceForge版本
|
1.2
|
2002年6月
|
第一个Apache Jakarta版本
|
1.3
|
2003年12月
|
复合索引格式,查询分析器增加,远程搜索,token定位,可扩展的API
|
1.4
|
2004年7月
|
Sorting, span queries, term vectors
|
1.4.1
|
2004年8月
|
排序性能的bug修正
|
1.4.2
|
2004年10月
|
IndexSearcher optimization and misc. fixes
|
1.4.3
|
2004年冬
|
Misc. fixes
|
注意 Lucene的创建者,
Doug Cutting,在信息搜索领域有很强的理论和实践经验。他发表过许多IR主题相关的研究论文并曾在Excite、Apple和Grand Central等公司工作。最近,考虑到web搜索引擎数目的减少和这个领域的潜在垄断,他创建了Nutch,第一个开源的万维网搜索引擎(http://www.nutch.org),它用来处理抓取、索引和搜索数十亿时常更新的网页。毫不奇怪,Lucene是Nutch的核心,10.1节包括Nutch如何使用Lucene的案例研究。
Doug Cutting 仍然是
Lucene的后台主力,但是自从Lucene加入到Apache Jakarta的庇护之后,更多的聪明智慧注入进来。在本书写作时,Lucene的核心工作组有数个积极的开发者,其中两位就是本书的作者。除了官方的项目开发人员,Lucene拥有大量积极的技术用户群,经常贡献补丁,Bug修复和新的特征。
1.2.4 谁在使用
Lucene
谁不使用呢?除了在
Lucene Wiki的Powered by Lucene页提到的那些组织外,还有大量的知名的跨图组织正在使用Lucene。它为Eclipse IDE、Encyclopedia Britannica CD-ROM/DVD、FedEx、Mayo Clinic、Hewlett-Packard、New Scientist杂志、Epiphany、MIT的OpenCourseware和Dspace、Akamai的EdgeComputing平台等等提供搜索能力。你的名字也将会出现在这个列表中。
1.2.5 Lucene其它版本:
Perl, Python, C++, .NET, Ruby
判断一个开源软件是否成功的一种方法是通过它被改编为其它语言版本的数量。使用这个标准,
Lucene是非常成功的!尽管开始时Lucene是用Java写的,Lucene已经有很多其它语言的版本了:Perl,Python,C++和.NET,并且一些基础已经用Ruby实现了。这对于那些需要访问用不同的语言写成的应用程序所得到的Lucene索引的开发者来说是个好消息。在第9章你将了解更多关于这方面的东西。
1.3 索引和搜索
所有搜索引擎的核心就是索引的概念:将原始数据处理成一个高效的交差引用的查找结构以便于快速的搜索。让我们对索引和搜索过程做一次快速的高层次的浏览。
1.3.1 什么是索引,为什么它很重要?
想像一下,你需要搜索大量的文件,并且你想找出包含一个指定的词或短语的文件。你如何编写一个程序来做到这个?一个幼稚的方法是针对给定的词或短语顺序扫描每个文件。这个方法有很多缺点,最明显的就是它不适合于大量的文件或者文件非常巨大的情况。这时就出现了索引:为了快速搜索大量的文本,你必须首先索引那个文本然后把它转化为一个可以让你快速搜索的格式,除去缓慢的顺序地扫描过程。这个转化过程称为索引,它的输出称为一条索引。
你可以把索引理解为一个可以让你快速随机访问存于其内部的词的数据结构。它隐含的概念类似于一本书最后的索引,可以让你快速找到讨论指定主题的页面。在
Lucene中,一个索引是一个精心设计的数据结构,在文件系统中存储为一组索引文件。我们在附录B中详细地说明了索引文件的结构,但是目前你只须认为Lucene的索引是一个能快速的词汇查找的工具。
1.3.2 什么是搜索?
搜索是在一个索引中查找单词来找出它们所出现的文档的过程。一个搜索的质量用精确度和召回率来描述。召回率衡量搜索系统搜索到相关文档的能力,精确度衡量系统过滤不相关文档的能力。然而,在考虑搜索时你必须考虑其它一些因素。我们已经提到速度和快速搜索大量文本的能力。支持单个和多个词汇的查询,短语查询,通配符,结果分级和排序也是很重要的,在输入这些查询的时候也是友好的语法。
Lucene强大的软件库提供了大量的搜索特征、bells和whistles,所以我们不得不把关于搜索的讨论展开为三章(第3、5、6章)。
1.4 Lucene实战:一个简单的程序
让我们来实战
Lucene。首先回忆在1.3.1节描述的索引和搜索文件的问题。此外,假设你要索引和搜索存放于一个目录树中的文件,并不只在一个目录中。为了向你展示Lucene的索引和检索能力,我们将用到两个命令行程序:Indexer和Searcher。首先我们将索引一个包含文本文件的目录树,然后我们搜索创建的索引。
这个示例程序将使你熟悉
Lucene的API,简单易用而功能强大。代码清单是完整的,现成的命令行程序。如果文件索引/搜索是你要解决的问题,那你可复制一份代码,用它来适应你的需要。在接下来的章节中,我们将更深入的描述Lucene使用中的每个方面。
在我们可以利用
Lucene搜索之前,需要创建一个索引,所以我们开始Indexer程序。
1.4.1 创建一个索引
在本节中,你将看到一个名为
Indexer的类和它的四个静态方法。它们共同递归遍历文件系统目录并索引所有具有.txt扩展名的文件。当Indexer执行完毕时,为它的后续Searcher(在1.4.2小节中介绍)留下一个创建好的Lucene索引。
我们不期望你熟悉例子中用到的几个
Lucene类和方法,我们马上就会解释它们。在有注释的代码列表之后,我们向你展示了如何使用Indexer。如果你感觉在看到编码之前学习Indexer如何使用很有帮助,直接跳到代码后面的用法讨论部分。
使用Indexer来索引文本文件
列表
1.1展示了Indexer命令行程序。它用到两个参数:
n
我们存放
Lucene索引的路径
n
包含我们要索引的文本文件的路径
列表 1.1 Indexer:遍历文件系统并且索引.txt文件
/**
* This code was originally written for
* Erik’s Lucene intro java.net article
*/
public class Indexer {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new Exception(“Usage: java ” + Indexer.class.getName()
+ “ <index dir> <data dir>
);
}
File indexDir = new File(args[0]);
File dataDir = new File(args[1]);
long start = new Data().getTime();
int numIndexed = index(indexDir, dataDir);
long end = new Date().getTime();
System.out.println(“Indexing ” + numIndexed + “ files took ”
+ (end - start) + “ milliseconds”);
}
// open an index and start file directory traversal
public static int index(File indexDir, File dataDir)
throws IOException {
if (!dataDir.exists() || !dataDir.isDirectory()) {
throw new IOException(dataDir
+ “ does not exist or is not a directory”);
}
IndexWriter writer = new IndexWriter(indexDir, ① 创建Lucene索引
new StandardAnalyzer(), true);
writer.setUseCompoundFile(false);
indexDirectory(writer, dataDir);
int numIndexed = writer.docCount();
writer.optimize();
writer.close();
return numIndexed;
}
// recursive method that calls itself when it finds a directory
private static void indexDirectory(IndexWriter writer, File dir)
throws IOException {
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files
;
if (f.isDirectory()) {
indexDirectory(writer, f);
② 递归
} else if (f.getName().endsWith(“.txt”)) {
indexFile(writer, f);
}
}
}
// method to actually index file using Lucene
private static void indexFile(IndexWriter writer, File f)
throws IOException {
if (f.isHidden() || !f.exists() || !f.canRead()) {
return;
}
System.out.println(“Indexing ” + f.getCanonicalPath());
Document doc = new Document();
doc.add(Field.Text(“contents”, new FileReader(f)));
③ 索引文件
内容
doc.add(Field.Keyword(“filename”, f.getCannicalPath()));
④ 索引
文件名称
writer.addDocument(doc);
⑤ 添加片段到Lucene索引
}
}
有趣的是,代码的大部分是执行目录的遍历(
②)。只有IndexWriter的创建和关闭(
①)和IndexFile方法中的四行(
③,
④,
⑤)使用了Lucene API—有效的6行代码。
这个示例只关注.txt扩展名的文本文件是为了在说明Lucene的用法和强大功能时保持尽量简单。在第7章,我们将向你展示如何处理非文本文件,并且我们开发了一个现成的小框架来分析和索引几种常见的格式的文档。
运行Indexer
在命令行中,我们针对包含Lucene本身的源文件的本地工作目录运行Indexer。我们使Indexer索引/lucene目录下的文件并将Lucene 索引保存在build/index目录中。
% java lia.meetlucene.Indexer build/index /lucene
Indexing /lucene/build/test/TestDoc/test.txt
Indexing /lucene/build/test/TestDoc/test2.txt
Indexing /lucene/BUILD.txt
Indexing /lucene/CHANGES.txt
Indexing /lucene/LICENSE.txt
Indexing /lucene/README.txt
Indexing /lucene/src/jsp/README.txt
Indexing /lucene/src/test/org/apache/lucene/analysis/ru/
→
stemsUnicode.txt
Indexing /lucene/src/test/org/apache/lucene/analysis/ru/
→
test1251.txt
Indexing /lucene/src/test/org/apache/lucene/analysis/ru/
→
testKOI8.txt
Indexing /lucene/src/test/org/apache/lucene/analysis/ru/
→
testUnicode.txt
Indexing /lucene/src/test/org/apache/lucene/analysis/rn/
→
wordsUnicode.txt
Indexing /lucene/todo.txt
Indexing 13 files took 2205 milliseconds
Indexer打印出索引的文件名称,你可以看出它只索引扩展名为.txt的文本文件。
注意 如果你在Windows平台的命令行中运行这个程序,你需要调整命令行的目录和路径分割符。Windows命令行是java build/index c:/lucene。
当索引完成后,Indexer输出它索引的文件数目和所花费的时间。因为报告的时间包含文件目录遍历和索引,你不能把它做为一个正式的性能衡量依据。在我们的示例中,每个索引的文件都很小,但只有了2秒索引这些文件还是不错的。
索引速度是要关注的,我们将在第2章中讨论它。但是通常,搜索更加重要。
1.4.2 在索引中搜索
在Lucene中搜索和索引一样高效和简单。它的功能惊人地强大,在第3章和第5章你将看到。现在,让我们看一下Searcher,一个我们用来搜索Indexer创建的索引的命令行程序。(记住我们的Seacher只是用来示范Lucene的搜索API的用法。你的搜索程序也可以是网页或带有GUI的桌面程序或EJB等形式。)
在上一部分,我们索引了一个目录中的文本文件。在本例中的索引,放在文件系统的一个目录中。我们让Indexer在build/index目录中创建Lucene索引,这个目录和我们调用Indexer的目录相关。在列表1.1中看出,这个索引包含被索引的文件和它们的绝对路径。现在我们要用Lucene来搜索这个索引以找出包含指定文本片段的文件。例如,我们可能想找出包含关键字java或Lucene的所有文件,或者可能想找出包含短语“system requirements”的所有文件。
使用Searcher实现搜索
Searcher程序和Indexer相辅相成并提供命令行搜索的能力。列表1.2展示了Searcher的全部代码。它接受两个命令行参数:
n
Indexer创建的索引的路径
n
搜索索引的查询
列表
1.2 Searcher:为参数传来的查询搜索Lucene索引
/**
* This code was originally written for
* Erik’s Lucene intro java.net article
*/
public class Searcher {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new Exception(“Usage: java ” + Searcher.class.getName()
+ “ <index dir> <auery>”);
}
File indexDir = new File(args[0]);
String q = args[1];
if (!indexDir.exists() || !indexDir.isDirectory()) {
throw new Exception(indexDir +
“ does not exist or is not a directory.”);
}
search(indexDir, q);
}
public static void search(File indexDir, String q)
throws Exception {
Directory fsDir = FSDirectory.getDirectory(indexDir, false);
IndexSearcher is = new IndexSearcher(fsDir); ① 打开索引
Query query = QueryParser.parse(q, “contents”, ② 分析查询
new StandardAnalyzer());
long start = new Date().getTime();
Hits hits = is.search(query); ③ 搜索索引
long end = new Date().getTime();
System.err.println(“Found ” + hits.length() +
“ document(s) (in ” + (end - start) +
“ milliseconds) that matched query ‘” +
q + “’:”);
for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i); ④ 得到匹配的文档
System.out.println(doc.get(“filename”));
}
}
}
Searcher类似于
Indexer,只有几行代码与Lucene相关。在search方法中出现了几种特别的事物,
① 我们使用
Lucene的IndexSearcher和FSDirectory类来打开我们的索引以进行搜索。
② 我们使用
QueryParser来把human-readable查询分析成Lucene的查询类。
③ 搜索以一个
Hits对象的形式返回结果集。
④ 注意
Hits对象包含的仅仅是隐含的文档的引用。换句话说,不是在搜索的时候立即加载,而是采用从索引中惰性加载的方式—仅当调用hits.doc(int)时。
运行Searcher
让我们运行
Searcher并用‘lucene’查询在索引中找出几个文档:
%java lia.meetlucene.Searcher build/index ‘lucene’
Found 6 document(s) (in 66 milliseconds) that matched query ‘lucene’:
/lucene/README.txt
/lucene/src/jsp/README.txt
/lucene/BUILD.txt
/lucene/todo.txt
/lucene/LICENSE.txt
/lucene/CHANGES.txt
输出显示我们用
Indexer索引的13个文档中的6个含有lucene这个单词,而且这次搜索花费66毫秒。因为Indexer在索引中存放了文件的绝对路径,Searcher可以输出它们。在这个例子中,我们决定把文件和路径存为一个字段并没有考虑什么,但是以Lucene的观点,可以给要索引的文档附加任意元数据。
当然,你可以使用更多复杂的查询,例如‘lucene AND doug’或者‘lucene AND NOT slow’或‘+lucene +book’等等。第
3、5和第6章所有搜索的不同方面,包括Lucene的查询语法。
使用xargs工具
Searcher类对
Lucene的搜索特征的非常简化的示例。所以,它仅仅把匹配结果输出到标准输出上。然而,Searcher还有另一个技巧。考虑你需要找出含有指定的关键词或短语的文件,并且你想以某种方式处理这些匹配文件。为了保持简单性,让我们考虑你想使用UNIX命令ls列出每个匹配的文件,或许看看该文件的大小、许可位或拥有者。既然已经简单地把匹配文档的路径写到标准输出上,又把统计输出写到了标准错误上,你可以利用UNIX的xargs工具来处理匹配文件,如下:
%java lia.meetlucene.Searcher build/index
→
‘lucene AND NOT slow’ | xargs ls –l
Found 6 document(s) (in 131 milliseconds) that
→
matched query ‘lucene AND NOT slow’:
-rw-r--r-- 1 erik staff 4215 10 Sep 21:51 /lucene/BUILD.txt
-rw-r--r-- 1 erik staff 17889 28 Dec 10:53 /lucene/CHANGES.txt
-rw-r--r-- 1 erik staff 2670 4 Nov 2001 /lucene/LICENSE.txt
-rw-r--r-- 1 erik staff 683 4 Nov 2001 /lucene/README.txt
-rw-r--r-- 1 erik staff 370 26 Jan 2002 /lucene/src/jsp/
→
README.txt
-rw-r--r-- 1 erik staff 943 18 Sep 21:27 /lucene/todo.txt
在这个例子中,我们选择布尔查询‘lucene AND NOT slow’来找出所有含有单词lucene但不含有单词
slow的文件。这个查询花费了131毫秒并找出6个匹配文件。我们把Searcher的输入传递给xargs命令,它将会依次使用ls –l命令来列出每个匹配的文件。与之类似,这些匹配文件可以被复制、连接、email或打印到标准输出。
我们的索引和搜索应用程序示例展示了
Lucene的优点。它的API使用简洁。代码的大小(并且这适用于所有使用Lucene的程序)与业务目的密切相关--在这个示例中,是Indexer中负责寻找文本文件的文件系统爬行器和Searcher中打印某一查询的匹配文件名称到标准输出的代码。但是别让这个事实或者这个示例的简单让你觉得自满:在Lucene的表面下有相当多的事情发生,而且我们还用到了一些最好的实践经验。为了更好的使用Lucene,更深入地理解它如何工作及如何在需要的时候扩展它是很重要的。本书作者毫无保留地给出了这些。
1.5 理解核心索引类
在
Indexer类中可见,你需要以下类来执行这个简单的索引过程:
n
IndexWriter
n
Directory
n
Analyzer
n
Document
n
Field
接下来是对这些类的一个简短的浏览,针对它们在
Lucene的角色,给出你粗略的概念。我们将在整本书中使用这些类。
1.5.1 IndexWriter
IndexWriter是在索引过程中的中心组件。这个类创建一个新的索引并且添加文档到一个已有的索引中。你可以把
IndexWriter想象成让你可以对索引进行写操作的对象,但是不能让你读取或搜索。不管它的名字,IndexWriter不是唯一的用来修改索引的类,2.2小节描述了如何使用Lucene API来修改索引。
1.5.2 Directory
Directory类代表一个
Lucene索引的位置。它是一个抽象类,允许它的子类(其中的两个包含在Lucene中)在合适时存储索引。在我们的Indexer示例中,我们使用一个实际文件系统目录的路径传递给IndexWriter的构造函数来获得Directory的一个实例。IndexWriter然后使用Directory的一个具体实现FSDirectory,并在文件系统的一个目录中创建索引。
在你的应用程序中,你可能较喜欢将
Lucene索引存储在磁盘上。这时可以使用FSDirectory,一个包含文件系统真实文件列表的Driectory子类,如同我们在Indexer中一样。
另一个
Directory的具体子类是RAMDirectory。尽管它提供了与FSDirectory相同的接口,RAMDirectory将它的所有数据加载到内存中。所以这个实现对较小索引很有用处,可以全部加载到内存中并在程序关闭时销毁。因为所有数据加载到快速存取的内存中而不是在慢速的硬盘上,RAMDirectory适合于你需要快速访问索引的情况,不管是索引或搜索。做为实例,Lucene的开发者在所有他们的单元测试中做了扩展使用:当测试运行时,快速的内存驻留索引被创建搜索,当测试结束时,索引自动销毁,不会在磁盘上留下任何残余。当然,在将文件缓存到内存的操作系统中使用时RAMDirectory和FSDirectory之间的性能差别较小。你将在本书的代码片断中看到Directory的两个实现的使用。
1.5.3 Analyzer
在文本索前之前,它先通过
Analyzer。Analyzer在IndexWriter的构造函数中指定,司职对文本内容提取关键词并除去其它的。如果要索引的内容不是普通的文本,首先要转化成文本,如果2.1所示。第7章展示了如何从常见的富媒体文档格式中提取文本。Analyzer是个抽象类,但是Lucene中有几个它的实现。有的处理的时候跳过终止词(不能用来把某个文件与其它文件区分开的常用的词);有的处理时把关键字转化为小写字母,所以这个搜索不是大小写敏感等等。Analyzer是Lucene的一个重要的部分并且不只是在输入过滤中使用。对一个将Lucene集成到应用程序中的开发者来说,对Analyzer的选择在程序设计中是重要元素。你将在第4章学到更多有关的知识。
1.5.4 Document
一个
Document代表字段的集合。你可以把它想象为以后可获取的虚拟文档—一块数据,如一个网页、一个邮件消息或一个文本文件。一个文档的字段代表这个文档或与这个文档相关的元数据。文档数据的最初来源(如一条数据库记录、一个Word文档、一本书的某一章等等)与Lucene无关。元数据如作者、标题、主题、修改日期等等,分别做为文档的字段索引和存储。
注意 当我们在本书中提到一个文档,我们指一个Microsoft Word、RTF、PDF或其它文档类型;我们不是谈论Lucene的Document类。注意大小写和字体的区别。
Lucene只用来处理文本。
Lucene的核心只能用来处理java.lang.String和java.io.Reader。尽管很多文档类型都能被索引并使之可搜索,处理它们并不像处理可以简单地转化为java的String或Reader类型的纯文本内容那样直接。你将在第7章学到处理非文本文档。
在我们的
Indexer中,我们处理文本文件,所以对我们找出的每个文本文件,创建一个Document类的实例,用Field(字段)组装它,并把这个Document添加到索引中,完成对这个文件的索引。
1.5.5 Field
在索引中的每个
Document含有一个或多个字段,具体化为Field类。每个字段相应于数据的一个片段,将在搜索时查询或从索引中重新获取。
Lucene提供四个不同的字段类型,你可以从中做出选择:
n
Keyword—不被分析,但是被索引并逐字存储到索引中。这个类型适合于原始值需要保持原样的字段,如
URL、文件系统路径、日期、个人名称、社会安全号码、电话号码等等。例如,我们在Indexer(列表1.1)中把文件系统路径作为Keyword字段。
n
UnIndexed—不被分析也不被索引,但是它的值存储到索引中。这个类型适合于你需要和搜索结果一起显示的字段
(如URL或数据库主键),但是你从不直接搜索它的值。因为这种类型字段的原始值存储在索引中,这种类型不适合于存放比较巨大的值,如果索引大小是个问题的话。
n
UnStored—和
UnIndexed相反。这个字段类型被分析并索引但是不存储在索引中。它适合于索引大量的文本而不需要以原始形式重新获得它。例如网页的主体或任休其它类型的文本文档。
n
Text—被分析并索引。这就意味着这种类型的字段可以被搜索,但是要小心字段大小。如果要索引的数据是一个
String,它也被存储;但如果数据(如我们的Indexer例子)是来自一个Reader,它就不会被存储。这通常是混乱的来源,所以在使用Field.Text时要注意这个区别。
所有字段由名称和值组成。你要使用哪种字段类型取决于你要如何使用这个字段和它的值。严格来说,
Lucene只有一个字段类型:以各自特征来区分的字段。有些是被分析的,有些不是;有些是被索引,然面有些被逐字地存储等等。
表
1.2提供了不同字段特征的总结,显示了字段如何创建以及基本使用示例。
表
1.2 不同字段类型的特征和使用方法
Fied method/type
|
Analyzed
|
Indexed
|
Stored
|
Example usage
|
Field.Keyword(String,String)
Field.Keyword(String,Date)
|
|
?
|
?
|
Telephone and Social Security numbers, URLs, personal names, Dates
|
Field.UnIndexed(String,
String)
|
|
|
?
|
Document type (PDF, HTML, and so on), if not used as search criteria
|
Field.UnStored(String,String)
|
?
|
?
|
|
Document titles and content
|
Field.Text(String,String)
|
?
|
?
|
?
|
Document titles and content
|
Field.Text(String,Reader)
|
?
|
?
|
|
Document titles and content
|
注意所有字段类型都能用代表字段名称和它的值的两个
String来构建。另外,一个Keyword字段可以接受一个String和一个Date对象,Text字段接受一个String和一个Reader对象。在所有情况下,这些值在被索引之前都先被转化成Reader,这些附加方法的存在可以提供比较友好的API。
注意 注意Field.Text(String, String)和Field.Text(String, Reader)之间的区别。String变量存储字段数据,而Reader变量不存储。为索引一个String而又不想存储它,可以用Field.UnStored(String, String)。
最后,
UnStored和Text字段能够用来创建词向量(高级的话题,在5.7节中描述)。为了让Lucene针对指定的UnStored或Text字段创建词向量,你可以使用Field.UnStored(String, String, true),Field.Text(String, String, true)或Field.Text(String, Reader, true)。
在使用
Lucene来索引时你会经常用到这几个类。为了实现基本的搜索功能,你还需要熟悉同样简单的几个Lucene搜索类。
1.6 理解核心搜索类
Lucene提供的基本搜索接口和索引的一样直接。只需要几个类来执行基本的搜索操作:
n
IndexSearcher
n
Term
n
Query
n
TermQuery
n
Hits
接下来的部分对这些类提供一个简要的介绍。我们将在深入更高级主题之前,在接下来的章节中展开这些解释。
1.6.1 IndexSearcher
IndexSearcher用来搜索而
IndexWriter用来索引:暴露几个搜索方法的索引的主要链接。你可以把IndexSearcher想象为以只读方式打开索引的一个类。它提供几个搜索方法,其中一些在抽象基类Searcher中实现;最简单的接受单个Query对象做为参数并返回一个Hits对象。这个方法的典型应用类似这样:
IndexSearcher is = new IndexSearcher(
FSDirectory.getDirectory(“/tmp/index”, false));
Query q = new TermQuery(new Term(“contents”, “lucene”));
Hits hits =
is.search(q);
我们将在第
3章中描述IndexSearcher的细节,在第5、6章有更多信息。
1.6.2 Term
Term是搜索的基本单元。与
Field对象类似,它由一对字符串元素组成:字段的名称和字段的值。注意Term对象也和索引过程有关。但是它们是由Lucene内部生成,所以在索引时你一般不必考虑它们。在搜索时,你可能创建Term对象并TermQuery同时使用。
Query q = new TermQuery(
new Term(“contents”, “lucene”));
Hits hits = is.search(q);
这段代码使
Lucene找出在contents字段中含有单词lucene的所有文档。因为TermQuery对象继承自它的抽象父类Query,你可以在等式的左边用Query类型。
1.6.3 Query
Lucene中包含一些
Query的具体子类。到目前为止,在本章中我们仅提到过最基本的Lucene Query:TermQuery。其它Query类型有BooleanQuery,PhraseQuery, PrefixQuery, PhrasePrefixQuery, RangeQuery, FilteredQuery和SpanQuery。所有这些都在第3章描述。Query是最基本的抽象父类。它包含一些通用方法,其中最有趣的是setBoost(float),在第3.5.9小节中描述。
1.6.4 TermQuery
TermQuery是
Lucene支持的最基本的查询类型,并且它也是最原始的查询类型之一。它用来匹配含有指定值的字段的文档,这在前几段只已经看到。
1.6.5 Hits
Hits类是一个搜索结果
(匹配给定查询的文档)文档队列指针的简单容器。基于性能考虑,Hits的实例并不从索引中加载所有匹配查询的所有文档,而是每次一小部分。第3章描述了其中的细节。
1.7 其它类似的搜索产品
在你选择
Lucene做为你的IR库之前,你可能想看看相同领域中的其它方案。我们对你可能相考虑的其它方案做了研究,这个小节对我们的发现做了总结。我们将这些产品分成两大类:
n
信息搜索
(IR, Information Retrieval)库
n
索引和搜索程序
第一组比较小;它由一些比
Lucene小的全文索引和搜索库组成。你可以把这个组的产品嵌入到你的程序中,如前面的图1.5所示。
第二组,比较大的组由一些现成的索引的搜索软件组成。这个软件一般设计为针对某种特定的数据,如网页,不如第一组的软件灵活。然而,其中一些产品也提供了它们的底层
API,所以有时你也可以把它们当做IR库。
1.7.1 IR库
在我们对本章的研究中,我们发现两个
IR库—Egothor和Xapian—提供了差不多的特征集合并且基本上都是辅助开发者的。我们也发现了MG4J,它并不是一个IR库而是一套创建IR库的有用工具;我们认为使用IR的开发者应该了解它。这里是我们对这三种产品的评论。
Egothor
一个全文索引和搜索的
Java库,Egothor的核心算法与Lucene类似。它已经存在了很多年并拥有少量积极的开发者和用户团体。领头人是捷克工程师Leo Galambos,一个在IR领域有深厚理论背景的博士研究生。他时常参与Lucene用户和开发者邮件列表的讨论。
Egothor提供一个扩展的
Boolean模块,使得它起到纯Boolean模块和Vector模块的作用。你可以通过一个查询时参数来选择使用哪个模块。这个软件有大量不同的查询类型,支持类似的查询语法,并允许多线程查询,如果你工作在多CPU计算机或搜索远程索引时是相当简单的。
Egothor多以现成的程序如网络爬行器
… …
1.7.2 索引和搜索程序
另一组可用的软件,包括免费的和商业的,包装成打包好的产品。这些软件通常不暴露大量的
API且不让你基于它构建定制的软件。其中大部分提供了一种机制使你控制有限的参数集合,但却不能以预期的方法来使用这个软件。(当然,也有些特殊情况。)
这样,我们不能把这种软件直接和
Lucene相比。然而,其中一些产品可能对你的需求来说是足够的,并能让你的工作运转起来,尽量Lucene或其它的IR库以长期角度来说是个不错的选择。这里是此类产品中比较流行的几种:
n
Glimpse和
Webglimpse—http://webglimpse.net/
n
Namazu—
http://www.namazu.org/
n
ht://Dig—
http://www.htdig.org/
n
Harvest和
Harvest-NG—http://www.sourceforge.net/projects/harvest/,http://
webharvest.sourceforge.net/ng/
webharvest.sourceforge.net/ng/
n
Microsoft Index Server—
http://www.microsoft.com/NTServer/techresources/webserv/
IndxServ.asp
IndxServ.asp
n
Verity—
http://www.verity.com/
1.7.3 在线资源
上一小节只是对相关产品的总的看法。很多资源可以帮你找到的其它的
IR库和产品:
n
DMOZ—在
DMOZ Open Directory Project(ODP)中,你将发现http://dmoz.org/Computers/
Software/Information_Retrieval/和它的子版块有大量的信息。
Software/Information_Retrieval/和它的子版块有大量的信息。
n
Google—尽管
Google Directory是基于Open Directory的数据,这两个目录确实不一样。所以你也应该访问http://directory.google.com/Top/Computers/Software/
Information_Retrival/。
Information_Retrival/。
n
Searchtools—有个搜索工具的专门的网站
http://www.searchtools.com/。这个网站并不时常更新,但是它已经很多年了并且相当广泛。软件根据操作系统、编程语言、许可证等等进行分类。如果你仅仅对用Java写的搜索软件感兴趣,访问http://www.searchtools.com/toos
/tools-java.html。
/tools-java.html。
我们提供了一些
Lucene的替代品的正面评论,但是我们确信你的工作会让你觉得Lucene才是最好的选择。
1.8 总结
在本章中,你获得了
Lucene的一些基本知识。现在你知道了Lucene是一个IR库而不是现成的产品,当然也不是Lucene的初识者常常认为的web爬行器。你也了解了Lucene是如何产生的以及在Lucene背后的关键人物和组织。
根据
Manning’s in Action的思想,我们先向你展示了两个独立的程序,Indexer和Searcher,它们可以对存储于文件系统的文本文件进行索引和搜索。然后我们主要描述了在那两个程序中用到每个Lucene类。最后贡献出我们对类似于Lucene的一些产品的研究。
搜索无处不在,在你阅读本书时也可能发生,你对你程序中不可或缺的搜索感兴趣。基于你的需求,集成
Lucene可能是微不足道的,或者它可能会包含在架构层的设计中。
我们像本章一样组织了后面两章的内容。首先我们需要做的是索引一些文档;在第
2章详细讨论这个过程。
第二章 索引
本章包括
n
执行基本索引操作
n
在索引时添加Document和Field
n
索引日期、数值和用来排序搜索结果的字段
n
使用影响Lucene索引性能和资料消耗的参数
n
优化索引
你可能想搜索存储在硬盘上的文件或者搜索你的邮件、网页甚至数据库中的数据。Lucene能够帮助你。然页,在你能够搜索之前,你应该对它进行索引,这也是本章你所要学习的内容。
在第1章中,你看到了一个简单的索引示例。本章将深入并都你有关索引更新、调整索引过程的参数和更多高级索引技术以帮助你更加了解Lucene。此处你也会发现Lucene索引的结构、当以多线程或多进程访问Lucene索引时要注意的重要问题和Lucene提供的防止并发修改索引的锁机制。
2.1 理解索引过程
正如你在第1章中所见,为索引一个文档只需调用Lucene API的几个方法。所以,表面看来,用Lucene进行索引是个简单的操作。然而在这些简单API的背后隐藏了一些有趣且相当复杂的操作集合。我们可以将这个集合分为三个主要的功能,如图2.1所示,在随后的几个小节中描述。
图2.1 使用Lucene索引分为三个主要步骤:
将数据转化为文本,分析,将它保存至索引
2.1.1 转化为文本
为用Lucene索引数据,你必须首先将它转化为纯文本单词流,Lucene能消化的格式。在第1 章中,我们限制示例索引和搜索.txt文件,这允许我们分析它的内容并使它来生成Field的实例。然而,事情并不总是那么简单。
假设你要索引一些PDF格式的手册。为了准备这些手册以进行索引,你必须首先提取出PDF文档中的文本信息并使用这些提取的数据来创建Lucene Document及其Field。你回顾21页的表1.2,你会发现Field方法总是接受String值,有时是Date和Reader值。没有哪个方法接受PDF的类型,既使这种类型存在。在索引Microsoft Word文档或其它任何非纯文本格式的文档时你都会遇到这个问题。甚至你在处理使用纯文本的符号的XML或HTML文档时,你仍然需要足够的智能来准备这些数据来进行索引,避免索引类似XML元素或HTML标签的东西,而要索引这些文档中的真实数据。
文本提取的细节在第7章,我们构建了一个小但是完整的框架以索引图2.1所示的所有文档格式及其它几种格式。实际上,你会发现图2.1和图7.3很类似。
2.1.2 分析
一旦你准备好了要索引的数据并创建了由Field组成的Lucene Document,你就可以调用IndexWriter的addDocument(Document)方法,把你的数据送入Lucene索引。当你做这些时,Lucene首先分析这些数据以使它更适合索引。它将文本数据分割成块或单词,并执行一些可选择的操作。例如,单词可以在索引之前转化为小写,以保证搜索是大小写无关的。典型的,它也有可能排除转入中所有经常出现但无意义的词,例如英语终止词(a, an, the, in, on等等)。类似的,通常分析输入单词以获取它的本质。
这个非常重要的步骤称为分析。Lucene的输入能够以很多有趣且有用的方法进行分析,所以我们将在第4章详细分析这个过程。目前,把这个步骤想像为一个过滤器。
2.1.3 写索引
在输入被分析完后,就可以添加到索引中了。Lucene将输入存储在一个反向索引的数据结构中。这个数据结构在允许快速关键字查询的同时有效地利用了磁盘空间。这个结构反向是因为它使用从输入中提取的单词做为查询键值而不是用处理的文档做为中枢入口。换句话说,代替尝试回答这个问题“这个文档中含有哪些单词?”,这个结构为提供快速回答“哪篇文档含有单词X?”做了优化。
如果你想一下你常用的Web搜索引擎和你典型的查询格式,你会发现你想得到的精确的查询。当今所有的Web搜索引擎的核心都是反向索引。使得各搜索引擎不同的是一组严格保密的附加参数来改进这个索引结构。例如Goolge知名的级别(PageRank, PR)因素。Lucene也有它自己的一套技术,你可以在附录B中学到其中一些。
2.2 基本索引操作
在第1章中,你看到了如何向索引中添加文档。但是我们将在此总结这个过程,同时描述删除和更新操作,以给你一个方便的参数点。
2.2.1 向索引中索加文档
为了总结你已知的,让我们来看一下在本章中作为单元测试基类的代码片断。代码列表2.1创建一个复合的索引称做index-dir(索引目录),存储于系统临时目录:UNIX的/tmp,或使用Windows的C:/TEMP。(复合索引在附录B中描述)我们使用SimpleAnalyzer来分析输入文本,然后我们索引两个简单的Document,每个都包含四种类型的Field:Keyword、UnIndexed、UnStored和Text。
列表2.1 在基本测试类的每个测试之前准备一个新的索引
public abstract class BaseIndexingTestCase extends TestCase {
protected String[] keywords = {“1”, “2”};
protected String[] unindexed = {“Netherlands”, “Italy”};
protected String[] unstored = {“Amsterdam has lots of bridges”, “Venice has lots of canals”};
protected String[] text = {“Amsterdam”, “Venice”};
protected Directory dir;
protected void setUp() throws IOException {
String indexDir =
System.getProperty(“java.io.tmpdir”, “tmp”) +
System.getProperty(“file.separator”) + “index-dir”;
dir = FSDirectory.getDirectory(indexDir, true);
addDocuments(dir);
}
protected void addDocuments(Directory dir)
throws IOException {
IndexWriter writer = new IndexWriter(dir, getAnalyzer(), true);
writer.setUseCompoundFile(isCompound());
for (int i = 0; i < keywords.length; i++) {
Document doc = new Document();
doc.add(Field.Keyword(“id”, keywords
));
doc.add(Field.UnIndexed(“country”, unindexed
));
doc.add(Field.UnStored(“contents”, unstored
));
doc.add(Field.Text(“city”, text
));
writer.addDocument(doc);
}
writer.optimize();
writer.close();
}
protected Analyzer getAnalyzer() {
return new SimplyAnalyzer();
}
protected boolean isCompound() {
return true;
}
}
因为BaseIndexingTestCase类要被本章的其它单元测试类继承,我们将指出几个重要的细节。BaseIndexingTestCase每次setUp()方法调用时创建相同的索引。因为setUp()在测试执行之前被调用,每个测试都是针对新创建的索引运行。尽管基类使用SimpleAnalyzer,子类可以覆盖getAnalyzer()方法以返回不同的Analyzer类型。
不同的Document
Lucene的一个重要特征是它允许有不同Field的Document在同一索引中共存。这就意味着你可以用一个索引来保存代表不同实体的Document。例如,你可以存放代表零售产品,有名称和价格字段的Document和代表people,有名称、年龄和性别字段的Document。
附加字段
假设你有个生成给定单词的同意词数组的程序,并且你想用Lucene来索引基本词和所有它的同意词。实现的一个方法是遍历所有的同意词并把它们添加到一个String中,然后你可以用它来创建Lucene字段。索引所有同意词和基本词另一个方法可能是更好的方法是把不同的值添加到相同的字段,如下:
String baseWord = “fast”;
String synonyms[] = String {“quick”, “rapid”, “speedy”};
Document doc = new Document();
doc.add(Field.Text(“word”, baseWord));
for (int i = 0; i < synonyms.length; i++) {
doc.add(Field.Text(“word”, synonyms
));
}
其中,Lucene添加所有的单词并把它们索引在同一个字段word中,允许你在搜索时使用其中任何一个。
2.2.2 在索引中清除Document
尽管大多程序关心的是添加Document到Lucene索引中,一些也需要清除它们。例如,报纸出版社可能只想在可搜索的索引中保留最近一个周的有价值的新闻。另外的程序可能想清除所有包含特定单词的Document。
Document的删除是由IndexReader来完成的。这个类并不立即从索引中删除Document。它只做个删除的标志,等待IndexReader的close()方法调用时真正的Document删除。理解了这些之后,让我们看一下列表2.2:它继承BaseIndexingTestCase类,这意味着在每次测试方法运行之前,基类重建两个文档的索引,在2.2.1小节中描述。
列表2.2 根据内部文档号删除Document
public class DocumentDeleteTest extends BaseIndexingTestCase {
public void testDeleteBeforeIndexMerge() throws IOException {
IndexReader reader = IndexReader.open(dir);
assertEquals(2, reader.maxDoc()); ① 下一个Document号是2
assertEquals(2, reader.numDocs()); ② 索引中有两个Document
reader.delete(1); ③ 删除号码为1的Document
assertTrue(reader.isDeleted(1)); ④ 删除Document
assertTrue(reader.hasDeletions()); ⑤ 包含删除的索引
assertEquals(2, reader.maxDoc()); ⑥ 1个索引的Document,下一个Document号是2
reader.close();
reader = IndexReader.open(dir);
assertEquals(2, reader.maxDoc()); ⑦ 在IndexReader重新打开后,
assertEquals(1, reader.numDocs()); 下一个Document号是2
reader.close();
}
public void testDeleteAfterIndexMerge() throws IOException {
IndexReader reader = IndexReader.open(dir);
assertEquals(2, reader.maxDoc());
assertEquals(2, reader.numDocs());
reader.delete(1);
reader.close();
IndexWriter writer = new IndexWriter(dir, getAnalyzer(), false);
writer.optimize();
writer.close();
reader = IndexReader.open(dir);
assertFalse(reader.isDeleted(1));
assertFalse(reader.hasDeletions()); ⑧ Optimizing
assertEquals(1, reader.maxDoc()); renumbers
assertEquals(1, reader.numDocs()); Documents
reader.close();
}
}
①②③ 列表2.2的代示展示了如何指定Document的内部编号来删除Document。它也展示了IndexReader经常混淆的两个方法的不同:maxDoc()和numDocs()。前者返回下一个可用的内部Document号,后者返回索引中的Document的数目。因为我们的索引只含有两个Document,numDocs()返回2;又因为Document号从0开始,maxDoc()也返回2。
注意 每个
Lucene的Document有个唯一的内部编号。这些编码不是永久分配的,因为Lucene索引分配时在内部重新分配Document的编号。因此,你不能假定一个给定的Document总是拥有同一个Document编号。
④⑤ 在
testDeleteBeforeIndexMerge()方法中的测试也示范了IndexReader的hasDeletions()方法以检查一个索引是否包含有删除标志的Document和isDeleted(int)方法以检查指定编号的Document的状态。
⑥⑦ 可见,
numDocs()能够立即感知到Document的删除,而maxDoc()不能。
⑧ 此外,在
testDeleteAfterIndexMerge()方法中,我们关闭IndexReader并强制Lucene优化索引以合并索引的各片断。然后我们用IndexReader打开索引,maxDoc()方法返回1而不是2,因为在删除和合并后,Lucene对剩余的Document重新编号。索引中只有一个Document,所以下一下可能Document编号是1。
除了我们通过指定
Document编号来删除单个Document之外,你可以用IndexReader的delete(Term)方法删除多个Document。使用这个删除方法,允许你删除所有包含指定Term的Document。例如,为了删除city字段中包含单词Amsterdam的Document,你可以这样用IndexReader:
IndexReader reader = IndexReader.open(dir);
reader.delete(new Term(“city”, “Amsterdam”));
reader.close();
你在使用这个方法时要特别小心,因为在所有索引的Document中指定一个term将会擦除整个索引。这个方法的使用类似于基于Document编号的删除方法;将在2.2.4小节中描述。
你可能奇怪为什么Lucene在IndexReader中执行Document删除而不是IndexWriter中。这个问题在Lucene社区中每几个月就问一次,大概因为有缺点或者可能是容易让人误解的类名。Lucene的用户经常认为IndexWriter是唯一可以修改索引的类,且IndexReader以只读的形式访问索引。实际上,IndexWriter只接触索引片断列表和合并片断时的一小部分索引文件。另一方面,IndexReader知道如何解析所有索引文件。当一个Document删除时,IndexReader在标记它被删除之前首先需要定位包含指定Document的片断。目前还没有计划改变这两个Lucene类的名称或行为。
2.2.3 恢复Document
因为Document的删除延迟到IndexReader实例关闭时才执行,Lucene允许程序改变想法并恢复已做删除标记的Document。对IndexReader的undeleteAll()方法的调用通过清除索引目录中的.del文件来恢复所有删除的Document。所以在关闭IndexReader实例关闭之后Document就保留在索引中了。只能使用与删除Document时同一个IndexReader实例,才能调用undeleteAll()来恢复Document。
2.2.4 更新索引中的Document
“如何才能更新索引中的文档?”是一个在Lucene用户邮件列表中经常问的问题。Lucene并没有提供更新方法;Document必须首先从索引中删除然后再重新添加它,如列表2.3所示。
列表2.3 通过删除再添加的方法更新索引的Document
public class DocumentUpdateTest extends BaseIndexingTestCase {
public void testUpdate() throws IOException {
assertEquals(1, getHitCount(“city”, “Amsterdam”));
IndexReader reader = IndexReader.open(dir);
reader.delete(new Term(“city”, “Amsterdam”));
reader.close();
assertEquals(0, getHitCount(“city”, “Amsterdam”));
IndexWriter writer = new IndexWriter(dir, getAnalyzer(), false);
Document doc = new Document();
doc.add(Field.Keyword(“id”, “1”));
doc.add(Field.UnIndexed(“country”, “Netherlands”));
doc.add(Field.UnStored(“contents”,
“Amsterdam has lots of bridges”));
doc.add(Field.Text(“city”, “Haag”));
writer.addDocument(doc);
writer.optimize();
writer.close();
assertEquals(1, getHitCount(“city”, “Haag”));
}
protected Analyzer getAnalyzer() {
return new WhitespaceAnalyzer();
}
private int getHitCount(String fieldName, String searchString) throws IOException {
IndexSearcher searcher = new IndexSeracher(dir);
Term t = new Term(fieldName, searchString);
Query query = new TermQuery(t);
Hits hits = searcher.search(query);
int hitCount = hits.length();
searcher.close();
return hitCount;
}
}
我们首先删除了city字段含有Amsterdam的所有Document;然后添加一个字段与删除的Document相同的新Document,除了把city字段设了一个新值。新的Document的city字段是Haag而不是Amsterdam。我们正确地更新了索引中的一个Document。
通过定量删除来更新
我们的例子删除和添加单个Document。如果你需要删除和添加多个Document,最好是进行批操作。按以下步骤:
1.
打开IndexReader。
2.
删除所有你要删除的Document。
3.
关闭IndexReader。
4.
打开IndexWriter。
5.
添加你要添加的所有Document。
6.
关闭IndexWriter。
要记住:批量Document删除和索引总是比交叉删除和添加操作快。
懂得了更新和删除操作,让我们讨论如何提升索引的性能并尽可能好地利用硬件资源。
技巧 当删除和添加Document时,批量进行。这样总是要比交叉删除和添加操作快。
2.3 Document和Field增量
并不是所有的Document和Field是平等创建的――或者至少你能确定选择性的Document或Field增量的情况。假设你要写一个索引和搜索公司Email的程序。可能需求是给公司员工的Email比其它Email消息更多的重要性。你会如何做呢?
Document增量是个使得这种需求能够简单实现的一个特征。默认情况下,所有的Document都没有增量――或者更恰当地说,它们都有相同的增量因数1.0。通过改变某个Document的增量因数,你可能让Lucene认为它比索引中的其他Document更重要或不重要。执行这些的API只需一个方法,setBoost(float),可以这样用:
public static final String COMPANY_DOMAIN = “example.com”;
public static final String BAD_DOMAIN = “yucky-domain.com”;
Document doc = new Document();
String senderEmail = getSenderEmail();
String senderName = getSenderName();
String subject = getSubject();
String body = getBody();
doc.add(Field.Keywork(“senderEmail”, senderEmail));
doc.add(Field.Text(“senderName”, senderName));
doc.add(Field.Text(“subject”, subject));
doc.add(Field.UnStored(“body”, body));
if (getSenderDomain().endsWithIgnoreCase(COMPANY_DOMAIN)) {
doc.setBoost(1.5); ① 员工增量因数:1.5
} else if (getSenderDomain().endsWithIgnoreCase(BAD_DOMAIN)) {
doc.setBoost(0.1); ② Bad域增量因数:0.1
}
writer.addDocument(doc);
在本例中,我们检查邮件域名来决定发件人是不是公司的员工。
①
当我们索引由公司员工发送的消息时,我们把他们的增量因数设为1.5,这默认的因数1.0大。
②
当我们碰到来自虚构的不可信域的发件人发送的消息时,我们通过把它们的增量因数隐为0.1把它们归类为不重要的。
就象你可以增量Document一样,你也可以增量个别的字段。当你增量Document时,Lucene内部使用相同的增量因数增量它的每个字段。假设Email索引程序的另一个需求是考虑标题字段比发件人的名称字段更重要。换句话说,搜索时对标题字段的匹配比同样对senderName字段的匹配更有价值。为了完成这个计划,我们使用Field类的setBoost(float)方法:
Field senderNameField = Field.Text(“senderName”, senderName);
Field subjectField = Field.Text(“subject”, subject);
subjectField.setBoost(1.2);
在本例中,我们随便选了一个增量因数1.2,就像我们随便为Document选了一个增量因数1.5和0.1一样。你要用的增量因数值取决于你要达到什么目的;你可能需要做一些实验和调整来达到预期的目标。
值得注意的是字段可以有和它们相关联的固定增量,是由于Lucene的算分方式。增量总得来说是个高级特征,没有它很多程序也能工作得很好。
Document和Field增量在搜索时起作用,你将在3.5.9小节中学到。Lucene的搜索结果根据每个Document与查询的接近程度来分级,每个匹配的Document分被赋于一个分值。Lucene的评分规则受许多因素影响,增量因数是其中之一。
2.4 索引日期
邮件含有发送和接收日期,文件有很多相关的日期,HTTP呼应有一个包含请求页面最后修改日期Last-Modified头。像很多其他Lucene用户一样,你可能需要索引日期。Lucene带有一个Field.Keyword(String, Date)方法,还有DateField类,这使得索引日期很简单。例如,为了索引当前日期,可以这样:
Document doc = new Document();
doc.add(Field.Keyword(“indexDate”, new Date()));
在内部,Lucene使用DateField类把给定的日期转化成适于索引的字符串。这样处理日期比较简单,但是你在使用这个方法时必须小心:使用DateField把日期转化成可以索引的String,包括日期的所有部分,甚至毫秒。你将在6.5节中看到,这可能会导致某些查询的性能问题。实际上,你很少需要精确到毫秒的日期,至少对查询来说是这样。通常,你可以把日期近似到小时或着甚至天。
因为所有Field值最后都会转化成文本,你可以像String一样索引日期。例如,如果你把日期近似到天,以YYYYMMDD格式的String索引日期,可以使用Field.Keyword(String, String)方法。运用这种方法的原因是你能够索引在Unix Epoch(1970年1月1日)之前的日期,DateField不能处理。尽管一些解决这个限制的修补在近几年来被很多人提出,但是没有一个很完美。所以他们只能在Lucene补丁列表中找到,而没有包含在Lucene中。根据Lucene用户提到这个限制的频率,不能索引1970年之前的日期通常不是个问题。
注意 如果你仅需要对日期进行搜索,而不是时间,用Field.Keyword(“date”,
“YYYYMMDD”)。如果你要取得完整的时间,用Field.Keyword(“timestamp”,
<java.util.Date>)索引另一个Field。
如果你想把日期或时间格式化为其它形式,一定注意String是以字典顺序排序的;这样做允许期间(date-range)查询。以YYYYMMDD格式索引日期的一个好处是能够仅用年来查询,或用年和月,或者精确地年和月和日。仅用年查询,使用PrefixQuery。我们将在3.4.3小节中深入讨论PrefixQuery。
2.5 索引数值
有两种常见的情况,数值索引非常重要。第一种,数值包含在要索引的文本中,并且你想确定这些数值被索引了,这样你就能在后来的搜索中使用它们。例如,你的文档可能包含“珠穆朗玛峰高8848米。”你想像搜索珠穆朗玛峰时找到含有这个句子的文档一样搜索数字8848。
第二种情况,有只含有数值的Field,并且你想索引和搜索它们。此外,你可能想利用这些Field来执行范围内搜索(range queries)。例如,如果你要索引邮件消息,有个字段保存邮件大小,你可能想找出所有指定大小的消息;或者,你可能想使用范围搜索来找出所有大小在一个特定范围内的消息。你可能也要根据大小来排序结果。
Lucene能够通过在内部把它们处理成字符串来索引数值。如果你需要索引自由格式文本中的数值,你要做的第一件事就是选择不过滤数字的Analyzer。在第4.3节中将讨论到的,WhiteSpaceAnalyzer和StandardAnalyzer是两个可能的选择。如果你传递给他们一个类似“珠穆朗玛峰高8848米”的句子,他们把8848提取为一个单词,并把它传到索引过程,允许稍后搜索8848。另外,SimpleAnalyzer和StopAnalyzer将把数字从单词流中分离出来,这意味着搜索8848不会有任何匹配的文档。