lucene分词

比较不同分词器的分词结果:

  • CJKAnalyzer二元覆盖的方式分词
Analyzer analyzer=new CJKAnalyzer();
       TokenStream tokenStream=analyzer.tokenStream("myfiled", new StringReader("待切分文本"));
       tokenStream.reset();
       while(tokenStream.incrementToken()){
           //取得下一个分词
           System.out.println("token:"+tokenStream);
       }
       analyzer.close();

结果:

  • SmartChineseAnalyzer
Analyzer analyzer=new SmartChineseAnalyzer();
        TokenStream tokenStream=analyzer.tokenStream("myfiled", new StringReader("待切分文本"));
        tokenStream.reset();
        while(tokenStream.incrementToken()){
            //取得下一个分词
            System.out.println("token:"+tokenStream);
        }
        analyzer.close();

结果:

  • StandardAnalyzer单字切分
Analyzer analyzer=new StandardAnalyzer();
        TokenStream tokenStream=analyzer.tokenStream("myfiled", new StringReader("待切分文本"));
        tokenStream.reset();
        while(tokenStream.incrementToken()){
            //取得下一个分词
            System.out.println("token:"+tokenStream);
        }
        analyzer.close();

结果:

lucene分词_第1张图片

自己动手写Analyzer

由于6.1.0版本相比于以前有很多改动,参照[1]中p148的例子,以及结合lucene6.1.0的文档,写一个简单的分词器例子。
  文档里面说,构建一个自己的分词器是非常简单的(I doubt that!),自己构建的分词器要继承Analyzer类,并且可以用现存的analysis components——CharFilter(可选),一个Tokenizer,以及TokenFilter(可选)——或者使用自己构建的组建,或者是混合来用。

  • 一个Whitespace tokenization的例子
      参照文档里面的一个例子(例子里面还用到了Version,但是在6.1.0里面似乎都已经摒弃这个了,所以就直接将其删掉):
import java.io.IOException;
import java.io.StringReader;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;

public class MyAnalyzer extends Analyzer {
 
   public MyAnalyzer(){}
 
   @Override
   protected TokenStreamComponents createComponents(String fieldName) {
     return new TokenStreamComponents(new WhitespaceTokenizer());
   }
   
   public static void main(String[] args) throws IOException {
     // text to tokenize
     final String text = "This is a demo of the TokenStream API";
     
     MyAnalyzer analyzer = new MyAnalyzer();
     TokenStream stream 
             = analyzer.tokenStream("field", new StringReader(text));
     
     // get the CharTermAttribute from the TokenStream
     CharTermAttribute termAtt 
             = stream.addAttribute(CharTermAttribute.class);
 
     try {
       stream.reset();
     
       // print all tokens until stream is exhausted
       while (stream.incrementToken()) {
         System.out.println(termAtt.toString());
       }
     
       stream.end();
     } finally {
       stream.close();
     }
   }
}

MyAnalyzer类是Analyzer的子类,实现了createComponents方法,然后在主方法中,对stream进行循环,通过WhitesSpaceTokenizer中提供的CharTermAttirbute,打印出token中的term text
(这段话原文:
   In main() a loop consumes the stream and prints the term text of the tokens by accessing the CharTermAttribute that the WhitespaceTokenizer provides.)。
  Tokenizer的子类需要重写incrementToken方法,通过incrementToken方法遍历Tokenizer分析出的词,当还有词可以获取时,返回true;已经遍历到结尾时,返回false。
  上面应该是基于属性的方法(CharTermAttribute),将无用的词特征和想要的词特征分隔开。每个TokenStream在构造时,增加它想要的属性。在TokenStream的整个生命周期中都保留一个属性的引用。这样在获取所有和TokenStream实例相关的属性时,可以保证属性的类型安全。
  上面代码得到的结果是:


lucene分词_第2张图片
  1. 添加LengthFilter:
      如果我们需要去除长度小于等于2的tokens,我们可以通过添加LengthFilter来实现,只需要对createComponets()方法做一些改动:
 protected TokenStreamComponents createComponents(String fieldName) {
     final Tokenizer source=new WhitespaceTokenizer();
     TokenStream result=new LengthFilter(source, 3,Integer.MAX_VALUE);
     return new TokenStreamComponents(source,result);
}

结果如下:

看一下LengthFilter类的源码:

public final class LengthFilter extends FilteringTokenFilter {

private final int min;
private final int max;

private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);

public LengthFilter(TokenStream in, int min, int max) {
  super(in);
  if (min < 0) {
    throw new IllegalArgumentException("minimum length must be greater than or equal to zero");
  }
  if (min > max) {
    throw new IllegalArgumentException("maximum length must not be greater than minimum length");
  }
  this.min = min;
  this.max = max;
}

@Override
public boolean accept() {
  final int len = termAtt.length();
  return (len >= min && len <= max);
}
}

可以看到在LengthFilter类里面,CharTermAttribute被添加以及存储到termAtt实例中,因为只能存在一个CharTermAtribute的实例(in the chain,这里的chain应该是说TokenStream的生命周期中),所以例子中的addAttribute()方法引用的就是LengthFilter返回的已经存在的CharTermAttribute。
  通过查看在CharTermAttribute中的term text,去除掉过长或者过短的tokens。(CharTermAttribute就是对应Token中的词)
  
