【Lucene3.6.2入门系列】第05节_自定义停用词分词器和同义词分词器

x33g5p2x  于2021-12-24 转载在 其他  
字(10.7k)|赞(0)|评价(0)|浏览(447)

完整版见https://jadyer.github.io/2013/08/18/lucene-custom-analyzer/

首先是用于显示分词信息的HelloCustomAnalyzer.java

  1. package com.jadyer.lucene;
  2. import java.io.IOException;
  3. import java.io.StringReader;
  4. import org.apache.lucene.analysis.Analyzer;
  5. import org.apache.lucene.analysis.TokenStream;
  6. import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
  7. import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
  8. import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
  9. import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
  10. /**
  11. * 【Lucene3.6.2入门系列】第05节_自定义分词器
  12. * @see -----------------------------------------------------------------------------------------------------------------------
  13. * @see Lucene3.5推荐的四大分词器:SimpleAnalyzer,StopAnalyzer,WhitespaceAnalyzer,StandardAnalyzer
  14. * @see 这四大分词器有一个共同的抽象父类,此类有个方法public final TokenStream tokenStream(),即分词的一个流
  15. * @see 假设有这样的文本"how are you thank you",实际它是以一个java.io.Reader传进分词器中
  16. * @see Lucene分词器处理完毕后,会把整个分词转换为TokenStream,这个TokenStream中就保存所有的分词信息
  17. * @see TokenStream有两个实现类,分别为Tokenizer和TokenFilter
  18. * @see Tokenizer---->用于将一组数据划分为独立的语汇单元(即一个一个的单词)
  19. * @see TokenFilter-->过滤语汇单元
  20. * @see -----------------------------------------------------------------------------------------------------------------------
  21. * @see 分词流程
  22. * @see 1)将一组数据流java.io.Reader交给Tokenizer,由其将数据转换为一个个的语汇单元
  23. * @see 2)通过大量的TokenFilter对已经分好词的数据进行过滤操作,最后产生TokenStream
  24. * @see 3)通过TokenStream完成索引的存储
  25. * @see -----------------------------------------------------------------------------------------------------------------------
  26. * @see Tokenizer的一些子类
  27. * @see KeywordTokenizer-----不分词,传什么就索引什么
  28. * @see StandardTokenizer----标准分词,它有一些较智能的分词操作,诸如将'jadyer@yeah.net'中的'yeah.net'当作一个分词流
  29. * @see CharTokenizer--------针对字符进行控制的,它还有两个子类WhitespaceTokenizer和LetterTokenizer
  30. * @see WhitespaceTokenizer--使用空格进行分词,诸如将'Thank you,I am jadyer'会被分为4个词
  31. * @see LetterTokenizer------基于文本单词的分词,它会根据标点符号来分词,诸如将'Thank you,I am jadyer'会被分为5个词
  32. * @see LowerCaseTokenizer---它是LetterTokenizer的子类,它会将数据转为小写并分词
  33. * @see -----------------------------------------------------------------------------------------------------------------------
  34. * @see TokenFilter的一些子类
  35. * @see StopFilter--------它会停用一些语汇单元
  36. * @see LowerCaseFilter---将数据转换为小写
  37. * @see StandardFilter----对标准输出流做一些控制
  38. * @see PorterStemFilter--还原一些数据,比如将coming还原为come,将countries还原为country
  39. * @see -----------------------------------------------------------------------------------------------------------------------
  40. * @see eg:'how are you thank you'会被分词为'how','are','you','thank','you'合计5个语汇单元
  41. * @see 那么应该保存什么东西,才能使以后在需要还原数据时保证正确的还原呢???其实主要保存三个东西,如下所示
  42. * @see CharTermAttribute(Lucene3.5以前叫TermAttribute),OffsetAttribute,PositionIncrementAttribute
  43. * @see 1)CharTermAttribute-----------保存相应的词汇,这里保存的就是'how','are','you','thank','you'
  44. * @see 2)OffsetAttribute-------------保存各词汇之间的偏移量(大致理解为顺序),比如'how'的首尾字母偏移量为0和3,'are'为4和7,'thank'为12和17
  45. * @see 3)PositionIncrementAttribute--保存词与词之间的位置增量,比如'how'和'are'增量为1,'are'和'you'之间的也是1,'you'和'thank'的也是1
  46. * @see 但假设'are'是停用词(StopFilter的效果),那么'how'和'you'之间的位置增量就变成了2
  47. * @see 当我们查找某一个元素时,Lucene会先通过位置增量来取这个元素,但如果两个词的位置增量相同,会发生什么情况呢
  48. * @see 假设还有一个单词'this',它的位置增量和'how'是相同的,那么当我们在界面中搜索'this'时
  49. * @see 也会搜到'how are you thank you',这样就可以有效的做同义词了,目前非常流行的一个叫做WordNet的东西,就可以做同义词的搜索
  50. * @see -----------------------------------------------------------------------------------------------------------------------
  51. * @create Aug 4, 2013 5:48:25 PM
  52. * @author 玄玉<http://blog.csdn.net/jadyer>
  53. */
  54. public class HelloCustomAnalyzer {
  55. /**
  56. * 查看分词信息
  57. * @see TokenStream还有两个属性,分别为FlagsAttribute和PayloadAttribute,都是开发时用的
  58. * @see FlagsAttribute----标注位属性
  59. * @see PayloadAttribute--做负载的属性,用来检测是否已超过负载,超过则可以决定是否停止搜索等等
  60. * @param txt 待分词的字符串
  61. * @param analyzer 所使用的分词器
  62. * @param displayAll 是否显示所有的分词信息
  63. */
  64. public static void displayTokenInfo(String txt, Analyzer analyzer, boolean displayAll){
  65. //第一个参数没有任何意义,可以随便传一个值,它只是为了显示分词
  66. //这里就是使用指定的分词器将'txt'分词,分词后会产生一个TokenStream(可将分词后的每个单词理解为一个Token)
  67. TokenStream stream = analyzer.tokenStream("此参数无意义", new StringReader(txt));
  68. //用于查看每一个语汇单元的信息,即分词的每一个元素
  69. //这里创建的属性会被添加到TokenStream流中,并随着TokenStream而增加(此属性就是用来装载每个Token的,即分词后的每个单词)
  70. //当调用TokenStream.incrementToken()时,就会指向到这个单词流中的第一个单词,即此属性代表的就是分词后的第一个单词
  71. //可以形象的理解成一只碗,用来盛放TokenStream中每个单词的碗,每调用一次incrementToken()后,这个碗就会盛放流中的下一个单词
  72. CharTermAttribute cta = stream.addAttribute(CharTermAttribute.class);
  73. //用于查看位置增量(指的是语汇单元之间的距离,可理解为元素与元素之间的空格,即间隔的单元数)
  74. PositionIncrementAttribute pia = stream.addAttribute(PositionIncrementAttribute.class);
  75. //用于查看每个语汇单元的偏移量
  76. OffsetAttribute oa = stream.addAttribute(OffsetAttribute.class);
  77. //用于查看使用的分词器的类型信息
  78. TypeAttribute ta = stream.addAttribute(TypeAttribute.class);
  79. try {
  80. if(displayAll){
  81. //等价于while(stream.incrementToken())
  82. for(; stream.incrementToken() ;){
  83. System.out.println(ta.type() + " " + pia.getPositionIncrement() + " ["+oa.startOffset()+"-"+oa.endOffset()+"] ["+cta+"]");
  84. }
  85. }else{
  86. System.out.println();
  87. while(stream.incrementToken()){
  88. System.out.print("[" + cta + "]");
  89. }
  90. }
  91. } catch (IOException e) {
  92. e.printStackTrace();
  93. }
  94. }
  95. }

下面是自定义的停用词分词器MyStopAnalyzer.java

  1. package com.jadyer.analysis;
  2. import java.io.Reader;
  3. import java.util.Set;
  4. import org.apache.lucene.analysis.Analyzer;
  5. import org.apache.lucene.analysis.LetterTokenizer;
  6. import org.apache.lucene.analysis.LowerCaseFilter;
  7. import org.apache.lucene.analysis.StopAnalyzer;
  8. import org.apache.lucene.analysis.StopFilter;
  9. import org.apache.lucene.analysis.TokenStream;
  10. import org.apache.lucene.util.Version;
  11. /**
  12. * 自定义的停用词分词器
  13. * @see 它主要用来过滤指定的字符串(忽略大小写)
  14. * @create Aug 5, 2013 1:55:15 PM
  15. * @author 玄玉<http://blog.csdn.net/jadyer>
  16. */
  17. public class MyStopAnalyzer extends Analyzer {
  18. private Set<Object> stopWords; //存放停用的分词信息
  19. /**
  20. * 自定义的用于过滤指定字符串的分词器
  21. * @param _stopWords 用于指定所要过滤的字符串(忽略大小写)
  22. */
  23. public MyStopAnalyzer(String[] _stopWords){
  24. //会自动将字符串数组转换为Set
  25. stopWords = StopFilter.makeStopSet(Version.LUCENE_36, _stopWords, true);
  26. //将原有的停用词加入到现在的停用词中
  27. stopWords.addAll(StopAnalyzer.ENGLISH_STOP_WORDS_SET);
  28. }
  29. @Override
  30. public TokenStream tokenStream(String fieldName, Reader reader) {
  31. //为这个分词器设定过滤器链和Tokenizer
  32. return new StopFilter(Version.LUCENE_36,
  33. //这里就可以存放很多的TokenFilter
  34. new LowerCaseFilter(Version.LUCENE_36, new LetterTokenizer(Version.LUCENE_36, reader)),
  35. stopWords);
  36. }
  37. }

