前言
关于LuciMint的诞生, 笔者觉得有必要说明一下。首先它不是一个开源项目,而只是笔者在工作过程中,总结出的一个工具包,因此,第一,它暂时不开源(代码还处于优化修改期),第二,它的功能还是比较有限的。与Lucene相对比,LuciMint就如同一部廉价的卡片机,Lucene则是一部单反相机了,哈哈~~ 但谁说卡片机不遭人喜爱呢,这也是笔者想把它与大家分享的原因。对于一般性的企业知识库应用,网站BBS搜索等,大数量零散数据(如,本地文件系统,邮件系统等等)的辅助索引,还是好用的。
现在言归正传,说说为啥会有LuciMint。笔者从5年前开始参与与Lucene相关的项目开发,发现有一些问题,几乎是所有系统都必须面对和解决的。如:
- 1.Lucene存在索引写入同步问题(虽然Lucene自身提供了文件级的同步写入锁,但面对时不时抛出的同步异常,还是很不爽的)。尤其,在写入请求并发量大的时候,从性能到稳定性上,问题都十分突出。
- 2.用过Lucene的用户会发现,Lucene的磁盘索引不适合于实时索引,既“实时写入,实时查询”。首先,实时的写入会产生大量零散的索引碎片,不利率Lucene的搜索效率;其次,Lucene的Reader对新进的文档是无法读取到的,必须重新打开,这个也影响效率。
- 3.Lucene的索引是以文件形式存在的,对于分布式的web系统而言,你不可能为每台APP服务器搭建一个Lucene索引,因此Lucene缺乏一个统一远程的索引/搜索服务。
- 4.对于不同的表结构(Java Bean实体),要创建不同的Document的结构,读取时也要自己解析,这个在hibernate /ibatis 流行的当今java界,也显得麻烦了。
- 5.默认的Lucene自带的QueryParser对用户,特别是中文用户的查询习惯不很吻合,笔者常常会遇到有人问,为什么我输入“Java加密”却搜不到“JavaMD5加密”。
为解决上述问题,笔者需要在不同的项目间复用相关的代码,并不断的优化,重构之。于是乎,便有了现在的LuciMint。为啥叫这个名字,因为取名的时候,笔者正在吃一颗味道不错的薄荷糖,哈哈哈~
1.LuciMint介绍
LuciMint(Lucene Index薄荷糖)是一个基于Lucene的全文索引小组件,它封装了Lucene的底层对Document对象的操作,提供了一个面向用户数据格式的,轻量级的索引操作接口。
采用异步请求方式,LuciMint实现了 “高并发的实时索引”,“实时搜索”,“多套索引配置”等核心功能。专门的,针对Java语言客户端,LuciMint设计了Annotation标签,通过Java Bean属性的简单的标注,用户可以直接将Bean生成Lucene索引.
同时,LuciMint结合了IKAnalyzer3.2.8版本的了“简易的搜索表达式”功能,实现了统一的HTTP搜索服务接口。
LuciMint适用于基于Lucene的,一般性的,大中型企业知识库、文档中心,或者中小型的互联网论坛,地图POI信息搜索等项目。
1.1 LuciMint组件结构
组件整体结构
Index Context内部结构
1.2 LuciMint特性
- 1.通过异步任务队列处理方式,LuciMint解决了,在高并发索引请求下的索引同步操作问题。
- 2.实现了Lucene索引信息的“实时写入,实时搜索”。
- 3.提供了HTTP + XML格式的web访问接口,实现了Lucene在分布式群集系统中的接入使用。
- 4.对于Java用户而言,LuciMint提供了简单的Annotation来辅助对Bean实现的索引创建。通过在Java Bean上标注Annotation,并传入相应的API,即可完成索引的创建。对于非Java用户而言,LuciMint也提供了介于HTTP和XML的web服务接口。
- 5.LuciMint为一般性用户提供了一个基于HTTP的搜索接口,用户通过“简易搜索表达式”,可以方便的进行搜索查询操作。有特殊搜索需求的用户,也可以尝试在LuciMint的基础上,构建专用的搜索接口,来实现高级搜索功能。
2.使用指南
2.1安装部署
1.LuciMint要引入的依赖包及配置文件
以本地jar包形式集成LuciMint,需要以下jar包
- LuciMint.jar
- IKAnalyzer3.2.8.jar
- Lucene Core 3.X.jar
- Jackson-core-asl-1.6.0.jar
- Jackson-mapper-asl-1.6.0.jar
- Spring 2.5.6.jar
- Spring.xml配置文件
- log4j-1.2.15jar (这个是Spring需要的依赖包)
- common-logging-1.1.1.jar((这个是Spring需要的依赖包)
以HTTP服务方式集成LuciMint,需要以下jar包
服务端:与“以本地jar包形式集成LuciMint”方式相同,此外还要在web.xml中配置一个HTTP服务(servlet)。(祥见部署部分说明)
客户端:客户端部署只需要
- LuciMint.jar
- Jackson-core-asl-1.6.0.jar
- Jackson-mapper-asl-1.6.0.jar
LuciMint的Spring.xml配置
不论你使用本地jar包方式或者HTTP服务方式,你都需要配置Spring.xml。用户需要靠它来指定索引的目录,及索引名称等自定义的信息。下面我们给出一个Spring.xml的配置实例来进行讲述。 值得说明的是,一个LuciMint是可以同时支持创建多个Luene索引实例的,这很适用于在一个物理服务器上构建不同内容,不容需求的索引服务。
以下是Spring.xml
-- 该属性指定LuciMint使用的分词器实例,目前及支持IKAnalzer3.2.8,所以你不用改这行配置。 --索引名称,该属性唯一指定一个索引控制器 --索引数据的主键名称,要跟Bean的主键字段对应 --索引数据存放的目录 --索引使用的分词器,引用先前的defaultAnalzyer
特别值得提醒的是,将不同的索引目录放置在不同的物理磁盘上,有助于提高索引的读写效率!!!
LuciMint在HTTP服务方式下的web.xml配置
前面文档中提到过,如果你使用的是LuciMint的HTTP服务方式部署,那么你需要在web.xml文件中配置一个HTTP Servlet。如下:
Luci索引RPC服务端HTTP实现 LuciIndexService org.wltea.luci.rpc.http.LuciIndexServlet LuciIndexService /web/indexservice --这里可以按你自己的需求定义url(啰嗦一句哈哈)
2.2快速入门
Java客户端使用
1.使用LuciMint的Annotation注释JavaBean
LuciMint的Annotation非常简单,一共就3个:
org.wltea.luci.annotation.PKey -- 主键字段标识。
使用@Pkey注释,表示该字段采用索引、存储、不切分的方式。注意Bean中的主键字段必须和Spring.xml索引配置中的索引主键域名字一致。
org.wltea.luci.annotation.FieldStore --非主键字段的存储标识。
使用@FieldStore表示这个字段要在索引中存储,不使用表示不存储。
org.wltea.luci.annotation.FieldIndex -- 非主键字段的索引表示。
不使用@FieldIndex等同于@FieldIndex("NO")表示不索引
使用@FieldIndex等同于@FieldIndex("NOT_ANALYZED")表示采用索引不分词策略。
使用@FieldIndex("ANALYZED")表示采用索引且分词策略。
要说明的是,如果Bean的属性不使用任何Luci Annotation的标识,则Luci在索引中将忽略这个属性。
以下是JavaBean的Annotation列子:
/** * */ package org.wltea.luci.sample; import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.wltea.luci.annotation.FieldIndex; import org.wltea.luci.annotation.FieldStore; import org.wltea.luci.annotation.PKey; /** * @author linliangyi * */ public class SampleJavaBean implements Serializable { /** * */ private static final long serialVersionUID = 7153417317917298956L; @PKey private int uuid; @FieldStore @FieldIndex("ANALYZED") private String userName; @FieldStore private boolean checkFlag; @FieldIndex private String url; @FieldStore @FieldIndex private Date registTime; public int getUuid() { return uuid; } public void setUuid(int uuid) { this.uuid = uuid; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public boolean isCheckFlag() { return checkFlag; } public void setCheckFlag(boolean checkFlag) { this.checkFlag = checkFlag; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Date getRegistTime() { return registTime; } public void setRegistTime(Date registTime) { this.registTime = registTime; } }
2.以本地jar包形式调用java client
/** * */ package org.wltea.luci.sample; import java.util.Date; import java.util.List; import org.wltea.luci.client.IndexService; import org.wltea.luci.client.IndexServiceFactory; import org.wltea.luci.client.QueryResults; /** * * 简易索引例子 * @author linliangyi * */ public class LuciMintSample { /** * 本地索引HelloWorld例子 * @param args */ public static void main(String[] args){ //以本地客户端方式,根据命名,获取Luci索引服务实例, //这里的名字要跟Spring文件配置的索引名称一致,注意大小写敏感哦 IndexService indexService = IndexServiceFactory.getLocalIndexService("BBS"); //使用带注释的Bean,创建索引 for(int i = 0 ; i < 20 ; i++){ //一个需要建索引的JavaBean SampleJavaBean bean = new SampleJavaBean(); bean.setCheckFlag(true); bean.setRegistTime(new Date()); bean.setUrl("http://sample.lucimint.org"); bean.setUserName("LuciMint" + i); bean.setUuid(20000 + i); //新增索引实际上就一句 indexService.add(bean); } try { Thread.sleep(1000); System.out.println("*****************************"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //索引查询部分代码 //编写查询逻辑 String queryString = "url='http://sample.lucimint.org'"; //查询索引也只要一句, QueryResults queryResults = indexService.query(queryString, 1, 20, true); System.out.println("PageNo :" + queryResults.getPageNo()); System.out.println("PageSize :" + queryResults.getPageSize()); System.out.println("TotalHit :" + queryResults.getTotalHit()); System.out.println("TotalPage :" + queryResults.getTotalPage()); //读取具体的数据列表 , 传入你的bean类型,Luci将帮你封装好结果集 ListbeanList = queryResults.getResultBeans(SampleJavaBean.class); for(SampleJavaBean bean : beanList){ System.out.println(bean.getUuid() + " | " +bean.getUserName()); } } }
3.以远程服务信息调用java client
远程调用Luci服务和本地调用的代码几乎是一样的,只有一句不同
/** * */ package org.wltea.luci.sample; import java.net.MalformedURLException; import java.net.URL; import java.util.Date; import java.util.List; import org.wltea.luci.client.IndexService; import org.wltea.luci.client.IndexServiceFactory; import org.wltea.luci.client.QueryResults; /** * 远程调用例子 * @author linliangyi * */ public class LuciMintRPCSample { /** * 远程索引HelloWorld例子 * @param args */ public static void main(String[] args){ //要声明远程服务的HTTP地址,如下 URL remoteHttpURL = null; try { remoteHttpURL = new URL("http://10.5.21.86/sc/web/indexservice"); } catch (MalformedURLException e1) { e1.printStackTrace(); } //唯一不一样的就是这里,根据索引命名,获取Luci远程索引器实例 IndexService indexService = IndexServiceFactory.getRemoteIndexService("SAMPLE", remoteHttpURL); //使用带注释的Bean,创建索引 for(int i = 0 ; i < 20 ; i++){ //一个需要建索引的JavaBean SampleJavaBean bean = new SampleJavaBean(); bean.setCheckFlag(true); bean.setRegistTime(new Date()); bean.setUrl("http://sample.lucimint.org"); bean.setUserName("LuciMint" + i); bean.setUuid(20000 + i); //新增索引 indexService.add(bean); } try { Thread.sleep(3000); System.out.println("*****************************"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //**********************************查询部分 //编写查询逻辑 String queryString = "url='http://sample.lucimint.org'"; //查询索引也只要一句, QueryResults queryResults = indexService.query(queryString, 1, 20, true); System.out.println("PageNo :" + queryResults.getPageNo()); System.out.println("PageSize :" + queryResults.getPageSize()); System.out.println("TotalHit :" + queryResults.getTotalHit()); System.out.println("TotalPage :" + queryResults.getTotalPage()); //读取具体的数据列表 , 传入你的bean类型,Luci将帮你封装好结果集 ListbeanList = queryResults.getResultBeans(SampleJavaBean.class); for(SampleJavaBean bean : beanList){ System.out.println(bean.getUuid() + " | " +bean.getUserName()); } } }
通用(非java客户端)HTTP 索引服务接口使用
如果你有PHP,或者其他的客户端,那么你可以使用这个通用接口
1. 索引操作接口
索引操作接口是指对索引数据进行 新增、删除、修改、优化等数据变更操作接口。接口使用标准的HTTP POST请求,
HTTP请求参数如下表:
2. 索引操作接口返回结果
返回结果使用了原生的HTTP response协议。如果索引操作接口提交的请求正常执行,则接口返回HTTP 200应答。如果提交出现错误,则接口返回HTTP 500错误,并将异常信息带着response message中返回。
HTTP response头部信息如下:
HTTP/1.0 200 OK
HTTP/1.0 500 <异常描述字窜>
3. 索引查询接口
索引查询接口是指对索引数据进行搜索操作的接口。接口使用标准的HTTP POST请求,
HTTP请求参数如下表:
4. 索引查询接口返回结果
查询结果使用JSON格式返回。格式如下:
{ "pageNo":2, //页号 "pageSize":20,//单页记录数 "totalHit":33,//总记录数 "totalPage":2,//总页数 "results":[ //结果数据集。内部是记录的具体属性 { "userName":"林良益", "uuid":"10000", "registTime":"20110225180130", "url":"http://www.sohu.com" }, { "userName":"蔡剑锋", "uuid":"10001", "registTime":"20110225180131", "url":"http://www.sohu.com" } ...... ] }
XML-DATA数据格式说明
10000 false 林良益 20110225180130 10001 false 蔡剑锋 20110225180131
- 1. XML文件必须是UTF-8编码
- 2. 根元素
表示数据集合的开始,一个 元素中可以含有多条 记录。 - 3. 元素
表示一条数据记录数据,一个 元素可以含有多个 字段属性。 - 4. 元素
表示一条数据记录中的一个字段 - 5. field的name属性表示字段名称, field中的文本表示字段值,复杂文本请使用CDATA标签。注意:文本中不能含有非法的XML字符[\\x00-\\x08|\\x0b-\\x0c|\\x0e-\\x1f]。
- 6. pkey 属性指定数据记录的主键,每条document必须有且只能有一个字段是主键。主键的字段要和索引服务端配置的主键名称相一致。
- 7. store 属性表示字段是否要在Lucene索引中保存。store="true"表示索引要保存该字段值,store="false"或者没有定义store属性,则不保存
- 8. index 属性表示字段是否要被索引,index有三个值:NO, NO_ANALYZED, ANALYZED,分别表示,不索引,索引但不分词,分词后索引。不定义index属性默认不索引。
简易搜索表达式说明
搜索表达式样例:
id='1231' && (title:'文档标题'|| content:'内容') – author='helloworld'
- 1. 表达式使用 属性名<->属性值一一对应的形式,属性值使用 单引号 标识。
- 2. 在属性名-属性值中使用 “=”等号,表示对该属性的精确搜索(不分词);使用“:”冒号表示对属性的模糊搜索(分词搜索)。
- 3. 表达式支持“&&”与 “||” 或“-”非的逻辑操作,以及“( )”括号优先级定义。注意“-”非逻辑不能单独使用。
关于索引的初始化
笔者专门提出一个章节,提醒用户关于导入历史数据的注意事项 (除非你是全新的系统,没有历史数据).
- 1. 请使用IndexService.build(Object)或者IndexService.build(List objs) 方法批量导入历史数据,不要使用IndexService.add(Object)。前者为数据批量迁移定制,后者为动态增量数据定制。
- 2. 在完成历史数据的索引初始化后,务必调用IndexService.optimize()方法,在正式使用前,对索引进行优化操作。否则,你的索引搜索性能有可能大幅度下降!!!
2.3 Java API说明
详细请参阅使用《LuciMint 索引组件 1.1 使用手册》和Java API DOC
相关下载:
IKAnalyzer3.2.8 下载