DocValues 详解

本贴最后更新于 2825 天前,其中的信息可能已经物是人非

搜索引擎最简单的查询场景是

  1. 通过查询条件的 term 找到 term 对应的倒排列表
  2. 对倒排列表进行合并,得到符合查询条件的文档 id 列表并同时计算每个文档的得分
  3. 进行排序,取 topN 个文档 id
  4. 根据 topN 的文档 id 到磁盘中的正排索引中读取要返回的 field 组成返回文档
  5. 最后返回给调用者。

如果复杂点,需要对搜索结果根据字段排序(sort),聚合统计(group、facet、stat)等操作。这种场景下就需要根据 docId 来获取相应的 field 值来进行计算了。但由于查询命中的结果数往往非常大,不能像构造 topN 的返回文档那样到磁盘去获取字段值,这样实在太慢了。
针对这种需求,Lucene 提供了 DocValues 来快速获取 field 值的方法。

DocValues 的功能。

DocVaues 的功能很简单,就是根据文档 Id,获取这个文档的字段值。下面是 NumericDocValues 类说明

/**
 * A per-document numeric value. 
 */
 public abstract class NumericDocValues {
  /** Sole constructor. (For invocation by subclass 
   *  constructors, typically implicit.) */
  protected NumericDocValues() {}
  /**
 * Returns the numeric value for the specified document ID. * @param docID document ID to lookup
 * @return numeric value
 */  public abstract long get(int docID);
}

可以把它看成是一个大数组,数组下标是文档 id,数组值是字段值。根据文档 id 读取字段值非常快。当然这个“大数组”是经过了压缩的(它的压缩算法非常巧妙,后面会另起一篇文章详细介绍),否则非常占内存。其实,在 3.x 之前的版本(其实是 fieldcache)中,还真是一个大数组构成的。

DocValues 的种类

  • NumericDocValues 数值型的 DocValues,用于存放 int、long、double、float 等数值

  • BinaryDocValues 单值二进制 DocValues,String 类型的字段就是用这种类型来存放的。

  • SortedDocValues 单值、有序的二进制 DocValues,也用于存放 String 值,相当于用两个数组来存放 DocValues,一个数组 dictionary[]存放去重后的值,并且是有序的,另外一个数组 order[]用来记录文档的 field 值在 dictionary[]的偏移量,order[]的下标是 docId,值是 dictionary 的下标。和 BinaryDocValues 相比,如果值的重复率比较高,则由于去重了,会比 BinaryDocValues 节省很多内存,并且 dictionary[]是有序的,还可以进一步压缩。

  • SortedNumericDocValues 用于存放多值数值类型。

  • SortedSetDocValues 用于存放多值二进制类型。

DocValues 的构建

DocValues 需要在构建索引的时候额外生成 docId 到字段值的映射文件,这样,根据映射文件构造存放于内存中的 docId 和字段值映射数据结构的时候就很快了。而且由于有 docId 到字段值的映射文件,因此,docvalue 还有基于磁盘的实现(直接读取磁盘上的映射文件),虽然比内存的实现要慢,但也比从正排索引中读取快多了。映射文件会额外占用磁盘空间,Lucene 也允许通过不生成映射文件的方式构造 DocValues,这时候就会遍历 term,从 term 找到 docId,进而构造出 docId 到字段值映射的数据结构了,这种方式比生成映射文件的方式要慢很多,好处是不占用磁盘空间。获取 DocValues 实例的两种方式:

  • 通过 DocValues 工具类获取

通过 DocValues 工具类获取,只能获取到通过映射文件生成的 DocValues

public static NumericDocValues getNumeric(LeafReader reader, String field) throws IOException {
  NumericDocValues dv = reader.getNumericDocValues(field);
 if (dv == null) {
	checkField(reader, field, DocValuesType.NUMERIC);
 return emptyNumeric();
  } else {
	return dv;
  }
}

可以看到,其实是通过底层的 LeafReader 获取的,我们再看看 LeafReader 获取 DocValues 的代码

@Override
public final NumericDocValues getNumericDocValues(String field) throws IOException {
  ensureOpen();
  Map,Object> dvFields = docValuesLocal.get();

  Object previous = dvFields.get(field);
 if (previous != null && previous instanceof NumericDocValues) {
	return (NumericDocValues) previous;
  } else {
	FieldInfo fi = getDVField(field, DocValuesType.NUMERIC);
 if (fi == null) {
	  return null;
  }
	NumericDocValues dv = getDocValuesReader().getNumeric(fi);
  dvFields.put(field, dv);
 return dv;
  }
}

可以看到最终是通过 DocValuesReader 来获取,并且这里用了一个 ThreadLocal 来缓存 DocValues,这里为什么是用 ThreadLocal 来缓存呢?如果并发线程多了,不就很占内存吗?而且,如果不是用了线程池,则 ThreadLocal 就起不到缓存的作用了。其实,从 MemoryDocValuesProducer 进去看一下,里面还会进行一次缓存的,不同线程引用的 DocValues 还是同一个。

@Override
public synchronized NumericDocValues getNumeric(FieldInfo field) throws IOException {
  NumericDocValues instance = numericInstances.get(field.name);
 if (instance == null) {
	instance = loadNumeric(field);
 if (!merging) {
	  numericInstances.put(field.name, instance);
  }
  }
  return instance;
}

那 ThreadLocal 在这里是何用意呢?其实,前面也说了,DocValues 还有直接读取磁盘映射文件的方式。ThreadLocal 在这里就是为了它实现的,基于磁盘读取,线程间共享是没有意义,而且基于磁盘的 DocValues 实例会记录读取的偏移量,这个是不能共享的,不同线程有不同的读取偏移量。所以,这里用了 ThreadLocal 就很巧妙的兼容了内存和磁盘的 DocValues 实例。

  • 通过 FieldCache 获取。

FieldCache 获取,会先看下 DocValues 映射文件是否存在,如果存在,则和 DocValues 工具类读取的方式是一样的,如果不存在,则遍历 Term 构造 DocValues。

为什么不统一都从 FieldCache 来获取呢。貌似官方论坛是说 ES 默认是都生成了 DocValues 映射文件的,不需要从 Term 进行构建,而 Solr 默认是没有生成 DocValues 映射文件的,因此就存在两种方式了。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...