Lucene介绍、分词详解

Lucene介绍

1、Lucene简介

最受欢迎的java开源全文搜索引擎开发工具包。提供了完整的查询引擎和索引引擎,部分文本分词引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索功能,或者是以此为基础建立起完整的全文检索引擎。
Lucene是Apache的子项目,网址:http://lucene.apache.org/

2、Lucene用途

为软件开发人员提供一个建议的工具包,以方便在目标系统中实现全文检索功能,或者是以此为基础建立的全文检索引擎。

3、Lucene适用场景
  • 在应用中为数据库中的数据提供全文检索实现。
  • 开发独立的搜索引擎服务、系统
4、Lucene的特性
  1. 稳定、索引性能高
  • 每小时能够索引150GB以上的数据
  • 对内存的要求小,只需要1MB的堆内存
  • 增量索引和批量索引一样快
  • 索引的大小约为索引文本大小的20%~30%
  1. 高效、准确、高性能的搜索算法
  • 良好的搜索排序
  • 强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等
  • 支持字段搜索(如标题、作者、内容)
  • 可根据任意字段排序
  • 支持多个索引查询结果合并
  • 支持更新操作和查询操作同时进行
  • 支持高亮、join、分组结果功能
  • 速度快
  • 可扩展排序模块,内置包含向量空间模型、BM25模型可选
  • 可配置存储引擎
  1. 跨平台
  • 纯java编写
  • 作为Apache开源许可下的开源项目,你可以在商业或开源项目中使用
  • Lucene有多种语言实现版(如C,C++、Python等),不仅仅是JAVA
5、Lucene架构
Lucene架构
6、Lucene集成
  • Lucene版本

当前最新版本7.3.0

  • 系统要求

JDK1.8及以上版本

  • 集成:将需要的Lucene模块的jar引入到你的应用中

方式一:官网下载zip,解压后拷贝jar到你的项目工程。
方式二:Maven引入依赖

7、Lucene 模块说明
  • core: Lucene core library 核心模块:分词、索引、查询
  • analyzers- * : 以analyzers开头的都为分词器
  • facet: Faceted indexing and search capabilities 提供分类索引、搜索能力
  • grouping: Collectors for grouping search results. 搜索结果分组支持
  • highlighter: Highlights search keywords in results 关键字高亮支持
  • join: Index-time and Query-time joins for normalized content 连接支持
  • queries: Filters and Queries that add to core Lucene 补充的查询、过滤方式实现
  • queryparser: Query parsers and parsing framework 查询表达式解析模块
  • spatial: Geospatial search 地理位置搜索支持
  • suggest: Auto-suggest and Spellchecking support 拼写检查、联想提示

1. 引入Lucene的核心模块

<!-- lucene 核心模块  -->
<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-core</artifactId>
  <version>7.3.0</version>
</dependency>

2. 了解核心模块的构成

核心模块的构成

分词器详解

1、Lucene - 分词器API
  • org.apache.lucene.analysl.Analyzer

分析器,Analyzer 分词器组件的核心API,它的职责:构建正真正对文本进行分词处理的TokenStream(分词处理器)。通过调用它的如下两个方法,得到输入文本的分词处理器。

//两个都为fianl方法说明方法不能扩展
public final TokenStream tokenStream(String fieldName, Reader reader)
//第一个参数:字段的名字,第二个参数:字符输入流
public final TokenStream tokenStream(String fieldName, String text)
//第一个参数:字段的名字,第二个参数:字符串

问题1:从哪里得到了TokenStream?

//查看源码可以看出它返回的是一个components.getTokenStream(); 说明TokenStream是从components下得来的
public final TokenStream tokenStream(final String fieldName,
                                       final Reader reader) {
    TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName);
    final Reader r = initReader(fieldName, reader);
    if (components == null) {
      components = createComponents(fieldName);
      reuseStrategy.setReusableComponents(this, fieldName, components);
    }
    components.setReader(r);
    return components.getTokenStream();
  }
  

问题2:方法传入的字符流Reader 给了谁?

//通过上面的源码中 
final Reader r = initReader(fieldName, reader);
components.setReader(r); 
//可以看出Reader 交给了components处理

