Lucene简介

2018/04/01 Lucene

Lucene简介

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。 Lucene 是一个基于 Java 的全文信息检索工具包,目前主流的搜索系统Elasticsearch和solr都是基于lucene的索引和搜索能力进行。想要理解搜索系统的实现原理,就需要深入lucene这一层,看看lucene是如何存储需要检索的数据,以及如何完成高效的数据检索。

本文具体会分以下几部分:

  • 介绍lucene的数据模型,细节可以参阅lucene数据模型一文。
  • 介绍lucene中如何存储需要搜索的term。
  • 介绍lucene的倒排链的如何存储以及如何实现docid的快速查找。
  • 介绍lucene如何实现倒排链合并。
  • 介绍lucene如何做范围查询和前缀匹配。
  • 介绍lucene如何优化数值类范围查询。

全文检索

假如你想看书,说出你的要求后,有经验的图书管理员可以从他们的书库中直接给你推荐几本。例如有10篇文档,编号为0~9。其中3篇文档中包含查询词,匹配出来文档集合{0,6,9}。对文档集合按相关性排序,得到文档数组{6,0,9}。返回结果中不仅存储文档,还存储分值。

   public class ScoreDoc {
        Document doc; //文档相关的信息,包括文档编号等
        public float score; //表示这个文档和查询词有多相关
    }

查找文档最原始的方式是通过文档编号查找。就像一个人对应有一个身份证号,一个文档从创建开始就有一个文档编号。 有的专业书籍末尾有名词术语索引,方便读者定位名词术语在书中出现的位置。为了按词快速查找文档,不是采用字符串匹配的方法在文档中查询词,而是按词建立文档的索引。以词为基础建立的全文索引,也称倒排索引 倒排索引是相对于正向索引来说的,首先用正向索引来存储每个文档对应的单词列表,然后再建立倒排索引,根据单词来索引文档编号。

例如要索引如下两个文档: Doc Id 1:自己动手写搜索引擎; Doc Id 2:自己动手写网络爬虫。 首先把这两个文档中的内容分成一个个的单词: Doc Id 1:自己/动手/写/搜索引擎; Doc Id 2:自己/动手/写/网络爬虫。

倒排索引结构 自己 (1,1)(2,1) (1),(1) 动手 (1,1)(2,1) (2),(2)
写 (1,1)(2,1) (3),(3) 搜索引擎 (1,1) (4) 网络爬虫 (1,1) (4)

每个词后面的文档编号(docId)列表称为投递列表(posting list)。除了在投递列表中记录文档编号外,还可以添加词频和位置信息。词频的添加有助于结果的排序。位置信息记录了一个索引词在文档中出现的位置,可以用于包含多个查询词的短语检索;此外,也可以用于快速高亮显示查询词。

Lucene快速入门

Lucene是一个开放源代码的全文索引库,待查询的文档集合按词组织成倒排索引。Lucene中的索引库是位于一个目录下的一些二进制文件。Lucene中的索引库叫作Index。和一般的数据库不一样,Lucene不支持定义主键。在Lucene中并不存在一个叫作Index的类。通过IndexWriter来写索引,通过IndexReader读索引。索引库在物理形式上一般是位于某个路径下的一系列文件。

先介绍如何创建索引库,然后介绍如何搜索索引库。总的来说,往Lucene中放的是文档,查询的是词,查询返回的也是文档。

Lucene数据模型

Lucene中包含了四种基本数据类型,分别是:

  • Index:索引,由很多的Document组成。
  • Document:由很多的Field组成,是Index和Search的最小单位。
  • Field:由很多的Term组成,包括Field Name和Field Value。
  • Term:由很多的字节组成。一般将Text类型的Field Value分词之后的每个最小单元叫做Term。

基本概念

在深入解读Lucene之前,先了解下Lucene的几个基本概念,以及这几个概念背后隐藏的一些东西。

Index(索引)

类似数据库的表的概念,但是与传统表的概念会有很大的不同。传统关系型数据库或者NoSQL数据库的表,在创建时至少要定义表的Scheme,定义表的主键或列等,会有一些明确定义的约束。而Lucene的Index,则完全没有约束。Lucene的Index可以理解为一个文档收纳箱,你可以往内部塞入新的文档,或者从里面拿出文档,但如果你要修改里面的某个文档,则必须先拿出来修改后再塞回去。这个收纳箱可以塞入各种类型的文档,文档里的内容可以任意定义,Lucene都能对其进行索引。

