Lucene系列(4)——Analyzer原理及代码分析

注:本文基于Lucene 8.2.0 版本。

前面的文章中多次提到了分析器Analyzer,它就像一个数据加工厂,输入是原始的文本数据,输出是经过各种工序加工的term,然后这些terms以倒排索引的方式存储起来,形成最终用于搜索的Index。所以Analyzer也是我们控制数据能以哪些方式检索的重要点,本文就带你来了解一下Analyzer背后的奥秘。

内置的Analyzer对比

Lucene已经帮我们内置了许多Analyzer,我们先来挑几个常见的对比一下他们的分析效果吧。看下面代码(源文件见 AnalyzerCompare.java ):

package com.niyanchun;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.KeywordAnalyzer;
import org.apache.lucene.analysis.core.SimpleAnalyzer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Compare Lucene Internal Analyzers.
 *
 * @author NiYanchun
 **/
public class AnalyzerCompare {

    private static final Analyzer[] ANALYZERS = new Analyzer[]{
            new WhitespaceAnalyzer(),
            new KeywordAnalyzer(),
            new SimpleAnalyzer(),
            // 标准分词器会处理停用词,但默认其停用词库为空,这里我们使用英文的停用词
            new StandardAnalyzer(EnglishAnalyzer.getDefaultStopSet())
    };

    public static void main(String[] args) throws Exception {
        String content = "My name is Ni Yanchun, I'm 28 years old. You can contact me with the email niyanchun@outlook.com";
        System.out.println("原始数据:\n" + content + "\n\n分析结果:");
        for (Analyzer analyzer : ANALYZERS) {
            showTerms(analyzer, content);
        }
    }

    private static void showTerms(Analyzer analyzer, String content) throws IOException {

        try (TokenStream tokenStream = analyzer.tokenStream("content", content)) {
            StringBuilder sb = new StringBuilder();
            AtomicInteger tokenNum = new AtomicInteger();
            tokenStream.reset();
            while (tokenStream.incrementToken()) {
                tokenStream.reflectWith(((attClass, key, value) -> {
                    if ("term".equals(key)) {
                        tokenNum.getAndIncrement();
                        sb.append("\"").append(value).append("\", ");
                    }
                }));
            }
            tokenStream.end();

            System.out.println(analyzer.getClass().getSimpleName() + ":\n"
                    + tokenNum + " tokens: ["
                    + sb.toString().substring(0, sb.toString().length() - 2) + "]");
        }
    }
}

这段代码的功能是使用常见的四种分词器( WhitespaceAnalyzer,KeywordAnalyzer,SimpleAnalyzer,StandardAnalyzer )对“ My name is Ni Yanchun, I'm 28 years old. You can contact me with the email niyanchun@outlook.com ”这句话进行analyze,输出最终的terms。其中需要注意的是,标准分词器会去掉停用词(stop word),但其内置的停用词库为空,所以我们传了一个英文默认的停用词库。

运行代码之后的输出如下:

原始数据:
My name is Ni Yanchun, I'm 28 years old. You can contact me with the email niyanchun@outlook.com

分析结果:
WhitespaceAnalyzer:
17 tokens: ["My", "name", "is", "Ni", "Yanchun,", "I'm", "28", "years", "old.", "You", "can", "contact", "me", "with", "the", "email", "niyanchun@outlook.com"]
KeywordAnalyzer:
1 tokens: ["My name is Ni Yanchun, I'm 28 years old. You can contact me with the email niyanchun@outlook.com"]
SimpleAnalyzer:
19 tokens: ["my", "name", "is", "ni", "yanchun", "i", "m", "years", "old", "you", "can", "contact", "me", "with", "the", "email", "niyanchun", "outlook", "com"]
StandardAnalyzer:
15 tokens: ["my", "name", "ni", "yanchun", "i'm", "28", "years", "old", "you", "can", "contact", "me", "email", "niyanchun", "outlook.com"]

最后附上上面四个停用词的功能,方便大家理解上述结果:

  • WhitespaceAnalyzer :仅根据空白字符(whitespace)进行分词。
  • KeywordAnalyzer :不做任何分词,把整个原始输入作为一个token。所以可以看到输出只有1个token,就是原始句子。
  • SimpleAnalyzer :根据非字母(non-letters)分词,并且将token全部转换为小写。所以该分词的输出的terms都是由小写字母组成的。
  • StandardAnalyzer :基于JFlex进行语法分词,然后删除停用词,并且将token全部转换为小写。

