Lucene与搜索引擎技术 TjuAILab windshow 2005.11.11 Analysis包分析 算法和数据结构分析: 由于Analysis包比较简单,不详述了! 算法:基于机械分词 1-gram,2-gram,HMM(如果使用ICTCLAS接口的话) 数据结构:部分源码用到了Set ,HashTable,HashMap 认真理解Token Lucene中的Analysis包专门用于完成对于索引文件的分词.Lucene中的Token是一个非常重要的概念. 看一下其源码实现: public final class Token { String termText; // the text of the term int startOffset; // start in source text int endOffset; // end in source text String type = "word"; // lexical type private int positionIncrement = 1; public Token(String text, int start, int end) public Token(String text, int start, int end, String typ) public void setPositionIncrement(int positionIncrement) public int getPositionIncrement() { return positionIncrement; } public final String termText() { return termText; } public final int startOffset() { return startOffset; } public void setStartOffset(int givenStartOffset) public final int endOffset() { return endOffset; } public void setEndOffset(int givenEndOffset) public final String type() { return type; } public String toString() } 下面编一段代码来看一下 TestToken.java package org.apache.lucene.analysis.test; import org.apache.lucene.analysis.*; import org.apache.lucene.analysis.standard.StandardAnalyzer; import java.io.*; public class TestToken { public static void main(String[] args) { String string = new String("我爱天大,但我更爱中国"); //Analyzer analyzer = new StandardAnalyzer(); Analyzer analyzer = new TjuChineseAnalyzer(); //Analyzer analyzer= new StopAnalyzer(); TokenStream ts = analyzer.tokenStream("dummy",new StringReader(string)); Token token; try { int n=0; while ( (token = ts.next()) != null) { System.out.println((n++)+"->"+token.toString()); } } catch(IOException ioe) { ioe.printStackTrace(); }
} }注意看其结果如下所示 0->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(我,0,1,<CJK>,1) 1->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,1,2,<CJK>,1) 2->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(天,2,3,<CJK>,1) 3->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(大,3,4,<CJK>,1) 4->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(但,5,6,<CJK>,1) 5->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(我,6,7,<CJK>,1) 6->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(更,7,8,<CJK>,1) 7->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,8,9,<CJK>,1) 8->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(中,9,10,<CJK>,1) 9->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(国,10,11,<CJK>,1) 注意:其中”,”被StandardAnalyzer给过滤掉了,所以大家注意第4个Token直接startOffset从5开始. 如果改用StopAnalyzer() 0->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(我爱天大,0,4,word,1) 1->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(但我更爱中国,5,11,word,1) 改用TjuChineseAnalyzer(我写的,下文会讲到如何去写) 0->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,3,4,word,1) 1->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(天大,6,8,word,1) 2->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(更,19,20,word,1) 3->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,22,23,word,1) 4->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(中国,25,27,word,1) 讲明白了Token,咱们来看以下其他的东西 一个TokenStream是用来走访Token的iterator(迭代器) 看一下其源代码: public abstract class TokenStream { public abstract Token next() throws IOException; public void close() throws IOException {} } 一个Tokenizer,is-a TokenStream(派生自TokenStream),其输入为Reader 看一下其源码如下: public abstract class Tokenizer extends TokenStream { protected Reader input; protected Tokenizer() {} protected Tokenizer(Reader input) { this.input = input; } public void close() throws IOException { input.close(); } } 一个TokenFilter is–a TokenStream(派生自TokenStream),其义如名就是用来完成对TokenStream的过滤操作,譬如 去StopWords,将Token变为小写等。 源码如下: public abstract class TokenFilter extends TokenStream { protected TokenStream input; protected TokenFilter() {} protected TokenFilter(TokenStream input) { this.input = input; } public void close() throws IOException { input.close(); } } 一个Analyzer就是一个TokenStream工厂 看一下其源码就: public abstract class Analyzer { public TokenStream tokenStream(String fieldName, Reader reader) { return tokenStream(reader); } public TokenStream tokenStream(Reader reader) { return tokenStream(null, reader); } } 好,现在咱们来看一下Lucene的Analysis包下面的各个类文件都是用来干什么的。按照字典排序。 Analysis包中的源码详解 Analyzer.java 上文已经讲过。 CharTokenizer.java 此类为简单一个抽象类,用来对基于字符的进行简单分词(tokenizer) LetterTokenizer.java两个非字符之间的字符串定义为token(举例来说英文单词由空白隔开,那个两个空白之间的字符串即被定义为一个token。备注:对于绝大多数欧洲语言来说,这个类工作效能很好。当时对于不用空白符分割的亚洲语言,效能极差(譬如中日韩)。) LowerCaseFilter.java is-a TokenFilter用于将字母小写化 LowerCaseTokenizer is-a Tokenizer功能上等价于LetterTokenizer+LowerCaseFilter PerFieldAnalyzerWrapper是一个Analyzer,因为继承自Analyzer当不同的域(Field)需要不同的语言分析器(Analyzer)时,这个Analyzer就派上了用场。使用成员函数addAnalyzer可以增加一个非缺省的基于某个Field的analyzer。很少使用。 PorterStemFilter.java使用词干抽取算法对每一个token流进行词干抽取。 PorterStemmer.java 有名的P-stemming算法 SimpleAnalyzer.java StopAnalyzer.java 具有过滤停用词的功能 StopFilter.java StopFilter为一个Filter,主要用于从token流中去除StopWords Token.java 上面已讲. TokenFilter.java 上面已经讲了 Tokenizer.java 上面已经讲了 TokenStream.java 上面已经讲了 WhitespaceAnalyzer.java WhitespaceTokenizer.java 只是按照space区分Token.
由于Lucene的analyisis包下的Standard包下的StandardAnalyzer()功能很强大,而且支持CJK分词,我们简要说一下. 此包下的文件是有StandardTokenizer.jj经过javac命令生成的.由于是机器自动生成的代码,可能可读性很差,想了解的话好好看看那个StandardTokenizer.jj文件就会比较明了了. Lucene常用的Analyzer功能概述. WhitespaceAnalyzer:仅仅是去除空格,对字符没有lowcase化,不支持中文 SimpleAnalyzer:功能强于WhitespaceAnalyzer,将除去letter之外的符号全部过滤掉,并且将所有的字符lowcase化,不支持中文 StopAnalyzer:StopAnalyzer的功能超越了SimpleAnalyzer,在SimpleAnalyzer的基础上 StandardAnalyzer:英文的处理能力同于StopAnalyzer.支持中文采用的方法为单字切分. ChineseAnalyzer:来自于Lucene的sand box.性能类似于StandardAnalyzer,缺点是不支持中英文混和分词. CJKAnalyzer:chedong写的CJKAnalyzer的功能在英文处理上的功能和StandardAnalyzer相同 TjuChineseAnalyzer:我写的,功能最为强大.TjuChineseAnlyzer的功能相当强大,在中文分词方面由于其调用的为ICTCLAS的java接口.所以其在中文方面性能上同与ICTCLAS.其在英文分词上采用了Lucene的StopAnalyzer,可以去除 stopWords,而且可以不区分大小写,过滤掉各类标点符号. 各个Analyzer的功能已经比较介绍完毕了,现在咱们应该学写Analyzer,如何diy自己的analyzer呢?? 如何DIY一个Analyzer 咱们写一个Analyzer,要求有一下功能 (1) 可以处理中文和英文,对于中文实现的是单字切分,对于英文实现的是以空格切分. (2) 对于英文部分要进行小写化. (3) 具有过滤功能,可以人工设定StopWords列表.如果不是人工设定,系统会给出默认的StopWords列表. (4) 使用P-stemming算法对于英文部分进行词缀处理. 代码如下: public final class DiyAnalyzer extends Analyzer { private Set stopWords; public static final String[] CHINESE_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", "我", "我们" }; public DiyAnalyzer() { this.stopWords=StopFilter.makeStopSet(CHINESE_ENGLISH_STOP_WORDS); }
public DiyAnalyzer(String[] stopWordList) { this.stopWords=StopFilter.makeStopSet(stopWordList); }
public TokenStream tokenStream(String fieldName, Reader reader) { TokenStream result = new StandardTokenizer(reader); result = new LowerCaseFilter(result);
result = new StopFilter(result, stopWords); result = new PorterStemFilter(result); return result; }
public static void main(String[] args) { //好像英文的结束符号标点.,StandardAnalyzer不能识别 String string = new String("我爱中国,我爱天津大学!I love China!Tianjin is a City"); Analyzer analyzer = new DiyAnalyzer(); TokenStream ts = analyzer.tokenStream("dummy", new StringReader(string)); Token token; try { while ( (token = ts.next()) != null) { System.out.println(token.toString()); } } catch (IOException ioe) { ioe.printStackTrace(); } } } 可以看见其后的结果如下: Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,1,2,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(中,2,3,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(国,3,4,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,6,7,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(天,7,8,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(津,8,9,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(大,9,10,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(学,10,11,<CJK>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(i,12,13,<ALPHANUM>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(love,14,18,<ALPHANUM>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(china,19,24,<ALPHANUM>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(tianjin,25,32,<ALPHANUM>,1) Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(citi,39,43,<ALPHANUM>,1)
到此为止这个简单的但是功能强大的分词器就写完了,下面咱们可以尝试写一个功能更强大的分词器. 如何DIY一个功能更加强大Analyzer 譬如你有词典,然后你根据正向最大匹配法或者逆向最大匹配法写了一个分词方法,却想在Lucene中应用,很简单 你只要把他们包装成Lucene的TokenStream就好了.下边我以调用中科院写的ICTCLAS接口为例,进行演示.你去中科院 网站可以拿到此接口的free版本,谁叫你没钱呢,有钱,你就可以购买了.哈哈 好,由于ICTCLAS进行分词之后,在Java中,中间会以两个空格隔开!too easy,我们直接使用继承Lucene的 WhiteSpaceTokenizer就好了. 所以TjuChineseTokenizer 看起来像是这样. public class TjuChineseTokenizer extends WhitespaceTokenizer { public TjuChineseTokenizer(Reader readerInput) { super(readerInput); } } 而TjuChineseAnalyzer看起来象是这样 public final class TjuChineseAnalyzer extends Analyzer { private Set stopWords;
/** An array containing some common English words that are not usually useful for searching. */ /* public static final String[] CHINESE_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", "我", "我们" }; */ /** Builds an analyzer which removes words in ENGLISH_STOP_WORDS. */ public TjuChineseAnalyzer() { stopWords = StopFilter.makeStopSet(StopWords.SMART_CHINESE_ENGLISH_STOP_WORDS); }
/** Builds an analyzer which removes words in the provided array. */ //提供独自的stopwords public TjuChineseAnalyzer(String[] stopWords) { this.stopWords = StopFilter.makeStopSet(stopWords); }
/** Filters LowerCaseTokenizer with StopFilter. */ public TokenStream tokenStream(String fieldName, Reader reader) { try { ICTCLAS splitWord = new ICTCLAS(); String inputString = FileIO.readerToString(reader); //分词中间加入了空格 String resultString = splitWord.paragraphProcess(inputString); System.out.println(resultString); TokenStream result = new TjuChineseTokenizer(new StringReader(resultString));
result = new LowerCaseFilter(result); //使用stopWords进行过滤 result = new StopFilter(result, stopWords); //使用p-stemming算法进行过滤 result = new PorterStemFilter(result); return result;
} catch (IOException e) { System.out.println("转换出错"); return null; } }
public static void main(String[] args) { String string = "我爱中国人民"; Analyzer analyzer = new TjuChineseAnalyzer(); TokenStream ts = analyzer.tokenStream("dummy", new StringReader(string)); Token token; System.out.println("Tokens:"); try { int n=0; while ( (token = ts.next()) != null) { System.out.println((n++)+"->"+token.toString()); } } catch (IOException ioe) { ioe.printStackTrace(); } } }对于此程序的输出接口可以看一下 0->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(爱,3,4,word,1) 1->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(中国,6,8,word,1) 2->Token‘s (termText,startOffset,endOffset,type,positionIncrement) is:(人民,10,12,word,1)
OK,经过这样一番讲解,你已经对Lucene的Analysis包认识的比较好了,当然如果你想更加了解,还是认真读读源码才好, 呵呵,源码说明一切! |
|