基本上创建索引需要三个步骤:
1、创建索引库IndexWriter对象
2、根据文件创建文档Document
3、向索引库中写入文档内容
这其中主要涉及到了IndexWriter(索引的核心组件,用于创建或追加索引)、Document(代表一些域Field的集合)、Field(具体的域,如文档创建时间、作者、内容等)、Analyzer(分词器)、Directory(用于描述索引存放位置)这些主要的类。
我们参照上一节的代码来看建立索引。
1、创建IndexWriter
// 索引文件的保存位置
Directory dir = FSDirectory.open(new File(indexStorePath));
// 分析器
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_4_9);
// 配置类
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_4_9, analyzer);
iwc.setOpenMode(OpenMode.CREATE);// 创建模式 OpenMode.CREATE_OR_APPEND 添加模式
IndexWriter writer = new IndexWriter(dir, iwc);
|
这里先是定义存放索引文件的位置,然后定义分析器(分词器),我这里没有使用昨天提到的一个官方的smartcn那个jar的分词器,自己试了一下,发现不是很好用,居然是搜索失败的。不过中文的话,应该还是IK Analyzer比较好吧。
StandardAnalyzer是 lucene 中内置的“标准分析器”,可以做如下功能:
l 对原有句子按照空格进行了分词
l 所有的大写字母都可以能转换为小写的字母
l 可以去掉一些没有用处的单词,例如”is”,”the”,”are”等单词,也删除了所有的标点
当然这里也可以一些其他的分词器,Lucene中也自带了中文的分词器,是lucene-analyzers-smartcn.Jar中的SmartChineseAnalyzer,但是我用了一下发现对中文的处理并不好。
当然其他的中文分词器还有很流行的IKAnalyzer,这个一定要用FF的版本,意思是for four才是给我们4.x用的。
在下面是定义配置类,这里有个setOpenMode,有下面几个选项:
APPEND:总是追加,可能会导致错误,索引还会重复,导致返回多次结果
CREATE:清空重建(推荐)
CREATE_OR_APPEND【默认】:创建或追加
这里还是建议先用CREATE吧,当然学习的深入了之后使用CREATE_OR_APPEND可能会让索引创建效率更高。
另外,IndexWriter的创建也是相当耗费资源的,所以如有可能,尽量使用单例(注意线程 安全 )。
2、根据数据创建文档
首先说下Field、Document、索引之间的关系,简单一句话:多个Field组成一个Document,多个Document组成一个索引。
在创建索引的过程中比较重要的就是创建不同的Field,看看api就可以知道Field有哪些实现:BinaryDocValuesField, DoubleField, FloatField, IntField, LongField, NumericDocValuesField, SortedDocValuesField, SortedNumericDocValuesField, SortedSetDocValuesField, StoredField, StringField, TextField。
比较常用的有LongField、StoredField、StringField、TextField。具体有哪些构造方法建议大家自己查阅api文档。
每种Field都有具体的介绍,我这里重点介绍一下常用的几个:
LongField:索引但是不分词,适用于全部搜索,一般用于文件创建、修改时间的秒数等。
StoredField:只存不索引
StringField:索引但是不分词,所以适用于全部搜索的内容,比如国家等,要么不对,要么就是全对。另外提一句,这个有个长度限制是32766,大家使用的时候自己斟酌一下,一般不会超过。
TextField:索引并分词,所以这个应该是我们做 全文检索 的时候应该最常用到的一个Field。
Document doc = new Document();
FileInputStream is = new FileInputStream(f);
Reader reader = new BufferedReader(new InputStreamReader(is));
// 字符串 StringField LongField TextField
Field pathField = new StringField("path", f.getAbsolutePath(), Field.Store.YES);
Field contenField = new TextField("contents", reader);
// 添加字段
doc.add(contenField);
doc.add(pathField);
|
这样首先声明一个Document,然后声明一系列Field,然后添加字段即可。
这里有个重点就是Field,其实也比较简单。在4.x以前是没有各种各样的Field的,都是通过传参,现在就已经有了不同的实现,可以按需选择了。
StringField即为NOT_ANALYZED的(即不对域的内容进行分割分析),而TextField是ANALYZED的,因此,创建Field对象时,无需再指定分析类型了。
下面是官方描述:
StringField: A field that is indexed but not tokenized
TextField: A field that is indexed and tokenized
下面介绍一些有关Field的相关选项
a. Field.Store.Yes/No
在创建Field的时候,需要传入一个参数,来指定内容是否需要存储到索引中。这些被存储的内容可以在搜索结果中返回,用于展现显示,即使用document.get(“name”)时,是否可以直接返回内容。
一般文件的作者、创建时间、文件名等信息可以存储,但是内容就没必要存储了,一方面是内容太多太大,另一方面也是因为我们的程序可以引导用户直接访问原文件查看即可。
b.加权(后面会细说)
可以对Filed及Document进行加权。注意加权是影响返回结果顺序的一个因素,但也仅仅是一个因素,它和其它因素一起构成了Lucene的排序 算法 。
下面继续介绍一个对富文本(非纯文本)索引的方法。
对于word,excel,pdf等富文本,FileReader读取到的会是 乱码 ,无法有效的索引。这时候可以使用Tika等工具先将其正文内容提取出来,然后再进行索引。
使用Tika进行正文提取的方法见我的github:
https://github.com/irfen/lucene-example/blob/master/src/test/java/me/irfen/lucene/ch03/TikaTest.java
更多Tika内容以后如果有时间会详细介绍。这里我使用的是Tika1.5版本,他依赖的包有点多,使用maven进行构建只需要如下两个配置即可:
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers</artifactId>
<version>1.5</version>
</dependency>
|
3、向索引库中写入文档内容
这一步很简单,直接就是writer.addDocument(doc);就可以了。
这里简单介绍下对索引的优化,索引过程中,会将索引结果放到多个索引文件中,这样会回收索引的效率,但在搜索时,需要将多个索引文件中的返回结果并进行合并处理,因此效率较低。
在添加文档之后执行writer.forceMerge(2);,索引的优化是将索引结果文件归为一个或者有限的多个,它加大索引过程中的耗时(降低了效率),减少了搜索时的耗时(提高了效率)。
4、关于Directory
目前用的比较多的有两个Directory,一个是FSDirectory,一个是RAMDirectory。
看一眼FSDirectory的源码,会发现下面这段:
public static FSDirectory open(File path, LockFactory lockFactory) throws IOException {
if (Constants.JRE_IS_64BIT && MMapDirectory.UNMAP_SUPPORTED) {
return new MMapDirectory(path, lockFactory);
} else if (Constants.WINDOWS) {
return new SimpleFSDirectory(path, lockFactory);
} else {
return new NIOFSDirectory(path, lockFactory);
}
}
|
其实FSDirectory是根据不同的操作系统和JRE返回不同的Directory。
而另一个常用的是RAMDirectory,这个是内存索引,只对小索引好用,大量索引会导致频繁GC。
另外介绍一下FileSwitchDirectory,这是一个基于文件目录切换的一个实现。有时候我们想要同时获得两个Directory的优点,这就是FileSwitchDirectory的作用。下面是他的构造方法:
public FileSwitchDirectory(Set primaryExtensions, Directory primaryDir, Directory secondaryDir, boolean doClose) {
this.primaryExtensions = primaryExtensions;
this.primaryDir = primaryDir;
this.secondaryDir = secondaryDir;
this.doClose = doClose;
this.lockFactory = primaryDir.getLockFactory();
}
|
这里可以看到他要两个Directory,通过第一个参数我们可以指定主索引需要加载的索引文件,其它的将会由从Directory来实现,由此达到快速切换不同的Directory来使用他们各自的优点。