lucene搜索原理

lucene思维导图

Posted by YuanLe on March 14, 2019

来源:lucene思维导图,让搜索引擎不再难懂

Table of Contents

简介

今天,我们来讲讲 lucene,同学们搬好板凳做好啦。

question

(lucene 干嘛的呀?)

首先我们来看看这个思维导图:

全文搜索引擎

以上是我们 java 常用的全文搜索引擎框架,很多项目的搜索功能都是基于以上4个框架完成的。

所以 lucene 到底是干啥的? Lucene 是一套用于全文检索和搜索的开放源代码程序库,一个能够轻松集添加搜索功能到一个应用程序中的简单却强大的核心代码库和 API。

Lucene,目前最受欢迎的 Java 全文搜索框架。原因很简单,hibernate search、solr、elasticsearch 都是基于 lucene 拓展出来的搜索引擎。

Hibernate Search 是在 apache Lucene 的基础上建立的主要用于 Hibernate 的持久化模型的全文检索工具。

Elasticsearch 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

Solr 它是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。提供了层面搜索(就是统计)、命中醒目显示并且支持多种输出格式(包括 XML/XSLT 和 JSON 等格式)。

所以 lucene 牛不牛逼!!

接下来,我们分为以下几个部分去理解、打开 lucene 的真面目。

  • 相关概念

  • 构建索引与查询索引过程

  • 倒排索引

  • 可视化工具

  • 项目应用指南

全文搜索引擎

相关概念

lucene 官方网站:http://lucene.apache.org/

既然是全文搜索工具,肯定有一定的排序结构和规则。当我们输入关键字的时候,lucene 能安装内部的层次结构快速检索出我需要的内容。这里面会涉及几个层次和概念。

lucene概念

索引库(Index)

一个目录一个索引库,同一文件夹中的所有的文件构成一个 Lucene 索引库。类似数据库的表的概念。

lucene的索引实例

段(Segment)

Lucene 索引可能由多个子索引组成,这些子索引称为段。每一段都是完整独立的索引,能被搜索。

文档(Document)

一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。段是索引数据存储的单元。类似数据库内的行或者文档数据库内的文档的概念。

域(Field)

一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等。类似于数据库表中的字段

词(Term)

词是索引的最小单位,是经过词法分析和语言处理后的字符串。一个Field由一个或多个Term组成。比如标题内容是“hello lucene”,经过分词之后就是“hello”,“lucene”,这两个单词就是Term的内容信息,当关键字搜索“hello”或者“lucene”的时候这个标题就会被搜索出来。

分词器(Analyzer)

一段有意义的文字需要通过Analyzer来分割成一个个词语后才能按关键词搜索。StandartdAnalyzer是Lucene中常用的分析器,中文分词有CJKAnalyzer、SmartChinieseAnalyzer等。

lucene索引存储结构概念图

上图大概可以这样理解,索引内部由多个段组成,当新文档添加进来时候会生成新的段,不同的段之间可以合并(Segment-0、Segment-1、Segment-2合并成Segment-4),段内含有文档号与文档的索引信息。而每个文档内有多个域可以进行索引,每个域可以指定不同类型(StringField,TextField)。

所以,从图中可以看出,lucene的层次结构依次如下:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)

在上面我们了解了 lucene 的一些基本概念,接下来我们进入原理分析的环节。

(为什么 lucene 搜索引擎查询这么快?)

倒排索引

我们都知道要想提高检索速度要建立索引,重点就在这里,lucene使用了倒排索引(也叫反向索引)的结构。

倒排索引(反向索引)自然就有正排索引(正向索引)。

  • 正排索引是指从文档检索出单词,正常查询的话我们都是从文档里面去检索有没这个关键字单词。

  • 倒排索引是指从单词检索出文档,与从正排索引是倒过来的概念,需要预先为文档准备关键字,然后查询时候直接匹配关键字得到对应的文档。

有一句这样的总结:由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)

lucene索引存储结构概念图

lucene索引存储结构概念图

(具体怎么实现的呀?)

咱们来举个例子来研究一下(例子来源于网络):

假如现在有两个文档,内容分别是:

  • 文档1:home sales rise in July.

  • 文档2:increase in home sales in July.

lucene索引存储结构概念图

分析上图可知,首先文档经过分词器(Analyzer)分词之后,我们可以得到词(term),词和文档ID是对应起来的,接下来这些词集进行一次排序,然后合并相同的词并统计出现频率,以及记录出现的文档ID。

所以:

实现时,lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。

索引时,假设要查询单词 “sales”,lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。

(原来如此!)

lucene可视化工具Luke

构建索引与查询索引过程

以上我们知道了lucene构建索引的原理,接下来我们在代码层面去使用lucene。

我们先来看一张图:

lucene索引存储结构概念图

检索文件之前先要建立索引,所以上图得从“待检索文件”节点开始看。

构建索引过程:

1、为每一个待检索的文件构建Document类对象,将文件中各部分内容作为Field类对象。

2、使用Analyzer类实现对文档中的自然语言文本进行分词处理,并使用IndexWriter类构建索引。

