咱们来看最复杂的部分,就是Term Dictionary和Term Index文件,Term Dictionary文件的后缀名为tim,Term Index文件的后缀名是tip,格式如图所示。apache
Term Dictionary文件首先是一个Header,接下来是PostingsHeader,这两个的格式一致,可是保存的是不一样的信息。SkipInterval是跳跃表的跳的幅度,MaxSkipLevels是跳跃表的层数,SkipMinimun是应用跳跃表的最小倒排表长度,接下来就是Term的部分了。数组
在tim文件中,Term是分红Block进行保存的,如何将Term进行分块,则须要和tip文件配合。Term Index文件对于每个Field都保存一个FSTIndex来帮助快速定位tim文件中属于这个Field的Term的位置,因为FSTIndex的长度不一样,为了快速定位某个Field的位置,则应用指针列表规则,为每个Field保存了指向这个Field的FSTIndex的指针。数据结构
这里比较使人困惑的一点就是,FST是什么,如何利用他来分块呢?eclipse
FST全程是Finite State Transducers,是一个带输出的有限状态机,看过前面有限状态机规则的能够知道,有限状态机逻辑上来说就是一颗树,就像图3-71中的那棵树,从初始状态输入字符a到达状态a,输入字符b到达状态b,输入字符d到达状态d,不一样的是状态d有输出,所谓的输出就是一个指针,指向tim文件中的位置。函数
Tim文件中Term的分块就是按照FST来的,图3-71中,Block 0中的全部的Term都是以abd为前缀的,Block 1中全部的Term都是以abe为前缀的。每个Block都有一个Block Header,里面指明这个Block包含几个Term,假设个数为N,Suffix里面包含了N个后缀,好比Block 0中包含Term “abdi”和”abdj”,则这里面保存”i”和”j”。Stats里面包含了N个统计信息,每一个统计信息包含docFreq和totalTermFreq。Metadata里面包含了指向倒排表文件frq和prx文件的指针。ui
Tim和tip文件的写入是由org.apache.lucene.codecs.BlockTreeTermsWriter来负责的,在它的构造函数中,生成了两个OutputStream,而且写入除了Block和FSTIndex以外的全部信息。编码
Lucene40PostingsWriter的start函数以下:3d
下面我们具体讨论,Term如何分块,Block如何写入,FSTIndex如何构造。指针
咱们首先经过一个简单的例子,来看一下一个普通的FST是如何构造的,Lucene的文档里面给了相似下面这样一个例子。code
这里InputValues是构造FST的输入,是根据这些字符串,构造出图3-71中的那棵树。
OutputValue是有限状态机的输出,因为在实际应用中,输出是一个指向tim文件的一个指针,通常是byte[]类型,因此咱们也在这里弄了三个byte[]做为输出。
Builder就是有限状态机的构造器,它支持多种输出类型,咱们这里用byte[]做为输出,因此输出类型咱们选择BytesRef,这是对byte[]的一个封装。
下一步就是用Builder的add函数将输入和输出关联起来,因为builder的输入必须是IntsRef类型,因此须要从字符串转换成为IntsRef类型,输出也要将byte[]封装为BytesRef。
Builder的finish函数真正构造一个FST,在内存中造成一个二进制结构,经过它能够经过输入,快速查询输出,例如程序中的给出输入”acf”就能获得输出[5 6]。
从表面现象来看,咱们甚至能够决定FST就是一个hash map,给出输入,获得输出。这就知足了做为Term Dictionary的要求,给出一个字符串,我立刻能找到倒排表的位置。
Builder里面一个很重要的成员变量UnCompiledNode<T>[] frontier,在FST的构造过程当中,它维护整棵FST树,其中里面直接保存的是UnCompiledNode,是当前添加的字符串所造成的状态节点,而前面添加的字符串造成的状态节点经过指针相互引用。
Builder.add函数主要包括四个部分:
当第一个字符串abd加入以后,frontier的结构如图3-72所示,图中蓝色的节点都是。
当新的字符串abe以后,首先(1)找出公共前缀ab,则prefixLenPlus1=3。而后调(2)用freezeTail将尾节点Sd进行冰封。为何要进行冰封(一个形象的说法)呢?由于Sd节点不会再改变了。在实际应用中,字符串都是按照字母顺序依次处理的,上一次的字符串是abd,下一个字符串多是abdm,再下一个字符串多是abdn,这都会致使Sd这个节点的变化。然而当abe出现后,说明abd*都不可能出现了,状态Sd也不可能再有新的子节点了,因此Sd也就肯定下来了,须要冰封。那么Sb节点要不要冰封呢?固然不行了,由于此次来了abe,下次还可能有abf, abg等等新的Sb的子节点出现,这就是为何要计算公共前缀了,公共前缀以后的状态节点都是能够冰封的了,而这些冰封的节点都从尾部开始,因此这一步的函数叫freezeTail。
freezeTail的实现以下:
freezeTail主要有两个分支,在Builder构造的时候,用户能够传进本身的freezeTail,若是用户指定了,则调用它的freeze函数,若是没有指定,则执行else部分默认的行为。在这里,咱们使用默认行为,在后面的代码分析中,咱们还能看到使用本身的freezeTail的状况。
默认行为中,从尾部到公共前缀节点,对于每一个状态节点,调用compileNode函数。在这以前,frontier里面保存的都是UnCompiledNode,通过compileNode函数后,就变成了CompiledNode,并从frontier摘下来,parent.replaceLast函数将父节点的指针指向新的CompiledNode。所谓compile过程,就是将内存中的数据结构变成二进制。
compileNode最终调用org.apache.lucene.util.fst.FST.addNode(UnCompiledNode<T>),代码以下:
而后(3)将新的input添加到frontier以后,变成如图3-73的数据结构。
依次类推,当添加acf以后,frontier变成以下的数据结构。
最后调用Builder的finish函数生成FST,代码以下:
造成的二进制数组如图3-75所示,因为有内容翻转,因此解析的时候须要从右向左解析。
了解了最基本的FST的原理以后,让咱们来一步一步经过代码,了解tim和tip文件的block和FSTIndex是如何生成的。
咱们如下图3-76为例子。默认状况下,BlockTreeTermsWriter有两个静态变量,DEFAULT_MIN_BLOCK_SIZE=25,DEFAULT_MAX_BLOCK_SIZE=48,MIN的意思是当某个状态节点的子节点个数超过25个的时候,能够写成一个Block,MAX的意思是当个数超过48的时候,则写成多个Block,多个Block构成一个层级Block。为了可以清晰的解析代码,咱们设DEFAULT_MIN_BLOCK_SIZE=2,DEFAULT_MAX_BLOCK_SIZE=4。咱们仅仅添加一篇文档,里面的Term依次为 abc abdf abdg abdh abei abej abek abel abem aben。所造成的状态树如图所示,根据MIN和MAX的设置,f, g, h会写成一个Block,i, j, k, l, m, n写成一个层级Block,c, d, e写成一个Block。咱们之因此把从a到n的十进制和十六进制列在这里,是由于在eclipse中,有时候字符显示的是十进制,有时候是十六进制,当看到这些数值的时候,知道是这些字符便可。
写tim和tip文件的过程纷繁复杂,下面的流程图3-77做为一个线索
每来一个新的Term,都调用finishTerm。
finishTerm的blockBuilder是没有output的,这个blockBuilder是用来进行Term分块的,而不是用来生成FSTIndex的。blockBuilder.add函数的流程和上面的叙述过的FST基本原理中的过程基本一致,不一样的是blockBuilder是被用户指定了freezeTail的,为org.apache.lucene.codecs.BlockTreeTermsWriter.TermsWriter.FindBlocks,因此freezeTail调用的是FindBlocks.freeze函数。这个freeze函数仅仅处理子节点的个数大于min的节点,调用writeBlocks函数将子节点写成block,对于不知足这个条件的节点,仅仅从frontier上摘下来,不作其余操做。
在整个过程当中,维护两个成员变量,一个是List<PendingEntry> pending保存还没有处理的Term或者block,对于Term,里面保存这个Term的text,docFreq,totalTermFreq信息。另外一个是pendingTerms,保存还没有处理的Term的freqStart和proxStart信息。
当加入abc,abdf,abdg,abdh以后,frontier成为以下的结构,在这个过程FindBlock.freeze什么都不作。这个时候的pending和pendingTerms也如图所示。
加入abei的时候,对Sd进行freeze的时候,发现Sd的出度为3,大于min,则开始调用BlockTreeTermsWriter.TermsWriter.writeBlocks(IntsRef, int, int)函数。
因为出度小于max,因此写成一个non floor的block。
写入一个Block的函数以下:
对于每个写成的block,都要为这个block生成一个FSTIndex,这个过程由函数BlockTreeTermsWriter.PendingBlock.compileIndex实现。
Block也写入了,FSTIndex也生成了,这个时候frontier,pending和pendingTerms的结果以下图所示。
这里须要解释一下的BLOCK:abd的FSTIndex里面的映射关系[-38,2]是如何得出来的?这是由下面这个函数计算出来的。fp=86, hasTerm=true, isFloor=false,则二进制位101011010,表示成为VInt为11011010, 00000010,为[-38,2],其实-38是补码。
接下来添加abei, abej, abek, abel, abem, aben以后,这个时候frontier,pending和pendingTerms的结果以下图3-80所示。
当全部的Term添加完毕后,BlockTreeTermsWriter.TermsWriter.finish被调用。
调用freezeTail(0)的时候,仍是调用FindBlocks.freeze函数,在freeze状态Se的时候,出度为6>min,因此调用writeBlocks,因为6>max,于是写入floor block。
写入firstBlock和floorBlocks的函数仍是上面写non floor block时调用的writeBlock函数,下面列出一些主要的变量的值。
写入了层级block而且生成FSTIndex以后,frontier,pending和pendingTerms的结果以下图所示。
这里须要解释的是[-77,3,1,107,33]表明的什么呢?首先abe指向的是层级Block,其中firstBlock的起始地址为108,fp=108, hasTerm=true, isFloor=true,则二进制为110110011,表示成为VInt为 [10110011, 00000011],为[-77,3],接下来是floorblock信息。
在函数BlockTreeTermsWriter.PendingBlock.compileIndex中,有这样一段:
接着写入floorBlock的个数,为1。接着写这个floorBlock的首字符k(107)。最后写floorBlock的首地址和firstBlock的首地址的差,sub.fp=124, fp=108, sub.hasTerms=true,因此为33。因此[abe]的output为[-77,3,1,107,33]。
在freeze状态Se以后,下面应该freeze状态Sb了,它的出度为3,因此先调用writeBlock写入一个non floor block的,而后调用compileIndex来为这个block产生新的FSTIndex。
写入Block的时候,一些重要的变量以下表所示。
表3-17 freeze状态Sb时writeBlock的变量
在compileIndex生成当前block的FSTIndex的时候,除了添加prefix=ab所对应的output以外,还会将子block,BLOCK:abd和BLOCK:abe的FSTIndex都添加过来,造成一个整的FSTIndex。
Freeze完状态Sb以后,frontier,pending和pendingTerms的结果以下图所示。
这里pending只有一项,全部子Block的FSTIndex都合并到BLOCK:ab中来,多了一个[ab]的output为[-30,4],这是由fp=152, hasTerm=true, isFloor=false编码出来的。
接下来对于状态Sa,出度为1,并不作什么。对于初始状态S0,出度也为1,按说不作什么,可是在FindBlocks.freeze函数中,有这样的代码:
这里除了判断出度是否>min,还有idx==0,对于状态S0,仍是须要调用writeBlocks,将BLOCK:ab写入tim中。
BlockTreeTermsWriter.TermsWriter.finish函数的blockBuilder.finish()就此结束。接下来从pending.get(0)获得根节点的FSTIndex,因为在compileIndex中,全部的子节点的FSTIndex都会加入到父节点中,最终根节点的FSTIndex是整个状态机的FSTIndex,而后将它写入在indexOut,也即tip文件中。
最终,tip和tim文件中Block和FSTIndex的格式和关系如图3-83所示。
最后咱们再看一下FSTIndex的二进制内容,以下图3-84所示。