Sunday, May 13, 2007

Annotated Lucene:第四节 什么是索引

1           什么是索引


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


 



 


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


1.1       索引数据结构介绍


1.1.1    术语定义


Lucene中基本的概念(fundamental concepts)是indexDocumentFieldterm


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


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


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


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


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


1.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


1.1.3    Fields的种类


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


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


1.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 ofsegments



1.1.5    文档编号(document numbers


在内部(internally),Lucene通过一个整数的(interger)文档编号(document number)来表示文档。第一篇被添加到索引中的文档编号为0be 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)可以更改:


ú            存储在每个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


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


 


1.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 toterm的频率(frequency)和接近度(proximity)的数据(data)。


ú            Term频率数据(frequency data):对字典中的每一个term来说,所有包含该termcontains 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)中逐一介绍。


 

Wednesday, May 09, 2007

Annotated Lucene:第三节 索引是如何创建的

4           索引是如何创建的


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


    // 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.1       索引创建类IndexWriter


一个IndexWriter对象创建并且维护(maintains) 一条索引.


它的构造函数(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)刷新到(flushDirectory中(在上述方法的调用期间)。一次flush操作会在如下时候触发(triggered):当从上一次flush操作后有足够多缓存的delete操作(参见setMaxBufferedDeleteTerms(int)),或者足够多已添加的文档(参见setMaxBufferedDocs(int)),无论哪个更快些(whichever is sooner)。当一次flush发生时,等待的(pendingdeleteadd文档都会被flush到索引中。一次flush可能触发一个或更多的片断合并(segment merges)。


构造函数中的可选参数(optional argumentautoCommit控制(controls)修改对IndexReader实体(instance)读取相同索引的能见度(visibility)。当设置为false时,修改操作将不可见(visible)直到close()方法被调用后。需要注意的是修改将依然被flushDirectory,就像新文件一样(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))。你还可以调用close(),这样可以关闭那些没有提交任何修改操作的writers,并且清除所有那些已经flush但是现在不被引用的(unreferenced)索引文件。这个模式(mode)对防止(preventreaders在一个错误的时间重新刷新(refresh)非常有用(例如在你完成所有delete操作后,但是在你完成添加操作前的时候)。它还能被用来实现简单的single-writer的事务语义(transactional semantics)("all or none")。


autoCommit设为true的时候,每次flush也会是一次提交(IndexReader实体将会把每次flush当作一次提交)。这是缺省的设置,目的是为了匹配(match2.2版本之前的行为(behavior)。当以这种模式运行时,当优化(optimize)或者片断合并(segment merges)正在进行(take place)的时候需要小心地重新刷新(refresh)你的readers,因为这两个操作会绑定(tie up)可观的(substantial)磁盘空间。


当一条索引暂时(for a while)将不会有更多的文档被添加,并且期望(desired)得到最理想(optimal)的检索性能(performance),于是optimize()方法应该在索引被关闭之前被调用。


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


专家(Expert):IndexWriter允许指定(specify)一个可选的(optionalIndexDeletionPolicy实现。你可以通过这个控制什么时候优先的提交(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)。


 


 


Annotated Lucene 作者:naven 日期:2007-5-1