下面是自定义的同义词分词器MySynonymAnalyzer.java

  1. package com.jadyer.analysis;
  2. import java.io.IOException;
  3. import java.io.Reader;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. import java.util.Stack;
  7. import org.apache.lucene.analysis.Analyzer;
  8. import org.apache.lucene.analysis.TokenFilter;
  9. import org.apache.lucene.analysis.TokenStream;
  10. import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
  11. import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
  12. import org.apache.lucene.util.AttributeSource;
  13. import com.chenlb.mmseg4j.ComplexSeg;
  14. import com.chenlb.mmseg4j.Dictionary;
  15. import com.chenlb.mmseg4j.analysis.MMSegTokenizer;
  16. /**
  17. * 自定义的同义词分词器
  18. * @create Aug 5, 2013 5:11:46 PM
  19. * @author 玄玉<http://blog.csdn.net/jadyer>
  20. */
  21. public class MySynonymAnalyzer extends Analyzer {
  22. @Override
  23. public TokenStream tokenStream(String fieldName, Reader reader) {
  24. //借助MMSeg4j实现自定义分词器,写法参考MMSegAnalyzer类的tokenStream()方法
  25. //但为了过滤并处理分词后的各个语汇单元,以达到同义词分词器的功能,故自定义一个TokenFilter
  26. //实际执行流程就是字符串的Reader首先进入MMSegTokenizer,由其进行分词,分词完毕后进入自定义的MySynonymTokenFilter
  27. //然后在MySynonymTokenFilter中添加同义词
  28. return new MySynonymTokenFilter(new MMSegTokenizer(new ComplexSeg(Dictionary.getInstance()), reader));
  29. }
  30. }
  31. /**
  32. * 自定义的TokenFilter
  33. * @create Aug 5, 2013 5:11:58 PM
  34. * @author 玄玉<http://blog.csdn.net/jadyer>
  35. */
  36. class MySynonymTokenFilter extends TokenFilter {
  37. private CharTermAttribute cta; //用于获取TokenStream中的语汇单元
  38. private PositionIncrementAttribute pia; //用于获取TokenStream中的位置增量
  39. private AttributeSource.State tokenState; //用于保存语汇单元的状态
  40. private Stack<String> synonymStack; //用于保存同义词
  41. protected MySynonymTokenFilter(TokenStream input) {
  42. super(input);
  43. this.cta = this.addAttribute(CharTermAttribute.class);
  44. this.pia = this.addAttribute(PositionIncrementAttribute.class);
  45. this.synonymStack = new Stack<String>();
  46. }
  47. /**
  48. * 判断是否存在同义词
  49. */
  50. private boolean isHaveSynonym(String name){
  51. //先定义同义词的词典
  52. Map<String, String[]> synonymMap = new HashMap<String, String[]>();
  53. synonymMap.put("我", new String[]{"咱", "俺"});
  54. synonymMap.put("中国", new String[]{"兲朝", "大陆"});
  55. if(synonymMap.containsKey(name)){
  56. for(String str : synonymMap.get(name)){
  57. this.synonymStack.push(str);
  58. }
  59. return true;
  60. }
  61. return false;
  62. }
  63. @Override
  64. public boolean incrementToken() throws IOException {
  65. while(this.synonymStack.size() > 0){
  66. restoreState(this.tokenState); //将状态还原为上一个元素的状态
  67. cta.setEmpty();
  68. cta.append(this.synonymStack.pop()); //获取并追加同义词
  69. pia.setPositionIncrement(0); //设置位置增量为0
  70. return true;
  71. }
  72. if(input.incrementToken()){
  73. //注意:当发现当前元素存在同义词之后,不能立即追加同义词,即不能在目标元素上直接处理
  74. if(this.isHaveSynonym(cta.toString())){
  75. this.tokenState = captureState(); //存在同义词时,则捕获并保存当前状态
  76. }
  77. return true;
  78. }else {
  79. return false; //只要TokenStream中没有元素,就返回false
  80. }
  81. }
  82. }

最后是JUnit4.x编写的小测试

  1. package com.jadyer.test;
  2. import org.apache.lucene.analysis.StopAnalyzer;
  3. import org.apache.lucene.analysis.standard.StandardAnalyzer;
  4. import org.apache.lucene.document.Document;
  5. import org.apache.lucene.document.Field;
  6. import org.apache.lucene.index.IndexReader;
  7. import org.apache.lucene.index.IndexWriter;
  8. import org.apache.lucene.index.IndexWriterConfig;
  9. import org.apache.lucene.index.Term;
  10. import org.apache.lucene.search.IndexSearcher;
  11. import org.apache.lucene.search.ScoreDoc;
  12. import org.apache.lucene.search.TermQuery;
  13. import org.apache.lucene.search.TopDocs;
  14. import org.apache.lucene.store.Directory;
  15. import org.apache.lucene.store.RAMDirectory;
  16. import org.apache.lucene.util.Version;
  17. import org.junit.Test;
  18. import com.jadyer.analysis.MyStopAnalyzer;
  19. import com.jadyer.analysis.MySynonymAnalyzer;
  20. import com.jadyer.lucene.HelloCustomAnalyzer;
  21. public class HelloCustomAnalyzerTest {
  22. /**
  23. * 测试自定义的用于过滤指定字符串(忽略大小写)的停用词分词器
  24. */
  25. @Test
  26. public void stopAnalyzer(){
  27. String txt = "This is my house, I`m come from Haerbin,My email is jadyer@yeah.net, My QQ is 517751422";
  28. HelloCustomAnalyzer.displayTokenInfo(txt, new StandardAnalyzer(Version.LUCENE_36), false);
  29. HelloCustomAnalyzer.displayTokenInfo(txt, new StopAnalyzer(Version.LUCENE_36), false);
  30. HelloCustomAnalyzer.displayTokenInfo(txt, new MyStopAnalyzer(new String[]{"I", "EMAIL", "you"}), false);
  31. }
  32. /**
  33. * 测试自定义的同义词分词器
  34. */
  35. @Test
  36. public void synonymAnalyzer(){
  37. String txt = "我来自中国黑龙江省哈尔滨市巴彦县兴隆镇";
  38. IndexWriter writer = null;
  39. IndexSearcher searcher = null;
  40. Directory directory = new RAMDirectory();
  41. try {
  42. writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new MySynonymAnalyzer()));
  43. Document doc = new Document();
  44. doc.add(new Field("content", txt, Field.Store.YES, Field.Index.ANALYZED));
  45. writer.addDocument(doc);
  46. writer.close(); //搜索前要确保IndexWriter已关闭,否则会报告异常org.apache.lucene.index.IndexNotFoundException: no segments* file found
  47. searcher = new IndexSearcher(IndexReader.open(directory));
  48. TopDocs tds = searcher.search(new TermQuery(new Term("content", "咱")), 10);
  49. for(ScoreDoc sd : tds.scoreDocs){
  50. System.out.println(searcher.doc(sd.doc).get("content"));
  51. }
  52. searcher.close();
  53. } catch (Exception e) {
  54. e.printStackTrace();
  55. }
  56. HelloCustomAnalyzer.displayTokenInfo(txt, new MySynonymAnalyzer(), true);
  57. }
  58. }

相关文章