Lucene in Action(中文版)
Lucene in Action
中文版
第一部分 Lucene核心
1. 接触Lucene
2. 索引
3. 为程序添加搜索
4. 分析
5. 高极搜索技术
6. 扩展搜索
第二部分 Lucene应用
7. 分析常用文档格式
8. 工具和扩充
9. Lucene其它版本
10. 案例学习
序
Lucene开始是做为私有项目。在1997年末,因为工作不稳定,我寻找自己的一些东西来卖。Java是比较热门的编程语言,我需要一个理由来学习它。我已经了解如何来编写搜索软件,所以我想我可以通过用Java写搜索软件来维持生计。所以我写了Lucene。
几年以后,在2000年,我意识到我没有销售天赋。我对谈判许可和合同没有任何兴趣,并且我也不想雇人开一家公司。我喜欢做软件,而不是出售它。所以我把Lucene放在SourceForge上,看看是不是开源能让我继续我想做的。
有些人马上开始使用Lucene。大约一年后,在2001年,Apache提出要采纳Lucene。Lucene邮件列表中的消息每天都稳定地增长。也有人开始贡献代码,大多是围绕Lucene的边缘补充:我依然是仅有的理解它的核心的开发者。尽管如些,Lucene开始成为真正的合作项目。
现在,2004年,Lucene有一群积极的深刻理解其核心的开发者。我早已不再每天作开发,这个强有力的工作组在进行实质性的增加与改进。
这些年来,Lucene已经翻译成很多其它的语言包括C++、C#、Perl和Python。在最开始的Java和其它这些语言中,Lucene的应用比我预想的要广泛地多。它为不同的应用(如财富100公司讨论组、商业Bug跟踪、Microsoft提供的邮件搜索和100页面范围的Web搜索引擎)提供搜索动力。在业内,我被介绍为“Lucene人”。很多人告诉我他们在项目中使用到Lucene。我依然认为我只听说了使用Lucene的程序的小部分。
如果我当初只是出售它,Lucene应用得不会这么广泛。程序开发人员看来更喜欢开源。他们在有问题时不用联系技术支持而只需查看一下源代码。如果这还不够,邮件列表中的免费支持比大多商业支持要好得多。类似Lucene的开源项目使得程序开发人员更加有效率。
Lucene通过开源已经变得比我想象的伟大的多。我见证了它的发展,是Lucene社区的努力才使得它如此兴旺。
Lucene的未来怎样?我无法回答。有了这本书,你现在也是Lucene社区的一员,现在由您将Lucene带往新的高地。旅途顺利!
DOUG CUTTING
Lucene和Nutch的作者
前言
来自Erik Hatcher
在Internet早期我就对搜索和索引感兴趣。我已经建立了用majordomo、MUSH(Mail User’s Shell)和少量Perl、awk及shell脚本来管理邮件列表的存储结构。我实现了一个CGI的web接口,允许用户搜索这个列表和其它用户的信息,其内部使用了grep。然后相继出现了Yahoo!、AltaVista和Excite,这些我都经常访问。
在我有了第一个儿子Jakob之后,我开始了数字照片档案的设计。我想开发一套管理图片的系统,可以给图片附加元数据,如关键字、拍摄日期。当然用我选择的尺寸定位图片是很容易的。在19世纪90年代末,我构建了基于文件系统的原型,使用了Microsoft的技术,包括Microsoft Index Server、Action Server Pages及处理图片的第三方COM组件。从那时起,我的职业生涯都消耗在这些类似的技术上了。I was able to cobble together a compelling application in a couple of days of spare-time hacking.
我的职业转向Java技术,并且我越来越少地利用Microsoft Windows。为了以系统无关的方式用Java技术重新实现我的个人照片档案系统及搜索引擎,我使用了Lucene。Lucene的简单易用远远超过了我的期望—我所期望的其它开源库或工具在概念上简单,但是却难以使用。
在2001年,Steve Loughran和我开始编写Java Development with Ant(Manning)。我们采用图片搜索引擎的思想,并把它推广为一个文档搜索引擎。这个程序示例在那本Ant书中使用,而且可被定制为图片搜索引擎。Ant的责任不仅来自于简单的编译打包的构建过程,也来自于定制的任务,<index>,我们在构建过程中使用Lucene创建索引文件。Ant任务现在生存在Lucene的Sandbox(沙箱)中,将在本书8.4节描述。
Ant已经应用在我的博客系统中,我称为BlogScene(http://www.blogscene.org/erik)。在建立一个博客实体之后,我运行一个Ant构建过程,索引新的实体并将它们上传到我的服务器上。我的博客服务器由一个Servlet、一些验证模板和一个Lucene索引组成,允许(rich)查询,甚至联合查询。与其它博客系统相比,BlogScene在特色和技巧上差很多,但是它的全文检索能力非常强大。
我现在效力于维吉尼亚大学对Patacriticism的应用研究小组(http://www.patacriticism.org)。我用对文本分析、索引和搜索的经验通过讨论量子力学与艺术的关系来测试及拓展我的思路。“诗人是世界上不被认可的最伟大的工程师”。
来自Otis Gospodnetic
我对信息搜索与管理的兴趣和热情开始于在Middlebury大学的学生时代。那时候,我发现了信息的广大资源,即Web。尽管Web仍然刚开始发展,但是对收集、分析、索引和搜索的长期需求是很明显的。我开始对建立来自Web的信息库感到困惑,开始编写Web爬行器梦想有种方法可以对这些收集的信息进行搜索。我认为在巨大的未知领域中搜索是杀手级软件。有了这种思想以后,我开始了一系列收集和搜索项目。
在1995年,和同学Marshall Levin一起创建了WebPh,一个用来收集和找出个人联系信息的开源程序。基本上,这是一个简单的具有Web接口(CGI)的电话本,那时排在首位的类型。(实际上,它在19世纪90年代末的案例学习中被引用为一个示例。)大学和政府机构是这个程序的主要用户,现在还有很多在使用它。在1997年使用我的WebPh,我继续创建了Populus,一个当时很流行的白页。尽管技术(与WebPh类似)很普通,但是Populus有很重的负担,并且能够与WhoWhere、Bigfoot和Infospace等大角色相媲美。
在两个关于个人联系信息的项目之后,是该探索新的领域了。我开始了下一个冒险,Infojump,用来在网上时事通讯、杂志、报纸中选择高质量的信息。我拥有的软件由大量的Perl模块和脚本组成,Infojump利用一个称作Webinator的Web爬行器和一个全文搜索的产品叫作Texis。在1998年Infojump提供的服务很像今天的FindArticles.com。
尽管WebPh、Populus和Infojump达到了它们的目的并是功能很完善,但它们都有技术的局限性。它们缺少的是一个用反向索引来支持全文搜索强大的信息搜索库。为了不重复相同的工作,我开始搜寻一个我认为不可能存在的解决方案。在2000年早期,我发现了Lucene,我正在寻找的缺少的部分,并且我一下子就喜欢上了它。
我在Lucene还在SourceForge的时候就加入了这个项目,后来2002年Lucene转移到Apache软件基金会。我对Lucene的热爱是因为这些年来它已经成为我很多思想的核心组件。这些思想中的一个是Simpy,我最近的一个项目。Simpy是个有许多特点的个性Web服务,可以让用户加标签、索引、搜索和共享在网上找到的信息。它主要使用了Lucene,上千条索引,由Doug Cutting的另一个项目Nutch(见第10章)提供动力支持。我对Lucene的积极参与导致我被邀请与Erik Hatcher共同编写Lucene in Action。
Lucene In Action有关于Lucene最全面的信息。接下来的10章包含的信息围绕你使用Lucene创建优秀程序所需的所有主题。这是它平坦且轻快的协作过程的结果,就像Lucene社区一样。Lucene和Lucene in Action证明了有类似兴趣的人们可以完成什么,不管在人生中会碰到什么情况,都会积极地为全球知识的共享做出贡献。
致谢
首先并且是最重要的,我们感谢我们的妻子Carole(Erik)和Margaret(Otis),一直支持这本书的写作。没有她们的支持,这本书就不可能出版。Erik感谢他的两个儿子,Ethan和Jakob,因为他们的忍耐和理解,Erik写这本书时没有时间陪他们玩耍。
我们真诚感谢Doug Cutting。没有Doug的贡献,就不可能有Lucene。没有其他Lucene的贡献者,Lucene就会少很多特征、更多的Bug,Lucene的成长就会花更长的时间。感谢所有的贡献者,包括Peter Carlson、Tal Dayan、Scott Ganyo、Eugene Gluzberg、Brian Goetz、Christoph Goller、Mark Harwook、Tim Jones、Daniel Naber、Andrew C. Oliver、Dmitry Serebrennikov、Kelvin Tan和Matt Tucher。同时,我们感谢所有贡献在第10章的案例的人:Dion Almaer、Michael Cafarella、Bob Carpenter、Karsten Konrad、Terence Parr、Robert Selvaraj、Ralf Steinbach、Holger Stenzhorn和Craig Walls。
…
…
本书简介
Lucene in Action为使用最好的Java开源搜索引擎的用户提供所有细节、最好的实践、警告、技巧。
本书假设读者熟悉基本的Java编程。Lucene本身是个Java档案(JAR)文件并能集成到简单的命令行程序和大型企业级应用程序中。
Roadmap
我们在本书第1部分覆盖Lucene核心编程接口(API)使你在将Lucene整合到你的程序中时愿意使用它:
n 第1章,接触Lucene。我们介绍了一些基本的信息搜索术语和Lucene的主要竞争对手。我们很快地构建了一个你马上能用或修改以适应需要的简单索引和搜索程序。这个示例程序向你打开了探索Lucene其它能力的大门。
n 第2章使你熟悉Lucene基本的索引操作。我们描述了索引数值和日期的不同字段类型和各种技术。包括调整索引过程、优化索引以及如何处理线程安全。
n 第3章向你介绍基本的搜索,包括Lucene如何根据查询来排列文档的细节。我们讨论基础的查询类型及它们如何通过用户输入的查询表达式创建。
n 第4章深入研究Lucene的索引核心,分析过程。分析器创建块及单词、单词流和单词过滤器。我们创建了一些定制的分析器,showcasing synonym injection and metaphone(like soundex) replacement.也分析了非英语语言,典型的分析汉字文本的示例。
n 第5章讲述搜索章节剩余的。我们描述了一些高级的搜索特征,包括排序、过滤及使用词向量。高级的查询类型在此出现,包括SpanQuery家族。最后,我们讨论了Lucene对查询多索引的内建支持,并行的及远程的。
n 第6章超越高级搜索,向你展示了如何扩展Lucene的搜索能力。你将学到如何定制搜索结果的排序、扩展查询表达式分析、实现Hit收集和调整查询性能。
第2部分超越Lucene内建的工具并向你展示围绕Lucene可以做什么。
n 第7章,我们创建了可重用、可扩展的用来分析Word、HTML、XML、PDF及其它格式文档的框架。
n 第8章包括围绕Lucene的扩展和工具。我们描述了一些Lucene的索引查看和开发工具以及Lucene沙箱中的好东西。高亮搜索项就是这种你想要的沙箱扩展,还有在Ant构建过程中创建索引的其它工具。使用noncore分析器,并使用类似WordNet的索引。
n 第9章描述Lucene翻译成其它各种语言的版本,如C++、C#、Perl和Python。
n 第10章将Lucene的技术细节带到大量优秀的案例学习中。这些案例由那些创建了以Lucene为核心的有趣的、快速的、可升级的程序的开发者提供。
谁应该阅读本书?
在程序中需要强大搜索能力的开发人员需要阅读这本书。Lucene in Action也适合于那些对Lucene或索引和搜索技术好奇的开发人员,他们可能不会马上就用到它。把Lucene添加到你的工具箱对以后的项目来说是值得的—搜索是个热门的话题并且将来也会是。
这本书主要使用Java版的Lucene(来自Apache Jakarta),并且大多数示例使用Java。最适合熟悉Java的读者。Java经验是很有帮助的,然而Lucene已经翻译成很多其它的语言包括C++、C#、Python和Perl。概念、技术甚至API本身都和Java版Lucene差不多。
代码示例
本书的源代码可以从Manning的网站http://www.manning.com/hatcher2上下载。代码的使用说明包含在代码包的README文件。
书中出现的大多数代码是由我们编写并包含在代码包中。某些代码(尤其是案例代码)不在我们的代码包中提供。书中的代码片断归贡献者所有。同时,我们包含了Lucene代码库的部分代码,基于Apache软件许可协议(http://www.apache.org/licenses/LICENSE-2.0)。
代码示例不包括package 和import 语句,以节省空间;具体请参照实际代码。
为什么是JUnit?
我们相信书中的代码示例应该都是高质量的。典型的“hello world”例子经常帮助读者测试他们的环境。
我们使用独特的方法来使用书中的代码示例。大部分示例是实际的JUnit测试用例(http://www.junit.org)。JUnit,是Java单元测试框架,可以断言一个特殊情况是否能以可重复的方式出现。通过IDE或Ant进行自动JUnit测试用例可以一步一步地构筑系统。我们在本书用使用JUnit是因为平时都在其它项目中使用,并想让你看看我们如何编码。测试驱动开发(Test Driven Development, TDD)是我们强烈推荐的开发模式。
如果你对JUnit不熟,请阅读以下基础。我们也建议你阅读Dave Thomas和Andy Hunt编著的《Pragmatic Unit Testing in Java with JUnit》,还有Vincent Massol和Ted Husted编著的《JUnit in Action》。
JUnit基础
这部分是对JUnit快速但当然不完整的介绍。我们将提供理解我们示例代码所需的基础知识。首先,我们的JUnit测试用例继承junit.framework.TestCase并且很多通过部LiaTestCase基类间接继承它。我们的具体测试类附合这个命名习惯:给类名加后缀Test。例如,我们的QueryParser的测试是QueryParserTest.java。
JUnit自动执行所有类似public void testXXX()的方法,此处XXX是个任意有意义的名称。JUnit测试方法必须简洁,保持好的设计。(例如创建可重复的功能模块等等)
断言
JUnit建立在一组assert语句上,使你自由编写简洁的测试代码并使JUnit框架处理失败状态及指出细节。最常用的assert语句是assertEquals;一些是为不同的数据类型而重载的assertEquals方法。一个示例测试方法如下:
public void testExample() {
SomeObject obj = new SomeObject();
assertEqueals(10, obj.someMethod());
}
如果指定的值(在本例中的10)不等于真实值(本例中是调用obj的someMethod的返回值),assert方法抛出运行时异常。除了assertEquals,为了方便还有一些其他assert方法。我们也使用assertTrue(expression)、assertFalse(expression)和assertNull(expression)语句。这些测试分别判断这个表达式是否是true、false和null。
assert语句有个接受一个附加的String参数的重载表示。String参数都是用来汇报的,在测试失败时向开发人员指出更多信息。我们使用这个String消息参数以更好的描述。
通过以这种风格编写我们的测试用例,可以从我们构建大系统的复杂中解放出来,而且可以每次只关注更少的细节。利用合适的测试用例,我们能够增强信心和灵活性。信心来自于我们知道代码的变化如优化算法不会破坏系统的其它部分,因为出现这种情况的话,自动测试组件能让我们在它影响产品之前发现。重构是一种改变代码内部结构的艺术(或者说科学),所以它能够适应变化的需求而又不影响系统的对外接口。
在上下文中的JUnit
让我们看一下到目前为止谈论的JUnit并把它放到本书的上下文中。JUnit测试用例继承于junit.framework.TestCase,且测试方法都类似public void testXXX()形式。我们的测试用例之一(第3章)如下:
public class BasicSearchingTest extends LiaTestCase {
public void testTerm() throws Exception {
IndexSearcher searcher = new IndexSearcher(directory);
Term t = new Term(“subject”, “ant”);
Query query = new TermQuery(t);
Hits hits = searcher.search(query);
assertEquals(“JDwA”, 1, hits.length());
One hit expected for
search for “ant”
t = new Term(“subject”, “junit”);
hits = searcher.search(new TermQuery(t));
assertEquals(2, hits.length());
Two hits expected for “junit”
searcher.close();
}
}
当然,我们将在之后解释这个测试用例中使用的Lucene API。现在我们只关注JUnit的细节。testTerm方法中的directory变量没在此类中定义。JUnit提供一个在执行每个测试方法之前的初始化钩子;这个钩子是名为public void setUp()的方法。我们的LiaTestCase基类以这种方式实现setUp:
public abstract class LiaTestCase extends TestCase {
private String indexDir = System.getProperty(“index.dir”);
protected Directory directory;
protected void setUp() throws Exception {
directory = FSDirectory.getDirectory(indexDir, false);
}
}
如果testTerm中的第一个断言失败,我们会得到一个异常:
junit.framework.AssertionFalsedError: JDwA expected:<1> but was:<0>
at lia.searching.BasicSearchingTest.
→ testTerm(BasicSearchingTest.java:20)
这个失败指出我们的测试数据与预期的结果不同。
测试Lucene
本书中的大部分测试都是测试Lucene本身的。实际上,这是否现实呢?难道要测的不是我们自己写的代码而是库本身?有个Test Driven Development的姊妹篇是用来学习API的:Test Driven Learning。它为新API写测试以了解它是如何工作以及你能从中得到什么时非常有帮助。这正是我们在大部分代码示例中所做的,所以测试都是测试Lucene它本身。但是不要把这些为学习而做的测试抛开。保留它们以确保你在升级到新版的API或因API改变而重构时,它们能够保持真值。
模型对象
在一些用例中,我们使用模型对象来测试。模型对象用来作为探测器传入真实的业务逻辑,以判断这个业务逻辑是否正常工作。例如,第4章中有个SynonymEngine接口(4.6节)。使用这个接口的真实业务逻辑是个分析器。当我们想测试这个分析器本身时,SynonymEngine使用什么类型就不重要,我们只想使用一个定义良好并有可预见行为的对象。模型对象可以使得测试用例尽可能简单,这样它们每次只测试系统的一个方面,在测试失败要修正什么错误时没有纠缠的依赖。使用模型对象的一个好处来自于设计的变动,例如关系的分离和设计使用接口代替直接具体实现。
我们的测试数据
为了避免每个小节都要用完全不同的数据,书中大部多都是围绕一组公共的示例数据以提供一致性。这些示例数据由书籍详细资料组成。表1显示了这些数据,你可以参考它来理解我们的例子。
表1 本书中用到的示例数据
Title / Author Category Subject
A Modern Art of Education
Rudoif Steiner /education/pedagogy education philosophy
psychology practice Waldorf
Imperial Secrets of Health
and Logevity
Bob Flaws /health/alternative/Chinese diet chinese medicine qi
gong health herbs
Tao Te Ching 道德經
Stephen Mitchell /philosophy/eastern taoism
Gödel, Escher, Bach:
an Eternal Golden Braid
Douglas Hofstadter /technology/computers/ai artificial intelligence number theory mathematics music
Mindstorms
Seymour Papert /technology/computers/programming/eduction children computers powerful
ideas LOGO eduction
Java Development with Ant
Erik Hatcher,
Steve Loughran /technology/computers/programming apache jakarta ant build tool
junit java development
JUnit in Action
Vincent Massol, Ted Husted /technology/computers/programming junit unit testing mock
objects
Lucene in Action
Otis Gospodnetic,
Erik Hatcher /technology/computers/programming lucene search
Extreme Programming
Explained
Kent Beck /technology/computers/programming/methodology extreme programming agile
test driven development
methodology
Tapestry in Action
Howard Lewis-Ship /technology/computers/programming tapestry web user interface
components
The Pragmatic Programmer
Dave Thomas, Andy Hunt /technology/computers/programming pragmatic agile methodology
developer tools
除了表中所显示的数据字段,还有ISBN、URL和出版日期。种类和标题字段是我们主观值,但是另一些都是有关这些书的真实客观值。
代码约定和下载
列表和正文中的代码都以等宽字体的形式出现以与普通文本区分。在正文中Java方法名称通常都不包含完整声明。
第一部分
Lucene核心
本书的前半部分覆盖了“盒子外的”(嗯…,JAR外的)Lucene。你将以整体的观点接触Lucene并开发一个完整的索引和搜索程序。每个后续的章节系统地深入研究特定的内容。“索引”数据和文档和后来的对它们的“搜索”是使用Lucene的第一步。在从索引过程返回后,“分析”将使你深入理解在使用Lucene索引文本时发生了什么。搜索是Lucene真正的亮点:本部分以仅使用内建特征的“高级搜索”技术结束,“扩展搜索”显示了Lucene针对定制目的的扩展性。
第一章 接触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 SWISH,SWISH-E和SWISH++ -- http://homepage.mac.com/pauljlucas/software/swish/, http://swish-e.org/
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/
n Microsoft Index Server—http://www.microsoft.com/NTServer/techresources/webserv/
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/和它的子版块有大量的信息。
n Google—尽管Google Directory是基于Open Directory的数据,这两个目录确实不一样。所以你也应该访问http://directory.google.com/Top/Computers/Software/
Information_Retrival/。
n Searchtools—有个搜索工具的专门的网站http://www.searchtools.com/。这个网站并不时常更新,但是它已经很多年了并且相当广泛。软件根据操作系统、编程语言、许可证等等进行分类。如果你仅仅对用Java写的搜索软件感兴趣,访问http://www.searchtools.com/toos
/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不会有任何匹配的文档。