各种类型的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() {//使用给定的docValueConsumer来处理,处理的参数是一个迭代器 @Override public Iterator iterator() { return new NumericIterator(maxDoc); } }); }
使用的迭代器是一个内部类,看下代码:
private class NumericIterator implements Iterator{ 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, Iterablevalues) throws IOException { addNumericField(field, values, true); } void addNumericField(FieldInfo field, Iterable 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 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 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的。