Lucene 中的Tokenizer, TokenFilter学习

释放双眼,带上耳机,听听看~!

 

lucene中的TokenStream,TokenFilter之间关系

 

TokenStream是一个能够在被调用后产生语汇单元序列的类,其中有两个类型:Tokenizer和TokenFilter,两者的不同在于TokenFilter中包含了一个TokenStream作为input,该input仍然可以为一种TokenFilter进行递归封装,是一种组合模式;而Tokenzier接受一个Reader对象读取字符并创建语汇单元,TokenFilter负责处理输入的语汇单元,通过新增、删除或者修改属性的方式来产生新的语汇单元。

 

Lucene 中的Tokenizer, TokenFilter学习

 

 

对照我们之前分析的同义词TokenizerFactory相关配置,其数据流的过程如下:

 


1
2
1java.io.Reader -> com.chenlb.mmseg4j.solr.MMSegTokenizer -> SynonymFilter -> StopFilter -> WordDelimiterFilter -> LowerCaseFilter -> RemoveDuplicatesTokenFilter
2

1
2
1  
2

 

对于某些TokenFilter来说,在分析过程中对事件的处理顺序非常重要。当指定过滤操作顺序时,还应该考虑这样的安排对于应用程序性能可能造成的影响。

 

在solr中,schema.xml(最新版本已经修改为managed-schema)的作用是告诉solr该如何对输入的文档进行索引。

 

http://www.liaozhida.net/solr/solr%E7%B3%BB%E5%88%97%E4%B8%83%E8%AF%A6%E8%A7%A3schema-xml%E7%89%B9%E6%80%A7.html

 

对于每个不同的field,需要设置其对应的数据类型,数据类型决定了solr如何去解释每个字段,以及怎样才能搜索到这个字段。在字段分析器中(field analyzers),指导solr怎样对输入的数据进行处理然后再构建出索引,类似于文本处理器或者文本消化器。

 

当一个document被索引或者检索操作的时候,分析器Analyzer会审阅字段field的文本内容,然后生成一个token流,analyzer可以由多个tokenizer和filter组成;tokenizer可以将field字段的内容切割成单个词或token,进行分词处理;filters可以接收tokenizer分词输出的token流,进行转化过滤处理,例如对词元进行转换(简繁体转换),舍弃无用词元(虚词谓词)。tokenizer和filter一起组成一个管道或者链条,对输入的文档和输入的查询文本进行处理,一系列的tokenizer和filter被称为分词器analyzer,得到的结果被存储成为索引字典用来匹配查询输入条件。

 

此外,我们还可以将索引分析器和查询分析器分开,例如下面的字段配置的意思:对于索引,先经过一个基本的分析器,然后转换为小写字母,接着过滤掉不在keepword.txt中的词,最后将剩下的词元转换为同义词;对于查询,先经过一个基本的分词器,然后转换为小写字母就可以了。

 


1
2
3
4
5
6
7
8
9
10
11
12
13
1<fieldType name="nametext" class="solr.TextField">
2  <analyzer type="index">
3    <tokenizer class="solr.StandardTokenizerFactory"/>
4    <filter class="solr.LowerCaseFilterFactory"/>
5    <filter class="solr.KeepWordFilterFactory" words="keepwords.txt"/>
6    <filter class="solr.SynonymFilterFactory" synonyms="syns.txt"/>
7  </analyzer>
8  <analyzer type="query">
9    <tokenizer class="solr.StandardTokenizerFactory"/>
10    <filter class="solr.LowerCaseFilterFactory"/>
11  </analyzer>
12</fieldType>
13

1
2
1  
2

 

