注:本文基于Lucene 8.2.0 版本。
前面的文章中多次提到了分析器Analyzer,它就像一个数据加工厂,输入是原始的文本数据,输出是经过各种工序加工的term,然后这些terms以倒排索引的方式存储起来,形成最终用于搜索的Index。所以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"]
最后附上上面四个停用词的功能,方便大家理解上述结果:
前面我们说了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对应的实现类是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.
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
作为经典的软件需求工程畅销书,经由需求社区两大知名领袖结对全面修订和更新,覆盖新的主题、实例和指南,全方位讨论软件项目所涉及的所有需求开发和管理活动,介绍当下的所有实践。书中描述实用性强的、高效的、经过实际检验的端到端需求工程管理技术,通过丰富的实例来演示如何利用实践来减少订单变更,提高客户满意度,减少开发成本。
最新内容
© 2016 - 2024 chengxuzhixin.com All Rights Reserved.