Lucene建立索引的时候,需要使用到分词器-Analyzer,分词器的作用就是将当前的文本按照分词规则进行分词,然后建立索引,检索结果的精确度很大程度上来自于索引的建立是否合理而准确。
lucene提供了一些内置的分词器:
- * SimpleAnalyzer 这个分词是一段一段话进行分
- * StandardAnalyzer 标准分词拿来分中文和ChineseAnalyzer一样的效果
- * PerFieldAnalyzerWrapper 这个很有意思,可以封装很多分词方式,还可以于先设置field用那个分词分
- * CJKAnalyzer 这个分词方式是正向退一分词(二分法分词),同一个字会和它的左边和右边组合成一个次,每个人出现两次,除了首字和末字
- * ChineseAnalyzer 这个是专业的中文分词器,一个一个字分
- * BrazilianAnalyzer 巴西语言分词
- * CzechAnalyzer 捷克语言分词
- * DutchAnalyzer 荷兰语言分词
- * FrenchAnalyzer 法国语言分词
- * GermanAnalyzer 德国语言分词
- * GreekAnalyzer 希腊语言分词
- * RussianAnalyzer 俄罗斯语言分词
- * ThaiAnalyzer 泰国语言分词
- * KeywordAnalyzer "Tokenizes" the entire stream as a single token. This is useful for data like zip codes, ids, and some product names.
- * PatternAnalyzer api讲这个分词方式很快,它是放在内存里面的
- * SnowballAnalyzer 经典分词用具 主要支持欧洲语言
- * StopAnalyzer 被忽略的词的分词器
- * WhitespaceAnalyzer 空格分词
这些分词器所提供的分词功能,其实并不能满足现实业务需求,比如上述的汉语分词器,其分词是通过一个字一个字的分词,那么:
“网易杭州研究院” 将分词成为 杭 、 州、 网、 易、 研、 究、 院
那么在进行检索的时候,通过这单字的检索,匹配结果暂且不论,单从使用上就有很大局限性,比如无法使用词语检索等,使用 “杭州” 检索,将得不到任何结果。
而上述的二元分词,是每个字会出现两次,分别是两个字组成一个词,建立索引,那么“杭州网易研究院”将分词结果为:
杭州、州网、网易、易研、研究、究院
(注意的是分词会将标点符号去掉并且将大写字母转换成小写字符,同时会去掉一些类似于is the a等之类的词语)
这样同样带有局限性,其他的针对于特定语言的分词更无法应用在汉语环境中。
很欣慰的是,现在有很多兴趣小组开发了一些丰富的开源分词器,比如著名的IK_CAnalyzer,他柔和了几种不同的分词算法,同时内置了二元分词,对于汉语的分词具有很不错的效果。
当然不同的公司也会有不同的分词器开发团队,比如网易的有道团队,开发了一些优秀的分词器,除了高效可靠精确的分词算法,还有庞大的分词库,所以这类企业级的分词器,十分精确而且满足现实多种业务需求。
而对于个人系统或者规模较小的企业系统而言,如果没有这些分词器团队的支持,只能使用目前开源的一些分词器。
但是现在有一个问题,我们在一个对数据库数据建立索引并且将需要显示的数据都放入索引而避免读取数据的情况时,总会有一些字段是不希望建立索引以干扰其他字段索引结果的。比如:
目前我们数据库中需要对以下三个字段建立索引:
ID、NAME、BRAND、IMG_SRC
上面的四个字段,我们在索引的时候,通常希望能通过ID和IMG_SRC匹配,而不是ID和IMG_SRC,因为这两个字段的一些值通常不具备什么业务属性,而是一些简单字符,所以如果这两个字段建立了索引,势必会对NAME和BRAND的索引结果产生干扰。
这里先讲一下为什么要对上述不需要使用检索的字段也放入索引中,google的nutla系统,基于lucene3的最新的分布式检索系统,对于索引的建立只是用了那些需要建立索引的字段,对于不需要检索的字段,则不会放入索引文件中。所以它的执行机制是,首先通过索引找到匹配的记录ID,然后通过这个ID去数据库获取数据,这样导致一个问题就是性能开销的问题,也就是通过了索引之后还要进行一次数据库的查询工作,这个性能的开销与数据库规模具有直接关系。
所以这也是我们尽力去避免的问题,也就是避免一次额外的数据库查询,我们将不需要搜索的字段也放入索引文件中,但是采用一些机制尽量减少对其他搜索字段的干扰,当获取到匹配结果的时候,可以一次性从索引文件中取出值,而不用再去数据库搜索。
这样如果对于所有的字段采用相同的分词器,那么上述的ID和IMG_SRC将被建立多种复合语言习惯的索引,不如
IMG_SRC = http://www.csdn.net/quzishen/img/001.jpg
可能会被分次成为: http www cs csdn net quzishen qu zi shen quzi img 0 01 001 jpg
这样的分词结果,首先会直接导致索引文件变大,搜索性能受到影响,其次会导致索引中充斥着非常多的“不需要的索引”,对于检索结果也是一个很大的干扰,我们希望,如果是这样的字段,那最多允许全部一个值建立一个索引,这样在使用检索的时候,这个字段几乎不可能有匹配结果,除非使用上述的全字段的值来直接检索,但是现实业务中不会存在这样变态的情况。
所以引出一点,就是针对于不同的字段,使用不同的分词器,建立不同精度的索引。比如对于NAME我们使用强大的ICK分词器,而对于ID和IMG_SRC我们使用WhitespaceAnalyzer也就是按照空格来建立索引,基本上就是一个字段一个索引了。
欣慰的是,lucene已经考虑到了这种情况,所以提供了一个PerFieldAnalyzerWrapper,他允许针对不同的索引字段,使用不同的分词器
上述我们实现了,默认的使用ICK分词器,但是对于特殊字段,则使用空格分词器。这样建立的索引,将满足我们的现实需求。
看看我们的全部实现代码:
首先我们采用一个配置文件的形式,这个xml文件中可以配置多种索引,其中可以配置的特殊选项是:需要转移的字段和不需要ICK分词的字段,分别用不同的标签标示
最终我们要解析上述的xml配置文件,生成如下的领域模型:
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
int index = className.lastIndexOf(".");
this.key = className.substring(index+1, className.length());
}
public String getIndexDir() {
return indexDir;
}
public void setIndexDir(String indexDir) {
this.indexDir = indexDir;
}
public String getFields() {
return fields;
}
public void setFields(String fields) {
this.fields = fields;
}
public void initMap(String key,String value){
if(StringUtils.isBlank(key) || StringUtils.isBlank(value)){
return;
}
specials.put(key, value);
}
public Map<String, String> getSpecials() {
return specials;
}
public void initList(String keys){
if(StringUtils.isBlank(keys)){
return;
}
String[] keyarray = StringUtils.split(keys, ",");
fieldsNoNeedAnalyzer = Arrays.asList(keyarray);
}
public List<String> getFieldsNoNeedAnalyzer() {
return fieldsNoNeedAnalyzer;
}
public void setFieldsNoNeedAnalyzer(List<String> fieldsNoNeedAnalyzer) {
this.fieldsNoNeedAnalyzer = fieldsNoNeedAnalyzer;
}
public void setSpecials(Map<String, String> specials) {
this.specials = specials;
}
}
那么具体的解析方式,我们采用apache的Digest类来完成
// 遇见lucene标签,生成java.util.ArrayList对象
digester.addObjectCreate("lucene", "java.util.ArrayList");
// 对lucene生成的对象赋值
digester.addSetProperties("lucene");
// 遇到lucene/rule标签,生成SearchConfigure对象
digester.addObjectCreate("lucene/rule", SearchConfigure.class.getName());
// 对其赋值
digester.addSetProperties("lucene/rule");
digester.addBeanPropertySetter("lucene/rule/className");
digester.addBeanPropertySetter("lucene/rule/fields");
digester.addBeanPropertySetter("lucene/rule/indexDir");
digester.addCallMethod("lucene/rule/special", "initMap",2);
digester.addCallParam("lucene/rule/special/special-field", 0);
digester.addCallParam("lucene/rule/special/special-convert", 1);
digester.addCallMethod("lucene/rule/noNeedAnalyzer", "initList",1);
digester.addCallParam("lucene/rule/noNeedAnalyzer", 0);
// 生成列表中的next项
digester.addSetNext("lucene/rule", "add",SearchConfigure.class.getName());
indexList = (List<SearchConfigure>)digester.parse(configureFilePath);
这样就完成了相应的配置,然后使用下面的方法来建立索引:
IndexWriter indexWriter = new IndexWriter(floder,
preFiledAnalyzerWrapper, IS_REBUILD_INDEX);
// 控制写入一个新的segment前在内存中保存的最大的document数目
indexWriter.setMaxBufferedDocs(500);
// 控制多个segment合并的频率,最大文档合并数
indexWriter.setMaxMergeDocs(Integer.MAX_VALUE);
buildIndex(searchConfigure, indexWriter);
indexWriter.optimize();
indexWriter.close();
} catch (IOException e) {
logger.error("create index failed!check the authentation!", e);
throw new RuntimeException(
"create index failed!check the authentation!", e);
}
}
索引建立完成之后,可以使用一些辅助的工具来查看索引情况,比如luke等。
http://download.csdn.net/source/2741843 下载地址
同样的,也可以写一个简单的方式,来打印一下,当然仅供测试的情况下使用的
System.out.println("索引:"+indexs);
int num = indexReader.numDocs();
for(int i=0;i<num;i++){
Document document = indexReader.document(i);
for(String field:indexs){
Field f = document.getField(field);
System.out.println(field+":"+f.stringValue());
}
System.out.println("====================================");
}
} catch (Exception e) {
e.printStackTrace();
}
}