Lucene的诞生背景

传统数据库

  1. 常见数据结构:
  • 结构化数据:表、字段表示的数据
  • 半结构化数据:xml、html等
  • 非结构化数据:文本、文档、图片、音频、视频等
  1. 索引原理:对列值创建排序存储,数据结构={列值、行地址},在有序数据列表中就可以利用二分查找快速找到要查找的行的地址,再根据地址直接取行数据
  2. 索引特点:数据库适合结构化数据的精确查询,而不适合半结构化、非结构化数据的模糊查询及灵活搜索(特别是数据量大时),无法提供想要的实时性
  3. 特例说明:在文档标题上建索引,当查询标题=”天气预报“时,数据库需要查询标题 LIKE '%天气预报%',此时索引失效,进行全表扫描,当数据量大时就会成为噩梦。

为了解决这个问题,搜索引擎应运而生,Lucene是其中极具代表性的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。由Apache软件基金会支持和提供,也是当前以及最近几年最受欢迎的免费Java信息检索程序库

 搜索引擎

  1. 工作原理
  1. 计算相关性,排序、输出
  2. 搜索时,对搜索输入进行分词,查找倒排索引
  3. 从数据源加载数据,分词、建立倒排索引
  1. 实现
  • 分词器
  • 倒排索引,索引存储
  • 相关性计算模型

Lucene部分概念

倒排索引

即关键词到文章的索引,倒排索引源于实际应用中需要根据属性的值来查找记录,Lucene基于关键词进行索引和查询。

获取关键词:

文章

关键字

关键字

关键字

文章1

tom

alice

jack

文章2

jack

tom

 

文章3

alice

jack

 

建立倒排索引:

关键字

文章

文章

tom

文章1

文章2

jack

文章2

文章3

alice

文章1

文章3

文件扩展名释义

名称

文件扩展名

描述

Segments File

segments_N

保存了一个提交点(a commit point)的信息

Lock File

write.lock

防止多个IndexWriter同时写到一份索引文件中

Segment Info

.si 

保存了索引段的元数据信息

Compound File

.cfs,.cfe 

一个可选的虚拟文件,把所有索引信息都存储到复合索引文件中

Fields

.fnm 

保存fields的相关信息

Field Index

.fdx 

保存指向field data的指针

Field Data 

.fdt  

文档存储的字段的值

Term Dictionary  

.tim    

term词典,存储term信息

Term Index 

.tip 

到Term Dictionary的索引

Frequencies 

.doc 

由包含每个term以及频率的docs列表组成

Positions 

.pos  

存储出现在索引中的term的位置信息

Payloads  

.pay 

存储额外的per-position元数据信息,例如字符偏移和用户payloads

Norms

.nvd,.nvm 

.nvm文件保存索引字段加权因子的元数据,.nvd文件保存索引字段加权数据

Per-Document Values

.dvd,.dvm

.dvm文件保存索引文档评分因子的元数据,.dvd文件保存索引文档评分数据

Term Vector Index 

.tvx 

将偏移存储到文档数据文件中

Term Vector Documents

.tvd 

包含有term vectors的每个文档信息

Term Vector Fields 

.tvf

字段级别有关term vectors的信息

Live Documents

.liv   

哪些是有效文件的信息

Point values 

.dii,.dim

保留索引点,如果有的话

Indexing-创建索引核心部分

  1. IndexWriter 此类在建立索引的过程中是创建/更新的核心
  2. Directory 此类表示索引的存储位置
  3. Analyzer Analyzer类负责分析一个文件,并从被索引的文本获取令牌/字。不经过分析完成,IndexWriter将不能创建索引
  4. Document Document代表一个虚拟文档与字段,其中字段是可包含在物理文档的内容,元数据等对象,Analyzer只能理解文档
  5. Field Field是最低单元或索引过程的起点,它代表其中一个键被用于识别要被索引的值的键值对关系,用于表示一个文件内容的字段将具有键为“内容”,值可以包含文本或文档的数字内容的部分或全部

