Lucene 源码剖析

1 目录

2 Lucene是什么

2.1.1 强大特性

2.1.2 API组成-

2.1.3 Hello World!

2.1.4 Lucene roadmap

3 索引文件结构

3.1 索引数据术语和约定 -

3.1.1 术语定义

3.1.2 倒排索引(inverted indexing)

3.1.3 Fields的种类

3.1.4 片断(segments)

3.1.5 文档编号(document numbers)

3.1.6 索引结构概述

3.1.7 索引文件中定义的数据类型 -

3.2 索引文件结构

3.2.1 索引文件概述

3.2.2 每个Index包含的文件

3.2.2.1 Segments文件

3.2.2.2 Lock文件

3.2.2.3 Deletable文件

3.2.2.4 Compound文件(.cfs)

3.2.3 每个Segment包含的文件

3.2.3.1 Field信息(.fnm)

3.2.3.2 Field数据(.fdx和.fdt)

3.2.3.3 Term字典(.tii和.tis)

3.2.3.4 Term频率数据(.frq)

3.2.3.5 Positions位置信息数据(.prx)

3.2.3.6 Norms调节因子文件(.nrm)-

3.2.3.7 Term向量文件  -

3.2.3.8 删除的文档 (.del)

3.3 局限性(Limitations)

4 索引是如何创建的

4.1 索引创建示例

4.2 索引创建类IndexWriter

4.2.1 org.apache.lucene.index.IndexWriter

4.2.2 org.apache.lucene.index.DocumentsWriter

4.2.3 org.apache.lucene.index.SegmentMerger -

5 数据是如何存储的

5.1 数据存储类Directory

5.1.1 org.apache.lucene.store.Directory

5.1.2 org.apache.lucene.store.FSDirectory

5.1.3 org.apache.lucene.store.RAMDirectory

5.1.4 org.apache.lucene.store.IndexInput

5.1.5 org.apache.lucene.store.IndexOutput

6 文档内容是如何分析的

6.1 文档分析类Analyzer

6.1.1 org.apache.lucene.store.Analyzer

6.1.2 org.apache.lucene.store.StandardAnalyzer -

7 如何给文档评分

7.1 文档评分类Similarity

7.1.1 org.apache.lucene.search.Similarity

7.2 Similarity评分公式

1Lucene是什么

Apache Lucene是一个高性能(high-performance)的全能的全文检索(full-featured text search engine)的搜索引擎框架库,完全(entirely)使用Java开发。它是一种技术(technology),适合于(suitable for)几乎(nearly)任何一种需要全文检索(full-text search)的应用,特别是跨平台(cross-platform)的应用。

Lucene 通过一些简单的接口(simple API)提供了强大的特征(powerful features):

