- 浏览: 419480 次
- 性别:
- 来自: 北京
最新评论
-
springdata_spring:
apache lucene开源框架demo使用实例教程源代码下 ...
有关Lucene的问题(6):Lucene的事务性 -
jaychang:
必须要感谢作者的分享,对理解Lucene的工作原理帮助很大
Lucene学习总结之一:全文检索的基本原理 -
yin_kaihua:
...
Lucene学习总结之三:Lucene的索引文件格式 (1) -
djh122:
...
Lucene 原理与代码分析完整版 -
wayne0830:
多谢楼主分享!
Lucene 原理与代码分析完整版
3、将文档加入IndexWriter
代码:
writer.addDocument(doc); -->IndexWriter.addDocument(Document doc, Analyzer analyzer) -->doFlush = docWriter.addDocument(doc, analyzer); --> DocumentsWriter.updateDocument(Document, Analyzer, Term) 注:--> 代表一级函数调用 |
IndexWriter继而调用DocumentsWriter.addDocument,其又调用DocumentsWriter.updateDocument。
4、将文档加入DocumentsWriter
代码:
DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delTerm) -->(1) DocumentsWriterThreadState state = getThreadState(doc, delTerm); -->(2) DocWriter perDoc = state.consumer.processDocument(); -->(3) finishDocument(state, perDoc); |
DocumentsWriter对象主要包含以下几部分:
- 用于写索引文件
- IndexWriter writer;
- Directory directory;
- Similarity similarity:分词器
- String segment:当前的段名,每当flush的时候,将索引写入以此为名称的段。
IndexWriter.doFlushInternal() --> String segment = docWriter.getSegment();//return segment --> newSegment = new SegmentInfo(segment,……); --> docWriter.createCompoundFile(segment);//根据segment创建cfs文件。 |
-
- String docStoreSegment:存储域所要写入的目标段。(在索引文件格式一文中已经详细描述)
- int docStoreOffset:存储域在目标段中的偏移量。
- int nextDocID:下一篇添加到此索引的文档ID号,对于同一个索引文件夹,此变量唯一,且同步访问。
- DocConsumer consumer; 这是整个索引过程的核心,是IndexChain整个索引链的源头。
基本索引链: 对于一篇文档的索引过程,不是由一个对象来完成的,而是用对象组合的方式形成的一个处理链,链上的每个对象仅仅处理索引过程的一部分,称为索引链,由于后面还有其他的索引链,所以此处的索引链我称为基本索引链。 DocConsumer consumer 类型为DocFieldProcessor,是整个索引链的源头,包含如下部分:
|
- 删除文档
- BufferedDeletes deletesInRAM = new BufferedDeletes();
- BufferedDeletes deletesFlushed = new BufferedDeletes();
类BufferedDeletes包含了一下的成员变量:
由此可见,文档的删除主要有三种方式:
删除文档既可以用reader进行删除,也可以用writer进行删除,不同的是,reader进行删除后,此reader马上能够生效,而用writer删除后,会被缓存在deletesInRAM及deletesFlushed中,只有写入到索引文件中,当reader再次打开的时候,才能够看到。 那deletesInRAM和deletesFlushed各有什么用处呢? 此版本的Lucene对文档的删除是支持多线程的,当用IndexWriter删除文档的时候,都是缓存在deletesInRAM中的,直到flush,才将删除的文档写入到索引文件中去,我们知道flush是需要一段时间的,那么在flush的过程中,另一个线程又有文档删除怎么办呢? 一般过程是这个样子的,当flush的时候,首先在同步(synchornized)的方法pushDeletes中,将deletesInRAM全部加到deletesFlushed中,然后将deletesInRAM清空,退出同步方法,于是flush的线程程就向索引文件写deletesFlushed中的删除文档的过程,而与此同时其他线程新删除的文档则添加到新的deletesInRAM中去,直到下次flush才写入索引文件。 |
- 缓存管理
- 为了提高索引的速度,Lucene对很多的数据进行了缓存,使一起写入磁盘,然而缓存需要进行管理,何时分配,何时回收,何时写入磁盘都需要考虑。
- ArrayList freeCharBlocks = new ArrayList();将用于缓存词(Term)信息的空闲块
- ArrayList freeByteBlocks = new ArrayList();将用于缓存文档号(doc id)及词频(freq),位置(prox)信息的空闲块。
- ArrayList freeIntBlocks = new ArrayList();将存储某词的词频(freq)和位置(prox)分别在byteBlocks中的偏移量
- boolean bufferIsFull;用来判断缓存是否满了,如果满了,则应该写入磁盘
- long numBytesAlloc;分配的内存数量
- long numBytesUsed;使用的内存数量
- long freeTrigger;应该开始回收内存时的内存用量。
- long freeLevel;回收内存应该回收到的内存用量。
- long ramBufferSize;用户设定的内存用量。
缓存用量之间的关系如下: DocumentsWriter.setRAMBufferSizeMB(double mb){ ramBufferSize = (long) (mb*1024*1024);//用户设定的内存用量,当使用内存大于此时,开始写入磁盘 DocumentsWriter.balanceRAM(){ if (numBytesAlloc+deletesRAMUsed > freeTrigger) { //当分配的内存加删除文档所占用的内存大于105%的时候,开始释放内存 while(numBytesAlloc+deletesRAMUsed > freeLevel) { //一直进行释放,直到95% //释放free blocks byteBlockAllocator.freeByteBlocks.remove(byteBlockAllocator.freeByteBlocks.size()-1); freeCharBlocks.remove(freeCharBlocks.size()-1); freeIntBlocks.remove(freeIntBlocks.size()-1); if (numBytesUsed+deletesRAMUsed > ramBufferSize){ //当使用的内存加删除文档占有的内存大于用户指定的内存时,可以写入磁盘 bufferIsFull = true; } } 当判断是否应该写入磁盘时:
DocumentsWriter.timeToFlushDeletes(){ return (bufferIsFull || deletesFull()) && setFlushPending(); } DocumentsWriter.deletesFull(){ return (ramBufferSize != IndexWriter.DISABLE_AUTO_FLUSH && } |
- 多线程并发索引
- 为了支持多线程并发索引,对每一个线程都有一个DocumentsWriterThreadState,其为每一个线程根据DocConsumer consumer的索引链来创建每个线程的索引链(XXXPerThread),来进行对文档的并发处理。
- DocumentsWriterThreadState[] threadStates = new DocumentsWriterThreadState[0];
- HashMap threadBindings = new HashMap();
- 虽然对文档的处理过程可以并行,但是将文档写入索引文件却必须串行进行,串行写入的代码在DocumentsWriter.finishDocument中
- WaitQueue waitQueue = new WaitQueue()
- long waitQueuePauseBytes
- long waitQueueResumeBytes
在Lucene中,文档是按添加的顺序编号的,DocumentsWriter中的nextDocID就是记录下一个添加的文档id。 当Lucene支持多线程的时候,就必须要有一个synchornized方法来付给文档id并且将nextDocID加一,这些是在DocumentsWriter.getThreadState这个函数里面做的。 虽然给文档付ID没有问题了。但是由Lucene索引文件格式我们知道,文档是要按照ID的顺序从小到大写到索引文件中去的,然而不同的文档处理速度不同,当一个先来的线程一处理一篇需要很长时间的大文档时,另一个后来的线程二可能已经处理了很多小的文档了,但是这些后来小文档的ID号都大于第一个线程所处理的大文档,因而不能马上写到索引文件中去,而是放到waitQueue中,仅仅当大文档处理完了之后才写入索引文件。 waitQueue中有一个变量nextWriteDocID表示下一个可以写入文件的ID,当付给大文档ID=4时,则nextWriteDocID也设为4,虽然后来的小文档5,6,7,8等都已处理结束,但是如下代码, WaitQueue.add(){ if (doc.docID == nextWriteDocID){ doPause() } 则把5, 6, 7, 8放入waiting队列,并且记录当前等待的文档所占用的内存大小waitingBytes。 当大文档4处理完毕后,不但写入文档4,把原来等待的文档5, 6, 7, 8也一起写入。 WaitQueue.add(){ if (doc.docID == nextWriteDocID) { writeDocument(doc); while(true) { doc = waiting[nextWriteLoc]; writeDocument(doc); } } else { ………… } doPause() } 但是这存在一个问题:当大文档很大很大,处理的很慢很慢的时候,后来的线程二可能已经处理了很多的小文档了,这些文档都是在waitQueue中,则占有了越来越多的内存,长此以往,有内存不够的危险。 因而在finishDocuments里面,在WaitQueue.add最后调用了doPause()函数 DocumentsWriter.finishDocument(){ doPause = waitQueue.add(docWriter); if (doPause) notifyAll(); } WaitQueue.doPause() { 当waitingBytes足够大的时候(为用户指定的内存使用量的10%),doPause返回true,于是后来的线程二会进入wait状态,不再处理另外的文档,而是等待线程一处理大文档结束。 当线程一处理大文档结束的时候,调用notifyAll唤醒等待他的线程。 DocumentsWriter.waitForWaitQueue() { WaitQueue.doResume() { 当waitingBytes足够小的时候,doResume返回true, 则线程二不用再wait了,可以继续处理另外的文档。 |
- 一些标志位
- int maxFieldLength:一篇文档中,一个域内可索引的最大的词(Term)数。
- int maxBufferedDeleteTerms:可缓存的最大的删除词(Term)数。当大于这个数的时候,就要写到文件中了。
此过程又包含如下三个子过程:
4.1、得到当前线程对应的文档集处理对象(DocumentsWriterThreadState)
代码为:
DocumentsWriterThreadState state = getThreadState(doc, delTerm); |
在Lucene中,对于同一个索引文件夹,只能够有一个IndexWriter打开它,在打开后,在文件夹中,生成文件write.lock,当其他IndexWriter再试图打开此索引文件夹的时候,则会报org.apache.lucene.store.LockObtainFailedException错误。
这样就出现了这样一个问题,在同一个进程中,对同一个索引文件夹,只能有一个IndexWriter打开它,因而如果想多线程向此索引文件夹中添加文档,则必须共享一个IndexWriter,而且在以往的实现中,addDocument函数是同步的(synchronized),也即多线程的索引并不能起到提高性能的效果。
于是为了支持多线程索引,不使IndexWriter成为瓶颈,对于每一个线程都有一个相应的文档集处理对象(DocumentsWriterThreadState),这样对文档的索引过程可以多线程并行进行,从而增加索引的速度。
getThreadState函数是同步的(synchronized),DocumentsWriter有一个成员变量threadBindings,它是一个HashMap,键为线程对象(Thread.currentThread()),值为此线程对应的DocumentsWriterThreadState对象。
DocumentsWriterThreadState DocumentsWriter.getThreadState(Document doc, Term delTerm)包含如下几个过程:
- 根据当前线程对象,从HashMap中查找相应的DocumentsWriterThreadState对象,如果没找到,则生成一个新对象,并添加到HashMap中
DocumentsWriterThreadState state = (DocumentsWriterThreadState) threadBindings.get(Thread.currentThread()); if (state == null) { …… state = new DocumentsWriterThreadState(this); …… threadBindings.put(Thread.currentThread(), state); } |
- 如果此线程对象正在用于处理上一篇文档,则等待,直到此线程的上一篇文档处理完。
DocumentsWriter.getThreadState() { waitReady(state); state.isIdle = false; } waitReady(state) { while (!state.isIdle) {wait();} } 显然如果state.isIdle为false,则此线程等待。 在一篇文档处理之前,state.isIdle = false会被设定,而在一篇文档处理完毕之后,DocumentsWriter.finishDocument(DocumentsWriterThreadState perThread, DocWriter docWriter)中,会首先设定perThread.isIdle = true; 然后notifyAll()来唤醒等待此文档完成的线程,从而处理下一篇文档。 |
- 如果IndexWriter刚刚commit过,则新添加的文档要加入到新的段中(segment),则首先要生成新的段名。
initSegmentName(false); --> if (segment == null) segment = writer.newSegmentName(); |
- 将此线程的文档处理对象设为忙碌:state.isIdle = false;
4.2、用得到的文档集处理对象(DocumentsWriterThreadState)处理文档
代码为:
DocWriter perDoc = state.consumer.processDocument(); |
每一个文档集处理对象DocumentsWriterThreadState都有一个文档及域处理对象DocFieldProcessorPerThread,它的成员函数processDocument()被调用来对文档及域进行处理。
线程索引链(XXXPerThread):
由于要多线程进行索引,因而每个线程都要有自己的索引链,称为线程索引链。 线程索引链同基本索引链有相似的树形结构,由基本索引链中每个层次的对象调用addThreads进行创建的,负责每个线程的对文档的处理。 DocFieldProcessorPerThread是线程索引链的源头,由DocFieldProcessor.addThreads(…)创建 DocFieldProcessorPerThread对象结构如下:
|
DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下几个过程:
4.2.1、开始处理当前文档
consumer(DocInverterPerThread).startDocument(); |
在此版的Lucene中,几乎所有的XXXPerThread的类,都有startDocument和finishDocument两个函数,因为对同一个线程,这些对象都是复用的,而非对每一篇新来的文档都创建一套,这样也提高了效率,也牵扯到数据的清理问题。一般在startDocument函数中,清理处理上篇文档遗留的数据,在finishDocument中,收集本次处理的结果数据,并返回,一直返回到DocumentsWriter.updateDocument(Document, Analyzer, Term) 然后根据条件判断是否将数据刷新到硬盘上。
4.2.2、逐个处理文档的每一个域
由于一个线程可以连续处理多个文档,而在普通的应用中,几乎每篇文档的域都是大致相同的,为每篇文档的每个域都创建一个处理对象非常低效,因而考虑到复用域处理对象DocFieldProcessorPerField,对于每一个域都有一个此对象。
那当来到一个新的域的时候,如何更快的找到此域的处理对象呢?Lucene创建了一个DocFieldProcessorPerField[] fieldHash哈希表来方便更快查找域对应的处理对象。
当处理各个域的时候,按什么顺序呢?其实是按照域名的字典顺序。因而Lucene创建了DocFieldProcessorPerField[] fields的数组来方便按顺序处理域。
因而一个域的处理对象被放在了两个地方。
对于域的处理过程如下:
4.2.2.1、首先:对于每一个域,按照域名,在fieldHash中查找域处理对象DocFieldProcessorPerField,代码如下:
final int hashPos = fieldName.hashCode() & hashMask;//计算哈希值 |
如果能够找到,则更新DocFieldProcessorPerField中的域信息fp.fieldInfo.update(field.isIndexed()…)
如果没有找到,则添加域到DocFieldProcessorPerThread.fieldInfos中,并创建新的DocFieldProcessorPerField,且将其加入哈希表。代码如下:
fp = new DocFieldProcessorPerField(this, fi); |
如果是一个新的field,则将其加入fields数组fields[fieldCount++] = fp;
并且如果是存储域的话,用StoredFieldsWriterPerThread将其写到索引中:
if (field.isStored()) { |
4.2.2.1.1、处理存储域的过程如下:
StoredFieldsWriterPerThread.addField(Fieldable field, FieldInfo fieldInfo) --> localFieldsWriter.writeField(fieldInfo, field); |
FieldsWriter.writeField(FieldInfo fi, Fieldable field)代码如下:
请参照fdt文件的格式,则一目了然: fieldsStream.writeVInt(fi.number);//文档号 fieldsStream.writeByte(bits); //域的属性位 if (field.isCompressed()) {//对于压缩域 fieldsStream.writeVInt(len);//写长度 |
4.2.2.2、然后:对fields数组进行排序,是域按照名称排序。quickSort(fields, 0, fieldCount-1);
4.2.2.3、最后:按照排序号的顺序,对域逐个处理,此处处理的仅仅是索引域,代码如下:
for(int i=0;i fields[i].consumer.processFields(fields[i].fields, fields[i].fieldCount); |
域处理对象(DocFieldProcessorPerField)结构如下:
域索引链: 每个域也有自己的索引链,称为域索引链,每个域的索引链也有同线程索引链有相似的树形结构,由线程索引链中每个层次的每个层次的对象调用addField进行创建,负责对此域的处理。 和基本索引链及线程索引链不同的是,域索引链仅仅负责处理索引域,而不负责存储域的处理。 DocFieldProcessorPerField是域索引链的源头,对象结构如下:
|
4.2.2.3.1、处理索引域的过程如下:
DocInverterPerField.processFields(Fieldable[], int) 过程如下:
- 判断是否要形成倒排表,代码如下:
boolean doInvert = consumer.start(fields, count); --> TermsHashPerField.start(Fieldable[], int) --> for(int i=0;i if (fields[i].isIndexed()) return true; return false; |
读到这里,大家可能会发生困惑,既然XXXPerField是对于每一个域有一个处理对象的,那为什么参数传进来的是Fieldable[]数组, 并且还有域的数目count呢?
其实这不经常用到,但必须得提一下,由上面的fieldHash的实现我们可以看到,是根据域名进行哈希的,所以准确的讲,XXXPerField并非对于每一个域有一个处理对象,而是对每一组相同名字的域有相同的处理对象。
对于同一篇文档,相同名称的域可以添加多个,代码如下:
doc.add(new Field("contents", "the content of the file.", Field.Store.NO, Field.Index.NOT_ANALYZED)); |
则传进来的名为"contents"的域如下:
fields Fieldable[2] (id=52) |
- 对传进来的同名域逐一处理,代码如下
for(int i=0;i final Fieldable field = fields[i]; if (field.isIndexed() && doInvert) { //仅仅对索引域进行处理 if (!field.isTokenized()) { //如果此域不分词,见(1)对不分词的域的处理 } else { //如果此域分词,见(2)对分词的域的处理 } } } |
(1) 对不分词的域的处理
(1-1) 得到域的内容,并构建单个Token形成的SingleTokenAttributeSource。因为不进行分词,因而整个域的内容算做一个Token.
String stringValue = field.stringValue(); //stringValue "200910240957"
final int valueLength = stringValue.length();
perThread.singleToken.reinit(stringValue, 0, valueLength);
对于此域唯一的一个Token有以下的属性:
- Term:文字信息。在处理过程中,此值将保存在TermAttribute的实现类实例化的对象TermAttributeImp里面。
- Offset:偏移量信息,是按字或字母的起始偏移量和终止偏移量,表明此Token在文章中的位置,多用于加亮。在处理过程中,此值将保存在OffsetAttribute的实现类实例化的对象OffsetAttributeImp里面。
在SingleTokenAttributeSource里面,有一个HashMap来保存可能用于保存属性的类名(Key,准确的讲是接口)以及保存属性信息的对象(Value):
singleToken DocInverterPerThread$SingleTokenAttributeSource (id=150) |
(1-2) 得到Token的各种属性信息,为索引做准备。
consumer.start(field)做的主要事情就是根据各种属性的类型来构造保存属性的对象(HashMap中有则取出,无则构造),为索引做准备。
consumer(TermsHashPerField).start(…) --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的就是上述HashMap中的TermAttributeImpl --> consumer(FreqProxTermsWriterPerField).start(f); --> if (fieldState.attributeSource.hasAttribute(PayloadAttribute.class)) { payloadAttribute = fieldState.attributeSource.getAttribute(PayloadAttribute.class); --> nextPerField(TermsHashPerField).start(f); --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的还是上述HashMap中的TermAttributeImpl --> consumer(TermVectorsTermsWriterPerField).start(f); --> if (doVectorOffsets) { offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class); |
(1-3) 将Token加入倒排表
consumer(TermsHashPerField).add();
加入倒排表的过程,无论对于分词的域和不分词的域,过程是一样的,因而放到对分词的域的解析中一起说明。
(2) 对分词的域的处理
(2-1) 构建域的TokenStream
final TokenStream streamValue = field.tokenStreamValue(); //用户可以在添加域的时候,应用构造函数public Field(String name, TokenStream tokenStream) 直接传进一个TokenStream过来,这样就不用另外构建一个TokenStream了。 if (streamValue != null) …… stream = docState.analyzer.reusableTokenStream(fieldInfo.name, reader); } |
此时TokenStream的各项属性值还都是空的,等待一个一个被分词后得到,此时的TokenStream对象如下:
stream StopFilter (id=112) |
(2-2) 得到第一个Token,并初始化此Token的各项属性信息,并为索引做准备(start)。
boolean hasMoreTokens = stream.incrementToken();//得到第一个Token
OffsetAttribute offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);//得到偏移量属性
offsetAttribute OffsetAttributeImpl (id=164) |
PositionIncrementAttribute posIncrAttribute = fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);//得到位置属性
posIncrAttribute PositionIncrementAttributeImpl (id=129) |
consumer.start(field);//其中得到了TermAttribute属性,如果存储payload则得到PayloadAttribute属性,如果存储词向量则得到OffsetAttribute属性。
(2-3) 进行循环,不断的取下一个Token,并添加到倒排表
for(;;) { if (!hasMoreTokens) break; …… …… |
(2-4) 添加Token到倒排表的过程consumer(TermsHashPerField).add()
TermsHashPerField对象主要包括以下部分:
- CharBlockPool charPool; 用于存储Token的文本信息,如果不足时,从DocumentsWriter中的freeCharBlocks分配
- ByteBlockPool bytePool;用于存储freq, prox信息,如果不足时,从DocumentsWriter中的freeByteBlocks分配
- IntBlockPool intPool; 用于存储分别指向每个Token在bytePool中freq和prox信息的偏移量。如果不足时,从DocumentsWriter的freeIntBlocks分配
- TermsHashConsumerPerField consumer类型为FreqProxTermsWriterPerField,用于写freq, prox信息到缓存中。
- RawPostingList[] postingsHash = new RawPostingList[postingsHashSize];存储倒排表,每一个Term都有一个RawPostingList (PostingList),其中包含了int textStart,也即文本在charPool中的偏移量,int byteStart,即此Term的freq和prox信息在bytePool中的起始偏移量,int intStart,即此term的在intPool中的起始偏移量。
形成倒排表的过程如下:
//得到token的文本及文本长度 final char[] tokenText = termAtt.termBuffer();//[s, t, u, d, e, n, t, s] final int tokenTextLen = termAtt.termLength();//tokenTextLen 8 //按照token的文本计算哈希值,以便在postingsHash中找到此token对应的倒排表 int downto = tokenTextLen; int hashPos = code & postingsHashMask; //在倒排表哈希表中查找此Token,如果找到相应的位置,但是不是此Token,说明此位置存在哈希冲突,采取重新哈希rehash的方法。 p = postingsHash[hashPos]; if (p != null && !postingEquals(tokenText, tokenTextLen)) { //如果此Token之前从未出现过 if (p == null) { if (textLen1 + charPool.charUpto > DocumentsWriter.CHAR_BLOCK_SIZE) { //当charPool不足的时候,在freeCharBlocks中分配新的buffer charPool.nextBuffer(); } //从空闲的倒排表中分配新的倒排表 p = perThread.freePostings[--perThread.freePostingsCount]; //将文本复制到charPool中 final char[] text = charPool.buffer; //将倒排表放入哈希表中 postingsHash[hashPos] = p; if (numPostingInt + intPool.intUpto > DocumentsWriter.INT_BLOCK_SIZE) intPool.nextBuffer(); //当intPool不足的时候,在freeIntBlocks中分配新的buffer。 if (DocumentsWriter.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE) bytePool.nextBuffer(); //当bytePool不足的时候,在freeByteBlocks中分配新的buffer。 //此处streamCount为2,表明在intPool中,每两项表示一个词,一个是指向bytePool中freq信息偏移量的,一个是指向bytePool中prox信息偏移量的。 intUptos = intPool.buffer; p.intStart = intUptoStart + intPool.intOffset; //在bytePool中分配两个空间,一个放freq信息,一个放prox信息的。 final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); //当Term原来没有出现过的时候,调用newTerm consumer(FreqProxTermsWriterPerField).newTerm(p); } //如果此Token之前曾经出现过,则调用addTerm。 else { intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT]; } |
(2-5) 添加新Term的过程,consumer(FreqProxTermsWriterPerField).newTerm
final void newTerm(RawPostingList p0) { writeProx(FreqProxTermsWriter.PostingList p, int proxCode) { termsHashPerField.writeVInt(1, proxCode<<1);//第一个参数所谓1,也就是写入此文档在intPool中的第1项——prox信息。为什么左移一位呢?是因为后面可能跟着payload信息,参照索引文件格式(1)中或然跟随规则。 } |
(2-6) 添加已有Term的过程
final void addTerm(RawPostingList p0) { FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; if (docState.docID != p.lastDocID) { //当文档ID变了的时候,说明上一篇文档已经处理完毕,可以写入freq信息了。 //第一个参数所谓0,也就是写入上一篇文档在intPool中的第0项——freq信息。至于信息为何这样写,参照索引文件格式(1)中的或然跟随规则,及tis文件格式。 if (1 == p.docFreq) //当文档ID不变的时候,说明此文档中这个词又出现了一次,从而freq加一,写入再次出现的位置信息,用差值。 |
(2-7) 结束处理当前域
consumer(TermsHashPerField).finish(); --> FreqProxTermsWriterPerField.finish() --> TermVectorsTermsWriterPerField.finish() endConsumer(NormsWriterPerField).finish(); --> norms[upto] = Similarity.encodeNorm(norm);//计算标准化因子的值。 --> docIDs[upto] = docState.docID; |
4.2.3、结束处理当前文档
final DocumentsWriter.DocWriter one = fieldsWriter(StoredFieldsWriterPerThread).finishDocument();
存储域返回结果:一个写成了二进制的存储域缓存。
one StoredFieldsWriter$PerDoc (id=322) |
final DocumentsWriter.DocWriter two = consumer(DocInverterPerThread).finishDocument();
--> NormsWriterPerThread.finishDocument()
--> TermsHashPerThread.finishDocument()
索引域的返回结果为null
4.3、用DocumentsWriter.finishDocument结束本次文档添加
代码:
DocumentsWriter.updateDocument(Document, Analyzer, Term) --> DocumentsWriter.finishDocument(DocumentsWriterThreadState, DocumentsWriter$DocWriter) --> doPause = waitQueue.add(docWriter);//有关waitQueue,在DocumentsWriter的缓存管理中已作解释 --> DocumentsWriter$WaitQueue.writeDocument(DocumentsWriter$DocWriter) --> StoredFieldsWriter$PerDoc.finish() --> fieldsWriter.flushDocument(perDoc.numStoredFields, perDoc.fdt);将存储域信息真正写入文件。 |
发表评论
-
Lucene应用开发揭秘
2011-09-25 22:13 5471Lucene应用开发揭秘 ... -
Lucene应用开发揭秘上线了
2011-09-09 23:54 114Lucene应用开发揭秘 华章培训网地址:http:/ ... -
LinkedIn公司实现的实时搜索引擎Zoie
2010-11-29 21:19 8677一、总体架构 Zoie是linkedin公司基于Luce ... -
Lucene 原理与代码分析完整版
2010-06-13 01:30 35275Lucene 原理与代码分析系列文章已经基本告一段落, ... -
Lucene学习总结之十:Lucene的分词器Analyzer
2010-06-06 22:13 73721、抽象类Analyzer 其主要包含两个接口,用于生 ... -
Lucene学习总结之九:Lucene的查询对象
2010-05-19 02:39 2903Lucene学习总结之九:Lucene的查询对象(1) ... -
Lucene学习总结之九:Lucene的查询对象(3)
2010-05-19 02:37 30206、FilteredQuery FilteredQu ... -
Lucene学习总结之九:Lucene的查询对象(2)
2010-05-19 02:36 26585、SpanQuery 所谓SpanQ ... -
Lucene学习总结之九:Lucene的查询对象(1)
2010-05-19 02:34 6448Lucene除了支持查询语法以外,还可以自己构造查询对象 ... -
Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser
2010-05-08 13:41 2431Lucene学习总结之八:Lucene的查询语法,Java ... -
Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser(2)
2010-05-08 00:25 5645三、解析QueryParser.jj 3.1、声明Qu ... -
Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser(1)
2010-05-08 00:20 8459一、Lucene的查询语法 Lucene所支持的查询语 ... -
Lucene学习总结之七:Lucene搜索过程解析
2010-04-05 14:52 2996本系列文章将详细描述几乎最新版本的Lucene的基本原理 ... -
Lucene学习总结之七:Lucene搜索过程解析
2010-04-04 22:54 2685本系列文章将详细描述几乎最新版本的Lucene的基本原理 ... -
Lucene学习总结之七:Lucene搜索过程解析(8)
2010-04-04 22:43 77602.4、搜索查询对象 2.4.4、收集文档结果集 ... -
Lucene学习总结之七:Lucene搜索过程解析(7)
2010-04-04 22:39 44802.4、搜索查询对象 2.4.3.2、并集Di ... -
Lucene学习总结之七:Lucene搜索过程解析(6)
2010-04-04 22:20 37102.4、搜索查询对象 2.4.3、进行倒排表合并 在 ... -
Lucene学习总结之七:Lucene搜索过程解析(5)
2010-04-04 21:26 44522.4、搜索查询对象 2.4.2、创建Score ... -
Lucene学习总结之七:Lucene搜索过程解析(4)
2010-04-04 20:46 45442.4、搜索查询对象 2.4.1.2、创建Weight ... -
Lucene学习总结之七:Lucene搜索过程解析(3)
2010-04-04 20:19 43812.3、QueryParser解析查询语句生成查询对象 代码 ...
相关推荐
Lucene学习总结之一:全文检索的基本原理 ...Lucene学习总结之四:Lucene索引过程分析(2) Lucene学习总结之四:Lucene索引过程分析(3) Lucene学习总结之四:Lucene索引过程分析(4) www.chinaandroid.com
索引创建(Indexing)阶段,Lucene会分析文档内容,将文本分解成一个个单独的词语(称为术语或Token),然后建立反向索引。反向索引的核心是一个词典,其中每个词汇都有一个列表,列出了包含该词汇的所有文档编号。...
总结起来,Lucene5学习之增量索引(Zoie)涉及到的关键技术点包括: 1. 基于Lucene的增量索引解决方案:Zoie系统。 2. 主从复制架构:Index Provider和Index User的角色。 3. 数据变更追踪:通过变更日志实现增量索引...
**Lucene5学习之创建索引入门示例** 在IT领域,搜索引擎的开发与优化是一项关键技术,而Apache Lucene作为一款高性能、全文本搜索库,是许多开发者进行文本检索的首选工具。本文将深入探讨如何使用Lucene5来创建一...
#### 四、Lucene索引过程分析 Lucene的索引过程是一个复杂而有序的操作流程,主要步骤如下: - **1. 创建IndexWriter对象**:初始化索引写入器。 - **2. 创建文档Document对象,并加入域(Field)**:定义文档结构和...
以上就是关于“Lucene索引的简单使用”的详细介绍,包括其核心概念、创建和查询索引的步骤以及一些高级特性。希望对你理解和应用Lucene有所帮助。在实际开发中,可以根据需求选择合适的Analyzer,优化索引策略,以...
Lucene通过分析这些文本,将其拆分为术语,并在倒排索引中存储每个术语的位置信息,以便快速定位到包含特定术语的文档。 **2. 增量索引的概念** 增量索引的目的是避免重新构建整个索引,尤其是在大型数据集上,这...
本篇文章将详细介绍如何使用Lucene3.0来创建索引,并通过一个具体的例子来演示整个过程。 #### 一、Lucene3.0简介 Lucene是一款高性能、全功能的全文搜索引擎库。它为开发者提供了构建搜索应用所需的所有基本工具...
同时,`HelloLucene_delete`这个压缩包文件可能是某个示例项目,通过分析其中的代码,你可以更直观地了解Lucene删除索引的实现方式。 总之,Lucene的删除索引机制是一个复杂但高效的过程,涉及到了位向量、段管理和...
以下是对Lucene索引机制的详细解析: 一、Lucene的索引过程 1. 文档分析:当向Lucene添加文档时,首先会经过一个分词器(Tokenizer),将文本拆分成一系列的词项(Token)。接着,这些词项会被过滤(Filter)和...
2. **Luke 工具**:Luke 是一个用于查看和分析 Lucene 索引的桌面应用程序,它的全名是 Lucene Index Toolbox。Luke 可以显示索引的详细信息,包括字段、文档数量、词项频率、术语字典、文档分词等,是开发者调试和...
2. `org.apache.lucene.document.Document` 和 `org.apache.lucene.document.Field`:构建索引的基本元素。 3. `org.apache.lucene.analysis.Analyzer`:查看分词器的实现,了解分词逻辑。 4. `org.apache.lucene....
通过阅读和分析源代码,我们可以学习到如何操作Lucene索引,以及如何构建类似的工具。 总结而言,luke作为Lucene索引的可视化工具,极大地便利了开发者对索引的理解和调试。无论是初学者还是经验丰富的开发人员,都...
#### 四、示例代码分析 1. **SAX处理器类** (`SAXXML`) ```java public class SAXXML extends DefaultHandler { private StringBuffer iobuf; private HashMap attrmap; private Document doc; public void ...
在使用 Lucene 进行信息检索时,有时我们需要对建立的索引进行查看、调试或分析,这时就需要借助 Lucene 的索引查看工具。 Luke 是一个非常实用的 Lucene 索引浏览器,全称为 Lucidworks Luke。它允许用户以图形化...
**Lucene学习总结** 在深入理解Lucene之前,我们首先需要了解什么是全文检索。全文检索是一种从大量文本数据中快速查找所需信息的技术。它通过建立索引来实现高效的搜索,而Lucene正是Java环境下最著名的全文搜索...
**Lucene索引器实例详解** Lucene是一个高性能、全文本搜索库,由Apache软件基金会开发,被广泛应用于各种搜索引擎的构建。它提供了一个高级的、灵活的、可扩展的接口,使得开发者能够轻松地在应用程序中实现全文...
【基于 Lucene 索引的分析与实现】 在当今信息爆炸的时代,互联网上的数据量呈指数级增长,人们对于高效检索信息的需求日益强烈。Lucene,作为一个强大的Java全文检索库,提供了便捷的索引和搜索功能,为解决海量...
**Lucene索引和查询** Lucene是Apache软件基金会的开放源码全文...本项目提供了一个基础的实现示例,对于初学者来说,是学习Lucene索引和查询的绝佳起点。在实际应用中,可以进一步扩展和优化,以满足更复杂的需求。