FST源码分析
我们以下面方法为入口查看FST的创建过程,这里in数组为写入的数据,out数组为对应的输出,通过builder协助构造FST
@Test
public void testFST() throws IOException {
String in[] = {
"cat", "deep", "do", "dog", "dogs"};
long out[] = {
5, 7, 17, 18, 21};
PositiveIntOutputs singleton = PositiveIntOutputs.getSingleton();
Builder<Long> longBuilder = new Builder<Long>(FST.INPUT_TYPE.BYTE1, singleton);
IntsRefBuilder intsRef = new IntsRefBuilder();
for (int i = 0; i < in.length; i++) {
String s = in[i];
BytesRef bytesRef = new BytesRef(s);
longBuilder.add(Util.toIntsRef(bytesRef, intsRef), out[i]);
}
FST<Long> fst = longBuilder.finish();
Long values = Util.get(fst, new BytesRef("cat"));
System.out.println(values);
}
由于FST的创建非常复杂,这里需要借助Builder来帮助创建,builder内容如下
public Builder(FST.INPUT_TYPE inputType, Outputs<T> outputs) {
this(inputType, 0, 0, true, true, Integer.MAX_VALUE, outputs, true, 15);
}
public Builder(FST.INPUT_TYPE inputType, int minSuffixCount1, int minSuffixCount2, boolean doShareSuffix,
boolean doShareNonSingletonNodes, int shareMaxTailLength, Outputs<T> outputs,
boolean allowArrayArcs, int bytesPageBits) {
//穿越某个节点的边的数量的下限,如果小于这个值,则要删除这个节点
this.minSuffixCount1 = minSuffixCount1;
//是0,在代码中不起作用
this.minSuffixCount2 = minSuffixCount2;
//当编译某一个节点的时候,如果这个节点是多个term都穿过的,是否要共享此节点,如果不共享,则直接编译入fst中,否则要放入一个去重的对象中,让其他的节点共享这个节点。
this.doShareNonSingletonNodes = doShareNonSingletonNodes;
//可以共享后缀个数
this.shareMaxTailLength = shareMaxTailLength;
//边是否启用定长存储,当某个节点发出的边非常多的时候可以使用fixedArray的格式,使用二分搜索加快查询速度,以空间换时间
this.allowArrayArcs = allowArrayArcs;
//封装的fst对象,bytesPageBits为BytesStore对象记录fst编译后的二进制内容时,使用的byte[]的大小
fst = new FST<>(inputType, outputs, bytesPageBits);
bytes = fst.bytes;
assert bytes != null;
//共享后缀的部分,用来查找共享后缀
if (doShareSuffix) {
dedupHash = new NodeHash<>(fst, bytes.getReverseReader(false));
} else {
dedupHash = null;
}
//arc无输出时候的默认输出
NO_OUTPUT = outputs.getNoOutput();
//frontier数组用于辅助构建fst,主要保存没有保存到fst的节点
@SuppressWarnings({
"rawtypes","unchecked"}) final UnCompiledNode<T>[] f =
(UnCompiledNode<T>[]) new UnCompiledNode[10];
frontier = f;
for(int idx=0;idx<frontier.length;idx++) {
frontier[idx] = new UnCompiledNode<>(this, idx);
}
}
我们先看下arc的属性
public static final class Arc<T> {
public int label;
public T output;
/** To node (ord or address) */
public long target;
byte flags;
public T nextFinalOutput;
// address (into the byte[]), or ord/address if label == END_LABEL
long nextArc;
/** Where the first arc in the array starts; only valid if
* bytesPerArc != 0 */
public long posArcsStart;
/** Non-zero if this arc is part of an array, which means all
* arcs for the node are encoded with a fixed number of bytes so
* that we can random access by index. We do when there are enough
* arcs leaving one node. It wastes some bytes but gives faster
* lookups. */
public int bytesPerArc;
/** Where we are in the array; only valid if bytesPerArc != 0. */
public int arcIdx;
/** How many arcs in the array; only valid if bytesPerArc != 0. */
public int numArcs;
- label:存放输入值的字符作为key,但是为ASCII的二进制形式
- output:存放对应的输出值value
- target:节点地址
- flags:标记位,每一位都记录了arc的属性信息,通过位运算获取信息
flags记录了arc的属性,主要有几下几种:
static final int BIT_FINAL_ARC = 1 << 0;
static final int BIT_LAST_ARC = 1 << 1;
static final int BIT_TARGET_NEXT = 1 << 2;
static final int BIT_STOP_NODE = 1 << 3;
public static final int BIT_ARC_HAS_OUTPUT = 1 << 4;
static final int BIT_ARC_HAS_FINAL_OUTPUT = 1 << 5;
- BIT_FINAL_ARC:arc对应字符是不是term最后一个字符
- BIT_LAST_ARC:arc是不是当前节点最后一条边
- BIT_TARGET_NEXT:arc连接的两个节点是临近节点,及需不需要记录跳转信息
- BIT_STOP_NODE:arc的目标节点是一个终止节点
- BIT_ARC_HAS_OUTPUT:arc是否有output输出值
- BIT_ARC_HAS_FINAL_OUTPUT:arc是否有final output输出值,当output不能满足输出时需要通过final output协助完成
这里我们以cat/5、deep/7、do/17、dog/18、dogs/21为例看下FST是如何生成的
第一步先写入cat/5,节点全部为UnCompiledNode保存在frontier数组中,并且每个节点都有一条边,通过arc保存了label值和边的属性

第二步插入deep/7,FST写入的数据必须是已经排序好的,上面的cat以c开头,当写入deep时候,说明以c开头的数据已经写入完毕,可以将cat字符串中的‘t’和’a’,进行冻结存入FST。FST的写入为倒叙,首先会写入flags,这里flags为15(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_STOP_NODE)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点。然后写入字符‘t’对应的二进制,最后对写入部分进行翻转。然后将字符‘a’写入FST,其flags为6(BIT_LAST_ARC+BIT_TARGET_NEXT)表明arc是当前节点最后一条边而且arc连接的两个节点是临近节点不需要记录target

第三步插入do/17,do和deep有公共字符d可以将字符p和字符e写入FST,这里字符p的flags和上面字符t的flags相同为15,表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,字符e的flags和上面的字符a的flags相同,表明arc是当前节点最后一条边而且arc连接的两个节点是临近节点

第四步插入dog/18和dogs/21,和上面的do公共前缀为do,这里无法知道以do开头的字符是否写入结束此时无数据写入FST

第五步所有数据写入完毕,调用finish方法将剩余数据写入FST,首先写入字符s,字符s的flags为31(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_STOP_NODE+BIT_ARC_HAS_OUTPUT)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,并且arc记录了output并且值为3

将字符g写入FST,flags为23(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_ARC_HAS_OUTPUT)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,并且arc记录了output并且值为1

然后将字符o和字符e写入FST,字符o的flags为23和上面一样,而字符e的flags为0说明上面所有属性都是没有的,字符e和前一个节点也不是临近节点所以这里记录了target值为8,8就是current数组的索引值,这里就是指向字符e的flags

最近将从root发出的所有边及字符d和c写入FST,这里d的flags为22(BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_ARC_HAS_OUTPUT)表明arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,并且arc记录了output并且值为7。字符c的flags为16(BIT_ARC_HAS_OUTPUT)表明arc记录了output并且值为5,记录了target值为4,指向了这里字符a的flags。下图current数组记录了FST的最终结果。

当我们需要读取FST时,比如读取cat时会先找到字符c对应的flags,该值为16说明arc记录了output值,不是指向最终节点,同时需要通过target找到下一个字符,这里target为4需要重置读取索引到4,读取字符a的flags为6,不是指向最终节点并且前一个节点为临近节点,则读取前一个字符t的flags值为15,说明字符t指向了最终节点结束,且字符和cat完全匹配,将查找过程经过的output值累加这里就一个output值及输出5。上面的图画的不够标准,这里主要为了方便理解
下面分析一下FST实现的源码,调用builder的add方法添加数据
public void add(IntsRef input, T output) throws IOException {
// De-dup NO_OUTPUT since it must be a singleton:
//无输出时候的默认值
if (output.equals(NO_OUTPUT)) {
output = NO_OUTPUT;
}
assert lastInput.length() == 0 || input.compareTo(lastInput.get()) >= 0: "inputs are added out of order lastInput=" + lastInput.get() + " vs input=" + input;
assert validOutput(output);
//System.out.println("\nadd: " + input);
//输入内容为空时候的处理,只允许作为第一个输入
if (input.length == 0) {
// empty input: only allowed as first input. we have
// to special case this because the packed FST
// format cannot represent the empty input since
// 'finalness' is stored on the incoming arc, not on
// the node
frontier[0].inputCount++;
frontier[0].isFinal = true;
fst.setEmptyOutput(output);
return;
}
// compare shared prefix length
//一、查询上次数据和本次数据共享前缀长度
int pos1 = 0;
int pos2 = input.offset;
//有个字符串结束就结束了
final int pos1Stop = Math.min(lastInput.length(), input.length);
while(true) {
//穿过节点的数量+1
frontier[pos1].inputCount++;
//System.out.println(" incr " + pos1 + " ct=" + frontier[pos1].inputCount + " n=" + frontier[pos1]);
//找到不相同的字符或已经到达其中一个字符串的尾部,也就是字符串遍历完了,就退出
if (pos1 >= pos1Stop || lastInput.intAt(pos1) != input.ints[pos2]) {
break;
}
pos1++;
pos2++;
}
//公共前缀的数量,由于第一个是root所以要加一
final int prefixLenPlus1 = pos1+1;
//二、frontier初始化时候的数组长度默认为10,如果字符串长度超过了,则需要对frontier进行扩容
if (frontier.length < input.length+1) {
final UnCompiledNode<T>[] next = ArrayUtil.grow(frontier, input.length+1);
for(int idx=frontier.length;idx<next.length;idx++) {
next[idx

最低0.47元/天 解锁文章
2238

被折叠的 条评论
为什么被折叠?



