2.3 Lucene分词详解
2.3.1 Lucene分词系统
在第1章中已经提到,索引和查询都是以词项为基本单位,词项是词条化的结果。在Lucene中,分词主要依靠Analyzer类解析实现。Analyzer类是一个抽象类(public abstract class org.apache.lucene.analysis.Analyzer),切分词的具体规则是由子类实现的,所以对于不同的语言规则,要有不同的分词器。
Analyzer内部主要通过TokenStream类实现。Tonkenizer类和TokenFilter类是TokenStream的两个子类。Tokenizer处理单个字符组成的字符流,读取Reader对象中的数据,处理后转换成词汇单元。TokenFilter完成文本过滤器的功能,但在使用过程中必须注意不同过滤器的使用顺序。
在创建索引的时候需要用到分词器,在进行索引查询的时候也会用到分词器,并且这两个地方要使用同一个分词器,否则可能会搜索不出来结果。Lucene提供了多种分词方法,简介如下:
● StopAnalyzer(停用词分词器)
StopAnalyzer能过滤词汇中的特定字符串和词汇,并且完成大写转小写的功能。
● StandardAnalyzer(标准分词器)
StandardAnalyzer根据空格和符号来完成分词,还可以完成数字、字母、E-mail地址、IP地址以及中文字符的分析处理,还可以支持过滤词表,用来代替StopAnalyzer能够实现的过滤功能。
● WhitespaceAnalyzer(空格分词)
WhitespaceAnalyzer使用空格作为间隔符的词汇分割分词器。处理词汇单元的时候,以空格字符作为分割符号。分词器不做词汇过滤,也不进行小写字符转换。实际中可以用来支持特定环境下的西文符号的处理。由于不完成单词过滤和小写字符转换功能,也不需要过滤词库支持。词汇分割策略上简单使用非英文字符作为分割符,不需要分词词库支持。
● SimpleAnalyzer(简单分词)
SimpleAnalyzer具备基本西文字符词汇分析的分词器,处理词汇单元时,以非字母字符作为分割符号。分词器不能做词汇的过滤,只进行词汇的分析和分割。输出的词汇单元完成小写字符转换,去掉标点符号等分割符。在全文检索系统开发中,通常用来支持西文符号的处理,不支持中文。由于不完成单词过滤功能,所以不需要过滤词库支持。词汇分割策略上简单使用非英文字符作为分割符,不需要分词词库的支持。
● CJKAnalyzer(二分法分词)
内部调用CJKTokenizer分词器,对中文进行分词,同时使用StopFilter过滤器完成过滤功能,可以实现中文的多元切分和停用词过滤。
● KeywordAnalyzer(关键词分词)
把整个输入作为一个单独词汇单元,方便特殊类型的文本进行索引和检索。针对邮政编码、地址等文本信息使用关键词分词器进行索引项建立非常方便。
2.3.2 分词器测试
Lucene标准分词器会把句子分成一个一个单个的单词,标准分词的代码见代码清单2-1。
代码清单2-1 Lucene标准分词
import java.io.IOException; import java.io.StringReader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; public class StdAnalyzer { private static String strCh = "中华人民共和国简称中国,是一个有13亿人口 的国家"; private static String strEn = "Dogs can not achieve a place, eyes can reach; "; public static void main(String[] args)throws IOException { System.out.println("StandardAnalyzer对中文分词:"); stdAnalyzer(strCh); System.out.println("StandardAnalyzer对英文分词:"); stdAnalyzer(strEn); } public static void stdAnalyzer(String str)throws IOException{ Analyzer analyzer = null; analyzer = new StandardAnalyzer(); StringReader reader = new StringReader(str); TokenStream toStream = analyzer.tokenStream(str, reader); toStream.reset(); CharTermAttribute teAttribute = toStream.getAttribute(CharTermAttribute.class); System.out.println("分词结果:"); while(toStream.incrementToken()){ System.out.print(teAttribute.toString()+ "|"); } System.out.println("\n"); analyzer.close(); } }
运行结果:
StandardAnalyzer对中文分词:
分词结果:
中|华|人|民|共|和|国|简|称|中|国|是|一|个|有|13|亿|人|口|的|国|家|
StandardAnalyzer对英文分词:
分词结果:
dogs|can|achieve|place|eyes|can|reach|
下面重构以上代码,测试多种分词器的分词效果,见代码清单2-2。
代码清单2-2 Lucene多种分词器示例
import java.io.IOException; import java.io.StringReader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cjk.CJKAnalyzer; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.analysis.core.KeywordAnalyzer; import org.apache.lucene.analysis.core.SimpleAnalyzer; import org.apache.lucene.analysis.core.StopAnalyzer; import org.apache.lucene.analysis.core.WhitespaceAnalyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; public class VariousAnalyzers { private static String str = "中华人民共和国简称中国,是一个有13亿人口的国家"; public static void main(String[] args)throws IOException { Analyzer analyzer = null; analyzer = new StandardAnalyzer(); // 标准分词 System.out.println("标准分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new WhitespaceAnalyzer(); // 空格分词 System.out.println("空格分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new SimpleAnalyzer(); // 简单分词 System.out.println("简单分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new CJKAnalyzer(); // 二分法分词 System.out.println("二分法分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new KeywordAnalyzer(); // 关键字分词 System.out.println("关键字分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new StopAnalyzer(); // 停用词分词 System.out.println("停用词分词:" + analyzer.getClass()); printAnalyzer(analyzer); analyzer = new SmartChineseAnalyzer(); // 中文智能分词 System.out.println("中文智能分词:" + analyzer.getClass()); printAnalyzer(analyzer); } public static void printAnalyzer(Analyzer analyzer) throws IOException { StringReader reader = new StringReader(str); TokenStream toStream = analyzer.tokenStream(str, reader); toStream.reset(); // 清空流 CharTermAttribute teAttribute = toStream.getAttribute (CharTermAttribute.class); while(toStream.incrementToken()){ System.out.print(teAttribute.toString()+ "|"); } System.out.println("\n"); analyzer.close(); } }
运行结果:
标准分词:
class org.apache.lucene.analysis.standard.StandardAnalyzer
中|华|人|民|共|和|国|简|称|中|国|是|一|个|有|13|亿|人|口|的|国|家|
空格分词:
class org.apache.lucene.analysis.core.WhitespaceAnalyzer
中华人民共和国简称中国,|是一个有13亿人口的国家|
简单分词:
class org.apache.lucene.analysis.core.SimpleAnalyzer
中华人民共和国简称中国|是一个有|亿人口的国家|
二分法分词:
class org.apache.lucene.analysis.cjk.CJKAnalyzer
中华|华人|人民|民共|共和|和国|国简|简称|称中|中国|是一|一个|个有|13|亿人|人口|口的|的国|国家|
关键字分词:
class org.apache.lucene.analysis.core.KeywordAnalyzer
中华人民共和国简称中国,是一个有13亿人口的国家|
停用词分词:
class org.apache.lucene.analysis.core.StopAnalyzer
中华人民共和国简称中国|是一个有|亿人口的国家|
中文智能分词:
class org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer
中华人民共和国|简称|中国|是|一个|有|13|亿|人口|的|国家|
2.3.3 IK分词器配置
Lucene 6.0使用IK分词器需要修改IKAnalyzer和IKTokenizer。在包tup.lucene.ik下新建一个IKTokenizer6x类和一个IKAnalyzer6x类。IKTokenizer6x类的代码见代码清单2-3, IKAnalyzer6x类的代码见代码清单2-4。
代码清单2-3 IKTokenizer6x.java
import java.io.IOException; import org.apache.lucene.analysis.Tokenizer; import org.apache.lucene.analysis.tokenattributes .CharTermAttribute; import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; import org.apache.lucene.analysis.tokenattributes.TypeAttribute; import org.wltea.analyzer.core.IKSegmenter; import org.wltea.analyzer.core.Lexeme; public class IKTokenizer6x extends Tokenizer { // IK分词器实现 private IKSegmenter _IKImplement; // 词元文本属性 private final CharTermAttribute termAtt; // 词元位移属性 private final OffsetAttribute offsetAtt; // 词元分类属性 //(该属性分类参考org.wltea.analyzer.core.Lexeme中的分类常量) private final TypeAttribute typeAtt; // 记录最后一个词元的结束位置 private int endPosition; // Lucene 6.x Tokenizer适配器类构造函数;实现最新的Tokenizer接口 public IKTokenizer6x(boolean useSmart){ super(); offsetAtt = addAttribute(OffsetAttribute.class); termAtt = addAttribute(CharTermAttribute.class); typeAtt = addAttribute(TypeAttribute.class); _IKImplement = new IKSegmenter(input, useSmart); } @Override public boolean incrementToken()throws IOException { clearAttributes(); // 清除所有的词元属性 Lexeme nextLexeme = _IKImplement.next(); if(nextLexeme ! = null){ // 将Lexeme转成Attributes termAtt.append(nextLexeme.getLexemeText()); // 设置词元文本 termAtt.setLength(nextLexeme.getLength()); // 设置词元长度 offsetAtt.setOffset(nextLexeme.getBeginPosition(), nextLexeme.getEndPosition()); // 设置词元位移 //记录分词的最后位置 endPosition = nextLexeme.getEndPosition(); typeAtt.setType(nextLexeme.getLexemeText()); // 记录词元分类 return true; // 返回true告知还有下个词元 } return false; // 返回false告知词元输出完毕 } @Override public void reset()throws IOException { super.reset(); _IKImplement.reset(input); } @Override public final void end(){ int finalOffset = correctOffset(this.endPosition); offsetAtt.setOffset(finalOffset, finalOffset); } }
代码清单2-4 IKAnalyzer6x.java
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.Tokenizer; public class IKAnalyzer6x extends Analyzer { private boolean useSmart; public boolean useSmart(){ return useSmart; } public void setUseSmart(boolean useSmart){ this.useSmart = useSmart; } public IKAnalyzer6x(){ this(false); // IK分词器Lucene Analyzer接口实现类; // 默认细粒度切分算法 } // IK分词器Lucene Analyzer接口实现类;当为true时,分词器进行智能切分 public IKAnalyzer6x(boolean useSmart){ super(); this.useSmart = useSmart; } // 重写最新版本的createComponents;重载Analyzer接口,构造分词组件 @Override protected TokenStreamComponents createComponents(String fieldName){ Tokenizer _IKTokenizer = new IKTokenizer6x(this.useSmart()); return new TokenStreamComponents(_IKTokenizer); } }
实例化IKAnalyzer6x即可使用IK分词器了,创建默认细粒度切分算法的IK Analyzer:
Analyzer analyzer = new IKAnalyzer6x();
创建智能切分算法的IK Analyzer:
Analyzer analyzer = new IKAnalyzer6x(true);
2.3.4 中文分词器对比
分词效果会直接影响到文档搜索的准确性,我们对比Lucene 6.0中自带的中文智能分词器SmartChineseAnalyzer和IK Analyzer,比较一下哪一个分词器的准确率更高,见代码清单2-5。
代码清单2-5 中文分词效果对比
import java.io.IOException; import java.io.StringReader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.analysis.tokenattributes .CharTermAttribute; import tup.lucene.ik.IKAnalyzer6x; public class IkVSSmartcn { private static String str1 = "公路局正在治理解放大道路面积水问题。"; private static String str2 = "IKAnalyzer是一个开源的,基于java语言开发 的轻量级的中文分词工具包。"; public static void main(String[] args)throws IOException { Analyzer analyzer = null; System.out.println("句子一: "+str1); System.out.println("SmartChineseAnalyzer分词结果:"); analyzer = new SmartChineseAnalyzer(); printAnalyzer(analyzer, str1); System.out.println("IKAnalyzer分词结果:"); analyzer = new IKAnalyzer6x(true); printAnalyzer(analyzer, str1); System.out.println("------------------------------------"); System.out.println("句子二:"+str2); System.out.println("SmartChineseAnalyzer分词结果:"); analyzer = new SmartChineseAnalyzer(); printAnalyzer(analyzer, str2); System.out.println("IKAnalyzer分词结果:"); analyzer = new IKAnalyzer6x(true); printAnalyzer(analyzer, str2); analyzer.close(); } public static void printAnalyzer(Analyzer analyzer, String str) throws IOException { StringReader reader = new StringReader(str); TokenStream toStream = analyzer.tokenStream(str, reader); toStream.reset(); // 清空流 CharTermAttribute teAttribute = toStream.getAttribute( CharTermAttribute.class); while(toStream.incrementToken()){ System.out.print(teAttribute.toString()+ "|"); } System.out.println(); } }
运行结果如下:
句子一:公路局正在治理解放大道路面积水问题。 SmartChineseAnalyzer分词结果: 公路局|正|在|治理|解放|大|道路|面积|水|问题| IKAnalyzer分词结果: 公路局|正在|治理|解放|大道|路面|积水|问题| ------------------------------------------- 句子二:IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。 SmartChineseAnalyzer分词结果: ikanalyz|是|一个|开|源|的|基于|java|语言|开发|的|轻量级|的|中文|分词|工具包| IKAnalyzer分词结果: ikanalyzer|是|一个|开源|的|基于|java|语言|开发|的|轻量级|的|中文|分词|工具包
从分词结果中可以看到,对于句子一,SmartChineseAnalyzer没有把“正在”分成一个词,“大道路面积水”分成了“大”“道路”“面积”“水”,而IK Analyzer把“正在”分成一个词,把“大道路面积水”分成了“大道”“路面”“积水”。对于句子二,两者的分词效果差不多,但是SmartChineseAnalyzer把“开源”分开了。总体而言,IK Analyzer的中文分词的准确性比SmartChineseAnalyzer要高一些。我们在以后的案例和项目中也会选择IK Analyzer作为我们的中文分词器。
开源的中分词工具有很多,比如ICTCLAS中文分词、Paoding分词、jcseg分词等,这里只对比了Lucene自带的中文智能分词器和IK分词器,读者如果有兴趣可以继续研究。
2.3.5 扩展停用词词典
IK Analyzer默认的停用词词典为IKAnalyzer2012_u6/stopword.dic,这个停用词词典并不完整,只有30多个英文停用词,推荐使用扩展的停用词词表(下载地址:https://github.com/cseryp/stopwords)。在工程中新增文件ext_stopword.dic,文件和IKAnalyzer.cfg.xml在同一目录,编辑IKAnalyzer.cfg.xml把新增的停用词字典写入配置文件,多个停用词字典用逗号隔开,配置如下:
<entry key="ext_stopwords">stopword.dic; ext_stopword.dic</entry>
2.3.6 扩展自定义词典
IK Analyzer也支持自定义词典,在IKAnalyzer.cfg.xml同一目录新建ext.dic,把新的词语按行写入文件,然后编辑IKAnalyzer.cfg.xml,把新增的停用词字典写入配置文件,多个字典用空格隔开,配置如下:
<entry key="ext_dict">ext.dic; </entry>
比如,对于网络流行语“厉害了我的哥”,默认的词库中没有这个词,要在自定义词典中将其写入以后才能分成一个词。测试自定义词典的代码见代码清单2-6。
代码清单2-6 自定义词典测试
import java.io.IOException; import java.io.StringReader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes .CharTermAttribute; import tup.lucene.ik.IKAnalyzer6x; public class ExtDicTest { private static String str = "厉害了我的哥!中国环保部门即将发布治理北京 雾霾的方法!"; public static void main(String[] args)throws IOException { Analyzer analyzer = new IKAnalyzer6x(true); StringReader reader = new StringReader(str); TokenStream toStream = analyzer.tokenStream(str, reader); toStream.reset(); CharTermAttribute teAttribute= toStream.getAttribute( CharTermAttribute.class); System.out.println("分词结果:"); while(toStream.incrementToken()){ System.out.print(teAttribute.toString()+ "|"); } System.out.println("\n"); analyzer.close(); } }
运行结果:
加载扩展词典:ext.dic 加载扩展停止词典:stopword.dic 分词结果: 厉|害了|的哥|中国|环保部门|发布|治理|北京|雾|霾|方法| 在ext.dic中添加自定义词项: 中国环保部门 北京雾霾 厉害了我的哥 再次运行,结果如下: 加载扩展词典:ext.dic 加载扩展停止词典:stopword.dic 分词结果: 厉害了我的哥|中国环保部门|发布|治理|北京雾霾|方法|