之前讲解了Lucene的分词器,这节记录下Lucene的索引。
Lucene索引创建API示例
Lucene索引创建代码示例
// 创建使用的分词器Analyzer analyzer = new IKAnalyzer4Lucene7(true);// 索引配置对象IndexWriterConfig config = new IndexWriterConfig(analyzer);try ( // 索引存放目录 // 存放到文件系统中 Directory directory = FSDirectory .open((new File("d:/test/indextest")).toPath()); // 也可以存放到内存中 // Directory directory = new RAMDirectory(); // 创建索引写对象 IndexWriter writer = new IndexWriter(directory, config);) { // 准备document Document doc = new Document(); // 商品id:字符串,不索引、但存储 String prodId = "p0001"; doc.add(new StoredField("prodId", prodId));// 往document中添加 商品名称字段 String name = "ThinkPad X1 Carbon 20KH0009CD/25CD 超极本轻薄笔记本电脑联想";doc.add(new TextField("name", name, Store.YES));.......
IndexWriter涉及类
IndexWriterConfig:写索引配置,装载着分词器,提供着配置信息
Directory: 索引存储的方式,文件系统或者内存或数据库
Document:索引存储的内容
IndexWriter 用来创建、维护一个索引 。它的API使用流程:
// 创建索引写对象IndexWriter writer = new IndexWriter(directory, config);// 创建documentDocument doc = new Document();// 将文档添加到索引writer.addDocument(doc);// 删除文档//writer.deleteDocuments(terms);//修改文档//writer.updateDocument(term, doc);// 刷新writer.flush();// 提交writer.commit();
那Document是如何存储的呢?
Document
Document即文档,要索引的数据记录、文档在lucene中的表示,是索引、搜索的基本单 元。一个Document由多个字段Field构成。就像数据库的记录-字段。
IndexWriter按加入的顺序为Document指定一个递增的id(从0开始),称为文档id。反向索引中存储的是这个id,文档存储中正向索引也是这个id。 业务数据的主键id只是文档的一个字段。
Filed
Filed即字段:由字段名name、字段值value(fieldsData)、字段类型 type 三 部分构成。 字段值可以是文本(String、Reader 或 预分析的 TokenStream)、二进制值(byte[])或数值。
IndexableFieldType
字段类型:描述该如何索引存储该字段
字段可选择性地保存在索引中,这样在搜索结果中,这些保存的字段值就可获得。 一个Document应该包含一个或多个存储字段来唯一标识一个文档。 未存储的字段,从索引中取得的document中是没有这些字段的。
Document 类关系:
IndexableFieldType:提供了是否分词,是否存储,是否标准化,如何索引等方法。
包括:stored,tokenized,indexOptions,storeTermVectors,omitNorms,
docValueType,point
IndexOptions
IndexOptions: 索引选项
NONE: Not indexed 不索引
DOCS:反向索引中只存储了包含该词的 文档id,没有词频、位置
DOCS_AND_FREQS:反向索引中会存储 文档id、词频
DOCS_AND_FREQS_AND_POSITIONS :反向索引中存储文档id、词频、位置
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:反向索引中存储 文档id、词频、位置、偏移量
例如在百度搜索Lucene
显示的信息:有一个标题,下面是正文的简单的描述,再下面是跳转的链接。搜索词Lucene的红色的高亮的显示。
当我们创建索引的时候,标题需要被索引,分词,存储。文章的链接,不是被索引,分词,只需要将它存储。文章的内容需要分词,建立索引,但不需要将部分描述存储。
有的情况下,如果要实现短语 查询、临近查询(跨度查询),例如:搜索包含“张三” “李四”,且两词之间跨度不超过5个字符 。
这时,需要存储分项的偏移量,位置等信息。
但是有的情况下某个字段不需要进行短语查询、临近查询,那么在反向索引中就 不需要保存位置、偏移数据。可以降低反向索引的数据量,提升效率。
为了提升反向索引的效率,这样的字段的位置、偏移数据是不应该保存到反向 索引中的。这也你前面看到 IndexOptions为什么有那些选项的原因。 在lucene4.0以前,反向索引中总会存储这些数据,4.0后改进为可选择的。
那对于做高亮显示,或者斜体,黑体(或得到搜索结果后需要使用这些信息)的字段怎么办?用IndexOptions可能不能满足需求,或者比较困难。
storeTermVectors
一个字段分词器分词后,每个词项会得到一系列属性信息,如出现频率、位 置、偏移量等,这些信息构成一个词项向量 termVectors。
对于不需要在搜索反向索引时用到,但在搜索结果处理时需要的位置、偏移 量、附加数据(payLoad) 的字段,我们可以单独为该字段存储(文档--->词项向量)的正向索引。
FieldType实现类中有对应的set方法
什么是附加信息Payloads
粉色:代表一个文档Id,DocId
绿色:词频,出现几次
橙色:词的位置
黑色:附加信息
a这个词,DocId为2,在文章出现一个,位置索引为2的位置
is的词,在DocId为1的文章中,出现一次,位置为1,附加信息为下划线。
附加信息非常有用,可用它来存储特殊信息,及减少词项数等。
我们往往需要对搜索的结果支持按不同的字段进行排序,如商品搜索 结果按价格排序、按销量排序等。以及对搜索结果进行按某字段分组统计,如 按品牌统计。
假如我们按关键字“娃娃”搜索后得到相关的文档id列表 {10,21,18,48,29,…..} 要对它们进行按价格排序 有的人想看销量排序 有时需要按品牌统计数量…
反向索引对排序有用吗? 需得到每个id对应的价格或销售是多少、品牌是什么,再进行排序、统计。 这个价格、销量、品牌数据在哪里? 如果搜到的文档列表量很大,排序会有什么问题没?
空间换时间
对这种需要排序、分组、聚合的字段,为其建立独立的文档->字段值的正向索引、列式存储。这样我们要加载搜中文档的这个字段的数据就快很多, 耗内存少。
docValuesType
IndexableFieldType 中的 docValuesType方法 就是让你来为需要排序、分组、 聚合的字段指定如何为该字段创建文档->字段值的正向索引的。
DocValuesType 选项说明:
DocValuesType是强类型要求的: 字段的值必须保证同类型。需要排序、分组、聚合、分类查询(面查询)的字段才创建docValues。
具体使用选择:
Point
IndexableFieldType中最后定义的的pointDimensionCount(), pointNumBytes() 是做何用的? Lucene6以后引入了点的概念来表示数值字段,废除了原来的IntField等。在Point 字段类中提供了精确、范围查询的便捷方法。 注意:只是引入点的概念,并未改变数值字段的本质。 既然是点,就有空间概念:维度。一维:一个值,二维:两个值的;…… pointDimensionCount() 返回点的维数 pointNumBytes() 返回点中数值类型的字节数。
以下为一个代码示例:
// 创建使用的分词器 Analyzer analyzer = new IKAnalyzer4Lucene7(true); // 索引配置对象 IndexWriterConfig config = new IndexWriterConfig(analyzer); try ( // 索引存放目录 // 存放到文件系统中 Directory directory = FSDirectory .open((new File("d:/test/indextest")).toPath()); // 存放到内存中 // Directory directory = new RAMDirectory(); // 创建索引写对象 IndexWriter writer = new IndexWriter(directory, config);) { // 准备document Document doc = new Document(); // 商品id:字符串,不索引、但存储 String prodId = "p0001"; FieldType onlyStoredType = new FieldType(); onlyStoredType.setTokenized(false); onlyStoredType.setIndexOptions(IndexOptions.NONE); onlyStoredType.setStored(true); onlyStoredType.freeze(); doc.add(new Field("prodId", prodId, onlyStoredType)); // 等同下一行 // doc.add(new StoredField("prodId", prodId)); // 商品名称:字符串,分词索引(存储词频、位置、偏移量)、存储 String name = "ThinkPad X1 Carbon 20KH0009CD/25CD 超极本轻薄笔记本电脑联想"; FieldType indexedAllStoredType = new FieldType(); indexedAllStoredType.setStored(true); indexedAllStoredType.setTokenized(true); indexedAllStoredType.setIndexOptions( IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS); indexedAllStoredType.freeze(); doc.add(new Field("name", name, indexedAllStoredType)); // 图片链接:仅存储 String imgUrl = "http://www.dongnao.com/aaa"; doc.add(new Field("imgUrl", imgUrl, onlyStoredType)); // 商品简介:文本,分词索引(不需要支持短语、临近查询)、存储,结果中支持高亮显示 String simpleIntro = "集成显卡 英特尔 酷睿 i5-8250U 14英寸"; FieldType indexedTermVectorsStoredType = new FieldType(); indexedTermVectorsStoredType.setStored(true); indexedTermVectorsStoredType.setTokenized(true); indexedTermVectorsStoredType .setIndexOptions(IndexOptions.DOCS_AND_FREQS); indexedTermVectorsStoredType.setStoreTermVectors(true); indexedTermVectorsStoredType.setStoreTermVectorPositions(true); indexedTermVectorsStoredType.setStoreTermVectorOffsets(true); indexedTermVectorsStoredType.freeze(); doc.add(new Field("simpleIntro", simpleIntro, indexedTermVectorsStoredType)); // 价格,整数,单位分,不索引、存储、要支持排序 int price = 999900; FieldType numericDocValuesType = new FieldType(); numericDocValuesType.setTokenized(false); numericDocValuesType.setIndexOptions(IndexOptions.NONE); numericDocValuesType.setStored(true); numericDocValuesType.setDocValuesType(DocValuesType.NUMERIC); numericDocValuesType.setDimensions(1, Integer.BYTES); numericDocValuesType.freeze(); doc.add(new MyIntField("price", price, numericDocValuesType)); // 与下两行等同 // doc.add(new StoredField("price", price)); // doc.add(new NumericDocValuesField("price", price)); // 类别:字符串,索引不分词,不存储、支持分类统计,多值 FieldType indexedDocValuesType = new FieldType(); indexedDocValuesType.setTokenized(false); indexedDocValuesType.setIndexOptions(IndexOptions.DOCS); indexedDocValuesType.setDocValuesType(DocValuesType.SORTED_SET); indexedDocValuesType.freeze(); doc.add(new Field("type", "电脑", indexedDocValuesType) { @Override public BytesRef binaryValue() { return new BytesRef((String) this.fieldsData); } }); doc.add(new Field("type", "笔记本电脑", indexedDocValuesType) { @Override public BytesRef binaryValue() { return new BytesRef((String) this.fieldsData); } }); // 等同下四行 // doc.add(new StringField("type", "电脑", Store.NO)); // doc.add(new SortedSetDocValuesField("type", new BytesRef("电脑"))); // doc.add(new StringField("type", "笔记本电脑", Store.NO)); // doc.add(new SortedSetDocValuesField("type", new // BytesRef("笔记本电脑"))); // 商家 索引(不分词),存储、按面(分类)查询 String fieldName = "shop"; String value = "联想官方旗舰店"; doc.add(new StringField(fieldName, value, Store.YES)); doc.add(new SortedDocValuesField(fieldName, new BytesRef(value))); // 上架时间:数值,排序需要 long upShelfTime = System.currentTimeMillis(); doc.add(new NumericDocValuesField("upShelfTime", upShelfTime)); writer.addDocument(doc); } catch (IOException e) { e.printStackTrace(); }}public static class MyIntField extends Field { public MyIntField(String fieldName, int value, FieldType type) { super(fieldName, type); this.fieldsData = Integer.valueOf(value); } @Override public BytesRef binaryValue() { byte[] bs = new byte[Integer.BYTES]; NumericUtils.intToSortableBytes((Integer) this.fieldsData, bs, 0); return new BytesRef(bs); }}