简单来说,Analysis就是把field Text转化成基本的Term的形式。
通过分词,将Text转化为Token,Token+对应的Field即为Term。
分词的处理包括:萃取、丢弃标点、移除发音、小写、移除常用单词、去除变形(去掉过去时等)等。
本章将介绍如何使用内置的分词器,以及如何根据语言、环境等特点创建自己的分词器。
4.1 使用Analysis
分词用于所有需要将Text转化成Term的场合,在Lucene中主要有两个:
1、Index(索引)
2、使用QueryParser的时候。
首先从一个简单例子看各内置分词器的效果:
例子1
对 "The quick brown fox jumped over the lazy dogs" 进行分词的结果:
WhitespaceAnalyzer :
[The] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs]
SimpleAnalyzer :
[the] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs]
StopAnalyzer :
[quick] [brown] [fox] [jumped] [over] [lazy] [dogs]
StandardAnalyzer:
[quick] [brown] [fox] [jumped] [over] [lazy] [dogs]
只有被分词处理后的Term才能被检索到。
例子2
对 "XY&Z Corporation - xyz@example.com" 进行分词
WhitespaceAnalyzer:
[XY&Z] [Corporation] [-] [xyz@example.com]
SimpleAnalyzer:
[xy] [z] [corporation] [xyz] [example] [com]
StopAnalyzer:
[xy] [z] [corporation] [xyz] [example] [com]
StandardAnalyzer:
[xy&z] [corporation] [xyz@example.com]
各个内置分词器的简介
WhitespaceAnalyzer:仅仅按照空白分隔开
SimpleAnalyzer:在非字母处分隔开,并小写化,它将丢弃所有数字。
StopAnalyzer:与SimpleAnalyser相同,除此之外,去除英文中所有的stop words(如a the等),可以自己指定这个集合。
StandardAnalyzer:较为高级的一个,能识别公司名字、E-mail地址、主机名等同时小写化并移除stop words。
在Index时使用Anaylsis
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
IndexWriter writer = new IndexWriter(directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
注意如果需要分词,在创建Field的时候,必须指定Field.Index.ANALYZED或者Field.Index.ANALYZED_NO_NORMS。
如果Text想被整体索引而不被分词,使用Field.Index.NOT_ANALYZED或者Field.Index.NOT_ANALYZED_NO_NORMS
另外,对于是否存储分词前原文,由Field.Store.YES or Field.Store.NO控制。
在QueryParser时使用Anaylsis
QueryParser parser = new QueryParser(Version.LUCENE_CURRENT , "contents", analyzer);
Query query = parser.parse(expression);
Anaylsis将expression拆分为必要的Term以用于检索,但Lucene并不会把全部的expression交给Analysis处理,而是分块进行。
例如对于
“president obama” +harvard +professor
会单独地触发analyser三次,分别是
1.president obama
2.harvard
3.professor
一般来说,index和parser应该使用相同的Analysis.
Analysis不适用的场景
Analysis只适用于文本内部使用,只限制在一个Field中用。对于HTML这种包含<body>、<head>等多个属性即多个Field的不适用,在这种情况下,需要在Analysis前进行Parsing,即预处理。
4.2 Analyzer内部详解
Analyzer类是抽象类,将text转换为TokenStream,一般只需要实现这个方法即可:
public TokenStream tokenStream(String fieldName, Reader reader)
例如SimpleAnalyzer
public final class SimpleAnalyzer extends Analyzer {
@Override
public TokenStream tokenStream(String fieldName, Reader reader) {
return new LowerCaseTokenizer(reader);
}
@Override
public TokenStream reusableTokenStream(String fieldName, Reader reader
throws IOException {
Tokenizer tokenizer = (Tokenizer) getPreviousTokenStream();
if (tokenizer == null) {
tokenizer = new LowerCaseTokenizer(reader);
setPreviousTokenStream(tokenizer);
} else
tokenizer.reset(reader);
return tokenizer;
}
}
LowerCaseTokenizer将text在非字母处分隔开,移除非字母的字符,并对字母小写化。
reusableTokenStream允许对已经使用过的TokenStream进行重用。
Token内部
Token除了存储每个独立词之外,还包含了每个词的Meta信息,例如在Text中的偏移pos,以及position increment,默认为1,但也可以调整以用于它用。
例如position increment = 0 可用于注入同义词
position increment = 2 表示中间有被删除的单词
Token还可以包含由Application指定的额外数据payload(byte [ ])。
TokenStream
TokenStream用来产生一系列的Token,主要有Tokenizer和TokenFilter两种,他们均继承自TokenStream。
Tokenizer:从Reader读取字符,并创建Tokens
TokenFilter:读入Tokens,并由此产生新的Token,或删除部分Token。
处理流程:
Reader -> Tokenizer -> TokenFilter -> ...... -> TokenFilter -> Tokens
核心的Tokenizer
Tokenizer:以Reader为输入的Token处理抽象类。
CharTokenizer:基于字符的Tokenizer抽象类,当isTokenChar()返回true的时候,产生新Token,也具备小(大)写化的功能。
WhitespaceTokenizer:继承自CharTokenizer,所有非空白的char,isTokenChar()均返回true,其他false。
KeywordTokenizer:将全部输入作为一个Token。
LetterTokenizer:继承自CharTokenizer,所有字母的char,isTokenChar()均返回true,其他false。
LowerCaseTokenizer:继承自,LetterTokenizer,并让所有字母变成小写。
SinkTokenizer:吸收Tokens,可结合TeeTokenizer用于分隔Token。
StandardTokenizer:基于语法的Tokenizer,例如可分离出高级结构例如E-Mail地址,可能要结合StandardFilter使用。
核心的TokenFilter
LowerCaseFilter:将Token小写化。
StopFilter:将存在于list中的Stopwords 移除
PorterStemFilter:同一化词根,例如country和countries被转化为countri。
TeeTokenFilter:分隔TokenStream。
ASCIIFoldingFilter:??
CachingTokenFilter:
LengthFilter:只接受Token长度在指定范围内的,其余丢弃。
StandardFilter:与StandardTokenizer配合使用,移除缩略词中的.省略号等。
在Analyzer中,可以把这些Tokenizer和TokenFilter结合起来使用。
public TokenStream tokenStream(String fieldName, Reader reader) {
return new StopFilter(true, new LowerCaseTokenizer(reader), stopWords);
}
如何查看Analyzer的结果
AnalyzerUtils.displayTokens(analyzer, text);
AnalyzerUtils将调用Analyzer模拟进行分词,并将生成的Token显示出来。
具体过程如下:
public static void displayTokens(Analyzer analyzer,
String text) throws IOException {
displayTokens(analyzer.tokenStream("contents", new StringReader(text))); //A
}
public static void displayTokens(TokenStream stream)
throws IOException {
TermAttribute term = (TermAttribute) stream.addAttribute(TermAttribute.class)
while(stream.incrementToken()) {
System.out.print("[" + term.term() + "] "); //B
}
}
步骤:
1、调用analyzer的tokenStream,生成TokenStream(N多Token)。
2、在TokenStream上注册Attibute,
2、使用stream.incrementToken()遍历TokenStream,使用注册的Attribute获取每一个Term。
也可以注册PositionIncrementAttribute、OffsetAttribute、TypeAttribute等以获取更详细的Term信息。
关于Attribute和AttributeSource
在Lucene2.9之后,废弃了单独的Token类,采用Attrubute来遍历Token以提升系统性能。
TokenStream继承自AttributeSource,它可以提供可拓展的强类型而无需耗时的运行时强制转换。
用法:
1.注册:通过TokenStream的addAttribute获得对应的Attribute,
2.遍历:在stream.incrementToken()返回true的情况下,通过attribute来读取具体属性。
这个属性是双向的,通过对Attribute的更改可同步更新到实际的Token中。
Start、End Offset能做什么?
一般被存储于TermVectors中,用于高亮。
TokenType
一般来说,在TokenStream总,Token是有类型的,并且是有用的。
TypeAttribute可获得Token的具体类型
StandardAnalyzer和StandardTokenizer会自动给Token加上不同的类型。
Type不计入Index中,而只在Analysis中使用。
TokenFilter的顺序很重要
Filter需要在Token基础上进一步处理,因此经常依赖某一些处理结果。
例如StopFilter是大小写敏感的,所以要求之前Filter已经把字母全部小写化,否则可能The这种StopWord就没法被过滤了。
记住Analyzer是一个链条,因此一定要注意顺序。
4.3 使用内置的Analyzer
内置的Analyzer:WhitespaceAnalyzer、SimpleAnalyzer、StopAnalyzer、KeywordAnalyzer、StandardAnalyzer,几乎可用于处理所有西方文字(西欧语系)。
StopAnalyzer
除了基础的分词、小写化外,还从Token中移除StopWords,这个StopWords的List可以指定,默认为:
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it", "no", "not", "of", "on", "or", "such",
"that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
StandardAnalyzer
StandardAnalyzer有“最通用”Analyzer之城,内置了JFlex进行语法分析。
能处理数字、字母、首字母缩略、公司名、电子邮件、计算机主机名、内部省略号的单词、数字、IP、中文字符等。
同时还包含StopWords、
4.4 Sounds Like查询
例如Indexs中的cool cat 但是查询词是kool cat,这就是Sounds Like。
可以构造下述的分词器:
public class MetaphoneReplacementFilter extends TokenFilter {
public static final String METAPHONE = "METAPHONE";
private Metaphone metaphoner = new Metaphone(); //#A
private TermAttribute termAttr;
private TypeAttribute typeAttr;
public MetaphoneReplacementFilter(TokenStream input) {
super(input);
termAttr = (TermAttribute) addAttribute(TermAttribute.class);
typeAttr = (TypeAttribute) addAttribute(TypeAttribute.class);
}
public boolean incrementToken() throws IOException {
if (!input.incrementToken()) //#B
return false; //#C
String encoded;
encoded = metaphoner.encode(termAttr.term()); //#D
termAttr.setTermBuffer(encoded); //#E
typeAttr.setType(METAPHONE); //#F
return true;
}
}
标红的部分,将本kool替换成了cool,实际上可能是更简单的形式。
例如,对于The quick brown fox jumped over the lazy dogs
将被metaphoner.encode整理为[0] [KK] [BRN] [FKS] [JMPT] [OFR] [0] [LS] [TKS]
对于Sounds Like的文本:Tha quik brown phox jumpd ovvar tha lazi dogz
也就是两个类似的词被整理为精简版的单词,显然这个方法挺土的。
4.5 同义词检索
SynonymAnalyzer的目的是,将同义词替换为一个统一的,替换掉原来的位置。
public class SynonymAnalyzer extends Analyzer {
private SynonymEngine engine;
public SynonymAnalyzer(SynonymEngine engine) {
this.engine = engine;
}
public TokenStream tokenStream(String fieldName, Reader reader) {
TokenStream result = new SynonymFilter(
new StopFilter(true,
new LowerCaseFilter(
new StandardFilter(
new StandardTokenizer(
Version.LUCENE_CURRENT, reader))),
StopAnalyzer.ENGLISH_STOP_WORDS_SET),
engine
);
return result;
}
}
这个Analyzer除了在最后的Chain加上了SynonymFilter外并没有什么其他区别。
public class SynonymFilter extends TokenFilter {
public static final String TOKEN_TYPE_SYNONYM = "SYNONYM";
private Stack synonymStack;
private SynonymEngine engine;
private TermAttribute termAttr;
private AttributeSource save;
public SynonymFilter(TokenStream in, SynonymEngine engine) {
super(in);
synonymStack = new Stack(); //#1
termAttr = (TermAttribute) addAttribute(TermAttribute.class);
save = in.cloneAttributes();
this.engine = engine;
}
public boolean incrementToken() throws IOException {
if (synonymStack.size() > 0) { //#2
State syn = (State) synonymStack.pop(); //#2
restoreState(syn); //#2
return true;
}
if (!input.incrementToken()) //#3
return false;
addAliasesToStack(); //#4
return true; //#5
}
private void addAliasesToStack() throws IOException {
String[] synonyms = engine.getSynonyms(termAttr.term()); //#6
if (synonyms == null) return;
State current = captureState();
for (int i = 0; i < synonyms.length; i++) { //#7
save.restoreState(current);
AnalyzerUtils.setTerm(save, synonyms[i]); //#7
AnalyzerUtils.setType(save, TOKEN_TYPE_SYNONYM); //#7
AnalyzerUtils.setPositionIncrement(save, 0); //#8
synonymStack.push(save.captureState()); //#7
}
}
}
4.8 处理其他语言
处理其他语言面临的问题,特别是亚洲语言:无法通过空格来分词!
关于编码:应该统一使用UTF8。
对于其他西欧语言,可以使用SnowballAnalyzer(在contrib中)
Character规则化
对于某些场景,需要在Analyzer(Tokenizer)之前,对Reader读入的字符进行规则化。
例如:将繁体中文字符替换成简体中文。
Lucene提供了CharFilters,用于包装Reader,从而完成字符的替换、规则化工作。
Core提供的唯一、具体的实现类是:MappingCharFilter,提供一个Mapping,将原始字符替换为目标字符~
对亚洲语言进行分词
简言之:最好不要使用SimpleAnalyzer、StandardAnalyzer,书中推荐的是SmartChineseAnalyzer,类似的还有很多,网上一搜一大把~