如果对Lucene不熟悉的,请移步:Lucene搜索引擎-分词器
对输入的一串内容进行分词以后,如果需要在后续进行检索,则必须定义如何存储以及存储的方式、内容,则这就是索引需要做的事情。
直接上代码:
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.BytesRef;
import com.dongnao.lucene.demo.analizer.ik.IKAnalyzer4Lucene7;
public class IndexWriteDemo {
public static void main(String[] args) {
// 创建使用的分词器,这里使用IKAnalyzer分词器
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));
writer.addDocument(doc);
} catch (IOException e) {
e.printStackTrace();
}
}
}
概念理解
先来理解下概念
-
Index:索引,类似传统数据库中的表的概念。Lucene的Index可以理解为一个文档收纳箱,你可以往内部塞入新的文档,或者从里面拿出文档,但如果你要修改里面的某个文档,则必须先拿出来修改后再塞回去。这个收纳箱可以塞入各种类型的文档,文档里的内容可以任意定义,Lucene都能对其进行索引。
-
Document:文档,类似传统数据库中的行记录的概念。一个Index内会包含多个Document。写入Index的Document会被分配一个唯一的ID,即Sequence Number。
-
Field:字段,类似于传统数据库中字段的概念。一个Document会由一个或多个Field组成,Field是Lucene中数据索引的最小定义单位。Lucene提供多种不同类型的Field,例如StringField、TextField、LongFiled或NumericDocValuesField等,Lucene根据Field的类型(FieldType)来判断该数据要采用哪种类型的索引方式(Invert Index、Store Field、DocValues或N-dimensional等。
-
Term和Term Dictionary:词项和词典,Lucene中索引和搜索的最小单位,一个Field会由一个或多个Term组成,Term是由Field经过Analyzer(分词)产生。Term Dictionary即Term词典,是根据条件查找Term的基本索引。
IndexWriter详解
IndexWriterConfig、Directory、Document作为IndexWriter的输入
-
IndexWriterConfig:索引配置。用这个配置对象创建好IndexWriter对象后,再修改这个配置对象的配置信息不会对IndexWriter对象起作用。涉及如下配置:
使用的分词器
如何打开索引(是新建、还是追加)
还可配置缓冲大小、或缓冲多少个文档,再刷新到存储中
还可配置合并、删除等策略 -
Directory:指定索引数据存放的位置,可以存放在内存、文件系统、数据库中。FSDirectory是存放于文件系统中,RAMDirectory是存放于内存中
注意:IndexWriter是线程安全的。若业务代码中有其他的同步控制,请不要使用IndexWriter作为所对象,以免死锁。 -
IndexWriter的API使用流程:
IndexWriter writer = new IndexWriter(directory, config);// 创建索引写对象
writer.addDocument(doc);// 创建document,将文档添加到索引
//writer.deleteDocuments(terms);// 删除文档
//writer.updateDocument(term, doc);//修改文档
writer.flush();// 刷新
writer.commit();// 提交
Document详解
Docement存储
一个Document相当于数据库中的表的行记录,由多个字段Field构成,一个Field就像数据库表中的字段。
IndexWriter按加入的顺序为Document指定一个递增的id(从0开始),称为文档id。反向索引中存储的其实就是这个文档id,正向索引也是这个id,业务数据的主键id只是文档的一个字段。
怎么又出现了一个正向索引?来,继续看。
保存索引的时候其实不仅仅只保存反向索引,同时也会保存正向索引,那正向索引保存的又是什么呢?举个例子会比较清晰:
- 百度搜索关键字:“新闻标题”
- 搜索结果有很多内容:新闻标题、新闻摘要、图片、URL地址
- 关键字"新闻标题"是存储在反向索引中的,而搜索结果中的其他内容"新闻摘要"、“图片”、"URL地址"正是存储在正向索引中的,先通过关键字找到对应的document id,在通过id找到具体的结果。
反向索引
词项 | document id |
---|---|
苍老师 | {1} |
麻仓优 | {2} |
正向索引
document id | 新闻id | 新闻标题 | 新闻摘要 | URL地址 |
---|---|---|---|---|
1 | 001 | 苍老师是世界的 | yyy | http://yyy |
2 | 002 | 麻仓优也是世界的 | xxx | http:/xxx |
当搜索“苍老师”时,通过document id关联到新闻信息,而更具体的新闻信息则是存储在数据库或其他地方的。
上述的每个信息统称为字段“Field”,有字段名name、字段值value、字段类型type三部分构成。字段值可以是文本、二进制或数值。
看完上述例子后应该对以下的问题会非常清楚:
- 新闻:新闻id,新闻标题、新闻内容、作者、所属分类、发表时间
- 网页搜索的结果:标题、内容、链接地址
- 商品:id、名称、图片链接、类别、价格、库存、商家、品牌、月销量、详情
- 我们收集数据创建document对象来为其创建索引,数据的所有属性是否都需要加入到document中?哪些字段应加入到document中?是不是所有加入的字段都需要进行索引?是不是所有加入的字段都要保存到索引库中?什么样的字段该被索引?什么样的字段该被存储?
Field索引类型
实际应用中会碰到要在搜索结果中做关键字高亮,实现短语
查询、临近查询(跨度查询)等等,那这些信息又是如何存储的呢?
比如:要搜索包含“张三” “李四”,且两词之间跨度不超过5个字符。
这种时候就需要用到词项向量。
词项向量:一个字段分词器分词后,每个词项会得到一系列属性信息,如 出现频率、位置、偏移量等,这些信息构成一个词项向量 termVectors
可以在Field的索引类型中设置是否保存这些词项向量
IndexOptions索引选项:
NONE:不索引
DOCS:反向索引中只存储了包含该词的 文档id
DOCS_AND_FREQS:反向索引中会存储 文档id、词频
DOCS_AND_FREQS_AND_POSITIONS:反向索引中存储 文档id、词频、位置
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:反向索引中存储 文档id、词频、位置、偏移量
其实为了提升反向索引的效率,这样的字段的位置、偏移数据是不应该保存到反向索引中的,这也是为什么 IndexOptions会有那些选项的原因。
在lucene4.0以前,反向索引中总会存储这些数据,4.0后改进为可选择的。
附加信息Payloads
对于不需要在搜索反向索引时用到,但在搜索结果处理时需要的位置、偏移
量等字段,我们可以单独为该字段存储(文档id–>词项
向量)的正向索引,即附加信息Payloads。
FieldType实现类中有对应的set方法:
boolean storeTermVectors() 是否存储词项向量
boolean storeTermVectorPositions() 是否在词项向量中存储位置
boolean storeTermVectorOffsets() 是否在词项向量中存储偏移量
boolean storeTermVectorPayloads() 是否在词项向量中存储附加信息
支持排序
我们往往需要对搜索的结果支持按不同的字段进行排序,如商品搜索
结果按价格排序、按销量排序等。以及对搜索结果进行按某字段分组统计,如
按品牌统计。
空间换时间:对这种需要排序、分组、聚合的字段,为其建立独立的文档->字段值的正向索引、列式存储。这样我们要加载搜中文档的这个字段的数据就快很多,耗内存少。
IndexableFieldType 中的 docValuesType方法 就是让你来为需要排序、分组、
聚合的字段指定如何为该字段创建文档->字段值的正向索引的。
DocValuesType选项如下:
- NONE 不开启docvalue
- NUMERIC 单值、数值字段,用这个
- BINARY 单值、字节数组字段用
- SORTED 单值、字符字段用, 会预先对值字节进行排序、去重存储
- SORTED_NUMERIC 单值、数值数组字段用,会预先对数值数组进行排序
- SORTED_SET 多值字段用,会预先对值字节进行排序、去重存储
具体使用选择:
- 字符串+单值 会选择SORTED作为docvalue存储
- 字符串+多值 会选择SORTED_SET作为docvalue存储
- 数值或日期或枚举字段+单值 会选择NUMERIC 作为docvalue存储
- 数值或日期或枚举字段+多值 会选择SORTED_SET作为docvalue存储
注意:
- DocValuesType是强类型要求的,字段的值必须保证同类型。
- 需要排序、分组、聚合、分类查询(面查询)的字段才创建docValues
Lucene所有字段子类
Lucene内部提供了很多Field的子类,可以根据实际情况进行灵活选择。
- 如果单个Field无法满足需求,还可以进行多个Field组合。
- 如果连组合都满足不了,还可以直接用Filed+FiledType的方式
Luke索引查看工具
下载地址:https://github.com/DmitryKey/luke/releases
当前最新版本7.3.1,可用于Lucene7.3.0版本
该工具可以用来查看我们创建索引后的结果。
索引更新
主要使用IndexWriter的API方法进行更新
- 删除流程:根据Term(词项即字段)、Query找到相关的文档id、同时删除索引信息,再根据文档id删除对应的文档存储
- 更新流程:先删除、再加入新的doc
- 注意:只能根据有所有的Term才能进行删除和更新