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

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的位数小,则使用最大公约数记录的方法。
		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源码分析

    ### Lucene3源码分析知识点概述 #### 一、全文检索的基本原理 ##### 1. 总论 全文检索系统是一种高效的信息检索技术,能够帮助用户在海量文档中快速找到包含特定关键词的信息。Lucene是Java领域内最受欢迎的全文...

    Lucene项目的文档和源码

    源码阅读是理解任何软件内部工作原理的最好方式,通过研究Lucene的源码,我们可以深入了解其内部的数据结构、算法实现以及优化技巧。例如,可以学习到如何实现Trie数据结构进行高效查询,或者如何使用BitSet进行布尔...

    lucene.net2.9.4.2源码版

    《深入剖析Lucene.NET 2.9.4.2源码》 Lucene.NET是一个开源全文搜索引擎库,它是Apache Lucene项目的.NET版本。这个源码版是2.9.4.2版本,相较于2.9.4版进行了一些局部改进,以适应.NET平台的需求和优化。在本文中...

    Lucene源码解读1

    【Lucene源码解读1】 Lucene是一款开源的全文搜索引擎库,由Apache软件基金会开发,广泛应用于各种信息检索系统。其强大的搜索功能和高效的性能深受开发者喜爱。在深入理解Lucene之前,我们需要先了解它的核心概念...

    Lucene3.5源码jar包

    本压缩包包含的是Lucene 3.5.0版本的全部源码,对于想要深入理解Lucene工作原理、进行二次开发或者进行搜索引擎相关研究的开发者来说,是一份非常宝贵的学习资源。 Lucene 3.5.0是Lucene的一个重要版本,它在3.x...

    Lucene.Net-2.9.2 c#源码

    二、Lucene.Net 2.9.2源码结构 源码主要分为几个关键部分: 1. 分析器(Analyzers):包含各种语言的分析器实现,如StandardAnalyzer、SimpleAnalyzer、WhitespaceAnalyzer等。 2. 搜索(Search):实现搜索算法...

    Lucene学习源码.rar

    本文将主要围绕Java Lucene进行深入探讨,并基于提供的“Lucene学习源码.rar”文件中的“Lucene视频教程_讲解部分源码”展开讨论。 一、Lucene核心概念 1. 文档(Document):Lucene中的基本单位,用于存储待检索...

    lucene全文检索案例源码

    通过对“lucene全文检索案例源码”的学习,我们可以理解Lucene如何在实际项目中实现全文检索。从索引构建到搜索执行,每个步骤都至关重要。通过源码的深入研究,有助于我们在实际开发中更好地运用Lucene,提升搜索...

    lucene.net 2.9.1 源码

    《深入剖析Lucene.NET 2.9.1:源码解析与应用开发》 Lucene.NET 2.9.1是开源搜索引擎库Lucene的.NET版本,它为.NET开发者提供了强大的全文检索和索引功能。这个版本的源码提供了一个宝贵的资源,帮助我们理解其内部...

    基于lucene搜索引擎的java源码

    学习这个源码包可以帮助你理解如何在Java环境中使用Lucene进行全文检索,以及如何实现数据库与索引之间的交互。这不仅涉及到了Lucene的核心功能,也涵盖了实际项目中常见的增量索引和数据库集成问题。通过阅读和理解...

    使用Lucene对doc、docx、pdf、txt文档进行全文检索功能的实现 - 干勾鱼的CSDN博客 - CSDN博客1

    在Java开发中,Lucene被广泛用于实现文件的全文检索功能,包括对doc、docx、pdf、txt等常见格式文档的文本内容检索。在本文中,我们将探讨如何使用Lucene对这些文件类型进行全文检索的实现。 首先,为了实现全文...

    lucene in action源码2

    总之,《Lucene in Action》的源码是一份宝贵的教育资源,它能帮助开发者深入理解搜索引擎的运作原理,从而在实际项目中更好地利用Lucene。通过细致研究源码,我们不仅可以解决具体的技术问题,还能培养出更强的解决...

    lucene 华电项目 源码

    本文将结合“lucene 华电项目 源码”,深度解析Lucene的核心原理以及在华电项目中的实际应用。 首先,我们要理解Lucene的基本架构。Lucene的核心组件包括Analyzer(分析器)、Document(文档)、IndexWriter(索引...

    Lucene中的FST算法描述

    4. FSTHashMap:这是一个基于探测法实现的HashMap,其key是基于FSTNode生成的hash值,而value是FSTnode在FSTbytes数组中的位置索引。FSTHashMap可以加速判断某个节点是否已经被存储到FSTbytes中。 5. Frontier:这...

    Lucene In Action 2源码

    源码文件通常包含了书中各个章节的示例程序,这些示例涵盖了Lucene的基本用法到高级特性的实现,如文档索引、搜索查询、结果排序、过滤器、分词器、高亮显示等。通过研究这些源码,开发者可以了解如何有效地利用...

    运用在lucene中的中文分词算法源码

    《深入剖析Lucene中的中文分词算法源码》 在信息检索领域,Lucene作为一款强大的全文搜索引擎库,被广泛应用于各种数据检索系统。而中文分词是Lucene处理中文文本时的关键步骤,它决定了搜索的准确性和效率。本文将...

    lucene源码和程序

    在`lucene-1.4-final`这个压缩包中,包含了Lucene 1.4版本的源代码,你可以深入研究其内部实现,理解各个类和方法的工作原理。同时,这也可以帮助你定制分析器、优化搜索性能,或者扩展Lucene的功能,例如集成到你的...

    Lucene.Net源码与说明文档

    **Lucene.Net** 是一个基于 .NET Framework 的全文搜索引擎库,它是 Apache Lucene 项目的 .NET 实现。这个开源项目提供了高效、可扩展的搜索功能,使得开发者能够在其应用程序中轻松地实现高级的文本检索功能。 **...

    lucene-2.9.2.jar包+源码

    在Lucene-2.9.2的源码中,你可以看到关于TF-IDF的具体实现,如`TFIDFSimilarity`类,它是Lucene对TF-IDF算法的封装。它不仅包含了TF和IDF的计算逻辑,还考虑了诸如短语匹配、长度惩罚等因素,以提升搜索精度。 除了...

    Lucene源码

    作为一款Java实现的全文搜索引擎架构,Lucene 提供了完整的索引和查询引擎,使得开发者能够快速、有效地在大量数据中进行文本搜索。 ### Lucene 的核心组件 1. **索引(Indexing)**: Lucene 的索引过程将文档内容...

Global site tag (gtag.js) - Google Analytics