问题3: components是什么?components的获取逻辑是怎样?

//通过上面的源码中 
TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName);
components.setReader(r); 
if (components == null) {
    components = createComponents(fieldName);
    reuseStrategy.setReusableComponents(this, fieldName, components);
}
//可以看出components 首先是从重用策略(reuseStrategy)中去获取components ,如果没有则回去创建一个components 然后将其放入到 reuseStrategy 中

lucene中有一个特性为 对内存的要求小,只需要1MB的堆内存 所以在代码中会大量使用重用策略

问题4:createComponents(fieldName) 方法是个什么方法?

//在源码中我们可以看出 createComponents 是一个抽象方法需要其子类区具体实现
protected abstract TokenStreamComponents createComponents(String fieldName);

问题5:Analyzer能直接创建对象吗?

Analyzer 是一个抽象类所以它不能是new他们自己,但可以new他们的实现类

问题6:为什么它要这样设计?

因为Analyzer只是帮你构建TokenStream,但是它并不知道你将要把它用在何种语言下。

问题7:请看一下Analyzer的实现子类有哪些?

//Analyzer本身 只有这一个标准实现类
public final class StandardAnalyzer extends StopwordAnalyzerBase

问题8:要实现一个自己的Analyzer,必须实现哪个方法?

//必须实现 Analyzer 只有这一个抽象方法
protected abstract TokenStreamComponents createComponents(String fieldName);
  • TokenStreamComponents createComponents(String fieldName)

createComponents 是Analizer中唯一的抽象方法,扩展点。通过提供该方法的实现来实现自己的Analyzer。

参数说明:fieldName,如果我们需要为不同的字段 创建 不同的分词处理器组件,则可根据这个参数来判断。否则,就用不到这个参数。

返回值:TokenStreamCoponents 分词处理器组件。

我们需要在 createComponents 方法中创建我们想要的分词处理器组件。

TonkenStreamComponents是什么?

TonkenStreamComponents 分词处理器组件:这个类中封装有供外部使用的TonkenStream分词处理器。提供了对source(源)和 sink(供外部使用分词处理器)两个属性的访问方法。

问题1:这个类的构造方法有几个?区别是什么?从中能发现什么?

//有两个构造方法,一个是传一个参数,一个是传两个参数。
//我们可以发现Tokenizer是继承了TokenStream的
public TokenStreamComponents(final Tokenizer source,
        final TokenStream result) {
      this.source = source;
      this.sink = result;
}
public TokenStreamComponents(final Tokenizer source) {
      this.source = source;
      this.sink = source;
}

问题2:source 和 sink属性分别是什么类型?这两个类型有什么关系?

Tokenizer(source)是TokenStream(sink)的的子类

问题3:在这个类中没有创建source、sink对象的代码(而是由构造方法传入)。也就是说我们在Analyzer.createCommponents方法中创建它的对象前,需要先创建什么?

需要创建source、sink对象的

问题4:在Analyzer中tokenStream() 方法中把输入流给了谁?得到的TokenStream对象是谁?TokenStream对象sink中是否必须封装有source对象?

//我们将输入流交给了 source(Tokenizer)
protected void setReader(final Reader reader) {
    source.setReader(reader);
}
//我们获取TokenStream对象为 sink(TokenStream)
public TokenStream getTokenStream() {
    return sink;
}
//所以说我们TokenStream对象sink中必须封装有source对象
  • org.apache.lucene.analysis.TokenStream

分词处理器,负责对输入文本完成分词、处理。

问题1:TokenStream中有没有对应的给入方法?

TokenStream并没有对应去获取Tokenizer的方法

问题2:TokenStream是一个抽象类,有哪些方法,它的抽象方法有哪些?它的构造方法有什么特点?


TokenStream的方法
  //一个抽象方法,这个方法主要是用于取得下一个分项的,如果有则返回true没有则返回false。每调一次返回一个分项
  public abstract boolean incrementToken() throws IOException;
 
  //构造方法三个都为 protected
  protected TokenStream() {
    super(DEFAULT_TOKEN_ATTRIBUTE_FACTORY);
    assert assertFinal();
  }
  protected TokenStream(AttributeSource input) {
    super(input);
    assert assertFinal();
  }
  protected TokenStream(AttributeFactory factory) {
    super(factory);
    assert assertFinal();
  }

