一個優秀的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過濾器。
要實現一種Lucene的分析器(Analyzer),至少要實現一個分詞器(Tokenizer)。對於特定語言來說,必要的過濾器 (TokenFilter)也是不可缺少的。其中過濾器有很多種,主要可以用來對分詞結果進行標準化。比如去停用詞、轉換大小寫、英文的詞幹化 (stemming)和詞類歸併 (lemmatization)等等。下面我們看看Tokenizer和TokenFilter的主要代碼:
分詞器和過濾器都是TokenStream的子類。而過濾器的構造參數需要的就是TokenStream。這是一種裝飾者的模式設計,我們可以通過 嵌套調用來達到不同的過濾目的。比如: new XTokenFilter(new YTokenFilter( new XTokenizer))。
相對於老版本的Lucene分詞器,3.0版本的Lucene的Tokenizer多了一種構造器。
二、Lucene的標準分析器——StandardAnalyzer
org.apache.lucene.analysis.standard 包含了Lucene的標準分析器(StandardAnalyzer),它由標準分詞器(StandardTokenizer)和標準過濾器 (StandardFilter)構成。都只能處理英文。
StandardAnalyzer 部分源代碼如下:
StandardAnalyzer是Lucene索引建立和檢索索引時都需要使用的分析器,tokenStream方法的作用就是對輸入流reader先進行分詞,再進行一系列的過濾。
標準分詞器:StandardTokenizer
Lucene的英文分詞器使用了JFlex的詞法掃描方法。其具體實現在初始化StandardTokenizerImpl類時,通過調用類中的靜 態方法和StandardTokenizerImpl.jflex詞法描述文件來一起解析待分詞的輸入流。並將最後掃描出來的詞語分成 <ALPHANUM>、<APOSTROPHE>、<ACRONYM>、<COMPANY>、<EMAIL>、<HOST>、<NUM>、<CJ>、 <ACRONYM_DEP>九大類。這一過程和Java編譯器的詞法分析程序對Java程序的關鍵字、變量名等進行解析是一樣的。因此想要了 解JFlex,必須知道編譯原理的相關知識,這裏就不展開了(因爲我也不知道)。
scanner是StandardTokenizerImpl類初始化的對象,這個對象裏存儲了掃描輸入流字串得到的詞元信息(詞元的內容、長度、 所屬的類別、所在位置等)。相對於較早的版本,Lucene 3.0在這裏有很大的變化。它沒有用next()方法直接得到TokenStream的下一個詞元內容,而是使用incrementToken()方法將 每一個scanner.getNextToken()的各種詞元信息保存在不同類型的Attribute裏面,比如TermAttribute用於保存詞 元的內容,TyteAttribute用於保存詞元的類型。
標準過濾器:StandardFilter
- public final class StandardFilter extends TokenFilter {
-
- /**
- * 去除詞語末尾的“'s” 如 it's-> it
- * 去除縮略語中的“.” 如U.S.A -> USA
- */
- @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<bufferLength;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 | <APOSTROPHE> | (0,3) | 1 |
a | <ALPHANUM> | (4,5) | 1 |
student | <ALPHANUM> | (6,13) | 1 |
these | <ALPHANUM> | (15,20) | 1 |
are | <ALPHANUM> | (21,34) | 1 |
apples | <ALPHANUM> | (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 就屬於<APOSTROPHE>,有撇號的類型
(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,說明肯定沒有緊挨着。怎麼樣,用處很大吧。輕而易舉 的解決了短語搜索的難題哦。