各种类型的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的位数小,则使用最大公约数记录的方法。 final long gcdDelta = (maxValue - minValue) / gcd; final long gcdBitsRequired = DirectWriter.unsignedBitsRequired(gcdDelta); format = gcdBitsRequired < deltaBitsRequired ? GCD_COMPRESSED : DELTA_COMPRESSED;//这里要判断是否会更小,因为在maxValue-minValue之后可能会变大也可能会变小,因为minValue可能是负数 } 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://基于差值的 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的。
相关推荐
### Lucene3源码分析知识点概述 #### 一、全文检索的基本原理 ##### 1. 总论 全文检索系统是一种高效的信息检索技术,能够帮助用户在海量文档中快速找到包含特定关键词的信息。Lucene是Java领域内最受欢迎的全文...
源码阅读是理解任何软件内部工作原理的最好方式,通过研究Lucene的源码,我们可以深入了解其内部的数据结构、算法实现以及优化技巧。例如,可以学习到如何实现Trie数据结构进行高效查询,或者如何使用BitSet进行布尔...
《深入剖析Lucene.NET 2.9.4.2源码》 Lucene.NET是一个开源全文搜索引擎库,它是Apache Lucene项目的.NET版本。这个源码版是2.9.4.2版本,相较于2.9.4版进行了一些局部改进,以适应.NET平台的需求和优化。在本文中...
【Lucene源码解读1】 Lucene是一款开源的全文搜索引擎库,由Apache软件基金会开发,广泛应用于各种信息检索系统。其强大的搜索功能和高效的性能深受开发者喜爱。在深入理解Lucene之前,我们需要先了解它的核心概念...
本压缩包包含的是Lucene 3.5.0版本的全部源码,对于想要深入理解Lucene工作原理、进行二次开发或者进行搜索引擎相关研究的开发者来说,是一份非常宝贵的学习资源。 Lucene 3.5.0是Lucene的一个重要版本,它在3.x...
二、Lucene.Net 2.9.2源码结构 源码主要分为几个关键部分: 1. 分析器(Analyzers):包含各种语言的分析器实现,如StandardAnalyzer、SimpleAnalyzer、WhitespaceAnalyzer等。 2. 搜索(Search):实现搜索算法...
本文将主要围绕Java Lucene进行深入探讨,并基于提供的“Lucene学习源码.rar”文件中的“Lucene视频教程_讲解部分源码”展开讨论。 一、Lucene核心概念 1. 文档(Document):Lucene中的基本单位,用于存储待检索...
通过对“lucene全文检索案例源码”的学习,我们可以理解Lucene如何在实际项目中实现全文检索。从索引构建到搜索执行,每个步骤都至关重要。通过源码的深入研究,有助于我们在实际开发中更好地运用Lucene,提升搜索...
《深入剖析Lucene.NET 2.9.1:源码解析与应用开发》 Lucene.NET 2.9.1是开源搜索引擎库Lucene的.NET版本,它为.NET开发者提供了强大的全文检索和索引功能。这个版本的源码提供了一个宝贵的资源,帮助我们理解其内部...
在Java开发中,Lucene被广泛用于实现文件的全文检索功能,包括对doc、docx、pdf、txt等常见格式文档的文本内容检索。在本文中,我们将探讨如何使用Lucene对这些文件类型进行全文检索的实现。 首先,为了实现全文...
学习这个源码包可以帮助你理解如何在Java环境中使用Lucene进行全文检索,以及如何实现数据库与索引之间的交互。这不仅涉及到了Lucene的核心功能,也涵盖了实际项目中常见的增量索引和数据库集成问题。通过阅读和理解...
总之,《Lucene in Action》的源码是一份宝贵的教育资源,它能帮助开发者深入理解搜索引擎的运作原理,从而在实际项目中更好地利用Lucene。通过细致研究源码,我们不仅可以解决具体的技术问题,还能培养出更强的解决...
本文将结合“lucene 华电项目 源码”,深度解析Lucene的核心原理以及在华电项目中的实际应用。 首先,我们要理解Lucene的基本架构。Lucene的核心组件包括Analyzer(分析器)、Document(文档)、IndexWriter(索引...
4. FSTHashMap:这是一个基于探测法实现的HashMap,其key是基于FSTNode生成的hash值,而value是FSTnode在FSTbytes数组中的位置索引。FSTHashMap可以加速判断某个节点是否已经被存储到FSTbytes中。 5. Frontier:这...
源码文件通常包含了书中各个章节的示例程序,这些示例涵盖了Lucene的基本用法到高级特性的实现,如文档索引、搜索查询、结果排序、过滤器、分词器、高亮显示等。通过研究这些源码,开发者可以了解如何有效地利用...
《深入剖析Lucene中的中文分词算法源码》 在信息检索领域,Lucene作为一款强大的全文搜索引擎库,被广泛应用于各种数据检索系统。而中文分词是Lucene处理中文文本时的关键步骤,它决定了搜索的准确性和效率。本文将...
在`lucene-1.4-final`这个压缩包中,包含了Lucene 1.4版本的源代码,你可以深入研究其内部实现,理解各个类和方法的工作原理。同时,这也可以帮助你定制分析器、优化搜索性能,或者扩展Lucene的功能,例如集成到你的...
**Lucene.Net** 是一个基于 .NET Framework 的全文搜索引擎库,它是 Apache Lucene 项目的 .NET 实现。这个开源项目提供了高效、可扩展的搜索功能,使得开发者能够在其应用程序中轻松地实现高级的文本检索功能。 **...
在Lucene-2.9.2的源码中,你可以看到关于TF-IDF的具体实现,如`TFIDFSimilarity`类,它是Lucene对TF-IDF算法的封装。它不仅包含了TF和IDF的计算逻辑,还考虑了诸如短语匹配、长度惩罚等因素,以提升搜索精度。 除了...
作为一款Java实现的全文搜索引擎架构,Lucene 提供了完整的索引和查询引擎,使得开发者能够快速、有效地在大量数据中进行文本搜索。 ### Lucene 的核心组件 1. **索引(Indexing)**: Lucene 的索引过程将文档内容...