Lucene的語彙分析與其擴充方式

Lucene的語彙分析與其擴充方式

Lucene這一個開放原碼的全文搜尋引擎,在軟體架構上的設計挺不錯的。尤其在整個全文搜尋程序中相關的各個環節,幾乎都可以獨立的拆解出來,由程式員自行再加以擴充或者是改變。最近我們遇到一個例子,正好可以說明它的這個彈性。

使用過Lucene的程式員應當都知道,在Lucene中,和語彙分析相關的幾個主要類別,以及它們各自的角色有:

  • org.apache.lucene.analysis.Token:語彙分析後的最小單位,代表一個不能再予以分割的字詞。

  • org.apache.lucene.analysis.TokenStreamToken類別的串流,能夠以遞代(iterative)的方式,讓你將一個一個Token從中取出。

  • org.apache.lucene.analysis.Tokenizer:繼承自TokenStream,能夠將給定的文字,劃分為一連串有順序性的Token,並且以TokenStream的型式提供外界讀取。

  • org.apache.lucene.analysis.TokenFilter:繼承自TokenStream。其建構子通常也是TokenStream,其責任在於對建構時所傳入的TokenStream進行「過濾」的動作,過濾後的結果則提供另一TokenStream做為外界讀取之用。

  • org.apache.lucene.analysis.Analyzer:依據輸入文字建構TokenStream。此類別是語彙分析相關類別中,用戶端程式員必須直接面對的類別。在大多數情況下,用戶端程式員並不需要接觸到其餘類別與其衍生類別。

Analyzer是整個語彙分析相關類別中最主要的類別。一般來說,Lucene內建提供了許多不同的Analyzer的衍生類別,分別具有不同的語彙分析策略,例如org.apache.lucene.analysis.WhitespaceAnalyzer,便是使用空白字元來劃分語彙。而多數情況下,org.apache.lucene.analysis.standard.StandardAnalyzer便足堪應付。

每個Analyzer其實內部都會運用到一個Tokenizer,並且依需要採用一個或多個的TokenFilter來對語彙串流(TokenStream)進行過濾。所以Analyzer對外呈現的,是它內部Tokenizer和所有TokenFilter的綜效,而它同時也隱藏了TokenizerTokenFilter的存在。而這意謂著Analyzer是整個語彙分析子系統的代表。

這樣的設計起碼可以看到兩個好處:(1)由於Analyzer是組裝各個獨立的TokenizerTokenFilter來呈現單一的語彙分析策略,因此,當多個Analyzer有部份效果是類似,它們就可以共用相同的TokenizerTokenFilter。例如都需要將文字一律轉換成為小寫的Analyzer,都可以使用相同的LowerCaseFilter。這一段轉換為小寫的程式碼,就產生了複用(reuse)的效果。(2)由Analyzer來代表整個語彙分析子系統,抽換掉Analyzer,就可以立即改變語彙分析的運作行為。毋需拖泥帶水,為了更動語彙分析行為,還得在用戶端程式中多處修改。

在其設計中,還可以觀察到的是,它採用了Decorator這個設計模式。你可以發現,TokenFilter本身不僅繼承自TokenStream,而且其建構子的參數,仍然也是TokenStream。這是很典型的Decorator。同樣的設計,你也可以在java.io.FilterInputStream身上看到,FilterInputStream繼承自InputStream,而其建構子的唯一參數仍然也是InputStream。基本上,Decorator是模仿父類別的行為(所以它有著和父類別一致的介面),但真正的行為是作用在一個和其位於相同族系(同樣也是其父類別的衍生類別)的物件(由建構子傳入)之上。

Decorator很適合動態的組裝你想要的複合行為,TokenFilter則利用這個特性。讓我們先來看看org.apache.lucene.analysis.standard.StandardAnalyzertokenStream()方法:

 

    TokenStream result = new StandardTokenizer(reader);

    result = new StandardFilter(result);

    result = new LowerCaseFilter(result);

    result = new StopFilter(result, stopSet);

 

 

