lucene中的docValue实现源码解读(二)——NumericDocValue的写入

各种类型的docValue的写入是在添加索引的时候,在org.apache.lucene.index.DefaultIndexingChain.indexDocValue(PerField, DocValuesType, IndexableField)方法里面,会有五种类型的docValue,会分别调用不同的DocValueWriter来实现,这篇介绍数字类型的docValue,他是一个很简单、很高效率的docValue的存储。

case NUMERIC://如果是数字类型的docValue
if (fp.docValuesWriter == null) {
	fp.docValuesWriter = new NumericDocValuesWriter(fp.fieldInfo, bytesUsed, true);
}
((NumericDocValuesWriter) fp.docValuesWriter).addValue(docID, field.numericValue().longValue());//添加指定docId的值,值是long类型的
break;

使用的docValueWriter是NumericDocValueWriter,我们看一下这个方法的源码。

构造方法
public NumericDocValuesWriter(FieldInfo fieldInfo, Counter iwBytesUsed, boolean trackDocsWithField) {
	pending = new AppendingDeltaPackedLongBuffer(PackedInts.COMPACT);//这个是用于存储具体的docVAlue的,也就是一些数字的,从他的名字就能看出来,他是根据差值进行存储的,然后再添加了些数字后就会启动压缩,实现更高效的内存使用。
	docsWithField = trackDocsWithField ? new FixedBitSet(64) : null;//这个是记录那些在该域中含有docValue的docid的。使用的类型是bitset类型。
	bytesUsed = pending.ramBytesUsed() + docsWithFieldBytesUsed();//下面这几行都是记录使用的内存的,因为lucnee自己要根据使用的内存进行flush,所以要记录内存。
	this.fieldInfo = fieldInfo;
	this.iwBytesUsed = iwBytesUsed;
	iwBytesUsed.addAndGet(bytesUsed);
}
/** 添加一个doc的值 ,这里有真正的添加docValue的逻辑 */
public void addValue(int docID, long value) {
	if (docID < pending.size()) {
		throw new IllegalArgumentException("DocValuesField \"" + fieldInfo.name
				+ "\" appears more than once in this document (only one value is allowed per field)");
	}
	// Fill in any holes: 填窟窿,因为下面要对所有的额值做迭代,加入这个就是为了使迭代器能正常工作
	for (int i = (int) pending.size(); i < docID; ++i) {
		pending.add(MISSING);
	}
	
	pending.add(value);//保存这个值,到pending中
	if (docsWithField != null) {//docWithFeidl是记录含有值得docid的,这里不是null
		docsWithField = FixedBitSet.ensureCapacity(docsWithField, docID);//使bitSet能存放docID。
		docsWithField.set(docID);//记录这个doc含有值。
	}
	updateBytesUsed();//更新使用的内存
}

 上面的方法是添加到内存里面,他有两个重要的地方,一个是保存具体的值,保存的格式是long类型的,尽管我们在使用lucene的时候可能会使用int、float、但是会统一的转化为long类型的。第二个是记录了含有值得docid,记录在一个bitset里面。再内存中的我们介绍完了,再看一下在flush到硬盘的时候的方法——flush吧:

public void flush(SegmentWriteState state, DocValuesConsumer dvConsumer) throws IOException {
	final int maxDoc = state.segmentInfo.getDocCount();//这个段中所有的doc的数量
	dvConsumer.addNumericField(fieldInfo, new Iterable<Number>() {//使用给定的docValueConsumer来处理,处理的参数是一个迭代器
		@Override
		public Iterator<Number> iterator() {
			return new NumericIterator(maxDoc);
		}
	});
}

使用的迭代器是一个内部类,看下代码:

