我们来看最复杂的部分,就是Term Dictionary和Term Index文件,Term Dictionary文件的后缀名为tim,Term Index文件的后缀名是tip,格式如图所示。

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是什么,如何利用他来分块呢?

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文件的指针。

Tim和tip文件的写入是由org.apache.lucene.codecs.BlockTreeTermsWriter来负责的,在它的构造函数中,生成了两个OutputStream,并且写入除了Block和FSTIndex之外的所有信息。

Lucene40PostingsWriter的start函数如下:

下面咱们具体讨论,Term如何分块,Block如何写入,FSTIndex如何构造。

我们首先通过一个简单的例子,来看一下一个普通的FST是如何构造的,Lucene的文档里面给了类似下面这样一个例子。

这里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所示。

Lucene 4.X 倒排索引原理与实现: (3) Term Dictionary和Index文件 (FST详细解析)的更多相关文章

  1. Lucene 4.X 倒排索引原理与实现: (3) Term Dictionary和Index文件 (FST详细解析)——直接看例子就明白了!!!

    转自: http://www.cnblogs.com/forfuture1978/p/3945755.html 好好看看吧 倒排列表信息中词典相关存储的最关键格式 占倒排列表中文件大小的多数 我们来看 ...

  2. Lucene 4.X 倒排索引原理与实现: (1) 词典的设计

    词典的格式设计 词典中所保存的信息主要是三部分: Term字符串 Term的统计信息,比如文档频率(Document Frequency) 倒排表的位置信息 其中Term字符串如何保存是一个很大的问题 ...

  3. Lucene 4.X 倒排索引原理与实现: (2) 倒排表的格式设计

    1. 定长编码 最容易想到的方式就是常用的普通二进制编码,每个数值占用的长度相同,都占用最大的数值所占用的位数,如图所示. 这里有一个文档ID列表,254,507,756,1007,如果按照二进制定长 ...

  4. Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理

    Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理 2017年01月04日 08:52:12 阅读数:18366 基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB ...

  5. 深度解析 Lucene 轻量级全文索引实现原理

    一.Lucene简介 1.1 Lucene是什么? Lucene是Apache基金会jakarta项目组的一个子项目: Lucene是一个开放源码的全文检索引擎工具包,提供了完整的查询引擎和索引引擎, ...

  6. Lucene 的索引文件锁原理

    Lucene 的索引文件锁原理 2016/11/24 · IT技术 · lucene   环境 Lucene 6.0.0Java “1.8.0_111”OS Windows 7 Ultimate 线程 ...

  7. es倒排索引原理解析

    倒排索引原理 普通的存储方式是给每个文档编一个序号 然后让这个序号对应单个文档的所有内容  如果用这样的方式查找   当需要查找某个单词的时候需要遍历所有的文档集合 查找文档的效率会非常的慢 2.基本 ...

  8. C++多态的实现及原理详细解析

    C++多态的实现及原理详细解析 作者: 字体:[增加 减小] 类型:转载   C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型 ...

  9. 《Java虚拟机原理图解》 1.2.2、Class文件里的常量池具体解释(上)

    [last updated:2014/11/27] NO1.常量池在class文件的什么位置? 我的上一篇文章<Java虚拟机原理图解> 1.class文件基本组织结构中已经提到了clas ...

随机推荐

  1. 使用JMeter进行负载测试——终极指南

    这篇教程讨论的是JMeter,它是一款基于Java的.集合了几个应用程序.具有特定用途的负载和性能测试工具. 本篇主要涉及的内容: 解释一下JMeter的用途 JMeter的实现方式以及采用的技术 安 ...

  2. gvim 配置Pydiction

    配置windows下gvim 的python关键字补全插件Pydiction 1.下载Pydiction 2.解压缩包,里面有after文件夹.complete-dict.pydiction.py 3 ...

  3. 20145225唐振远 实验二 "Java面向对象程序设计"

    20145225<Java程序设计> 实验二 Java面向对象程序设计 实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S ...

  4. List的使用

    List<string> AllFilesPath = new List<string>(); ) // get all files path { ; i < files ...

  5. Android自动化测试之MonkeyRunner

    1.Monkeyrunner简介 Monkeyrunner是Android系统自带的四大自动化测试工具之一,其他三个是Monkey.CTS.Benchmark:Monkeyrunner需要通过Andr ...

  6. 数据库知识整理<四>

    使用DML语句更改数据: 所谓DML语句是指数据库操作语句,其中包括的是对数据库数据的修改.删除.插入. 4.1添加新数据: 插入单行的记录:基本的SQL语句为-insert into <表明& ...

  7. 根据Url 获取图片尺寸 iOS

    // 根据图片url获取图片尺寸 +(CGSize)getImageSizeWithURL:(id)imageURL {     NSURL* URL = nil;     if([imageURL ...

  8. 利用Formdata实现form提交文件上传不跳转页面

    作者:幻月九十链接:https://www.zhihu.com/question/19631256/answer/119911045来源:知乎著作权归作者所有,转载请联系作者获得授权. $('form ...

  9. 15 个很棒的 Bootstrap UI 界面编辑器

    Bootstrap Magic BootSwatchr Bootstrap Live Editor Fancy Boot Style Bootstrap Lavish Bootstrap ThemeR ...

  10. ArcServer 10.0 “No Content”、“Server Host Cannot be null” 错误

    问题一:"No Content" 问题描述: 在输入服务地址时:http://192.168.1.180/arcgis/services   结果出现下面的错误:   解决办法: ...