全文检索引擎的开放源代码Lucene
2010-10-25
Lucene 是一个开放源代码、高性能的Java 全文检索工具包。
1 全文检索引擎lucene
1 .1 Lucene 简介
Lucene 是apache 软件基金会jakarta 项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,即它不是一个的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,它为数据访问和管理提供了简单的函数调用接口,可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。Lucene 的APl 接口设计得比较通用,输入输出结构都很像数据库的表、记录和字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射至lJLucene 的存储结构或接口中。总体上看,可以先把Lucene 当成一个支持全文索引的数据库系统。
Lucene 的原作者是Doug Cutting ,他是一位资深全文索引/ 检索专家,曾经是V-Twin 搜索引擎[6] 的主要
开发者,后在Excite[7] 担任高级系统架构设计师,目前从事于一些Internet 底层架构的研究。早先发布
在作者自己的 http://www.lucene.com/ ,后来发布在 SourceForge [8] ,2001 年年底成为apache 软件基金
会jakarta 的一个子项目: http://jakarta.apache.org/lucene/ 。
Lucene 目前是 Apache Jakarta 家族中的一个开源项目,下载 http://lucene.apache.org/
1 .2 Lucene 系统结构
Lucene 作为~个优秀的全文检索引擎,其系统结构运用了大量的面向对象的设计思想。首先是定义了一个与平台无关的索引文件格式,其次通过抽象将系统的核心组成部分设
计为抽象类,具体的平台部分设计为抽象类,此外与具体平台相关的部分比如文件存储也封装为类,经过层层的面向对象编程的处理,最终达成了一个低耦合、高效率、容易二次开发
等的检索引擎系统。Lucene 系统其结构如图1 所示。可以从图1 清楚地看到,Lucene 系统是由基础结构封装、索引核心、对外接口3 大部分组成。其中直接操作索引文件的索引核心又是系统的重点,索引的最后结果就是产生许多的索引文件,这些索引文件构成索引库。
Lucene 系统将所有源码分为了7 个模块( 在Java 语言中以包即package 来表示) ,各个模块( 包) 完成特定的功能。在Lucene 系统的这7 个包中,核心类包主要有3 个(org .apache .1ucene .analysis 、org .apache.1ucene .index 、org .apache .1ucene .search) :
(1)org .apache .1ucene .analysis 该模块主要用于切分词。切分词的工作由Analyzer 的扩展类来实现,Lucene 自带了Stan .dardAnalyzer 类,我们可以参照该类的实现写出自己的切分词分析器类,如中文分析器等。
(2)org .apache .1ucene .index 该模块主要提供库的读写接口。通过该包可以创建库、添加删除记录及读取记录等。
(3)org .apache .hcene .search 该模块主要提供了检索接口。通过该包,我们可以输入条件,得到查询结果集,与org .apache .1ucene .queryParser 包配合还可以自定义查询规则,像google 一样支持查询条件问的与、或、非、属于等复合查询。
1 .3 Lucene 程序运行机制
Lucene 系统,其功能强大,实现复杂,但从根本上说,主要包括两个主要功能:一是建立索引库,也就是将待索引的纯文本内容经切分词后的索引入库;二是检索索引库,即根据查询
条件从索引库中找出符合条件的文档。
1 .3 .1 建立索引逻辑库
建立索引逻辑库按先后顺序,其查询逻辑如下:
首先,建库者定义入库文档的结构,比如需要把网站内容加载到全文检索库,让用户通过“站内检索”搜索到相关的网页内容。入库文档结构与关系型数据库中表的行结构类似,
每个入库的文档由多个域构成,假设这里需要入库的网站内容包括如下项目:文章标题、作者、发布时间、原文链接、正文内容( 一般作为网页快照) ,那么每个网页的这些项目可以作为文档的域,共有5 个域。
其次,对于文档中需要切分词的域,系统使用语言解析器(hcene .analysis) 对其切分词,形成一个个Token,Token 是Lucene 内部所使用的概念,是对传统文字中的词的概念的抽象,
也是Lucene 在建立索引时直接处理的最小单位;简单的讲Token 就是一个词和所在域值的组合。
最后,切分后的Token 通过索引器(1ucene .index) 的处理,最终添加( 插入) 到索引库中,存储器(1ucene.store) 负责数据存储管理,主要包括一些底层的I /O 操作。
1 .3 .2 检索索引逻辑库
建立检索索引逻辑库按先后顺序,查询逻辑如下:
首先,输入查询条件,比如用户希望查询到含有词“china ”
和“panda ”但不含词“cat ”的记录,那么输入条件为“china+panda-cat ”;查询条件传入搜索器(1ucene.search) ,搜索器里有一个查询解析器(incene .queryParser) ,搜索器调用这个查询解析器来解析查询条件。
其次,查询条件“china+panda-cat" 被传到查询解析器中,解析器将对“china+panda-cat" 进行分析,首先分析器解析字符串的连接符,即加号和减号,然后调用语言解析器(1ucene .analysis) 对每个词进行切词,一般英文将按空格来切词,最后得到的查询条件表示为:“china"AND “panda"AND NOT “cat ”。
最后,查询器根据这个条件去检索事先已建立好的索引库,得到查询结果,并返回结果集lucene .search.Hits ,Hits 类似于JDBC 中的ResultSet 。
2 基于Lucene 的搜索引擎设计与实现
下面介绍一个采用Lucene 全文检索技术实现的一个校园网站内搜索引擎,通过介绍该搜索引擎的设计与实现来展示如何运用Lucene 。
2 .1 系统设计要求
一个校园网站的搜索引擎应该满足下列要求:①以校园网为搜索目标,用户可以通过该系统检索校园网站上所有静态网页的内容和大多数动态网页的内容,提供基于Web 的查
询接口;②具有较高的查询准确率和较快的响应速度。
2 .2 系统设计与实现
基于Lucene 全文检索搜索引擎的系统结构图如图2 所示。
上述系统分两大部分:创建维护索引与检索索引,本文将主要介绍与Lucene 相关的设计与实现,由于Lucene不是一个完整的全文检索引擎,而是一个全文检索引擎的工具
包,所以需要利用工具包提供的类,并对其扩展来实现具体的应用。
2 .2 .1 建立索引库
建立索引库就是往索引库添加一条条索引记录,Lucene 为添加一条索引记录提供了接口,添加索引。
主要用到了“写索引器”、“文档”、“域”这3 个类。
要建立索引,首先要构造一个Document 文档对象,确定Document 的各个域,这类似于关系型数据库中表结构的建立,Document 相当于表中的一个记录行,域相当于一行中的列,在
Lucene 中针对不同域的属性和数据输出的需求,对域还可以选择不同的索引/存储字段规则,在本系统中,网页标题、内容、URL 和建立日期作为Document 的域。IndexWriter 负责接收新加入的文档,并写入索引库中。在创建“写索引器”IndexWriter 时需要指定所使用的语言
分析器,Lucene 从1 .3 版开始支持中文,它通过个包org .apache .1ucene .analysis .standard 来支持中文,它对汉字采用单字切分,这种切分方法实现简单,但检索准确率不高,对于本系统来讲足够了,当然也可以通过扩展Analyzer 类来开发自己的语言所在的目录,IndexSearcher 有一个search 方法执行索引的检索,这个方法接受Query 作为参数,返回Hits ,Hists 是一系列排好序的查询结果的集合,集合的元素是Document 。通过Document 的get 方法可以得到与这个文档对应网页的信息,比如:网页标题,URL 等。
这些信息经过处理显示给用户。
下面是索引检索的示例代码片断:
……
Searcher searcher=new IndexSearcher( ”E :/index ”) :
Analyzer analyzer=new SimpleAnalyzer0 ;
Query query=QueryParser .parse(1ine ,”Fulllndex",analyzer) ;
Hits hits=searcher .search(query) ;
for(int i-O ;i<hits .1ength0 ;i++){
Document doe=hits .doe(i) ;
String path=doe .get( ”Ud ”) ;
String title=doe .get( ”Title ”) ;
String content--doe .get( ”Content ”) :
String pubTime=doc .get( ”PubTime ”) ;
……
2 .3 系统特点
本系统在开放源码工具包Lucene 的基础上,根据系统特点和要求,按照Lucene 的框架规范,扩展Lucene 的功能,将Lucene 很好地嵌入到搜索引擎中。系统具有Lucene 的技术特点,全文检索效率高,查全率和准确率都达到了设计的要求,系统运行稳定,查询精确,支持跨平台,多用户。
Lucene 作为一个全文检索引擎,其具有如下突出的 优点 :
(1 )索引文件格式独立于应用平台。Lucene 定义了一套以8 位字节为基础的索引文件格式,使得兼容系
统或者不同平台的应用能够共享建立的索引文件。
(2 )在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提
升索引速度。然后通过与原有索引的合并,达到优化的目的。
(3 )优秀的面向对象的系统架构,使得对于Lucene 扩展的学习难度降低,方便扩充新功能。
(4 )设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token 流完成索引文件的创立,用户
扩展新的语言和文件格式,只需要实现文本分析的接口。
(5 )已经默认实现了一套强大的查询引擎,用户无需自己编写代码即使系统可获得强大的查询能力,Luc
ene 的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search[11] )、分组查询等等。
表 3.2 基础类包org.apache.lucene.util
类
说明
Arrays
一个关于数组的排序方法的静态类,提供了优化的基于快排序的排序方法sort
BitVector
C/C++ 语言中位域的java 实现品,但是加入了序列化能力
Constants
常量静态类,定义了一些常量
PriorityQueue
一个优先队列的抽象类,用于后面实现各种具体的优先队列,提供常数时间内的最小元素访问能力,内部
实现机制是哈析表和堆排序算法
表 3.3 基础类包org.apache.lucene.document
类
说明
Document
是文档概念的一个实现类,每个文档包含了一个域表(fieldList ),并提供了一些实用的方法,比如多
种添加域的方法、返回域表的迭代器的方法
Field
是域概念的一个实现类,每个域包含了一个域名和一个值,以及一些相关的属性
DateField
提供了一些辅助方法的静态类,这些方法将java 中Date 和Time 数据类型和String 相互转化
总的来说,这两个基础类包中含有的类都比较简单,通过阅读源代码,可以很容易的理解,因此这里不作
过多的展开。
Lucence API 介绍
org.apache.Lucene.search 搜索入口
org.apache.Lucene.index 索引入口
org.apache.Lucene.analysis 语言分析器
org.apache.Lucene.queryParser 查询分析器
org.apache.Lucene.document 存储结构
org.apache.Lucene.store 底层IO/ 存储结构
org.apache.Lucene.util 一些公用的数据结构
全文检索常见问题
• 文档域的存储设置
• 分词分析器个性化实现
• 文档检索高亮显示查询过程
• 查询分页、排序
• 索引建立过程异常处理
• 部分搜索结果搜索不到
索引内容的删除、更新
中文分词处理
文章相关内容计算法
分布式全文检索
不能及时更新索引时与数据库内容一致方法
高级查询:通配符、模糊、间距查询、范围查询、权重查询、组合条件、转义字符
• 英文检索:单词缩为词根形式 steamming lemmatization 两个过程需要建立对应的分词器
• Linux Window 平台差异
• Nutch
• 不同版本的Lucene 之间API 有较大差异
• 是否要转向Sphinx
简单实例(一)
说明一下 , 这一篇文章的用到的 lucene, 是用 2.0 版本的 , 主要在查询的时候 2.0 版本的 lucene 与以前的版本有了一些区别 .
其实这一些代码都是早几个月写的 , 自己很懒 , 所以到今天才写到自己的博客上 , 高深的文章自己写不了,只能记录下一些简单的记录与点滴,其中的代码算是自娱自乐的,希望高手不要把重构之类的砸下来 ...
1 、在 windows 系统下的的 C 盘,建一个名叫 s 的文件夹 , 在该文件夹里面随便建三个 txt 文件,随便起名啦,就叫 "1.txt","2.txt"和 "3.txt" 啦
其中 1.txt 的内容如下:
Java 代码
1. 中华人民共和国
2. 全国人民
3. 2006 年
而 "2.txt" 和 "3.txt" 的内容也可以随便写几写,这里懒写,就复制一个和 1.txt 文件的内容一样吧
2 、下载 lucene 包,放在 classpath 路径中
建立索引 :
Java 代码
1. package lighter.javaeye.com;
2.
3. import java.io.BufferedReader;
4. import java.io.File;
5. import java.io.FileInputStream;
6. import java.io.IOException;
7. import java.io.InputStreamReader;
8. import java.util.Date;
9.
10. import org.apache.lucene.analysis.Analyzer;
11. import org.apache.lucene.analysis.standard.StandardAnalyzer;
12. import org.apache.lucene.document.Document;
13. import org.apache.lucene.document.Field;
14. import org.apache.lucene.index.IndexWriter;
15.
16. /**
17. * author lighter date 2006-8-7
18. */
19. public class TextFileIndexer {
20. public static void main(String[] args) throws Exception {
21. /* 指明要索引文件夹的位置 , 这里是 C 盘的 S 文件夹下 */
22. File fileDir = new File( "c://s" );
23.
24. /* 这里放索引文件的位置 */
25. File indexDir = new File( "c://index" );
26. Analyzer luceneAnalyzer = new StandardAnalyzer();
27. IndexWriter indexWriter = new IndexWriter(indexDir, luceneAnalyzer,
28. true );
29. File[] textFiles = fileDir.listFiles();
30. long startTime = new Date().getTime();
31.
32. // 增加 document 到索引去
33. for ( int i = 0 ; i < textFiles.length; i++) {
34. if (textFiles[i].isFile()
35. && textFiles[i].getName().endsWith( ".txt" )) {
36. System.out.println( "File " + textFiles[i].getCanonicalPath()
37. + " 正在被索引 ...." );
38. String temp = FileReaderAll(textFiles[i].getCanonicalPath(),
39. "GBK" );
40. System.out.println(temp);
41. Document document = new Document();
42. Field FieldPath = new Field( "path" , textFiles[i].getPath(),
43. Field.Store.YES, Field.Index.NO);
44. Field FieldBody = new Field( "body" , temp, Field.Store.YES,
45. Field.Index.TOKENIZED,
46. Field.TermVector.WITH_POSITIONS_OFFSETS);
47. document.add(FieldPath);
48. document.add(FieldBody);
49. indexWriter.addDocument(document);
50. }
51. }
52. //optimize() 方法是对索引进行优化
53. indexWriter.optimize();
54. indexWriter.close();
55.
56. // 测试一下索引的时间
57. long endTime = new Date().getTime();
58. System.out
59. .println( " 这花费了 "
60. + (endTime - startTime)
61. + " 毫秒来把文档增加到索引里面去 !"
62. + fileDir.getPath());
63. }
64.
65. public static String FileReaderAll(String FileName, String charset)
66. throws IOException {
67. BufferedReader reader = new BufferedReader( new InputStreamReader(
68. new FileInputStream(FileName), charset));
69. String line = new String();
70. String temp = new String();
71.
72. while ((line = reader.readLine()) != null ) {
73. temp += line;
74. }
75. reader.close();
76. return temp;
77. }
78. }
索引的结果:
Java 代码
1. File C:/s/ 1 .txt 正在被索引 ....
2. 中华人民共和国全国人民 2006 年
3. File C:/s/ 2 .txt 正在被索引 ....
4. 中华人民共和国全国人民 2006 年
5. File C:/s/ 3 .txt 正在被索引 ....
6. 中华人民共和国全国人民 2006 年
7. 这花费了 297 毫秒来把文档增加到索引里面去 !c:/s
3 、建立了索引之后,查询啦 ....
Java 代码
1. package lighter.javaeye.com;
2.
3. import java.io.IOException;
4.
5. import org.apache.lucene.analysis.Analyzer;
6. import org.apache.lucene.analysis.standard.StandardAnalyzer;
7. import org.apache.lucene.queryParser.ParseException;
8. import org.apache.lucene.queryParser.QueryParser;
9. import org.apache.lucene.search.Hits;
10. import org.apache.lucene.search.IndexSearcher;
11. import org.apache.lucene.search.Query;
12.
13. public class TestQuery {
14. public static void main(String[] args) throws IOException, ParseException {
15. Hits hits = null ;
16. String queryString = " 中华 " ;
17. Query query = null ;
18. IndexSearcher searcher = new IndexSearcher( "c://index" );
19.
20. Analyzer analyzer = new StandardAnalyzer();
21. try {
22. QueryParser qp = new QueryParser( "body" , analyzer);
23. query = qp.parse(queryString);
24. } catch (ParseException e) {
25. }
26. if (searcher != null ) {
27. hits = searcher.search(query);
28. if (hits.length() > 0 ) {
29. System.out.println( " 找到 :" + hits.length() + " 个结果 !" );
30. }
31. }
32. }
33.
34. }
其运行结果:
引用
找到 :3 个结果 !
简单实例(二)
写文章的时候 , 感觉比较难写的就是标题 , 有时候不知道起什么名字好 , 反正这里写的都是关于 lucene 的一些简单的实例 , 就随便起啦 .
Lucene 其实很简单的 , 它最主要就是做两件事 : 建立索引和进行搜索
来看一些在 lucene 中使用的术语 , 这里并不打算作详细的介绍 , 只是点一下而已 ---- 因为这一个世界有一种好东西,叫搜索。
IndexWriter :lucene 中最重要的的类之一,它主要是用来将文档加入索引,同时控制索引过程中的一些参数使用。
Analyzer : 分析器 , 主要用于分析搜索引擎遇到的各种文本。常用的有 StandardAnalyzer 分析器 ,StopAnalyzer 分析器,WhitespaceAnalyzer 分析器等。
Directory : 索引存放的位置 ;lucene 提供了两种索引存放的位置,一种是磁盘,一种是内存。一般情况将索引放在磁盘上;相应地lucene 提供了 FSDirectory 和 RAMDirectory 两个类。
Document : 文档 ;Document 相当于一个要进行索引的单元,任何可以想要被索引的文件都必须转化为 Document 对象才能进行索引。
Field :字段。
IndexSearcher : 是 lucene 中最基本的检索工具,所有的检索都会用到 IndexSearcher 工具 ;
Query : 查询, lucene 中支持模糊查询,语义查询,短语查询,组合查询等等 , 如有TermQuery,BooleanQuery,RangeQuery,WildcardQuery 等一些类。
QueryParser : 是一个解析用户输入的工具,可以通过扫描用户输入的字符串,生成 Query 对象。
Hits : 在搜索完成之后,需要把搜索结果返回并显示给用户,只有这样才算是完成搜索的目的。在 lucene 中,搜索的结果的集合是用Hits 类的实例来表示的。
上面作了一大堆名词解释,下面就看几个简单的实例吧 :
1 、简单的的 StandardAnalyzer 测试例子
Java 代码
1. package lighter.javaeye.com;
2.
3. import java.io.IOException;
4. import java.io.StringReader;
5.
6. import org.apache.lucene.analysis.Analyzer;
7. import org.apache.lucene.analysis.Token;
8. import org.apache.lucene.analysis.TokenStream;
9. import org.apache.lucene.analysis.standard.StandardAnalyzer;
10.
11. public class StandardAnalyzerTest
12. {
13. // 构造函数,
14. public StandardAnalyzerTest()
15. {
16. }
17. public static void main(String[] args)
18. {
19. // 生成一个 StandardAnalyzer 对象
20. Analyzer aAnalyzer = new StandardAnalyzer();
21. // 测试字符串
22. StringReader sr = new StringReader( "lighter javaeye com is the are on" );
23. // 生成 TokenStream 对象
24. TokenStream ts = aAnalyzer.tokenStream( "name" , sr);
25. try {
26. int i= 0 ;
27. Token t = ts.next();
28. while (t!= null )
29. {
30. // 辅助输出时显示行号
31. i++;
32. // 输出处理后的字符
33. System.out.println( " 第 " +i+ " 行 :" +t.termText());
34. // 取得下一个字符
35. t=ts.next();
36. }
37. } catch (IOException e) {
38. e.printStackTrace();
39. }
40. }
41. }
显示结果:
引用
第 1 行 :lighter
第 2 行 :javaeye
第 3 行 :com
提示一下:
StandardAnalyzer 是 lucene 中内置的 " 标准分析器 ", 可以做如下功能 :
1 、对原有句子按照空格进行了分词
2 、所有的大写字母都可以能转换为小写的字母
3 、可以去掉一些没有用处的单词,例如 "is","the","are" 等单词,也删除了所有的标点
查看一下结果与 "new StringReader("lighter javaeye com is the are on")" 作一个比较就清楚明了。
这里不对其 API 进行解释了,具体见 lucene 的官方文档。需要注意一点,这里的代码使用的是 lucene2 的 API ,与 1.43 版有一些明显的差别。
2 、看另一个实例 , 简单地建立索引,进行搜索
Java 代码
1. package lighter.javaeye.com;
2. import org.apache.lucene.analysis.standard.StandardAnalyzer;
3. import org.apache.lucene.document.Document;
4. import org.apache.lucene.document.Field;
5. import org.apache.lucene.index.IndexWriter;
6. import org.apache.lucene.queryParser.QueryParser;
7. import org.apache.lucene.search.Hits;
8. import org.apache.lucene.search.IndexSearcher;
9. import org.apache.lucene.search.Query;
10. import org.apache.lucene.store.FSDirectory;
11.
12. public class FSDirectoryTest {
13.
14. // 建立索引的路径
15. public static final String path = "c://index2" ;
16.
17. public static void main(String[] args) throws Exception {
18. Document doc1 = new Document();
19. doc1.add( new Field( "name" , "lighter javaeye com",Field.Store.YES,Field.Index.TOKENIZED));
20.
21. Document doc2 = new Document();
22. doc2.add( new Field( "name" , "lighter blog" ,Field.Store.YES,Field.Index.TOKENIZED));
23.
24. IndexWriter writer = new IndexWriter(FSDirectory.getDirectory(path, true ), new StandardAnalyzer(), true );
25. writer.setMaxFieldLength( 3 );
26. writer.addDocument(doc1);
27. writer.setMaxFieldLength( 3 );
28. writer.addDocument(doc2);
29. writer.close();
30.
31. IndexSearcher searcher = new IndexSearcher(path);
32. Hits hits = null ;
33. Query query = null ;
34. QueryParser qp = new QueryParser( "name" , new StandardAnalyzer());
35.
36. query = qp.parse( "lighter" );
37. hits = searcher.search(query);
38. System.out.println( " 查找 /"lighter/" 共 " + hits.length() + " 个结果 " );
39.
40. query = qp.parse( "javaeye" );
41. hits = searcher.search(query);
42. System.out.println( " 查找 /"javaeye/" 共 " + hits.length() + " 个结果 " );
43.
44. }
45.
46. }
运行结果:
Java 代码
1. 查找 "lighter" 共 2 个结果
2. 查找 "javaeye" 共 1 个结果