lucene学习

更新时间:2024-03-27 03:50:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

1. 基本概念

信息检索(IR)是指文档搜索、 文档内信息搜索或者文档相关的元数据搜索等操作。 文档:用于搜索的内容部件。

文档过滤器:将文本格式信息从原始内容中提取出来,便于后期建立搜索引擎文档。如Tika,与数据库的内容连接,DBSight、Hibernate Search、LuSQL、Compass和Oracle/Lucene集成项目。

词汇单元:即分词。词干提取器,如Snowball。

搜索质量主要由查准率(Precision)和查全率(Recall)来衡量。[1]P13 语法检查器: Lucene的 contrib目录提供了两个模块完成此功能。

查询对象: Lucene提供了一个称之为查询解析器(QueryParser),用它可以根据通用查询语法将用户输入的文本处理成查询对象。

查询搜索:査询检索索引并返回与査询语句匹配的文档,结果返回时 按照査询请求来排序。搜索查询组件涵盖了搜索引擎内部复杂的工作机制,Lucene正是 如此,它为你完成这一切。 倒排索引:inverted index

常见的搜索理论模型有如下3种。

■ 纯布尔模型(Pure Boolean model) 文档不管是否匹配查询请求,都不会被评分.在该模型下,匹配文档与评分不相关,也是无序的;一条查询仅获取所有匹配文档集合的一个子集。

■ 向量空间模型(Vector space model)

查询语句和文档都是高维空间的向

量模型,这里每一个独立的项都是一个维度。查询语句和文档之间的相关性或相似性由各自向量之间的距离计算得到.

■概率模型(Probabilistic model)在该模型中,采用全概率方法来计算文档和查询语句的匹配概率。

Lucene在实现上采用向量空间模型和纯布尔模型,并能针对具体搜索让你决定采用哪种模型。最后,Lucene返回的文档结果必须用比较经济的方式展现给用户。

搜索范围:涉及分布式搜索,Apache Lucene项目下的Solr和Nutch项目提供了对索引拆分和复制的支持,另Katta和Elastic search。

1.1 Lucene核心类概貌

执行简单的索引过程需要用到以下几个类: ■ IndexWriter ■ Directory ■ Analyzer ■ Document ■ Field

? IndexWriter (写索引)是索引过程的核心组件。这个类负责创建新索引或者打

开 已有索引,以及向索引中添加、删除或更新被索引文档的信息。可以把IndexWriter 看做这样一个对象:它为你提供针对索引文件的写人操作,但不能用于读取或搜索索引。 IndexWriter需要开辟一定空间来存储索引,该功能可以由Directory完成。

? Directory类描述了 Lucene索引的存放位置。它是一个抽象类,它的子类负

责具体指定索引的存储路径。在前面的Indexer例子中,我们用FSDirectory.open方法来获取真实文件在文件系统的存储路径,然后将它们依次传递给IndexWriter类构造方法。

? 文本文件在被索引之前,需要经过Analyzer (分析器)处理。Analyzer是由

IndexWriter 的构造方法来指定的,它负责从被索引文本文件中提取语汇单元,并提出剩下的无用信 息。如果被索引内容不是纯文本文件,那就就需要先

将其转换为文本文档。

? Document (文档)对象代表一些域(Field)的集合。你可以将Document对象

理解 为虚拟文档——比如Web页面、E-mail信息或者文本文件——然后你可以从中取回大量 数据。文档的域代表文档或者和文档相关的一些元数据。文档的数据源(比如数据库记 录、Word文档、书中的某章节等)对于Lucene来说是无关紧要的。Lucene只处理从二 进制文档中提取的以Field实例形式出现的文本。

? 索引中的每个文档都包含一个或多个不同命名的域,这些域包含在Field类

中。 每个域都有一个域名和对应的域值,以及一组选项来精确控制Lucene 索引操作各个域值。文档可能拥有不止一个同名的域。在这种情况下,域的值就按照索引操作顺序添加进去。在搜索时,所有域的文本就好像连接在一起,作为一个文本域来处理。

搜索操作: ■ IndexSearcher ■ Term ■ Query ■ TermQuery ■ TopDocs

? IndexSearcher类用于搜索由IndexWriter类创建的索引:这个类公开了几个

搜 索方法,它是连接索引的中心环节。可以将IndexSearcher类看做一个以只读方式打 开索引的类。它需要利用Directory实例来掌控前期创建的索引,然后才能提供大量 的搜索方法,其中一些方法在它的抽象父类Searcher中实现,最简单的搜索方法是将 单个Query对象和int topN计数作为该方法的参数,并返回一个TopDocs对象。

