近两年项目回顾系列——基于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分词器的主配置,包括定义拓展词典、停顿词典。内容如下:
ext.dic是上述文件中配置的IK分词器的拓展词库,新的流行词或项目需求中的特殊词可以自行添加,如:“微博”、“通达中心”、“我处”、“屌丝”等等(去库里看了一眼,还真有人注册【屌丝】牌的商品 - -|||)。
stopword.dic是IK分词的停词,如:a/an/the等等。
以下主要说说本项目中索引的创建、更新以及检索的过程:
一、索引创建:
库里数据量可能很大,动辄数百万、千万条。我们使用了增量索引的方式。同时由于我们的全文检索数据来自多个库,而且可能偶尔修改要索引的库、表、字段信息,所以使用WebRoot/searchdict/SearchItems.xml这个xml来描述索引任务。内容包括:
上述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.close(); //释放资源
//然后重新写增量索引配置文件:SearchItems.xml,修改下次更新索引的数据库起始位置(略)
二、检索内容:
主要使用org.apache.lucene.search.IndexSearcher、FSDirectory、TopDocs完成
三、测试结果:
画了一个简陋的页面测试,基本满足要求。检索“啤酒”、“天津”等关键字的结果:
四、改进和反思:
1、虽然实现了增量索引的全文检索,但最初使用时遇到一个问题:如果项目被重新部署,应用服务器中记录最后一次索引位置的XML会被覆盖,下次索引将从数据库第一条记录开始。这样可能会导致索引重复,检索的结果可能有重复。经过思索后来加上了判断:当索引的rowNum位置是0时,先删除该表的索引文件,进行一次全表索引,下次再每隔两分钟检查数据库记录是否有增加,有的话便增量索引合并进来。
2、在后台准备的数据不具备通用性,包含了HTML代码(主要是高亮、分页信息部分),后来改进为Json格式。
3、开始只有HTTP接口和jar包的Java接口,不方便异构系统的调用。后来新增了WebService接口(将在后面的博文中继续讲述)。《近两年项目回顾系列——WebService》《Flex的webService调用全文检索》
前半年主要做了总局的网上申请系统、网上查询系统、中国驰名商标申报系统,后台大都是的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调用全文检索》
相关文章推荐
- 站内搜索------仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 仿造百度实现基于Lucene.net全文检索
- 火力全开——仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 近两年项目回顾系列——基于Flex和RMI的自动化部署工具
- 仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 火力全开——仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 火力全开——仿造Baidu简单实现基于Lucene.net的全文检索的功能
- 近两年项目回顾系列——基于MINA+Flex的即时通讯系统
- 全文检索(二)-基于lucene4.10的增删改查
- 全文检索、数据挖掘、推荐引擎系列2---异步服务实现
- Lucene:基于Java的全文检索引擎简介
- android+lucene实现全文检索并高亮关键字
- Lucene:基于Java的全文检索引擎简介
- 全文检索、数据挖掘、推荐引擎系列2---异步服务实现
- 全文检索引擎Solr系列——整合中文分词组件IKAnalyzer
- 基于ASP.NET的lucene.net全文搜索实现步骤
- 近两年项目回顾系列——WebService
- Lucene:基于Java的全文检索引擎简介
- 在应用中加入全文检索功能——基于Java的全文索引引擎Lucene简介