【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.       //......  
  10. }  
 

要实现一种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.      }  
  17. }  
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.   }  
  15. }  

分词器和过滤器都是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.   
  24. }  

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"  "xy12@yahoo.com"  "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-> it 
  5.     * 去除缩略语中的“.”  如U.S.A -> 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 && bufferLength >= 2 && buffer[bufferLength-2] == '\'' && (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<bufferLength;i++) {  
  21.                        char c = buffer[i];  
  22.                        if (c != '.')  
  23.                              buffer[upto++] = c;  
  24.                  }  
  25.                  termAtt.setTermLength(upto);  
  26.            }  
  27.        return true;  
  28.     }  
  29. }  

 

 

三、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.         }  
  23. }  

 

打印结果:

termAtttypeAttoffsetAttposAtt
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,说明肯定没有紧挨着。怎么样,用处很大吧。轻而易举的解决了短语搜索的难题哦。

 

其实还有两种:PayloadAttribute和FlagsAttribute。我暂时还不知道他们的具体作用

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值