Lucene从入门到熟悉(一)概念&建立索引

Lucene 是一个基于 Java 的全文信息检索工具包,它不是一个完整的搜索应用程序,而是为你的应用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta 家族中的一个开源项目。也是目前最为流行的基于 Java 开源全文检索工具包。

优点:

高效-信息检索 (Information Retrieval)
成熟-经过多个项目检验
免费-开源(open-source project in Java)

Lucene经典应用

Eclipse-Eclipse的帮助系统的搜索功能。

Jive-一个广受欢迎的开放的源码的论坛项目,其目标是建设一个开放结构的、强壮的、易于扩展的基于JSP的论坛。Jive的搜索使用了lucene搜索引擎。
Cocoon-基于XML的web发布框架,全文检索部分使用了Lucene。

Lucene 索引创建读取架构图



系统架构

.核心索引类

IndexWriter :建立索引的核心组件。使用 IndexWriter 可以新建一个索引并将对象文件逐一添加到索引当中,但不可以执行读取和搜索操作。
Directory :代表一个 lucene 索引项的位置。这是一个抽象类,其具体实现有 FSDirectory和 RAMDirectory。前者将索引写入硬盘,对应于真实的文件系统路径,后者则将索引写入内存,相比于前者效率高但可用空间小。
Analyzer :对文本内容进行分析的抽象类,具体实现中有停用词切除、词干分析、大小写切换等功能。
Document :可以视作文本经过处理后所对应的对象,由多个字段组成,如路径、标题、摘要、修改日期等等。
IndexSearcher :检索操作的核心组件,用于对 IndexWriter 创建的索引执行,只读的检索操作,工作模式为接收 Query 对象而返回 Hits 对象。
Term :检索的基本单元,标示检索的字段名称和检索对象的值,如Term( “title”, “lucene” )。即表示在 title 字段中搜寻关键词 lucene 。
Query :表示查询的抽象类,由相应的 Term 来标识。
TermQuery :最基本的查询类型,用于匹配含有指定值字段的文档。
Hits :用来装载搜索结果文档队列指针的容器。

Lucene 与数据库对比

 

数据库

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先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。

而用普通的顺序匹配算法,不建索引,对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。 

高效-通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页, 上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是 它是排好序的。对于 检索系统来说其核心是一个排序问题。  



全文检索 ≠ like "%keyword%"

   由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%"and like "%keyword2%" ...其效率也就可想而知了。   


采用反向索引机制

   所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。

非常不确定的问题

  由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。   

Lucene索引建立

大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。



                                      Lucene 索引合并过程

IndexWriter :建立索引的核心组件。

Directory:代表一个 lucene 索引项的位置。

Analyzer :对文本内容进行分析的抽象类,具体实现中有停用词切除、词干分析、大小写切换等功能。

Document :可以视作文本经过处理后所对应的对象,由多个字段组成,如路径、标题、摘要、修改日期等等。

Field :字段,对应于文本的某一部分数据,便于检索时根据结果提取。早期版本分为四个类型: Keyword 、 UnIndexed 、 UnStored 和 Text ,其主要区别归结于三个方面:是否被分析,是否被索引,是否存储于索引中。但是在最新版本的 Lucene中,使用了一种更为统一的形式,也即只有Field一个类,然后使用一些参数来描述这个字段的属性,通过参数组合,可以组合出各种类别,甚至那四种不存在的类别理论上也是可以组合出来。

现在的Field构造函数原型是如下样子的:

   public Field(Stringname, String value, Store store, Index index)

Lucene 底层打分机制

 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();
	}
}


Lucen索引效率设置

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();

	}
}


通过Document对象将数据读取,再通过IndexWriter建立索引

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值