lucene 思惟导图,让搜索引擎再也不难懂

image

   (公众号回复“lucene”获取源导图)java

今天,咱们来说讲lucene,同窗们搬好板凳坐好啦。git

(lucene干吗的呀?)github

首先咱们来看张思惟导图:数据库

image

以上是咱们java经常使用的全文搜索引擎框架,不少项目的搜索功能都是基于以上4个框架完成的。apache

因此lucene究竟是干啥的?服务器

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

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

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

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

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

因此lucene牛不牛逼!!

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

  • 相关概念

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

  • 倒排索引

  • 可视化工具

  • 项目应用指南

相关概念

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

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

image

索引库(Index)

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

image

(lucene的索引实例)

段(Segment)

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

文档(Document)

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

域(Field)

一篇文档包含不一样类型的信息,能够分开索引,好比标题,时间,正文,做者等。相似于数据库表中的字段*。*

词(Term)

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

**分词器(**Analyzer)

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

image

(lucene 索引存储结构概念图)

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

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

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

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

倒排索引

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

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

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

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

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

image

(具体怎么实现的呀?)

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

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

  • 文档1:home sales rise in July.

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

image

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

因此:

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

索引时,假设要查询单词 “sales”,lucene先对词典二元查找、找到该词,经过指向频率文件的指针读出全部文章号,而后返回结果。词典一般很是小,于是,整个过程的时间是毫秒级的。  

(原来如此!)

lucne可视化工具Luke

image

构建索引与查询索引过程

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

咱们先来看一张图:

image

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

构建索引过程:

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

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

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

检索索引过程:

四、使用IndexReader类读取索引。

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

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

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

image

构建索引代码:

//建立索引
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();
    }
}

对应时序图:

image

查询索引代码:

//查询索引
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());
            }
    }
}

对应时序图:

image

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能够问咱们解决单点问题、备份问题、集群分片等问题,更加符合发展趋势。

至此,整篇完~

image

java思惟导图

长按关注,天天java一下,成就架构师

相关文章
相关标签/搜索