3、使用FSDirectory类设定索引存储的方式和位置,实现索引的存储。

检索索引过程:

4、使用IndexReader类读取索引。

5、使用Term类表示用户所查找的关键字以及关键字所在的字段,使用QueryParser类表示用户的查询条件。

6、使用IndexSearcher类检索索引,返回符合查询条件的Document类对象

其中虚线指向的是这个类所在的包名(packege)。如Analyzer在org.apache.lucene.analysis包下。

lucene索引存储结构概念图

构建索引代码:

//创建索引

public class CreateTest {

    public static void main(String[] args) throws Exception {
        Path indexPath = FileSystems.getDefault().getPath("d:\\index\\");

//        FSDirectory有三个主要的子类,open方法会根据系统环境自动挑选最合适的子类创建

//        MMapDirectory:Linux, MacOSX, Solaris

//        NIOFSDirectory:other non-Windows JREs

//        SimpleFSDirectory:other JREs on Windows

        Directory dir = FSDirectory.open(indexPath);

        // 分词器

        Analyzer analyzer = new StandardAnalyzer();
        boolean create = true;
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
        if (create) {
            indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        } else {
            // lucene是不支持更新的,这里仅仅是删除旧索引,然后创建新索引

            indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
        }
        IndexWriter indexWriter = new IndexWriter(dir, indexWriterConfig);

        Document doc = new Document();
        // 域值会被索引,但是不会被分词,即被当作一个完整的token处理,一般用在“国家”或者“ID

        // Field.Store表示是否在索引中存储原始的域值

        // 如果想在查询结果里显示域值,则需要对其进行存储

        // 如果内容太大并且不需要显示域值(整篇文章内容),则不适合存储到索引中

        doc.add(new StringField("Title", "sean", Field.Store.YES));
        long time = new Date().getTime();
        // LongPoint并不存储域值

        doc.add(new LongPoint("LastModified", time));
//        doc.add(new NumericDocValuesField("LastModified", time));

        // 会自动被索引和分词的字段,一般被用在文章的正文部分

        doc.add(new TextField("Content", "this is a test of sean", Field.Store.NO));

        List<Document> docs = new LinkedList<>();
        docs.add(doc);

        indexWriter.addDocuments(docs);
        // 默认会在关闭前提交

        indexWriter.close();
    }
}

对应时序图:

lucene索引存储结构概念图

查询索引代码:

//查询索引

public class QueryTest {

    public static void main(String[] args) throws Exception {
        Path indexPath = FileSystems.getDefault().getPath("d:\\index\\");
        Directory dir = FSDirectory.open(indexPath);
        // 分词器

        Analyzer analyzer = new StandardAnalyzer();

        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher searcher = new IndexSearcher(reader);

        // 同时查询多个域

//        String[] queryFields = {"Title", "Content", "LastModified"};

//        QueryParser parser = new MultiFieldQueryParser(queryFields, analyzer);

//        Query query = parser.parse("sean");


        // 一个域按词查doc

//        Term term = new Term("Title", "test");

//        Query query = new TermQuery(term);


        // 模糊查询

//        Term term = new Term("Title", "se*");

//        WildcardQuery query = new WildcardQuery(term);


        // 范围查询

        Query query1 = LongPoint.newRangeQuery("LastModified", 1L, 1637069693000L);

        // 多关键字查询,必须指定slop(key的存储方式)

        PhraseQuery.Builder phraseQueryBuilder = new PhraseQuery.Builder();
        phraseQueryBuilder.add(new Term("Content", "test"));
        phraseQueryBuilder.add(new Term("Content", "sean"));
        phraseQueryBuilder.setSlop(10);
        PhraseQuery query2 = phraseQueryBuilder.build();

        // 复合查询

        BooleanQuery.Builder booleanQueryBuildr = new BooleanQuery.Builder();
        booleanQueryBuildr.add(query1, BooleanClause.Occur.MUST);
        booleanQueryBuildr.add(query2, BooleanClause.Occur.MUST);
        BooleanQuery query = booleanQueryBuildr.build();

        // 返回doc排序

        // 排序域必须存在,否则会报错

        Sort sort = new Sort();
        SortField sortField = new SortField("Title", SortField.Type.SCORE);
        sort.setSort(sortField);

        TopDocs topDocs = searcher.search(query, 10, sort);
        if(topDocs.totalHits > 0)
            for(ScoreDoc scoreDoc : topDocs.scoreDocs){
                int docNum = scoreDoc.doc;
                Document doc = searcher.doc(docNum);
                System.out.println(doc.toString());
            }
    }
}

对应时序图:

lucene索引存储结构概念图

lucene 版本信息:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>7.4.0</version>
</dependency>

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>7.4.0</version>
</dependency>

项目应用指南

在实际开发,比较少会直接用lucene,现在主流的搜索框架solr、Elasticsearch都是基于lucene,给我们提供了更加简便的API。特别是在分布式环境中,Elasticsearch可以问我们解决单点问题、备份问题、集群分片等问题,更加符合发展趋势。

Done.

lucene索引存储结构概念图