CLUCENE-0.9.10 索引与优化过程

一.文件索引过程
主要流程描述

IndexWriter writer("ndx", &an, true);
writer.setMergeFactor(10);
writer.setMinMergeDocs(10);
Document *lpDoc = new Document;
lpDoc->add(*new Field("content", "This is demo content.", true, true, true) );
writer.addDocument(lpDoc);
delete lpDoc;
writer.close();

下面描述该流程

1.IndexWriter writer("ndx", &an, true);
directory = FSDirectory::getDirectory("ndx", true);
从一个全局的列表中取得一个对象
如果对象不存在,则新建一个并加入到列表中
主要目的是为了使用同一个目录只使用同一个FSDirectory对象
新建目录时删除该目录下所有文件级子目录
analyzer = an;
该对象由对象外部创建
segmentInfos = _CLNEW SegmentInfos;
建立一个SegmentInfos对象,该对象包含一个SegmentInfo对象的列表
建立时指定了SegmentInfo对象的列表并不在移除指针时删除SegmentInfo对象
一个SegmentInfo对象包括其名称(用于文件名前缀)和文档数
closeDir = true;
指定是否在索引对象关闭close时,是否同时调用目录的close函数
缺省为关闭目录对象
similarity = CL_NS(search)::Similarity::getDefault();
取得缺省的文档分值score计算对象,如果不存在则建立
可以自己实现一个Similarity的继承类,然后用Similarity::setDefault方法设置成缺省的
变量Similarity* _defaultImpl用于保存缺省对象
ramDirectory = _CLNEW TransactionalRAMDirectory;
建立一个TransactionalRAMDirectory对象,该对象包含一个事务取消时的删除文件列表及恢复文件列表
同时还包含一个当前文件列表
恢复文件列表自动删除key和value,删除文件列表和当前文件列表则不自动删除
LuceneLock* newLock = directory->makeLock("write.lock");
建立了一个FSLock文件锁对象,并不同时建立文件
该文件锁用于写时锁定,会同时在IndexReader进行文档删除时使用
newLock->obtain(LUCENE_WRITE_LOCK_TIMEOUT);
writeLock = newLock;
建立文件锁的文件
建立方法:_open(lockFile, O_RDWR | O_CREAT | O_RANDOM , _S_IREAD | _S_IWRITE)
LuceneLock* lock = directory->makeLock("commit.lock");
IndexWriterLockWith with ( lock,LUCENE_WRITE_LOCK_TIMEOUT,this,create );
LOCK_MUTEX(directory->DIR_OBJ);
with.run();
UNLOCK_MUTEX(directory->DIR_OBJ);
_CLDELETE(lock);
建立了一个用于事务提交的锁
锁住directory->DIR_OBJ
锁住事务锁
如果建立新的目录,则调用writer->segmentInfos->write(writer->getDirectory());
建立文件segments.new
将segmentInfos信息写入到该文件中(格式\版本号\总seg数\(seg名称\seg中文档数,...))
将文件改名为segments
每写一次版本号都+1
否则调用writer->segmentInfos->read(writer->getDirectory());
打开segments文件
读入格式\版本号\总seg数
依次读入各seg的名称和文档数,生成SegmengInfo对象并加入到列表中
解事务锁
解directory->DIR_OBJ锁
2.writer.setMergeFactor(10);
设置合并频率,具体含义见后面的函数解释
3.writer.setMinMergeDocs(10);
设置最小缓冲文档数
4.Document *lpDoc = new Document;
boost = 1.0f;
设置文档的权重乘数为1.0f(表示不改变)
fieldList = NULL;
初始化字段列表为NULL
一个字段列表对象包括一个指向Field*的指针和一个指向下一个字段列表的指针DocumentFieldList* next;
对段列表对象析构时,会自动其指向的删除Field对象和下一个DocumentFieldList对象
5.LpDoc->add(*new Field("content", "This is demo content.", true, true, true) );
new Field("content", "This is demo content.", true, true, true)
name = CLStringIntern::intern( Name CL_FILELINE);
在字符串缓冲表中查找该字符串,如找到则增加引用计数并返回找到的,否则复制Name然后放在缓冲中并返回
该操作并不是线程安全的,没有采用互斥量,是否有问题呢?
与intern对应的函数是unintern,即减少引用计数,为1时删除复制的字符串
_stringValue = stringDuplicate( String );
复制字段值
_readerValue = NULL;
未设置读字段对象
_isStored = store;
是否保存字段值到索引中
_isIndexed = index;
是否索引该字段
_isTokenized = token;
是否对字段值进行分词
this->storeTermVector = storeTermVector;
?????,缺省值为false
boost=1.0f;
设置字段的权重乘数为1.0f(表示不改变)
fieldList = _CLNEW DocumentFieldList(&field, fieldList);
生成一个新的字段列表对象,并放在原来的列表开头
6.writer.addDocument(lpDoc);
SCOPED_LOCK_MUTEX(THIS_LOCK);
建立一个互斥量对象并调用lock,在析构时会调用unlock,文件锁write.lock已经保证不会存在多个写索引的对象
该锁只保证本对象不被同时多次调用即可
ramDirectory->transStart();
检查是否有异常状态(两个列表都为空,且未开始事务)
然后设置状态为事务开始
try
{
char* segmentName = newSegmentName();
生成一个新的seg的名称,线程安全格式为'-'字符+segmentinfos.counter36进制
分配了内存来保存该名称
try
{
DocumentWriter* dw = _CLNEW DocumentWriter(ramDirectory, analyzer, similarity, maxFieldLength);
生成一个DocumentWriter对象,注意最大分词token数量为maxFieldLength,缺省为10000,超过则异常
可以通过IndexWriter对象的setMaxFieldLength函数修改该缺省值
analyzer = analyzer,
directory = ramDirectory
maxFieldLength = maxFieldLength;
fieldInfos = NULL
包含一个字段信息FieldInfo对象列表,可根据字段名称查找FieldInfo对象
FieldInfo对象的字段名称从字符串缓冲中取得,析构时释放
fieldLengths = NULL
similarity = similarity
fieldPositions = NULL
fieldBoosts = NULL
termBuffer(_CLNEW Term( LUCENE_BLANK_STRING, LUCENE_BLANK_STRING ,false))
初始化时设置好分词对象、目录对象、最大分词数、计分对象,以及Term级冲
Term对象为一个FieldName与FieldValue组合,其中FieldName从字符串缓冲中取得,FieldValue
为复制的字符串,都在Term对象析构时释放。
try
{
dw->addDocument(segmentName, doc);
将文档写入到内存目录的segmenmt片段中
fieldInfos = _CLNEW FieldInfos();
fieldInfos->add(doc);
生成一个字段信息列表对象,并将文档中的所有字段加入到列表中(不包括字段值)
const char* buf = Misc::segmentname(segment, ".fnm");
fieldInfos->write(directory, buf);
_CLDELETE_CaARRAY(buf);
将字段信息列表写入到segname.fnm文件中,格式:
(总字段数\字段名称\是否索引及保存term标志)
FieldsWriter fieldsWriter(directory, segment, fieldInfos);
try
{
fieldsWriter.addDocument(doc);
}
_CLFINALLY( fieldsWriter.close() );
打开segname.fdt和segname.fdx文件,其中fdt保存了文档中需要保存的字段,格式为
(字段序号\分词标志\字段值的长度\字段值)
clearPostingTable();
清空Term*= Posting*列表,该列表不会自动删除key和value对象
什么时候删除Term*和Posting*对象呢???
fieldLengths = _CL_NEWARRAY(int32_t,fieldInfos->size());
生成字段长度列表,表示字段的token数,总元素为字段数
fieldPositions = _CL_NEWARRAY(int32_t,fieldInfos->size());
生成字段位置列表,表示字段的token的Position总和,总元素为字段数
int32_t fbl = fieldInfos->size();
float_t fbd = doc->getBoost();
fieldBoosts = _CL_NEWARRAY(float_t,fbl);
生成计分乘值列表,表示计分乘值(文档*字段),总元素为字段数
{
for ( int32_t i=0;ifieldBoostsIdea [I] = fbd;
}
将计分乘值初始化为文档的计分乘值
{
for ( int32_t i=0;isize();i++ )
fieldLengthsIdea [I] = 0;
}
将字段长度列表初始化为0
???没有初始化字段位置列表???
invertDocument(doc);
对所有字段分词并生成term,然后保存到postingTable中
同样的term会保存一个位置列表和数量
同时填写fieldLengths、fieldPositions和fieldBoosts
fieldPositions中每个字段的Term的Position总是从0开始
并不写入到目录
Posting** postings = NULL;
int32_t postingsLength = 0;
sortPostingTable(postings,postingsLength);
将postingTable复制到数组并按Term字段名和值排序,不影响原来的postingTable
结果数组保存在postings中,大小保存在postingsLength中
数组在该函数结束时删除
writePostings(postings,postingsLength, segment);
建立四个文件segname.frq和segname.prx,以及segname.tii和segname.tis
segname.tii和segname.tis文件格式相同,如下:
文件头\Term\包含该Term的文档数\
该Term在frq中的位置\该Term在prx中的位置
该Term的skipOffset??? (这个字段是什么意思?)
该Term在tis文档中的位置
frq文件格式如下:
出现次数为1
1
否则写入
0
该Term在文档中出现的次数
prx文件格式如下:
依次写入该Term在文档中出现的位置
如果选择storeTermVector,则还会生成三个文件:segname.tvx, .tvd, .tvf
这三个文件的具体格式是什么?什么情况下需要storeTermVector?
writeNorms(doc, segment);
写入每个字段的分值到segment.f<字段序号>的文件中,用一个字节表示
}
_CLFINALLY(_CLDELETE(dw););
SegmentInfo* si = _CLNEW SegmentInfo(segmentName, 1, ramDirectory);
segmentInfos->add(si);
将当前segment加入到segmeng列表中去
}
_CLFINALLY(_CLDELETE_CaARRAY(segmentName););
释放segmentName的内存
maybeMergeSegments();
合并频率为mergeFactor,每minMergeDocs个文档合并成一个segment,然后:
(1)每mergeFactor个含minMergeDocs个文档的segment合并成一个segment
(2)每mergeFactor个含minMergeDocs*mergeFactor文档的segment合并成一个segment
(3)...
mergeFactor的值越大,合并频率越小,同时打开的文件数越多,检索速度会更慢;一般情况下取10
minMergeDocs值越大,合并频率越小,需要内存越多。
合并级数越大,则每次合并的时间越长,总的合并时间也越长,最好合并级数控制在二级,最多三级
因此,原则是,尽量取大的mergeFactor和minMergeDocs值可显著的缩小索引合并时间。
以500万记录为例,取mergeFactor为10,则minMergeDocs值为5万时,合并次数为二级,最后生成1050万记录的索引文件
以下对mergeSegments(minSegment)函数详细说明(参数表示从该seg开始往后的seg合成一个):
CLVector segmentsToDelete(false);
一个用于保存合并后需要删除的seg的列表,该列表并不会在析构时自动删除对象
const char* mergedName = newSegmentName();
新的索引,用于保存多个seg合并后的新的seg
SegmentMerger merger(directory, mergedName, useCompoundFile);
定义一个用于合并操作的对象,构造函数参数描述合并后的seg
for (int32_t i = minSegment; i < segmentInfos->size(); i++)
{
SegmentInfo* si = segmentInfos->info(i);
SegmentReader* reader = _CLNEW SegmentReader(si);
merger.add(reader);
if ((reader->getDirectory() == this->directory) ||
reader->getDirectory() == this->ramDirectory))
{
segmentsToDelete.push_back((SegmentReader*)reader);
}
对于每个要合并的seg生成一个SegmentReader对象,并加入到合并对象中
这些生成的reader会在合并对象merger析构时被删除
合并完成后,只与本IndexWriter对象所属目录相关的seg实际内容才会被删除,不会
删除保存在其它目录中seg的实际内容。
}
int32_t mergedDocCount = merger.merge();
执行合并操作,将多个seg合并到一个seg,返回的是合并的文档数量
此时只是生成了新的seg的内容,如果失败,不影响现有seg
???函数细节???
segmentInfos->clearto(minSegment);
从seg列表中将被合并的seg对象删除,此时并没有删除seg的实际文件内容
???这儿有内存泄漏,因为segmentInfos并不会自动删除value???
segmentInfos->add( _CLNEW SegmentInfo(mergedName, mergedDocCount, directory));
将合并后的seg加入到seg列表中
merger.closeReaders();
关闭所有被合并的seg的reader对象,但此时并没有删除这些对象(析构时删除)
LuceneLock* lock = directory->makeLock("commit.lock");
IndexWriterLockWith2 with ( lock,LUCENE_COMMIT_LOCK_TIMEOUT,this,&segmentsToDelete );
LOCK_MUTEX(directory->DIR_OBJ);
with.run();
writer->segmentInfos->write(writer->getDirectory());
将seg信息写入到writer所属目录中
???如果有来自其它目录的seg呢,因为实际seg内容并不在本目录,是否不正确???
???两种操作有这种情况:1.合并其它目录时; 2.ramdir中还有数据未刷到本目录中时???
writer->deleteSegments(segmentsToDelete);
UNLOCK_MUTEX(directory->DIR_OBJ);
_CLDELETE( lock );
_CLDELETE_CaARRAY( mergedName );
删除锁对象和mergeName的内存,这儿从逻辑上来讲应该是有问题的,如果前面合并失败,
mergedName的内存无法释放,如果with.run失败,则目录中的锁都无法释放,会导致目录不能操作。
另外,还会导致新建的seg无法删除,会占用大量的多余空间。
}
catch (...)
{
ramDirectory->transAbort();
出错时回退本文档操作,实际为将备份文件copy回目录中,并删除新增的文件
throw;
???此时如果是在maybeMergeSegments函数里出错,是否会导致索引格式非法???
}
ramDirectory->transCommit();
提交本文档操作,实际为清空保留的备份文件
7.delete lpDoc;
删除文档对象,同时删除其所属的字段
8.writer.close();
将内存数据刷到目录中,删除内存目录对象
必要时关闭目录(???如果不需要关闭目录时,也删除了该目录对象,确认为BUG???)
二.优化索引文件过程
主要流程描述
IndexWriter writer("ndx", &an, true);
writer.setMergeFactor(10);
writer.setMinMergeDocs(10);
writer.optimize();
writer.close();
最终应该组合成一个seg
其它过程都比较简单,这儿只描述writer.optimize()
SCOPED_LOCK_MUTEX(optimize_LOCK);
建立合并锁,以防止合并过程同时被调用,该锁会在函数退出时自动解锁
flushRamSegments();
将内存中的segment写入到目录中去,会将segmentInfos中从后往前连续的ramdir的seg合并成一个seg存放在目录中
调用该函数后,可以保证segmentInfos最后一个seg肯定是在目录中的。
???这种方法在合并目录时有问题,比如先加几个文件,然后合并一个IndexReader方式的目录,则不符合上述条件???
???这种情况下索引会出错的,内存中的seg内容并没有写入到目录中???
while (segmentInfos->size() > 1 ||
(segmentInfos->size() == 1 &&
(SegmentReader::hasDeletions(segmentInfos->info(0)) ||
segmentInfos->info(0)->getDir()!=directory ||
(useCompoundFile &&
(!SegmentReader::usesCompoundFile(segmentInfos->info(0)) ||
SegmentReader::hasSeparateNorms(segmentInfos->info(0))
)
)
)
)
)
条件是:
0.没有seg时不执行
1.不止一个seg时一定执行
2.或者,有被删除的记录
3.或者,第一个seg不是本目录时
4.或者,当采用组合文件且第一个seg不是组合文件或采用了分离的norms文件时。
{
int32_t minSegment = segmentInfos->size() - mergeFactor;
这儿比较奇怪,为什么不直接用minSegment=0,而是分批合并?
mergeSegments(minSegment < 0 ? 0 : minSegment);
执行合并操作
}
三:问题
for(int i = 0; i < 10; i ++)
{
StandGBAnalyzer an;
IndexWriter writer("index1", &an, (i == 0));
writer.setMergeFactor(2);
writer.setMinMergeDocs(10);
writer.setUseCompoundFile(true);
for(int j = 0; j < 20; j ++)
{
writer.addDocument(lpDoc);
}
writer.close();
}
StandGBAnalyzer an;
IndexWriter writer("index1", &an, false);
writer.setMergeFactor(2);
writer.setMinMergeDocs(2);
writer.optimize();
这样会在index1目录下生成几个文件? 按我的理解应该为1个.cfs文件+deletable+segments,实际上会生成41个文件,其中39个.cfs文件+deletable+segments,为什么呢?

http://spaces.msn.com/chenjm/Blog/cns!1p_4MqRmezVYLBO0XPor_HdQ!161.entry

发表评论

电子邮件地址不会被公开。 必填项已用*标注