这里用一个简单的Demo来说明,索引的创建过程:

public class LuceneXinTest {
   public static void main(String[] args) throws Exception {
       // 1. 准备中文分词器
       Analyzer analyzer = new IKAnalyzer();

       long startTime = System.currentTimeMillis();
       System.out.println("开始时间:"+startTime);

       // 2. 索引
       List<String> adNames = new ArrayList<>();
       adNames.add("大话西游曾经有一份真诚的爱情放在我面前,我没有珍惜");
       adNames.add("辛德勒的名单这辆车,歌德应该会买.我为什么留这辆车,它能换十条命,十条命,多救十个人");
       
       Directory index = createIndex(analyzer, adNames);

       // 3.动态显示索引创建过程
       int numberPerPage = 1000;
       System.out.printf("当前一共有%d条数据%n",adNames.size());

       long endTime = System.currentTimeMillis();
       System.out.println("结束时间:"+endTime);
       System.out.println("总耗时:"+(endTime-startTime)+"ms");
  }

   private static Directory createIndex(Analyzer analyzer, List<String> adNames) throws IOException {
       // 使用内存存放索引
       // Directory index = new RAMDirectory();
       // 使用文件系统存放索引
       Directory index = FSDirectory.open(new File("C:\\Users\\RovisuK\\Desktop\\Test\\Index").toPath());

       IndexWriterConfig config = new IndexWriterConfig(analyzer);
       // 不合并文件,会显示很多带后缀名的索引、数据文件等
       config.setUseCompoundFile(false);
       System.out.println("Codec is : "+config.getCodec());

       IndexWriter writer = new IndexWriter(index, config);

       // 循环放入数据
       for (String name : adNames) {
           addDoc(writer, name);
      }
       writer.close();
       return index;
  }

   private static void addDoc(IndexWriter writer, String name) throws IOException {
       Document doc = new Document();
       doc.add(new TextField("name", name, Field.Store.YES));
       writer.addDocument(doc);
  }
}

运行效果,如下图:

Lucene 可以创建多个索引吗_字段

此处重点提及一下org.apache.lucene.index.DefaultIndexingChain这个类,提取其中flush()的部分源码(由于篇幅过长省略部分源码,此处以Lucene5.3.1版本为例进行分析):

