vlambda博客
学习文章列表

lucene搜索引擎的使用及原理分析

从基本概念、使用示例、原理解析三方面,记录lucene学习笔记,以备后面查阅。


简单介绍

首先,lucene是什么

  • Apache开源项目,Java实现,高性能、可扩展的全文检索引擎工具库

  • 提供一套简单易用的索引/检索/全文分析等API

  • 专注于文本索引和检索,不是一个完整的全文检索引擎


那么,lucene可以怎么用

  • 嵌入应用程序中实现搜索功能

  • 以此为基础构建完整的全文检索引擎:ElasticSearch / Solr(类比跑车与引擎)


基本概念
  • Index

    索引,类比关系型数据库的表,但无schema约束

  • Document

    文档,类比行,唯一ID seqNo,或叫 docId

  • Field

    域,一个文档包含一个或多个不同命名的域,每个域有一个域名和对应的域值

  • Term

    项,索引和检索的最小单位

  • Segment

    先写内存buffer,然后flush为一个Segment,docId 在Segment内唯一


本质是建立倒排索引,从Term到Term字典;

检索可看作是一个两阶段查询,从Term找到DocId,从DocId取到Doc。



使用示例
  • 索引-写-构建

1、指定索引目录Directory directory = FSDirectory.open(Paths.get("索引目录"));2、指定分词器Analyzer analyzer = new StandardAnalyzer();3、创建索引配置IndexWriterConfig config = new IndexWriterConfig(analyzer);4、创建WriterIndexWriter indexWriter = new IndexWriter(directory, config);5、写入文档Document document = new Document();document.add(new TextField("title", "test", Field.Store.NO));document.add(new TextField("body", "the program has a lot of errors", Field.Store.YES));indexWriter.addDocument(document);6、提交indexWriter.commit();
  • 索引-读-检索

1、指定索引目录Directory directory = FSDirectory.open(Paths.get("索引目录"));2、创建ReaderIndexReader indexReader = DirectoryReader.open(directory);3、创建SearcherIndexSearcher indexSearcher = new IndexSearcher(indexReader);4、构建查询对象Query query = new QueryParser("title", new StandardAnalyzer()).parse("test");5、检索TopDocs topDocs = indexSearcher.search(query, 10);6、获取目标文档Document one = Arrays.stream(topDocs.scoreDocs).findFirst().map(scoreDoc -> indexSearcher.doc(scoreDoc.doc)).orElse(null);
  • 索引-读-查询语法
  1. 查询对象:Query

    1. 项查询:TermQuery,某个Document某个域Field是否包含某个Term

    2. 组合查询:BooleanQuery,指定条件 BooleanClause.Occur

    3. 配符查询:WildcardQuery,* 零个或多个,? 一个,\ 转义

    4. 前缀查询:PrefixQuery

    5. 模糊查询:FuzzyQuery,编辑距离算法,计算字符串转换需要的最少操作数

    6. 短语查询:PhraseQuery,指定Term及相对位置

  2. 查询解析器:QueryParser

  3. and , or , +, -, *, ?, (), ~ 等


原理解析

1. 索引目录

  • Directory类:索引文件存储

    • SimpleFSDirectory:java io api,无法很好支持多线程操作

    • NIOFSDirectory:java nio api,FileChannel 位置读写,非windows平台很好支持多线程

    • MMapDirectory:内存映射,消耗虚拟内存空间

    • RAMDirectory:写入内存

    • FileSwitchDirectory:主目录和扩展目录切换


    FSDirectory.open() 的默认返回:linux平台 mmap,非windows平台 nio,windows ram


2. 索引文件


3. 索引锁

  • 线程安全

    • 同一个索引可被任意数量的只读的IndexReader打开

    • IndexWriter与IndexWriterConfig互相绑定

    • IndexWriter使用的索引目录排他

    • 文件锁,write.lock


  • FSLockFactory类

      • NativeFSLockFactory:默认,使用java nio FileLock,获取write.lock文件上的锁,安全,不适合NFS文件系统

      • SimpleFSLockFactory:使用Files.createNewFile,异常情况下不安全,因为锁文件write.lock不会自动删除

      • SingleInstanceLockFactory:内存中维护单一实例,RAMDirectory默认


    4. 索引创建

    以更新文档为例,简单的调用流程图:


    三个关键流程:

    • 刷盘 Flush:索引数据从内存Buffer刷到索引目录,减少内存占用

    • 提交 Commit:产生Segment文件,保存一个提交点所有有效的索引数据,异常可回滚至上一提交状态,提交后新增索引数据可被检索

    • 合并 Merge:将小的段文件合并,减少文件大小及数量,提高检索效率

    另,

    • 写线程类 DocumentsWriterPerThread (DWPT) ,一个IndexWriter下多个线程操作索引时,每个线程获得一个DWPT实例

    • 写线程池 DocumentsWriterPerThreadPool,一个DWPT的对象池结构,使用Dequeue队列存储DWPT,使用LIFO顺序取DWPT


    5. 检索
    • segments_N文件

      • 可独立被查询,不可修改

      • 包含当前索引目录中所有的索引信息

      • 每个Segment_N文件代表某次commit()时的索引状态

      • 每个segment文件对应一个SegmentReader,检索时读多个segment数据进行合并

    • 非实时/近实时检索

      • DirectoryReader.open(final Directory directory)

        • 非实时,因为每次打开Readers时,是读索引目录Directory中N值最大的segment_N文件,即最近的一次commit,获取已经提交的索引信息,通过IndexWriter新增的索引数据无法获知

      • DirectoryReader.open(final IndexWriter indexWriter)

        • 近实时,因为IndexWriter写入时会打开索引目录,所以通过writer获取reader,既可以获取已通过commit提交的索引数据,也可以获取writer新添加的索引数据


      6. 编码

      记录其中两种编码方式:

      • VByte格式

        • 变长格式存储

        • 每个byte的高1位:是否有更多byte可读,

        • 每个byte的低7位:具体值,低字节在前,高字节在后


      • ZigZag整数编码

        • Protocol Buffers / Thrift 使用

        • 位运算:(i >> 31) ^ (i << 1)

        • 目的:有符号整数映射为无符号

        • 结果:-1 => 1,1 => 2,-2 => 3,2 => 4等


      知识点很多,后续要继续结合源码进行学习分析,come on !