问题3:TokenStream的具体子类分为哪两类?有什么区别?


TokenStream子类

概念说明:Token:分项,从字符流中分出一个一个项。
Tokenizer:表示分词器,完成分词工作。输入:字符流。
TokenFilter:表示分项过滤器。输入:TokenStream。
先分词再分项

问题4:TokenStream继承了谁?他是干什么用的?

继承于AttributeSource类。
Token Attribute:分项属性(分项的信息):如包含的词、位置等

TokenStream的两类子类

Tokenizer:分词器,输入是Reader字符流的TokenStream,完成从流中分出分项
TokenFilter:分项过滤器,它的输入是另一个TokenStream,完成对从上一个TokenStream中流出的token的特殊处理。

问题1:请查看Tokenizer类的源码及注释,这个类该如何使用?要实现自己的Tokenizer只需要做什么?

使用实现就必须要实现它父类中的 incrementToken() 抽象方法。

问题2:Tokenizer的子类有那些?

在Lucene-core核心包中只有 StandardTokenizer

Tokenizer的子类
  //StandardTokenizer 实现incrementToken方法
  @Override
  public final boolean incrementToken() throws IOException {
    clearAttributes();
    skippedPositions = 0;

    while(true) {
      int tokenType = scanner.getNextToken();

      if (tokenType == StandardTokenizerImpl.YYEOF) {
        return false;
      }

      if (scanner.yylength() <= maxTokenLength) {
        posIncrAtt.setPositionIncrement(skippedPositions+1);
        scanner.getText(termAtt);
        final int start = scanner.yychar();
        offsetAtt.setOffset(correctOffset(start), correctOffset(start+termAtt.length()));
        typeAtt.setType(StandardTokenizer.TOKEN_TYPES[tokenType]);
        return true;
      } else
        // When we skip a too-long term, we still increment the
        // position increment
        skippedPositions++;
    }
  }

问题3:请查看TokenFilter类的源码及注释,如何实现自己的TokenFilter?

使用实现就必须要实现它父类中的 incrementToken() 抽象方法。

问题4:TokenFilter的子类有哪些?


TokenFilter的子类

问题5:TokenFilter是不是一个典型的装饰器模式?

如果我们需要对分词进行各种处理,只需要按我们的处理顺序一层层包裹即可(每一层完成特定的处理)。不同的处理需要,只用不同的包裹顺序、层数。

TokenStream 继承了 AttributeSource

问题1:我们在TokenStream及它的两个子类中是否有看到关于分项信息的存储,如该分项的词是什么、这个词的位置索引?

概念说明:Attribute 属性 Token Attribute 分项属性(分项信息),如分项的词、词的索引位置等等。这些属性通过不同的Tokenizer/TokenFilter处理统计得出。

不同的Tokenizer/TokenFilter组合,就会有不同的分项信息。它是会动态变化的,你不知道有多少,是什么。那该如何实现分项信息的存储呢?

答案就是:AttributeSource、Attribute、AttributeImpl、AttributeFactory

  1. AttributeSource 负责存放Attribute对象,他提供对应的存、取方法
  2. Attribute 对象中则可以存储一个或多个属性信息
  3. AttributeFactory 则是负责创建Attribute对象的工厂,在TokenStream中默认使用了 AttributeFactory.getStaticImplementation 我们不需要提供,遵守它的规则即可。

AttributeSource使用规则说明

  1. 某个TokenStream 实现中如要储存分项属性,通过AttributeSource的两个add方法之一,往AttributeSource中加入属性对象。
<T extends Attribute> T addAttribute(Class<T> attClass)
  • 该方法要求传入你需要添加的属性的接口类(继承Attribute),返回对应的实现类实例给你。从接口到实例,这就是为什么需要AttributeFactory的原因。
