1.1 前言
l 编制本手册的目的:
1.描述Lucene用途,使开发人员依据本手册初步认识Lucene;
2.描述Lucene的API,使开发人员可以快速认识并利用Lucene开发搜索引擎;
3.描述Lucene和webdt的融合;
4. 重点描述Lucene的应用,使开发人员可按照本手册的描述开发。
l 本手册面向的读者:
熟悉Java开发,并对WEBDT软件的特点(请参阅《技术白皮书》)具有初步认识的技术人员。
1.2 概述
本手册首先介绍了Lucene的概念,详细描述了简单快速地将Lucene融入WEBDT运行平台并进行实际开发的方法,使技术人员对Lucene有一个概要性的框架认识,为下一步开发工作奠定基础。
第2章 LUCENE 简介
2.1 什么是LUNCENE
Lucene是一套java API,就如同Servlet是一套API一样。Lucene不是一个独立的搜索引擎系统,但是你可以使用Luncene来开发搜索引擎系统。这正如Servlet不是网站系统但是你可以用Servlet开发网站一样。
有人已经用Lucene开发出了独立的搜索引擎系统,你可以下载,然后不写一行代码就是用它。Nutch是最出名的了。
Lucene是一个全文搜索框架,而不是应用产品。因此它并不像www.baidu.com 或者google Desktop那么拿来就能用,它只是提供了一种工具让你能实现这些产品。
2.2 LUNCENE能做什么
要回答这个问题,先要了解Lucene的本质。实际上Lucene的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出现在哪里。知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了,做个资料库;你可以把一个数据库表的若干个字段索引起来,那就不用再担心因为“%like%”而锁表了;你也可以写个自己的搜索引擎……
2.3 你该不该选择Lucene
下面给出一些测试数据,如果你觉得可以接受,那么可以选择。
测试一:250万记录,300M左右文本,生成索引380M左右,800线程下平均处理时间300ms。
测试二:37000记录,索引数据库中的两个Varchar字段,索引文件2.6M,800线程下平均处理时间1.5ms。
第3章 LUCENE 的 概述
3.1 下载LUCENE
进入Lucene的主页:http://lucene.apache.org。
可以从主页中单击“free download”链接,从而转入http://www.apache.org/dyn/closer.cgi/lucen/java/页面,这是网站为访问者推荐的最佳镜像地址,从这里往往可以得到最快的下载速度。
单击页面中推荐的镜像地址,进入Lucene的下载页面:http://government-grants.org/mirrors/apache.org/lucene/java。
从页面中选择lucene-2.0.1.zip文件,这是Lucene的二进制文件。如果要下载其源代码,则选择lucene-2.1.0-src.zip。
3.2 LUCENE基本概念
3.2.1 Analyzer
Analyzer是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的“of”、 “the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。
分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。
3.2.2 Document
用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。
3.2.3 Field
一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在Document中存储的。
Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:
还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。
3.2.4 Term
term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现的field。
3.2.5 Tocken
tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。
3.2.6 Segment
添加索引时并不是每个document都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个segment。
3.3 Lucene的结构
Lucene包括core和sandbox两部分,其中core是Lucene稳定的核心部分,sandbox包含了一些附加功能,例如highlighter、各种分析器。
Lucene core有七个包:analysis,document,index,queryParser,search,store,util。
3.3.1 analysis
Analysis包含一些内建的分析器,例如按空白字符分词的WhitespaceAnalyzer,添加了stopwrod过滤的StopAnalyzer,最常用的StandardAnalyzer。
3.3.2 document
Document包含文档的数据结构,例如Document类定义了存储文档的数据结构,Field类定义了Document的一个域。
3.3.3 index
Index包含了索引的读写类,例如对索引文件的segment进行写、合并、优化的IndexWriter类和对索引进行读取和删除操作的 IndexReader类,这里要注意的是不要被IndexReader这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完成, IndexWriter只关心如何将索引写入一个个segment,并将它们合并优化;IndexReader则关注索引文件中各个文档的组织形式。
3.3.4 queryParser
QueryParser包含了解析查询语句的类,Lucene的查询语句和sql语句有点类似,有各种保留字,按照一定的语法可以组成各种查询。 Lucene有很多种Query类,它们都继承自Query,执行各种特殊的查询,QueryParser的作用就是解析查询语句,按顺序调用各种 Query类查找出结果。
3.3.5 Search
Search包含了从索引中搜索结果的各种类,例如刚才说的各种Query类,包括TermQuery、BooleanQuery等就在这个包里。
4.6 store
Store包含了索引的存储类,例如Directory定义了索引文件的存储结构,FSDirectory为存储在文件中的索引,RAMDirectory为存储在内存中的索引,MmapDirectory为使用内存映射的索引。
3.3.6 Util
Util包含一些公共工具类,例如时间和字符串之间的转换工具。
3.4 如何建索引
3.4.1 创建Field
创建File的方法有很多种,下面是最常用的方法。
Field field= new Field(Field 名称,Field 内容,存储方式,索引方式);
这四个参数的含义如下:
(1)Field 名称就是为Field取得名字,类似数据表的字段名称。
(2)Field 内容就是该Field 的内容,类似数据表的字段内容。
(3)存储方式包括三种:
不存储(Field.Store.NO)、完全存储(Field.Store.YES)和压缩存储(Field.Store.COMPRESS)
通常,如果不担心索引太大的话,可以可以都使用完全存储的方式。但是,出于对性能的考虑,索引文件的内容是越小越好。因此,如果Field的内容很少就采用完全存储(如标题),如果Field的内容很多就采用不存储或压缩存储的方式(如正文)。
(4)索引方式包括四种:
不索引(Field.Store.NO)、索引但不分析(Filed.Index.NO_NORMS)、索引但不分词(Field.Index.UN_TOKENIZED)、分次并索引(Field.Index. TOKENIZED)。
以新闻稿为例,通常我们会按照标题和全文进行模糊搜索,这类需要进行模糊搜索的字段就用Field.Index. TOKENIZED。通常我们会按照作者名称进行精确搜索,需要精心精确搜索的字段就用Field.Index.UN_TOKENIZED。对于那些只需要跟着搜索结果显示出来却不需要按照其内容进行搜索的字段,使用Field.Index.NO。
3.4.2 创建Document
创建Document的方法如下:
Document doc= new Document();
这个方法用来创建一个不含任何Field的空Document。
如果想把Field添加到Document里面,只需使用add方法。例如
doc.add(field);
重复的使用这个方法,就可以将多个Field加入到一个Document里面。
3.4.3 创建IndexWriter
创建IndexWriter的方法很多,下面是最常用的方法:
IndexWriter writer= new IndexWriter(存储索引的路径,分析器的实例);
这两个参数的含义如下:
(1) 存储索引的路径就是索引被存储在硬盘上的物理路径。如:c:\my等。
(2) 分析器的实例。分析器是用来做词法分析的,包括英文分析器和中文分析器等。
要根据所要建立索引的文件情况选择适当的分析器。常用的有StandardAnalyzer(标准分析器)、CJKAnaLyzer(二分法分析器)、ChineseAnlyzer(中文分析器)和FrenchAnalyzer(法语分析器)等。
可以根据自己的需要去编写分析器,从而处理不同的语言文字。
通过建立IndexWriter,就把逻辑索引核物理索引联系起来了,这样就可以很方便的建立索引。例如:
IndexWriter wrier=new IndexWriter(“c:/my/index”,new CJKAnalyzer());
这是使用二分法分次器来分析字段内容,然后建索引建立在C:/gad/index目录下。
使用 new IndexWriter() 方法建立起来的是一个空索引器,要把Document添加到索引中来,需要使用addDocument方法。
例如:
writer.addDocument();
这类似于我们前面提到过的向Document中添加Field,重复执行这个操作就可以向一个IndexWriter中添加多个Field。
最后,在索引创建完成之时,要使用close方法关闭索引器。例如:
writer.close();
最简单的能完成索引的代码片断
- IndexWriterwriter=newIndexWriter(“/data/index/”,newStandardAnalyzer(),true);
- Documentdoc=newDocument();
- doc.add(newField("title","Luceneintroduction",Field.Store.YES,Field.Index.TOKENIZED));
- doc.add(newField("content","Luceneworkswell",Field.Store.YES,Field.Index.TOKENIZED));
- writer.addDocument(doc);
- writer.optimize();
- writer.close();IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
下面我们分析一下这段代码。
首先我们创建了一个writer,并指定存放索引的目录为“/data/index”,使用的分析器为StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。
然后我们新建一个document。
我们向document添加一个field,名字是“title”,内容是“Lucene introduction”,对它进行存储并索引。
再添加一个名字是“content”的field,内容是“Lucene works well”,也是存储并索引。
然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建document并添加。
添加完所有document,我们对索引进行优化,优化主要是将多个segment合并到一个,有利于提高索引速度。
随后将writer关闭,这点很重要。
3.5 执行搜索
索引创建好以后,就可以执行搜索了。执行搜索的过程就是:将用户输入的关键字处理,从而得到搜索结果。使用Lucene执行搜索,首先要创建IndexSearcher对象,然后要通过Term和Query对象来封装用户输入的搜索条件,最后将结果封装在Hits对象中,返回给用户。
3.5.1 创建搜索器对象:IndexSearcher
创建IndexSearcher对象的方法如下:
IndexSearcher searcher= new IndexSearcher(索引存放路径);
创建IndexSearcher对象很容易,创建完成之后,就可以使用它进行搜索了。它最常用的方法是search()。使用search 方法将返回一个结果集对象,即Hits。例如:
Hits h =searcher.search();
搜索执行完毕之后,应该使用close()方法关闭IndexSearcher对象。例如:
Searcher.close();
3.5.2 封装搜索条件:使用Term和Query 对象
如果我们要从一个索引中搜索“title”字段包含“中国”的文档,该怎么办?
这时,用户的搜索条件是Field为title,关键词为“中国”。
创建Trem对象来封装这个搜索条件,可用:Term t= new Term(“title”,“中国”);
由此可见Term对象的创建方法,即:
Term t=new Term(“字段名称”,“关键字”);
然后,我们要创建一个Query对象,从而把Term对象转化成可执行的查询条件。
Query对象有很多种,我们暂时只介绍最简单的TermQuery对象。用法如下:
Query q =new TermQuery(t);
至此,用户的搜索请求就被封装好了,封装在Query对象中。
3.5.2.1 各种各样的Query
下面我们看看Lucene到底允许我们进行哪些查询操作:
TermQuery
首先介绍最基本的查询,如果你想执行一个这样的查询:“在content域中包含‘Lucene’的document”,那么你可以用TermQuery:
Term t = new Term("content", " Lucene";
Query query = new TermQuery(t);
BooleanQuery
如果你想这么查询:“在content域中包含java或perl的document”,那么你可以建立两个TermQuery并把它们用BooleanQuery连接起来:
- TermQuerytermQuery1=newTermQuery(newTerm("content","java");
- TermQuerytermQuery2=newTermQuery(newTerm("content","perl");
- BooleanQuerybooleanQuery=newBooleanQuery();
- booleanQuery.add(termQuery1,BooleanClause.Occur.SHOULD);
- booleanQuery.add(termQuery2,BooleanClause.Occur.SHOULD);
TermQuery termQuery1 = new TermQuery(new Term("content", "java");
TermQuery termQuery 2 = new TermQuery(new Term("content", "perl");
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);
booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);
WildcardQuery
如果你想对某单词进行通配符查询,你可以用WildcardQuery,通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符,例如你搜索’use*’,你可能找到’useful’或者’useless’:
- Queryquery=newWildcardQuery(newTerm("content","use*");
Query query = new WildcardQuery(new Term("content", "use*");
PhraseQuery
你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5个字的距离内)的文章,超过这个距离的不予考虑,你可以:
- PhraseQueryquery=newPhraseQuery();
- query.setSlop(5);
- query.add(newTerm("content",“中”));
- query.add(newTerm(“content”,“日”));
PhraseQuery query = new PhraseQuery();
query.setSlop(5);
query.add(new Term("content ", “中”));
query.add(new Term(“content”, “日”));
那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中国某高层领导说日本欠扁”。
PrefixQuery
如果你想搜以‘中’开头的词语,你可以用PrefixQuery:
- PrefixQueryquery=newPrefixQuery(newTerm("content","中");
PrefixQuery query = new PrefixQuery(new Term("content ", "中");
FuzzyQuery
FuzzyQuery用来搜索相似的term,使用Levenshtein算法。假设你想搜索跟‘wuzza’相似的词语,你可以:
- Queryquery=newFuzzyQuery(newTerm("content","wuzza");
Query query = new FuzzyQuery(new Term("content", "wuzza");
你可能得到‘fuzzy’和‘wuzzy’。
RangeQuery
另一个常用的Query是RangeQuery,你也许想搜索时间域从20060101到20060130之间的document,你可以用RangeQuery:
- RangeQueryquery=newRangeQuery(newTerm(“time”,“20060101”),newTerm(“time”,“20060130”),true);
RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true);
最后的true表示用闭合区间。
3.5.2.2 QueryParser
看了这么多Query,你可能会问:“不会让我自己组合各种Query吧,太麻烦了!”当然不会,Lucene提供了一种类似于SQL语句的查询语句,我们姑且叫它Lucene语句,通过它,你可以把各种查询一句话搞定,Lucene会自动把它们查分成小块交给相应Query执行。下面我们对应每种Query演示一下:
TermQuery可以用“field:key”方式,例如“content:Lucene”。
BooleanQuery中‘与’用‘+’,‘或’用‘ ’,例如“content:java contenterl”。
WildcardQuery仍然用‘?’和‘*’,例如“content:use*”。
PhraseQuery用‘~’,例如“content:"中日"~5”。
PrefixQuery用‘*’,例如“中*”。
FuzzyQuery用‘~’,例如“content: wuzza ~”。
RangeQuery用‘[]’或‘{}’,前者表示闭区间,后者表示开区间,例如“time:[20060101 TO 20060130]”,注意TO区分大小写。
你可以任意组合query string,完成复杂操作,例如“标题或正文包括Lucene,并且时间在20060101到20060130之间的文章”可以表示为:“+ (title:Lucene content:Lucene) +time:[20060101 TO 20060130]”。代码如下:
- Directorydir=FSDirectory.getDirectory(PATH,false);
- IndexSearcheris=newIndexSearcher(dir);
- QueryParserparser=newQueryParser("content",newStandardAnalyzer());
- Queryquery=parser.parse("+(title:Lucenecontent:Lucene)+time:[20060101TO20060130]";
- Hitshits=is.search(query);
- for(inti=0;i<hits.length();i++)
- {
- Documentdoc=hits.doc(i);
- System.out.println(doc.get("title");
- }
- is.close();
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new IndexSearcher(dir);
QueryParser parser = new QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("+(title:lucene content:lucene) +time:[20060101 TO 20060130]";
Hits hits = is.search(query);
for (int i = 0; i < hits.length(); i++)
{
Document doc = hits.doc(i);
System.out.println(doc.get("title");
}
is.close();
首先我们创建一个在指定文件目录上的IndexSearcher。
然后创建一个使用StandardAnalyzer作为分析器的QueryParser,它默认搜索的域是content。
接着我们用QueryParser来parse查询字串,生成一个Query。
然后利用这个Query去查找结果,结果以Hits的形式返回。
这个Hits对象包含一个列表,我们挨个把它的内容显示出来。
3.5.2.3 Filter
filter的作用就是限制只查询索引的某个子集,它的作用有点像SQL语句里的where,但又有区别,它不是正规查询的一部分,只是对数据源进行预处理,然后交给查询语句。注意它执行的是预处理,而不是对查询结果进行过滤,所以使用filter的代价是很大的,它可能会使一次查询耗时提高一百倍。
最常用的filter是RangeFilter和QueryFilter。RangeFilter是设定只搜索指定范围内的索引;QueryFilter是在上次查询的结果中搜索。
Filter的使用非常简单,你只需创建一个filter实例,然后把它传给searcher。继续上面的例子,查询“时间在20060101到20060130之间的文章”除了将限制写在query string中,你还可以写在RangeFilter中:
- Directorydir=FSDirectory.getDirectory(PATH,false);
- IndexSearcheris=newIndexSearcher(dir);
- QueryParserparser=newQueryParser("content",newStandardAnalyzer());
- Queryquery=parser.parse("title:Lucenecontent:Lucene";
- RangeFilterfilter=newRangeFilter("time","20060101","20060230",true,true);
- Hitshits=is.search(query,filter);
- for(inti=0;i<hits.length();i++)
- {
- Documentdoc=hits.doc(i);
- System.out.println(doc.get("title");
- }
- is.close();
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new IndexSearcher(dir);
QueryParser parser = new QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("title:lucene content:lucene";
RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true);
Hits hits = is.search(query, filter);
for (int i = 0; i < hits.length(); i++)
{
Document doc = hits.doc(i);
System.out.println(doc.get("title");
}
is.close();
3.5.2.4 Sort
有时你想要一个排好序的结果集,就像SQL语句的“order by”,Lucene能做到:通过Sort。
Sort sort = new Sort(“time”); //相当于SQL的“order by time”
Sort sort = new Sort(“time”, true); // 相当于SQL的“order by time desc”
下面是一个完整的例子:
- Directorydir=FSDirectory.getDirectory(PATH,false);
- IndexSearcheris=newIndexSearcher(dir);
- QueryParserparser=newQueryParser("content",newStandardAnalyzer());
- Queryquery=parser.parse("title:Lucenecontent:Lucene";
- RangeFilterfilter=newRangeFilter("time","20060101","20060230",true,true);
- Sortsort=newSort(“time”);
- Hitshits=is.search(query,filter,sort);
- for(inti=0;i<hits.length();i++)
- {
- Documentdoc=hits.doc(i);
- System.out.println(doc.get("title");
- }
is.close();
3.5.3 执行搜索
用户的搜索请求被封装好了之后,就该把请求传递给IndexSearcher对象,使其执行搜索。IndexSearcher对象调用search方法,以Query对象为参数,返回搜索结果,封装在Hits对象中。如下所示:
Hits hs=searcher.search(q);
3.5.4 提取搜索结果:了解Hits对象
搜索结果被封装在Hits对象中,要想从中得到结果的具体内容,就需要了解Hits对象的相关方法和属性。
我们知道,搜索结果是由一组Document构成的,所以,只要从Hits对象中获得这些Document,之后就可以从Document中取得Field,进而得到原文档的一切内容。
Hits对象有如下常用方法:
(1) Document doc(int n)
返回指定序号的Document。
(2) int id(int n)
返回指定序号的Document的id属性。
(3) int length()
返回Hits对象的长度,也就是Hits对象中包含的Document的数量。
(4) float score(int n)
返回指定序号的Document的score属性(即文档得分)。
有了这些方法,我们就可以灵活地操作搜索结果。
3.5.5 提取搜索结果:了解Document对象
在从Hits对象中提取出Document之后,想要获取这个Document中含有的具体内容时,就要了解他的一些常用方法。
(1)Field getField(String name)
参数是Field名称,返回值是该Field对象。
(2)List getFields()
无参数,返回值是List类型,包含该Document的所有Field。
(3)Enumeration fields()
无参数,返回值是Enumeration类型,包含该Document的所有Field。
(4)String get(String name)
参数是Field名称,返回值是该Field对象的字符串值。
3.5.6 提取搜索结果:了解Field对象
使用Document对象的相关方法获得Field对象之后,就可以跳用Field对象的方法来获得详细信息。Field对象的常用方法如下:
(1)byte[] binary Vakue()
获得指定Field对象的二进制值。
(2)Reader readerValue()
获得指定Field对象的内容,以Reader形式返回。
(3)String stringValue()
获得指定Field对象的字符串值,这个方法是最常用的。
3.6 中文分词
3.6.1 分词的方法
分词的方法主要有以下几种:
(1) 单字切分
单字切分就是把一段文字按照每个字去建立索引。
如果用来切分“我爱你”,就会切成“我”,“爱”,“你”3个词。这种分此法效率低,但也能解决一些问题。
(2) 二分法
二分法就是把一段文字的每两个相邻的字算做一个词。
如果用来切分“我爱你”,就切成“我爱”,“爱你”。这种分词法效率也低,但比单字切分好得多。
(3) 词典法
词典法就是建立一个词典文件,然后使用词典和文字段落进行匹配,从而得出分此结果。在这种分此方法中,词典和匹配算法是关键。
(5)语义法
3.6.2 LUCENE的分词器
3.6.2.1 二分法分词器
在Lucene软件包的contrib/analyzers目录下有一个lucene-analyzers-2.1.0.jar文件,其中含有二分法分词器CJKAnalyzer。只需将此jar放入编译路径即可使用CJKAnalyzer。
分词效果:“我爱你”,切成“我爱”,“爱你”。
3.6.2.2 Lucene自带的中文分词器
在Lucene软件包的contrib/analyzers目录下有一个lucene-analyzers-2.1.0.jar文件,其中含有二分法分词器ChineseAnalyzer。只需将此jar放入编译路径即可使用ChineseAnalyzer,这个分词器目前使用的是单字切分。
分词效果:“我爱你”,切成“我”,“爱”,“你”。
3.6.2.3 NGram分词器的原理和用法
在Lucene软件包的contrib/analyzers目录下有一个lucene-analyzers-2.1.0.jar文件,其中含有二分法分词器NGramTokenizer。只需将此jar放入编译路径即可使用NGramTokenizer,这个分词器目前使用的是单字切分。
分词效果:“我爱你”,切成“我|爱|你”,“我爱|爱你”,“我|我爱|爱你|你”。
3.6.2.4 JE分词器的原理和用法
JE分词器是一个不错的分词器,许多人都在使用。可以从http://www.jesoft.cn/上面下载。
最新版(2007-5-31)的JE分词器是1.5.1,基于词库,可以向词库中增加新词。
将je-analysis-1.5.1.jar放到编译路径中即可使用。
分词效果:“我爱你”,切成“我|爱你”。
除了以上列举的分词器之外,网上还有其他一些免费的分词器,大家可以更多的了解。
3.7 数据解析
数据解析的方法有很多种,这里重点介绍Lius类库。
3.7.1 初识Lius
3.7.1.1 Lius 简介
Lius是一套用于建立索引的java框架,它基于Lucene项目而建。使用Lius框架可以索引Microsoft Word、Microsoft Excel、Microsoft Powerpoint、RTF、PDF、XML、HTML、TXT、OpenOffice套件、Zip文档、MP3、Vcard、Latex和JavaBean文件。
3.7.1.2 下载Lius
Lius的项目主页是http://sourceforge.net/projects/lius/。
页面上有一个绿色的下载链接,单击它,可以进入Lius的下载列表页面,我们选择最新的版本Lius-1.0。
使用Lius1.0,需要Java5环境支持。同时,需要把Lius根目录下的Lius-1.0.jar和lib目录下的外部类库加入到编译路径。
3.7.2 借助Lius解析普通数据
3.7.2.1 解析Word
使用Lius解析Word的方法如下面案例所示。程序名称:LiusWord.java,
// 解析Word文档
public String LiusWord(String filepath) {
WordIndexer wi = new WordIndexer();
File f = new File(filepath);
String content = "";
try {
wi.setStreamToIndex(new FileInputStream(f));
content = wi.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
3.7.2.2 解析Excel
使用Lius解析Excel的方法如下面案例所示。程序名称:LiusExcel.java
// 解析Excel文档
public String LiusExcel(String filepath) {
ExcelIndexer ei = new ExcelIndexer();
File f = new File(filepath);
String content = "";
try {
ei.setStreamToIndex(new FileInputStream(f));
content = ei.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
3.7.2.3 解析PDF
使用Lius解析PDF的方法如下面案例所示。程序名称:LiusPDF.java
// 解析PDF文档
public String LiusPDF(String filepath) {
PdfIndexer pi = new PdfIndexer();
File f = new File(filepath);
String content = "";
try {
pi.setStreamToIndex(new FileInputStream(f));
content = pi.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
3.7.2.4 解析PowerPoint
使用Lius解析PPT的方法如下面案例所示。程序名称:LiusPPT.java
// 解析PPT文档
public String LiusPPT(String filepath) {
PPTIndexer pi = new PPTIndexer();
File f = new File(filepath);
String content = "";
try {
pi.setStreamToIndex(new FileInputStream(f));
content = pi.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
Lius解析PowerPoint文件时借助POI组件。POI组件不仅支持Excel解析,而且支持Word、PowerPoint等多种文件格式的解析,只是目前解析质量差,解析方法复杂。目前Lius对PowerPoint文件的解析还不支持中文。
如果Lius使用了Jacob组件,然后通过PowerPoint的COM组件操作,就可以获得完好的解析结果。
因为目前Lius对PowerPoint文件的解析还不支持中文在这里我使用的是POI组件,先下载poi-3.5-FINAL-20090928.jar、poi-scratchpad-3.5-FINAL-20090928.jar将它们加入到工程的编译路径。不过这两个包的加入可能会与Lius中的poi-2.5.1-final-20040804冲突,冲突见导致
java.lang.NoSuchMethodError: org.apache.poi.poifs.filesystem.POIFSFileSystem.getRoot()Lorg/apache/poi/poifs/filesystem/DirectoryEntry;错误。
解析PPT的方法如下:
//解析PPT文档
publicstatic String handlePPT(String filename){
StringBuffer content = new StringBuffer("");
try{
File file=new File(filename);
if(!file.exists()) {
return content.toString();
}
FileInputStream instream=new FileInputStream(file);
SlideShow ppt = new SlideShow(instream);
Slide[] slides = ppt.getSlides();
for(int i=0;i<slides.length;i++){
TextRun[] t = slides[i].getTextRuns();//为了取得幻灯片的文字内容,建立TextRun
for(int j=0;j<t.length;j++){
content.append(t[j].getText());//这里会将文字内容加到content中去
}
content.append(slides[i].getTitle());
}
}catch(Exception e){
e.printStackTrace();
}
return content.toString();
}
3.7.2.5 解析TXT
使用Lius解析TXT的方法如下面案例所示。程序名称:LiusTXT.java
// 解析TXT文档
publicstaticString LiusTXT(String filepath) {
TXTIndexer ti = new TXTIndexer();
File f = new File(filepath);
String content = "";
try {
ti.setStreamToIndex(new FileInputStream(f));
content = ti.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
3.7.2.6 解析HTML
使用Lius解析HTML的方法有两个类:JTidyHtmlIndexer和NekoHtmlIndexer。这两个类分别利用了JTidy类库和Neko类库。
// 解析HTML文档1
public String LiusJTidy (String filepath) {
JTidyHtmlIndexer ji = new JTidyHtmlIndexer ();
File f = new File(filepath);
String content = "";
try {
ji.setStreamToIndex(new FileInputStream(f));
content = ji.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
// 解析HTML文档2
public String LiusNeko (String filepath) {
NekoHtmlIndexer ni = new NekoHtmlIndexer ();
File f = new File(filepath);
String content = "";
try {
ni.setStreamToIndex(new FileInputStream(f));
content = ni.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
关于文件解析还有很多种方法比如PDF的解析还可以借助PDFBox类库解析,大家可以在网上查阅,也可参考如《LUCENE搜索引擎开发权威经典》于天恩著,等书籍。
3.8 索引的管理
3.8.1 索引的删除
Lucene提供了两种从索引中删除document的方法,一种是
void deleteDocument(int docNum)
这种方法是根据document在索引中的编号来删除,每个document加进索引后都会有个唯一编号,所以根据编号删除是一种精确删除,但是这个编号是索引的内部结构,一般我们不会知道某个文件的编号到底是几,所以用处不大。另一种是
void deleteDocuments(Term term)
这种方法实际上是首先根据参数term执行一个搜索操作,然后把搜索到的结果批量删除了。我们可以通过这个方法提供一个严格的查询条件,达到删除指定document的目的。
下面给出一个例子:
- Directorydir=FSDirectory.getDirectory(PATH,false);
- IndexReaderreader=IndexReader.open(dir);
- Termterm=newTerm(field,key);
- reader.deleteDocuments(term);
- reader.close();
3.8.2 索引的更新
Lucene并没有提供专门的索引更新方法,我们需要先将相应的document删除,然后再将新的document加入索引。例如:
- Directorydir=FSDirectory.getDirectory(PATH,false);
- IndexReaderreader=IndexReader.open(dir);
- Termterm=newTerm(“title”,“Luceneintroduction”);
- reader.deleteDocuments(term);
- reader.close();
- IndexWriterwriter=newIndexWriter(dir,newStandardAnalyzer(),true);
- Documentdoc=newDocument();
- doc.add(newField("title","Luceneintroduction",Field.Store.YES,Field.Index.TOKENIZED));
- doc.add(newField("content","Luceneisfunny",Field.Store.YES,Field.Index.TOKENIZED));
- writer.addDocument(doc);
- writer.optimize();
- writer.close();
第4章 LUCENE 的实际运用
4.1.1 LUCENE和WEBDT的融合
在Webdt框架下开发Lucene搜索引擎其实只需将Lucene开发中会使用到的jar包加入到工程的编译路径下即可,不用再做过多的融合,Lucene开发中会用到的jar包,在前面几章中已经有过相应的介绍,大家使用时下载即可。
4.1.2 LUCENE开发实例
这里我们就前面几章学过的知识来统一做一个例子,为大家今后的开发作参考。
需求实例:
该需求需要实现文件系统的全文检索,即用户输入要检索的关键字,就可从文件系统中将包含该关键字的所有文件搜索出来,此功能能够方便用户对文件系统的检索。
解决方案:
(1) 首先系统的文件系统就是磁盘空间的一个文件夹,我们要对文件夹内的可以进行索引的文件格式种类的所有文件建立索引,就必须先找出文件夹下的所有可以建立索引的文件。
(2) 为每一个符合条件的文件建立索引。
(3) 对关键字进行搜索。
编写一个类该类名为Lucene,该类的目的是为文件系统中所有的文件建立索引,该类还包含进行全文检索的方法和删除索引的方法。
package services.fileSys;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import lius.index.excel.ExcelIndexer;
import lius.index.html.JTidyHtmlIndexer;
import lius.index.msword.WordIndexer;
import lius.index.pdf.PdfIndexer;
import lius.index.txt.TXTIndexer;
import org.apache.log4j.Logger;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.poi.hslf.model.Slide;
import org.apache.poi.hslf.model.TextRun;
import org.apache.poi.hslf.usermodel.SlideShow;
import org.htmlparser.beans.StringBean;
import services.Base;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import com.nantian.webdt.control.request.RequestWrapper;
import com.nantian.webdt.control.trans.dbaccess.ConnectionFactory;
publicclass Luceneextends Base {
privatestatic ConnectionFactorydbclass =new ConnectionFactory();// 数据库操作实例
privatestaticfinal Loggerlogger = Logger.getLogger(Lucene.class);
private Vectortmpv = null;
//当指定文件夹中没有索引文件时为指定文件夹中每个文件建立索引
publicvoid CreateIndex1() {
IndexWriter writer = null;
try {
//"D:/index",索引建立后存放的目录
writer = new IndexWriter("D:/index",new StandardAnalyzer(),true);
LinkedList list = GetUrl();
int i = 0;
for (Iterator it = list.iterator(); it.hasNext();) {
String filepath = (String) it.next();
String filename = (new File(filepath)).getName();
String filestyle = filepath.substring(
filepath.indexOf(".") + 1, filepath.length());
Document doc = new Document();
Field field = new Field("filename", filename, Field.Store.YES,
Field.Index.TOKENIZED);
doc.add(field);
field = new Field("filepath", filepath, Field.Store.YES,
Field.Index.TOKENIZED);
doc.add(field);
if (filestyle.equalsIgnoreCase("doc")) {
field = new Field("content", LiusWord(filepath),
Field.Store.COMPRESS, Field.Index.TOKENIZED);
doc.add(field);
}
if (filestyle.equalsIgnoreCase("xls")
|| filestyle.equalsIgnoreCase("et")) {
field = new Field("content", LiusExcel(filepath), Field.Store.COMPRESS, Field.Index.TOKENIZED);
doc.add(field);
}
if (filestyle.equalsIgnoreCase("dps")
|| filestyle.equalsIgnoreCase("ppt")) {
field = new Field("content",handlePPT(filepath) ,
Field.Store.COMPRESS, Field.Index.TOKENIZED); doc.add(field);
}
if (filestyle.equalsIgnoreCase("txt")) {
field = new Field("content",LiusTXT(filepath),
Field.Store.COMPRESS, Field.Index.TOKENIZED);
doc.add(field);
}
if (filestyle.equalsIgnoreCase("wps")) {
field = new Field("content",LiusWPS(filepath),
Field.Store.COMPRESS, Field.Index.TOKENIZED);
doc.add(field);
}
if (filestyle.equalsIgnoreCase("pdf")) {
field = new Field("content", LiusPDF(filepath),
Field.Store.COMPRESS, Field.Index.TOKENIZED);
doc.add(field);
}
writer.addDocument(doc);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer !=null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//搜索词条:对此条进行全文检索,方法参数为要检索的关键字或词条,返回值为包含要搜索词条的文件名的集合。
public List SearchCotent(String searchPhrase) {
Document doc = new Document();
String filename = "";
List list = new ArrayList();
try {
//创建IndexSearcher
IndexSearcher searcher = new IndexSearcher("D:/index");
String searchField = "content";
QueryParser parser = new QueryParser(searchField,
new StandardAnalyzer());
Query q = parser.parse(searchPhrase);
//创建Hits
Hits hs = searcher.search(q);
int num = hs.length();
StringBuffer sb = new StringBuffer("");
//显示搜索结果
for (int i = 0; i < num; i++) {
//获得 document
doc = hs.doc(i);
//获得 Field content
filename = doc.getField("filename").stringValue();
list.add(filename);
sb.append("filename :" + filename +"\n");
sb.append("-----------------------------------" +"\n");
}
searcher.close();
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
//删除索引
publicvoid RemoveIndex(String filename) {
Directory dir = FSDirectory.getDirectory("D:/index",false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term("filename",filename);
reader.deleteDocuments(term);
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取文件夹下的所有文件,把可以建立索引的文件单独取出来public LinkedList GetUrl() {
String rootPath = "D:/SYS_FILES";
LinkedList list = new LinkedList();
LinkedList filepath = new LinkedList();
File indexDir = new File(rootPath);
File[] files = indexDir.listFiles();
int i;
String filestyle="";
try {
for (i = 0; i < files.length; i++) {
if (files[i].isDirectory()
&& (!files[i].getName().equalsIgnoreCase("SYS_INFO"))
&& (!files[i].getName().equalsIgnoreCase("SYS_TEMP"))&&((files[i].getName().indexOf(".files"))==-1)) {
list.add(files[i]);
} elseif (files[i].isFile()) {
filestyle = (files[i].getCanonicalPath()).substring(
(files[i].getCanonicalPath()).indexOf(".") + 1,
(files[i].getCanonicalPath()).length());
if (filestyle.equalsIgnoreCase("pdf")
|| filestyle.equalsIgnoreCase("ppt")
|| filestyle.equalsIgnoreCase("xls")
|| filestyle.equalsIgnoreCase("txt")
|| filestyle.equalsIgnoreCase("wps")
|| filestyle.equalsIgnoreCase("doc")
|| filestyle.equalsIgnoreCase("et")
|| filestyle.equalsIgnoreCase("dps")) {
filepath.add(files[i].getCanonicalPath());}
}
}
File tmp;
while (!list.isEmpty()) {
tmp = (File) list.removeFirst();
if (tmp.isDirectory()) {
files = tmp.listFiles();
if (files ==null)
continue;
for (i = 0; i < files.length; i++) {
if (files[i].isDirectory()
&& (!files[i].getName().equalsIgnoreCase("SYS_INFO"))
&& (!files[i].getName().equalsIgnoreCase("SYS_TEMP"))&&((files[i].getName().indexOf(".files"))==-1)) {
list.add(files[i]);
} elseif (files[i].isFile()) {
filestyle = (files[i].getCanonicalPath()).substring( (files[i].getCanonicalPath()).indexOf(".") + 1, (files[i].getCanonicalPath()).length());
if (filestyle.equalsIgnoreCase("pdf")
|| filestyle.equalsIgnoreCase("ppt")
|| filestyle.equalsIgnoreCase("xls")
|| filestyle.equalsIgnoreCase("txt")
|| filestyle.equalsIgnoreCase("wps")
|| filestyle.equalsIgnoreCase("doc")
|| filestyle.equalsIgnoreCase("et")
|| filestyle.equalsIgnoreCase("dps")) { filepath.add(files[i].getCanonicalPath());}
}
}
} else {
filepath.add(files[i].getAbsolutePath());
}
}
} catch (Exception e) {
e.printStackTrace();
}
return filepath;
}
//获取文档内容
public String getText(File f) {
StringBuffer sb = new StringBuffer("");
try {
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
String s = br.readLine();
while (s !=null) {
sb.append(s);
s = br.readLine();
}
br.close();
} catch (Exception e) {
e.printStackTrace();
sb.append("");
}
return sb.toString();
}
//解析PPT文档
publicstatic String handlePPT(String filename){
StringBuffer content = new StringBuffer("");
try{
File file=new File(filename);
if(!file.exists()) {
return content.toString();
}
FileInputStream instream=new FileInputStream(file);
SlideShow ppt = new SlideShow(instream);
Slide[] slides = ppt.getSlides();
for(int i=0;i<slides.length;i++){
TextRun[] t = slides[i].getTextRuns();//为了取得幻灯片的文字内容,建立TextRun
for(int j=0;j<t.length;j++){
content.append(t[j].getText());//这里会将文字内容加到content中去
}
content.append(slides[i].getTitle());
}
}catch(Exception e){
e.printStackTrace();
}
return content.toString();
}
//解析Word文档
public String LiusWord(String filepath) {
WordIndexer wi = new WordIndexer();
File f = new File(filepath);
String content = "";
try {
wi.setStreamToIndex(new FileInputStream(f));
content = wi.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
//解析Excel文档
public String LiusExcel(String filepath) {
ExcelIndexer ei = new ExcelIndexer();
File f = new File(filepath);
String content = "";
try {
ei.setStreamToIndex(new FileInputStream(f));
content = ei.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
//解析PDF文档
public String LiusPDF(String filepath) {
PdfIndexer pi = new PdfIndexer();
File f = new File(filepath);
String content = "";
try {
pi.setStreamToIndex(new FileInputStream(f));
content = pi.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
//解析TXT文档
publicstatic String LiusTXT(String filepath) {
TXTIndexer ti = new TXTIndexer();
File f = new File(filepath);
String content = "";
try {
ti.setStreamToIndex(new FileInputStream(f));
content = ti.getContent();
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
//解析WPS文档
publicstatic String LiusWPS(String filepath) {
String content = "";
try {
File file = new File(filepath.substring(0, filepath.indexOf("."))
+ ".htm");
if (!file.exists()) {
changeWord(filepath, file.getCanonicalPath());
}
JTidyHtmlIndexer ji = new JTidyHtmlIndexer();
File f = new File(file.getCanonicalPath());
ji.setStreamToIndex(new FileInputStream(f));
content = ji.getContent();
content = new String(content.getBytes("iso-8859-1"));
} catch (Exception e) {
e.printStackTrace();
}
return content;
}
//转换WORD为HTM文档
publicstaticvoid changeWord(String docfile, String htmlfile) {
ActiveXComponent app = new ActiveXComponent("Word.Application");//启动word
try {
app.setProperty("Visible",new Variant(false));
//设置word不可见
Object docs = app.getProperty("Documents").toDispatch();
Object doc = Dispatch.invoke( (Dispatch) docs, "Open",
Dispatch.Method,
new Object[] { docfile,new Variant(false),
new Variant(true) },newint[1]).toDispatch();
//打开word文件
Dispatch.invoke((Dispatch) doc,"SaveAs", Dispatch.Method,
new Object[] { htmlfile,new Variant(8) },newint[1]);// 如果8换成9则转换成mht文件
//作为html格式保存到临时文件
Variant f = new Variant(false);
Dispatch.call((Dispatch) doc,"Close", f);
} catch (Exception e) {
e.printStackTrace();
} finally {
app.invoke("Quit",new Variant[] {});
}
}
//解析HTML文档
publicstatic String getHTML(String f) {
StringBean sb = new StringBean();
sb.setLinks(false);
sb.setReplaceNonBreakingSpaces(true);
sb.setCollapse(true);
sb.setURL(f);
String s = sb.getStrings();
try {
s = new String(s.getBytes("gb2312"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return s;
}
//最后我们添加main方法对我们所写的方法进行测试
publicstaticvoid main(String args[]) {
Lucene luc = new Lucene();
// System.out.println("luc.GetUrl() :" + luc.GetUrl());
// luc.CreateIndex1();
// System.out.println("map :" + Lucene.map);
// List list = luc.SearchCotent("自然人担保");
// System.out.println("list :" + list);
// luc.RemoveIndex("index.txt");
// IndexReader ir;
// try {
// ir = IndexReader.open("D:/index");
// Document doc = ir.document(0);
// System.out.println("doc :" + doc);
// } catch (IOException e) {
// //TODO Auto-generated catch block
// e.printStackTrace();
// }
// System.out.println(handlePPT("C:/Documents and Settings/Administrator/桌面/lv.dpt"));
System.out.println(luc.LiusWord("C:/Documents and Settings/Administrator/桌面/问题(民生银行中小企业平台系统)-20100714.doc"));
//System.out.println(luc.ExtractorWord("C:/Documents and Settings/Administrator/桌面/银行中小企业平台项目-详细设计(授信调查报告模块说明).doc"));
} }
第5章 LUCENE的开发总结
到此我们已经将Lucene开发搜索引擎的原理做了基本的介绍,Lucene技术还在不断的发展之中,前面章节的介绍也只是Lucene的一部分,在实际开发中还需要大家一起进一步学习和交流。在开发中大家可以多上网查询资料和参看更多的书籍来学习这门技术。
参考资料:《LUCENE 搜索引擎开发权威经典》于天恩著