您的位置:首页 > 其它

近两年项目回顾系列——基于lucene+IKanalyzer实现全文检索

2013-04-19 14:03 435 查看
  回顾最近这两年,技术及技术之外都成长了很多。因为项目一直忙而荒废了博客,今日开贴,回顾这两年主要用到的东西。

  前半年主要做了总局的网上申请系统、网上查询系统、中国驰名商标申报系统,后台大都是的struts2+spring+hibernate/ibatis完成后台架构。前台是jsp/Ext+jQuery ajax基于json与后台交互。都是常规技术,没什么可说的。后来一年半边回归之前的BUG,边开发最核心的CTCS3系统。主要技术是:Flex+BlazeDS+Hibernate+Spring。所用的主要技术,将在这个系列的博文中一一列出来,也供有类似需求的朋友参考。

今天先回顾进项目组后第一个有挑战的项目:国家XX总局企业争议案件全文检索系统。

  在此项目之前对全文检索的了解仅仅在于之前在IBM developerworks上零散看过lucene的文章、以及地铁上碎片时间看过的《lucene in action》。之前只写过简单的demo。总局要求提供一个易用、易拓展的全文检索接口(包括Http、WebServices接口)、因为之前使用过lucene做过demo,拿到需求后google了几把,确定lucene基本可以满足要求,时间比较紧,可以暂时不用再去学习solr :)

   lucene是apache孵化的一个高效的Java全文检索库。可方便地为我们提供索引创建和检索的过程。具体可以参见《lucene in action》和园子里“觉先”的博文http://www.cnblogs.com/forfuture1978/category/300665.html

其大体的原理如下:



主要分索引、检索两个过程:

索引:

  lucene可以将我们的文本文档建立反向索引
  什么是反向索引呢?举个例子:现在有100个文本文档(比如100个.txt文件),我们想从他们中找出哪一个文档含有字符串“lucene”,则需要在这几个文件中挨个查找。但是下一次我们要查找哪个文件含有“hadoop”字符串时,又需要一个一个文档地查找。这样效率很低。lucene将我们的文本进行分词,抽取成形如下图的结构:



左边保存的是一系列字符串,称为词典

每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。

这样在检索的时候,直接找到lucene、hadoop的词典,即可迅速定位包含这些词的文章列表。这样就达到了一次所有、多次使用的目的。

索引过程:

1) 有一系列被索引文件

2) 被索引文件经过语法分析和语言处理形成一系列词(Term)

3) 经过索引创建形成词典和反向索引表。

4) 通过索引存储将索引写入硬盘。

搜索过程:

a) 用户输入查询语句。

b) 对查询语句经过语法分析和语言分析得到一系列词(Term)

c) 通过语法分析得到一个查询树。

d) 通过索引存储将索引读入到内存。

e) 利用查询树搜索索引,从而得到每个词(Term)的文档链表,对文档链表进行交,差,并得到结果文档。

f) 将搜索到的结果文档对查询的相关性进行排序。(权重算法、空间向量夹角等)

g) 返回查询结果给用户。

lucene的主要组件:



创建索引的主要的对象有:

org.apache.lucene.store.Directory
org.apache.lucene.analysis.Analyzer

org.apache.lucene.index.IndexWriter
org.apache.lucene.document.Field

org.apache.lucene.document.Document
检索的主要对象有:

org.apache.lucene.search.IndexSearcher

org.apache.lucene.queryParser.QueryParser

org.apache.lucene.search.Query

org.apache.lucene.search.TopDocs

org.apache.lucene.search.ScoreDoc

  本项目分词器在IK-analyzer、庖丁paoding、imdict中抉择。经过简单考察,似乎IK的demo资料较多且效率更高。后来决定使用lucene+IKanalyzer+struts2+spring+JDBC+XFire完成一个对外提供http、webservices检索接口,返回Json字符串。

最终搭出来的环境如下:



简单说一下目录:

src下java文件在常规的action、service、servlet、util中,spring路径是所有的IoC配置,META-INF下是XFire的WebServices配置。

IKAnalyzer.cfg.xml是IK分词器的主配置,包括定义拓展词典、停顿词典。内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--配置扩展字典 -->
<entry key="ext_dict">ext.dic;</entry>

<!--配置扩展的停止词字典-->
<entry key="ext_stopwords">stopword.dic;</entry>

</properties>


ext.dic是上述文件中配置的IK分词器的拓展词库,新的流行词或项目需求中的特殊词可以自行添加,如:“微博”、“通达中心”、“我处”、“屌丝”等等(去库里看了一眼,还真有人注册【屌丝】牌的商品 - -|||)。

stopword.dic是IK分词的停词,如:a/an/the等等。

以下主要说说本项目中索引的创建、更新以及检索的过程:

一、索引创建:

  库里数据量可能很大,动辄数百万、千万条。我们使用了增量索引的方式。同时由于我们的全文检索数据来自多个库,而且可能偶尔修改要索引的库、表、字段信息,所以使用WebRoot/searchdict/SearchItems.xml这个xml来描述索引任务。内容包括:

<row dictName="T_XXX" >
<indexFilePath comments="索引文件位置">D:/temp/index/twkcase/</indexFilePath>
<SearchName comments="全文检索系统显示名称">XX案件情况</SearchName>
<SearchId comments="索引文件中的标识">1</SearchId>
<DBName comments="需要索引的数据库名称SID">sid</DBName>
<UName comments="需要索引的数据库用户名">name</UName>
<TName comments="需要索引的表名">T_WK_XXXX</TName>
<FName   comments="需要索引的字段名称">XXX,XXX</FName>
<PFieldName  comments="需要索引的字段并分词">CASE_XXX</PFieldName>
<Amount   comments="每次生成索引查询的数量">1000</Amount>
<startNum   comments="每次索引数据的起始编号">0</startNum>
</row>