private class NumericIterator implements Iterator<Number> {
	final AppendingDeltaPackedLongBuffer.Iterator iter = pending.iterator();//获得存储所有的docValue的对象的迭代器,使用这个对象的目的很简单,就是对数字的存储进行压缩,提高对内存的使用率。
	final int size = (int) pending.size();//所有的doc的数量
	final int maxDoc;//最大的id
	int upto;//当前处理的doc的id
	NumericIterator(int maxDoc) {
		this.maxDoc = maxDoc;
	}
	@Override
	public boolean hasNext() {//判断还有没有数字要存储
		return upto < maxDoc;
	}
	@Override
	public Number next() {//在DocValueConsumer中调用的就是这个方法,获得下一个要存储的值
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		Long value;
		if (upto < size) {
			long v = iter.next();//从pending中获得下一个doc,这个值不会是null,因为即使没有值也会补一个特殊值的,也就是上面的填窟窿的地方
			if (docsWithField == null || docsWithField.get(upto)) {//如果这个doc有值,则返回的就是真实的值
				value = v;
			} else {
				value = null;//如果没有值(尽管填0了,但是他不在docsWithField中,也就是说填窟窿只是为了这里的迭代方法),返回的就是null。
			}
		} else {
			value = docsWithField != null ? null : MISSING;
		}
		upto++;
		return value;
	}

	@Override
	public void remove() {
		throw new UnsupportedOperationException();
	}
}

  上面的这两段代码都不难,就是将存放在内存中准备写入到硬盘上,到底怎么写的额,还得看docValueConsumer.addNumericField方法。

我看的是这个实现类

		CodecUtil.writeHeader(data, dataCodec, Lucene49DocValuesFormat.VERSION_CURRENT);//写入头文件到dvd
		String metaName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix,metaExtension);//dvm文件,
		
public Lucene410DocValuesConsumer(SegmentWriteState state, String dataCodec, String dataExtension, String metaCodec,
		String metaExtension) throws IOException {
	boolean success = false;
	try {
		String dataName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix,dataExtension);//dvd的名字,比如 _3_lucene410_0.dvd
		data = state.directory.createOutput(dataName, state.context);//这个是真正存储docValue的文件
		CodecUtil.writeHeader(data, dataCodec, Lucene410DocValuesFormat.VERSION_CURRENT);//在data文件中写入使用的lucene的版本号
		String metaName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix,metaExtension);//data文件的索引文件的名字,记住,data文件也是有索引的,为了更快速的找到某个域,因为docValue是按照列进行存储的。
		meta = state.directory.createOutput(metaName, state.context);
		CodecUtil.writeHeader(meta, metaCodec, Lucene410DocValuesFormat.VERSION_CURRENT);
		maxDoc = state.segmentInfo.getDocCount();
		success = true;
	} finally {
		if (!success) {
			IOUtils.closeWhileHandlingException(this);
		}
	}
}

 在lucene4.10中,docValue是有两个文件的,一个是具体的存储docValue的文件dvd,在一个段中,所有的域的docValue都是存放在这个文件中的,他有一个索引文件,也就是dvm,他是dvd文件的索引文件,他索引的东西很简单,包含某个域的开始位置在dvd文件中的偏移量(下文统一使用fp表示,fp即file pointer),其他的要看具体的存储格式。看一下具体的代码吧:

/** 添加某个域的所有的docValue,某个域用field表示,所有的value是一个迭代器。就是所有的docValue的值 */
@Override
public void addNumericField(FieldInfo field, Iterable<Number> values) throws IOException {
	addNumericField(field, values, true);
}

void addNumericField(FieldInfo field, Iterable<Number> values, boolean optimizeStorage) throws IOException {
	
	long count = 0;
	long minValue = Long.MAX_VALUE;
	long maxValue = Long.MIN_VALUE;
	long gcd = 0;//最大公约数,如果是1,则表示不用最大公约数存储。
	boolean missing = false;//有没有某个doc没有docValue,
	// TODO: more efficient?
	HashSet<Long> uniqueValues = null;//超过256后不使用,表示不重复的数字太多。
	if (optimizeStorage) {
		uniqueValues = new HashSet<>();
		for (Number nv : values) {
			final long v;//循环的值。
			if (nv == null) {
				v = 0;
				missing = true;//有的doc没有值
			} else {
				v = nv.longValue();
			}
			if (gcd != 1) {
				if (v < Long.MIN_VALUE / 2 || v > Long.MAX_VALUE / 2) {//这种情况下最大公约数没有意义,因为数字太大了,
					// in that case v - minValue might overflow and make the GCD computation return
					// wrong results. Since these extreme values are unlikely, we just discard GCD computation for them
					gcd = 1;
				} else if (count != 0) { // minValue needs to be set first
					gcd = MathUtil.gcd(gcd, v - minValue);
				}
			}
			minValue = Math.min(minValue, v);
			maxValue = Math.max(maxValue, v);
			if (uniqueValues != null) {
				if (uniqueValues.add(v)) {
					if (uniqueValues.size() > 256) {//如果超过256个,则不适用某个存储格式
						uniqueValues = null;
					}
				}
			}
			++count;
		}
	} else {//这个不使用
		for (Number nv : values) {
			long v = nv.longValue();
			minValue = Math.min(minValue, v);
			maxValue = Math.max(maxValue, v);
			++count;
		}
	}

	final long delta = maxValue - minValue;//差值,也就是最大的不重复的数
	//记录最大的差值需要使用的bit的个数,如果用差值规则记录的话,记录一个数字使用的bit的数量一定会小于这个值,也就是说,这个值就是用差值记录的时候记录某一个值使用的bit的数量的最大的值
	final int deltaBitsRequired = DirectWriter.unsignedBitsRequired(delta);
        //这个是使用table_compressed格式存储时,记录一个docValue所需要的bit的最大值。
	final int tableBitsRequired = uniqueValues == null ? Integer.MAX_VALUE : DirectWriter.bitsRequired(uniqueValues.size() - 1);

	final int format;
	if (uniqueValues != null && tableBitsRequired < deltaBitsRequired) {//当不重复的值得数量不是很多的时候
		format = TABLE_COMPRESSED;//使用表格压缩记录方式
	} else if (gcd != 0 && gcd != 1) {//如果除以最大公约数后,每个存储每个值使用的位数比deltaBitsRequired的位数小,则使用最大公约数记录的方法。只要能进入这个,都会使用最大公约数的压缩方式,因为gcd一定是大于1的。
		final long gcdDelta = (maxValue - minValue) / gcd;
		final long gcdBitsRequired = DirectWriter.unsignedBitsRequired(gcdDelta);
		format = gcdBitsRequired < deltaBitsRequired ? GCD_COMPRESSED : DELTA_COMPRESSED;//一定会更小,因为除以了gcd
	} else {
		format = DELTA_COMPRESSED;//否则使用差值规则记录
	}
	//下面的meta就是表示的索引文件dvm,data文件就是dvd
	meta.writeVInt(field.number);//在索引文件dvm中写入域号
	meta.writeByte(Lucene49DocValuesFormat.NUMERIC);//存储格式的名字
	meta.writeVInt(format);//具体的存储格式
	if (missing) {//如果有的doc没有值得,则在da记录
		meta.writeLong(data.getFilePointer());//在meta中记录data的fp,也就是在文件中的偏移量,能更快速的找到文件。
		writeMissingBitset(values);//记录哪些doc没有docValue。记录在data文件中记录那些含有值得docid。
	} else {
		meta.writeLong(-1L);
	}
	meta.writeLong(data.getFilePointer());//在meta中再次记录现在data的fp,因为可能在data中又记录了missingBitset,这样能快速的找到真正存储数字时的开始位置
	meta.writeVLong(count);
	
	switch (format) {//具体使用什么格式已经在meta中记录了,这样在读取的时候也会知道。
	case GCD_COMPRESSED://基于最大公约数
		meta.writeLong(minValue);//记录最小的值
		meta.writeLong(gcd);//记录最大公约数
		final long maxDelta = (maxValue - minValue) / gcd;
		final int bits = DirectWriter.unsignedBitsRequired(maxDelta);//记录一个值需要的bit的位数
		meta.writeVInt(bits);//这是为了解码用的,因为lucene在实际存储的时候还会压缩,不过可以忽略,不影响这里的理解
		final DirectWriter quotientWriter = DirectWriter.getInstance(data, count, bits);
		for (Number nv : values) {
			long value = nv == null ? 0 : nv.longValue();//对于那些没有值得doc,写入默认值0,虽然有的doc没有docValue,但是已经在data中记录了那些没有docValue的id了所以这个写0不要紧,仍然可以识别出来。写入的目的仅仅是为了更加快速的读取那些有值得doc的值。
			quotientWriter.add((value - minValue) / gcd);
		}
		quotientWriter.finish();
		break;
	case DELTA_COMPRESSED://基于差值的,这个可以看做最大公约数是1的GCD_COMPRESSED格式
		final long minDelta = delta < 0 ? 0 : minValue;
		meta.writeLong(minDelta);
		meta.writeVInt(deltaBitsRequired);
		final DirectWriter writer = DirectWriter.getInstance(data, count, deltaBitsRequired);
		for (Number nv : values) {
			long v = nv == null ? 0 : nv.longValue();//虽然有的doc没有docValue,但是这个写0不要紧,因为可以识别出来,已经在data中记录了那些没有docValue的id了。
			writer.add(v - minDelta);
		}
		writer.finish();
		break;
	case TABLE_COMPRESSED://这种情况会增大meta文件的大小,所以是对于数字比较少的情况下才使用
		final Long[] decode = uniqueValues.toArray(new Long[uniqueValues.size()]);
		Arrays.sort(decode);//对所有的数字从小到大进行排序
		final HashMap<Long, Integer> encode = new HashMap<>();
		meta.writeVInt(decode.length);
		for (int i = 0; i < decode.length; i++) {
			meta.writeLong(decode[i]);//把具体的值写入meta文件中,写入的都是long
			encode.put(decode[i], i);//记录某个值和其序号的对应关系,比如数字100的排序是第10,101的排序是第11,这样记录在一个hashmap中。
		}
		meta.writeVInt(tableBitsRequired);//这个是用于解码用的。
		final DirectWriter ordsWriter = DirectWriter.getInstance(data, count, tableBitsRequired);
		for (Number nv : values) {
			ordsWriter.add(encode.get(nv == null ? 0 : nv.longValue()));//在data文件中写入的是序号,也就是在meta中的值的排序后的序号,同样这里对于那些没有值得doc,仍然是写入了0.
		}
		ordsWriter.finish();
		break;
	default:
		throw new AssertionError();
	}
	meta.writeLong(data.getFilePointer());//写入结束位置,因为在读取数字类型的docValue的时候,会把一块slice读取到内存中,所以要知道开始位置和结束位置。
}

通过上面的代码,我们可以知道数字类型的docValue有三个格式,一个是基于最大公约数的,一个是基于差值的(可以视为最大公约数的特殊形式,公约数是1),一个是压缩表的。

对于最大公约数的,是讲最小值、最大公约数记录在meta文件中,然后再data文件中记录的是一个docvalue的值减去最小值后除以最大公约数的值,这样记录的值就要小得多。对于差值的,和最大公约数一样,只不过最大公约数是1.对于压缩表的,比较特殊,他的使用条件有限,仅仅是在去重后的docValue的值得数量很少的情况下使用,他会把那些值排序,然后放在meta文件(也就是索引文件中),然后再在data文件中放入每个doc的值在排序后的所有的值中的序列号,这样就会使索引的体积小很多,查找时也更快。还有一个需要注意的是,如果某个doc没有值,就会写入0,。

这样,数字类型的DocValue的写入就完成了,下一篇 文章中看下是如何读取数字类型的docValue的。

猜你喜欢

转载自suichangkele.iteye.com/blog/2410207