在Lucene实战一书中,详解了如何从头编写一个同义词Analyzer,通过改写termAttribute以及positionIncrementAttribute的方式来达到实现同义词的方式,不过由于书上的示例比较陈旧,而charTermAttribute不能达到修改同义词元的目的(只能进行append),因此替换最终的目的没有达到。

 

 


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
1public class SynonymFilter extends TokenFilter {
2
3    private static final String TOKEN_TYPE_SYNONYM = "SYNONYM";
4
5    private Stack<String> synonymStack;
6    private SynonymEngine synonymEngine;
7    private AttributeSource.State current;
8    private final CharTermAttribute bytesTermAttribute;
9    private final PositionIncrementAttribute positionIncrementAttribute;
10
11    /**
12     * Construct a token stream filtering the given input.
13     *
14     * @param input
15     */
16    protected SynonymFilter(TokenStream input, SynonymEngine synonymEngine) {
17        super(input);
18        this.synonymEngine = synonymEngine;
19        synonymStack = new Stack<>();
20
21        this.bytesTermAttribute = addAttribute(CharTermAttribute.class);
22        this.positionIncrementAttribute = addAttribute(PositionIncrementAttribute.class);
23    }
24
25    @Override
26    public boolean incrementToken() throws IOException {
27        if (!synonymStack.isEmpty()) {
28            String syn = synonymStack.pop();
29            restoreState(current);
30
31//            bytesTermAttribute.setBytesRef(new BytesRef(syn.getBytes()));
32//            bytesTermAttribute.resizeBuffer(0);
33            bytesTermAttribute.append(syn);
34
35            positionIncrementAttribute.setPositionIncrement(0);
36            return true;
37        }
38
39        if (!input.incrementToken()) {
40            return false;
41        }
42
43        if (addAliasesToStack()) {
44            current = captureState();
45        }
46
47        return true;
48    }
49
50    private boolean addAliasesToStack() throws IOException {
51        String[] synonyms = synonymEngine.getSynonyms(bytesTermAttribute.toString());
52        if (synonyms == null) {
53            return false;
54        }
55        for (String synonym : synonyms) {
56            synonymStack.push(synonym);
57        }
58        return true;
59    }
60}
61
62

1
2
1  
2

 

Analyzer,用于将tokenizer和filter串联起来:

 


1
2
3
4
5
6
7
8
9
1public class SynonymAnalyzer extends Analyzer {
2    @Override
3    protected TokenStreamComponents createComponents(String fieldName) {
4        StandardTokenizer source = new StandardTokenizer();
5        return new TokenStreamComponents(source, new SynonymFilter(new StopFilter(new LowerCaseFilter(source),
6                new CharArraySet(StopAnalyzer.ENGLISH_STOP_WORDS_SET, true)), new TestSynonymEngine()));
7    }
8}
9

1
2
1  
2

 

我们定义一个简易的同义词匹配引擎:

 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1public interface SynonymEngine {
2    String[] getSynonyms(String s) throws IOException;
3}
4
5public class TestSynonymEngine implements SynonymEngine {
6
7    public static final Map<String, String[]> map = new HashMap<>();
8
9    static {
10        map.put("quick", new String[]{"fast", "speedy"});
11    }
12
13    @Override
14    public String[] getSynonyms(String s) throws IOException {
15        return map.get(s);
16    }
17}
18
19

1
2
1  
2