可扩展的高性能的索引能力(Scalable, High-Performance Indexing

ü 超过20M/分钟的处理能力(Pentium M 1.5GHz)

ü 很少的RAM内存需求,只需要1MB heap

ü 增量索引(incremental indexing)的速度与批量索引(batch indexing)的速度一样快

ü 索引的大小粗略(roughly)为被索引的文本大小的20-30%

强大的精确的高效率的检索算法(Powerful, Accurate and Efficient Search Algorithms

ü 分级检索(ranked searching)能力,最好的结果优先推出在前面

ü 很多强大的query种类:phrase queries, wildcard queries, proximity queries, range queries等

ü 支持域检索(fielded searching),如标题、作者、正文等

ü 支持日期范围检索(date-range searching)

ü 可以按任意域排序(sorting by any field)

ü 支持多个索引的检索(multiple-index searching)并合并结果集(merged results)

ü 允许更新和检索(update and searching)并发进行(simultaneous)

跨平台解决方案(Cross-Platform Solution

ü 以Open Source方式提供并遵循Apache License,允许你可以在即包括商业应用也包括Open Source程序中使用Lucene

ü 100%-pure Java(纯Java实现)

ü 提供其他开发语言的实现版本并且它们的索引文件是兼容的

Lucene API被分成(divide into)如下几种包(package)

· org.apache.lucene.analysis

定义了一个抽象的Analyser API,用于将text文本从一个java.io.Reader转换成一个TokenStream,即包括一些Tokens的枚举容器(enumeration)。一个TokenStream的组成(compose)是通过在一个Tokenizer的输出的结果上再应用TokenFilters生成的。一些少量的Analysers实现已经提供,包括StopAnalyzer和基于语法(gramar-based)分析的StandardAnalyzer。

· org.apache.lucene.document

提供一个简单的Document类,一个document只不过包括一系列的命名了(named)的Fields(域),它们的内容可以是文本(strings)也可以是一个java.io.Reader的实例。

· org.apache.lucene.index

提供两个主要类,一个是IndexWriter用于创建索引并添加文档(document),另一个是IndexReader用于访问索引中的数据。

· org.apache.lucene.search

提供数据结构(data structures)来呈现(represent)查询(queries):TermQuery用于单个的词(individual words),PhraseQuery用于短语,BooleanQuery用于通过boolean关系组合(combinations)在一起的queries。而抽象的Searcher用于转变queries为命中的结果(hits)。IndexSearcher实现了在一个单独(single)的IndexReader上检索。

· org.apache.lucene.queryParser

使用JavaCC实现一个QueryParser。

· org.apache.lucene.store

定义了一个抽象的类用于存储呈现的数据(storing persistent data),即Directory(目录),一个收集器(collection)包含了一些命名了的文件(named files),它们通过一个IndexOutput来写入,以及一个IndexInput来读取。提供了两个实现,FSDirectory使用一个文件系统目录来存储文件,而另一个RAMDirectory则实现了将文件当作驻留内存的数据结构(memory-resident data structures)。

· org.apache.lucene.util

包含了一小部分有用(handy)的数据结构,如BitVector和PriorityQueue等。

2Hello World!

下面是一段简单的代码展示如何使用Lucene来进行索引和检索(使用JUnit来检查结果是否是我们预期的):

1 // Store the index in memory:
2    Directory directory = new RAMDirectory();
3 // To store an index on disk, use this instead:
4    //Directory directory = FSDirectory.getDirectory(”/tmp/testindex”);
5    IndexWriter iwriter = new IndexWriter(directory, analyzer, true);
6    iwriter.setMaxFieldLength(25000);
7    Document doc = new Document();
8    String text = “This is the text to be indexed.“;
9    doc.add(new Field(“fieldname“, text, Field.Store.YES,
10        Field.Index.TOKENIZED));
11    iwriter.addDocument(doc);
12    iwriter.optimize();
13    iwriter.close();
14
15 // Now search the index:
16    IndexSearcher isearcher = new IndexSearcher(directory);
17 // Parse a simple query that searches for ”text”:
18    QueryParser parser = new QueryParser(“fieldname“, analyzer);
19    Query query = parser.parse(“text“);
20    Hits hits = isearcher.search(query);
21    assertEquals(1, hits.length());
22 // Iterate through the results:
23 for (int i = 0; i < hits.length(); i++) {
24      Document hitDoc = hits.doc(i);
25      assertEquals(“This is the text to be indexed.“, hitDoc.get(“fieldname“));
26    }
27    isearcher.close();
28    directory.close();

为了使用Lucene,一个应用程序需要做如下几件事:

1. 通过添加一系列Fields来创建一批Documents对象。

2. 创建一个IndexWriter对象,并且调用它的AddDocument()方法来添加进Documents。

3. 调用QueryParser.parse()处理一段文本(string)来建造一个查询(query)对象。

4. 创建一个IndexReader对象并将查询对象传入到它的search()方法中。

3Lucene Roadmap

Lucene 源码剖析

2索引文件

为了使用Lucene来索引数据,首先你得把它转换成一个纯文本(plain-text)tokens的数据流(stream),并通过它创建出Document对象,其包含的Fields成员容纳这些文本数据。一旦你准备好些Document对象,你就可以调用IndexWriter类的addDocument(Document)方法来传递这些对象到Lucene并写入索引中。当你做这些的时候,Lucene首先分析(analyzer)这些数据来使得它们更适合索引。详见《Lucene In Action》

下面先了解一下索引结构的一些术语。

2.1       索引数据术语和约定

2.1.1    术语定义

Lucene中基本的概念(fundamental concepts)是index、Document、Field和term。

1  一条索引(index)包含(contains)了一连串(a sequence of)文档(documents)。

2  一个文档(document)是由一连串fields组成。

3  一个field是由一连串命名了(a named sequence of)的terms组成。

4  一个term是一个string(字符串)。

相同的字符串(same string)但是在两个不同的fields中被认为(considered)是不同的term。因此(thus)term被描述为(represent as)一对字符串(a pair of strings),第一个string取名(naming)为该field的名字,第二个string取名为包含在该field中的文本(text within the field)。

2.1.2    倒排索引(inverted indexing)

索引(index)存储terms的统计数据(statistics about terms),为了使得基于term的检索(term-based search)效率更高(more efficient)。Lucene的索引分成(fall into)被广为熟悉的(known as)索引种类(family of indexex)叫做倒排索引(inverted index)。这是因为它可以列举(list),对一个term来说,所有包含它的文档(documents that contain it)。这与自然关联规则(natural relationship)是相反,即由documents列举它所包含的terms。

2.1.3    Fields的种类

在Lucene中,fields可以被存储(stored),在这种情况(in which case)下它们的文本被逐字地(literally)以一种非倒排的方式(in non-inverted manner)存储进index中。那些被倒排的fields(that are inverted)称为(called)被索引(indexed)。一个field可以都被存储(stored)并且被索引(indexed)。

一个field的文本可以被分解为(be tokenized into)terms以便被索引(indexed),或者field的文本可以被逐字地使用为(used literally as)一个term来被索引(be indexed)。大多数fields被分解(be tokenized),但是有时候对某种唯一性(certain identifier)的field来逐字地索引(be indexed literally)又是非常有用的,如url。

2.1.4    片断(segments)

Lucene的索引可以由多个复合的子索引(multiple sub-indexes)或者片断(segments)组成(be composed of)。每一个segment都是一个完全独立的索引(fully independent index),它能够被分离地进行检索(be searched seperately)。索引按如下方式进行演化(evolve):

1. 为新添加的文档(newly added documents)创建新的片断(segments)。

2. 合并已存在的片断(merging existing segments)。

检索可以涉及(involve)多个复合(multiple)的segments,并且/或者多个复合(multiple)的indexes。每一个index潜在地(potentially)包含(composed of)一套(a set of)segments。

2.1.5    文档编号(document numbers)

在内部(internally),Lucene通过一个整数的(interger)文档编号(document number)来表示文档。第一篇被添加到索引中的文档编号为0(be numbered zero),每一篇随后(subsequent)被添加的document获得一个比前一篇更大的数字(a number one greater than the previous)。

需要注意的是一篇文档的编号(document’s number)可以更改,所以在Lucene之外(outside of)存储这些编号时需要特别小心(caution should be taken)。详细地说(in particular),编号在如下的情况(following situations)可以更改:

1  存储在每个segment中的编号仅仅是在所在的segment中是唯一的(unique),在它能够被使用在(be used in)一个更大的上下文(a larger context)中前必须被转变(converted)。标准的技术(standard technique)是给每一个segment分配(allocate)一个范围的值(a range of values),基于该segment所使用的编号的范围(the range of numbers)。为了将一篇文档的编号从一个segment转变为一个扩展的值(an external value),该片断的基础的文档编号(base document number)被添加(is added)。为了将一个扩展的值(external value)转变回一个segment的特定的值(specific value),该segment将该扩展的值所在的范围标识出来(be indentified),并且该segment的基础值(base value)将被减少(substracted)。例如,两个包含5篇文档的segments可能会被合并(combined),所以第一个segment有一个基础的值(base value)为0,第二个segment则为5。在第二个segment中的第3篇文档(document three from the second segment)将有一个扩展的值为8。

2  当文档被删除的时候,在编号序列中(in the numbering)将产生(created)间隔段(gaps)。这些最后(eventually)在索引通过合并演进时(index evolves through merging)将会被清除(removed)。当segments被合并后(merged),已删除的文档将会被丢弃(dropped),一个刚被合并的(freshly-merged)segment因此在它的编号序列中(in its numbering)不再有间隔段(gaps)。

2.1.6    索引结构概述

每一个片断的索引(segment index)管理(maintains)如下的数据:

Fields名称:这包含了(contains)在索引中使用的一系列fields的名称(the set of field names)。

已存储的field的值:它包含了,对每篇文档来说,一个属性-值数据对(attribute-value pairs)的清单(a list of),其中属性即为field的名字。这些被用来存储关于文档的备用信息(auxiliary information),比如它的标题(title)、url、或者一个访问一个数据库(database)的唯一标识(identifier)。这套存储的fields就是那些在检索时对每一个命中的(hits)文档所返回的(returned)信息。这些是通过文档编号(document number)来做为key得到的。

Term字典(dictionary):一个包含(contains)所有terms的字典,被使用在所有文档中所有被索引的fields中。它还包含了该term所在的文档的数目(the number of documents which contains the term),并且指向了(pointer to)term的频率(frequency)和接近度(proximity)的数据(data)。

Term频率数据(frequency data):对字典中的每一个term来说,所有包含该term(contains the term)的文档的编号(numbers of all documents),以及该term出现在该文档中的频率(frequency)。

Term接近度数据(proximity data):对字典中的每一个term来说,该term出现在(occur)每一篇文档中的位置(positions)。

调整因子(normalization factors):对每一篇文档的每一个field来说,为一个存储的值(a value is stored)用来加入到(multiply into)命中该field的分数(score for hits on that field)中。

Term向量(vectors):对每一篇文档的每一个field来说,term向量(有时候被称做文档向量)可以被存储。一个term向量由term文本和term的频率(frequency)组成(consists of)。怎么添加term向量到你的索引中请参考Field类的构造方法(constructors)。

删除的文档(deleted documents):一个可选的(optional)文件标示(indicating)哪一篇文档被删除。

关于这些项的详细信息在随后的章节(subsequent sections)中逐一介绍。

2.1.7    索引文件中定义的数据类型

数据类型

所占字节长度(字节)

说明

Byte

1

基本数据类型,其他数据类型以此为基础定义

UInt32

4

32位无符号整数,高位优先

UInt64

8

64位无符号整数,高位优先

VInt

不定,最少1字节

动态长度整数,每字节的最高位表明还剩多少字节,每字节的低七位表明整数的值,高位优先。可以认为值可以为无限大。其示例如下

字节1

字节2

字节3

0

00000000

   

1

00000001

   

2

00000010

   

127

01111111

   

128

10000000

00000001

 

129

10000001

00000001

 

130

10000010

00000001

 

16383

10000000

10000000

00000001

16384

10000001

10000000

00000001

16385

10000010

10000000

00000001

Chars

不定,最少1字节

采用UTF-8编码[20]的Unicode字符序列

String

不定,最少2字节

由VInt和Chars组成的字符串类型,VInt表示Chars的长度,Chars则表示了String的值

3.1索引文件结构

Lucene使用文件扩展名标识不同的索引文件,文件名标识不同版本或者代(generation)的索引片段(segment)。如.fnm文件存储域Fields名称及其属性,.fdt存储文档各项域数据,.fdx存储文档在fdt中的偏移位置即其索引文件,.frq存储文档中term位置数据,.tii文件存储term字典,.tis文件存储term频率数据,.prx存储term接近度数据,.nrm存储调节因子数据,另外segments_X文件存储当前最新索引片段的信息,其中X为其最新修改版本,segments.gen存储当前版本即X值,这些文件的详细介绍上节已说过了。

下面的图描述了一个典型的lucene索引文件列表:

如果将它们的关系划成图则如下所示

这些文件中存储数据的详细结构是怎样的呢,下面几个小节逐一介绍它们,熟悉它们的结构非常有助于优化Lucene的查询和索引效率和存储空间等。

3.2每个Index包含的单个文件

下面几节介绍的文件存在于每个索引index中,并且只有一份。

3.2.1Segments文件

索引中活动(active)的Segments被存储在segment info文件中,segments_N,在索引中可能会包含一个或多个segments_N文件。然而,最大一代的那个文件(the one with largest generation)是活动的片断文件(这时更旧的segments_N文件依然存在(are present)是因为它们暂时(temporarily)还不能被删除,或者,一个writer正在处理提交请求(in the process of committing),或者一个用户定义的(custom)IndexDeletionPolicy正被使用)。这个文件按照名称列举每一个片断(lists each segment by name),详细描述分离的标准(seperate norm)和要删除的文件(deletion files),并且还包含了每一个片断的大小。

对2.1版本来说,还有一个文件segments.gen。这个文件包含了该索引中当前生成的代(current generation)(segments_N中的_N)。这个文件仅用于一个后退处理(fallback)以防止(in case)当前代(current generation)不能被准确地(accurately)通过单独地目录文件列举(by directory listing alone)来确定(determened)(由于某些NFS客户端因为基于时间的目录(time-based directory)的缓存终止(cache expiration)而引起)。这个文件简单地包含了一个int32的版本头(version header)(SegmentInfos.FORMAT_LOCKLESS=-2),遵照代的记录(followed by the generation recorded)规则,对int64来说会写两次(write twice)。

版本

包含的项

数目

类型

描述

2.1之前版本

Format

1

Int32

在Lucene1.4中为-1,而在Lucene 2.1中为-3(SegmentsInfos.FORMAT_SINGLE_NORM_FILE)

Version

1

Int64

统计在删除和添加文档时,索引被更改了多少次。

NameCounter

1

Int32

用于为新的片断文件生成新的名字。

SegCount

1

Int32

片断的数目

SegName

SegCount

String

片断的名字,用于所有构成片断索引的文件的文件名前缀。

SegSize

SegCount

Int32

包含在片断索引中的文档的数目。

2.1及之后版本

Format

1

Int32

在Lucene 2.1和Lucene 2.2中为-3(SegmentsInfos.FORMAT_SINGLE_NORM_FILE)

Version

1

Int64

同上

NameCounter

1

Int32

同上

SegCount

1

Int32

同上

SegName

SegCount

String

同上

SegSize

SegCount

Int32

同上

DelGen

SegCount

Int64

为分离的删除文件的代的数目(generation count of the separate deletes file),如果值为-1,表示没有分离的删除文件。如果值为0,表示这是一个2.1版本之前的片断,这时你必须检查文件是否存在_X.del这样的文件。任意大于0的值,表示有分离的删除文件,文件名为_X_N.del

HasSingleNormFile

SegCount

Int8

该值如果为1,表示Norm域(field)被写为一个单一连接的文件(single joined file)中(扩展名为.nrm),如果值为0,表示每一个field的norms被存储为分离的.fN文件中,参考下面的“标准化因素(Normalization Factors)”

NumField

SegCount

Int32

表示NormGen数组的大小,如果为-1表示没有NormGen被存储。

NormGen

SegCount * NumField

Int64

记录分离的标准文件(separate norm file)的代(generation),如果值为-1,表示没有normGens被存储,并且当片断文件是2.1之前版本生成的时,它们全部被假设为0(assumed to be 0)。而当片断文件是2.1及更高版本生成的时,它们全部被假设为-1。这时这个代(generation)的意义与上面DelGen的意义一样。

IsCompoundFile

SegCount

Int8

记录是否该片断文件被写为一个复合的文件,如果值为-1表示它不是一个复合文件(compound file),如果为1则为一个复合文件。另外如果值为0,表示我们需要检查文件系统是否存在_X.cfs

2.3

Format

1

Int32

在Lucene 2.3中为-4 (SegmentInfos.FORMAT_SHARED_DOC_STORE)

Version

1

Int64

同上

NameCounter

1

Int32

同上

SegCount

1

Int32

同上

SegName

SegCount

String

同上

SegSize

SegCount

Int32

同上

DelGen

SegCount

Int64

同上

DocStoreOffset

1

Int32

如果值为-1则该segment有自己的存储文档的fields数据和term vectors的文件,并且DocStoreSegment, DocStoreIsCompoundFile不会存储。在这种情况下,存储fields数据(*.fdt和*.fdx文件)以及term vectors数据(*.tvf和*.tvd和*.tvx文件)的所有文件将存储在该segment下。另外,DocStoreSegment将存储那些拥有共享的文档存储文件的segment。DocStoreIsCompoundFile值为1如果segment存储为compound文件格式(如.cfx文件),并且DocStoreOffset值为那些共享文档存储文件中起始的文档编号,即该segment的文档开始的位置。在这种情况下,该segment不会存储自己的文档数据文件,而是与别的segment共享一个单一的数据文件集。

[DocStoreSegment]

1

String

如上

[DocStoreIsCompoundFile]

1

Int8

如上

HasSingleNormFile

SegCount

Int8

同上

NumField

SegCount

Int32

同上

NormGen

SegCount * NumField

Int64

同上

IsCompoundFile

SegCount

Int8

同上

2.4及以上

Format

1

Int32

在Lucene 2.4中为-7 (SegmentInfos.FORMAT_HAS_PROX)

Version

1

Int64

同上

NameCounter

1

Int32

同上

SegCount

1

Int32

同上

SegName

SegCount

String

同上

SegSize

SegCount

Int32

同上

DelGen

SegCount

Int64

同上

DocStoreOffset

1

Int32

同上

[DocStoreSegment]

1

String

同上

[DocStoreIsCompoundFile]

1

Int8

同上

HasSingleNormFile

SegCount

Int8

同上

NumField

SegCount

Int32

同上

NormGen

SegCount * NumField

Int64

同上

IsCompoundFile

SegCount

Int8

同上

DeletionCount

SegCount

Int32

记录该segment中删除的文档数目

HasProx

SegCount

Int8

值为1表示该segment中至少一个fields的omitTf设置为false,否则为0

Checksum

1

Int64

存储segments_N文件中直到checksum的所有字节的CRC32 checksum数据,用来校验打开的索引文件的完整性(integrity)。

3.2.2Lock文件

写锁(write lock)文件名为“write.lock”,它缺省存储在索引目录中。如果锁目录(lock directory)与索引目录不一致,写锁将被命名为“XXXX-write.lock”,其中“XXXX”是一个唯一的前缀(unique prefix),来源于(derived from)索引目录的全路径(full path)。当这个写锁出现时,一个writer当前正在修改索引(添加或者清除文档)。这个写锁确保在一个时刻只有一个writer修改索引。

需要注意的是在2.1版本之前(prior to),Lucene还使用一个commit lock,这个锁在2.1版本里被删除了。

3.2.3Deletable文件

在Lucene 2.1版本之前,有一个“deletable”文件,包含了那些需要被删除文档的详细资料。在2.1版本后,一个writer会动态地(dynamically)计算哪些文件需要删除,因此,没有文件被写入文件系统。

3.2.4 Compound文件s(.cfs)

从Lucene 1.4版本开始,compound文件格式成为缺省信息。这是一个简单的容器(container)来服务所有下一章节(next section)描述的文件(除了.del文件),格式如下:

版本

包含的项

数目

类型

描述

1.4之后版本

FileCount

1

VInt

 

DataOffset

FileCount

Long

 

FileName

FileCount

String

 

FileData

FileCount

raw

Raw文件数据是上面命名的所有单个的文件数据(the individual named above)。

结构如下图所示:

3.3每个Segment包含的文件

剩下的文件(remaining files)都是per-segment(每个片断文件),因此(thus)都用后缀来定义(defined by suffix)。

3.3.1Fields域数据文件

3.3.1.1Field信息(.fnm

Field的名字都存储在Field信息文件中,后缀是.fnm。

文件

包含的项

数目

类型

版本

描述

FieldsInfo(.fnm)

FieldsCount

1

VInt

   

FieldName

FieldsCount

String

   

FieldBits

FieldsCount

Byte

 

最低阶的bit位(low-order bit)值为1表示是被索引的Fields,0表示非索引的Fields。

 

第二个最低阶的bit位(second lowest-order bit)值为1表示该Field有term向量存储(term vectors stored),0表示该field没有term向量。

>=1.9

如果第三个最低阶的bit位(third lowest-order bit)设置(0×04),term的位置(term positions)将和term向量一起被存储(stored with term vectors)。

>=1.9

如果第四个最低阶的bit位(fourth lowest-order bit)设置(0×08),term的偏移(term offsets)将和term向量一起被存储(stored with term vectors)。

>=1.9

如果第五个最低阶的bit位(fifth lowest-order bit)设置(0×10),norms将对索引的field忽略掉(norms are omitted for the indexed field)。

>=1.9

如果第六个最低阶的bit位(sixth lowest-order bit)设置(0×20),payloads将为索引的field存储(payloads are stored for the indexed field)。

注明:payloads概念:

词条载荷(payloads)――允许用户将任意二进制数据和索引中的任意词条(term)相关联。

词条载荷是一个允许信息在索引中按逐词条储存的新特性。例如,当索引Web页面时,储存某个关键词的额外信息可能会很有用,例如这个关键词关联的URL或者经过文字分析后得出的权重系数。在更高级的应用中,为了突出语句中的名次成分相对于其它成分的重要性,储存语句中这个关键词出现的部分可能会很有帮助。我今年在ApacheCon Europe会议上的演讲中就有几张讲述词条载荷的幻灯片,感兴趣的读者可以去看看。

Fields将使用它们在这个文件中的顺序来编号(fields are numbered by their order in this file)。需要注意的是,就像文档编号(document numbers)一样,field编号(field numbers)与片断是相关的(are segment relative)。结构如下图所示:

3.3.1.2存储的Field.fdx.fdt

存储的fields(stored fields)通过两个文件来呈现(represented by two files),即field索引文件(.fdx)和field数据文件(.fdt)。

文件

包含的项

父项

数目

类型

版本

描述

Fields Index(.fdx) 对每个文档来说,存储指向它的fields数据的指针(pointer)

FieldValuesPosition

 

SegSize

UInt64

 

用于找详细文档(a particular document)的所有fields的field数据文件中的位置(position),因为它包含的(contains)是固定长度的数据(fixed-length data),这个文件可以很容易地进行随机访问(randomly accessed)。

 

文档n的field数据的位置是在该文件中n*8的位置中(UInt64类型)。

Fields Data(.fdt)这个文件存储每个文档的field数据

DocFieldData

 

SegSize

     

FieldCount

DocFieldData

1

VInt

   

FieldNum

DocFieldData

FieldCount

VInt

   

Bits

DocFieldData

FieldCount

Byte

<=1.4

只有最低阶的bit位(low-order bits of Bits)被使用,值为1表示tokenized field(分解过的field),0表示non-tokenized field。

Byte

>=1.9

最低阶的bit位表示tokenized field

>=1.9

第二个bit(second bit)用于表示该field存储binary数据。

>=1.9

第三个bit(third bit)表示该field的压缩选项被开启(field with compression enabled),如果压缩选项开启,采用的压缩算法(algorithm)是ZLIB

Value

DocFieldData

FieldCount

String

<=1.4

 

String | BinaryValue

>=1.9

依赖于Bits的值

BinaryValue

>=1.9

ValueSize,<Byte>^ValueSize

ValueSize

Value

1

VInt

>=1.9

 

结构如下图所示:

3.3.2存储的term字典(.tii.tis

Term字典使用如下两种文件存储,第一种是存储term信息(TermInfoFile)的文件,即.tis文件,格式如下:

版本

包含的项

数目

类型

描述

全部版本

TIVersion

1

UInt32

记录该文件的版本,1.4版本中为-2

TermCount

1

UInt64

 

IndexInterval

1

UInt32

 

SkipInterval

1

UInt32

 

MaxSkipLevels

1

UInt32

 

TermInfos

1

TermInfo…

 

TermInfos->TermInfo

TermCount

TermInfo

 

TermInfo->Term

TermCount

Term

 

Term->PrefixLength

TermCount

VInt

Term文本的前缀可以共享,该项的值表示根据前一个term的文本来初始化的字符串前缀长度,前一个term必须已经预设成后缀文本以便构成该term的文本。比如,如果前一个term为“bone”,而当前term为“boy”,则该PrefixLength值为2,suffix值为“y”

Term->Suffix

TermCount

String

如上

Term->FieldNum

TermCount

VInt

用来确定term的field,它们存储在.fdt文件中。

TermInfo->DocFreq

TermCount

VInt

包含该term的文档数目

TermInfo->FreqDelta

TermCount

VInt

用来确定包含在.frq文件中该term的TermFreqs的位置。特别指出,它是该term的数据在文件中位置与前一个term的位置的差值,当为第一个term时,该值为0

TermInfo->ProxDelta

TermCount

VInt

用来确定包含在.prx文件中该term的TermPositions的位置。特别指出,它是该term的数据在文件中的位置与前一个term的位置地差值,当为第一个term时,该值为0。如果fields的omitTF设置为true,该值也为0,因为prox信息没有被存储。

TermInfo->SkipDelta

TermCount

VInt

用来确定包含在.frq文件中该term的SkipData的位置。特别指出,它是TermFreqs之后即SkipData开始的字节数目,换句话说,它是TermFreq的长度。SkipDelta只有在DocFreq不比SkipInteval小的情况下才会存储。

TermInfoFile文件按照Term来排序,排序方法首先按照Term的field名称(按照UTF-16字符编码)排序,然后按照Term的Text字符串(UTF-16编码)排序。 结构如下图所示:

另一种是存储term信息的索引文件,即.tii文件,该文件包含.tis文件中每一个IndexInterval的值,与它在.tis中的位置一起被存储,这被设计来完全地读进内存中(read entirely into memory),以便用来提供随机访问.tis文件。该文件的结构与.tis文件非常相似,只是添加了一项数据,即IndexDelta。格式如下

版本

包含的项

数目

类型

描述

全部版本

TIVersion

1

UInt32

同tis

IndexTermCount

1

UInt64

同tis

IndexInterval

1

UInt32

同tis

SkipInterval

1

UInt32

是TermDocs存储在skip表中的分数(fraction),用来加速(accelerable)TermDocs.skipTo(int)的调用。在更小的索引中获得更大的结果值(larger values result),将获得更高的速度,但却更小开销?(fewer accelerable cases while smaller values result in bigger indexes, less acceleration (in case of a small value for MaxSkipLevels)

MaxSkipLevels

1

UInt32

是.frq文件中为每一个term存储的skip levels的最大数目,A low value results in smaller indexes but less acceleration, a larger value results in slighly larger indexes but greater acceleration.参见.frq文件格式中关于skip levels的详细介绍。

TermIndices

IndexTermCount

TermIndice

同tis

TermIndice->TermInfo

IndexTermCount

TermInfo

同tis

TermIndice->IndexDelta

IndexTermCount

VLong

用来确定该Term的TermInfo在.tis文件中的位置,特别指出,它是该term的数据的位置与前一个term位置的差值。

结构如下图所示:

Lucene源代码剖析

3.3.3Term频率数据(.frq)

Term频率数据文件(.frq文件)存储容纳了每一个term的文档列表,以及该term出现在该文档中的频率(出现次数frequency,如果omitTf设置为fals时才存储)。

版本

包含的项

父类型

类型

描述

全部版本

TermFreqs

TermCount

TermFreq

按照term顺序排序,term是隐含的(?implicit),来自.tis文件。TermFreq按文档编号递增的顺序排序。

SkipData

TermCount

SkipData

 

TermFreq->DocDelta

TermCount

VInt

如果omitTf设置为false,要同时检测文档编号和频率,特别指出,DocDelta/2时该文档编号与上一个文档编号的差值(如果是第一个文档值为0)。当DocDelta为单数时频率为1,当DocDelta为偶数时频率为读取下一个VInt的值。如果omitTf设置为true,DocDelta为文档编号之间的差值(gap,不用乘以2,multiplited),频率信息则不被存储。

TermFreq->[Freq?]

TermCount

VInt

 

SkipData->SkipLevelLength

NumSkipLevels-1

VInt

 

SkipData->SkipLevel

TermCount

SkipDatums

 

SkipLevel->SkipDatum

DocFreq/(SkipInterval^(Level + 1))

SkipDatum

 

SkipData->SkipDatum

TermCount

SkipDatum

 

SkipDatum->DocSkip

1

VInt

 

SkipDatum->PayloadLength?

1

VInt

 

SkipDatum->FreqSkip

1

VInt

 

SkipDatum->ProxSkip

1

VInt

 

SkipDatum->SkipChildLevelPointer?

1

VLong

 

结构如下图所示:

举例来说,当omitTf设置为false时,一个term的TermFreqs在文档7出现1次并且在文档11中出现3次,则为如下的VInt数字序列:
    15, 8, 3
如果omitTf设置为true时,则为如下数字序列:
    7, 4
    DocSkip记录在TermFreqs中每隔SkipInterval个文档之前的文档编号。如果该term的域fields中被禁用payloads时,则DocSkip呈现在序列中(in the sequence)与上一个值之间的差值(difference)。如果payloads启用时,则DocSkip/2表示序列中与上一个值之间的差值。如果payloads启用并且DocSkip为奇数时,PayloadLength将被存储并表示(indicating)在TermPositions中第SkipInterval个文档之前的最后一个payload的长度。FreqSkip和ProxSkip分别(respectively)记录在FreqFile和ProxFile文件中每SkipInterval个记录(entry)的位置。文件的位置信息对序列中前一个SkipDatumn来说与TermFreqs和Positions的起始信息相关。

例如,如果DocFreq=35并且SkipInterval=16,则在TermFreqs中有两个SkipData记录,容纳第15和第31个文档编号。第一个FreqSkip代表第16个SkipDatumn起始的TermFreqs数据开始之后的字节数目,第二个FreqSkip表示第32个SkipDatumn开始之后的字节数目。第一个ProxSkip代表第16个SkipDatumn起始的Positions数据开始之后的字节数目,第二个ProxSkip表示第32个SkipDatumn开始之后的字节数目。
在Lucene 2.2版本中介绍了skip levels的想法(notion),每一个term可以有多个skip levels。一个term的skip levels的数目等于NumSkipLevels = Min(MaxSkipLevels, floor(log(DocFreq/log(SkipInterval))))。对一个skip level来说SkipData记录的数目等于DocFreq/(SkipInterval^(Level + 1))。然而(whereas)最低的(lowest)skip level等于Level = 0。
例如假设SkipInterval = 4, MaxSkipLevels = 2, DocFreq = 35,则skip level 0有8个SkipData记录,在TermFreqs序列中包含第3、7、11、15、19、23、27和31个文档的编号。Skip level 1则有2个SkipData记录,在TermFreqs中包含了第15和第31个文档的编号。
在所有level>0之上的SkipData记录中包含一个SkipChildLevelPointer,指向(referencing)level-1中相应)(corresponding)的SkipData记录。在这个例子中,level 1中的记录15有一个指针指向level 0中的记录15,level 1中的记录31有一个指针指向level 0中的记录31。

3.3.4Positions位置信息数据(.prx)

Positions位置信息数据文件(.prx文件)容纳了每一个term出现在所有文档中的位置的列表。注意如果在fields中的omitTf设置为true时将不会在此文件中存储任何信息,并且如果索引中所有fields中的omitTf都设置为true,此.prx文件将不会存在。

版本

包含的项

数目

类型

描述

全部版本

TermPositions

TermCount

TermPositions

按照term顺序排序,term是隐含的(?implicit),来自.tis文件。

TermPositions->Positions

DocFreq

Positions

按文档编号递增的顺序排序。

Positions->PositionDelta

Freq

VInt

如果term的fields中payloads被禁用,则取值为term出现在该文档中当前位置与前一个位置的差值(第一个位置取值0)。如果payloads被启用,则取值为当前位置与上一个位置之间差值的2倍。如果payloads启用并且PositionDelta为单数,则PayloadLength被存储,表示当前位置的payloads的长度。

Positions->Payload?

Freq

Payload

 

Payload->PayloadLength?

1

VInt

 

Payload->PayloadData

PayloadLength

byte

 

结构如下图所示:

例如,如果一个term的TermPositions为一个文档中出现的第4个term,并且为后来的文档(subsequent document)中出现的第5个和第9个term,则将被存储为下面的VInt数据序列(payloads禁用):

4, 5, 4

PayloadData是与term的当前位置相关联元数据(metadata),如果该位置的PayloadLength被存储,则它表示此payload的长度。如果PayloadLength没存储,则此payload与前一个位置的payload拥有相等的PayloadLength。

3.3.5Norms调节因子文件(.nrm)

在Lucene 2.1版本之前,每一个索引都有一个norm文件给每一个文档都保存了一个字节。对每一个文档来说,那些.f[0-9]*包含了一个字节容纳一个被编码的分数,值为对hits结果集来说在那个field中被相乘得出的分数(multiplied into the score)。每一个分离的norm文件在适当的时候(when adequate)为复合的(compound)和非复合的segment片断创建,格式如下:

Norms (.f[0-9]*) –> <Byte> SegSize 在Lucene 2.1及以上版本,只有一个norm文件容纳了所有norms数据:

版本

包含的项

数目

类型

描述

2.1及之后版本

NormsHeader

1

raw

‘N’,'R’,'M’,Version:4个字节,最后字节表示该文件的格式版本,当前为-1

Norms

NumFieldsWithNorms

Norms

 

Norms->Byte

SegSize

Byte

每一个字节编码了一个float指针数值,bits 0-2 容纳 3-bit 尾数(mantissa),bits 3-8容纳 5-bit 指数(exponent),这些被转换成一个IEEE单独的float数值,如图所示

NormsHeader->Version

1

Byte

 

结构如下图所示:

一个分离的norm文件在一个存在的segment的norm数据被更改的时候被创建,当field N被修改时,一个分离的norm文件.sN被创建,用来维护该field的norm数据。

Lucene 源码剖析

3.3.6Term向量文件

Term向量(vector)的支持是field基本组成中对一个field来说的可选项,它包含如下4种文件:

1. 文档索引或.tvx文件:对每个文档来说,它把偏移(offset)存储进文档数据(.tvd)文件和域field数据(.tvf)文件

版本

包含的项

数目

类型

描述

全部版本

TVXVersion

1

Int

在Lucene 2.4中为3 (TermVectorsReader.FORMAT_VERSION2)

DocumentPosition

NumDocs

UInt64

在.tvd文件中的偏移

FieldPosition

NumDocs

UInt64

在.tvf文件中的偏移

结构如下图所示:

2. 文档或.tvd文件:对每个文档来说,它包含fields的数目,有term向量的fields的列表,还有指向term向量域文件(.tvf)中的域信息的指针列表。该文件用于映射(map out)出那些存储了term向量的fields,以及这些field信息在.tvf文件中的位置。

版本

包含的项

数目

类型

描述

全部版本

TVDVersion

1

Int

在Lucene 2.4中为3 (TermVectorsReader.FORMAT_VERSION2)

NumFields

NumDocs

VInt

 

FieldNums

NumDocs

FieldNums

 
 

FieldNums->FieldNumDelta

NumFields

VInt

 
 

FieldPositions

NumDocs

FieldPositions

 
 

FieldPositions->FieldPositionDelta

NumField-1

VLong

 

结构如下图所示:

3. 域field或.tvf文件:对每个存储了term向量的field来说,该文件包含了一个term的列表,及它们的频率,还有可选的位置和偏移信息。

版本

包含的项

数目

类型

描述

全部版本

TVFVersion

1

Int

在Lucene 2.4中为3 (TermVectorsReader.FORMAT_VERSION2)

NumTerms

NumFields

VInt

 

Position/Offset

NumFields

Byte

 

TermFreqs

NumFields

TermFreqs

 

TermFreqs->TermText

NumTerms

TermText

 

TermText->PrefixLength

NumTerms

VInt

 

TermText->Suffix

NumTerms

String

 

TermFreqs->TermFreq

NumTerms

VInt

 

TermFreqs->Positions?

NumTerms

Positions

 

Positions->Position

TermFreq

VInt

 

TermFreqs->Offsets?

NumTerms

Offsets

 

Offsets->StartOffset

TermFreq

VInt

 

Offsets->EndOffset

TermFreq

VInt

 

结构如下图所示:

备注:

l Positions/Offsets 字节存储的条件是当该term向量含有存储的位置或偏移信息时。
l Term Text prefixes文本前缀是共享的,表示根据前一个term的文本来初始化的字符串前缀长度,前一个term必须已经预设成后缀文本以便构成该term的文本。比如,如果前一个term为“bone”,而当前term为“boy”,则该PrefixLength值为2,suffix值为“y”。
l Positions存储为Delta编码的VInts,意思是我们只能存储当前位置与最后位置的差值。
l Offsets存储为Delta编码的VInts,第一个VInt是startOffset,第二个VInt是endOffset。

3.3.7删除的文档 (.del)

删除的文档(.del)文件是可选的,而且仅当一个segment存在有被删除的文档时才存在。即使对每一单个segment,它也是维护复合segment的外部数据(exterior)。

对Lucene 2.1及以前版本,它的格式为:Deletions (.del) –> ByteCount,BitCount,Bits

对2.2及以上版本,格式如下:

版本

包含的项

数目

类型

描述

2.2之后版本

[Format]

1

UInt32

可选,-1表示为DGaps,非负数(negative)值表示为Bit,并且此时不存储Format

ByteCount

1

UInt32

代表Bit里的字节数目,而且一般值为(SegSize/8)+1

BitCount

1

UInt32

表示Bit里当前设置的字节数目

Bit|DGaps

1

 

Bit还是DGaps取决于Format。Bits中对每一个索引的文档均包含一个字节,当一个bit对应的一个文档编号被设置时,表示该文档被删除。Bit从最低(least)到最重要(significant)的文档排序。所以Bits包含两个字节,0×00和0×02,则文档9被标记为删除。DGaps表示松散(sparse)的bit-vector向量比Bits更有效率(efficiently)。DGaps由索引中非0的Bits位生成,以及非0的字节数据本身。Bits中非0字节数目(NonzeroBytesCoun)不会存储。

Bit->Byte

ByteCount

Byte

 

DGaps->DGap

NonzeroBytesCount

VInt

 

DGaps-> NonzeroBytes

NonzeroBytesCount

Byte

 

结构如下图所示:

举例来说,如果有8000 bits,并且只有bits 10, 12, 32 被设置,DGaps将会存储如下数据:

(VInt) 1 , (byte) 20 , (VInt) 3 , (Byte) 1

3.3.8局限性(Limitations)

有几个地方这些文件格式会让terms和文档的最大数目受限于32-bit的大小,大约最大40亿。这在今天不是一个问题,长远来看(in the long term)可能会成为个问题。因此它们应该替换为UInt64类型或者更好的类型,如VInt则没有大小限制。

Lucene 源码剖析

4索引是如何创建的

为了使用Lucene来索引数据,首先你比把它转换成一个纯文本(plain-text)tokens的数据流(stream),并通过它创建出Document对象,其包含的Fields成员容纳这些文本数据。一旦你准备好些Document对象,你就可以调用IndexWriter类的addDocument(Document)方法来传递这些对象到Lucene并写入索引中。当你做这些的时候,Lucene首先分析(analyzer)这些数据来使得它们更适合索引。详见《Lucene In Action》

4.1 索引创建示例

下面的代码示例如何给一个文件建立索引。

// Store the index on disk
    Directory directory = FSDirectory.getDirectory(“/tmp/testindex“);
// Use standard analyzer
    Analyzer analyzer = new StandardAnalyzer(); 
// Create IndexWriter object
    IndexWriter iwriter = new IndexWriter(directory, analyzer, true);
    iwriter.setMaxFieldLength(25000);
// make a new, empty document
    Document doc = new Document();
    File f = new File(“/tmp/test.txt“);
// Add the path of the file as a field named ”path”.  Use a field that is 
    // indexed (i.e. searchable), but don’t tokenize the field into words.
    doc.add(new Field(“path“, f.getPath(), Field.Store.YES, Field.Index.UN_TOKENIZED));
    String text = “This is the text to be indexed.“;
    doc.add(new Field(“fieldname“, text, Field.Store.YES,      Field.Index.TOKENIZED));
// Add the last modified date of the file a field named ”modified”.  Use 
    // a field that is indexed (i.e. searchable), but don’t tokenize the field
    // into words.
    doc.add(new Field(“modified“,
        DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE),
        Field.Store.YES, Field.Index.UN_TOKENIZED));
// Add the contents of the file to a field named ”contents”.  Specify a Reader,
    // so that the text of the file is tokenized and indexed, but not stored.
    // Note that FileReader expects the file to be in the system’s default encoding.
    // If that’s not the case searching for special characters will fail.
    doc.add(new Field(“contents“, new FileReader(f)));
    iwriter.addDocument(doc);
    iwriter.optimize();
    iwriter.close();

下面详细介绍每一个类的处理机制。

4.2 索引创建类IndexWriter

一个IndexWriter对象创建并且维护(maintains) 一条索引并生成segment,使用DocumentsWriter类来建立多个文档的索引数据,SegmentMerger类负责合并多个segment。

4.2.1 org.apache.lucene.store.IndexWriter

一个IndexWriter对象只创建并维护一个索引。IndexWriter通过指定存放的目录(Directory)以及文档分析器(Analyzer)来构建,direcotry代表索引存储(resides)在哪里;analyzer表示如何来分析文档的内容;similarity用来规格化(normalize)文档,给文档算分(scoring);IndexWriter类里还有一些SegmentInfos对象用于存储索引片段信息,以及发生故障回滚等。以下是它们的类图:

它的构造函数(constructor)的create参数(argument)确定(determines)是否一条新的索引将被创建,或者是否一条已经存在的索引将被打开。需要注意的是你可以使用create=true参数打开一条索引,即使有其他readers也在在使用这条索引。旧的readers将继续检索它们已经打开的”point in time”快照(snapshot),并不能看见那些新已创建的索引,直到它们再次打开(re-open)。另外还有一个没有create参数的构造函数,如果提供的目录(provided path)中没有已经存在的索引,它将创建它,否则将打开此存在的索引。

另一方面(in either case),添加文档使用addDocument()方法,删除文档使用removeDocument()方法,而且一篇文档可以使用updateDocument()方法来更新(仅仅是先执行delete在执行add操作而已)。当完成了添加、删除、更新文档,应该需要调用close方法。

这些修改会缓存在内存中(buffered in memory),并且定期地(periodically)刷新到(flush)Directory中(在上述方法的调用期间)。一次flush操作会在如下时候触发(triggered):当从上一次flush操作后有足够多缓存的delete操作(参见setMaxBufferedDeleteTerms(int)),或者足够多已添加的文档(参见setMaxBufferedDocs(int)),无论哪个更快些(whichever is sooner)。对被添加的文档来说,一次flush会在如下任何一种情况下触发,文档的RAM缓存使用率(setRAMBufferSizeMB)或者已添加的文档数目,缺省的RAM最高使用率是16M,为得到索引的最高效率,你需要使用更大的RAM缓存大小。需要注意的是,flush处理仅仅是将IndexWriter中内部缓存的状态(internal buffered state)移动进索引里去,但是这些改变不会让IndexReader见到,直到commit()和close()中的任何一个方法被调用时。一次flush可能触发一个或更多的片断合并(segment merges),这时会启动一个后台的线程来处理,所以不会中断addDocument的调用,请参考MergeScheduler。

构造函数中的可选参数(optional argument)autoCommit控制(controls)修改对IndexReader实体(instance)读取相同索引的能见度(visibility)。当设置为false时,修改操作将不可见(visible)直到close()方法被调用后。需要注意的是修改将依然被flush进Directory,就像新文件一样(as new files),但是却不会被提交(commit)(没有新的引用那些新文件的segments_N文件会被写入(written referencing the new files))直道close()方法被调用。如果在调用close()之前发生了某种严重错误(something goes terribly wrong)(例如JVM崩溃了),于是索引将反映(reflect)没有任何修改发生过(none of changes made)(它将保留它开始的状态(remain in its starting state))。你还可以调用rollback(),这样可以关闭那些没有提交任何修改操作的writers,并且清除所有那些已经flush但是现在不被引用的(unreferenced)索引文件。这个模式(mode)对防止(prevent)readers在一个错误的时间重新刷新(refresh)非常有用(例如在你完成所有delete操作后,但是在你完成添加操作前的时候)。它还能被用来实现简单的single-writer的事务语义(transactional semantics)(“all or none”)。你还可以执行两条语句(two-phase)的commit,通过调用prepareCommit()方法,之后再调用commit()方法。这在Lucene与外部资源(例如数据库)交互的时候是很需要的,而且必须执行commit或rollback该事务。

当autoCommit设为true的时候,该writer会周期性地提交它自己的数据。已过时:注意在3.0版本中,IndexWriter将不会接收autoCommit=true,它会硬设置(hardwired)为false。你可以自己在需要的时候经常调用commit()方法。这不保证什么时候一个确定的commit会处理。它被曾经用来在每次flush的时候处理,但是现在会在每次完成merge操作后处理,如2.4版本中即如此。如果你想强行执行commit,请调用commit方法或者close这个writer。一旦一个commit完成后,新打开的IndexReader实例将会看到索引中该commit更改的数据。当以这种模式运行时,当优化(optimize)或者片断合并(segment merges)正在进行(take place)的时候需要小心地重新刷新(refresh)你的readers,因为这两个操作会绑定(tie up)可观的(substantial)磁盘空间。

不管(Regardless)autoCommit参数如何,一个IndexReader或者IndexSearcher只会看到索引在它打开的当时的状态。任何在索引被打开之后提交到索引中的commit信息,在它被重新打开之前都不会见到。当一条索引暂时(for a while)将不会有更多的文档被添加,并且期望(desired)得到最理想(optimal)的检索性能(performance),于是optimize()方法应该在索引被关闭之前被调用。

打开IndexWriter会为使用的Directory创建一个lock文件。尝试对相同的Directory打开另一个IndexWriter将会导致(lead to)一个LockObtainFailedException异常。如果一个建立在相同的Directory的IndexReader对象被用来从这条索引中删除文档的时候,这个异常也会被抛出。

专家(Expert):IndexWriter允许指定(specify)一个可选的(optional)IndexDeletionPolicy实现。你可以通过这个控制什么时候优先的提交(prior commit)从索引中被删除。缺省的策略(policy)是KeepOnlyLastCommitDeletionPolicy类,在一个新的提交完成的时候它会马上所有的优先提交(prior commit)(这匹配2.2版本之前的行为)。创建你自己的策略能够允许你明确地(explicitly)保留以前的”point in time”提交(commit)在索引中存在(alive)一段时间。为了让readers刷新到新的提交,在它们之下没有被删除的旧的提交(without having the old commit deleted out from under them)。这对那些不支持“在最后关闭时才删除”语义(”delete on last close” semantics)的文件系统(filesystem)如NFS,而这是Lucene的“point in time”检索通常所依赖的(normally rely on)。

专家(Expert):IndexWriter允许你分别修改MergePolicy和MergeScheduler。MergePolicy会在该索引中的segment有更改的任何时候被调用。它的角色是选择哪一个merge来做,如果有(if any)则传回一个MergePolicy.MergeSpecificatio来描述这些merges。它还会选择merges来为optimize()做处理,缺省是LogByteSizeMergePolicy。然后MergeScheduler会通过传递这些merges来被调用,并且它决定什么时候和怎么样来执行这些merges处理,缺省是ConcurrentMergeScheduler。

4.2.2 org.apache.lucene.index.DocumentsWriter

DocumentsWriter是由IndexWriter调用来负责处理多个文档的类,它通过与Directory类及Analyzer类、Scorer类等将文档内容提取出来,并分解成一组term列表再生成一个单一的segment所需要的数据文件,如term频率、term位置、term向量等索引文件,以便SegmentMerger将它合并到统一的segment中去。以下是它的类图:

该类可接收多个添加的文档,并且直接写成一个单独的segment文件。这比为每一个文档创建一个segment(使用DocumentWriter)以及对那些segments执行合作处理更有效率。

每一个添加的文档都被传递给DocConsumer类,它处理该文档并且与索引链表中(indexing chain)其它的consumers相互发生作用(interacts with)。确定的consumers,就像StoredFieldWriter和TermVectorsTermsWriter,提取一个文档的摘要(digest),并且马上把字节写入“文档存储”文件(比如它们不为每一个文档消耗(consume)内存RAM,除了当它们正在处理文档的时候)。

其它的consumers,比如FreqProxTermsWriter和NormsWriter,会缓存字节在内存中,只有当一个新的segment制造出的时候才会flush到磁盘中。

一旦使用完我们分配的RAM缓存,或者已添加的文档数目足够多的时候(这时候是根据添加的文档数目而不是RAM的使用率来确定是否flush),我们将创建一个真实的segment,并将它写入Directory中去。

4.3 索引创建过程

文档的索引过程是通过DocumentsWriter的内部数据处理链完成的,DocumentsWriter可以实现同时添加多个文档并将它们写入一个临时的segment中,完成后再由IndexWriter和SegmentMerger合并到统一的segment中去。DocumentsWriter支持多线程处理,即多个线程同时添加文档,它会为每个请求分配一个DocumentsWriterThreadState对象来监控此处理过程。处理时通过DocumentsWriter初始化时建立的DocFieldProcessor管理的索引处理链来完成的,依次处理为DocFieldConsumers、DocInverter、TermsHash、FreqProxTermsWriter、TermVectorsTermsWriter、NormsWriter以及StoredFieldsWriter等。

索引创建处理过程及类的主线请求链表如下图所示:

下面介绍主要步骤的处理过程

4.3.1 DocFieldProcessorPerThread.processDocument()

该方法是处理一个文档的调度函数,负责整理文档的各个fields数据,并创建相应的DocFieldProcessorPerField对象来依次处理每一个field。该方法首先调用索引链表的startDocument()来初始化各项数据,然后依次遍历每一个fields,将它们建立一个以field名字计算的hash值为key的hash表,值为DocFieldProcessorPerField类型。如果hash表中已存在该field,则更新该FieldInfo(调用FieldInfo.update()方法),如果不存在则创建一个新的DocFieldProcessorPerField来加入hash表中。注意,该hash表会存储包括当前添加文档的所有文档的fields信息,并根据FieldInfo.update()来合并相同field名字的域设置信息。

建立hash表的同时,生成针对该文档的fields[]数组(只包含该文档的fields,但会共用相同的fields数组,通过lastGen来控制当前文档),如果field名字相同,则将Field添加到DocFieldProcessorPerField中的fields数组中。建立完fields后再将此fields数组按field名字排序,使得写入的vectors等数据也按此顺序排序。之后开始正式的文档处理,通过遍历fields数组依次调用DocFieldProcessorPerField的processFields()方法进行(下小节继续讲解),完成后调用finishDocument()完成后序工作,如写入FieldInfos等。

下面举例说明此过程,假设要添加如下一个文档:

文档域

内容

是否索引

title

Lucene 源码分析

true

url

http://javenstudio.org

false

content

索引是如何创建的

true

content

索引的创建过程

true

下图描述处理后fields数组的数据结构

Lucene 源码剖析

5索引是如何存储的

5.1 数据存储类Directory

Directory及相关类负责文档索引的存储。

5.1.1 org.apache.lucene.store.Directory

一个Directory对象是一系列统一的文件列表(a flat list of files)。文件可以在它们被创建的时候一次写入,一旦文件被创建,它再次打开后只能用于读取(read)或者删除(delete)操作。并且同时在读取和写入的时候允许随机访问(random access)。

在这里并不直接使用Java I/O API,但是更确切地说,所有I/O操作都是通过这个API处理的。这使得读写操作方式更统一起来,如基于内存的索引(RAM-based indices)的实现(即RAMDirectory)、通过JDBC存储在数据库中的索引、将一个索引存储为一个文件的实现(即FSDirectory)。

Directory的锁机制是一个LockFactory的实例实现的,可以通过调用Directory实例的setLockFactory()方法来更改。

5.1.2 org.apache.lucene.store.FSDirectory

FSDirectory类直接实现Directory抽象类为一个包含文件的目录。目录锁的实现使用缺省的SimpleFSLockFactory,但是可以通过两种方式修改,即给getLockFactory()传入一个LockFactory实例,或者通过调用setLockFactory()方法明确制定LockFactory类。

目录将被缓存(cache)起来,对一个指定的符合规定的路径(canonical path)来说,同样的FSDirectory实例通常通过getDirectory()方法返回。这使得同步机制(synchronization)能对目录起作用。

5.1.3 org.apache.lucene.store.RAMDirectory

RAMDirectory类是一个驻留内存的(memory-resident)Directory抽象类的实现。目录锁的实现使用缺省的SingleInstanceLockFactory,但是可以通过setLockFactory()方法修改。

5.1.4 org.apache.lucene.store.IndexInput

IndexInput类是一个为了从一个目录(Directory)中读取文件的抽象基类,是一个随机访问(random-access)的输入流(input stream),用于所有Lucene读取Index的操作。BufferedIndexInput是一个实现了带缓冲的IndexInput的基础实现。

5.1.5 org.apache.lucene.store.IndexOutput

IndexOutput类是一个为了写入文件到一个目录(Directory)中的抽象基类,是一个随机访问(random-access)的输出流(output stream),用于所有Lucene写入Index的操作。BufferedIndexOutput是一个实现了带缓冲的IndexOutput的基础实现。RAMOuputStream是一个内存驻留(memory-resident)的IndexOutput的实现类。

文档内容是如何分析的

Analyzer类负责分析文档结构并提取内容。

6.1 文档分析类Analyzer

6.1.1 org.apache.lucene.store.Analyzer

Analyzer类构建用于分析文本的TokenStream对象,因此(thus)它表示(represent)用于从文本中分解(extract)出组成索引的terms的一个规则器(policy)。典型的(typical)实现首先创建一个Tokenizer,它将那些从Reader对象中读取字符流(stream of characters)打碎为(break into)原始的Tokens(raw Tokens)。然后一个或更多的TokenFilters可以应用在这个Tokenizer的输出上。警告:你必须在你的子类(subclass)中覆写(override)定义在这个类中的其中一个方法,否则的话Analyzer将会进入一个无限循环(infinite loop)中。

6.1.2 org.apache.lucene.store.StandardAnalyzer

StandardAnalyzer类是使用一个English的stop words列表来进行tokenize分解出文本中word,使用StandardTokenizer类分解词,再加上StandardFilter以及LowerCaseFilter以及StopFilter这些过滤器进行处理的这样一个Analyzer类的实现。

Lucene 源码剖析

7如何给文档评分

Similarity类负责给文档评分。

7.1 文档评分类Similarity

7.1.1 org.apache.lucene.search. Similarity

Similarity类实现算分(scoring)的API,它的子类实现了检索算分的算法。DefaultSimilarity类是缺省的算分的实现,SimilarityDelegator类是用于委托算分(delegating scoring)的实现,在Query.getSimilarity(Searcher)}的实现里起作用,以便覆写(override)一个Searcher中Similarity实现类的仅有的确定方法(certain methods)。

查询q相对于文档d的分数与在文档和查询向量(query vectors)之间的余弦距离(cosing-distance)或者点乘积(dot-product)有关系(correlates to),文档和查询向量存于一个信息检索(Information Retrieval)的向量空间模型(Vector Space Model (VSM))之中。一篇文档的向量与查询向量越接近(closer to),它的得分也越高(scored higher),这个分数按如下公式计算:

其中:

1. tf(t in d) 与term的出现次数(frequency)有关系(correlate to),定义为(defined as)term t在当前算分(currently scored)的文档d中出现(appear in)的次数(number of times)。对一个给定(gived)的term,那些出现此term的次数越多(more occurences)的文档将获得越高的分数(higher score)。缺省的tf(t in d)算法实现在DefaultSimilarity类中,公式如下:

2. idf(t) 代表(stand for)反转文档频率(Inverse Document Frequency)。这个分数与反转(inverse of)的docFreq(出现过term t的文档数目)有关系。这个分数的意义是越不常出现(rarer)的term将为最后的总分贡献(contribution)更多的分数。缺省idff(t in d)算法实现在DefaultSimilarity类中,公式如下:

3. coord(q,d) 是一个评分因子,基于(based on)有多少个查询terms在特定的文档(specified document)中被找到。通常(typically),一篇包含了越多的查询terms的文档将比另一篇包含更少查询terms的文档获得更高的分数。这是一个搜索时的因子(search time factor)是在搜索的时候起作用(in effect at search time),它在Similarity对象的coord(q,d)函数中计算。

4. queryNorm(q) 是一个修正因子(normalizing factor),用来使不同查询间的分数更可比较(comparable)。这个因子不影响文档的排名(ranking)(因为搜索排好序的文档(ranked document)会增加(multiplied)相同的因数(same factor)),更确切地说只是(but rather just)为了尝试(attempt to)使得不同查询条件(甚至不同索引(different indexes))之间更可比较性。这是一个搜索时的因子是在搜索的时候起作用,由Similarity对象计算。缺省queryNorm(q)算法实现在DefaultSimilarity类中,公式如下:

sumOfSquaredWeights(查询的terms)是由查询Weight对象计算的,例如一个布尔(boolean)条件查询的计算公式为:

5. t.getBoost() 是一个搜索时(search time)的代表查询q中的term t的boost数值,具体指定在(as specified in)查询的文本中(参见查询语法),或者由应用程序调用setBoost()来指定。需要注意的是实际上(really)没有一个直接(direct)的API来访问(accessing)一个多个term的查询(multi term query)中的一个term 的boost值,更确切地说(but rather),多个terms(multi terms)在一个查询里的表示形式(represent as)是多个TermQuery对象,所以查询里的一个term的boost值的访问是通过调用子查询(sub-query)的getBoost()方法实现的。

6. norm(t,d) 是提炼取得(encapsulate)一小部分boost值(在索引时间)和长度因子(length factor):

ú document boost – 在添加文档到索引之前通过调用doc.setBoost()来设置。

ú Field boost – 在添加Field到文档之前通过调用field.setBoost()来设置。

ú lengthNorm(field) – 在文档添加到索引的时候,根据(in accordance with)文档中该field的tokens数目计算得出,所以更短(shorter)的field会贡献更多的分数。lengthNorm是在索引的时候起作用,由Similarity类计算得出。

当一篇文档被添加到索引的时候,所有上面计算出的因子将相乘起来(multiplied)。如果文档拥有多个相同名字的fields(multiple fields with same name),所有这些fields的boost值也会被一起相乘起来(multiplied together):

然而norm数值的结果在被存储(stored)之前被编码成(encoded as)一个单独的字节(single byte)。在检索的时候,这个norm字节值从索引目录(index directory)中读取出来,并解码回(decoded back)一个norm浮点数值(float value)。这个编/解码(encoding/decoding)行为,会缩减(reduce)索引的大小(index size),这得自于(come with)精度损耗的代价(price of precision loss)- 它不保证decode(encode(x))=x,举例来说decode(encode(0.89))=0.75。还有需要注意的是,检索的时候再修改评分(scoring)的这个norm部分已近太迟了,例如,为检索使用不同的Similarity。

以前收集的关于lucene分析的资料,版本有点老,已经忘了是在哪找到的了,如果你是文档作者,侵犯了你的版权,@我即可

Lucene的分析资料【转】的更多相关文章

  1. lucene 基础知识点

    部分知识点的梳理,参考<lucene实战>及网络资料 1.基本概念 lucence 可以认为分为两大组件: 1)索引组件 a.内容获取:即将原始的内容材料,可以是数据库.网站(爬虫).文本 ...

  2. 用lucene替代mysql读库的尝试

    采用lucene对mysql中的表建索引,并替代全文检索操作. 备注:代码临时梳理很粗糙,后续修改. import java.io.File; import java.io.IOException; ...

  3. Lucene的评分(score)机制研究

    首先,需要学习Lucene的评分计算公式—— 分值计算方式为查询语句q中每个项t与文档d的匹配分值之和,当然还有权重的因素.其中每一项的意思如下表所示: 表3.5 评分公式中的因子 评分因子 描 述 ...

  4. Lucene提供的条件判断查询

    第一.按词条搜索 - TermQuery query = new TermQuery(new Term("name","word1"));hits = sear ...

  5. Lucene 单域多条件查询

    在Lucene 中 BooleanClause用于表示布尔查询子句关系的类,包括:BooleanClause.Occur.MUST表示and,BooleanClause.Occur.MUST_NOT表 ...

  6. lucene自定义过滤器

    先介绍下查询与过滤的区别和联系,其实查询(各种Query)和过滤(各种Filter)之间非常相似,可以这样说只要用Query能完成的事,用过滤也都可以完成,它们之间可以相互转换,最大的区别就是使用过滤 ...

  7. lucene+IKAnalyzer实现中文纯文本检索系统

    首先IntelliJ IDEA中搭建Maven项目(web):spring+SpringMVC+Lucene+IKAnalyzer spring+SpringMVC搭建项目可以参考我的博客 整合Luc ...

  8. 全文检索解决方案(lucene工具类以及sphinx相关资料)

    介绍两种全文检索的技术. 1.  lucene+ 中文分词(IK) 关于lucene的原理,在这里可以得到很好的学习. http://www.blogjava.net/zhyiwww/archive/ ...

  9. MySQL和Lucene索引对比分析

    MySQL和Lucene都可以对数据构建索引并通过索引查询数据,一个是关系型数据库,一个是构建搜索引擎(Solr.ElasticSearch)的核心类库.两者的索引(index)有什么区别呢?以前写过 ...

随机推荐

  1. WebAPI

    WebAPI的Host OWIN IIS WebAPI 的handler和Filter有啥区别? WebAPI  常用 Filters Exception Filter Timer Filter Lo ...

  2. 我的MYSQL学习心得(三) 查看字段长度

    我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(四) 数据类型 我的MYSQL学习心得(五) 运 ...

  3. 腾讯云 安装mono

    一.yum -y update 运行出现以下错误: http://centos.tencentyun.com/contrib/x86_64/repodata/filelists.xml.gz: [Er ...

  4. ucos实时操作系统学习笔记——内核结构和任务创建

    对于ucos实时操作系统,邵贝贝的那本书已经写得很详细了,我因为之前不深的研究过ucos,所以在这里做一个笔记,写一些个人对该操作系统的理解,仅仅是个人理解,如果有人看到这边随笔有不对的地方,望给我指 ...

  5. 关于bug分析与异常处理的一些思考

    前言:工作三年了,工作内容主要是嵌入式软件开发和维护,用的语言是C,毕业后先在一家工业自动化控制公司工作两年半,目前在一家医疗仪器公司担任嵌入式软件开发工作.软件开发中,难免不产生bug:产品交付客户 ...

  6. 延迟求值-如何让Lo-Dash再提速x100?

    「注释」作者在本文里没有说明这么一个事实: 目前的版本Lo-Dash v2.4.1并没有引入延迟求值的特性,Lo-Dash 3.0.0-pre中部分方法进行了引入,比如filter(),map(),r ...

  7. CSharpGL(26)在opengl中实现控件布局/渲染文字

    CSharpGL(26)在opengl中实现控件布局/渲染文字 效果图 如图所示,可以将文字.坐标轴固定在窗口的一角. 下载 CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入( ...

  8. 一种简单的md5加盐加密的方法(防止彩虹表撞库)

    md5加密(或者说摘要算法)大家都很熟悉了 就不解释了 现在很多数据库设计都喜欢用单向加密的方式保存密码,验证时对提交的密码再次加密之后做密文对比 /// <summary> 使用MD5加 ...

  9. C# 面向对象基础

    面向对象编程OOP(Object-Oriented Programming) 类和对象 类是对象的定义,对象是类的实现. [属性集信息] [类修饰符] class 类名 [:类基] //定义类的语法格 ...

  10. delete

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...