tokenStream()方法是每個Analyzer都會實作的方法,用以對外界提供語彙串流。不同的Analyzer最大的差別,幾乎都在此處,這個方法通常顯露出Analyzer究竟是組裝那些TokenizerTokenFilter。所以我們可以看到StandardAnalyzer採用的是StandardTokenizer,以及StandardFilterLowerCaseFilterStopFilterTokenFilter。你可以注意到,當建出StandardTokenizer後,程式就得到了一個最原始的TokenStream,接著便將此TokenStream傳入第一個TokenFilter。你可以想像這是一個接水管的過程,一開始我們有一小段水管,水會從一端流至另一端,接著我們在這小段水管上接上了一個會染色的另一小段水管,當水自其出口流出時,顏色就會被改變了。接著,程式依樣畫葫蘆的再接上兩段水管,所以我們最後可以得到這四段水管的綜合效果。這裡你就可以看到Decorator的影子,每個TokenFilter都是個TokenStream也接受TokenStream物件做為建構子的輸入(也就是來源TokemStream),並且會對此輸入物件進行行為的改變。

自訂語彙分析行為,有三處可為之。首先是自訂Analyzer,這是最簡單的層次,你幾乎只要在tokenStream()中選擇欲組裝的TokenizerTokenFilter就行了。再來便是自訂TokenFilter,你可以在自己實作的TokenFilter中選擇丟棄來源TokenStream中所讀出的Token,也可以對Token內容進行修改。難度最高的便是自訂Tokenizer,在這裡頭你得自己制定劃分語彙的方式。

最近我們所遇上的應用是這樣子的,當我們的文字中含有網址時,例如: www.yahoo.com.tw,我們希望輸入yahoo也能找出來。然而,當我們使用的是StandardAnalyzer,其採用的StandardTokenizerJavaCC所產生出來的,而JavaCC會依據其StandardTokenizer.jj檔所規範的語法來產生StandardTokenizer的原始碼。在StandardTokenizer.jj檔有這麼一條語法:

 

| <HOST: <ALPHANUM> ("." <ALPHANUM>)+ >

 

 

所以www.yahoo.com.tw就會符合這條語法,而被視為是一個單一的語彙(如果你並不了解JavaCC,就只要了解這個結論就好了)。一旦它被視為是一個單一的語彙時,在建索引時,整個www.yahoo.com.tw就會是一個完整的索引項目,日後用yahoo進行搜尋,自然也就搜尋不到了。

為了解這個問題,我可以有兩種解法。首先,我可以小改StandardTokenizer.jj檔成為另一個自訂的.jj檔,藉此產生出另一個Tokenizer。或者,我也可以自己撰寫一個TokenFilter,針對Token一一去判斷,如果是HOST的型式,那麼就自行再加以細分。

我自己偏好後者,因為寫好這個TokenFilter後,還可以提供給日後其它不同Analyzer來使用,如此一來才能收綜合搭配的效果。

所以我們寫了如下的TokenFilter

 

import java.io.IOException;

import java.util.Vector;

 

 

import org.apache.lucene.analysis.Token;

import org.apache.lucene.analysis.TokenFilter;

import org.apache.lucene.analysis.TokenStream;

 

 

public class URLPlusFilter extends TokenFilter

{

         private Vector vTokenBuffer = new Vector();

         //

         public Token next() throws IOException

         {

                 if( vTokenBuffer.size() > 0 )

                          return (Token) vTokenBuffer.remove(0);

                 Token token = input.next();

                 if( token == null )

                          return null;

                 String text = token.termText();

                 int p = 0;

                 int q = text.indexOf('.');

                 if( q < 0 )

                          return token;

                 while( p < (text.length()-1) )

                 {

                          if( q < 0 )

                          {

                                   vTokenBuffer.add(new Token(text.substring(p), token.startOffset()+p, token.startOffset()+text.length()));

                                   break;

                          }

                          else

                                   vTokenBuffer.add(new Token(text.substring(p, q), token.startOffset()+p, token.startOffset()+q));

                          p = q+1;

                          q = text.indexOf('.', p+1);

                 }

                 return next();

         }

         public URLPlusFilter(TokenStream input)

         {

                 super(input);

         }

}

 

 

這個TokenFilter最重要的責任,便在next()這個方法之內。整個TokenFilter的寫法是這樣子的,先利用父類別的建構子來將傳入的來源TokemStream記住(也就是TokenFilter.input這個欄位)。而在next()當中,基本上就直接回傳input.next()。但是有一個例外,就是當input.next()所回傳的Token物件,其文字內容含有「.」時,就對此Token做再進一步的拆解。將一個Token依據「.」拆解成多個Token後,接著將拆解後的多個Token,儲存在一個buffer裡頭。所以當next()被呼叫時,會先檢查buffer中是否有未傳回的Token,如果有的話,就直接回傳,反之,才利用input.next()自來源TokemStream取得。透過這個方法,當來源TokemStream回傳的Token含有「.」時,便會被拆解成多個Token,並且被適當的回傳。

最後,就只要再寫一個Analyzer來選擇搭配TokenizerFilter就可以了。

 

import java.io.Reader;

import java.util.Set;

 

 

import org.apache.lucene.analysis.TokenStream;

import org.apache.lucene.analysis.Analyzer;

import org.apache.lucene.analysis.LowerCaseFilter;

import org.apache.lucene.analysis.StopAnalyzer;

import org.apache.lucene.analysis.StopFilter;

import org.apache.lucene.analysis.standard.StandardFilter;

import org.apache.lucene.analysis.standard.StandardTokenizer;

 

 

public class URLPlusAnalyzer extends Analyzer

{

         public static final String[] STOP_WORDS;

         public static final String[] STOP_WORDS1 = StopAnalyzer.ENGLISH_STOP_WORDS;

         public static final String[] STOP_WORDS2 = {"http", "www", "com", "net"};

         //

         private Set stopSet;

         //

         static

         {

                 STOP_WORDS = new String[STOP_WORDS1.length+STOP_WORDS2.length];

                 for(int i=0;i<STOP_WORDS1.LENGTH;I++)

                          STOP_WORDS[i] = STOP_WORDS1[i];

                 for(int i=0;i<STOP_WORDS2.LENGTH;I++)

                          STOP_WORDS[i+STOP_WORDS1.length] = STOP_WORDS2[i];

         }

         public URLPlusAnalyzer()

         {

                 this(STOP_WORDS);

         }

         public URLPlusAnalyzer(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 URLPlusFilter(result);

                 result = new StopFilter(result, stopSet);

                 return result;

         }

}

 

 

這個URLPlusAnalyzer是仿StandardAnalyzer而得。但有兩個地方不同,一個是額外再定義了一組stop words。所謂的stop words是餵給StopFilter做為輸入之用,代表的是不希望被拿來做為索引的語彙。例如,像網址中的「www」、「com」、「net」都太常出現,而且不具太多的實質意義,就應該被加到stop words中。另一個不同的地方便是tokenStream()中選擇組裝的TokenFilter多了一個適才完成的URLPlusFilter。這代表所有分析出來的Token都會經過URLPlusFilter的檢查,如果是網址的型式,便會被再細分出來。

這要必須要注意,URLPlusFilter必須在StopFilter起作用前先行發揮功效,這個順序性是必要的。倘若把URLPlusFilter擺在StopFilter的後頭,那麼由於在StopFilter起作用時,整個網址是被視為一個單一的語彙,所以並不會符合stop words中的comnet等字詞,因此也不會被過濾掉。因此,必須將URLPlusFilter擺在前頭,先讓URLPlusFilter把這些字詞劃分出來後,才能由StopFilter將它們過濾掉。

一如本文所展示的,Lucene允許你透過多種途徑來改變其語彙的行為。藉由修改或擴充原有Lucene類別的方式,只需添加少許額外的程式碼,就可以進行這樣的改變,這顯示出其原始架構的彈性與可擴充性。我們透過這樣子的一個範例,希望明白的不只是Lucene的語彙分析運作行為,也不只是在Lucene中如何對語彙分析行為進行擴充,更重要的是,如何在觀察其架構的設計之後,學習或模仿如何設計此種具彈性與擴充性的軟體架構。

qing 發表於 September 29, 2004 10:17 PM

迴響

趕快來試一下

Posted by: 祐任 發表於 October 5, 2004 07:26 AM

phentermine pharmacy phentermine info incest cartoon free incest story naked gay men gay male sex russian teen teen pictures adipex review canadian pharmacies selling adipex cialis reviews cialis forum levitra side effects best price levitra online life insurance quotes insurance jobs adipex lose weight transsexual pictures shemale links celebrity nudes porn video clips pile of shit scat thumbs nfl tickets travel directions matures mature sex stories stockings pictures nylon stocking poker chips casino rama pee girls pee lover justin timberlake pics elijah wood pictures anime nude hardcore anime dating uk teen dating advice spyware blaster free virus scanner bondage stories bondage movies beastiality video free dog sex incest rape bondage rape viagra canada buying viagra free sex cartoons cartoon nude finance refinance britney spears sex britney spears sexy fed ex tramadol tramadol no prescription hydrocodone withdrawal hydrocodone information

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值