构建索引过程
文档是Lucene索引和被搜索的最小单位,一个文档包含一个或者多个域,而域则包含了真正 被搜索 的内容,每个域都有一个标识名称(如标题,描述)对应一个值(比如 标题:lucene)
建立了文档和域以后,就可以调用IndexWriter的addDocument方法
倒排索引
其实是表示 ,哪些文档包含单词X ,而不是这个文档包含哪些单词
基础入门
既然是索引,那么肯定需要有地方存储他们,Lucene提供了多种存储索引的方式,Lucene在文件系统中存储索引的最基本的抽象实现类是BaseDirectory,其中最常使用的是FSDirectory 和 RAMDirectory,前者是主要用来存储到文件到文件系统(其中有几个子类,实现不同的存储策略,包括Nio,内存映射等等),后者是直接存储到内存中,适合小型应用或者实验学习性质的Demo,如果数据量较大的话,内存会吃不住的(官方文档表示,20G原始数据,大概需要4-6GB的索引结构数据)
使用如下:
valindexConfig:IndexWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)
// indexConfig.setInfoStream(System.out)
val directory:Directory = FSDirectory.open(Paths.get(indexPath))
val indexWriter:IndexWriter = new IndexWriter(directory,indexConfig)
这样就成功实例化了一个IndexWriter,可以对索引进行写操作,IndexWriter负责创建索引和打开已经存在的索引,向其中更新和删除索引,不能够用作读,如果开辟内存空间,则需要Directory来完成,因为底层的IO抽象在Directory中,不同的场景需要使用不同的Directory实现。同时,也要对IndexWriter进行一些配置,比如设定分析器为 StandardAnalyzer ,设定文件操作模式等等,这需要使用IndexWriterConfig 类(具体操作文档 http://lucene.apache.org/core/6_4_0/core/org/apache/lucene/index/IndexWriterConfig.html)
索引写入对象准备好以后,就可以开始构建索引了,,要构建索引,首先要了解索引的相关概念,在Lucene中索引相关的概念如下:
Lucene概念
传统数据库概念
备注
IndexSearcher
table,读取的句柄
IndexWriter
table,写入的句柄
Directory
底层IO写入的句柄
描述了Lucene索引的存放位置,它的子类负责具体指定索引的存储路径
派生出
FSDirectory,RAMDirectory等
DirectoryReader
底层IO读取句柄 读取Directory
Document
一条记录
代表一些域(Field)的集合,你可以将Document对象理解为虚拟文档-例如Web页面、E-mail信息或者文本文件
Field
每个字段
分为可被索引的,可切分的,不可被切分的,不可被索引的几种组合类型
Hits
RecoreSet
结果集
Analyzer
分析器
负责文本分析,从被索引文本文件中提取出语汇单元。对于文本分析器Analyzer,需要注意一点,就是使用哪种Analyzer进行索引创建,查询的时候也要使用哪种Analyzer查询,否则查询结果不正确。
FieldType
域类型,每一个Field存储类型
描述了Field的各种属性,在不使用某种具体的Field类型(例如StringField,TextField)时需要用到此类
也就是说一条索引的记录就是一个document,比如某篇文章,它的全部信息就可以看作为一个document,而其中的作者,标题,编号,摘要就可以看做是各个Field,如果需要写索引,就要通过IndexWriter实例化的对象去操作。如果需要搜索结果,就需要 IndexSearcher 实例,搜索后得到hits结果集。
创建索引
创建索引具体实现的代码如下:
def createIndex(mapList: Array[Map[String,String]]): Unit ={
val fieldType = new FieldType()
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)
fieldType.setStored(true)
fieldType.setTokenized(true)
for(a <- 0 until mapList.length){
var documentation = new Document();
var single = mapList(a)
documentation.add(new Field("goods_id",single("goods_id"),fieldType))
documentation.add(new Field("goods_name",single("goods_name"),fieldType))
documentation.add(new Field("goods_price",single("goods_price"),fieldType))
documentation.add(new Field("goods_seller",single("goods_seller"),fieldType))
indexWriter.addDocument(documentation)
}
indexWriter.commit()
}
//测试调用
deftestCreateIndex(): Unit ={
val index = new Index("./index_store")
val arrayList = Array(
Map("goods_id" -> "sa2a", "goods_name" -> "xs", "goods_price" -> "19.22", "goods_seller" -> "A&TT")
)
index.createIndex(arrayList)
index.close()
}
一个域是属于一个document的,一个document可以包含多个域,可以把document理解为数据库中的一行,而Field是其中的字段,操作如下:
var documentation = new Document();
documentation.add(new Field("goods_id",single("goods_id"),fieldType))
document添加的时候接受一个实现 IndexableField 接口对象,Field 实现了 IndexableField,所以直接创建一个Field即可,完成document操作后,将其写入到索引中,并提交:
indexWriter.addDocument(documentation)
indexWriter.commit()
这样就是一个完整的索引建立过程
域类型
首先先创建了FieldType,定义Field的类型,这里定义为以为存储和Tokenized。
这些字段类型可以由几个成员函数调用来配置
1.Stored表示要存储到索引中,要配置为Stored,调用 setStored(boolean value) ,通常我们只存储一些短小精悍且必要的字段,像标题,id,url这种,而文章正文这样的大篇幅数据一般不存储与索引。
- 如果要这个Field的值在进入之前先通过分析器过滤调用setTokenized(boolean value)
3.设定索引的类型使用 setIndexOptions(IndexOptions value) ,需要传入索引参数,是一个枚举,有这些值如下
DOCS
只有文档会被索引,词频和位置都会被省略
DOCS_AND_FREQS
文档和词频被索引,位置省略
DOCS_AND_FREQS_AND_POSITIONS
文档 词频 位置都被索引
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS
除了文档和词频位置还有偏移量也会被索引
NONE
不索引
有些字段我们需要在查询的时候返回,但不希望它进入索引影响查询效率,可以用setIndexOptions设为False,同时有些词我们只需要对其进行过滤即可,比如权限和时间过滤,这种值我们不太需要记录其出现的频率(词频)和位置(偏移量),所以只需要DOCS级别即可,通常对于要索引的字段我们都设置为DOCS_AND_FREQS_AND_POSITIONS.
汇总起来,操作代码如下:
val fieldType = new FieldType()
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)
fieldType.setStored(true)
fieldType.setTokenized(true)
设定好FieldType后,就可以用类型去创建域,代码如下
var f1 = new Field("goods_id",single("goods_id"),fieldType)
注意,老版本的Lucene是通过存储选项+索引选项+项向量组合起来决定一个Field的性质,调用起来比较复杂,新版使用一个新的类FieldType并使用一些方法来设定,比较清晰和方便
多值域问题
在某些场景下,我们需要一个作用域有多个值,比如作者信息,很多时候作者不止一个人,需要向一个Field里面写入多个值,这种情况只需要直接向一个Document写入多个相同名字相同但是值不同的Field即可,至于这些Field该如何定义优先级,可以在分析的时候进行干预(见后文)。
Lucene处理Field的时候还设计了针对多种IO的构造函数,除了string外,TokenStream/Reader还可以针对占用内存空间较大的Field进行分析,避免一次读入占用内存
加权
加权操作是认为的对结果进行干预,可以在索引期间完成,也可以在搜索期间完成,这里着重描述如何在索引期间加权。
调用加权的操作在3.0+版本上可以在一个Document上面进行,但在6.0+版本上只在文档中找到了基于Field的操作,如下
var goods_id = new Field("goods_id",single("goods_id"),fieldType)
goods_id.setBoost(1.5F)
加权值高的Field会更比较低的更加优先被搜索到,Lucene通过查询语句的匹配程度来对搜索结果进行排名,每个匹配的文档都有一个评分,加权数是评分的一个重要因素。
Lucene会基于域的语汇单元来计算加权值(更短的域有较高的加权,这里隐含了如果越短,则优先级可能越高),这些加权会被合并量化为一个单一的字节值(加权基准Norms),并且存储,在搜索的时候被加载到内存,还原为浮点数,然后用于计算评分。Norms可以在搜索时候用IndexReader.setNorm进行修改.
对于域比较多的文档来说,加载norms信息会占用大量内存空间,可以在FieldType进行设定,关闭norms相关操作。
fieldType.setOmitNorms(false)
索引非字符串类型
很多场景下都需要索引数字类型,比如价格,时间等,一种情况是数字包含在文字中比如‘我买50块钱的东西’,50要被索引,需要选择一个不丢弃数字的分析器,比如StandardAnalyzer(而SimpleAnalyzer和StopAnalyzer是反例,他们会剔除数字),这样就可以达到想要的目标。另一种情况是我们直接就想索引一个数字,这就需要使用IntPoint等数据类型,他们是Field派生出的子类,标准文档上给出了这些Field:
BinaryDocValuesField, BinaryPoint, DoublePoint, FloatPoint, IntPoint, LegacyDoubleField, LegacyFloatField, LegacyIntField, LegacyLongField, LongPoint, NumericDocValuesField, SortedDocValuesField, SortedNumericDocValuesField, SortedSetDocValuesField, StoredField, StringField, TextField
var price = new IntPoint("price",15,fieldType)
price.setIntValues(15)//也可以通过这种方式进行修改
由于这些类型都是继承Field,所以执行Field相关的方法,同上,我们也可以对一个域添加多个值,在搜索的时候对这些值的处理方式是or关系,且排序是不确定的。int也可以处理时间,将时间转换为Int即可(更精确的时间可以使用LongPoint)。
Field截取
对于一些尺寸未知的文件,我们需要进行截取,从而控制内存和硬盘的使用量,对一个IndexWriter调用API来实现setMaxFieldLength todo找到文档
实时搜索
很多时候修改了索引以后需要马上看到效果,但从新New一个IndexReader会非常的耗时,3.0+版本让我们使用indexWriter . getReader(),但目前这个接口已经被标记为废弃
getReader(int termInfosIndexDivisor)
Deprecated. Please use IndexReader.open(IndexWriter,boolean) instead. Furthermore, this method cannot guarantee the reader (and its sub-readers) will be opened with the termInfosIndexDivisor setting because some of them may have already been opened according to IndexWriterConfig.setReaderTermsIndexDivisor(int). You should set the requested termInfosIndexDivisor through IndexWriterConfig.setReaderTermsIndexDivisor(int) and use getReader().
我们直接使用IndexReader从新打开IndexWriter即可。
索引优化
当多次对一个索引进行写操作时,会产生很多独立的段,当搜索时,lucene必须单独搜索每个段,然后合并段的搜索结果,当处理大量数据时,需要尽量合并这些段,早起indexWriter提供了一些api,不过现在已经废弃,参考https://lucene.apache.org/core/3_5_0/api/core/org/apache/lucene/index/IndexWriter.html#optimize(),目前indexWriter/IndexReader会自动进行优化。
Directory子类介绍
前面我们简单介绍了Directory,这里深入描述下几种Directory子类的差别和使用范围:
- SimpleFSDirectory: 直接使用java.io操作文件系统,不能很好的支持多线程,如要要做到就必须使用外部加锁,并且不支持按位置读取。
2.NIOFSDirectory:使用java nio进行文件操作,异步进行,可以很好的支持多线程读取
3.MMapDirectory:内存映射io进行文件访问,不需要用锁机制就可以在多线程下很好的运行,但由于内存映射IO消耗的地址空间和索引尺寸是相等,所以在32位jvm上比较鸡肋(也就是说索引最多只能4G),推荐64位使用,不过由于JVM没有取消映射关系的机制,所以只有在垃圾回收的时候,才会释放内存空间和文件描述符,会造成一些困扰。
4.RAMDirectory:直接存在内存,实验用。
作用用户调用我们不用太纠结选择,直接使用FSDirectory即可,他会根据当前环境来选择最适合的方式,如果需要自定,可以考虑自己实例化相关的类。
并发
Lucene在并发方面有以下的特性:
1.任意只读的IndexReader可以同时打开一个索引,无论这些Reader是否在一台机器上。最好的办法是一个jvm内只有一个Reader,多个线程共享进行搜索
2.对于一个索引一次只能打开一个Writer,Lucene提供一个文件锁来保障这一特性。
3.IndexReader可以在Writer进行写入的时候打开,他可以看到IndexWriter提交之前的数据。
4.IndexReader是线程安全的
可以通过IndexWriter.isLocked来判断是否有锁,也有一些方法来自定义锁的实现。
参考:
Lucene6.4文档 http://lucene.apache.org/core/6_4_0/core/index.html
Lucene3.5文档 https://lucene.apache.org/core/3_5_0/api/core/overview-summary.html