lucene4.7 分词器(三) 之自定义分词器

时间:2022-09-18 03:13:16

一些特殊的分词需求,在此做个总结。本来的Lucene的内置的分词器,差不多可以完成我们的大部分分词工作了,如果是英文文章那么可以使用StandardAnalyzer标准分词器,WhitespaceAnalyzer空格分词器,对于中文我们则可以选择IK分词器,Messeg4j,庖丁等分词器。 

我们先来看看下面的几个需求 

编号 需求分析
1 按单个字符进行分词无论是数字,字母还是特殊符号
2 按特定的字符进行分词,类似String中spilt()方法
3 按照某个字符或字符串进行分词

仔细分析下上面的需求,会觉得上面的需求很没意思,但是在特定的场合下确实是存在这样的需求的,看起来上面的需求很简单,但是lucene里面内置的分析器却没有一个支持这种变态的"无聊的"分词需求,如果想要满足上面的需求,可能就需要我们自己定制自己的分词器了。 


先来看第一个需求,单个字符切分,这就要不管你是the一个单词还是一个电话号码还是一段话还是其他各种特殊符号都要保留下来,进行单字切分,这种特细粒度的分词,有两种需求情况,可能适应这两种场景 
(-)100%的实现数据库模糊匹配 
(=)对于某个电商网站笔记本的型号Y490,要求用户无论输入Y还是4,9,0都可以找到这款笔记本 


这种单字切分确实可以实现数据库的百分百模糊检索,但是同时也带来了一些问题,如果这个域中是存电话号码,或者身份证之类的与数字的相关的信息,那么这种分词法,会造成这个域的倒排链表非常之长,反映到搜索上,就会出现中文检索很快,而数字的检索确实非常之慢的问题。原因是因为数字只有0-9个字符,而汉字则远远比这个数量要大的多,所以在选用这种分词时,还是要慎重的考虑下自己的业务场景到底适不适合这种分词,否则就会可能出一些问题。 

再来分析下2和3的需求,这种需求可能存在这么一种情况,就是某个字段里存的内容是按照逗号或者空格,#号,或者是自己定义的一个字符串进行分割存储的,而这种时候我们可能就会想到一些非常简单的做法,直接调用String类的spilt方法进行打散,确实,这种方式是可行的,但是lucene里面的结构某些情况下,就可能不适合用字符串拆分的方法,而是要求我们必须定义一个自己的分词器来完成这种功能,因为涉及到一些参数需要传一个分词器或者索引和检索时都要使用分词器来构造解析,所以有时候就必须得自己定义个专门处理这种情况的分词器了。 

好了,散仙不在唠叨了,下面开始给出代码,首先针对第一个需求,单字切分,其实这个需求没什么难的,只要熟悉lucene的Tokenizer就可以轻松解决,我们改写ChineseTokenizer来满足我们的需求.
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package  com.piaoxuexianjing.cn;
 
import  java.io.IOException;
import  java.io.Reader;
 
import  org.apache.lucene.analysis.Tokenizer;
import  org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import  org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import  org.apache.lucene.util.AttributeSource.AttributeFactory;
 
public  class  China  extends  Tokenizer {
     
      public  China(Reader in) {
           super (in);
         }
 
         public  China(AttributeFactory factory, Reader in) {
           super (factory, in);
         }
            
         private  int  offset =  0 , bufferIndex= 0 , dataLen= 0 ;
         private  final  static  int  MAX_WORD_LEN =  255 ;
         private  final  static  int  IO_BUFFER_SIZE =  1024 ;
         private  final  char [] buffer =  new  char [MAX_WORD_LEN];
         private  final  char [] ioBuffer =  new  char [IO_BUFFER_SIZE];
 
 
         private  int  length;
         private  int  start;
 
         private  final  CharTermAttribute termAtt = addAttribute(CharTermAttribute. class );
         private  final  OffsetAttribute offsetAtt = addAttribute(OffsetAttribute. class );
         
         private  final  void  push( char  c) {
 
             if  (length ==  0 ) start = offset- 1 ;             // start of token
             buffer[length++] = Character.toLowerCase(c);   // buffer it
 
         }
 
         private  final  boolean  flush() {
 
             if  (length> 0 ) {
                 //System.out.println(new String(buffer, 0,
                 //length));
               termAtt.copyBuffer(buffer,  0 , length);
               offsetAtt.setOffset(correctOffset(start), correctOffset(start+length));
               return  true ;
             }
             else
                 return  false ;
         }
 
         @Override
         public  boolean  incrementToken()  throws  IOException {
             clearAttributes();
 
             length =  0 ;
             start = offset;
 
 
             while  ( true ) {
 
                 final  char  c;
                 offset++;
 
                 if  (bufferIndex >= dataLen) {
                     dataLen = input.read(ioBuffer);
                     bufferIndex =  0 ;
                 }
 
                 if  (dataLen == - 1 ) {
                   offset--;
                   return  flush();
                 else
                     c = ioBuffer[bufferIndex++];
 
 
                 switch (Character.getType(c)) {
 
                 case  Character.DECIMAL_DIGIT_NUMBER: //注意此部分不过滤一些熟悉或者字母
                 case  Character.LOWERCASE_LETTER: //注意此部分
                 case  Character.UPPERCASE_LETTER: //注意此部分
//                    push(c);
//                    if (length == MAX_WORD_LEN) return flush();
//                    break;
              
                 case  Character.OTHER_LETTER:
                     if  (length> 0 ) {
                         bufferIndex--;
                         offset--;
                         return  flush();
                     }
                     push(c);
                     return  flush();
 
                 default :
                     if  (length> 0 return  flush();
                      
                         break ;
                     
                 }
             }
         }
         
         @Override
         public  final  void  end() {
           // set final offset
           final  int  finalOffset = correctOffset(offset);
           this .offsetAtt.setOffset(finalOffset, finalOffset);
         }
 
         @Override
         public  void  reset()  throws  IOException {
           super .reset();
           offset = bufferIndex = dataLen =  0 ;
         }
 
}

然后定义个自己的分词器

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package  com.piaoxuexianjing.cn;
 
import  java.io.Reader;
 
import  org.apache.lucene.analysis.Analyzer;
import  org.apache.lucene.analysis.Tokenizer;
 
/**
  * @author 三劫散仙
  * 单字切分
 
  * **/
public  class  MyChineseAnalyzer  extends  Analyzer {
 
     @Override
     protected  TokenStreamComponents createComponents(String arg0, Reader arg1) {
        
         Tokenizer token= new  China(arg1);
         
         return  new  TokenStreamComponents(token);
     }
     
     
     
     
 
}

下面我们来看单字切词效果,对于字符串 
String text="天气不错132abc@#$+-)(*&^.,/";
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1
3
2
a
b
c
@
#
$
+
-
)
(
*
&
^
.
,
/

对于第二种需求我们要模仿空格分词器的的原理,代码如下 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package  com.splitanalyzer;
 
import  java.io.Reader;
 
import  org.apache.lucene.analysis.util.CharTokenizer;
import  org.apache.lucene.util.Version;
 
/***
  *
  *@author 三劫散仙
  *拆分char Tokenizer
 
  * */
public  class  SpiltTokenizer  extends  CharTokenizer {
  
        char  c;
     public  SpiltTokenizer(Version matchVersion, Reader input, char  c) {
         super (matchVersion, input);
         // TODO Auto-generated constructor stub
         this .c=c;
     }
 
     @Override
     protected  boolean  isTokenChar( int  arg0) {
         return  arg0==c? false : true  ;
     }
     
     
     
 
}

然后在定义自己的分词器 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package  com.splitanalyzer;
 
import  java.io.Reader;
 
import  org.apache.lucene.analysis.Analyzer;
import  org.apache.lucene.util.Version;
 
/**
  * @author 三劫散仙
  * 自定义单个char字符分词器
  * **/
public  class  SplitAnalyzer  extends  Analyzer{
     char  c; //按特定符号进行拆分
     
     public  SplitAnalyzer( char  c) {
         this .c=c;
     }
 
     @Override
     protected  TokenStreamComponents createComponents(String arg0, Reader arg1) {
         // TODO Auto-generated method stub
         return   new  TokenStreamComponents( new  SpiltTokenizer(Version.LUCENE_43, arg1,c));
     }
     
 
}

下面看一些测试效果 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package  com.splitanalyzer;
 
import  java.io.StringReader;
 
import  org.apache.lucene.analysis.TokenStream;
import  org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 
/**
  * 测试的demo
 
  * **/
public  class  Test {
     
     public  static  void  main(String[] args) throws  Exception {
          SplitAnalyzer analyzer= new  SplitAnalyzer( '#' );
              //SplitAnalyzer analyzer=new SplitAnalyzer('+');
         //PatternAnalyzer analyzer=new PatternAnalyzer("abc");
         TokenStream ts=    analyzer.tokenStream( "field" new  StringReader( "我#你#他" ));
        // TokenStream ts=    analyzer.tokenStream("field", new StringReader("我+你+他"));
         CharTermAttribute term=ts.addAttribute(CharTermAttribute. class );
         ts.reset();
         while (ts.incrementToken()){
             System.out.println(term.toString());
         }
         ts.end();
         ts.close();
          
     }
 
}

到这里,可能一些朋友已经看不下去了,代码太多太臃肿了,有没有一种通用的办法,解决此类问题,散仙的回答是肯定的,如果某些朋友,连看到这部分的耐心都没有的话,那么,不好意思,你只能看到比较低级的解决办法了,当然能看到这部分的道友们,散仙带着大家来看一下比较通用解决办法,这个原理其实是基于正则表达式的,所以由此看来,正则表达式在处理文本字符串上面有其独特的优势。下面我们要做的就是改写自己的正则解析器,代码非常精简,功能却是很强大的,上面的3个需求都可以解决,只需要传入不用的参数即可。 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package  com.splitanalyzer;
 
import  java.io.Reader;
import  java.util.regex.Pattern;
 
import  org.apache.lucene.analysis.Analyzer;
import  org.apache.lucene.analysis.pattern.PatternTokenizer;
 
/**
  * @author 三劫散仙
  * 自定义分词器
  * 针对单字切
  * 单个符号切分
  * 多个符号组合切分
 
  * **/
public  class  PatternAnalyzer   extends  Analyzer {
     
     String regex; //使用的正则拆分式
     public  PatternAnalyzer(String regex) {
          this .regex=regex;
     }
 
     @Override
     protected  TokenStreamComponents createComponents(String arg0, Reader arg1) {
         return  new  TokenStreamComponents( new  PatternTokenizer(arg1, Pattern.compile(regex),- 1 ));
     }
}

我们来看下运行效果: 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package  com.splitanalyzer;
 
import  java.io.StringReader;
 
import  org.apache.lucene.analysis.TokenStream;
import  org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 
/**
  * 测试的demo
 
  * **/
public  class  Test {
     
     public  static  void  main(String[] args) throws  Exception {
        //  SplitAnalyzer analyzer=new SplitAnalyzer('#');
          PatternAnalyzer analyzer= new  PatternAnalyzer( "" );
          //空字符串代表单字切分  
         TokenStream ts=    analyzer.tokenStream( "field" new  StringReader( "我#你#他" ));
         CharTermAttribute term=ts.addAttribute(CharTermAttribute. class );
         ts.reset();
         while (ts.incrementToken()){
             System.out.println(term.toString());
         }
         ts.end();
         ts.close();
          
     }
 
}

输出效果: 

?
1
2
3
4
5
#
#

传入#号参数 

?
1
PatternAnalyzer analyzer= new  PatternAnalyzer( "#" );

输出效果: 

?
1
2
3

传入任意长度的字符串参数

?
1
2
  PatternAnalyzer analyzer= new  PatternAnalyzer( "分割" );
TokenStream ts=    analyzer.tokenStream( "field" new  StringReader( "我分割你分割他分割" ));

输出效果:

?
1
2
3
http://my.oschina.net/MrMichael/blog/220781