[1]
(1)
Lucene是一个基于Java全文搜索引擎,利用它可以轻易地为Java软件加入全文搜寻功能。
(2)
Lucene能做什么?
Lucene可以对任何的数据做索引和搜索。 Lucene不管数据源是什么格式,
只要它能被转化为文字的形式,就可以被Lucene所分析利用。也就是说不管是MS word, Html ,pdf还是其他什么形式的文件只要你可以从中抽取出文字形式的内容就可以被Lucene所用。
(3)
Lucene实现全文检索功能主要有三个步骤
1、 建立索引
建立索引是全文搜索的基础,lucene根据索引搜索用户需要查找的目标文档,如果没有索引也就无所谓搜索。
2、 查找索引
当索引建立好后,就可以对其进行查找了。Lucene所建立的索引是存放在文件系统中的.
3、 更新索引
由于我们要查找的内容总是不断变化的,所以索引文件也不是一成不变的,而是一个不断更新的过程。Lucene也提供了丰富的功能来支持索引的更新功能。
[2]
Lucene几个重要概念:
(1)
Document:一个要进行索引的单元,相当于数据库的一行纪录,任何想要被索引的数据,都必须转化为
Document对象存放。
(2)
Field:Document中的一个字段,相当于数据库中的Column。
IndexWriter:负责将Document写入索引文件。通常情况下,IndexWriter的构造函数包括了以下3个参数
:索引存放的路径,分析器和是否重新创建索引。特别注意的一点,当IndexWriter执行完addDocument方法后,一定要记得调用自身的close方法来关闭它。只有在调用了close方法后,索引器才会将存放在内在中的所有内容写入磁盘并关闭输出流。
Analyzer:分析器,主要用于文本分词。常用的有StandardAnalyzer分析器,StopAnalyzer分析器,
WhitespaceAnalyzer分析器等。
Field对象
从Lucene的源代码中,可以看出Field 典型构造函数如下:
Field(String name, String value, Field.Store store, Field.Index index)
其中:
Field.Index有四种属性,分别是:
Field.Index.TOKENIZED:分词索引
Field.Index.UN_TOKENIZED:不分词进行索引,如作者名,日期等,不再需要分。
Field.Index.NO:不进行索引,存放不能被搜索的内容如文档的一些附加属性如文档类型, URL等。
Field.Index.NO_NORMS:不分词,建索引.但是Field的值不像通常那样被保存,而是只取一个byte,这样节约存储空间。
Field.Store也有三个属性,分别是:
Field.Store.YES:索引文件本来只存储索引数据, 此设计将原文内容直接也存储在索引文件中,如文档的标题。
Field.Store.NO:原文不存储在索引文件中,搜索结果命中后,再根据其他附加属性如文件的Path,数据库的主键等,重新连接打开原文,适合原文内容较大的情况。
Field.Store.COMPRESS 压缩存储。
(3)
Directory:索引存放的位置。Lucene提供了两种索引存放的位置,一种是磁盘,一种是内存。一般情况将索引放在磁盘上;相应地Lucene提供了FSDirectory和RAMDirectory两个类。
(4)
Segment:段,是Lucene索引文件的最基本的一个单位。Lucene说到底就是不断加入新的Segment,然后按一定的规则算法合并不同的Segment以合成新的Segment。
[3]
用lucene作搜索,主要步骤:
(1)建立索引:
1.创建IndexWriter.
2.创建Document,将各个document放入writer中.
3.对于数据库查询,先将要查询的内容,从数据库中获取出来.用rs进行循环放入到doc中.
同时应用增量索引:用一个文本记录当前已做索引的最后一个ID,下次做索引,从此文件获取ID,从它的
下一个ID进行做索引追加到原来的索引文件中.而不是重头做索引.
(2)进行搜索:
1.new IndexSearcher(索引目录)=searcher
2.添加各种查询器,生成query.
3.searcher.search(query);
4.返回Hits,用hits.length(),hits.doc(i)来输出doc,再用doc.get("field名")输出.
(3)
进行多域搜索:
0.new IndexSearcher(索引目录)创建多个要查询的索引文件,再放于数组中,最近放于MultiSearcher
1.建立field[],occurs[]
2.建立多域查询器:MultiFieldQueryParser
3.searcher.search(query);返回Hits
4.用Hits.doc(n);可以遍历出Document
(4)
搜索数据库:
1.可轻在根目录下建立一个index的目录,用来存放index.
2.对数据表某表进行查询,将查询出来的字段结果-->放于document中-->通过indexwriter放入index目录中.
3.查询时,同上.
4.在JSP页面,导入lucene类.可以设置一个field,填写查询条件,提交到hits=searcher.search()中,将
hits结果,放于<%%>中显示出来.
(5)
增量查询,方法基础同上.只是在创建index时,不同:
1.增加多一个文本文件,来存放建立索引的数据表最后的ID,对数据表进行查询时,在SQL中添加 where id > **,则是采用增量索引.而不是重头再来.
[4]
实例
(1)数据库索引实例:
索引辅助类:
public class JLuceneUtils {
public static Document createConfigDoc(String id,String confContext)
{
Document doc= new Document();
doc.add( new Field( "id",id,Field.Store.YES,Field.Index.TOKENIZED));
doc.add( new Field( "confContext",confContext,Field.Store.YES,Field.Index.TOKENIZED));
return doc;
}
public static String getStoreId(String idPath)
{
String storeId= null;
try {
File file = new File(idPath);
if (!file.exists())
file.createNewFile();
FileReader reader= new FileReader(file);
BufferedReader br= new BufferedReader(reader);
storeId=br.readLine();
if(storeId== null||storeId=="")
storeId= "0";
br.close();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
return storeId;
}
public static boolean writeStoreId(String idPath,String id )
{
boolean b = false;
try {
File file = new File(idPath);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fw = new FileWriter(idPath);
PrintWriter out = new PrintWriter(fw);
out.write(id);
out.close();
fw.close();
b= true;
} catch (IOException e) {
e.printStackTrace();
}
return b;
}
public static Hits luceneSearch(String indexPath,String searchMess)
{
Hits rs= null;
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
QueryParser parser = new QueryParser( "id", new StandardAnalyzer());
Query query=parser.parse(searchMess);
rs=searcher.search(query);
} catch (Exception e) {
e.printStackTrace();
}
return rs;
}
}
索引方法:
public String luceneSearch()
{
String idPath= "D:/tomcat_bbs/webapps/BBSCS_8_0_3/storeId.txt";
String indexPath= "D:/tomcat_bbs/webapps/BBSCS_8_0_3/index";
try {
File file = new File(indexPath);
if (!file.exists()) {
//for iBatis
List bbsConfigs = (List) sqlMapClientTemplate.queryForList( "getAllBBSConfig");
IndexWriter writer = new IndexWriter(indexPath, new StandardAnalyzer(), true);
for ( int a = 0; a < bbsConfigs.size(); a++) {
BBSConfig bbsConfig = (BBSConfig) bbsConfigs.get(a);
Document doc = JLuceneUtils.createConfigDoc(bbsConfig.getId(), bbsConfig.getConfContext());
writer.addDocument(doc);
}
writer.optimize(); //优化索引文件
writer.close();
}
Hits hits = JLuceneUtils.luceneSearch(indexPath, searchMess);
System.out.println( "----------hits.length():" + hits.length());
for ( int a = 0; a < hits.length(); a++) {
Document doc2 = (Document) hits.doc(a);
System.out.println(searchMess + "的值是:"+ doc2.get( "confContext"));
}
} catch (Exception e) {
e.printStackTrace();
}
// return "search";
return SUCCESS;
}
(2)增量索引,各种高级索引
增量查询:
若用增量查询,则创建索引的逻辑,应该在用户点击索引时,先对storeID.txt中的ID与相应数据表中的ID进行对比,若有新记录则进行'创建索引',若无,则直接进行对现在索引文件的查询.
IndexWriter writer = new IndexWriter(indexPath,new StandardAnalyzer(), false);
对于增量索引,创建索引与搜索不能放在一起.因为增量索引,是追加,若放在一起,则每次进行搜索前,都会追回索引文件中已有的索引,造成重复.
IndexWriter writer = new IndexWriter(indexPath,new StandardAnalyzer(), true);
对于要索引的对象,若不是很大的话,不要用增量索引.因为增量索引涉及到更新索引问题.若用创建新索引,即索引文件会是最新的.
对应数据表(jason_lucene):
id name info date
1 jason 中国人 20090610
2 hwj 中原人 20090612
3 hello 国人的事怀20090616
4 jaosn 大家好 20090619
辅助类,同上.
实现方法:
public String luceneSearch2()
{
try {
String idPath = "D:/tomcat_bbs/webapps/BBSCS_8_0_3/index/storeId.txt";
String indexPath = "D:/tomcat_bbs/webapps/BBSCS_8_0_3/index";
//createIdnex 考虑增量功能
String storeId = JLuceneUtils.getStoreId(idPath);
String sql = "select id, name,info,date from jason_lucene where id >"+ storeId+ " order by id";
ResultSet rs2 = this.getResult(sql);
String id= "0";
/*
IndexWriter writer = new IndexWriter(indexPath,new StandardAnalyzer(), false);
对于增量索引,创建索引与搜索不能放在一起.因为增量索引,是追加,若放在一起,则每次进行搜索前,都
会追回索引文件中已有的索引,造成重复.
IndexWriter writer = new IndexWriter(indexPath,new StandardAnalyzer(), true);
对于要索引的对象,若不是很大的话,不要用增量索引.因为增量索引涉及到更新索引问题.若用创建新索
引,即索引文件会是最新的.
*/
IndexWriter writer = new IndexWriter(indexPath, new StandardAnalyzer(), false); //区别就在此!
while(rs2.next())
{
Document doc= new Document();
doc.add( new Field( "name",rs2.getString( "name"),Field.Store.YES,Field.Index.TOKENIZED));
doc.add( new Field( "info",rs2.getString( "info"),Field.Store.YES,Field.Index.TOKENIZED));
doc.add( new Field( "date",rs2.getString( "date"),Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
id=rs2.getString( "id");
}
writer.optimize();
writer.close();
boolean isSuccess=JLuceneUtils.writeStoreId(idPath, id);
System.out.println( "最新索引ID写入storeId.txt文件成功:"+isSuccess);
//删除索引:
IndexReader reader=IndexReader.open(indexPath);
Term delterm=new Term("name","hwj"); //好像不支持中文.
reader.deleteDocuments(delterm);
reader.close();
//更新索引:即是先用上面的方法删除特定的索引,然后再对特定的索引进行添加.添加方法也同上.
//查询:
IndexSearcher searcher = new IndexSearcher(indexPath);
//1.普通查询:列出包含查询关键字的记录.可在关键字的前面添加+/-.如:
//+jason -hwj:表示一定要包含jason,一定不要包含hwj
// QueryParser parser = new QueryParser("info",new StandardAnalyzer());
// Query query=parser.parse(searchMess);
//2进行通配符查询:用*,表示0个或多个;?,表示一个. jas*,可查询出jaosn,jason.
Query query1=new WildcardQuery(new Term("name",searchMess));
//3模糊查询:若查询的关键字是jason ,则会查出jason,jaosn等相似的记录
// Query query=new FuzzyQuery(new Term("name",searchMess));
//4 范围查询:查询出在一个范围内的记录
Term term1=new Term("date","20090611");
Term term2=new Term("date","20090618");
Query query2=new RangeQuery(term1,term2,true);
//5多条件查询:
BooleanQuery query=new BooleanQuery();
query.add(query1, BooleanClause.Occur.MUST_NOT);
query.add(query2, BooleanClause.Occur.MUST);
Hits hits=searcher.search(query);
System.out.println( "符合查询条件'"+searchMess+ "'的共有:" + hits.length() + "个结果");
for ( int a = 0; a < hits.length(); a++) {
Document doc2 = (Document) hits.doc(a);
System.out.println(doc2.get( "name") + "的值是:"+ doc2.get( "info"));
}
} catch (Exception e) {
e.printStackTrace();
}
return SUCCESS;
}
(3)多域索引
public class LuceneTest {
public static void main(String[] args) {
try {
LuceneTest luceneTest = new LuceneTest();
// 创建索引
luceneTest.index();
// 在索引所在目录下搜索"中国 金牌"
luceneTest.search( "中国 金牌");
luceneTest.search( "2008 jaosn");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println( "ok");
}
public void index() throws Exception {
/* 创建索引初始化,执行这些语句将创建或清空d:\\save\\目录下所有索引 */
IndexWriter writer1 = new IndexWriter("d:\\save\\",
new StandardAnalyzer(), true);
writer1.close();
/*
* 往创建的初始化索引中添加索引内容,StandardAnalyzer表示用lucene自带的标准分词机制,
* false表示不覆盖原来该目录的索引,细心的读者可能已经发现, 这句话和上面的那句就这个false不一样
*/
IndexWriter writer2 = new IndexWriter("d:\\save\\",
new StandardAnalyzer(), false);
/* 创建一份文件 */
Document doc1 = new Document();
/*
* 创建一个域ArticleTitle,并往这个域里面添加内容 "Field.Store.YES"表示域里面的内容将被存储到索引
* "Field.Index.TOKENIZED"表示域里面的内容将被索引,以便用来搜索
*/
Field field1 = new Field( "ArticleTitle", "北京2008年奥运会", Field.Store.YES,
Field.Index.TOKENIZED);
/* 往文件里添加这个域 */
doc1.add(field1);
/* 同理:创建另外一个域ArticleText,并往这个域里面添加内容 */
Field field2 = new Field( "ArticleText", "这是一届创造奇迹、超越梦想的奥运会.......",
Field.Store.YES, Field.Index.TOKENIZED);
doc1.add(field2);
// 在这里还可以添加其他域
/* 添加这份文件到索引 */
writer2.addDocument(doc1);
/* 同理:创建第二份文件 */
Document doc2 = new Document();
field1 = new Field( "ArticleTitle", "中国获得全球赞誉", Field.Store.YES,
Field.Index.TOKENIZED);
doc2.add(field1);
field2 = new Field( "ArticleText", "中国所取得的金牌总数排行榜的榜首........",
Field.Store.YES, Field.Index.TOKENIZED);
doc2.add(field2);
writer2.addDocument(doc2);
// 在这里可以添加其他文件
/* 关闭 */
writer2.close();
}
public void search(String serchString) throws Exception {
/* 创建一个搜索,搜索刚才创建的d:\\save\\目录下的索引 */
IndexSearcher indexSearcher = new IndexSearcher("d:\\save\\");
/* 在这里我们只需要搜索一个目录 */
IndexSearcher indexSearchers[] = { indexSearcher };
/* 我们需要搜索两个域"ArticleTitle", "ArticleText"里面的内容 */
String[] fields = { "ArticleTitle", "ArticleText" };
/* 下面这个表示要同时搜索这两个域,而且只要一个域里面有满足我们搜索的内容就行
BooleanClause.Occur.MUST表示and,
BooleanClause.Occur.MUST_NOT表示not,
BooleanClause.Occur.SHOULD表示or.
*/
BooleanClause.Occur[] clauses = { BooleanClause.Occur.SHOULD,
BooleanClause.Occur.SHOULD };
/*
* MultiFieldQueryParser表示多个域解析,
* 同时可以解析含空格的字符串,如果我们搜索"中国 金牌",根据前面的索引,显然搜到的是第二份文件
*/
Query query = MultiFieldQueryParser.parse(serchString, fields, clauses,
new StandardAnalyzer());
/* Multisearcher表示多目录搜索,在这里我们只有一个目录 */
MultiSearcher searcher = new MultiSearcher(indexSearchers);
/* 开始搜索 */
Hits h = searcher.search(query);
/* 把搜索出来的所有文件打印出来 */
for ( int i = 0; i < h.length(); i++) {
/* 打印出文件里面ArticleTitle域里面的内容 */
System.out.println(h.doc(i).get( "ArticleTitle"));
/* 打印出文件里面ArticleText域里面的内容 */
System.out.println(h.doc(i).get( "ArticleText"));
}
/* 关闭 */
searcher.close();
}
}
补充内容:
term
term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现
的field。
如:
Term term1=new Term("date","20090611");
date即是对应的field:即对应document中的field==> new Field("field名".....)
20090611即是对应的搜索值.
(3)
将索引直接写在内存
你需要首先创建一个RAMDirectory,并将其传给writer,代码如下:
Directory dir = new RAMDirectory();
IndexWriter writer = new IndexWriter(dir, 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();
(4)
如何删除索引
lucene本身支持两种删除模式
1,DeleteDocument(int docNum)
2,DeleteDocuments(Term term)
一般使用的是第二种.这种方法实际上是首先根据参数term执行一个搜索操作,然后把搜索到的结果批量
删除了。我们可以通过这个方法提供一个严格的查询条件,达到删除指定document的目的。
下面给出一个例子:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(field, key);
reader.deleteDocuments(term);
reader.close();
(5)
如何更新索引
lucene并没有提供专门的索引更新方法,我们需要先将相应的document删除,然后再将新的document加
入索引。例如:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(“title”, “lucene introduction”);
reader.deleteDocuments(term);
reader.close();
IndexWriter writer = new IndexWriter(dir, 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 is funny", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
[2]
各种各样的Query
下面我们看看lucene到底允许我们进行哪些查询操作:
(1)
TermQuery
首先介绍最基本的查询,如果你想执行一个这样的查询:“在content域中包含‘lucene’的document”
,那么你可以用TermQuery:
Term t = new Term("content", " lucene";
Query query = new TermQuery(t);
(2)
PhraseQuery
你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5个字的距离内)的文章,超过这个
距离的不予考虑,你可以:
PhraseQuery query = new PhraseQuery();
query.setSlop(5);
query.add(new Term("content ", “中”));
query.add(new Term(“content”, “日”));
PrefixQuery
如果你想搜以‘中’开头的词语,你可以用PrefixQuery:
PrefixQuery query = new PrefixQuery(new Term("content ", "中");
(3)
还有:
BooleanQuery
WildcardQuery
FuzzyQuery
RangeQuery
(4)
QueryParser,可以对上面各种query进行整合实现.
下面我们对应每种Query在QueryParser中演示一下:
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,完成复杂操作.
如:
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);
即表示:
标题或正文包括lucene,并且时间在20060101到20060130之间的文章.
(5)
Filter
filter的作用就是限制只查询索引的某个子集,它的作用有点像SQL语句里的 where,但又有区别,它不是正规查询的一部分,只是对数据源进行预处理,然后交给查询语句。注意它执行的是预处理,而不是对查询结果进行过滤,所以使用filter的代价是很大的,它可能会使一次查询耗时提高一百倍。
最常用的filter是RangeFilter和QueryFilter。RangeFilter是设定只搜索指定范围内的索引;QueryFilter是在上次查询的结果中搜索。
Filter的使用非常简单,你只需创建一个filter实例,然后把它传给searcher。继续上面的例子,查询“时间在20060101到20060130之间的文章”除了将限制写在query string中,你还可以写在RangeFilter
中:
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);
(6)
Sort
有时你想要一个排好序的结果集,就像SQL语句的“order by”,lucene能做到:通过Sort。
Sort sort Sort(“time”); //相当于SQL的“order by time”
Sort sort = new Sort(“time”, true); // 相当于SQL的“order by time desc”
下面是一个完整的例子:
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);
Sort sort = new Sort(“time”);
Hits hits = is.search(query, filter, sort);
(7)
优化搜索性能
由以上数据可以得出结论:
1、 尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用cache还好,最好不使用filter。
2、 在不能降低时间精度的情况下,使用cache能带了10倍左右的性能提高。
Lucene的中文分词器(PaodingAnalyzer)+高亮显示
[1]
分词功能介绍
分词模块对于搜索是很重要的。搜索其实就是对分词的每一个片段进行查询,检查是否匹配.
(1)
Lucene版本是lucene-2.4.0,它(如standardAnalyzer)已经能够支持中文分词,但它是采用一元分词(逐字拆分)的方法,即把每一个汉字当作是一个词,这样会:
使建立的索引非常庞大,
检索缓慢,
同时查询的准确性不高,会影响查询效率.
如:
字符串"中华为何怎么做",用standardAnalyzer一元切词,结果如下:
中 华 为 何 怎 么 做
当我们想查询"华为"时,"华为"分为"华 为",分别与上面的"华 为"相匹配.但这不是我们想查询的结果.
又如:搜索“和服”会出现“产品和服务”,搜索“海尔”会出现“海尔德”.
所以有必要给文本增加词的边界信息以提高检索精确度。这里我就介绍最为常用的"庖丁解牛" 分词包.
(2)
对于英语分词
以单词之间的间隔来划分.
无论是standardAnalyzer,PaodingAnalyzer,英文的切词,一个连接的单词,不会被切成两分.如:
AttachFileSize,只能切为AttachFileSize;
Attach File Size,会被切成Attach File Size
若要用Attach去查找AttachFileSize,则须用通配符.
而中文切词上:如:"中华为何怎么做"
standardAnalyzer:一元切词(如:中 华 为 何 怎 么 做)
PaodingAnalyzer:二元切词(如:中华 华为 为何 怎么 做 )
(3)
安装"庖丁解牛"分词包:
庖丁中文分词需要一套词典,这些词典需要统一存储在某个目录下,这个目录称为词典安装目录。词典安装目录可以是文件系统的任何目录,它不依赖于应用程序的运行目录。将词典拷贝到词典安装目录的过程称为安装词典。增加、删除、修改词典目录下的词典的过程称为自定制词典。
其安装的方法:
1.
"庖丁解牛"的下载地址是 http://code.google.com/p/paoding/downloads/list,下载好后解压,我解压在E:\paoding2_0_4;
2.
进入该目录,首先将paoding-analysis.jar拷贝到项目的WEB-INF/lib目录;
3.
接着需要设置环境变量PAODING_DIC_HOME,变量名:PAODING_DIC_HOME 变量值:E:\paoding2_0_4\dic
4.
第三步将E:\paoding2_0_4\src目录下的paoding-dic-home.properties属性文件拷贝到项目的src目录下(注意是src的根目录下),添加一行aoding.dic.home=E:/paoding2_0_4/dic.
好了,到这里,已经完成了Lucene和"庖丁解牛"的整合.
(4)
查看各种分词器的分词情况:
public static void main(String[] args) throws Exception {
//StandardAnalyzer: 一元分词
//Analyzer analyzer = new StandardAnalyzer();
//PaodingAnalyzer: 二元分词
Analyzer analyzer = new PaodingAnalyzer();
String indexStr = "我的QQ号码是58472399";
StringReader reader = new StringReader(indexStr);
TokenStream ts = analyzer.tokenStream(indexStr, reader);
Token t = ts.next();
while (t != null) {
System.out.print(t.termText()+" ");
t = ts.next();
}
}
StandardAnalyzer分词结果:我 的 qq 号 码 是 58472399
PaodingAnalyzer分词结果:我的 qq 号码 58472399
如果把indexStr换成是"中华人民共和国万岁" ,那么分词结果为:
中华 华人 人民 共和 共和国 万岁
很明显,它的分词效果要比Lucene自带的中文分词器的效果好的多.Lucene自带分词器是将中文逐字拆分的,这是最为原始的分词方法,现在大都不采用.
[2]
关键字高亮显示
关键字高亮显示也就是在页面显示时,事先对要显示的内容处理,抽取出关键字并加亮,这里抽取关键字也是用lucene,lucene自带有heightlight包就可以实现此功能。
Highlighter包括了三个主要部分:段划分器(Fragmenter)、计分器(Scorer)和格式化器(Formatter)。
通常要用到的几个重要类有:
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleFragmenter;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
1)SimpleFragmenter
Highlighter利用Fragmenter将原始文本分割成多个片段。内置的SimpleFragmenter将原始文本分割成相同大小的片段,片段默认的大小为100个字符。这个大小是可控制的。
2)SimpleHTMLFormatter:用来控制你要加亮的关键字的高亮方式
此类有2个构造方法
1:SimpleHTMLFormatter()默认的构造方法.加亮方式:<B>关键字</B>
相当于如下代码:
Formatter formatter = new Formatter() {
public String highlightTerm(String srcText, TokenGroup g) {
if (g.getTotalScore() <= 0) {
return srcText;
}
return "<b>" + srcText + "</b>";
}
};
2:SimpleHTMLFormatter(String preTag, String postTag).加亮方式:preTag关键字postTag
3)QueryScorer
QueryScorer 是内置的计分器。计分器的工作首先是将片段排序。QueryScorer使用的项是从用户输入的查询中得到的;它会从原始输入的单词、词组和布尔查询中提取项,并且基于相应的加权因子(boost factor)给它们加权。
为了便于QueryScoere使用,还必须对查询的原始形式进行重写。比如,带通配符查询、模糊查询、前缀查询以及范围查询 等,都被重写为BoolenaQuery中所使用的项。在将Query实例传递到QueryScorer之前,可以调用
Query.rewrite (IndexReader)方法来重写Query对象.
[3]
实例之
使用Lucene+Paoding构建SSH2系统的站内搜索
说明:
(1)
用paodingAnalyzer进行分词的操作,与用standardAnalyzer几乎一样,只是:
在创建索引时:
用
IndexWriter writer = new IndexWriter(indexPath,new PaodingAnalyzer(), true);
代替:
IndexWriter writer = new IndexWriter(indexPath,new StandardAnalyzer(), true);
用:
doc.add(new Field
("id",id,Field.Store.YES,Field.Index.TOKENIZED,Field.TermVector.WITH_POSITIONS_OFFSETS));
代替:
doc.add(new Field("id",id,Field.Store.YES,Field.Index.TOKENIZED));
在搜索时,
用:
QueryParser parser = new QueryParser("id",new PaodingAnalyzer());
代替:
QueryParser parser = new QueryParser("id",new StandardAnalyzer());
如下:
public static Document createConfigDoc(String id,String confContext)
{
Document doc=new Document();
//采用默认的standardAnalyzer
// doc.add(new Field("id",id,Field.Store.YES,Field.Index.TOKENIZED));
// doc.add(new Field
("confContext",confContext,Field.Store.YES,Field.Index.TOKENIZED));
//采用paodingAnalyzer
doc.add(new Field
("id",id,Field.Store.YES,Field.Index.TOKENIZED,Field.TermVector.WITH_POSITIONS_OFFSETS));
doc.add(new Field
("confContext",confContext,Field.Store.YES,Field.Index.TOKENIZED,Field.TermVector.WITH_POSI
TIONS_OFFSETS));
return doc;
}
(2)
对于高亮显示:请见代码;
如下:
try {
IndexReader reader = IndexReader.open(indexPath);
IndexSearcher searcher = new IndexSearcher(indexPath);
QueryParser parser = new QueryParser("id",analyzer);
Query query=parser.parse(searchMess);
query = query.rewrite(reader);
System.out.println("Searching for: " + query.toString());
hits=searcher.search(query);
String SearchMess="";
String SearchValue="";
for (int i = 0; i < hits.length(); i++)
{
String highLightText="";
Document doc = hits.doc(i);
SearchMess=doc.get("id");
SearchValue=doc.get("confContext");
System.out.println("-----高亮前------SearchMess:\n\t" +
SearchMess);
// 设置高亮
SimpleHTMLFormatter formatter = new SimpleHTMLFormatter
("<font color='red'><b>", "</b></font>");
Highlighter highlighter = new Highlighter(formatter, new
QueryScorer(query));
//Highlighter利用Fragmenter将原始文本分割成多个片段。片段默
认的大小为100个字符。
//设置每一片段的大小
highlighter.setTextFragmenter(new SimpleFragmenter(200));
if (SearchMess != null) {
//id:为上面查询分析器中设置查询的项:QueryParser
parser = new QueryParser("id",analyzer);
//SearchMess:为上面查询结果中的内容:Document doc =
hits.doc(i);SearchMess=doc.get("id");
//若设置出错,则高亮显示为null
TokenStream tokenStream = analyzer.tokenStream("id",new
StringReader(SearchMess));
//highLightText,则是高亮显示的结果.下面的highLightText,输出
格式如:
//中<font color='red'><b>华为</b></font>何
highLightText = highlighter.getBestFragment
(tokenStream,SearchMess);
v.add(highLightText);
v.add(SearchValue);
System.out.println("-----高亮后------highLightText:\n\t" +
highLightText);
}
}
searcher.close();
} catch (Exception e) {
e.printStackTrace();
}
(3)
对于创建索引,与搜索过程:
创建索引,
(若搜索文件比较小,也可是:创建索引-->进行搜索.但一般情况下,采用创建索引,与搜索过程分离.如下)
我们可以利用Quartz在夜间自动重新创建lucene最新的index,从而保证索引内容与数据库中的内容相对
比较一致(相当于旧索引的更新.因为用户可能会修改原记录的内容,所以索引对应的内容也应该被相应的更新);
当用户进行搜索时,我们进行:
当用户进行搜索时,可以设置"增量索引"的方式,把新增的DB记录追加到索引文件中,从而保证用户最新的消息可以被搜索到.
然后再进行搜索操作.
如下:
增量索引方式,可以如上面的实现那样设置;这里只列出定时创建索引代码:
创建索引代码:
public void createLuceneIndex()
{
String idPath="D:/tomcat_bbs/webapps/BBSCS_8_0_3/storeId.txt";
String indexPath="D:/tomcat_bbs/webapps/BBSCS_8_0_3/index";
try {
//for iBatis
List bbsConfigs = (List) sqlMapClientTemplate.queryForList
("getAllBBSConfig");
//IndexWriter writer = new IndexWriter(indexPath,new
StandardAnalyzer(), true);
IndexWriter writer = new IndexWriter(indexPath,new PaodingAnalyzer
(), true);
for (int a = 0; a < bbsConfigs.size(); a++) {
BBSConfig bbsConfig = (BBSConfig) bbsConfigs.get(a);
Document doc = JLuceneUtils.createConfigDoc
(bbsConfig.getId(), bbsConfig.getConfContext());
writer.addDocument(doc);
}
writer.optimize(); //优化索引文件
writer.close();
System.out.println("-------------定时创建lucene的Indexe success!");
} catch (Exception e) {
e.printStackTrace();
}
}
定时代码:
<!-- 利用Quartz在夜间自动重新创建lucene最新的index. -->
<bean id="creLuceneIndexJobDetailBean"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref local="JLogin"/>
</property>
<property name="targetMethod">
<value>createLuceneIndex</value>
</property>
</bean>
<bean id="LuceneIndexCronTriggerBean"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref local="creLuceneIndexJobDetailBean"/>
</property>
<property name="cronExpression">
<value>0 0 4 * * ?</value>
</property>
</bean>
<bean id="schedulerFactoryBean"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTriggerBean"/>
<ref bean="jasonGetPersonCronTriggerBean"/>
<ref bean="mailCronTriggerBean"/>
<ref bean="LuceneIndexCronTriggerBean"/> -->此处
</list>
</property>
</bean>
(4)
最后调用后,将highLightText放于session中,通过return 'search';返回到前台,再获取显示.因为是前台是HTML/JSP页面,所以,如:highLightText="中<font color='red'><b>华为</b></font>何",就会显示出高亮效果.若用system.out打印,则只能看到:
中<font color='red'><b>华为</b></font>何