FST是lucene中用来存储字典,并进行检索的核心数据结构,我记得在lucene3.0版本前都用的是跳跃链表(不清楚的同学以后我会专门写一篇磁盘索引技术来说清楚,网上也有相关资料),实际上采用该数据结构的最大的好处是比跳跃链表更加节省存储空间,它的速度与跳跃链表相比如何具体没测过(但从理论上分析,fst本质上是个图结构,当查询一个词典时需要对图中节点进行解析,其中有大量的位运算并按路径进行搜索比较耗费cpu,而跳跃链表没那么多位运算但是需要遍历小区间的每个词典进行匹配,所以各有优势),但可以肯定的是它比hashmap慢。其实跳跃链表索引技术在当前最新的lucene版本7.6中的DocValues特性中仍然使用具体请参考谈谈lucene的DocValues特性之SortedDocValuesField,不知是否可以认为跳跃链表会比FST速度更快?

FST本质上是一个比HashMap有更强大功能keyvalue存储结构,它的概念视图可以访问“http://examples.mikemccandless.com/fst.py?terms=pop%2Fabe%0D%0Astar%2Fabc%0D%0Asto%2Facs%0D%0Astop%2Fabd%0D%0Asuop%2Fcvb%0D%0A%0D%0A%0D%0A&cmd=Build+it%21”该网站看到。注意:在生成FST时,词典必须按key进行排序(原因在于排序后能生成一个最小化的DFA),它是共享前缀和后缀的,实际上前缀树就是它的一个简化版。

以下对照FST截图说明节点与边的关键属性:

谈谈lucene中的FST-LMLPHP

说明:红线代表该节点的最后一个路径,加粗的线代表该路径指向结束节点,value会通过提取公共前缀然后存储在线上(有种特殊情况会将值存储在节点上如sto/acs与stop/abd,但是lucene在序列化的时候会将值存储在线上的nextFinalOut属性中),从开始节点到结束节点的每一个路径代表每个key/value对,在查找一个key时,通过对路径节点上的边进行二分查找就可以快速进行定位。当然当一个节点的边太多的时候lucene还会进一步优化,是否共享后缀也是可配置的,当不共享后缀并且没有value时就是一个典型的前缀树。大家应该了解到双数组trie是对前缀树的一种优化,实际上FST又何尝不是呢!

理解FST并不难,难点在于一个高效的实现,以下是对lucene源码实现的关键性分析,具体细节还请看源码:

1、对于节点node而言分为序列化的node与未序列化的node,未序列化的node是可以不断复用的(当将节点序列化后,node会被重置),每次插入一个key时,首先查找与上一个key的公共前缀,然后将上个key的第lastkey.length个节点到第prefixLen+1个节点开始序列化(由于经过排序后这些节点以后也不会发生变化因此可以序列化);

2、当序列化完成后会对value值通过查找公共前缀进行进一步压缩处理,从第2个节点开始一直处理到第prefixLen个节点。实现时将当前值与上一个值的公共前缀设置给当前节点的父节点,将剩余的后缀推送到当前节点的每个边上(当遇到终止节点时,会将后缀保存在节点上一份),然后将当前值与公共前缀的后缀设置为当前值继续该循环;

3、根节点总是在最后进行序列化。

4、在对节点序列化时会将每条边依次写入一个类似于ByteBlockPool这样的逻辑字节流中(其意义可参考谈谈lucene中的BytesRefHash),首先会写入元数据(就是上面提到的边的属性),然后写入边上对应的lab,再写入value,最后写入指向节点的地址。注意:这里不考虑当一个节点的边太多的时候所做的优化,写完之后需要将字节序列进行反转,然后从后向前进行读取。

01-27 19:43