【Lucene3.0】 Analyzer

一個優秀的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過濾器。

 

Java代碼
 
  1. /** 
  2.  * Analyzer 定義了從文本中抽取詞的一組規範。 
  3.  * 首先要實現一個Tokenizer,這個類會把輸入流中的字符串切分成原始的詞元。 
  4.  * 然後多個TokenFilter 就能夠將這些詞元規範化得到分詞的結果 
  5.  */ 
  6. public abstract class Analyzer implements Closeable { 
  7.       //具體實現應該是要返回一個嵌套了分詞器和過濾器的對象。      
  8.       public abstract TokenStream tokenStream(String fieldName, Reader reader); 
  9.       //...... 
 

要實現一種Lucene的分析器(Analyzer),至少要實現一個分詞器(Tokenizer)。對於特定語言來說,必要的過濾器 (TokenFilter)也是不可缺少的。其中過濾器有很多種,主要可以用來對分詞結果進行標準化。比如去停用詞、轉換大小寫、英文的詞幹化 (stemming)和詞類歸併 (lemmatization)等等。下面我們看看Tokenizer和TokenFilter的主要代碼:

Java代碼  收藏代碼
 
  1. //Tokenizer 
  2. public abstract class Tokenizer extends TokenStream { 
  3.      /**待分詞的文本輸入流 */ 
  4.      protected Reader input; 
  5.      /**無參構造器 */ 
  6.      protected Tokenizer() { 
  7.      } 
  8.      /** 帶輸入流的構造器*/ 
  9.      protected Tokenizer(Reader input) { 
  10.           this.input = CharReader.get(input); 
  11.      } 
  12.      /** 關閉輸入流 */ 
  13.      @Override 
  14.      public void close() throws IOException { 
  15.          input.close(); 
  16.      } 
Java代碼  收藏代碼
 
  1. //TokenFilter 
  2. public abstract class TokenFilter extends TokenStream { 
  3.   /** 待過濾的詞元流 */ 
  4.   protected final TokenStream input; 
  5.   /** 構造器 */ 
  6.   protected TokenFilter(TokenStream input) { 
  7.        super(input); 
  8.         this.input = input; 
  9.   } 
  10.   /** 關閉流 */ 
  11.   @Override 
  12.   public void close() throws IOException { 
  13.         input.close(); 
  14.   } 

分詞器和過濾器都是TokenStream的子類。而過濾器的構造參數需要的就是TokenStream。這是一種裝飾者的模式設計,我們可以通過 嵌套調用來達到不同的過濾目的。比如: new XTokenFilter(new YTokenFilter( new XTokenizer))。

 

相對於老版本的Lucene分詞器,3.0版本的Lucene的Tokenizer多了一種構造器。

Java代碼  收藏代碼
  1. protected Tokenizer(AttributeSource source) 
  

二、Lucene的標準分析器——StandardAnalyzer

 

org.apache.lucene.analysis.standard 包含了Lucene的標準分析器(StandardAnalyzer),它由標準分詞器(StandardTokenizer)和標準過濾器 (StandardFilter)構成。都只能處理英文。

 

StandardAnalyzer 部分源代碼如下:

Java代碼  收藏代碼
 
  1. public class StandardAnalyzer extends Analyzer { 
  2.         /**英語停用詞表*/ 
  3.         public static final Set<?> STOP_WORDS_SET = StopAnalyzer.ENGLISH_STOP_WORDS_SET; 
  4.         /**若干構造器*/ 
  5.         public StandardAnalyzer(Version matchVersion) { 
  6.            this(matchVersion, STOP_WORDS_SET); 
  7.         } 
  8.         /**分詞並進行標準過濾、大小寫過濾和停用詞過濾*/ 
  9.         @Override 
  10.         public TokenStream tokenStream(String fieldName, Reader reader) { 
  11.                  //構造一個標準分詞器,並進行分詞 
  12.                  StandardTokenizer tokenStream = new StandardTokenizer(matchVersion, reader); 
  13.                  //設置分詞後詞元流的最大長度 
  14.                  tokenStream.setMaxTokenLength(maxTokenLength); 
  15.                  //進行標準過濾 
  16.                  TokenStream result = new StandardFilter(tokenStream); 
  17.                  //進行大小寫過濾 
  18.                  result = new LowerCaseFilter(result); 
  19.                  //進行停用詞過濾 
  20.                  result = new StopFilter(enableStopPositionIncrements, result, stopSet); 
  21.                  return result; 
  22.        } 
  23.  

StandardAnalyzer是Lucene索引建立和檢索索引時都需要使用的分析器,tokenStream方法的作用就是對輸入流reader先進行分詞,再進行一系列的過濾。

 

標準分詞器:StandardTokenizer

 

Java代碼  收藏代碼
 
  1. public final class StandardTokenizer extends Tokenizer { 
  2.  
  3.        /**JFlex掃描器*/ 
  4.        private final StandardTokenizerImpl scanner; 
  5.       /**從輸入流字串中解析出的詞元的各種信息*/ 
  6.        private TermAttribute termAtt; //詞元的內容,如"tearcher"  "[email protected]"  "1421" 
  7.        private OffsetAttribute offsetAtt;  //詞元的首字母和尾字母在文本中的位置信息 
  8.        private PositionIncrementAttribute posIncrAtt;  //當前詞元在TokenStream中相對於前一個token的位置,用於短語搜索 
  9.        private TypeAttribute typeAtt;  //詞元所屬的類別,,如<ALPHANUM>、<EMAIL>、<NUM> 
  10.  
  11.        //標準分詞器構造器,並用JFlex對象解析輸入流 
  12.        public StandardTokenizer(Version matchVersion, Reader input) { 
  13.               super(); 
  14.               this.scanner = new StandardTokenizerImpl(input); 
  15.               init(input, matchVersion); 
  16.        } 
  17.         //初始化詞元的屬性信息 
  18.        private void init(Reader input, Version matchVersion) { 
  19.            if (matchVersion.onOrAfter(Version.LUCENE_24)) { 
  20.                   replaceInvalidAcronym = true
  21.            } else { 
  22.                  replaceInvalidAcronym = false
  23.            } 
  24.             this.input = input;     
  25.             termAtt = addAttribute(TermAttribute.class); 
  26.             offsetAtt = addAttribute(OffsetAttribute.class); 
  27.             posIncrAtt = addAttribute(PositionIncrementAttribute.class); 
  28.             typeAtt = addAttribute(TypeAttribute.class); 
  29.        } 
  30.  
  31.        //將JFlex掃描後的匹配結果按詞元的不同屬性存儲 
  32.        //比如當前詞元是I'm  則將I'm存儲到TermAttribute中,而<APOSTROPHE>則存放到TypeAttribute中。 
  33.        @Override 
  34.        public final boolean incrementToken() throws IOException { 
  35.             clearAttributes(); 
  36.             int posIncr = 1
  37.  
  38.             while(true) { 
  39.                  //通過JFlex掃描器scanner取得與規則相匹配的當前詞元,否則返回-1 
  40.                  int tokenType = scanner.getNextToken(); 
  41.  
  42.                  if (tokenType == StandardTokenizerImpl.YYEOF) { 
  43.                      return false
  44.                  } 
  45.                  //scanner.yylength() 是當前詞元的長度,maxTokenLength是詞元允許的最大長度,值爲255     
  46.                  if (scanner.yylength() <= maxTokenLength) { 
  47.                        posIncrAtt.setPositionIncrement(posIncr); 
  48.                        //將當前詞元字串儲記錄在TermAttribute屬性中,比如“I'm” 
  49.                        scanner.getText(termAtt); 
  50.                        //得到當前詞元首字母在整個文本內容中的位置 
  51.                        final int start = scanner.yychar();              
  52.                        //將當前詞元的位置信息(開始位置,結束位置)記錄在OffsetAttribute屬性中       
  53.                        offsetAtt.setOffset(correctOffset(start), correctOffset(start+termAtt.termLength())); 
  54.                        //確定當前詞元的類別信息,並記錄在TypeAttribute屬性中 
  55.                        if (tokenType == StandardTokenizerImpl.ACRONYM_DEP) { 
  56.                        if (replaceInvalidAcronym) { 
  57.                            typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[StandardTokenizerImpl.HOST]); 
  58.                            termAtt.setTermLength(termAtt.termLength() - 1);  
  59.                        } else { 
  60.                            typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[StandardTokenizerImpl.ACRONYM]); 
  61.                        } 
  62.                   } else { 
  63.                        typeAtt.setType(StandardTokenizerImpl.TOKEN_TYPES[tokenType]); 
  64.                   } 
  65.                   return true
  66.              } else 
  67.                  posIncr++; 
  68.             } 
  69.        } 
  70.  } 

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

Java代碼  收藏代碼

 

  1. public final class StandardFilter extends TokenFilter { 
  2.   
  3.    /** 
  4.     * 去除詞語末尾的“'s”   如  it's-&gt; it 
  5.     * 去除縮略語中的“.”  如U.S.A -&gt; USA 
  6.     */ 
  7.     @Override 
  8.     public final boolean incrementToken() throws java.io.IOException { 
  9.          if (!input.incrementToken()) { 
  10.              return false
  11.          } 
  12.          char[] buffer = termAtt.termBuffer(); 
  13.          final int bufferLength = termAtt.termLength(); 
  14.          final String type = typeAtt.type(); 
  15.          if (type == APOSTROPHE_TYPE &amp;&amp; bufferLength &gt;= 2 &amp;&amp; buffer[bufferLength-2] == '\'' &amp;&amp; (buffer[bufferLength-1] == 's' || buffer[bufferLength-1] == 'S')) { 
  16.        
  17.                  termAtt.setTermLength(bufferLength - 2); 
  18.           } else if (type == ACRONYM_TYPE) {     
  19.                  int upto = 0
  20.                  for(int i=0;i&lt;bufferLength;i++) { 
  21.                        char c = buffer[i]; 
  22.                        if (c != '.'
  23.                              buffer[upto++] = c; 
  24.                  } 
  25.                  termAtt.setTermLength(upto); 
  26.            } 
  27.        return true
  28.     } 

 

 

三、token的屬性結構Attribute

 

首先我們用下面的代碼來看看打印標準分詞器的運行結果

 

Java代碼  收藏代碼

 

  1. class StandardTest{ 
  2.     public static void main(String[] args) throws IOException{ 
  3.         //輸入流 
  4.         StringReader s=new StringReader(new String("I'm a student. these are apples")); 
  5.                 //標準分詞 
  6.         TokenStream tokenStream = new StandardTokenizer(Version.LUCENE_CURRENT, s); 
  7.         //標準過濾 
  8.                 tokenStream=new StandardFilter(tokenStream); 
  9.                 //大小寫過濾 
  10.         tokenStream=new LowerCaseFilter(tokenStream); 
  11.          
  12.         TermAttribute termAtt=(TermAttribute)tokenStream.getAttribute(TermAttribute.class); 
  13.         TypeAttribute typeAtt=(TypeAttribute)tokenStream.getAttribute(TypeAttribute.class); 
  14.         OffsetAttribute offsetAtt=(OffsetAttribute)tokenStream.getAttribute(OffsetAttribute.class); 
  15.         PositionIncrementAttribute  posAtt=(PositionIncrementAttribute)tokenStream.getAttribute(PositionIncrementAttribute.class); 
  16.          
  17.          
  18.         System.out.println("termAtt       typeAtt       offsetAtt       posAtt"); 
  19.         while (tokenStream.incrementToken())  {   
  20.             System.out.println(termAtt.term()+" "+typeAtt.type()+" ("+offsetAtt.startOffset()+","+offsetAtt.endOffset()+")   "+posAtt.getPositionIncrement());   
  21.         }  
  22.         } 

 

打印結果:

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,說明肯定沒有緊挨着。怎麼樣,用處很大吧。輕而易舉 的解決了短語搜索的難題哦。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章