对最终结果进行测试:

 

   


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1public static void main(String[] args) throws IOException {
2        SynonymAnalyzer analyzer = new SynonymAnalyzer();
3        TokenStream tokenStream = analyzer.tokenStream("contents", new StringReader("The quick brown fox"));
4        tokenStream.reset();
5
6        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
7        OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
8        PositionIncrementAttribute positionIncrementAttribute =
9                tokenStream.addAttribute(PositionIncrementAttribute.class);
10        TypeAttribute typeAttribute = tokenStream.addAttribute(TypeAttribute.class);
11
12        int position = 0;
13        while (tokenStream.incrementToken()) {
14            int positionIncrement = positionIncrementAttribute.getPositionIncrement();
15            if (positionIncrement > 0) {
16                position += positionIncrement;
17                System.out.println();
18                System.out.print(position + " : ");
19            }
20
21            System.out.printf("[%s : %d ->  %d : %s]", charTermAttribute.toString(), offsetAttribute.startOffset(), offsetAttribute.endOffset(),
22                    typeAttribute.type());
23        }
24

1
2
1  
2

 

测试出的结果,可以看出位置1的谓词the已经被剔除,位置2处加入了较多的同义词,由于使用的append,所以同义词记在了一起。

 


1
2
3
4
12 : [quick : 4 ->  9 : <ALPHANUM>][quickspeedy : 4 ->  9 : <ALPHANUM>][quickfast : 4 ->  9 : <ALPHANUM>]
23 : [brown : 10 ->  15 : <ALPHANUM>]
34 : [fox : 16 ->  19 : <ALPHANUM>]
4

1
2
1  
2

 

 

 

Solr同义词设置

 

Solr中的同义词使用的是 SynonymFilterFactory 来进行加载的,我们需要在定义schema时,对某个字段设置同义词时,可以使用:

 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1<fieldtype name="textComplex" class="solr.TextField" positionIncrementGap="100">
2        <analyzer type="index">
3            <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="/Users/mazhiqiang/develop/tools/solr-5.5.0/server/solr/product/conf/dic" />
4            <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt"/>
5            <filter class="solr.WordDelimiterFilterFactory"/>
6            <filter class="solr.LowerCaseFilterFactory"/>
7            <filter class="solr.NGramFilterFactory" minGramSize="1" maxGramSize="20"/>
8            <filter class="solr.StandardFilterFactory"/>
9        </analyzer>
10        <analyzer type="query">
11            <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="/Users/mazhiqiang/develop/tools/solr-5.5.0/server/solr/product/conf/dic" />
12            <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
13            <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt"/>
14            <filter class="solr.WordDelimiterFilterFactory"/>
15            <filter class="solr.LowerCaseFilterFactory"/>
16            <!--  <filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="20"/> -->
17            <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
18        </analyzer>
19    </fieldtype>
20

1
2
1  
2

 

需要配置对应的 synonyms 属性,指定 定义同义词的配置文件,设置是否忽略大小写等属性。

 

而在加载同义词时,对文件进行逐行读取(使用LineNumberReader),对于每一行的数据,先使用 => 作为分隔符,同义词在左右两边(左边作为input,右边作为output)都可以配置成多个,以逗号分隔,最后以笛卡尔积的形式将其放至map中。

 


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
1String line = null;
2    while ((line = in.readLine()) != null) {
3      if (line.length() == 0 || line.charAt(0) == '#') {
4        continue; // ignore empty lines and comments
5      }
6
7      // TODO: we could process this more efficiently.
8      String sides[] = split(line, "=>");
9      if (sides.length > 1) { // explicit mapping
10        if (sides.length != 2) {
11          throw new IllegalArgumentException("more than one explicit mapping specified on the same line");
12        }
13        String inputStrings[] = split(sides[0], ",");
14        CharsRef[] inputs = new CharsRef[inputStrings.length];
15        for (int i = 0; i < inputs.length; i++) {
16          inputs[i] = analyze(unescape(inputStrings[i]).trim(), new CharsRefBuilder());
17        }
18
19        String outputStrings[] = split(sides[1], ",");
20        CharsRef[] outputs = new CharsRef[outputStrings.length];
21        for (int i = 0; i < outputs.length; i++) {
22          outputs[i] = analyze(unescape(outputStrings[i]).trim(), new CharsRefBuilder());
23        }
24        // these mappings are explicit and never preserve original
25        for (int i = 0; i < inputs.length; i++) {
26          for (int j = 0; j < outputs.length; j++) {
27            add(inputs[i], outputs[j], false);
28          }
29        }
30

1
2
1  
2

 

所有的同义词加载完成后,会生成一个SynonymMap,该map就被用来在全文检索的过程中进行同义词替换。

 

在我们对某个单词进行查询时,可以查询到我们设置的字段query分析器结构,生成一个TokenizerChain对象,对应的Tokenizer为我们设置的分词器,filters为我们设置的过滤器链条,会根据过滤器链条Chain进行

 

Lucene 中的Tokenizer, TokenFilter学习

 

通过input的方式设置同义词Filter,组成该链条结果。

 


1
2
3
4
5
6
7
8
9
10
1@Override
2  protected TokenStreamComponents createComponents(String fieldName) {
3    Tokenizer tk = tokenizer.create();
4    TokenStream ts = tk;
5    for (TokenFilterFactory filter : filters) {
6      ts = filter.create(ts);
7    }
8    return new TokenStreamComponents(tk, ts);
9  }
10

1
2
1  
2

 

而具体到每个FilterFactory,例如SynonymFilterFactory,都通过create方法来创建对应的Filter用于同义词过滤。

 


1
2
3
4
5
6
7
1@Override
2  public TokenStream create(TokenStream input) {
3    // if the fst is null, it means there's actually no synonyms... just return the original stream
4    // as there is nothing to do here.
5    return map.fst == null ? input : new SynonymFilter(input, map, ignoreCase);
6  }
7

1
2
1  
2

 

创建一个SynonymFilter来进行最后真正的筛选,将同义词进行替换,整体的类结构图如下:

 

Lucene 中的Tokenizer, TokenFilter学习

lucene内置的Token

 

lucene中除了内置的几个Tokenizer,在solr中的field analyzer以及index中也得到了应用,下面就对这几种filter进行测试,我们分析的文本为:Please email clark.ma@gmail.com by 09, re:aa-bb

 

 

StandardAnalyzer 1 : [please : 0 ->  6 : <ALPHANUM>] 2 : [email : 7 ->  12 : <ALPHANUM>] 3 : [clark.ma : 13 ->  21 : <ALPHANUM>] 4 : [gmail.com : 22 ->  31 : <ALPHANUM>] 6 : [09 : 35 ->  37 : <NUM>] 7 : [re:aa : 39 ->  44 : <ALPHANUM>] 8 : [bb : 45 ->  47 : <ALPHANUM>] 去除空格,标点符号,@;  
ClassicAnalyzer 1 : [please : 0 ->  6 : <ALPHANUM>] 2 : [email : 7 ->  12 : <ALPHANUM>] 3 : [clark.ma@gmail.com : 13 ->  31 : <EMAIL>] 5 : [09 : 35 ->  37 : <ALPHANUM>] 6 : [re : 39 ->  41 : <ALPHANUM>] 7 : [aa : 42 ->  44 : <ALPHANUM>] 8 : [bb : 45 ->  47 : <ALPHANUM>] 能够识别互联网域名和email地址,
LetterTokenizer 1 : [Please : 0 ->  6 : word] 2 : [email : 7 ->  12 : word] 3 : [clark : 13 ->  18 : word] 4 : [ma : 19 ->  21 : word] 5 : [gmail : 22 ->  27 : word] 6 : [com : 28 ->  31 : word] 7 : [by : 32 ->  34 : word] 8 : [re : 39 ->  41 : word] 9 : [aa : 42 ->  44 : word] 10 : [bb : 45 ->  47 : word] 丢弃掉所有的非文本字符
KeywordTokenizer 1 : [Please email clark.ma@gmail.com by 09, re:aa-bb : 0 ->  47 : word]   将整个文本当做一个词元
LowerCaseTokenizer 1 : [please : 0 ->  6 : word] 2 : [email : 7 ->  12 : word] 3 : [clark : 13 ->  18 : word] 4 : [ma : 19 ->  21 : word] 5 : [gmail : 22 ->  27 : word] 6 : [com : 28 ->  31 : word] 7 : [by : 32 ->  34 : word] 8 : [re : 39 ->  41 : word] 9 : [aa : 42 ->  44 : word] 10 : [bb : 45 ->  47 : word] 对其所有非文本字符,过滤空格,标点符号,将所有的大写转换为小写
NGramTokenizer 可以定义最小minGramSize(default=1), 最大切割值maxGramSize(default=2),生成的词元较多。 假设minGramSize=2, maxGramSize=3,输入abcde,输出:ab abc abc bc bcd cd cde 读取字段并在给定范围内生成多个token
PathHierachyTokenizer c:\my document\filea\fileB,new PathHierarchyTokenizer('\', '/') 1 : [c: : 0 ->  2 : word][c:/my document : 0 ->  14 : word][c:/my document/filea : 0 ->  20 : word][c:/my document/filea/fileB : 0 ->  26 : word] 使用新的文件目录符去代替文本中的目录符
PatternTokenizer 需要两个参数,pattern正则表达式,group分组。 pattern=”[A-Z][A-Za-z]*” group=”0″ 输入: “Hello. My name is Inigo Montoya. You killed my father. Prepare to die.” 输出: “Hello”, “My”, “Inigo”, “Montoya”, “You”, “Prepare” 进行正则表达式分组匹配
UAX29URLEmailTokenizer 1 : [Please : 0 ->  6 : <ALPHANUM>] 2 : [email : 7 ->  12 : <ALPHANUM>] 3 : [clark.ma@gmail.com : 13 ->  31 : <EMAIL>] 4 : [by : 32 ->  34 : <ALPHANUM>] 5 : [09 : 35 ->  37 : <NUM>] 6 : [re:aa : 39 ->  44 : <ALPHANUM>] 7 : [bb : 45 ->  47 : <ALPHANUM>] 去除空格和标点符号,但保留url和email连接

1
1

 

 

Lucene内置的TokenFilter

 

过滤器能够组成一个链表,每一个过滤器处理上一个过滤器处理过后的词元,所以过滤器的排序很有意义,第一个过滤器最好能处理大部分常规情况,最后一个过滤器是带有针对特殊性的。

 

 

ClassicFilter “I.B.M. cat’s can’t” ==> “I.B.M”, “cat”, “can’t” 经典过滤器,可以过滤无意义的标点,需要搭配ClassicTokenizer使用
ApostropheFilter 1 : [abc : 0 ->  3 : <ALPHANUM>] 2 : [I.B.M : 4 ->  9 : <ALPHANUM>] 3 : [cat : 10 ->  15 : <ALPHANUM>] 4 : [can : 16 ->  21 : <ALPHANUM>] 省略所有的上撇号
LowerCaseFilter 1 : [i.b.m : 0 ->  5 : <ALPHANUM>] 2 : [cat's : 6 ->  11 : <ALPHANUM>] 3 : [can't : 12 ->  17 : <ALPHANUM>] 转换成小写
TypeTokenFilter <filter class=”solr.TypeTokenFilterFactory” types=”email_type.txt” useWhitelist=”true”/> 如果email_type.txt设置为ALPHANUM,会保留该类型的所有分析结果,否则会被删除掉 给定一个文件并设置成白名单还是黑名单,只有符合条件的type才能被保留
TrimFilter   去掉空格
TruncateTokenFilter 1 : [I.B : 0 ->  5 : <ALPHANUM>] 2 : [cat : 6 ->  11 : <ALPHANUM>] 3 : [can : 12 ->  17 : <ALPHANUM>] 截取文本长度,左边为prefixLength=3
PatternCaptureGroupFilter 可配置属性pattern和preserve_original(是否保留原文) 从输入文本中保留能够匹配正则表达式的
PatternReplaceFilter    
StopFilter   创建一个自定义的停词词库列表,过滤器遇到停词就直接过滤掉
KeepWordFilter 与StopFilter的含义正好相反  
LengthFilter 设置一个最小值min和最大值max 为词元的长度设置在一个固定范围
WordDelimiterFilter A:-符号 wi-fi 变成wi fi B:驼峰写法 LoveSong 变成 love song 对应参数 C:字母-数字 xiaomi100 变成 xiaomi 100 D:–符号 like–me 变成 like me E:尾部的’s符号 mother’s 变成 mother F:-符号 wi-fi 变成 wifi 于规则A不同的是没有分成两个词元 G:-符号,数字之间 400-884586 变成 400884586 H:-符号 无论字母还是数字,都取消-符号 wi-fi-4 变成wifi4   其他参数 splitOnCaseChange=”1″ 默认1,关闭设为0 规则B generateWordParts=”1″ 默认1 ,对应规则AB generateNumberParts=”1″ 默认1 对应规则F catenateWords=”1″ 默认0 对应规则A splitOnNumerics=”1″ 默认1,关闭设0 规则C stemEnglishPossessive 默认1,关闭设0 规则E catenateNumbers=”1″ 默认0 对应规则G catenateAll=”1″ 默认0 对应规则 H preserveOriginal=”1″ 默认0 对词元不做任何修改 除非有其他参数改变了词元 protected=”protwords.txt” 指定这个单词列表的单词不被修改 通过分隔符分割单元

1
1

 

 

 

 

 

 

 

 

 

 

  • 大小: 18.6 KB

  • 大小: 34.1 KB

  • 大小: 107.4 KB

  • 查看图片附件

给TA打赏
共{{data.count}}人
人已打赏
安全运维

OpenSSH-8.7p1离线升级修复安全漏洞

2021-10-23 10:13:25

安全运维

设计模式的设计原则

2021-12-12 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索