Document(文档)

类似数据库内的行或者文档数据库内的文档的概念,一个Index内会包含多个Document。写入Index的Document会被分配一个唯一的ID,即Sequence Number(更多被叫做DocId),关于Sequence Number后面会再细说。

Field(字段)

一个Document会由一个或多个Field组成,Field是Lucene中数据索引的最小定义单位。Lucene提供多种不同类型的Field,例如StringField、TextField、LongFiled或NumericDocValuesField等,Lucene根据Field的类型(FieldType)来判断该数据要采用哪种类型的索引方式(Invert Index、Store Field、DocValues或N-dimensional等),关于Field和FieldType后面会再细说。

Term和Term Dictionary

Lucene中索引和搜索的最小单位,一个Field会由一个或多个Term组成,Term是由Field经过Analyzer(分词)产生。Term Dictionary即Term词典,是根据条件查找Term的基本索引。

Segment

一个Index会由一个或多个sub-index构成,sub-index被称为Segment。Lucene的Segment设计思想,与LSM类似但又有些不同,继承了LSM中数据写入的优点,但是在查询上只能提供近实时而非实时查询。 Lucene中的数据写入会先写内存的一个Buffer(类似LSM的MemTable,但是不可读),当Buffer内数据到一定量后会被flush成一个Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是Batch和Append,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除的文档的DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并,这点与LSM对SSTable的Merge类似。 Segment在被flush或commit之前,数据保存在内存中,是不可被搜索的,这也就是为什么Lucene被称为提供近实时而非实时查询的原因。读了它的代码后,发现它并不是不能实现数据写入即可查,只是实现起来比较复杂。原因是Lucene中数据搜索依赖构建的索引(例如倒排依赖Term Dictionary),Lucene中对数据索引的构建会在Segment flush时,而非实时构建,目的是为了构建最高效索引。当然它可引入另外一套索引机制,在数据实时写入时即构建,但这套索引实现会与当前Segment内索引不同,需要引入额外的写入时索引以及另外一套查询机制,有一定复杂度。

Sequence Number

Sequence Number(后面统一叫DocId)是Lucene中一个很重要的概念,数据库内通过主键来唯一标识一行,而Lucene的Index通过DocId来唯一标识一个Doc。不过有几点要特别注意: DocId实际上并不在Index内唯一,而是Segment内唯一,Lucene这么做主要是为了做写入和压缩优化。那既然在Segment内才唯一,又是怎么做到在Index级别来唯一标识一个Doc呢?方案很简单,Segment之间是有顺序的,举个简单的例子,一个Index内有两个Segment,每个Segment内分别有100个Doc,在Segment内DocId都是0-100,转换到Index级的DocId,需要将第二个Segment的DocId范围转换为100-200。 DocId在Segment内唯一,取值从0开始递增。但不代表DocId取值一定是连续的,如果有Doc被删除,那可能会存在空洞。 一个文档对应的DocId可能会发生变化,主要是发生在Segment合并时。 Lucene内最核心的倒排索引,本质上就是Term到所有包含该Term的文档的DocId列表的映射。所以Lucene内部在搜索的时候会是一个两阶段的查询,第一阶段是通过给定的Term的条件找到所有Doc的DocId列表,第二阶段是根据DocId查找Doc。Lucene提供基于Term的搜索功能,也提供基于DocId的查询功能。 DocId采用一个从0开始底层的Int32值,是一个比较大的优化,同时体现在数据压缩和查询效率上。例如数据压缩上的Delta策略、ZigZag编码,以及倒排列表上采用的SkipList等,这些优化后续会详述。

Lucene实战

Maven依赖

<dependencies>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!--一般分词器适用于英文分词-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!--中文分词器-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>7.2.0</version>
        </dependency>

        <!--对分词索引查询解析-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!--检索关键字高亮显示-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>7.2.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.janeluo/ikanalyzer -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>

    </dependencies>

Search

    微信好友

    博士的沙漏

    Table of Contents