转载请注明出处: http://www.liubida.com/uncategorized/lucene_analyzer_intro/
没接触过lucene的童鞋可以看看 这篇文章, 算是lucence入门比较好的文章, 读完之后可以对lucene有个简单的认识. 我也是最近有项目用到了lucene, 具体实践是对数据库的某个字段做索引, 实现了一个针对该字段匹配查询. 有点意思. 然后就趁机看了下lucene中分析器(Analyzer)的一些代码, 觉得它的分词和过滤的设计还是蛮有意思的.
lucene的分析器的功能就是对输入的数据进行分词(对文本资源进行切分, 将文本按规则切分为一个个可以进入索引的最小单位)和过滤(对最小单位进行预处理, 比如大写转小写). 在lucene中, 所有的分析器都继承自抽象类Analyzer(org.apache.lucene.analysis.Analyzer). Analyzer的代码里只定义了两个毫无意义的方法. 如下所示:
* policy for extracting index terms from text.
* <p>
* Typical implementations first build a Tokenizer, which breaks the stream of
* characters from the Reader into raw Tokens. One or more TokenFilters may
* then be applied to the output of the Tokenizer.
* <p>
* WARNING: You must override one of the methods defined by this class in your
* subclass or the Analyzer will enter an infinite loop.
*/
public abstract class Analyzer {
/** Creates a TokenStream which tokenizes all the text in the provided
Reader. Default implementation forwards to tokenStream(Reader) for
compatibility with older version. Override to allow Analyzer to choose
strategy based on document and/or field. Must be able to handle null
field name for backward compatibility. */
public TokenStream tokenStream(String fieldName, Reader reader)
{
// implemented for backward compatibility
return tokenStream(reader);
}
/** Creates a TokenStream which tokenizes all the text in the provided
* Reader. Provided for backward compatibility only.
* @deprecated use tokenStream(String, Reader) instead.
* @see #tokenStream(String, Reader)
*/
public TokenStream tokenStream(Reader reader)
{
return tokenStream(null, reader);
}
}
分析器的分词功能由Tokenizer的子类来完成, 过滤功能由TokenFilter的子类来完成. 同样的, 这也是两个虚无飘渺的抽象类. 下面我直接以StandardAnalyzer为实例来讲述分词和过滤的过程.
StandardAnalyzer是lucene开发包中内置的一种Analyzer的实现, 这个分析器是最容易使用也是使用很频繁的一种Analyzer, 它使用了lucene内部自带的几种分词器和过滤器.
在其代码中, 可以看到StandardAnalyzer也是继承了Analyzer, 并且实现了抽象类Analyzer的tokenStream方法. 我们注意到在这个方法里, reader首先是作为参数创建了一个分词器StandardTokenizer, 然后用这个分词器依次创建了LowerCaseFilter(大写转小写)和StopFilter(停止词过滤器)这两个过滤器. 目前为止, 基本还是一头雾水, 完全不知道分词和过滤是怎样一个过程? 其实, 到目前为止, tokenStream方法只是利用分词器, 过滤器在reader这个读入流后面铺设了一个管道而已, 真正的分词和过滤过程是在另一个地方执行的. 打个比方, reader就好像是自来水管道, tokenStream所做的只是接着这个管道又铺设了一段管道, 并且这一段的管道是用StandardTokenizer这种材质制造的, 能够对流过的文本进行切分, 然后又加上了LowerCaseFilter和StopFilter这两个过滤网.
private Set stopSet;
public static final String[] STOP_WORDS = StopAnalyzer.ENGLISH_STOP_WORDS;
public StandardAnalyzer() {
this(STOP_WORDS);
}
public StandardAnalyzer(String[] stopWords) {
stopSet = StopFilter.makeStopSet(stopWords);
}
public TokenStream tokenStream(String fieldName, Reader reader) {
TokenStream result = new StandardTokenizer(reader);
result = new StandardFilter(result);
result = new LowerCaseFilter(result);
result = new StopFilter(result, stopSet);
return result;
}
}
分词和过滤的操作到底是从哪里开始的呢? 以车东blog上的一段代码为例
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] …
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc); //在这里!
is.close();
};
//关闭写索引器
writer.close();
}
}
debug进入writer.addDocument(doc), 最后发现它实际最终调用的是index包下DocumentWriter.java中的DocumentWriter类的private final void invertDocument(Document doc) throws IOException方法. 方法中这样一段代码, for循环中每次都会调用TokenStream的next()方法. 这个next()方法就是在不断的读取文本流的下一个token, 但是这个next()可并不那么简单.
TokenStream stream = analyzer.tokenStream(fieldName, reader);
try {
for (Token t = stream.next(); t != null; t = stream.next()) {
position += (t.getPositionIncrement() - 1);
addPosition(fieldName, t.termText(), position++);
if (++length > maxFieldLength) break;
}
} finally {
stream.close();
}
TokenStream stream = analyzer.tokenStream(fieldName, reader); 这一句就是调用了StandardAnalyzer的tokenStream方法, 根据前面的描述, 在这个方法中依次创建分词器和过滤器. tokenStream方法最终返回的result的类型其实是StopFilter, 所以invertDocument方法的for循环里调用的是StopFilter的next()方法, 如下代码所示:
for (Token token = input.next(); token != null; token = input.next())
if (!stopWords.contains(token.termText)) return token;
return null;
}
我们看到, StopFilter的next()方法的for循环里又调用了input的next()方法, 分析代码(此处略)可以发现这个input其实是传入参数result的类型, 也就是LowerCaseFilter. LowerCaseFilter的代码如下所示:
public LowerCaseFilter(TokenStream in) {
super(in);
}
public final Token next() throws IOException {
Token t = input.next();
if (t == null)
return null;
t.termText = t.termText.toLowerCase();
return t;
}
}
LowerCaseFilter的next()方法里又再次调用了input的next()方法, 而此时的input其实就是构造器的in参数, 所以input和in的类型是StandardFilter. 继续进入StandardFilter的next()方法, 代码如下所示:
org.apache.lucene.analysis.Token t = input.next();
if (t == null)
return null;
String text = t.termText();
String type = t.type();
if (type == APOSTROPHE_TYPE && // remove 's
(text.endsWith("'s") || text.endsWith("'S"))) {
return new org.apache.lucene.analysis.Token
(text.substring(0,text.length()-2),
t.startOffset(), t.endOffset(), type);
} else if (type == ACRONYM_TYPE) { // remove dots
StringBuffer trimmed = new StringBuffer();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c != '.')
trimmed.append(c);
}
return new org.apache.lucene.analysis.Token
(trimmed.toString(), t.startOffset(), t.endOffset(), type);
} else {
return t;
}
}
好吧, 第一行又调用了input的next()方法, 这里的input是传入参数result的类型->StandardTokenizer, 继续进入StandardTokenizer的next()方法.
Token token = null;
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case ALPHANUM:
token = jj_consume_token(ALPHANUM);
break;
case APOSTROPHE:
token = jj_consume_token(APOSTROPHE);
break;
case ACRONYM:
token = jj_consume_token(ACRONYM);
break;
case COMPANY:
token = jj_consume_token(COMPANY);
break;
case EMAIL:
token = jj_consume_token(EMAIL);
break;
case HOST:
token = jj_consume_token(HOST);
break;
case NUM:
token = jj_consume_token(NUM);
break;
case CJK:
token = jj_consume_token(CJK);
break;
case 0:
token = jj_consume_token(0);
break;
default:
jj_la1[0] = jj_gen;
jj_consume_token(-1);
throw new ParseException();
}
if (token.kind == EOF) {
{if (true) return null;}
} else {
{if (true) return
new org.apache.lucene.analysis.Token(token.image,
token.beginColumn,token.endColumn,
tokenImage[token.kind]);}
}
throw new Error("Missing return statement in function");
}
终于, 在这里, 我们没有继续看到next()方法. 在StandardTokenizer的next()方法里, 我们看到了深藏其中的分词功能代码. StandardTokenizer其实就是JavaCC为lucene生成的分词器, 它的next()方法则是从reader对象中取出下一个词. 这个代码的分析我就不细谈了, 水太深, 暂时也用不太着.
我们来回顾下, 源头是DocumentWriter类的invertDocument()方法中for循环, 每次循环都会调用一次next()方法, 就好像自来水管道的口, 每一次调用就好像是从管道里吸水, 调用一次就吸一次. 这里需要注意的是StopFilter的next()方法, 里面也有一个for循环, 但是注意, 这里的for循环的目的是不停的寻找停止词, 也就是说invertDocement()方法中每次调用next(), 就是一次从管道里吸水的过程, 而从管道里吸多少水是由StopFilter中next()决定的, 一旦遇到停止词就返回本次next()调用提取的词.
为了更加清晰的理解这个过程, 这几个类的关系如下图所示:

好了, 就这样吧.
——————我是分割线—————-
停止词, 指的就是如英文中的a, an, the这样的小单词, 它们在建立索引的过程中并无实际的意义, 因此需要被过滤掉, 不能加入索引.
“StopAnalyzer.ENGLISH_STOP_WORDS;” 定义了这些停止词的集合.
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "s", "such",
"t", "that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
};
车东的blog: http://www.chedong.com/tech/lucene.html
lucene细致深入的分析: http://forfuture1978.iteye.com/category/89151
书: 《Ajax+Lucene构建搜索引擎》