Analyzer原理

前面我们说了Analyzer就像一个加工厂,包含很多道工序。这些工序在Lucene里面分为两大类: Tokenizer TokenFilter

Tokenizer永远是Analyzer的第一道工序,有且只有一个。它的作用是读取输入的原始文本,然后根据工序的内部定义,将其转化为一个个token输出。

TokenFilter只能接在Tokenizer之后,因为它的输入只能是token。然后它将输入的token进行加工,输出加工之后的token。一个Analyzer中,TokenFilter可以没有,也可以有多个。

也就是说一个Analyzer内部的流水线是这样的:

比如 StandardAnalyzer 的流水线是这样的:

所以,Analyzer的原理还是比较简单的,Tokenizer读入文本转化为token,然后后续的TokenFilter将token按需加工,输出需要的token。我们可以自由组合已有的Tokenizer和TokenFilter来满足自己的需求,也可以实现自己的Tokenizer和TokenFilter。

Analyzer源码分析

Analyzer和TokenStream

Analyzer对应的实现类是 org.apache.lucene.analysis.Analyzer ,这是一个抽象类。它的主要作用是构建一个 org.apache.lucene.analysis.TokenStream 对象,该对象用于分析文本。代码中的类描述是这样的:

An Analyzer builds TokenStreams, which analyze text. It thus represents a policy for extracting index terms from text.

因为它是一个抽象类,所以实际使用的时候需要继承它,实现具体的类。比如第一部分我们使用的4个内置Analyzer都是直接或间接继承的该类。继承的子类需要实现 createComponents 方法,之前说的一系列工序就是加在这个方法里的,可以认为一道工序就是整个流水线中的一个Component。Analyzer抽象类还实现了一个 tokenStream 方法,并且是final的。该方法会将一系列工序转化为 TokenStream 对象输出。比如 SimpleAnalyzer 的实现如下:

public final class SimpleAnalyzer extends Analyzer {

  /**
   * Creates a new {@link SimpleAnalyzer}
   */
  public SimpleAnalyzer() {
  }
  
  @Override
  protected TokenStreamComponents createComponents(final String fieldName) {
    Tokenizer tokenizer = new LetterTokenizer();
    return new TokenStreamComponents(tokenizer, new LowerCaseFilter(tokenizer));
  }

  @Override
  protected TokenStream normalize(String fieldName, TokenStream in) {
    return new LowerCaseFilter(in);
  }
}

TokenStream的作用就是流式的产生token。这些token可能来自于indexing时文档里面的字段数据,也可能来自于检索时的检索语句。其实就是之前说的indexing和查询的时候都会调用Analyzer。TokenStream类文档是这样描述的:

A TokenStream enumerates the sequence of tokens, either from Fields of a Document or from query text.

Tokenizer和TokenFilter

TokenStream有两个非常重要的抽象子类: org.apache.lucene.analysis.Tokenizerorg.apache.lucene.analysis.TokenFilter 。这两个类的实质其实都是一样的,都是对Token进行处理。不同之处就是前面介绍的,Tokenizer是第一道工序,所以它的输入是原始文本,输出是token;而TokenFilter是后面的工序,它的输入是token,输出也是token。实质都是对token的处理,所以实现它两个的子类都需要实现 incrementToken 方法,也就是在这个方法里面实现处理token的具体逻辑。 incrementToken 方法是在 TokenStream 类中定义的。比如前面提到的 StandardTokenizer 就是实现 Tokenizer 的一个具体子类; LowerCaseFilterStopFilter 就是实现 TokenFilter 的具体子类。

最后要说一下,Analyzer的流程越长,处理逻辑越复杂,性能就越差,实际使用中需要注意权衡。Analyzer的原理及代码就分析到这里,因为篇幅,一些源码没有在文章中全部列出,如果你有兴趣,建议去看下常见的Analyzer的实现的源码,一定会有收获。下篇文章我们分析Analyzer处理之后的token和term的细节。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章