FST, 全称Finite State Transducer, 中文翻译: 有限状态转换器或有限状态传感器。
FST最重要的功能是可以实现Key到Value的映射,相当于HashMap
但FST的查询速度比HashMap要慢。FST在Lucene中被大量使用,例如:倒排索引的存储,同义词词典的存储,搜索关键字建议等。
一个FST是一个6元组 (Q, I, O, S, E, f):
例如有下面一组映射关系:
cat -> 5
deep -> 10
do -> 15
dog -> 2
dogs -> 8
可以用下图中的FST来表示:
FST查询时,从初始状态开始,沿箭头走到终止状态,途中所有的边的输出之和再加上终止状态的Final Output即为最终的输出。例如:
Lucene中已经有FST的开源实现,在org.apache.lucene.util.fst包下,我使用的lucene版本为6.6.1。
lucene中的FST有以下特性:
整个byte[]数组是倒序的,查询时从后往前阅读。
每一个节点圆圈中的数字对应了该节点在byte[]中的起始位置。从某一个节点的起始位置开始,到下一个节点的起始位置结束,
为该节点的全部内容。 例如byte[]中(27,26,25,24,23,22,21)位置为节点27的全部内容。
byte[0] = 0 ,表示图中的-1节点,在查询时表示FST的结束。
没有存储任何节点相关的属性,所有的信息都以边(Arc)的属性给出。
对于每一个节点,在byte[]按输入符号顺序存储每一条边的信息。例如,对于节点27,
(27,26,25,24)存储了它的第一条边的信息:输入符号为’c’,输出符号为5,目标节点为4
(23,22,21)存储了它的第二条边的信息:输入符号为’d’,输出符号为2
对于每一条边,都以一个标识位flag开始,flag占一个字节,总共8bit,其中:
以节点27为例:
Lucene使用Builder类来构造一个FST,一个示例如下:
String inputValues[] = {"cat", "deep", "do", "dog", "dogs"};
long outputValues[] = {5, 10, 15, 2, 8};
PositiveIntOutputs outputs = PositiveIntOutputs.getSingleton();
Builder builder = new Builder(FST.INPUT_TYPE.BYTE1, outputs);
IntsRefBuilder scratchInts = new IntsRefBuilder();
for (int i = 0; i < inputValues.length; i++) {
BytesRef scratchBytes = new BytesRef(inputValues[i]);
builder.add(Util.toIntsRef(scratchBytes, scratchInts), outputValues[i]);
}
FST fst = builder.finish();
Lucene FST在构造完毕之后,所有数据都存储在一个byte[]之中(反向排列),没有任何多余的对象。
但是在构造过程中,需要Node(节点)和Arc(边)两类对象来辅助构造。
Lucene FST中的Node分为两种:
Builder类主要维护两部分数据:
下面是FST构造过程中,frontier和bytes的变化过程。在下面的图中:
此时frontier中为空
初始化bytes,并写入一个0,表示FST的结束。
Builder.add(IntsRef input, T output)方法主要有以下四个步骤:
do->15, dog->2
此时无论怎么调整边的值,都无法满足要求(必须在正数范围内)。因此引入一个新的概念Final output ,即下图中的黄色注释部分。
对于每一个终止状态,可以有一个Final output。从起始状态遍历到终止状态:
output1 + output2 + … + outputN + FinalOutput = 最终输出
至此, FST已经全部构造完成。 frontier可以全部丢弃,只留下bytes即可。
HashMap的结构如下图:
HashMap的Key,Value,Entry等都以对象的形式存储,因此对象的额外占用内存很多。但HashMap一般有比较快的查询速度。
FST压缩率一般在3倍~20倍之间,相对于TreeMap/HashMap的膨胀3倍,内存节省就有9倍到60倍(摘自https://blog.csdn.net/whinah/article/details/9980893)
使用132762个<英文单词, 随机正整数>键值对(txt文件,2313KB)来构造FST和HashMap,对比结果如下:
FST | HashMap | 比较 | |
---|---|---|---|
占用内存 | 1194KB | 19682KB | HashMap的内存使用量约为FST的16倍 |
1000W个随机Key查询时间 | 12500ms | 1300ms | FST的查询耗时大约为HashMap的10倍 |
BytesRef bytesRef = new BytesRef("dog");
IntsRef intsRef = Util.toIntsRef(bytesRef, new IntsRefBuilder());
Long result = Util.get(fst, intsRef);
Util.get()方法:
/** Looks up the output for this input, or null if the
* input is not accepted. */
public static T get(FST fst, IntsRef input) throws IOException {
// TODO: would be nice not to alloc this on every lookup
final Arc arc = fst.getFirstArc(new Arc());
final BytesReader fstReader = fst.getBytesReader();
// Accumulate output as we go
T output = fst.outputs.getNoOutput();
for(int i=0;i
FST fst = buildFST();
IntsRefFSTEnum fstEnum = new IntsRefFSTEnum<>(fst);
IntsRefFSTEnum.InputOutput inputOutput;
BytesRefBuilder scratch = new BytesRefBuilder();
while ((inputOutput = fstEnum.next()) != null) {
String input = Util.toBytesRef(inputOutput.input, scratch).utf8ToString();
Long output = inputOutput.output;
System.out.println(input + "\t" + output);
}
也就是在FST上应用Dijikstra求最短路径。
@Test
public void testShortestPath() throws IOException {
final String userInput = "do"; // 假如用户输入了"do"
BytesRef bytesRef = new BytesRef(userInput);
IntsRef input = Util.toIntsRef(bytesRef, new IntsRefBuilder());
FST fst = buildFST();
// 从开始位置走到"do"位置
FST.Arc arc = fst.getFirstArc(new FST.Arc<>());
FST.BytesReader fstReader = fst.getBytesReader();
for (int i = 0; i < input.length; i++) {
if (fst.findTargetArc(input.ints[input.offset + i], arc, arc, fstReader) == null) {
System.out.println("没找到。。。");
return;
}
}
// 从"do"位置开始找走到终止状态最近的2条路径
Util.TopResults results = Util.shortestPaths(fst, arc, PositiveIntOutputs.getSingleton().getNoOutput(), new Comparator() {
@Override
public int compare(Long o1, Long o2) {
return o1.compareTo(o2);
}
}, 2, false);
// 打印结果: dog dogs。 即用户输入"do",给用户建议"dog"和"dogs"
BytesRefBuilder bytesRefBuilder = new BytesRefBuilder();
for (Util.Result result : results) {
IntsRef intsRef = result.input;
System.out.println(userInput + Util.toBytesRef(intsRef, bytesRefBuilder).utf8ToString());
}
}
FST定义:https://en.wikipedia.org/wiki/Finite-state_transducer
FST图示:http://examples.mikemccandless.com/fst.py
https://www.cnblogs.com/forfuture1978/p/3945755.html
http://blog.sina.com.cn/s/blog_4bec92980101hvdd.html
https://www.cnblogs.com/bonelee/p/6226185.html
https://blog.csdn.net/whinah/article/details/9980893