注:本文基于Lucene 8.2.0 版本。

回忆一下之前文章中创建字段(Field)的一些代码片段:

// 片段1
Field pathField = new StringField("path", file.toString(),Field.Store.YES);

// 片段2
FieldType fieldType = new FieldType();
fieldType.setStored(true);
fieldType.setStoreTermVectors(true);
fieldType.setStoreTermVectorOffsets(true);
fieldType.setStoreTermVectorPositions(true);
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
Field field = new Field("content", sentence, fieldType);

在创建Field的时候,第一个参数是字段名,第二个是字段值,第三个就是字段属性了。字段的属性决定了字段如何Analyze,以及Analyze之后存储哪些信息,进而决定了以后我们可以使用哪些方式进行检索。本文就来介绍字段以及它的的一些常用属性。

1. Field类

Field对应的类是org.apache.lucene.document.Field,该类实现了org.apache.lucene.document.IndexableField接口,代表用于indexing的一个字段。Field类比较底层一些,所以Lucene实现了许多Field子类,用于不同的场景,比如下图是IDEA分析出来的Field的子类:

image-20190918223509632

如果有某个子类能满足我们的场景,那推荐使用子类。在介绍常用子类之前,需要了解一下字段的三大类属性:

  • 是否indexing(只有indexing的数据才能被搜索)
  • 是否存储(即是否保存字段的原始值)
  • 是否保存term vector

这些属性就是由之前文章中介绍的org.apache.lucene.index.IndexOptions枚举类定义的:

  • NONE:不索引
  • DOCS:只索引字段,不保存词频等位置信息
  • DOCS_AND_FREQS:索引字段并保存词频信息,但不保存位置信息
  • DOCS_AND_FREQS_AND_POSITIONS:索引字段并保存词频及位置信息
  • DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:索引字段并保存词频、位置、偏移量等信息

Field的各个子类就是实现了对不同类型字段的存储,同时选择了不同的字段属性,这里列举几个常用的:

  • TextField:存储字符串类型的数据。indexing+analyze;不存储原始数据;不保存term vector。适用于需要全文检索的数据,比如邮件内容,网页内容等。
  • StringField:存储字符串类型的数据。indexing但不analyze,即整个字符串就是一个token,之前介绍的KeywordAnalyzer就属于这种;不存储原始数据;不保存term vector。适用于文章标题、人名、ID等只需精确匹配的字符串。
  • IntPoint, LongPoint, FloatPoint, DoublePoint:用于存储各种不同类型的数值型数据。indexing;不存储原始数据;不保存term vector。适用于数值型数据的存储。

所以,对于Field及其子类我们需要注意以下两点:

  1. 几乎所有的Field子类(除StoredField)都默认不存储原始数据,如果需要存储原始数据,就要额外增加一个StoredField类型的字段,专门用于存储原始数据。注意该类也是Field的一个子类。当然也有一些子类的构造函数中提供了参数来控制是否存储原始数据,比如StringField,创建实例时可以通过传递Field.Store.YES参数来存储原始数据。
  2. Field子类的使用场景和对应的属性都已经设置好了,如果子类不能满足我们的需求,就需要对字段属性进行自定义,但子类的属性一般是不允许更改的,需要直接使用Field类,再配合FieldType类进行自定义化。

2. FieldType类

org.apache.lucene.document.FieldType类实现了org.apache.lucene.index.IndexableFieldType接口,用于描述字段的属性。之前的文章中有使用过该类来自定义字段属性的代码,这里再贴一下相关的代码片段:

FieldType fieldType = new FieldType();
fieldType.setStored(true);
fieldType.setStoreTermVectors(true);
fieldType.setStoreTermVectorOffsets(true);
fieldType.setStoreTermVectorPositions(true);
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
Field field = new Field("content", sentence, fieldType);

该类定义了一些成员变量,这些成员变量就是字段的一些属性,这里列一下代码中的成员变量:

private boolean stored;
private boolean tokenized = true;
private boolean storeTermVectors;
private boolean storeTermVectorOffsets;
private boolean storeTermVectorPositions;
private boolean storeTermVectorPayloads;
private boolean omitNorms;
private IndexOptions indexOptions = IndexOptions.NONE;
private boolean frozen;
private DocValuesType docValuesType = DocValuesType.NONE;
private int dataDimensionCount;
private int indexDimensionCount;
private int dimensionNumBytes;
private Map<String, String> attributes;

大部分属性含义已经比较清楚了,这里再简单介绍一下其含义:

  • stored:是否存储字段,默认为false。
  • tokenized:是否analyze,默认为true。
  • storeTermVectors:是否存储TermVector(如果是true,也不存储offset、position、payload信息),默认为false。
  • storeTermVectorOffsets:是否存储offset信息,默认为false。
  • storeTermVectorPositions:是否存储position信息,默认为false。
  • storeTermVectorPayloads:是否存储payload信息,默认为false。
  • omitNorms:是否忽略norm信息,默认为false。那什么是norm信息呢?Norm的全称是“Normalization”,理解起来非常简单,按照TF-IDF的计算方式,包含同一个搜索词的多个文本,文本越短其权重(或者叫相关性)越高。比如有两个文本都包含搜索词,一个文本只有100个词,另外文本一个有1000个词,那按照TF-IDF的算法,第一个文本跟搜索词的相关度比第二个文本高。这个信息就是norm信息。如果我们将其忽略掉,那在计算相关性的时候,会认为长文本和短文本的权重得分是一样的。
  • indexOptions:即org.apache.lucene.index.IndexOptions,已经介绍过了。默认值为DOCS_AND_FREQS_AND_POSITIONS
  • frozen:该值设置为true之后,字段的各个属性就不允许再更改了,比如Field的TextField、StringField等子类都将该值设置为true了,因为他们已经将字段的各个属性定制好了。
  • dataDimensionCountindexDimensionCountdimensionNumBytes:这几个和数值型的字段类型有关系,Lucene 6.0开始,对于数值型都改用Point来组织。dataDimensionCount和indexDimensionCount都可以理解为是Point的维度,类似于数组的维度。dimensionNumBytes则是Point中每个值所使用的字节数,比如IntPoint和FloatPoint是4个字节,LongPoint和DoublePoint则是8个字节。
  • attributes:可以选择性的以key-value的形式给字段增加一些元数据信息,但注意这个key-value的map不是线程安全的。
  • docValuesType:指定字段的值指定以何种类型索引DocValue。那什么是DocValue?DocValue就是从文档到Term的一个正向索引,需要这个东西是因为排序、聚集等操作需要根据文档快速访问文档内的字段。Lucene 4.0之前都是在查询的时候将所有文档的相关字段信息加载到内存缓存,一方面用的时候才加载,所以慢,另一方面对于内存压力很大。4.0中引入了DocValue的概念,在indexing阶段除了创建倒排索引,也可以选择性的创建一个正向索引,这个正向索引就是DocValue,主要用于排序、聚集等操作。其好处是存储在磁盘上,减小了内存压力,而且因为是事先计算好的,所以使用时速度也很快。弊端就是磁盘使用量变大(需要耗费 document个数*每个document的字段数 个字节),同时indexing的速度慢了。

本文内容比较杂乱,但其实对于我们使用(包括ES、Solr等基于Lucene的软件)而言,只需要知道我们检索一个字段的时候可以控制保存哪些信息,以及这些信息在什么场景下使用,能带来什么好处,又会产生什么弊端即可。举个例子:比如我们在设计字段的时候,如果一个字段不会用来排序,也不会做聚集,那就没有必要生成DocValue,既能节省磁盘空间,又能提高写入速度。另外对于norm信息,如果你的场景只关注是否包含,那无需保存norm信息,但如果也关注相似度评分,并且文本长短是一个需要考虑的因素,那就应该保存norm信息。