`
suichangkele
  • 浏览: 201410 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

FST源代码解读5——FST的压缩

阅读更多

lucene fst 源代码 解读

 

 FST中最麻烦的其实是压缩,压缩的代码很难懂,必要性也不是很大(比如再词典表的存储的时候就没有使用压缩),所以我就仅把代码注释写上了,我估计有可能读的人不会很多,所以就不过多解释了.压缩的本质是对那些含有指向自己的arc的数量很多node编号做精简,有两种方式,或者是使用一个单独的对象记录这种节点的编号到位置的映射,在记录一个arc的指向的target的位置的时候记录编号,然后再用编号查找位置;或者是使用target的开始位置和指向这个节点的arc的开始位置差值(因为target一定再arc的后面,插值一定会更小)。在压缩之前生成的BytesStore中,每个arc会记录自己指向的target,可能用编号(在最终做压缩的fst中是编号,不压缩的时候是用具体值),对于那些重复次数很多的,这种方式可以起到压缩空间的作用。

/**
 * Expert: creates an FST by packing this one. This process requires substantial additional RAM (currently up to ~8 bytes per node depending
 * on <code>acceptableOverheadRatio</code>), but then should produce a smaller FST.
 * <p>
 * The implementation of this method uses ideas from <a target="_blank" href= "http://www.cs.put.poznan.pl/dweiss/site/publications/download/fsacomp.pdf">
 * Smaller Representation of Finite State Automata</a>, which describes techniques to reduce the size of a FST. However, this is not a strict implementation of the algorithms described in this paper.
 */
FST<T> pack(Builder<T> builder, int minInCountDeref, int maxDerefNodes, float acceptableOverheadRatio) throws IOException {

	// NOTE: maxDerefNodes is intentionally int: we cannot
	// support > 2.1B deref nodes

	// TODO: other things to try
	// - renumber the nodes to get more next / better locality?
	// - allow multiple input labels on an arc, so
	// singular chain of inputs can take one arc (on
	// wikipedia terms this could save another ~6%)
	// - in the ord case, the output '1' is presumably
	// very common (after NO_OUTPUT)... maybe use a bit
	// for it..?
	// - use spare bits in flags.... for top few labels /
	// outputs / targets

	if (nodeAddress == null) {
		throw new IllegalArgumentException("this FST was not built with willPackFST=true");
	}

	T NO_OUTPUT = outputs.getNoOutput();

	Arc<T> arc = new Arc<>();

	final BytesReader r = getBytesReader();//倒着读的
	// 一共收集多少个
	final int topN = Math.min(maxDerefNodes, inCounts.size());

	// Find top nodes with highest number of incoming arcs:
	NodeQueue q = new NodeQueue(topN);

	// TODO: we could use more RAM efficient selection algo here...
	NodeAndInCount bottom = null;//未填充满堆之前,都是null
	// 将所有的节点都写入到堆中,保留最大的topN个
	for (int node = 0; node < inCounts.size(); node++) {// node==0的不知道干啥的,忽略,因为等于0的不会进入下面的if判断。
		if (inCounts.get(node) >= minInCountDeref) {
			if (bottom == null) {
				q.add(new NodeAndInCount(node, (int) inCounts.get(node)));
				if (q.size() == topN) {
					bottom = q.top();
				}
			} else if (inCounts.get(node) > bottom.count) {//插入,挤掉最小的
				q.insertWithOverflow(new NodeAndInCount(node, (int) inCounts.get(node)));
			}
		}
	}

	// Free up RAM:
	inCounts = null;
	
	final Map<Integer, Integer> topNodeMap = new HashMap<>();//指向的arc数量最多的n个中排名第几,从0开始,key是节点,value是排名
	for (int downTo = q.size() - 1; downTo >= 0; downTo--) {
		NodeAndInCount n = q.pop();
		topNodeMap.put(n.node, downTo);
	}

	// +1 because node ords start at 1 (0 is reserved as stop node):
	// 按照正向存储,已经生成的最新的fst中每个节点的开始位置(nodeAddress记录的是反向存储时的结束位置),加一是GrowableWriter是从0开始的,但是在fst中node=0是给最后一个节点准备的(参见addNode方法nodeIn.numArcs == 0的情况),
	// 其他的不是最后一个节点的编号是从1开始的,所以记录这1-n个节点最大的是n,所以需要n+1个
	// 但是下面会更新这个newNodeAddress的值,在更新后就可以将其理解为每个节点在新的fst中的开始的值了。
	final GrowableWriter newNodeAddress = new GrowableWriter(PackedInts.bitsRequired(builder.bytes.getPosition()), (int) (1 + builder.nodeCount), acceptableOverheadRatio);
	// Fill initial coarse guess:   注意这里写的仅仅是大概的值,因为下面是从root开始写入的,在写一个前面的节点时,需要记录其arc的target的节点,而此时距离他的target的路径上可能还有好多节点
	// 这些节点还没有处理过他们将来会变,所以写入的仅仅是大概的值
	for (int node = 1; node <= builder.nodeCount; node++) {
		// builder.bytes.getPosition() - nodeAddress.get(node)表示全部的结束位置减去这个节点的结束位置,也就是反向记录的话这个节点的开始位置;因为bytes的开始会写入一个0的byte,所以开始位置需要再加1。
		newNodeAddress.set(node, 1 + builder.bytes.getPosition() - nodeAddress.get(node));//newNodeAddress从1开始,供n个,但是GrowableWriter从0开始的,所以上面是n+1个
	}
	// 还是对上面的newNodeAddress的解释:里面存储的有的是开始位置,是预估的位置,下面还会有解释。
	
	FST<T> fst;

	// Iterate until we converge:
	while (true) {
		boolean changed = false;//这次压缩中有没有有没有发生新的变化,如果发生了变化,说明要继续压缩,直到不再发生变化。
		// for assert:
		boolean negDelta = false;
		// 新的fst
		fst = new FST<>(inputType, outputs, builder.bytes.getBlockBits());
		final BytesStore writer = fst.bytes;
		// Skip 0 byte since 0 is reserved target: 先写入0,所以上面的newNodeAddress中会加一个1.
		writer.writeByte((byte) 0);
		// 当前节点向后移动的距离(负数表示向前移动了)只要这个不是0,说明从这个节点后面的所有的节点的位置都发生了变化,需要继续进行重复压缩,也就是上面的while true需要继续进行,因为之后的节点的位置都发送了变化,前面的节点的target也要变化。
		long addressError = 0;//对于已经处理过的节点,新的值一定不会比之前的值大,因为是压缩过的,所以这个会是负的(或者等于0)

		// Since we re-reverse the bytes, we now write the nodes backwards, so that BIT_TARGET_NEXT is unchanged:
		for (int node = (int) builder.nodeCount; node >= 1; node--) {//遍历所有的node,从最后一个node开始遍历,也就是从root开始(之前是倒叙的,root是最后写入的)
			
			final long address = writer.getPosition();//这个节点在新的fst中的开始位置
			if (address != newNodeAddress.get(node)) {//如果新的开始位置和之前的开始位置不一样
				addressError = address - newNodeAddress.get(node);//从这里看,不会大于0
				changed = true;//当前的节点的开始位置发生了变化,更新这个节点的最新的位置,后续的所有的节点都不准确了,都会进入这个if
				newNodeAddress.set(node, address);//更新当前节点的最新的位置
			}
			int bytesPerArc = 0;//最新的fst中这个节点每个arc使用的内存的最大值,在第一次压缩的时候,就是最早生成的fst中的最大值,第二次(如果存在)压缩的时候就是第一次压缩过程中确定的最大值
			boolean retry = false;//retry表示在此次的fst的压缩过程中,该节点是否是第二次尝试,第一次压缩的时候是false,最多只有两次压缩。
			// for assert:
			boolean anyNegDelta = false;
			
			// Retry loop: possibly iterate more than once, if this is an array'd node and bytesPerArc changes:
//				writeNode: 
			while (true) {//处理这个node,采用while true是因为可能存在第二次压缩,因为第一次压缩的时候不确定bytesPerArc到底是多大。

				readFirstRealTargetArc(node, arc, r);//读取这个节点的第一个arc,读取到arc里面

				final boolean useArcArray = arc.bytesPerArc != 0;//之前有没有用fixedBytes格式
				// 写入这个节点的一些元数据信息
				if (useArcArray) {//之前采用了fixedBytes格式,此时需要先写入这个node的header,里面包括了使用fixedArc的标识,每个arc使用内存的大小还有arc的数量
					// Write false first arc:
					if (bytesPerArc == 0) {
						bytesPerArc = arc.bytesPerArc;
					}
					writer.writeByte(ARCS_AS_FIXED_ARRAY);
					writer.writeVInt(arc.numArcs);
					writer.writeVInt(bytesPerArc);
				}

				int maxBytesPerArc = 0;//压缩之后这个节点在所有的arc中占用内存最大的一个
				while (true) { // iterate over all arcs for this node   写这个节点的多个arc

					final long arcStartPos = writer.getPosition();//arc在bytesStore中的开始

					byte flags = 0;

					if (arc.isLast()) {
						flags += BIT_LAST_ARC;
					}
					if (!useArcArray && node != 1 && arc.target == node - 1) {// 进入fst的node是从1开始的,如果等于1说明他的target节点不在fst中,arc.target == node - 1说明他在fst中的下一个节点就是target节点(倒着读的)
						flags += BIT_TARGET_NEXT;
						if (!retry) {
//								nextCount++;
						}
					}
					if (arc.isFinal()) {//这个arc构成一个term(路径)
						flags += BIT_FINAL_ARC;
						if (arc.nextFinalOutput != NO_OUTPUT) {
							flags += BIT_ARC_HAS_FINAL_OUTPUT;
						}
					} else {
						assert arc.nextFinalOutput == NO_OUTPUT;
					}
					if (!targetHasArcs(arc)) {//指向最后一个节点
						flags += BIT_STOP_NODE;
					}

					if (arc.output != NO_OUTPUT) {//是否有输出
						flags += BIT_ARC_HAS_OUTPUT;
					}

					final long absPtr;//记录target的位置的绝对值
					// 是否需要记录target
					final boolean doWriteTarget = targetHasArcs(arc) && (flags & BIT_TARGET_NEXT) == 0;//是否需要记录指向的target,如果target没有arc,则是最后一个,不需要记录,如果满足BIT_TARGET_NEXT,也不需要记录
					if (doWriteTarget) {//记录target
						final Integer ptr = topNodeMap.get(arc.target);// 新的编号
						if (ptr != null) {//有编号的话用编号
							absPtr = ptr;//这个值一定小于 topNodeMap.size(),这样再取出的时候,通过与 topNodeMap.size()判断大小,就可以知道是哪种情况了
						} else {//没有编号,计算一个,记录的还是绝对值,+topNodeMap.size()仅仅是为了和上面的ptr != null的情况有所区别 
							absPtr = topNodeMap.size() + newNodeAddress.get((int) arc.target) + addressError;//后面的 newNodeAddress.get((int) arc.target) + addressError表示target节点的预估距离,是不准确的
						}
						
						//计算差值: target节点顺序写的话在原来fst中的开始位置  + 移动的距离 - 这个arc在新的fst中的开始位置 ,-2这个可以理解为一个阈值,只有当delta - absPtr < 2的时候才使用差值
						long delta = newNodeAddress.get((int)arc.target) +  addressError - writer.getPosition() - 2;
						if (delta < 0) {
							anyNegDelta = true;
							delta = 0;
						}

						if (delta < absPtr) {//如果记录差值更小,则记录差值
							flags |= BIT_TARGET_DELTA;//此边的目标节点以差值编码方式存储 
						}
					} else {
						absPtr = 0;
					}

					assert flags != ARCS_AS_FIXED_ARRAY;
					writer.writeByte(flags);//写入flag

					fst.writeLabel(writer, arc.label);//写入label

					if (arc.output != NO_OUTPUT) {//写入输出
						outputs.write(arc.output, writer);
					}
					if (arc.nextFinalOutput != NO_OUTPUT) {//写入fianl输出
						outputs.writeFinalOutput(arc.nextFinalOutput, writer);
					}

					if (doWriteTarget) {
						// 差值,计算方式是:目标节点的预估位置(不准确的)减去现在位置,解释: newNodeAddress.get((int) arc.target) + addressError 表示target节点在新的fst中的预估位置(不经过压缩的真实位置+现在已经节省的空间),
						// writer.getPosition()表示这个arc的开始位置;
						long delta = newNodeAddress.get((int) arc.target)/* 这个值是不准确的,因为路径上后面的节点还没有处理,可能会发生变化 */ + addressError/* 这个极可能是负数 */ - writer.getPosition();
						if (delta < 0) {//TODO 这个不可能小于0 啊!
							anyNegDelta = true;
							delta = 0;
						}

						if (flag(flags, BIT_TARGET_DELTA)) {//如果记录差值的,
							writer.writeVLong(delta);
						} else {
							writer.writeVLong(absPtr);//不是记录差值的,还是用绝对的
						}
					}

					if (useArcArray) {// 扩容用的
						final int arcBytes = (int) (writer.getPosition() - arcStartPos);//这个arc占用了多少内存
						maxBytesPerArc = Math.max(maxBytesPerArc, arcBytes);//更新这个节点的所有的arc使用的最大内存(从这个也看的出来,第二次压缩不会更新maxBytesPerArc)
						// NOTE: this may in fact go "backwards", if somehow (rarely, possibly never) we use
						// more bytesPerArc in this rewrite than the incoming FST did... but in this case we
						// will retry (below) so it's OK to ovewrite bytes: wasted += bytesPerArc - arcBytes;
						writer.skipBytes((int) (arcStartPos + bytesPerArc - writer.getPosition()));//扩容,每个arc都占用一个bytesPerArc的大小
					}
					if (arc.isLast()) {//这个arc是最后一个,不读取了,否则继续读下一个
						break;
					}
					readNextRealArc(arc, r);//继续读取下一个
				}// 写这个节点的多个arc 循环结束

				// 如果不是使用fixedArray,则已经是紧凑的了,不会有多余的浪费,退出;如果是fixedArray的,因为之前写入的header中是原来的bytesPerArc,压缩后可能会发生变化,所以这一次写错了,需要重新写,直到maxBytesPerArc == bytesPerArc
				if (useArcArray) {
					if (maxBytesPerArc == bytesPerArc/* 经过压缩之后没有发生变化,退出 */ || (retry && maxBytesPerArc <= bytesPerArc) /*retry=true表示不是第一次压缩了,第一次的时候是false,后面的一定是true,第二次压缩不会改变maxBytesPerArc */ ) {
						break;
					}
				} else {
					break;
				}

				// Retry: 
				bytesPerArc = maxBytesPerArc;//本次的最大值(也可以叫做第一次压缩的最大值,因为后面的几次不会修改这个)
				writer.truncate(address);//废弃掉本次的输入
				retry = true;//表示不是第一次压缩这个节点了
				anyNegDelta = false;
			}// 处理这个node结束

			negDelta |= anyNegDelta;
		}//遍历所有的node

		if (!changed) {//只要有一个arc变化了,则必须要从头全部重来一次。
			// We don't renumber the nodes (just reverse their order) so nodes should only point forward to
			// other nodes because we only produce acyclic FSTs w/ nodes only pointing "forwards":
			assert !negDelta;
			break;
		}
	}

	long maxAddress = 0;
	for (long key : topNodeMap.keySet()) {//key是节点,这个循环的目的是查找最大的值,供下面的packedInts使用
		maxAddress = Math.max(maxAddress, newNodeAddress.get((int) key));
	}

	// 记录上面使用编号的节点的开始位置,因为之前仅仅是记录的起编号,没有记录其开始位置,要获得开始位置,需要使用这个
	PackedInts.Mutable nodeRefToAddressIn = PackedInts.getMutable(topNodeMap.size(), PackedInts.bitsRequired(maxAddress), acceptableOverheadRatio);
	for (Map.Entry<Integer, Integer> ent : topNodeMap.entrySet()) {
		nodeRefToAddressIn.set(ent.getValue(), newNodeAddress.get(ent.getKey()));
	}
	fst.nodeRefToAddress = nodeRefToAddressIn;

	fst.startNode = newNodeAddress.get((int) startNode);

	if (emptyOutput != null) {
		fst.setEmptyOutput(emptyOutput);
	}

	fst.bytes.finish();
	fst.cacheRootArcs();

	return fst;
}

 

这段代码很晦涩,需要好好理解,而且我觉得需要一点点数学证明证明一点点的压缩是可以压缩完的,我没有完成这个证明。如果有读者在看这个代码时发现我写的不对,可以和我联系,我的qq是:1308567317

 

分享到:
评论

相关推荐

    FST:快速Java序列化的替代品

    4. **优化配置**:根据具体需求,可以通过配置项调整FST的行为,例如禁用默认的序列化策略、启用压缩等。 5. **测试与性能监控**:在使用FST后,应进行性能测试,确保其满足应用程序的需求,并在生产环境中监控序列...

    fst-2.57-API文档-中英对照版.zip

    赠送源代码:fst-2.57-sources.jar; 赠送Maven依赖信息文件:fst-2.57.pom; 包含翻译后的API文档:fst-2.57-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:de.ruedigermoeller:fst:2.57; 标签:...

    Lucene中的FST算法描述

    5. Frontier:这个数组用来存放尚未转换到FSTbytes数组中的数据信息。在FST的构建过程中,Frontier用于存放待转换的节点信息。 在FST构建的过程中,会使用到Hash算法来计算节点的hash值,并用这个值来判断该节点...

    关于Lucene的词典FST深入剖析-申艳超1

    《关于Lucene的词典FST深入剖析》这篇文章是由申艳超撰写的,主要探讨了Apache Lucene这个全文搜索引擎库中的一个关键数据结构——有限状态转换器(Finite State Transducer,简称FST)。FST在Lucene中被用于构建和...

    fst-2.57-API文档-中文版.zip

    赠送源代码:fst-2.57-sources.jar; 赠送Maven依赖信息文件:fst-2.57.pom; 包含翻译后的API文档:fst-2.57-javadoc-API文档-中文(简体)版.zip; Maven坐标:de.ruedigermoeller:fst:2.57; 标签:ruedigermoeller...

    FST610变频器说明书.pdf

    FST610变频器是深圳市佛斯特科技有限公司生产的一款高性能矢量通用型变频器,它支持多种控制方式,如VF控制、无PG矢量控制、转矩控制,拥有丰富的参数功能,适用于对速度、转矩响应速度、低频力矩要求较高的场合。...

    fst2.4及其依赖包

    《fst2.4及其依赖包详解》 在Java开发中,高效的序列化和反序列化是关键环节之一,尤其在大数据处理、网络通信以及持久化存储等场景中。FST(Fast Serialization)库是一个高性能的Java对象序列化库,旨在替代标准...

    Go-FST有限状态变换器的一个Go库实现

    源代码通常包括示例、测试用例和文档,这些都对理解库的用法和实现原理非常有帮助。 总的来说,Go库"vellum"为开发人员提供了一种强大而灵活的工具,用于在Go环境中处理有限状态变换器。通过深入研究这个库,我们...

    佛斯特FST-500系列变频器说明书.rar

    佛斯特FST-500系列变频器是一款广泛应用在工业自动化领域的设备,它通过调节电机的供电频率来实现电机速度的控制,从而达到节能、调速的目的。本说明书主要涵盖了该系列变频器的基本原理、功能特性、安装调试、操作...

    SITRANS FST030使用手册(英文版).pdf

    SITRANS FST030是一款由Siemens推出的超声波流量计,适用于各种工业环境中的流量测量。这款设备采用先进的Modbus/HART通信协议,使得数据交换和远程操作更为便捷。本手册是该设备的英文使用指南,旨在帮助用户正确...

    fst-1.60.zip

    jaque.zip,Java查询库使语言级代码表达式在运行时以表达式树的形式表示为对象,

    fst 配置文件读取

    在IT领域,配置文件的管理和读取是系统初始化和运行中的关键步骤,特别是在软件开发、运维自动化...通过阅读README.TXT,你可以获得关于如何使用和解读“fst”配置文件的具体步骤和示例,从而更好地应用到实际工作中。

    帧差法改进matlab源代码

    ### 帧差法改进Matlab源代码解析与详解 #### 一、帧差法基本原理及应用背景 帧差法是一种广泛应用于视频处理领域的技术,主要用于运动目标检测。其核心思想是通过比较相邻帧之间的差异来判断物体的移动情况。这种...

    lucene 源码 fst.rar 分析

    FST通过压缩数据结构,减少了内存占用,同时提高了查询速度。在Lucene中,FST主要应用于词汇表的构建,尤其是对于大型词汇表,如倒排索引的术语字典,它能显著提升性能。 一、FST的工作原理 1. **图结构**:FST的...

    FST_for_Mobile_LE_1209.pdf

    风河自动化软件测试框架(以下简称FST)是专为移动设备设计的一款自动化测试工具,旨在帮助制造商确保基于Android或Moblin操作系统的移动设备符合规定标准、性能稳定可靠。此框架能够自动接收构建结果、执行成百上千...

    FST650变频器说明书.pdf

    FST650变频器说明书,包括安装,配线,接线端子图,操作说明,详细功能讲解,故障检查与排除,保养和维护,通信协议,以及功能参数简表等技术资料。

    论文研究-一种新的快速报文分类算法——RC-FST*.pdf

    RC-FST 算法利用IP 地址高8 比特前缀建立Hash 压缩索引表, 将分类规则集分成多个子集, 并针对每个子集建立快速搜索树, 而这些规模相对小的本地搜索树更利于实现快速建立、查找和优化。为提高搜索树性能, 在规则分割...

    AES.zip_AES_AES源代码_aes c语言_c++ aes加密解密_c语言 源代码

    提供的源代码可以作为学习AES加密算法的宝贵资源,也可以直接应用于实际项目中,提供加密和解密的功能。为了确保代码安全,开发者应遵循良好的编程实践,如输入验证、错误处理和安全的内存管理,同时要注意避免常见...

    FST文件生产工具

    可根据seed值及dat数据,计算fst文件,可用于软件加密狗的复制等。

    74FST3253 高速4选1数据手册

    根据提供的文件内容,这里是一份关于74FST3253高速4选1数据手册的知识点整理: 1. 器件介绍: 74FST3253是由ON Semiconductor生产的高速4选1多路选择器/解复用器总线开关。这款器件在4到5.5伏的电压范围内与CMOS ...

Global site tag (gtag.js) - Google Analytics