添加custom Attribute(自己定制一个Attribute)
  定义一个part-of-speech tagging(词性标注)的Attribute,名为PartOfSpeechAttribute,首先需要为这个Attribute定义接口:

import org.apache.lucene.util.Attribute;

public interface PartOfSpeechAttribute extends Attribute {
   public static enum PartOfSpeech {
     Noun, Verb, Adjective, Adverb, Pronoun, Preposition, Conjunction, Article, Unknown
   }
 
   public void setPartOfSpeech(PartOfSpeech pos);
 
   public PartOfSpeech getPartOfSpeech();
 }

然后写一个实现类,值得注意的是,在Lucene中,会默认检查一个Attribute的名字是否有后缀Impl,所以我们在这里实现类的名字为PartOfSpeechAttributeImpl。
  当然也可以实现AttributeFactory,这个工厂类接收Atrribute的接口作为参数,然后返回一个实例。

import org.apache.lucene.util.AttributeImpl;
import org.apache.lucene.util.AttributeReflector;

public final class PartOfSpeechAttributeImpl extends AttributeImpl implements PartOfSpeechAttribute{
    private PartOfSpeech pos=PartOfSpeech.Unknown;
    @Override
    public void setPartOfSpeech(PartOfSpeech pos) {
        this.pos=pos;
    }
    @Override
    public PartOfSpeech getPartOfSpeech() {
        return pos;
    }

    @Override
    public void clear() {
        pos=PartOfSpeech.Unknown;
    }

    @Override
    public void reflectWith(AttributeReflector reflector) {
    }

    @Override
    public void copyTo(AttributeImpl target) {
        ((PartOfSpeechAttribute)target).setPartOfSpeech(pos);
    }

}

上面这个类只存在一个变量,用来存储词性的token,它继承了AttributeImpl类并实现了里面的抽象方法。现在我们需要一个TokenFilter(Token过滤器),在这个例子中,我们设置一个很简单的filter:如果一个单词的首字母是大写,则标记为‘Noun’,其他标记为‘Unknown’.

import java.io.IOException;

import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;


public class PartOfSpeechTaggingFilter extends TokenFilter {
     PartOfSpeechAttribute posAtt 
          = addAttribute(PartOfSpeechAttribute.class);
     CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
 
     
     protected PartOfSpeechTaggingFilter(TokenStream input) {
       super(input);
     }
     
     public boolean incrementToken() throws IOException {
       if (!input.incrementToken()) {return false;}
       posAtt.setPartOfSpeech(
             determinePOS(termAtt.buffer(), 0, termAtt.length()));
       return true;
     }
     
     // determine the part of speech for the given term
     protected PartOfSpeechAttribute.PartOfSpeech 
               determinePOS(char[] term, int offset, int length) {
       // naive implementation that tags every uppercased word as noun
       if (length > 0 && Character.isUpperCase(term[0])) {
         return PartOfSpeechAttribute.PartOfSpeech.Noun;
       }
       return PartOfSpeechAttribute.PartOfSpeech.Unknown;
     }
   }

下面将这个filter运用到the chain in MyAnalyzer,同样是修改createComponents()方法:

   protected TokenStreamComponents createComponents(String fieldName) {
       final Tokenizer source=new WhitespaceTokenizer();
       TokenStream result=new LengthFilter(source, 3,Integer.MAX_VALUE);
       result=new PartOfSpeechTaggingFilter(result);
       return new TokenStreamComponents(source,result);
   }

得到的结果如下:
  


似乎跟之前相比没有改变。这表明了在TokenStream/Filter chain添加一个定制的attribute不会影响已经存在的consumers(TokenStream是生产者,产生Token,生成词索引程序的是消费者,调用TokenStream的increamentToken()方法得到一个Token),这是因为他们并不知道新的Attribute。现在需要让consumer来运用PartOfSpeechAttribute来打印:

  public static void main(String[] args) throws IOException {
     // text to tokenize
     final String text = "This is a demo of the TokenStream API";
     
     MyAnalyzer analyzer = new MyAnalyzer();
     TokenStream stream 
          = analyzer.tokenStream("field", new StringReader(text));
     
     // get the CharTermAttribute from the TokenStream
     CharTermAttribute termAtt 
          = stream.addAttribute(CharTermAttribute.class);
     
     //get the PartOfSpeechAttribute from TokenStream
     PartOfSpeechAttribute posAtt 
          = stream.addAttribute(PartOfSpeechAttribute.class);
     try {
       stream.reset();
     
       // print all tokens until stream is exhausted
       while (stream.incrementToken()) {
         System.out.println(termAtt.toString()+":"
                              +posAtt.getPartOfSpeech());
       }
     
       stream.end();
     } finally {
       stream.close();
     }
   }

得到的结果如下:


lucene分词_第3张图片

每个词都被标注上了PartOfSpeech的标签。
  
参考文献:
[1]罗刚. 解密搜索引擎技术实战--LUCENE & JAVA精华版(第3版)[M]. 电子工业出版社, 2016.

你可能感兴趣的:(lucene分词)