Lucene的诞生背景
传统数据库
- 常见数据结构:
- 结构化数据:表、字段表示的数据
- 半结构化数据:xml、html等
- 非结构化数据:文本、文档、图片、音频、视频等
- 索引原理:对列值创建排序存储,数据结构={列值、行地址},在有序数据列表中就可以利用二分查找快速找到要查找的行的地址,再根据地址直接取行数据
- 索引特点:数据库适合结构化数据的精确查询,而不适合半结构化、非结构化数据的模糊查询及灵活搜索(特别是数据量大时),无法提供想要的实时性
- 特例说明:在文档标题上建索引,当查询标题=”天气预报“时,数据库需要查询标题 LIKE '%天气预报%',此时索引失效,进行全表扫描,当数据量大时就会成为噩梦。
为了解决这个问题,搜索引擎应运而生,Lucene是其中极具代表性的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。由Apache软件基金会支持和提供,也是当前以及最近几年最受欢迎的免费Java信息检索程序库
搜索引擎
- 工作原理
- 计算相关性,排序、输出
- 搜索时,对搜索输入进行分词,查找倒排索引
- 从数据源加载数据,分词、建立倒排索引
- 实现
- 分词器
- 倒排索引,索引存储
- 相关性计算模型
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-创建索引核心部分
- IndexWriter 此类在建立索引的过程中是创建/更新的核心
- Directory 此类表示索引的存储位置
- Analyzer Analyzer类负责分析一个文件,并从被索引的文本获取令牌/字。不经过分析完成,IndexWriter将不能创建索引
- Document Document代表一个虚拟文档与字段,其中字段是可包含在物理文档的内容,元数据等对象,Analyzer只能理解文档
- 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);
}
}
运行效果,如下图:
此处重点提及一下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,相信索引部分会有更深刻的认识。