一个优秀的IR system要做好的第一件事就是利用自然语言处理技术(NLP)对文本进行分析。其中分词是最基本的,其性能直接决定IR system的搜索精度和速度。因此,大型Web搜索引擎都有自己的分词工具。
Lucene3.0 的分析器由三个包组成:
(1) org.apache.lucene.analysis 是Lucene分析器的基本结构包。包含了分析器最底层的结构(Analyzer、Tokenizer、TokenFilter接口和抽象类),一些简单分析器的具体实现类(如SimpleAnayzer, StopAnalyzer),一些常用的分词器和过滤器(如LowerCaseTokenizer、LowerCaseFilter)。
(2) org.apache.lucene.analysis.standard 是Lucene标准分析器的实现包。其功能就是为了实现英文的标准分词。
(3) org.apache.lucene.analysis.tokenattribute 是分词后token的属性结构包。其实Lucene分词并不仅仅只是得到词语本身,而是要得到每个词语的多种信息(属性)。比如词语字符串、类型、位置信息、存储的时候元数据信息等等。
一、 Lucene的分析器结构
org.apache.lucene.analysis 是Lucene Analyzer底层结构包。主要包括Analyzer、Tokenizer和TokenFilter的接口规定。实际上,Lucene的Analyzer主要功能包括两个部分:(1)Tokenzier 分词器 (2)TokenFilter过滤器。
-
-
-
-
-
-
public abstract class Analyzer implements Closeable {
-
-
public abstract TokenStream tokenStream(String fieldName, Reader reader);
-
-
}
要实现一种Lucene的分析器(Analyzer),至少要实现一个分词器(Tokenizer)。对于特定语言来说,必要的过滤器(TokenFilter)也是不可缺少的。其中过滤器有很多种,主要可以用来对分词结果进行标准化。比如去停用词、转换大小写、英文的词干化(stemming)和词类归并 (lemmatization)等等。下面我们看看Tokenizer和TokenFilter的主要代码:
-
-
public abstract class Tokenizer extends TokenStream {
-
-
protected Reader input;
-
-
protected Tokenizer() {
-
}
-
-
protected Tokenizer(Reader input) {
-
this.input = CharReader.get(input);
-
}
-
-
@Override
-
public void close() throws IOException {
-
input.close();
-
}
-
}
-
-
public abstract class TokenFilter extends TokenStream {
-
-
protected final TokenStream input;
-
-
protected TokenFilter(TokenStream input) {
-
super(input);
-
this.input = input;
-
}
-
-
@Override
-
public void close() throws IOException {
-
input.close();
-
}
-
}
分词器和过滤器都是TokenStream的子类。而过滤器的构造参数需要的就是TokenStream。这是一种装饰者的模式设计,我们可以通过嵌套调用来达到不同的过滤目的。比如: new XTokenFilter(new YTokenFilter( new XTokenizer))。
相对于老版本的Lucene分词器,3.0版本的Lucene的Tokenizer多了一种构造器。
-
protected Tokenizer(AttributeSource source)
二、Lucene的标准分析器——StandardAnalyzer
org.apache.lucene.analysis.standard 包含了Lucene的标准分析器(StandardAnalyzer),它由标准分词器(StandardTokenizer)和标准过滤器(StandardFilter)构成。都只能处理英文。
StandardAnalyzer 部分源代码如下:
-
public class StandardAnalyzer extends Analyzer {
-
-
public static final Set> STOP_WORDS_SET = StopAnalyzer.ENGLISH_STOP_WORDS_SET;
-
-
public StandardAnalyzer(Version matchVersion) {
-
this(matchVersion, STOP_WORDS_SET);
-
}
-
-
@Override
-
public TokenStream tokenStream(String fieldName, Reader reader) {
-
-
StandardTokenizer tokenStream = new StandardTokenizer(matchVersion, reader);
-
-
tokenStream.setMaxTokenLength(maxTokenLength);
-
-
TokenStream result = new StandardFilter(tokenStream);
-
-
result = new LowerCaseFilter(result);
-
-
result = new StopFilter(enableStopPositionIncrements, result, stopSet);
-
return result;
-
}
-
-
}
StandardAnalyzer是Lucene索引建立和检索索引时都需要使用的分析器,tokenStream方法的作用就是对输入流reader先进行分词,再进行一系列的过滤。
标准分词器:StandardTokenizer
-
public final class StandardTokenizer extends Tokenizer {
-
-
-
private final StandardTokenizerImpl scanner;
-
-
private TermAttribute termAtt;
-
private OffsetAttribute offsetAtt;
-
private PositionIncrementAttribute posIncrAtt;
-
private TypeAttribute typeAtt;
-
-
-
public StandardTokenizer(Version matchVersion, Reader input) {
-
super();
-
this.scanner = new StandardTokenizerImpl(input);
-
init(input, matchVersion);
-
}
-
-
private void init(Reader input, Version matchVersion) {
-
if (matchVersion.onOrAfter(Version.LUCENE_24)) {
-
replaceInvalidAcronym = true;
-
} else {
-
replaceInvalidAcronym = false;
-
}
-
this.input = input;
-
termAtt = addAttribute(TermAttribute.class);
-
offsetAtt = addAttribute(OffsetAttribute.class);
-
posIncrAtt = addAttribute(PositionIncrementAttribute.class);
-
typeAtt = addAttribute(TypeAttribute.class);
-
}
-
-
-
-
@Override
-
public final boolean incrementToken() throws IOException {
-
clearAttributes();
-
int posIncr = 1;
-
-
while(true) {
-
-
int tokenType = scanner.getNextToken();
-
-
if (tokenType == StandardTokenizerImpl.YYEOF) {
-
return false;
-
}
-
-
if (scanner.yylength() <= maxTokenLength) {
-
posIncrAtt.setPositionIncrement(posIncr);
-
-
scanner.getText(termAtt);
-
-
final int start = scanner.yychar();
-
-
offsetAtt.setOffset(correctOffset(start), correctOffset(start+termAtt.termLength()));
-
-
if (tokenType == StandardTokenizerImpl.ACRONYM_DEP) {
-
if (replaceInvalidAcronym) {
-
typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[StandardTokenizerImpl.HOST]);
-
termAtt.setTermLength(termAtt.termLength() - 1);
-
} else {
-
typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[StandardTokenizerImpl.ACRONYM]);
-
}
-
} else {
-
typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[tokenType]);
-
}
-
return true;
-
} else
-
posIncr++;
-
}
-
}
-
}
Lucene的英文分词器使用了JFlex的词法扫描方法。其具体实现在初始化StandardTokenizerImpl类时,通过调用类中的静态方法和StandardTokenizerImpl.jflex词法描述文件来一起解析待分词的输入流。并将最后扫描出来的词语分成 、、、、、、、、 九大类。这一过程和Java编译器的词法分析程序对Java程序的关键字、变量名等进行解析是一样的。因此想要了解JFlex,必须知道编译原理的相关知识,这里就不展开了(因为我也不知道)。
scanner是StandardTokenizerImpl类初始化的对象,这个对象里存储了扫描输入流字串得到的词元信息(词元的内容、长度、所属的类别、所在位置等)。相对于较早的版本,Lucene 3.0在这里有很大的变化。它没有用next()方法直接得到TokenStream的下一个词元内容,而是使用incrementToken()方法将每一个scanner.getNextToken()的各种词元信息保存在不同类型的Attribute里面,比如TermAttribute用于保存词元的内容,TyteAttribute用于保存词元的类型。
标准过滤器:StandardFilter
-
public final class StandardFilter extends TokenFilter {
-
-
-
-
-
-
@Override
-
public final boolean incrementToken() throws java.io.IOException {
-
if (!input.incrementToken()) {
-
return false;
-
}
-
char[] buffer = termAtt.termBuffer();
-
final int bufferLength = termAtt.termLength();
-
final String type = typeAtt.type();
-
if (type == APOSTROPHE_TYPE && bufferLength >= 2 && buffer[bufferLength-2] == '\'' && (buffer[bufferLength-1] == 's' || buffer[bufferLength-1] == 'S')) {
-
-
termAtt.setTermLength(bufferLength - 2);
-
} else if (type == ACRONYM_TYPE) {
-
int upto = 0;
-
for(int i=0;i
-
char c = buffer[i];
-
if (c != '.')
-
buffer[upto++] = c;
-
}
-
termAtt.setTermLength(upto);
-
}
-
return true;
-
}
-
}
三、token的属性结构Attribute
首先我们用下面的代码来看看打印标准分词器的运行结果
-
class StandardTest{
-
public static void main(String[] args) throws IOException{
-
-
StringReader s=new StringReader(new String("I'm a student. these are apples"));
-
-
TokenStream tokenStream = new StandardTokenizer(Version.LUCENE_CURRENT, s);
-
-
tokenStream=new StandardFilter(tokenStream);
-
-
tokenStream=new LowerCaseFilter(tokenStream);
-
-
TermAttribute termAtt=(TermAttribute)tokenStream.getAttribute(TermAttribute.class);
-
TypeAttribute typeAtt=(TypeAttribute)tokenStream.getAttribute(TypeAttribute.class);
-
OffsetAttribute offsetAtt=(OffsetAttribute)tokenStream.getAttribute(OffsetAttribute.class);
-
PositionIncrementAttribute posAtt=(PositionIncrementAttribute)tokenStream.getAttribute(PositionIncrementAttribute.class);
-
-
-
System.out.println("termAtt typeAtt offsetAtt posAtt");
-
while (tokenStream.incrementToken()) {
-
System.out.println(termAtt.term()+" "+typeAtt.type()+" ("+offsetAtt.startOffset()+","+offsetAtt.endOffset()+") "+posAtt.getPositionIncrement());
-
}
-
}
-
}
打印结果:
|
termAtt
|
typeAtt
|
offsetAtt
|
posAtt
|
|
i'm
|
|
(0,3)
|
1
|
|
a
|
|
(4,5)
|
1
|
|
student
|
|
(6,13)
|
1
|
|
these
|
|
(15,20)
|
1
|
|
are
|
|
(21,34)
|
1
|
|
apples
|
|
(25,31)
|
1
|
在前面讲 StandardTokenizer的的时候,我们已经谈到了token的这四种属性。在这里我们再次强调一下这些Lucene的基础知识。
Lucene 3.0之后,TokenStream中的每一个token不再用next()方法返回,而是采用了incrementToken()方法(具体参见上面)。每调用一次incrementToken(),都会得到token的四种属性信息(org.apache.lucene.analysis.tokenattributes包中):
如上例:
原文本:I'm a student. these are apples
TokenSteam: [1: I'm ] [2:a] [3:student] [4:these] [5:are ] [6:apples]
(1) TermAttribute: 表示token的字符串信息。比如"I'm"
(2) TypeAttribute: 表示token的类别信息(在上面讲到)。比如 I'm 就属于,有撇号的类型
(3) OffsetAttribute:表示token的首字母和尾字母在原文本中的位置。比如 I'm 的位置信息就是(0,3)
(4) PositionIncrementAttribute:这个有点特殊,它表示tokenStream中的当前token与前一个token在实际的原文本中相隔的词语数量。
比如: 在tokenStream中[2:a] 的前一个token是[1: I'm ] ,它们在原文本中相隔的词语数是1,则token="a"的PositionIncrementAttribute值为1。如果token是原文本中的第一个词,则默认值为1。因此上面例子的PositionIncrementAttribute结果就全是1了。
如果我们使用停用词表来进行过滤之后的话:TokenSteam就会变成: [1: I'm ] [2:student] [3:apples]这时student的PositionIncrementAttribute值就不会再是1,而是与[1: I'm ]在原文本中相隔词语数量=2。而apples则变成了5。
那么这个属性有什么用呢,用处很大的。加入我们想搜索一个短语student apples(假如有这个短语)。很显然,用户是要搜索出student apples紧挨着出现的文档。这个时候我们找到了某一篇文档(比如上面例子的字符串)都含有student apples。但是由于apples的PositionIncrementAttribute值是5,说明肯定没有紧挨着。怎么样,用处很大吧。轻而易举的解决了短语搜索的难题哦。
其实还有两种:PayloadAttribute和FlagsAttribute。我暂时还不知道他们的具体作用
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/25851087/viewspace-1413657/
,如需转载,请注明出处,否则将追究法律责任。