2.1构建Lucene搜索
2.1.1 Lucene内容模型
一个文档(document)就是Lucene建立索引和搜索的原子单元,它由一个或者多个字段(field)组成,字段才是Lucene的真实内容。每一个字段有都有一个名字来标识它,一个文本或者一个二进制值以及一系列的详细的选择项。为检索到你原生态的内容,你必须首先将其解释成Lucene的文档和字段。然后,在搜索时,字段值被检索到。比如,用户要检索“title:Java”,Lucene库将会检索那些包含标题为Java字段(field)的文档(document)。
2.1.2理解Lucene检索过程
在检索时,文本(text)能够从原始内容中抓取,通常是创建一个Document的实例,包含Field实例来存放内容。在字段中的文本被分析,产生一个标志流(token stream)。最终,这些token stream被添到一个称之为段式架构(segmented architecture)的数据结构的文件,这些文件就是索引。
2.1.2.1索引段(IndexSegments)
Lucene有一个丰富且详细的索引文件格式,这个文件格式会已经随着时间而被小心地优化。尽管你为了使用Lucene而没有必要知道这个格式的细节,但是,在更高级的层面,有一个基本的理解是很有帮助的。如果你对这个文件格式很感兴趣,你可以查阅有关的web资源。每个Lucene索引文件由一个或者多个segments组成。每个segment有一独立索引,它是所有索引文档的一个集合。无论什么时候,writer冲刷缓存中添加的文档(document)和填充删除到对应的目录下,一个新的segment会被创建。在搜索的时候,任何segment被分开地访问并且结果会被结合起来。任何segment相应地由多个文件组成,形成_X.<ext>。X就是segment的名字,<ext>是一种扩展,它能够标识文件相对应索引的一部分。那儿有分开文件保存索引的不同部分(term矢量,存储字段,反转索引等等)。如果你使用组合文件格式(默认有效,但是你可以改变使用writer.etUserCompandFile),然而,大部分这些索引文件被折叠刀一个单一组合文件_X.cfs中,这将少量地牺牲搜索和建立索引的性能。
有一种特殊的文件,称之为segments文件,命名为segments_<N>。它引用所有存在的segments。这个文件很重要,它也发挥着至关重要的作用。Lucene首先打开这个文件,然后打开每个被它应用的segment。这个<N>称为”代”(“the generation”),是一个integer值,每次随着一个改变提交索引后自增长对应的序列值。本质上,索引将会聚集许多segments,尤其当你频繁地打开和关闭你的writer。间歇地,IndexWriter将通过合并segment到一个新的segment中,然后移除老的segments,这样来选择segments和合并它们。用来合并segments的选择是通过一个分离的MergePolicy来支配的。一旦合并选择完成,这个执行是由MergeScheduler来完成。了解有关这些类的使用,请查阅Lucene API。
2.1.2.2 Lucene基本操作
使用Lucene的API就是实现对应的増删改查.。由于这部分的内容仅仅涉及document的基本操作,我们这里仅简绍有关API。
添加操作。
- addDocument(Document)-–实用默认分析器添加doucment,当创建IndexWriter时,你可以指定analyzer。
- addDocument(Document, Aanlyzer) --使用指定的analyzer添加document。但是,请小心!为了搜索正确,你需要在搜索时使用analyzer来匹配产生的标志(token)。
删除操作。
- deleteDocuments(Term)—删除所有包含提供term的documents
- deleteDocuments(Term[])—删除所有包含任何指定数组term的documents。
- deleteDocuments(Query) –删除所有匹配的查询的documents。
- deleteDocuments(Query[]) –删除所有包含任何指定数组查询的documents。
- deleteAll() –删除所有索引中的所有文档。
更新操作。
- updateDocument(Term, Document) –首先删除多有包含提供term的文档,然后实用默认analyzer添加一个新的document。
- updateDocument(Term, Document,Analyzer) –与上面的方法相同,但是,这里采用的是指定的analyzer。
2.1.3 字段选择
在建立索引文档时,字段(Field)可能是最重要的类:它是一个事实上能存储任何需要建立索引的类。
2.1.3.1 索引字段
对于建立索引的选择项(Field.Index.*)控制着文本(text)如何通过转换的索引检索到。这些选择项如下描述:
Ø Index.ANALYZED – 使用analyzer划分字段的值为一个分离tokens流以及使每个token可被检索到。这个选项对于正常的text字段很有用。(body, title, abstract, etc)。
Ø Index.NOT_ANALYZED – 索引这个字段,但是不用分析String值。取而代之,对待这个字段的整个值为一个单一token以及使这个token可以被检索到。对于那些你想检索但是不想字段被破外时很有用,比如URLs、文件系统路径、日期、个人的名字、社交安全码以及电话号码。Index.ANALYZED_NO_NORMS –Index.ANALYZED的变量在索引中不
- addDocument(Document, Aanlyzer) --使用指定的analyzer添加document。但是,请小心!为了搜索正确,你需要在搜索时使用analyzer来匹配产生的标志(token)。 删除操作。
- deleteDocuments(Term)—删除所有包含提供term的documents
- deleteDocuments(Term[])—删除所有包含任何指定数组term的documents。
- deleteDocuments(Query) –删除所有匹配的查询的documents。
- deleteDocuments(Query[]) –删除所有包含任何指定数组查询的documents。
- deleteAll() –删除所有索引中的所有文档。
更新操作。
- updateDocument(Term, Document) –首先删除多有包含提供term的文档,然后实用默认analyzer添加一个新的document。
- updateDocument(Term, Document,Analyzer) –与上面的方法相同,但是,这里采用的是指定的analyzer。
2.1.3 字段选择
在建立索引文档时,字段(Field)可能是最重要的类:它是一个事实上能存储任何需要建立索引的类。
2.1.3.1 索引字段
对于建立索引的选择项(Field.Index.*)控制着文本(text)如何通过转换的索引检索到。这些选择项如下描述:
Ø Index.ANALYZED – 使用analyzer划分字段的值为一个分离tokens流以及使每个token可被检索到。这个选项对于正常的text字段很有用。(body, title, abstract, etc)。
Ø Index.NOT_ANALYZED – 索引这个字段,但是不用分析String值。取而代之,对待这个字段的整个值为一个单一token以及使这个token可以被检索到。对于那些你想检索但是不想字段被破外时很有用,比如URLs、文件系统路径、日期、个人的名字、社交安全码以及电话号码。
Index.ANALYZED_NO_NORMS –Index.ANALYZED的变量在索引中不
Ø 存储正常(Norms)信息。在检索中,正常记录索引时间能提升信息,但是Norm信息在搜索时候会消耗内存。
Ø Index.NOT_ANALYZED_NO_NORMS—就像Index.NOT_ANALYZED,
但是也不存储Norms。这个选项频繁地被使用到,有利于节省索引空间和内存的使用。
Ø Index.NO – 使这个字段不被搜索到。
2.1.3.2 存储字段的选择项
存储字段选择项(Field.Store.*)决定了是否字段的值该不该被存储,这样便于你之后的搜索过程中能够检索到它。
Ø Store.YES – 存储这个值。当这个值存储时,整个原始字符串将会在索引中记录下来,通过IndexReader检索它。当展示搜索的结果(URL、title或者数据库主关键字)时,这个选择项对于字段来说就很有用了。注意,如果存储大字段时,索引会消耗比较大的内存。
Ø Store.NO – 不存储这个值。在原始的表单中,这个选项伴随着Index.ANALYZED去检索一个数据巨大且不必被检索到的文本字段时才有用。比如网页的bodies,文本的任何类型等。
Lucene包含一个很有用的实用类,CompressionTools暴露了静态方法来压缩和解压缩字节数组。在它的实现内部,它使用Java的构建API java.util.Zip类。注意到,即使这样做将会节省索引的空间,依赖于这个内容如何被压缩,这将会使建立索引和搜索变得缓慢。你需要花费更多的CPU换得更少磁盘空间的使用,这对于许多应用程序来说不是一个好的折中。如果这个字段值很小,这样压缩就不值得了。
2.1.3.3 term矢量字段选择项
Ø TermVector.YES— 在任何文档中,记录下独一无二的出现过的term,包括它们的数量。但是,它不存储任何位置或者偏移信息。
Ø TermVector.WITH_POSITIONS— 记录下独一无二的terms和它们的数量,同时,也有每个term出现过的位置。但是,没有偏移量。
Ø TermVector.WITH_OFFSETS—记录独一无二的terms和它们的数量,同时,也有每个term出现过的偏移量。
Ø TermVector.WITH_POSITIONS_OFFSETS— 存储一无二的terms和它们的数量,伴随位置和偏移量。
既然这样,你不能够检索term矢量,除非你也已经转向对于字段的索引。直接注明:如果对于一个字段指定Index.NO了,你必须也指定TermVector.NO。
表格统计field特性:
2.1.4 Boosting文档和字段
想象一下,你不得不去写一个能够索引和搜索公司邮件的应用程序。当排序搜索结构时,可能需要给公司员工的邮件比其它任何邮件消息更重要。你如何着手做这个呢?文档boosting是一个能够使这个简单需求默认实现的特性,所有的文档(document)都有没有得到提升(boost)。不,是由于它们都有一个默认boost因子1.0。通过改变一个文档的boost因子,你能够指导Lucene在计算相关性时,认为这样的document或多或少更重要。如下代码实现了这一个功能。
Document doc = new Document();
String senderEmail = getSenderEmail();
String senderName = getSenderName();
String subject = getSubject();
String body =getBody();
doc.add(newField("senderEmail", senderEmail, Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(newField("senderName", senderName, Field.Store.YES, Field.Index.ANALYZED));
doc.add(newField("subject", subject, Field.Store.YES, Field.Index.ANALYZED));
doc.add(newField("body", body, Field.Store.NO, Field.Index.ANALYZED));
StringlowerDomain = getSenderDomain().toLowerCase();
if(isImportant(lowerDomain)) {
doc.setBoost(1.5F);
} else if(isUnimportant(lowerDomain)) {
doc.setBoost(0.1F);
}
writer.addDocument(doc);
2.1.4.1 Boosting字段(Field)
真如你提升document时,你也能够提升个别的document。当你提升你哥document,Lucene内部使用相同的提升因子来提升它的任何字段。假如有这样的一个需求,需要将邮件的题目信息比发送的名字更重要。换句话说,标题的内容比匹配到的发送者的名字更有价值。实现这样需求,我们为这个标题字段使用setBoost(float)方法。
Field subjectField = new Field("subject",subject, Field.Store.YES,
Field.Index.ANALYZED);
subjectField.setBoost(1.2F);
在这个中,我们随意地选择一个提升因子1.2。你可以根据你的需要设定合理的boost因子,有时,你需要做一些实验,这样能保证达到你的预取效果。
2.1.4.2 标准(Norms)
在建立索引的过程中,索引时间提升因子(boosts)的所有数据源被结合到对应每个文档字段中的单一浮点数中。Document可能有他自己的boost;每个字段可能有一个boost;Lucene基于字段的标志(tokens)计算一个自动boost(更短的字段有更高的boost)。这些boosts被结合,然后,紧紧地压缩编码到一个单一字节(byte)中,每一个文档的每一个字段中存储。在搜索过程中,对于任何字段的标准被搜索到并加载到内存中,解码成一个浮点数,在当计算相关的分数时被用到。尽管在建立索引的过程中,norms被初始化地计算,在随后使用IndexReader.setNorm方法可能会改变它们。SetNorm是一个高级的方法,这需要你重新计算你的norm因子,但是,在更高的动态提升因子中,它是一个功能强大的方式,比如最近的document或者点击的流行度。一个问题伴随norms总是会碰到,就是每次搜索时间高内存的消耗。这是因为整个norm数组,它需要每个文档每个字段一个字节的搜索消耗,而这个操作会加载它到内存中的。对于大索引的每个文本的多个字段,这个能够迅速地增加许多RMA的空间。庆幸地是,有能够很容易实用字段索引NO_NORMS检索操作之一关闭norms或者调用Field.setOmitNorm(true)之前使索引文档包含那个字段。这样做会潜在地影响得分,因为在搜索过程中,没有检索时间boost信息被用到,但是这个影响是平凡的,尤其是当文本字段趋于相同的长度,你没有独自地做任何提升操作(boosting)。
小心:如果你在建立索引的半路上关闭norms,你必须重构整个索引,因为即使一个单一的文档有那个做了索引了norms字段,然后通过段(segment)合并这个文档是将会扩展“spread”其它文档,这样的话所有的文档消耗一个字节,即使他们已经注销了norms。这种情况是可以发生的,因为Lucene没有对norms使用稀疏存储。
2.1.5 索引数字、日期和时间
2.1.5.1 数字索引
有两个建立索引数字的一般解决方案是很重要的。第一种方案,在文本中的数字被索引到,你想确保这些数字被保护起来和建立索引作为它们自己的标志(token),以便于你能够随后的搜索中作为普通的标志(token)。比如,你的文档中可能包含一句话“Be sure to include Form 1099 inyour tax return”:你想能够搜索到数字1099,正如你搜索短语“tax return”和检索到所有包含确切数字的文档一样。
实现这个方案,简单地挑选一个不会丢弃数字的Analyzer就可以了。像WhitespaceAnalyzer和StandardAnalyzer都可以满足你的需要。相反,使用SimpleAnalyzer和StopAnalyzer就会从数据流中丢弃数字,这意味着搜索1099不会匹配到任何文档。如有任何疑问,可以使用Luke工具解析一下。
另一个方案,你有一个包含单一数字的字段,你想去通过数字值检索它。然后,是用它作为精确匹配,区间匹配或者排序。例如,你可能想以零售分类的形式建立product索引,每个产品有一个数字价格以及你必须能够然你的用户通过价格区间搜索。
在过去发布的Lucene版本中,Lucene仅仅能够操作与文本terms。这就需要非常小心地预处理数字,比如零填充或者高级文本数字编码,转换它们到String,以便于恰当地通过文本terms排序和区间搜索。幸运地是,本版2.9后,Lucene包含容易对数字字段使用构建支持,已经有一个field类NumericField。
2.1.5.2 日期和时间索引
邮件信息包括发生和接受日期,文件由几个timestamps关联它们以及HTTP响应有一个最近修改的包含请求最近修改日期的头部。这样的日期和时间的操作是将它们转化为一个相等int或者long值,然后,索引那个值作为一个数字。最简单的方式是使用Date.getTime来获取相等的值,用微秒作为精确值。
doc.add(newNumericField("timestamp").setLongValue(new Date().getTime()));
可供选择,如果对日期的微秒解决方案,你能够量子化它们。如果你需要量子化到秒、分、时和天,直接做除法操作就可以了。
doc.add(newNumericField("day")
.setIntValue((int) (new Date().getTime()/24/3600)));
如果你需要进一步地量化到具体的月、年,或者你想对一天的某个时刻,或者一个星期或者月的某一天,你将不得不创建Calendar实例,再从它获取值。
Calendar cal = Calendar.getInstance();
cal.setTime(date);
doc.add(newNumericField("dayOfMonth")
.setIntValue(cal.get(Calendar.DAY_OF_MONTH)));
正如你看到的,Lucene使建立索引数字字段变得很繁琐。你已经看到几个将日期和时间转换成相等数字值。
现在然我们学习一下相关字段:truncation。