Directory dir = FSDirectory.open(new File(\; IndexSearcher searcher = new IndexSearcher(dir);

Query q = new TermQuery(new Term(\TopDocs hits - searcher.search(q,10); searcher.close();

? Term对象是搜索功能的基本单元。与Field对象类似,Term对象包含一对字

符串元 素:域名和单词(或域文本值)。注意Term对象还与索引操作有关。然而,由于Term 对象是由Lucene内部创建的,我们并不需要在索引阶段详细了解它们。在搜索过程中 可以创建Term对象,并和TermQuery对象一起使用:

Query q = new TermQuery(new Term(\; TopDocs hits = searcher.search(q, 10);

? Lucene含有许多具体的Query (查询)子类。到目前为止,我们谈到的只是

Lucene 基本的 Query 子类:TermQuery 类。其他的 Query 子类有:BooleanQuery、PhraseQuery、 PrefixQuery 、 PhrasePrefixQuery、 TermRangeQuery 、 NumericRangeQuery、 FilteredQuery和SpanQuery。 它包含了一些非常私用的方法,其中最有趣的当属 setBoost (float) 该方法能使你告知Lucene某个子查询相对其他子査询来说必须对最后的评分有更强贡献。 ? TopDocs类是一个简单的指针容器,指针一般指向前N个排名的搜索结果,

搜索结 果即匹配查询条件的文档。TopDocs会记录前N个结果中每个结果的int docID (可以 用它来恢复文档)和浮点型分数。

1.2 术语

规范化:将字母转换成小写 词干还原:将单词还原为词干形式

词形归并(lemmatization):将单词转换为基本形式 词汇单元(token):从文本流中提取的文本块 项(term):语汇单元与它的域名结合后的产物

2.构建索引

文档是Lucene索引和搜索的原子单位。文档为包含一个或多个域的容器,而域则 依次包含“真正的”被搜索内容。每个域都有一个标识名称,该名称为一个文本值或二 进制值。当你将文档加人到索引中时,可以通过一系列选项来控制Lucene的行为。在 对原始数据进行索引操作时,你得首先将数据转换成

Lucene所能识别的文档和域。在 随后的搜索过程中,被搜索对象则为域值,例如,用户在输人搜索内容“title:lucene” 时,搜索结果则为标题域值包含单词“lucene”的所有文档。

进一步地,Lucene可以针对域进行3种操作。

■ 域值可以被索引(或者不被索引)。如果需要搜索一个域,则必须首先对它进行 索引.被索引的域值必须是文本格式的(二进制格式的域值只能被存储而不能被 索引).在索引一个域时,需要首先使用分析过程将域值转换成语汇单元,然后 将语汇单元加入到索引中.有关索引域值的具体操作选项可以参考[1]2.4.1小节.

■域被索引后,还可以选择性地存储项向量,后者可以看做该域的一个小型反向 索引集合,通过该向量能够检索该域的所有语汇单元.这个机制有助于实现一些高级功能,比如搜索与当前文档相似的文档(更多的高级功能详见[1]5.7小节). 有关控制索引项向量的具体选项请参考[1]2.4.3小节.

■域值可以被单独存储,即是说被分析前的域值备份也可以写进索引中,以便后 续的检索.这个机制可以使你将原始域值展现给用户,比如文档的标题或摘要。 域值的存储选项请参考[1]2.4.2小节.

2.7 并发、线程安全及锁机制

并发操作

? 任意数量的只读属性的IndexReader类都可以同时打开一个索引.无论这些

Reader是否属于同一个JVM,以及是否属于同一台计算机都无关紧要.但需要 记住:在单个JVM内,利用资源和发挥效率的最好办法是用多线程共享单个的 IndexReader实例.例如,多个线程或进程并行搜索同一个索引.

? 对于一个索引来说,一次只能打开一个Writer. Lucene采用文件锁来提供保

障.一旦建立起IndexWriter对象,系统即会分配一个锁给它. 该锁只有当IndexWriter对象被关闭时才会释放。注意如果你使用IndexReader对象来改变索引的话 比如修改norms或者删除文档。这时IndexReader对象会作为Writer使用:它必须在修改上述内容之前成功地获取Write锁,并在被关闭时释放该锁.

? IndexReader对象甚至可以在IndexWriter对象正在修改索引时打开。每个

IndexReader对象将向索引展示自己被打开的时间点.该对象只有在 IndexWriter对象提交修改或自己被重新打开后才能获知索引的修改情况.所 以一个更好的选择是,在已经有IndexReader对象被打开的情况下,打开新 IndexReader时采用参数create=true:这样,新的IndexReader会持续检查索引的情况.

? 任意多个线程都可以共享同一个IndexReader 类或IndexWriter类。这些类不

仅是线程安全 的,而且是线程友好的,即是说它们能够很好地 扩展到新增线程(假定你的硬件支持并发访问, 因为这些类中标识为同步(synchronized)的代码 数并不多,仅为最小值).

2.7.1远程文件系统访问索引

Solr能比较好的支持索引复制策略。

对于不连贯的客户端缓存出现的间断性问题。程序将在open或reopen方法中抛出FileNotFoundException异常。该问题的解决方法:稍后重新操作即可,因为客户端缓存会在出现问题一段时间后自动修复。

NFS系统中客户端删除操作可能引起IOException异常。处理方式详见[1]P57

2.7.2 锁机制

详见[1]P57页起。

2.8 其他

IndexReader删除文档[1]P61

回收被删除文档所使用过的磁盘空间[1]P62 expungeDeletes 缓

[1]P62

setRAMBufferSizeMB

setMaxBufferedDocs

setMaxBufferedDeleteTerm。刷新操作是用来释放被缓存的更改的;提交操作是用来让所有的更改在索引中保持可视。 索引提交与事务[1]P63 两阶段提交 prepareCommit() 合并段[1]P66

3. 实现搜索

QueryParser可将输入的搜索语句转化为查询条件Query,而Query也可通过简单查询对象来构建。

IndexSearcher

大多数情况下我们搜索的索引都是存在于文件系统的: Directory dir = FSDirectory.open(new File(\然后建立一个 IndexReader:

IndexReader reader = IndexReader.open(dir); 最后,创建IndexSearcher:

IndexSearcher searcher = new IndexSearcher(reader);

由于打开一个IndexReader需要较大的系统开销,因此最好是在所有搜索期间都重复使用同一个IndexReader实例,只在必要时才打开新的IndexReader。 如需获取索引中的变更信息,必须打开一个新的reader。IndexReader.reopen可有效获取新的IndexReader,并耗费较少。可使用writer.getReader()获取处于相同jvm中IndexWriter的近实时reader,此速度更快。 IndexReader newReader = reader.reopen(); if (reader != newReader) {

reader.close(); reader = newReader;

searcher = new IndexSearcher(reader); }

如果索引有所变更,reopen方法值返回一个新的reader,这时程序必须关闭旧的reader并创建新的IndexSearcher。在实际的应用程序中,可能有多个线程还在使用旧的reader进行捜索,因此必须保证这段代码是线程安全的。

QueryParser

QueryParser parser = new QueryParser(Version matchVersion, String field,

Analyzer analyzer)

域名参数指所有被搜索项所定义的默认域,除了以下情况:搜索文本明确要求与 之匹配的域名必须用“field:text”的语法形式(详见[1]3.5.11小节)。然后调用parse()方法。

解析不成功会抛出ParseException异常。

TopDocs

TopDocs.totalHits属性会返回匹配文档数量。默认情况下,匹配文档是根据评分按照降 序排列的。TopDocs.scoreDocs属性是一个数组,它包含程序所要求的顶部匹配文档数量。 每个ScoreDoc实例都有一个浮点类型的评分,该评分是相关性评分,该实例还包括一个整 型的文档号,它用于标识文档ID,能够用于检索文档中保存的域,具体可以通过调用 IndexSearcher .document (doc) 方法进行。最后,TopDocs .getMaxScore () 方法会返回 所有匹配文档的最大评分,当使用相关性评分进行排序时(在默认情况下),返回的最大评分通常就是第一个搜索结果对应的评分。

分页功能

? 将首次搜索获得的多页搜索结果收集起来并保存在ScoreDocs和

IndexSearcher实 例中,并在用户换页浏览时展现这几页结果; ? 每次用户换页浏览时都重新进行查询操作。

重新查询通常是更好的解决方案。这个方案可以不用存储每个用户的当前浏览状态, 而这个操作对于Web应用程序来说开销巨大,特别是对于拥有巨大用户群的应用程序来 说。咋一看,重新查询似乎比较浪费,但Lucene的快速处理

能力正好可以弥补缺陷。 另外,得益于当今操作系统的I/O缓存机制,重新查询操作通常会很快完成,因为该操作所 需要处理的磁盘数据已经被缓存至RAM 了。

3.1 lucene评分机制

相似度评分公式,用来衡量查询语句和对应匹配文档之间的相似程度。分值计算方式为查询语句(q)中每个项(t)与文档(d)的匹配分值之和。

得到原始评分,然后对评分进行归一化处理,即用该条查询对应的评分除以最大评分。

IndexSearcher类包含一个explain方法,调用该方法需要传入一个Query对象和一个文档ID,然后该方法会返回一个Explanation对象。该对象包含评分计算的各种信息细节。

短语査询是根据短语匹配所需要的编辑距离来进行评分的。项之间的距离越小,具 有的权重也就越大。评分与距离成反比关系,距离越大的匹配其评分越低。

1

distance?13.2 预置查询类

查询最终是调用IndexSearcher.search方法,该方法需要Query对象作为参数,该对象可以由QueryParser分析产生,也可以使用预置的Query对象。

3.2.1 TermQuery 通过项搜索

对索引中特定项进行搜索是最基本的搜索方式。Term是最小的索引片段,每个Term 包含了一个域名和一个文本值。下面这段代码 初始化了一个Term实例: Term t = new Term(\

TermQuery构造方法允许一个单独的Term对象作为其参数: Query query = new TermQuery(t);

使用这个TermQuery对象进行搜索,可以返回在content域包含单词“java”的所有文档。值得注意的是,该査询值是区分大小写的,因此搜索前一定要对索引后的项大小写进行匹配。

3.2.2在指定的项范围内捜索:TermRangeQuery类

索引中的各个Term对象会按照字典编排顺序(通过String.compareTo方法)进行排序,并允许在Lucene的TermRangeQuery对象提供的范围内进行文本项的直接搜索。 搜索时包含或不包含起始项和终止项。如果这个项为空,那么它对应的端就是无边界的。 举例来说,一个空的lowerTerm意味着没有下边界,这样所有比上边界项小的项都会 被计算在内。该査询只适用于文本范围,比如搜索从N到Q范围内的域名称。下一节 提到的NumericRangeQuery可以用于数值域的范围查询。

下面的代码为TermRangeQuery的使用方法,其功能是搜索起始字母范围从d到j 的书籍标题。这里需要注意,书籍索引中的title2域是小写字母形式的,并使用Field. NOT_ANALYZED_NO_NORMS将该域索引成单个语汇单元。 public void testTermRangeQuery{) throws Exception { Directory dir = TestUtil.genBooklndexDirectory(); IndexSearcher searcher = new IndexSearcher(dir);

TermRangeQuery query = new TermRangeQuery ( \true, true);

TopDocs matches = searcher.search(query, 100); assertEquals(3, matches.totalHits);

searcher.close(); dir.close(); }

其中TermRangeQuery初始化方法中的的两个Boolean对象参数表示是(用true 表示)否(用false表示)包含捜索范围的起点或终点。该对象还可以处理自定义的Collator对象,并用于搜索范围的检查,但对于大的索引来说,这个处理异常缓慢,因为他在检查边界条件时需要列举出索引的每个项。CollationKeyAnalyzer可以提高程序性能。

3.2.3在指定的数字范围内捜索:NumericRangeQuery类

如果使用NumericField对象来索引域,那么你就能有效地使用NumericRange Query类在某个特定范围内搜索该域。Lucene会在后台将提交的搜索范围转换成与前面索引操作生成的树结构(trie structure)等效的括号集。这里每个括号都是索引中各不相同的项,它们对应的文档都会在搜索时进行或运算。使用NumericRangeQuery类搜索时所需要的括号数量相对较小,这使得该类运行时与TermRangeQuery类相比,性能好很多。

下面的程序示例,该程序基于书籍索引中的pub month域运行。程序将该域作为整数进行索引,并且索引精度为月,也就是说,2010年3月会被索引成域值为 201003的NumericField。程序接下来进行了一次范围内搜寻:

public void testlnclusive() throws Exception { Directory dir = TestUtil.getBooklndexDirectory(); IndexSearcher searcher = new IndexSearcher(dir); // pub date of TTC was September 2006

NainericRangeQuery query = NumericRangeQuery.newIntRange ( \200605,

200609, true, true);

TopDocs matches = searcher.search(query, 10); assertEquals(1, matches.totalHits); searcher.close(); dir.close{); }

程序最后newIntRange方法中的两个Boolean参数 表示搜索范围是(用true表示)否(用false表示)包含起点和终点。

该类可传入与NumericField相同的precisionStep参数。该参数的匹配详见Java文档。

3.2.4通过字符串捜索:PrefixQuery类

搜索程序使用PrefixQuery来搜索包含以指定字符串开头的项的文档。这个操作看起来是很容易的。下面的代码展示了如何通过简单的PrefixQuery对象对某个层次结构进行递归査询。包含category域的文档会呈现一个层次结构,该结构可以完美地用于匹配一个PrefixQuery对象

public class PrefixQueryTest extends TestCase { public void testPrefix() throws Exception {

Directory dir = TestUtil.getBooklndexDirectory(); IndexSearcher searcher = new IndexSearcher(dir); Term term = new Term(\

\;

PrefixQuery query = new PrefixQuery(term); //搜索编程方面的书籍,包括它们的子类书籍 TopDocs matches = searcher.search(query, 10); int progratnmingAndBelow = matches. totalHits; // 搜素编程方面的书籍不包括它们的子类

matches = searcher. search (new TermQuery (term) , 10) ; int justProgramming = matches.totalHits

assertTrue(programmingAndBelow > justPrograiraming); searcher.close(); dir.close(); } }

PrefixQueryTest 类展示了 PrefixQuery 和 TermQuery 之间的差异。methodology 分类存在于/technology/computers/programming分类目录下。在这个methodology子类中的书籍程序通过PrefixQuery,而不是通过TermQuery找到的。

3.2.5组合查询:BooleanQuery 类

通过使用BooleanQuery类可以将本章讨论的各种查询类型组合成复杂的查询方式, 而BooleanQuery本身是一个Boolean子句(clauses)的容器。这个子句可以是表示逻辑 “与”、逻辑“或”或者逻辑“非”的一个子査询。这些属性允许

进行逻辑AND、OR和NOT 组合。你可以调用如下API的方法将査询子句加入到BooleanQuery对象中:

public void add(Query query, BooleanClause.Occur occur)

这里 Occur 对象可以设置为 BooleanClause .Occur .MUST、BooleanClause.Occur. SHOULD 或者 BooleanClause. Occur.MUST_NOT。BooleanQuery对象还可以作为另一个BooleanQuery对象的字句,这样就允许它们任意地嵌套了。 public void testAnd() throws Exception { TermQuery searchingBooks =

new TermQuery (new Term( \; Query books2010 =

NumericRangeQuery.newIntRange(\ 201012, true, true);

BooleanQuery searchingBooks2010 = new BooleanQuery();

searchingBooks2010.add(searchingBooks, BooleanClause.Occur.MUST); ? searchingBooks2010.add(books2010, BooleanClause.Occur.MUST); Directory dir = TestUtil.getBooklndexDirectory(); IndexSearcher searcher = new IndexSearcher(dir);

TopDocs matches = searcher.search{searchingBooks2010, 10); assertTrue(TestUtil.hitslncludeTitle(searcher, matches, .\searcher.close(); dir.close(); )

BooleanClause.Occur.MUST的精确含义是:只有匹配该查询子句的文档才在考虑之列。BooleanClause. Occur .SHOULD意味着该项只是可选项,可用于或选项。BooleanClause. Occur .MUST_NOT意味着搜索结果不会包含任何匹配该查询子句的文档。

BooleanQuery对其中包含的查询子句是有数量限制的,默认情况下允许包含1024 个査询子句。该限制主要是为了防止时对应用程序的性能造成影响。当子句数量超过最大 值时,程序会抛出TooManyClauses异常。对于Lucene早期版本来说,这个限制是必要 的,因为某些查询语句可能会在后台被改写成等效的BooleanQuery类。但从版本2.9开始, Lucene会以一种更有效地方式处理这些查询语句。如果你在一些特殊情况下需要增大查 询子句的数量限制,可以使用BooleanQuery类提供的ClauseCount (int)方法进行设置,但设置前需要意识到这个做法对程序性能产生的影响。

3.2.6通过短语搜索:PhraseQuery类

索引时如果不用omitTermFreqAndPositions选项(详见[1]2.4.1小节)建立纯Boolean 域的话,索引会根据默认设置包含各个项的位置信息。PhraseQuery类会根据这些位置信息定位某个距离范围内的项所对应的文档。例如,假设某个域中包含短语“the quick brown fox jumped over the lazy\即时我们不知道这个短语的完整写法,也一样可以通过 查找域中quick和fox相关并且相距很近的文档。当然,一个简单的TermQuery类也能够通 过对这两个项的单独査询而找到同样的文档,但在该例中,我们仅仅希望査到域中quick紧 邻fox (quick fox)的或者两者之间只有一个单词(quick [其他单词]fox)的文档。

在匹配的情况下,两个项的位置之间所允许的最大间隔距离称为slop。这里的距离是指项若要按顺序组成给定的短语所需要移动位置的次数。 public class PhraseQueryTest extends TestCase { private Directory dir;

private IndexSearcher searcher;

protected void setup() throws IOException { dir = new RAMDirectory();

IndexWriter writer = new IndexWriter(dir, new WhitespaceAnalyzer{),

IndexWriter.MaxFieldLength.UNLIMITED); Document doc = new Document(); doc.add(new Field(\

\Field .Store. YES , Field.Index.ANALYZED)); writer.addDocument(doc); writer.close();

searcher = new IndexSearcher(dir); )

protected void tearDown() throws IOException {

searcher.close(); dir.close(); }

private boolean matched(String[] phrase, int slop) throws IOException { PhraseQuery query = new PhraseQuery(); query.setSlop(slop);

//初始化PhraseQuery对象

for (String word : phrase) {

query.add (new Term(\}

TopDocs matches = searcher.search(query, 10) ; return matches.totalHits > 0; } }

如果进入query的短语顺序不对,则slop需要按移动次数进行调整,后面的移到前面位置,同时前面的也要移到后面位置,所以slop数更多。

PhraseQuery支持复合项短语,slop因子规定了按顺序移动项位置的总次数的最大值。

在QueryParser的分析表达式中,双引号里面的若干个项被转换成一个 PhraseQuery对象.Slop因子的默认值是0,但是你可以在QueryParser的查询表 达式中加上~n声明,以此来调整slop因子的值.例如,表达式“quick fox\的意思为:为fox和quick项生成一个slop因子为3的PhraseQuery对象. 与之相对应 SpanNearQuery能确保按次序的匹配。

3.2.7通配符查询:WildcardQuery 类

通配符查询可以让我们使用不完整的、缺少某些字母的项进行查询,但是仍然可以查到相关匹配结果。Lucene使用两个标准的通配符:*代表0个或者多个字 母,?代表0个或者1个字母。

3.2.8 搜索类似项:FuzzyQuery类

Lucene的模糊查询FuzzyQuery类用于匹配与指定项相似的项。Levenshtein距离算法用来决定索引文件中的项与指定目标项的相似程度(有关Levenshtein距离算法的更多信息请参考http://eitwikipedia. org/wiki/ Levenshtein_Distance)。这种算法又称为编辑距离算法,它是两个字符串之间相似度的一个度量方法,编辑距离就是用来计算从一个字符串转换到另一个字符串所需的最少插入、删除和替换的字母个数。例如,“three”和“tree”两个字符串的编辑距离为1,因为前者只需要删除一个字符它们就一样了。

Levenshtein距离计算不同于PhraseQuery和PhrasePrefixQuery中所使用的距离 计算方法。短语查询距离是为了匹配目标文档短语所需移动项的次数,而Levenshtein 距离则是一个项内部字母移动的次数。

FuzzyQuery通过一个阈值来控制 匹配,而不是单纯依靠编辑距离。这个阈值是通过字符串长度除以编辑距离得到的一个因子。编辑距离能影响匹配结果的评分,编辑距离越小的项所获得的评分就越高。

1?distance

min(textlen,targetlen)该类会尽可能枚举出一个索引中所有项,对程序性能会有所影响。

3.2.9匹配所有文档:MatchAIIDocsQuery 类

MatchAilDocsQuery类,顾名思义,就是匹配索引中所有文档。默认情况下, 该类对匹配的文档分配了一个固定的评分,该文档具体的査询加权默认值为1.0。

如果你将该查询作为顶层查询,那么除了使用默认的相关性排序之外,最好通过域来排序。

对于一些特殊域来说,还可以用MatchAHDocsQuery来为索引中的文档进行评分 加权,操作如下:

Query query = new MatchAHDocsQuery (field);

如果执行这样的操作,文档就会根据该特定域的加权情况而被评分(详见[1]2.5 小节)。

3.3 解析查询表达式:QueryParser

QueryParser使用反斜杠(\\)表示转义字符。需转义的字符有: \\ + - ! ( ) : ^ ] { } ~ * ?

Query 的 toString 方法可以查看构建的复杂查询,也可看到QueryParser对查询的解析结果。

3.3.1 单词查询

单个词的查询相当于TermQuery,被解析为 field:key

3.3.2 范围搜索

范围搜索:中括号表示包含在内;大括号表示排除在外。起止点要么都包含在内,要么都排除在外。文本范围查询会将查询边界转换为小写字母形式,除非程序调用了QueryParser.setLowercaseExtendedTerms(false)。如果起止点内包含空格,则必须用双引号括起来。

3.3.3 前缀查询和通配符查询

当查询包含了星号或问号,就被当做通配符查询对象WildcardQuery。当查询只在末尾有一个星号时,QueryParser将优化为前缀查询对象PrefixQuery。默认查

询对象都会被转化为小写字母形式,解除这一默认值,调用QueryParser.setLowercaseExtendedTerms(false)。要在项开端包含通配符,需要调用setAllowLeadingWildcard方法,同时牺牲一部分性能。

3.3.4 布尔查询

AND OR NOT 必须大写。如果查询项之间没有指定布尔操作符,则默认为OR。程序可改变对隐含操作符的设置。

parser.setDefaultOperator(QueryParser.AND_OPERATOR); NOT操作单独使用将不起作用。

3.3.5 短语查询

查询语句中用双引号扩起来的项可以用来创建一个PhraseQuery。引号之间的文本将被进行分析,作为分析结果,PhraseQuery可能不会跟原始短语一样精确。双引号内的文本会促使分析器将之转换为PhraseQuery,停用词被问号代替,默认Slop因子为0,但可以调用QueryParser.setPhraseSlop()方法来改变这个默认因子,也可以用波浪号(~)和预期的整型slop值来修改。单项短语(single-term phrase)将被优化成TermQuery对象。

3.3.6 模糊查询

波浪符(~)会针对正在处理的项来创建模糊查询。需要注意的是,波浪符还可以用于指定松散短语查询,但具体环境是各不相同的。双引号表示短语查询,它并不能用于模糊査询。你可以选择性指定一个浮点数,用来表示所需的最小相似程度。下面是使用案例:

public void testFuzzyQuery() throws Exception {

QueryParser parser = new QueryParser{Version.LUCENE_30, \;

Query query = parser.parse(\

System.out.printIn(\query = parser.parse(\System.out.printIn(\)

该程序会产生如下输出:. fuzzy: subject:kountry~0.5 fuzzy 2: subject:kcountry~0.7

同样的性能可以揭示:使用通配符查询(WildcardQuery)和使用模糊査询是一致 的,它们还可以通过自定义方式禁止运行,具体可以参考[1]6.3.2小节。

3.3.7 MatchAIIDocsQuery

当输人*:*后,QueryParser 会生成 MatchAIIDocsQuery。

这个操作会将QueryParser所产生的所有Lucene核心査询类型进行包装。但这并 不是QueryParser的一切:它还支持一些非常实用的Query子句分组语法、加权子句以及将子句限制在特定的域上。

3.3.8 分组查询、域选择与加权

使用小括号建立子查询。创建查询时,默认的域名是创建QueryParser时提供的。使用 field:key可将搜索范围限定在某一个具体域,也可以使用field(a b c)的形式将三个项的查询以逻辑OR的形式连接起来。

在浮点数前面附上一个^符号可以对查询处理进行加权因子的设置。举例来说,查询表达式junit^2.0 testing会将junit TermQuery的加权系数设置为2.0,并维持 testing TermQuery的默认加权系数1.0。你可以对任何类型的查询进行加权,这其中包括插入的组。

ANTLR和JFlex项目可提供高于QueryParser能力的语法和功能。

4. 分析器与分析过程

对于Lucene核心来说,分析操作会出现在两个时间点:建立索引期间和使用QueryParser对象进行搜索时。在搜索结果中高亮显示被搜索内容时,也可能要用到分析操作。

几个内置分析器的解析示例如下:

4.1内置分析器说明

? WhitespaceAnalyzer:顾名思义,该分析器通过空格来分割文本信息,而并不

对生成的语汇单元进行其他的规范化处理.

? SimpIeAnalyzer:该分析器会首先通过非字母字符来分割文本信息,然后将语

汇单元统一为小写形式。需要注意的是,该分析器会去掉数字类型的字符, 但会保留其他字符。

? StopAnalyzer:该分析器功能与SimpIeAnalyzer类似.区别在于,前者会去除常

用单词.在默认情况下,它会去除英文中的常用单词(如the、a等),但你也可以根据需要自己设置常用单词.

? StandardAnalyzer:这是Lucene最复杂的核心分析器。基于JFlex-based语法

操作。它包含大量的逻辑操作来识别某些种类的语汇单元,比如公司名称、E-mail地址以及主机名称等. 它还会将语汇单元转换成小写形式,并去除停用词和标点符号.

详见[1] 第4.3节

在真正应用中,一般创建自己的分析器链。这需要自己扩展TokenFilter和Analyzer,一个例子见[1]4.4同音词,4.5同义词处理,4.6词干分析

4.2 分析过程简述

更多描述见[1]第四章

创建IndexWriter后,即为文档选择了默认的分析器。若某个文档需要使用特殊的分析器时,addDocument方法和updateDocument方法都可以选择对应的分析器。为了确保文本信息被分析器处理,可以在创建域时指定Field.Index.ANALYZED

或者Field. Index.ANLYZED_NO_NORMS参数。若需要将整个域值作为一个语汇单元处 理,如图 4.1 中 Field 3 所示,则可以将 Field. Index.NOT_ANALYZED 或 Field. Index. NOT_ANALYZED_NO_NORMS作为第4个参数传人该域。 Analyzer通过TokenStream类将文本逐字转换为语汇单元流。

语汇单元是分析过程产生的基本段元,携带文本值和其他一些元数据。[1]P111 项(term),位置增量(position-Increment)、 偏移量(offset)、类型(type)、标志位(flags)和有效负载。位置增量为0通常用于插入同义词或其他类似信息。

如:

public TokenStream tokenStream{String fieldName, Reader reader) { return new StopFilter{true,

new LowerCaseTokenizer(reader), stopWords); )

过滤顺序对分析过程的影响很重要。[1]P121

AnalyzerUtils的displayTokens(analyzer,text)方法可对文本分析过程进行显示。 AnalyzerDemo可以在命令行显示这一过程。详见[1]P117

4.3 域分析与处理,多值域处理

一个文档可能有多个同名域,这些域建立索引时会附加在一起,如需分离,可继承Analyzer类,并重载getPositionIncrementGap方法,将返回值增加到足够大,以表示两个域相隔较远。

针对不同的特定域使用不同的分析器,需要自己创建分析器,另外也可使用内置工具类 PerFieldAnalyzerWapper。其addAnalyzer方法可针对不同域添加新的

分析器。

只有在索引期间才知道一个域是否被分析过,该域的项一旦被编入索引,就与其他项完全一样。该问题非常重要,参见[1]P137。组合使用KeywordAnalyzer。索引与分析时,均可以对特定域使用特定分析器,这可以减少NOT_ANALYZED_NO_NORMS的使用。

4.4语言分析

对于不同语言的规范化处理,通常需要用到字符过滤类,CharReader、CharStream、CharFilter等。

适用于亚洲语言的分析器,contrib目录中:CJK、 Chinese、 Smart Chinese。

4.5 一些项目中使用的分析技术

Nutch 分析技术: 二元语法技术[1] 4.9节

5.高级搜索技术 5.1 域缓存

使用域缓存可以根据域值对搜索结果进行排序,在后台使用域缓存等。使用域缓存的一个重要限制是所有文档都必须拥有一个与指定域对应的单一域值,因此可能不能处理多值域,需要看版本支持。

域缓存只能用于包含单个项的域.这通常意味着该域在被索引时需要使用参

数Index .NOT_ANALYZED 或 Index . N0T_ANALYZED_N0_N0RMS ,如果使用诸如 Keyword- Analyzer等分析器的话,这些域还是可以被分析的,该分析过程只产生一个语汇单元. 如下使用方式:

如果每个文档都有一个称之为“weight”的域,那么你就可以获得每个文档中该域的域值,操作如下:

float [] weights = FieldCache.DEFAULT.getFloats(reader, \

然后,在需要知道产品文档重量的时候加人引用weights [dOCID]即可。域缓存支 持很多本地数据类型:byte、short、int、long、float, double、string 以及 Stringlndex 类,最后一项包括string值的排序顺序。

域缓存的初次建立耗费较大。只有reader关闭并删除其对应引用时,该缓存才会释放。

5.2 搜索结果排序、按不同语言字符集排序

能对结果进行排序的方法可调用search (Query,Filter,int, Sort)。如果不需要对结果进 行过滤的话,需要将Filter对象置为null。

使用IndexSearcher类的setDefaultFieldSortScoring () 方法,该方法有两个布尔类型参数:doTrackScores和doMaxScore。如果 doTrackScores参数为true,那么每个搜索命中结果都会被进行评分操作。如果 doMaxScore为true,那么程序将对最大分值的搜索命中结果进行评分操作。需要注意 的是,后者通常会耗费更多的系统资源,因为前者只对最有效的搜索结果进行评分操作。 按照相关性排序,默认排序方式。Sort.RELEVANCE 按索引顺序排序,Sort.INDEXORDER

通过域排序,当域整个被索引成单个语汇单元时可用,即不分析。当域内容存储时,可使用如下例的方式:

new Sort(new SortField(\

默认情况下,Lucene排序方向使用的是自然排序方式。对相关度采用降序排列,对其他域采用升序排列。需进行倒序处理,如下: new Sort(new SortField(\

需要对多个域进行排序时,创建Sort对象时,加入多个SortField对象。SortField 对象都包含域名、域类型和反向排序标志位。另外,SortField对象还包含集中域类型的常 量,它们是 SCORE、DOC、STRING, BYTE, SHORT、INT、LONG、FLOAT 和 DOUBLE。 其中SCORE和DOC常量是用于针对相关性和文档ID进行排序的特殊类型。

按照SortField.STRING类型排序时,程序内部会默认地调用String.compareTo() 方法来确定排列次序。不过,如果需要另一种不同的排列顺序,SortField类允许你通过制定一个本地化的方法来达到这个目的。Collator对象是通过Collator.getInstance(Locale)方法来初始化的,而Collator.compare()方法则用于决定排序方式。在指定Locale对象时,有两个可重载的SortField初始化方法可用: public SortField (String field, Locale locale)

public SortField (String field, Locale locale, boolean reverse)

这两个构造方法所创建的对象都是按照SortField.STRING类型排序的SortField对 象,因为Locale对象只适用于字符类型的排序,而不能用于数值类型。

5.3 MultiPhraseQuery

可用来将不同位置的相似匹配进行组合。如:

MultiPhraseQuery query = new MultiPhraseQuery(); query.add(new Term[]{ new Term(\允许其中一个项被首 new Term(\ });

query.add(new Term(\

先匹配

5.4 针对多个域的查询

第一种实现方式就是创建多值的全包含域来对所有域的文本进行索引。一定要在添加域值时对域值之间的空格进行位置增量处理,这样能避免程序错误地将两个域之间的域值进行査询匹配操作。然后程序就可以对这个全包含域进行搜索了。该方案有一些缺陷:你不能直接对每个域的加权进行控制,并且假如你还分开使用这些域的话,它会浪费磁盘空间。

第二种实现方式就是使用MultiFieldQueryParser,它是QueryParser的子类。它在会在后台程序中实例化一个QueryParser对象,用来针对每个域进行查询表达式的解析,然后使用BooleanQuery将查询结果合并起来。当程序向BooleanQuery添加查询子句时,默认操作符OR被用于最简单的解析方法中。为了实现更好的控制,布尔操作符可以使用BooleanClause的常量指定给每个域,如果需要指定的话可以使用 BooleanClause.Occur.MUST ,如果禁止指定可以使用 BooleanClause. Occur.MUST_NOT,或者普通情况为 BooleanClause.Occur. SHOULD。该方法用于复杂查询时,情况会更加复杂,需要分析以确定查询模式正确。

用于自动査询多值域的第三种实现方式就是使用髙级DisjunctionMaxQuery类,它会封装一个或多个任意的查询,将匹配的文档进行OR操作。你可以使用 BooleanQuery完成这个功能,正如MultiFieldQueryParser所完成的那样,但 DisjunctionMaxQuery的有趣之处在于它的评分方式:当某个文档匹配到多于一条查询时,该类会将这个文档的评分记为最髙分,而与BooleanQuery相比,后者会将所有匹配的评分相加。这样能产生更好的终端用户相关性。

DisjunctionMaxQuery还包含一个可选的仲裁器,因此所有处理都是平等的,一个 匹配更多查询的文档能够获得更高的评分。为了用DisjunctionMaxQuery进行多值域 査询,你需要创建一个新的基于域的Query,该Query得包含所有需要用到的域,然后 使用DisjunctionMaxQuery的add方法来包含这个Query。

5.5 跨度查询

即域中的启示语汇单元与终止语汇单元的位置。在搜索期间,跨度查询所跟踪的文档要比匹配的文档要多:每个单独的跨度(每个域可包含多个跨度)都会被跟踪。与TermQuery做个比较,TermQuery只是对文档进行简单的匹配操作,而SpanTermQuery除了完成这个功能外,还会保留每个匹配文档 对应的项位置信息。总的来说,跨度查询是一种计算密集型操作。

详见[1]5.5节,在短语查询及短语嵌套查询中,以上类能实现QueryPhrase不能实现的功能。

5.6 搜索过滤

过滤是Lucene中用于缩小搜索空间的一种机制,它把可能的搜索匹配结果仅限制在 所有文档的一个子集中。它们可以用来对已经得到的搜索匹配结果进行进一步搜索,以实现在搜索结果中的继续搜索(search-within-search)特性。此外,它们还可以用来限制文 档的搜索空间。安全过滤器允许用户只能看见属于“自己的”文档搜索结果,即使这些査 询实际上还匹配了其他的文档。

通过重载一个带有Filter对象参数的search()方法,你可以对任何一个Lucene搜索进行过滤。下面介绍一些Lucene内置的过滤器子类。

? TermRangeFilter只对包含特定项范围的文档进行匹配操作.功能与

TermRangeQuery—致,但前者没有评分操作.

? NumericRangeFilter只对特定域的特定數值范围进行文档匹配操作.功能与

NumericRangeQuery 一致,但前者没有评分搮作。

? FieldCacheRangeFilter针对某个项或者某个数值范围进行文档匹配操作,使用

时可以结合FieldCache (详见[1]5.1小节)获得更好的性能表现.

? FieldCacheTermsFilter针对特定项进行文档匹配操作,使用时可以结合

FieldCache获得更好的性能表现。

? QueryWrapperFilter可以将任意Query实例转换为Filter实例,转换时仅将

Query实例对应的匹配文档作为过滤空间,该操作忽略文档评分.

? SpanQueryFilter将SpanQuery实例转换成SpanFilter实例,这个操作会将Filter

基类派生为对应的子类并且会在子类中新增一个方法,用于为每个文档匹配操作提供位置跨度访问.该类与QueryWrapperFilter类似,只不过前者的转换结果为SpanQuery类。

? PrefixFilter只匹配包含特定域和特定前缀的项的文档.功能与PrefixQuery—致,

但前者没有评分操作。

? CachingWrapperFilter是其他过滤器的封装器(Decorator),它将结果缓存起来

以便再次使用,从而提高系统性能。

? CachingSpanFilter 与 CachingWrapperFilter 功能一致,但前者的缓冲目标是

SpanFilter.

? FilteredDocIdSet允许对过滤器进行过滤,一次处理一个文档。使用它时, 必

须首先派生它的子类,然后在该子类中定义匹配方法。 详见[1]5.6节

QueryWrapperFilter使用查询中匹配的文档来对随后搜索中可以访问的文档进行 限制。它允许你将带有评分功能的查询转换为不带评分功能的过滤器。使用 QueryWrapperFilter可以将被搜索文档限制在特定的类别范围内。 public void testQueryWrapperFiIter() throws Exception { TermQuery categoryQuery =

new TermQuery(new Term(\; Filter categoryFilter = new QueryWrapperFilter(categoryQuery); assertEquals(\1,

TestUtil.hitCount(searcher, allBooks, categoryFilter}); )

SpanQueryFilter与之类似。

对于一些安全权限应用,在索引期间将文档和用户或角色关联起来,然后使用上述类,就可以限定在一定范围内。

还可以进行反向操作,使用 ConstantScoreQuery将过滤器转换成查询以用于随后

的搜索。生成的查询只对过滤器 所包含的文档进行匹配,然后赋予它们与查询加权相等的评分。

FilteredDocldSet类是一个抽象类,它能够接受一个filter参数,在随后的匹配操作期间,只要碰到一个文档,它就会调用对应的match方法(即我们所实现的其子类的同名方法)来检查该文档是否匹配。这使得你能够自定义实现match方法 中的逻辑,并能够对其他过滤器进行动态过滤。该方法效率很高,因为 FilteredDocldSet从不为过滤器完全分配字节空间。而实际上,每个匹配操作都是 根据程序要求而运行的。

该方法可以用来加强权限操作,特别是在大量权限为静态(当前在索引中)但又需要动态检查一定数量权限的时候非常有用。对于这样一个使用案例,你需要创建一个基于索引内容的标准权限过滤器,然后实现FilteredDocIdSet子类,并覆盖其match 方法以实现自己的动态权限逻辑。

lucene contrib模块有一个ChainedFilter过滤器,可组成复杂的过滤器链。

5.7 使用功能查询实现自定义评分

功能查询带给你一定的自由度来通过编程的形式使用自己的逻辑来对匹配文档进 行评分。所有用到的类都来自于org .apache .lucene. search. function程序包。

所有功能査询类的基类是ValueSourceQuery。该查询会对所有文档进行匹配操作, 但对每个匹配文档的评分是通过该类初始化时传人的ValueSource而设置的。该程序包提供了一个FieldCacheSource子类,它负责从域缓存中导出域值。你还可以创建自 定义的ValueSource——举例来说,通过这个自定义的类从外部数据库中导出评分。但很可能最简单的方案是使用FieldScoreQuery,它是ValueSourceQuery的子类,并能从指定的索引域中静态导出每个文档的评分。该域必须是数值型的,并且不能使用 norms索引,以及每个文档中该域只能由一个语汇单元。通常你可以使用 Field. Index.NOT_ANALYZED_NO_NORMS参数进行语汇单元化处理。一个样例如下:

首先,文档中需要包含“score”域,如下所示:

doc.add(new Field(\\

Field.Store.NO,

Field.Index.NOT_ANALYZED_NO_NORMS)); 然后,创建功能查询:

Query g = new FieldScoreQuery(\

该查询会对所有文档进行匹配操作,并根据文档中的“score”域而对每个文档賦予 评分。你还可以使用SHORT、INT或FLOAT类型的常量。在程序后台,该功能査询会使 用域缓存,因此这里也需要对此进行[1]5.1小节中的所提到的性能权衡。

当你使用第二种功能查询CustomScoreQuery时,就能实 现一些有趣的功能。该查询使你能够将通常的Lucene査询和一个或多个其他功能查询 联合起来使用。 现在我们可以使用先前创建的FieldScoreQuery类以及一个CustomScoreQuery类来 计算评分:

Query q - new QueryParser(Version.LUCENE_30, \

new StandardAnalyzer( Version.LUCENE_3 0)) .parse{\;

FieldScoreQuery qf = new FieldScoreQuery(\FieldScoreQuery.Type.BYTE);

CustomScoreQuery customQ = new CustomScoreQuery(q, qf) {

public CustomScoreProvider getCustomScoreProvider(IndexReader r) { return new CustomScoreProvider(r) { public float customScore(int doc, float subQueryScore, float valSrcScore) {

return (float) (Math.sqrt(subQueryScore) * valSrcScore);

在本示例中,我们通过解析用户的搜索文本而创建了一个通常的查询q。接下来

我们将创建先前使用过的同样的FieldScoreQuery,并根据score域来对文档进行评最后,我们创建了 一个 CustomScoreQuery 类,并覆盖了其中的 getCustomScoreProvider 方法, 让其返回一个包含针对每个匹配文档所采用的自定义评分算法的类。在这个人为的案例中,我们将查询评分开平方,并随后用它与FieldScoreQuerry所提供的静态评分相乘。你可以使用任意的程序逻辑来创建自己的评分系统。

需要注意的是,传给getCustomScoreProvider的IndexReader参数是针对段的,意思是如果索引包含的段不止一个,那么搜索期间会多次调用这个方法。强调这点是重要的, 因为它使你的评分逻辑能够有效使用段reader来对域缓存中的值进行检索。 其他示例见[1]5.7节

5.8 针对多索引的搜索

某些应用程序需要保持多个分离的Lucene索引,但又需要在捜索过程中能够使针 对这几个索引的所有搜索结果合并输出。有时候,导致这类分离索引出现可能是为了方便程序运行或者管理上的原因——例如,如果不同的用户或者组织为不同的文档集合创建了不同的索引,就会导致多个分离索引的出现。有时这种情况的出现是为了增大文档容量。例如,一个新闻网站可能每月新创建一个索引,然后在搜索时指定该月份对应的索引即可。出于这些原因,Lucene提供了两个很实用的类来针对多索引进行搜索。

使用Multisearcher类可以搜索到所有索引,并以一种指定的顺序(或者是以评分递 减的顺序)将搜索结果合并起来。多索引搜索(MultiSearcher)类的使用是相对于单索引搜索(IndexSearcher)而言的,除非你通过一组IndexSearcher对象去搜索一个以上 的目录(因而这是一^髙效的封装模式,它将大部分工作都委托给了子搜索器完成)。

使用方法很简单,new MultiSearcher(IndexSearcher[] searchers),就可以直接使用了。

ParallelMultiSearcher是MultiSearcher的多线程版本,它会为每个

Searchable对象分配一个新线程,然后在程序调用搜索方法时等待这些线程进行处理直到处理完毕。基本的搜索和搜索过滤操作时并行执行的,但是基于Collector的搜索暂时还不能进行并行处理。Lucene 公开的 ParallelMultiSearcher API 与 MultiSearcher一致,使用起来是很简单。

使用ParallelMultiSearcher是否能够获得性能提升取决于你的程序架构。如果各个索引位于不同的物理硬盘并且你的计算机支持CPU并发并发处理的话,使用 ParallelMultiSearcher就能获得性能提升。

在 Lucene 的 contrib/remote 目录还有一个 ParallelMultiSearcher 的姊妹类,它 允许你对多个索引进行远程并行搜索。

5.9项向量

从技术上讲,项向量是一组由项-频率对(term-frequency pair)组成的集合,该向量还可选择性包含各个项出现的位置信息。我们之中很多人可能很难在多维空间想象向量的样子,因此为了将向量概念可视化,我们看看只包含cat 和dog这两个项的两个文档。这两个单词在每个文档中都出现了很多次。我们在二维空间的X、Y坐标上标记出这两个项出现的频率,如图5.5所示。我们感兴趣的是两个项向量之间的夹角

夹角小,则相似度高。 项向量说明参见[1]2.4.3节。

我们在索引书籍样本数据时,曾使用项向量索引过标题,作者,主题和内容域。在一个给定文档中通过ID来检索域的项向量时,需要调用一个IndexReader所包含的方法:

TermFreqVector termFreqVector =

reader.getTermFreqVector(id, \ TermFreqVector实例有几个方法用于检索向量信息,它们主要用于返回一些字符串和整形数组(它们分别表示域中某个项的值以及该项在域中出现的频率)。如果你的程序还需要保存项向景的位置偏移量和(或)位置信息,可以使用Field. TermVector.WITH_POSITIONS_OFFSETS选项,然后你会在加载项向量时得到一 个TermPositionVector对象。该对象包含文档中有关每个项出现的位置信息以及位置偏移量。

查找相似书籍的例子参见[1]5.9.1节。Lucene包含了一个LikeThisQuery的类实现。参见[1]8.6.1节。

自动分类的例子参见[1]5.9.2节。

有时,IndexReader .getTermFreqVector ()所返回的平行整列结构可能并不方便 程序使用。也许除了通过Term进行排序之外,你还想通过自己的准则来对项向量进行排序。或者你可能想只对一些有用的项进行加载。所有这些需求都可以通过Lucene最近新增的TermVectorMapper完成。它是一个抽象基类,当它的子类被传递给 IndexReader.getTermFreqVector()方法后,能够分别接收各个项,同时能根据各个项的位置和偏移量信息来选择用自己的方式存储对应的数据。表5.2描述了 TermVectorMapper子类所必须实现的方法。 表5.2 自定义TermVectorMapper类必须实现的方法 方 法 功 能 setDocumentNumber 针对毎个文档调用一次,并返回目前正在被加载的文档 setExpectacions 针对每个域调用一次.并返回域中包含的项数量,以及对应的位置和偏移鍵是否已被存储 map isIgnoringPositions isIgnoringOffsets 针对每个项调用一次,提供实际的项向量数据 只有在需要查看项向量的位置信息时才返回false 只有在需要査看项向量的位置偏移信息时才返回false Lucene包含一些有关TermVectorMapper类的核心子类,如表5.3所示。你还可以创建自己的对应子类。

表5.3 TermVectort!apper的内置实现方法 方 法 功 能 PositionBasedTermVectorMapper 对于每个域,都保存其项与对应型位置信息的映射关系,该位置对应的偏移量以可选方式保存 SortedTermVectorMapper 将所有域的项向量合并成一个单一的SortedSet.将它们以指定 Comparator的形式排序.有一个Comparator是由Lucene的核 心类 TermVectorEntryFreqSortedComparator 提供的,它初次 以项频率作为排序标准,后续以项本身作为排序标准 FieldSortedTermVectorMapper 与SortedTermVectorMapper类似,区别在于域并不被合并,而 是让每个域分别保存对应的项

5.10 选择加载域:FieldSelector

FieldSelector 类的位置在 org .apache . lucene .document 程序包中, 它允许你针对每个文档加载特定范围的域集合。这个接口带有一个简单的方法: FieldSelectorResult accept(String fieldName);

实现该接口的类会返回一个FieldSelectorResult对象,用来描述是否需要对特定域名的域进行加载,以及如何加载。FieldSelectorResult是一个带有七种值的枚举类型,如表5.4所示。

选 项 LOAD LAZY_LOAD 功 能 加载域 延迟加载域.只有在程序调用Field.stringValue()或者Field.binaryValue() 方法时才会实际加载域. NO_LOAD 跳过该域的加载 LOAD_AND_BREAK 加载完该域后停止加載剩余的域 LOAD_FOR_MERGE 内部用于段合并期间的域加載,该操作会跳过对压缩域的解压处理 SIZE 只读取域的长度,然后用4字节长的数组对该长度进行編码并形成一个新的二进制域 SIZE_AND_BREAK 与SIZE选项类似,但不再加载剰余的域

当使用FieldSelector加载存储域时,IndexReader会逐个访问文档中的域,访 问顺序是根据索引期间的添加顺序决定的。IndexReader会针对每一个域调用 FieldSelector并根据返回的结果来加载(或不加载)该域。 表5.5 类 FieldSelector 核心实现 功 能 LoadFirstFieldSelector 只对遇到的第一个域进行加載 MapFieldSelector 自行指定将被加载的域名:程序会跳过对其他域的加载 SetBasedFieldSelector 自行指定两个集合:第一个集合包含被加载的域,第二个集合包含延迟加载的域 尽管FieldSelector会节省域加载时间,但这还是需要依赖具体程序的。域加载期间大量的开销会用在找寻索引中域存储位置所对应的指针上,因此你可能会发现就算跳过对一些域的加载也不会节省大量的时间。具体实施时需要对自己的应用程序进行测试,以找到最佳的性能取舍。

5.11 停止较慢的搜索

通常情况下,Lucene的搜索速度是很快的。但如果索引较大,或者搜索条件异常复杂的话,这就有可能使Lucene耗费较长时间来执行搜索。所幸的是,Lucene有一个特殊的Collector子类TimeLimitingColIector,它能在耗时较长的情况下停止搜索。 有关Collector的更多细节见[1]6.2小节中介绍。

TimeLimitingColIector实现了 Collector的所有方法,并能够在搜索耗时较长的情况下抛出TimeExceededException异常。它的使用方式很简单,如程序5.23 所示。

程序 5.23 使用 TimeLimitingCollector 停止 slow 捜索

public class TimeLimitingCollectorTest extends TestCase { public void testTimeLimitingCollector() throws Exception { Directory dir = TestUtil.getBooklndexDirectory(); IndexSearcher searcher = new IndexSearcher(dir); Query q = new MatchAllDocsQuery ();

int numAlIBooks = TescUtil.hitCount(searcher, q);

TopScoreDocCollector topDocs = TopScoreDocCollector.create(10,false); Collector collector = new TimeLimitingCollector(topDocs, 1000); try {

searcher.search(q, collector);

assertEquals(numAliBooks, topDocs.getTotalHits()); } catch (TimeExceededException tee) { |

System.out.println(\//打印超时信息 }

I

searcher.close(); dir.close();

使用TimeLimitingCollector有一些限制.首先,它会在收集搜索结果时添加一 些自己的操作(比如每当获取一个搜索结果文档时都检查此时是否已超时),这

会使得 搜索速度变得稍慢,尽管影响程度并不大。其次,它只在收集搜索结果时判断是否超时, 然后一些查询有可能在Query.rewrite()操作期间消耗较长时间。对于这类查询来说,程序可能只有在搜索超时时才能捕获TimeExceededException异常。

6.扩展搜索 6.1 自定义排序

如果按搜索结果的评分、文档ID或者域值进行排序方式都不能满足你的排序要求 时,Lucene还允许我们实现自定义的排序方式,而这个排序方式是通过自己创建抽象基类FieldComparatorSource的子类来实现的。

依据地理位置排序的样例见[1]6.1节 P199,Lucene contrib包含了空间相关包 spatial package,参见[1]9.7节。

在查看搜索结果时,使用TopFieldDocs,其是TopDocs的子类。访问值域将ScoreDoc转换为FieldDoc,然后访问fields[]。

6.2自定义Collector

继承抽象基类Collector并创建子类,Lucene就允许针对每一个匹配文档作完全定制化的操作。

表6.1 方法名 实现自定义Collector类的方法 功能 setNextReader(IndexReade通知Collector程序正在搜索一个新段,并提供该段对r reader,int docBase) setScorer(Scorer scorer) 应的IndexReader和 起始文档 为Collector提供一个Scorer.每处理一个段都会调用这个方法.Collector必须调用自己collect () 方法内部的Scorer. score()方法来对当前匹配的文档进行评分 collect(int docID) 针对每个匹配检索的文档调用该方法。docID与当前段相关,因此必须加入docBase以使之绝对化 acceptsDocsOutOfOrder() 如果Collector能够处理乱序docID时便返回true.如果返回true的话, 一些BooleanQuery实例就能更快收集搜索结果 Lucene所有的核心搜索方法都在后台使用Collector子类来完成搜索结果收集。 举例来说,当通过相关性进行排序时,后台会使用TopScoreDocCollector类。当通过 域进行排序时,则是使用TopFieldCollector。以上两个公开类都存在于org.apache. lucene. search程序包中,你可以在需要的时候再对它们进行实例化。 在搜索期间,当Lucene找到一个匹配文档时,会调用Collector类的collect (int dociD)方法,Lucene并不关心对文档作何种操作,如果需要记录匹配结果的话,这取 决于Collector的操作。以上是有关搜索的热点问题,因此要确认你的collect方法 只完成所需的最小操作。

为了获得更髙的搜索性能,Lucene每次只对一个段进行搜索,并通过调用setNextReader (IndexReader reader, int docBase)方法来通知程序在段之间进行切换。我们提供的 IndexReader类是特定于段进行操作的。对于每个段来说,对应的IndexReader实例是不同的。对于Collector来说,重要的是在此时记录docBase,因为传入collect方法的 docID参数是与具体的段相关的。为了获取绝对或全局性的docID,你必须向该方法传人docBase参数。该方法还能完成collector所要求的针对任意段的初始化操作。

需要注意的是,相关性评分不会传递给collect方法,这样能为不需要该参数的 Collector节省一些CPU资源。作为替代,Lucene会针对每个段调用Collector

实例 中的setScorer (Scorer)方法并提供一个Scorer实例。如果需要的话,你必须保留这个Scorer,然后通过调用Scorer. score ()方法来检索当前匹配文档的相关性评分。该方法必须通过collect方法调用,因为它保留了针对正被收集的当前文档docID对应的可变数据。需要注意的是,Scorer, score()方法每次都会重新计算文档评分,因此如果 collect方法将会多次调用score方法时,你必须在内部第一次调用它后,后续改为直接使用它返回的结果即可。作为选择,Lucene提供了 ScoreCachingWrapperScorer类,它是Scorer的子类,能够对每个文档的评分进行缓存。同时需要注意的是,Score是一 个复杂而高级的API,但当前介绍的内容只使用其中的score方法。

最后一个方法是acceptsDocsOutOfOrder(),它会返回一个Boolean对象,该方法由Lucene调用,目的是测试你的Collector是否能处理乱序到达的docID。很多 collector都有这个处理能力,但有一些collector只能在乱序到达的docID和顺序到达 的docID之间选择一种进行处理,或者在处理两种情况时需要进行大量额外的操作。如果可能的话,此时你必须返回true,因为某些BooleanQuery实例在足够自由的情况下会在后台调用更快的scorer方法。 Collector自定义示例,详见[1]6.2节 P206 自定义Collector的调用:

CustomCollector collector = new CustomCollector(searcher); searcher.search(query,collector);

6.3 扩展QueryParser

QueryParser也有针对查询类型的扩展点。这些扩展点与表6.2中所列出扩展点区别在于,前者会根据请求创建并返回查询类型。如果你想在不改变查询逻辑的情况下替换某类查询类型所对应的Query对象,那么就可以重载以下方法:newBooleanQuery、 newTermQuery、 newPhraseQuery、 newMultiPhraseQuery、 newPrefixQuery、 newFuzzyQuery、newRangeQuery、newMatchAllDocsQuery 和 newWildcardQuery。举例来说,如果QueryParser在某个时候创建了TermQuery对象,你可以简单地通过重载 该类的newTermQuery ()方法来实例化自己的TermQuery子类对象。

扩展QueryParser示例详见[1]6.3节P209 修改QueryParser处理日期的工具类,可用

parser.setDateResolution(\ 设定QueryParser的地区,可用 parser.setLocale(Locale.US);

6.4 自定义过滤器

如果执行过滤操作所需要的信息都存储在索引中,我们就没有必要自己编写过滤器了,因为使用QueryWrapperFilter就可以完成过滤操作,具体请参考[1]5.6.5小节。不过在某些情况下,为了处理一些外部信息,我们需要编写自定义的过滤器。

自定义过滤器继承自org.apache.lucene.search.Filter类,并必须实现getDocIdSet(IndexReader reader)方法,最后返回DocIdSet对象。

为了添加针对术语的过滤器,一个选择就是使用FiltereredQuery类。FilteredQuery 类颠覆了现有的使用Filter类进行过滤的局面。借助一个Filter对象,IndexSearcher的 search ()方法会在査询期间执行此过滤操作。使用FilteredQuery对象能够将任何过滤器转换为具体査询,该功能为你提供了某些可能性,比如将某个过滤器添加到 BooleanQuery的査询子句中。即该类为某个具体查询提供过滤,而不是针对整个搜索。

6.5有效载荷(Payloads)

有效载荷是Lucene一个髙级功能,它使应用程序能够针对索引期间出现的项保存任意数量的字节数组。该字节数组对于Lucene是完全不透明的:它只是在索引期间简单地存储每个项的位置信息,然后将这些信息用于随后的搜索。另外,Lucene核心功能模块并不使用有效载荷进行任何操作,也不对这些内容作出任何假设。这意味着你可以存储任意数量的对程序较为重要的编码数据,并在随后的搜索中使用这些数据,或者在程序中判断哪些文档存在于搜索结果中,或者判断这些匹配文档是如何进行评分和排序的。 使用示例参见[1]6.5节P219。

在分析索引过程中,用于该分析器的Tokenstream必须定义PayloadAttribute,然后在恰当时机创建Payload实例,并调用其incrementToken方法内部的 PayloadAttribute. setPayload方法。Payload实例的初始化方法如下: Payload(byte[] data)

Payload(byte[] data, int offset, int length)

本文来源:https://www.bwwdw.com/article/k59r.html

Top