/**
   * 参数state中的segmentInfo是DocumentsWriterPerThread构造函数中创建的SegmentInfo,保存了相应的段信息,
   * maxDoc函数返回目前在内存中的文档树。DefaultIndexingChain的flush函数接下来通过writeNorms函数将norm信息写入.nvm和.nvd文件中。
   **/
   int maxDoc = state.segmentInfo.maxDoc();
   /* 写入nvd数据文件以及nvm元数据文件
    * Lucene53Codec的normsFormat函数最终返回Lucene53NormsFormat,对应的normsConsumer函数返回一个Lucene53NormsConsumer
    * IndexFileNames的segmentFileName函数会根据传入的参数段名(例如_0)和拓展名(例如.nvd)构造文件名_0.nvd。接着通过FSDirectory的createOutput创建输出流,>TrackingDirectoryWrapper::createOutput
    * 回到Lucene53NormsFormat的normsConsumer函数中,接下来就通过writeIndexHeader向文件写入头信息
    * 回到DefaultIndexingChain的writeNorms函数中,接下来通过getPerField获取PerField,其中的FieldInfo保存了域信息
    * 继续往下看,PerField中的成员变量norms在其构造函数中被定义为NormValuesWriter,对应的finish为空函数,而flush函数如下
    * 这里的normsConsumer就是Lucene53NormsConsumer,对应的addNormsField函数如下
    * 该函数再往下看就是将FieldInfo中的数据通过刚刚创建的FSIndexOutput写入到.nvd和.nvm文件中
    **/
   writeNorms(state);

   /**
    * writeDocValues函数遍历得到每个PerField,PerField中的docValuesWriter根据不同的Field值域类型被定义为BinaryDocValuesWriter、NumericDocValuesWriter、SortedDocValuesWriter、SortedNumericDocValuesWriter和SortedSetDocValuesWriter
    * 注意查看DefaultIndexingChain::indexDocValue
    * 假设PerField中的docValuesWriter被定义为BinaryDocValuesWriter,回到writeDocValues函数中,再往下通过docValuesFormat函数返回一个PerFieldDocValuesFormat,并通过PerFieldDocValuesFormat的fieldsConsumer获得一个DocValuesConsumer,fieldsConsumer最后返回的其实是一个FieldsWriter
    * 回到DefaultIndexingChain的writeDocValues函数中,接下来继续调用docValuesWriter也即前面假设的BinaryDocValuesWriter的flush函数
    * BinaryDocValuesWriter的flush函数主要调用了FieldsWriter的addBinaryField函数添加FieldInfo中的数据
    * addBinaryField首先通过getInstance函数最终获得一个Lucene54DocValuesConsumer
    * 假设是第一次进入该函数,format会通过getDocValuesFormatForField函数被定义为Lucene53DocValuesFormat,然后通过Lucene53DocValuesFormat的fieldsConsumer函数构造一个Lucene53DocValuesConsumer并返回
    * 和前面的分析类似,这里创建了.dvd和.dvm的文件输出流并写入相应的头信息
    * Lucene50DocValuesConsumer的addBinaryField函数就不往下看了,就是调用文件输出流写入相应的数据
    **/
   writeDocValues(state);
   
   // it's possible all docs hit non-aborting exceptions...
   /**
    * 接下来通过initStoredFieldsWriter函数初始化一个StoredFieldsWriter
    * storedFieldsFormat函数返回Lucene50StoredFieldsFormat,其fieldsWriter函数会接着调用CompressingStoredFieldsFormat的fieldsWriter函数,最后返回CompressingStoredFieldsWriter
    * CompressingStoredFieldsWriter的构造函数创建了.fdt和.fdx两个文件并建立输出流
    * 回到DefaultIndexingChain的flush函数中,接下来调用fillStoredFields,进而调用startStoredFields以及finishStoredFields函数,startStoredFields函数会调用刚刚上面构造的CompressingStoredFieldsWriter的startDocument函数,该函数为空,finishStoredFields函数会调用CompressingStoredFieldsWriter的finishDocument函数
    * 经过一些值得计算以及设置后,finishDocument通过triggerFlush函数判断是否需要进行flush操作,该flush函数定义在CompressingStoredFieldsWriter中
    * fieldsStream是根据fdt文件创建的FSIndexOutput,对应的函数getFilePointer返回当前可以插入数据的位置。indexWriter被定义为CompressingStoredFieldsIndexWriter,是在CompressingStoredFieldsWriter的构造函数中创建的
    * 当blockChunks增大到blockSize时,启动writeBlock函数写入索引,writeBlock函数最终向.fdx中写入索引信息,这里就不往下看了
    * 回到CompressingStoredFieldsWriter的flush函数中,接下来通过writeHeader函数向.fdt文件中写入头信息。再往下的compressor被定义为LZ4FastCompressor,其compress函数将缓存bufferedDocs.bytes中的数据写入到fieldStream,也即.fdt文件对应的输出流中
    * 再回到DefaultIndexingChain的flush函数中,接下来看finish函数
    * CompressingStoredFieldsWriter的flush函数前面已经分析过了,这里的fieldsStream代表.fdt文件的输出流,最终将索引数据写入到fdt文件中
    * 这里最重要的是writeBlock函数,其最终也是将索引数据写入.fdx文件中
    **/
   initStoredFieldsWriter();
   fillStoredFields(maxDoc);
   storedFieldsWriter.finish(state.fieldInfos, maxDoc);
   storedFieldsWriter.close();
   ......

经过以上分析,进一步理解了Lucene索引文件的生成以及写入存储过程,再参考之前写的Demo,相信索引部分会有更深刻的认识。