《Lucene In Action》第四章.Analysis(分词)

简单来说,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,类似的还有很多,网上一搜一大把~

Leave a Reply

Your email address will not be published. Required fields are marked *