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

2021-11-25 From NYC's Blog By 倪彦春
注:本文基于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里面分为两大类:TokenizerTokenFilter

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

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

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

Analyzer Pipeline

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

StandardAnalyzer Pipeline

所以,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类文档是这样描述的:

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

Tokenizer和TokenFilter

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

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

本文来源:NYC's Blog,转载请注明出处!

来源地址:https://niyanchun.com/lucene-learning-4.html

君子曰:学不可以已。
《软件需求(第3版)》

作为经典的软件需求工程畅销书,经由需求社区两大知名领袖结对全面修订和更新,覆盖新的主题、实例和指南,全方位讨论软件项目所涉及的所有需求开发和管理活动,介绍当下的所有实践。书中描述实用性强的、高效的、经过实际检验的端到端需求工程管理技术,通过丰富的实例来演示如何利用实践来减少订单变更,提高客户满意度,减少开发成本。

发表感想

© 2016 - 2024 chengxuzhixin.com All Rights Reserved.

浙ICP备2021034854号-1    浙公网安备 33011002016107号