Eclipse-Eclipse的帮助系统的搜索功能。
|
数据库 |
Lucene |
概念 |
列/字段 |
Filed |
行/记录 |
Doucument |
|
查询(SELECT) |
Searcher |
|
操作 |
添加(INSERT) |
IndexWriter.addDocument |
删除(DELETE) |
IndexWriter.delete |
|
修改(UPDATE) |
不支持(可删除后重新添加) |
1)设有两篇文章1和2
文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too.
文章2的内容为:He once lived in Shanghai.
2)由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施:
a.我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。
b.文章中的”in”, “once” “too”等词没有什么实际意义,中文中的 “的” “是”等字通常也无具体含义,这些不代表概念的词可以过滤掉
c.用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。
d.用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live”
e.文章中的标点符号通常不表示某种概念,也可以过滤掉
3)在lucene中由Analyzer类完成经过上面处后
文章1的所有关键词为:[tom][live] [guangzhou] [i] [live][guangzhou]
文章2的所有关键词为:[he][live] [shanghai]
4)有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。文章1,2经过倒排后变成
关键词 文章号
guangzhou1
he 2
i1
live 1,2
shanghai 2
tom 1
5).通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:
a)字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快)
b)关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置
6)加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
关键词 文章号[出现频率] 出现位置
guangzhou1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1
以live这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,5”就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“2”就表示live是文章2中第 2个关键字。
以上就是lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(lucene没有使用B树结构),因此lucene可以用二元搜索算法快速定位关键词。
实现时 lucene将上面三列分别作为词典文件(TermDictionary)、频率文件(frequencies)、位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。
为了减小索引文件的大小,Lucene对索引还使用了压缩技术。首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减小数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)
假设要查询单词 “live”,lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。
而用普通的顺序匹配算法,不建索引,对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。
由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%"and like "%keyword2%" ...其效率也就可想而知了。
所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。
由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。
现在的Field构造函数原型是如下样子的:
public Field(Stringname, String value, Store store, Index index)
lucene的score其实是 tf * idf * Boost * lengthNorm 计算得来
tf : 查询的词在文档中出现的次数的平方根
idf:反转文档频率
boots:激励因子,可通过setBoots方法设置,通过filed 和 document都可以设置,所设置的值会同时起作用
lengthNorm: 由搜索的filed的长度觉得,越长文档的分值越低
控制score就是设置 boots的值
lucene会把计算后,最大分值超过1.0的分值作为分母,其他文档的分值都除以这个最大值,计算出最终的得分。
例子:
package com.firstproject.testindex; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; public class UpdateDocument { public static void main(String[] args) throws IOException { Analyzer analyzer=new StandardAnalyzer(); String indexDir="d:/luceneindex"; Directory dir=FSDirectory.getDirectory(indexDir); IndexReader reader=IndexReader.open(dir); System.out.println("before delete : "+reader.numDocs()); reader.deleteDocuments(new Term("id","2")); System.out.println("after delete : "+reader.numDocs()); reader.close(); IndexWriter writer=new IndexWriter(dir,analyzer,true,IndexWriter.MaxFieldLength.LIMITED); Document document=new Document(); Field field1=new Field("id","2",Field.Store.YES,Field.Index.ANALYZED); field1.setBoost(1.5f); document.add(field1); document.add(new Field("name","Tom",Field.Store.YES,Field.Index.NO)); document.add(new Field("address","tianjin",Field.Store.YES,Field.Index.ANALYZED)); // document.setBoost(1.5f);//默认1.0,大于1.0,比较重要 document.setBoost(0.5f);//不重要 writer.addDocument(document); writer.close(); reader=IndexReader.open(dir); System.out.println("after add : "+reader.numDocs()); reader.close(); dir.close(); } }
IndexWriter Method |
Default Value |
Description |
setMaxBufferedDocs |
16M |
Determines the amout of RAM that May be used for buffering added documents before they are flushed as a new segment |
setMergeFactor |
10 |
Controls segment merge frequency and size |
setMaxMergeDocs |
Integer MAX_VALUE |
Limit the number of documents per segment |
package com.lucene.test.T01; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; public class TestIndex { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { String[] ids = { "1", "2", "3", "4" }; String[] names = { "zhangsan", "lisi", "wangwu", "zhaoliu" }; // String[] names = { "zhangsan", "zhangsun", "zhangson", "zhaoliu" }; String[] address = { "shanghai", "beijing", "guangzhou", "beijing" }; String[] birthday = { "19880101", "19860105", "19760205", "19550719" }; Analyzer analyzer = new StandardAnalyzer(); String indexDir = "d:/temp/luceneindex"; Directory dir = FSDirectory.getDirectory(indexDir); // true 表示创建或覆盖当前索引;false表示对当前索引进行追加 // Default value is 128 IndexWriter writer = new IndexWriter(dir, analyzer, true, IndexWriter.MaxFieldLength.LIMITED); for (int i = 0; i < ids.length; i++) { Document document = new Document(); document.add(new Field("id", ids[i], Field.Store.YES, Field.Index.ANALYZED)); document.add(new Field("name", names[i], Field.Store.YES, Field.Index.ANALYZED)); // Field.Index.NO表示不建立索引 document.add(new Field("address", address[i], Field.Store.YES, Field.Index.NO)); document.add(new Field("birthday", birthday[i], Field.Store.YES, Field.Index.ANALYZED)); writer.addDocument(document); } writer.optimize(); writer.close(); } }