void addAttributeImpl(AttributeImpl att)
  1. 加入每一个Attribute实现类在AttributeSource中只会有一个实例,分词过程中,分项是重复使用这一实例来存放分项的属性信息。重复调用add方法添加它返回已存储的实例对象。

  2. 要获取分享的某属性信息,则需要持有某属性的实例对象,通过 addAttribute 方法获得Attribute对象,在调用实例的方法来获取、设置值。

  3. 在TokenStream中,我们用自己实现的Attribute,默认的工厂。当我们调用这个add方法时,他怎么知道实现类是哪个?这里有一定规则要遵守:

    • 自定义的属性接口 MyAttribute 继承 Attribute
    • 自定义的属性实现类必须继承AttributeImpl,实现自定义的接口MyAttribute
    • 自定义的属性实现必须提供无参构造方法
    • 为了让默认工厂(AttributeFactory)能根据自定义接口找到实现类,实现类需为 接口名+Impl

TokenStream的使用步骤

我们在应用中并不直接使用分词器,只需要为索引引擎和搜索引擎创建我们想要的分词器对象。但我们在选择分词器时,会需要测试分词器的效果,就需要知道如何使用得到的分词处理器TokenStream,使用步骤

  1. 从tokenStream获得你想要获得的分项属性对象(信息是存放在属性对象中的)
  2. 调用 tokenStream的 reset() 方法,进行重置。因为tokenStream是重复利用的。
  3. 循环调用tokenStream的increamentToken(),一个一个分词,直到它返回false
  4. 再循环中去除每个分项你想要的属性值
  5. 调用tokenStream的end(),执行任务需要的结束处理
  6. 调用tokenStream的close()方法,释放占有的资源。
简单实现一个我们自己的Analyzer
实现
  • 说明:Tokenizer分词时,是从字符流中一个一个字符读取,判断是否是空白字符来进行分词。

代码github地址

思考:Tokenizer是一个 AttributeSource对象,TokenFilter 又是一个AttributeSource对象。在这两个我们自己的实现类中,我们都调用了addAttribute方法,怎么会只有一个 attribute对象? 请查看源码找到答案。

在源码中TokenFilter继承于TokenStream

public abstract class TokenFilter extends TokenStream {

并且TokenFilter构造方法为

  protected TokenFilter(TokenStream input) {
    super(input);
    this.input = input;
  }

TokenStream构造方法,继承与AttributeSource

  protected TokenStream(AttributeSource input) {
    super(input);
    assert assertFinal();
  }

AttributeSource构造方法

  public AttributeSource(AttributeSource input) {
    Objects.requireNonNull(input, "input AttributeSource must not be null");
    this.attributes = input.attributes;
    this.attributeImpls = input.attributeImpls;
    this.currentState = input.currentState;
    this.factory = input.factory;
  }

在实例代码中,贴出主要代码

//创建Tokenizer
Tokenizer source = new MyWhitespaceTokenizer();
//创建TokenStream 分项过滤器
TokenStream filter = new MyLowerCaseTokenFilter(source);

public MyLowerCaseTokenFilter(TokenStream input){
    super(input);
}

MyLowerCaseTokenFilter 继承于 TokenFilter

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1. 概念 所有传递给Lucene进行索引的文本都需要经历一个过程----分词,即:将文本分割为一个个的足够小的...
    _chubby阅读 864评论 0 1
  • 也是项目需要用的框架之一,为了不让自己轻易忘记它,在此记录一系列的lucene学习笔记(基于lucene4.4,I...
    JackFrost_fuzhu阅读 1,994评论 4 27
  • 目录结构:1.全文检索 2.Lucene入门3.Lucene进阶 全文检索 一, 生活中的搜索:1.Win...
    CoderZS阅读 1,648评论 0 12
  • 去年八月初是我第三次來阿鲁科尔沁,心情由然有了一种梦想家园的感觉。置身于这辽阔的草原,特有的草芳香沁人心肺,顿...
    庶文阅读 229评论 0 0
  • 90天的秋天过了一半。 在已平衡的天平两边 一边是黑夜,一边是白天。 秋日终是深了许多,天气渐渐薄凉,暂短呈现的秋...
    蓝桉lu阅读 241评论 0 0