上述XML描述了索引T_WK_XXXX表数个字段,其中CASE_XXX字段需要分词,每次根据表中的create_time、update_time字段order by之后增量1000条,然后修改此XML的startNum值。下次增量时从新的startNum向后新索引1000条。

当应用启动时会从startNum开始将全表创建一次索引,之后用JDK的timer(任务太简单,用不着quartz)每隔2min读一次上述XML,rownum加Amount,创建完索引后更新XML。

索引创建的语句(去除了try catch以及业务处理代码):

//通过路径、分词器等参数创建indexWriter对象
indexWriter = new IndexWriter(directory, analyzer, isEmpty,IndexWriter.MaxFieldLength.LIMITED);        //isEmpty为true重新索引,false增量索引


//使用XML中的配置拿到合适的数据源,查到结果集后,构建一个个lucene的Filed并逐一映射到Document对象中:
while (rs.next()) {
  //org.apache.lucene.document.Document
  doc = new Document();
  if(rs.getObject(2)!=null){
  //表的ID,不用分词
    doc.add(new Field("id", rs.getObject(2).toString(),Field.Store.YES, Field.Index.NOT_ANALYZED));
  }
  //循环XML中的索引字段,凡是PFieldName中配置的,均需要分词,其他的filed不用分词
  for(int i=0;i<fields.length;i++){
   //判断是否需要分词
   if(Arrays.asList(pfields).contains(fields[i])){
     isANALYZED=true;
   }
if(isANALYZED){
      doc.add(new Field(fields[i], rs.getObject((i+3)).toString(),Field.Store.YES, Field.Index.ANALYZED));
   }else{
      doc.add(new Field(fields[i], rs.getObject((i+3)).toString(),Field.Store.YES, Field.Index.NOT_ANALYZED));
   }
  }
}


写索引:

indexWriter.addDocument(doc);

indexWriter.optimize();  //将小的segment合并

 indexWriter.close(); //释放资源

//然后重新写增量索引配置文件:SearchItems.xml,修改下次更新索引的数据库起始位置(略)

二、检索内容:

  主要使用org.apache.lucene.search.IndexSearcher、FSDirectory、TopDocs完成

/**
*
* @author liujian
* @version 1.0
* 日期:2012-8-9
* @param keyWord 关键字
* @param curPage 当前页码
* @param bean 增量索引XML映射bean
* @param isShowPaging 是否显示分页信息(包括上一页  下一页 等)
* @return 当 isShowPaging=false 是返回Object[2] Object[0]: 总页数     Object[1]: 查询内容
*         当 isShowPaging=true  是返回Object[2] Object[0]: 查询内容   Object[1]: 分页信息(上一页下一页等)
*/
public Object[] searchData(String keyWord,int curPage,SearchItemBean bean,boolean isShowPaging) {
//略过异常处理、数据业务处理
Object[] results=new Object[2];
File indexFile = new File(bean.getIndexFilePath());
if (!indexFile.exists()) {
return null;
}
Directory dir = FSDirectory.open(indexFile);
//构建indexSearcher、queryParse核心对象
indexSearcher = new IndexSearcher(dir);
QueryParser qp = new MultiFieldQueryParser(Version.LUCENE_CURRENT, pFiledname, analyzer);
qp.setDefaultOperator(QueryParser.AND_OPERATOR);
//将关键字进行分词处理
Query query = qp.parse(keyWord);
//检索
TopDocs topDocs = indexSearcher.search(query, 1000);
ScoreDoc[] hits = topDocs.scoreDocs;
if (hits.length==0) {
return results;
}
//之后便是将hits数组组设置高亮,装成所需的业务数据,添加分页信息,然后return。

}

//高亮设置代码片段:
SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<font color=\"red\">", "</font>");//设定高亮显示的格式,也就是对高亮显示的词组加上前缀后缀
Highlighter highlighter = new Highlighter(simpleHtmlFormatter,new QueryScorer(query));
highlighter.setTextFragmenter(new SimpleFragmenter(80));
//将检索结果hits中的pFileds设置高亮格式
highlighter.getBestFragment(tokenStream, doc.get(pFileds[p]));


三、测试结果:

画了一个简陋的页面测试,基本满足要求。检索“啤酒”、“天津”等关键字的结果:







四、改进和反思:

1、虽然实现了增量索引的全文检索,但最初使用时遇到一个问题:如果项目被重新部署,应用服务器中记录最后一次索引位置的XML会被覆盖,下次索引将从数据库第一条记录开始。这样可能会导致索引重复,检索的结果可能有重复。经过思索后来加上了判断:当索引的rowNum位置是0时,先删除该表的索引文件,进行一次全表索引,下次再每隔两分钟检查数据库记录是否有增加,有的话便增量索引合并进来。

2、在后台准备的数据不具备通用性,包含了HTML代码(主要是高亮、分页信息部分),后来改进为Json格式。

3、开始只有HTTP接口和jar包的Java接口,不方便异构系统的调用。后来新增了WebService接口(将在后面的博文中继续讲述)。《近两年项目回顾系列——WebService》《Flex的webService调用全文检索》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: