文章目录
2. lucene的数据结构
2.1 索引的逻辑层次
在lucene中索引存储的逻辑层次有多个层次,从大到小依次是
- index:索引代表了一类数据的完整存储
- segment: 一个索引可能有一个或者多个段构成
- doc: segment中存储的是一篇一篇的文档doc,每个segment是一个doc的集合
- field: 每个doc都有多个field构成,filed才包含了具体的文本,类似于一个json对象的一个属性
- term: 每个field的值可以进行分词,进而得到多个term,term是最基本的单元,每个field可以保存自己的词向量,用来计算搜索相似度。
2.2 lucene 存储的数据概述
2.2.1 lucene的存储模块
在lucene中,因为lucene是一个存储系统,所以他的首要任务是以一定的数据结构将输入进来的数据(doc)保存起来,在检索的时候才能将这些信息返回给用户
同时,为了能够支撑高效的检索,还要有额外的数据结构来支撑高效检索
为了支撑高效检索,lucene创造了倒排索引,同时,为了能够满足更加复杂的查询,比如按照某一个字段进行排序等功能,lucene又在3.x引入了docvalues 正排索引的概念,为了支持数值查询或者地理位置查询等多维度的查询,lucene使用了BKD-Tree,来支撑数值型的range查询。
综上,目前lucene的数据结构模块包括
- 文档存储信息:保存了doc的信息,是从doc–>term的信息维度
- 反向信息:倒排索引,保存了从term—>doc_id 的信息
- docvalues : 保存了doc_id—> field的信息,方便根据doc_id快速检索某个field
- point-value: 数值类型信息,可以是多维的,比如地理位置信息等,可以进行多维数值数据的范围查询
2.2.2 lucene的一个index的文件
基于lucene7.5 打开对应的文件夹,看看lucene的一个索引中包含哪些文件
Name | Extension | Brief Description | 中文 |
---|---|---|---|
Segments File | segments_N | Stores information about a commit point | 存储一次commit point信息 |
Lock File | write.lock | The Write lock prevents multiple IndexWriters from writing to the same file. | 写锁,防止多个indxer-writer写同一个文件 |
Segment Info | .si | Stores metadata about a segment | 保存一个segment的元数据信息,lucene版本,segment内的doc-count |
Compound File | .cfs, .cfe | An optional “virtual” file consisting of all the other index files for systems that frequently run out of file handles. | 复合文件,可以没有,就是多个索引的一些信息都存储在里面,分析数据结构的时候可以先不关注者一点 |
Fields | .fnm | Stores information about the fields | 保存doc的field的元信息 |
Field Index | .fdx | Contains pointers to field data | .fdt的索引文件 |
Field Data | .fdt | The stored fields for documents | 存储每个doc的具体的内容,按照doc-numer分组,每组存储了一个doc的field信息 |
Term Dictionary | .tim | The term dictionary, stores term info | 保存term的词典 |
Term Index | .tip | The index into the Term Dictionary | term词典的索引文件 |
Frequencies | .doc | Contains the list of docs which contain each term along with frequency | 词典中的每个term指向的列表,每一项包含了docId+term-frequency信息(词频信息) |
Positions | .pos | Stores position information about where a term occurs in the index | 倒排保存term在field中经过tekenAnalizer后的term所在的位置,是第几个term |
Payloads | .pay | Stores additional per-position metadata information such as character offsets and user payloads | 词典中的每个term指向的列表,每一项都包含了文档的元数据信息或者用户自定义的一些信息 |
Norms | .nvd, .nvm | Encodes length and boost factors for docs and fields | 长度归一化,doc-boost, field-boost的记录信息 |
Per-Document Values | .dvd, .dvm | Encodes additional scoring factors or other per-document information. | 保存了额外的排序因子,或者是每个文档独有的信息 |
Term Vector Index | .tvx | Stores offset into the document data file | .tvd的索引文件 |
Term Vector Data | .tvd | Contains term vector data. | 保存了每个doc的每个field的term vector |
Live Documents | .liv | Info about what documents are live | 只有在一个segment中包含被删除的文档时才会生成,它记录了当前段中没有被删除的文档号 |
Point values | .dii, .dim | Holds indexed points, if any | 存储point类型的数据,int,long等 |
2.2.3 lucene 的数据类型
- Byte:是最基本的类型,长8位(bit)。
- UInt32:由4个Byte组成。
- UInt64:由8个Byte组成。
- VInt:
变长的整数类型,它可能包含多个Byte,对于每个Byte的8位,其中后7位表示数值,最高1位表示是否还有另一个Byte,0表示没有,1表示有。
越前面的Byte表示数值的低位,越后面的Byte表示数值的高位。
例如130化为二进制为 1000, 0010,总共需要8位,一个Byte表示不了,因而需要两个Byte来表示,第一个Byte表示后7位,并且在最高位置1来表示后面还有一个Byte,所以为(1) 0000010,第二个Byte表示第8位,并且最高位置0来表示后面没有其他的Byte了,所以为(0) 0000001。 - Chars:是UTF-8编码的一系列Byte。
- String:一个字符串首先是一个VInt来表示此字符串包含的字符的个数,接着便是UTF-8编码的字符序列Chars。
2.2.4 ducoment number
在开始正式了解各个文件之前,我们要说一下ducoment number 的概念
数据库内通过主键来唯一标识一行,而Lucene的Index通过DocId来唯一标识一个Doc。这个DocId也就是document number
不过有几点要特别注意:
- DocId实际上并不在Index内唯一,而是Segment内唯一,Lucene这么做主要是为了做写入和压缩优化。那既然在Segment内才唯一,又是怎么做到在Index级别来唯一标识一个Doc呢?方案很简单,Segment之间是有顺序的,举个简单的例子,一个Index内有两个Segment,每个Segment内分别有100个Doc,在Segment内DocId都是0-100,转换到Index级的DocId,需要将第二个Segment的DocId范围转换为100-200。
- DocId在Segment内唯一,取值从0开始递增。但不代表DocId取值一定是连续的,如果有Doc被删除,那可能会存在空洞。
- 一个文档对应的DocId可能会发生变化,主要是发生在Segment合并时。
Lucene内最核心的倒排索引,本质上就是Term到所有包含该Term的文档的DocId列表的映射。所以Lucene内部在搜索的时候会是一个两阶段的查询,第一阶段是通过给定的Term的条件找到所有Doc的DocId列表,第二阶段是根据DocId查找Doc。Lucene提供基于Term的搜索功能,也提供基于DocId的查询功能。就是倒排索引和正排信息共同支撑了整个查询过程。
2.3 正向信息的存储
- 按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)
- 也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了那些词。
- 既然是层次结构,则每个层次都保存了本层次的信息以及下一层次的元信息,也即属性信息,比如一本介绍中国地理的书,应该首先介绍中国地理的概况,以及中国包含多少个省,每个省介绍本省的基本概况及包含多少个市,每个市介绍本市的基本概况及包含多少个县,每个县具体介绍每个县的具体情况。
包含正向信息的文件有:
- segments_N保存了此索引包含多少个段,每个段包含多少篇文档。
- XXX.fnm保存了此段包含了多少个域,每个域的名称及索引方式。
- XXX.fdx,XXX.fdt保存了此段包含的所有文档,每篇文档包含了多少域,每个域保存了那些信息。
- XXX.tvx,XXX.tvd 保存了此段包含多少文档,每篇文档包含了多少域,每个域包含了多少词,每个词的字符串,位置等信息。
因为segments_N信息含量相对较少,我们就不再具体介绍,看看其他几个文件都存储了什么吧。
2.3.1 .fnm 文件
对应的官方文档的信息参考这里
Header,FieldsCount, [{FieldName,FieldNumber, FieldBits,DocValuesBits,DocValuesGen,Attributes}]*FieldsCount FieldsCount,Footer
2.3.1.1 一级数据
- Header: 一些元信息是fnm文件的版本号,对于Lucene 2.9为-2
- FieldsCount: 域的数目
- 一个数组的域(Fields),包含下面的信息
2.3.1.2 二级数据Field
一个filed包含的信息:
- FieldName:域名,如"title",“modified”,"content"等。
- FieldNumber: 不像之前的版本那样通过field的顺序来隐含的表达fieldNumber,这里直接给了每个field一个number
- FieldBits: 总共占用一个字节,一系列标志位,表明对此域的索引方式
- 倒数第一位:只对索引field有效,1表示保存词向量,0为不保存词向量。
对于长度较小的字段不建议开启term verctor,因为只需要重新做一遍分词即可拿到term信息,而针对长度较长或者分词代价较大的字段,则建议开启term vector。Term vector的用途主要有两个,一是关键词高亮,二是做文档间的相似度匹配(more-like-this)。 - 倒数第二位:只对index field有效,1表示不保存标准化因子,0则是保存标准化因子
- 倒数第三位:是否保存payload,1保存,0不保存
- 倒数第一位:只对索引field有效,1表示保存词向量,0为不保存词向量。
- IndexOptions: 一个字节,用来指导index相关的设置
0: not indexed 不进行索引
1: indexed as FieldInfo.IndexOptions.DOCS_ONLY 索引文档编号
2: indexed as FieldInfo.IndexOptions.DOCS_AND_FREQS 索引文档编号、关键词频率
3: indexed as FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS 索引文档编号、关键词频率、位置
4: indexed as FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS 索引文档编号、关键词频率 - DocValuesBits:一个字节,存放docvalue相关的信息,分为两个4位,其中高4个bit用来描述是否记录norm,低4个bit用来描述DocValues类型,DocValues的类型包括以下类型:
0:NONE
1:NUMERIC
2:BINARY
3:SORTED
4:SORTED_SET
5:SORTED_NUMERIC - Attributes
该字段描述了存储当前域的索引文件的格式(format),比如说当前是一个DocValues的域,那么Attributes的字段会有下面的值:
PerFieldDocValuesFormat.format:Lucene70
表示使用Lucene70这种格式来生成索引文件.dvd、.dvm。
4.0以后Field.Index废弃,使用FieldType来进行设置,并通过setIndexOptions方法设置索引选项
StringField 不分词并索引的字段,搜索时整字段匹配
TextField 分词并索引
StoredField 仅保存,不索引
FieldType type = new FieldType();
type.setIndexed(true);
type.setStored(true);
type.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS);
indexableFields.add(new Field("content", content, type));
这里只是api层面的变化,实际的存储结构并没有变化。
2.3.2 XXX.fdx,XXX.fdt文件
这两个信息存储了segment中文档的原始信息
2.3.2.1 .fdt 文件
.fdt存储了当前segment中所有doc的field信息,是按照documnet-number来分组的。在新的文档中,因为存储上压缩的考虑等,信息不是很容易理解。
fdt中的信息是按照Chunk 来进行组织的,每128个doc组成一个chunk,每个chunk包含 DocBase,ChunkDoc,DocFieldCounts,DocLengths,CompressedDocs
- DocBase: 当前chunk中第一个文档的文档号,因为根据这个文档号来差值存储,在读取的阶段需要根据该值恢复其他文档号。
- ChunkDocs: 当前chunk中的doc数量
- DocFieldCounts:当前chunk中所有doc的field-num
- DocLengths: 每篇文档的长度
- CompressedDocs: 存储了真正的所有doc的field信息
CompressedDocs 中的数据结构
- CompressedDocs 中存储了一个[Doc] 数组,数组的长度为 ChunkDocs
- Doc中的数据结构
- 域的编号filed number ,和.fnm中的field number对应
- 域值的类型:String、BinaryValue、Int、Float、Long、Double
- 域值的编号跟域值的类型组合存储为FieldNumAndType
- Value:域值
通过这里也可以看出,每个doc的信息就得到了存储。但是如果知道了一个文档号,想要获取这个文档的内容,可能要遍历这些chunk,则是很慢的一个操作,所以lucene增加了一个.fdx文件来做为索引文件,加快根据docId提取文档内容的过程。
2.3.2.2 .fdx 文件
.fdx中存储的主要是一个Block列表,每当.fdt文件中生成1024个chunk的时候便会在.fdx中生成一个Block。
Block的数据分为三个主要部分:
- BlockChunks :block中包含的chunk的个数,即1024个
- DocBases:存储了block中的每个chunk的文档号信息,但是不是列表形式的,而是使用了差值来压缩存储
- DocBase: block中第一个文档的文档号。用来在读取阶段,恢复所有chunk中其他被编码的文档号。
- AvgChunkDocs: block中平均一个chunk中包含的文档数,因为block是等所有chunk都产生的时候才会产生的block,所以avg是可以求出的
- BitsPerDocBaseDelta: 描述了存储文档号的需要的bit个数。
- DocBaseDeltas: 数组,使用差值存放了每个chunk中的文档数,结合DocBase,AvgChunkDocs 可以很快的算出block中第n个chunk的第一个文档号doc number,
第n个chunk的doc base = DocBase+n*AvgChunkDocs+DocBaseDeltas[n]
- 上面的信息主要是为了快速定位一个docId为k的doc所在的chunck编号n,有了chunck编号n,结合下面的StartPointers就可以在.fdt文件中定位chunk,进而快速找到doc的内容。
- StartPointers: 存储了block中的每个chunk在.fdt文件中的位置
- StartPointerBase: 当前block中第一个chunk的在.fdt中的位置。
- AvgChunkSize: block中平均每一个chunk的大小。
- BitsPerStartPointerDelta: 存储每一个chunk大小需要固定bit个数。
- StartPointerDeltas: 逻辑跟DocBaseDeltas一样,
第n个chunk 的pointer= StartPointerBase + AvgChunkSize * n + StartPointerDeltas[n]
正常的一个工作逻辑是,我们有了一个docId,
- 首先.fdx的信息加载到内存中,根据每个Block的DocBases 进行二分查找,可以得到该docId属于哪个Block
- 将该Block中的的DocBases进行重构,可以得到每个chunk的DocBase构成的一个数组,再使用二分查找,可以得到chunk的编号n
- 根据
第n个chunk 的pointer= StartPointerBase + AvgChunkSize * n + StartPointerDeltas[n]
可以得到该doc在.fdt中的chunk的初始位置 - 根据chunk的DocBase,DocLengths等得到CompressedDocs的位置,即可得到doc的信息
2.3.3 XXX.tvx,XXX.tvd
参考这里
.tvd 存储的是每个doc的每个field的term vector,按照doc进行分组, .tvx 是.tvd的索引文件。
.tvd文件存储了每个doc的每个filed的terms,term 对应的frequencies, positions, offsets 信息。
.tvd文件中主要的数据结构类似.fdt文件,是由一个个chunk组成的,但是chunk中的数据结构略有不同。
每个chunk中的结构如下:
- DocBase: DocBase是chunk中第一个文档的文档号。
- ChunkDocs: chunk中包含的文档个数。
- NumFields: NumFields记录了每篇文档中存储域的个数,使用了一个DocNumFields 数组来进行记录每个doc的field的个数
- FieldNums:是一个数组,存储了chunk中所有的filed的编号(不是每个doc的,是filed去重后得到的所有的filed的编号,不会大于.fnm中filed的编号个数)
- FieldNumOffs: [FieldNumOff]*TotalFields FieldNumOffs想要存储的是当前chunk中每个doc的所拥有的list[Field-Number]
- FieldNumOff 存储的是每个Field的Field-Number,实际上存储的是FieldNums中的数组的下标
- TotalFields= chunk中所有的doc的所有field的数量的和= sum(NumFields)
- Flags: Flags用来描述域是否存放位置position、偏移offset、负载payload信息,flag的值可以是下面3个值的组合:
0x01:包含位置position信息
0x02:包含偏移offset信息
0x04:包含负载payload信息 - TermData: 真正存储了当前chunk中所有doc的term信息,并没有假如doc的标识信息,需要冲前面的信息中解析。里面的数据结构相对比较多
- NumTerms: NumTerms描述了每一个域包含的term个数,使用PackedInts存储。
- TermLengths: TermLengths描述了每一个域中的每一个term的长度,使用PackedInts存储。
- TermFreqs: TermFreqs描述了每一个域中的每一个term在当前文档中的词频,使用PackedInts存储。
- Positions: Positions描述了每一个域中的每一个term在当前文档中的所有位置position信息,使用PackedInts存储。
- StartOffset: StartOffset描述了每一个域中的每一个term的startoffset,使用PackedInts存储。
- Lengths: Lengths描述了每一个域中的每一个term的偏移长度,使用PackedInts存储。
- TermAndPayloads: 使用LZ4算法存储每一个域中的每一个term值跟payload(如果有的话)。
2.3.4 .nvd,.nvm
.nvm文件保存索引字段加权因子的元数据,.nvd文件保存索引字段加权数据
2.3.4.1 nvd中到底存储了什么
关于nvd文件中具体存储了什么,一度非常疑惑,感觉lucene并没有直接说清楚,后面自己想到,norm是打分排序的一个归一化因子组合,不同的相似度计算算法可能需要是不一样的。
可以参考这个问题
比如之前的金典的TF/IDF算法对norm的需求是:
- Document boost:此值越大,说明此文档越重要。
- Field boost:此域越大,说明此域越重要。
- lengthNorm(field) = (1.0 / Math.sqrt(numTerms)):一个域中包含的Term 总数越多,也即文档越长,此值越小,文档越短,此值越大。
由此想到norm应该会因为你选择了不同的相似度计算算法而不一样。在网上也翻阅到,lucene确实有很多norm的计算方式
abstract long Similarity.computeNorm(FieldInvertState state)
Computes the normalization value for a field, given the accumulated state of term processing for this field (see FieldInvertState).
long BM25Similarity.computeNorm(FieldInvertState state)
long MultiSimilarity.computeNorm(FieldInvertState state)
long PerFieldSimilarityWrapper.computeNorm(FieldInvertState state)
long SimilarityBase.computeNorm(FieldInvertState state)
Encodes the document length in the same way as TFIDFSimilarity.
long TFIDFSimilarity.computeNorm(FieldInvertState state)
float DefaultSimilarity.lengthNorm(FieldInvertState state)
Implemented as state.getBoost()*lengthNorm(numTerms), where numTerms is getLength() if DefaultSimilarity.setDiscountOverlaps(boolean) is false, else it's getLength() - getNumOverlap().
abstract float TFIDFSimilarity.lengthNorm(FieldInvertState state)
Compute an index-time normalization value for this field instance.
所以nvd中是一个开放式的多个byte的数据结构,各路开发者可以按照自己喜欢的方式来产生和解析norm即可
2.3.4.2 .nvd文件
因为norm的信息主要是基于域进行设置的,所以他的统计维度是按照field进行统计的。就是统计了某个field每个doc在该field下的norm值
.nvd文件的主体内容是一个数组 [FieldData]*NumFields ,这里的NumFields 是指Field的数量,应该是指text的field的数量
FieldData的内容是:
- DocsWithFieldData: 记录了包含该field的docIdList(实际上根据不同情况有优化,但是主要要表达的就是这个意思)
- 优化一,当所有的doc都含有该field的话,对应的这里就不需要存储了,在.nvm中直接标识一下就行了
- 优化二,当所有的doc都不含有该field的话,这个地方也不需要进行存储了,在.nvm中直接标识一下就可以了
- NormsData: 记录了具体的norms,是一些列的byte,没有具体的分界,使用的时候是通过.nvm的索引进行取用和重建的
2.3.4.3 .nvm文件
.nvm文件保存索引字段加权因子的元数据。
.nvm的数据结构主要是一个数组的Entry,[Entry]*NumFields
Entry中的结构如下
- FieldNumber: 域的编号,用来唯一标识一种域。
- DocsWithFieldAddress: 指向了.nvd的DocsWithFieldData开始处,但是我们谈到了在.nvd中DocsWithFieldData有两点优化,所以在这里也有对应的改变
- 优化一,当所有的doc都含有该field的话,这里存储的是-1
- 优化二,当所有的doc都不含有该field的话,这里存储的是-2
- DocsWithFieldLength: .nvd中的DocsWithFieldData的长度,这里同样适用DocsWithFieldAddress的两个优化
- 优化一,当所有的doc都含有该field的话,这里存储的是0
- 优化二,当所有的doc都不含有该field的话,这里存储的是0
- NumDocsWithField: NumDocsWithField描述了包含当前域的文档个数。
- BytesPerNorm: 找出当前域在所有所属文档中的最大跟最小的两个标准化值来判断存储一个标准化值最大需要的字节数
- NormsAddress: NormsAddress作为索引映射nvd中一块数据区域,指向了.nvd中的NormsData这块数据区域的开始处,即当前域在所有文档中的标准化值。
2.3.4.4 根据DocId查找norm的过程简述
因为norm是在相似度计算阶段适用,所以是在通过query在term词典中过滤得到了倒排DocIdlist之后的事情。
有了TargetDocId,和query适用的Field信息,
- 通过.nvm中的信息快速的找到对应的Entry,根据FieldNumber来找Entry,实际上Entry不会太多
- 判断DocsWithFieldAddress,根据其不同情况来找到DocId的Field对应的NormsData
- 当所有的doc都含有该field的话,这里存储的是-1
- 因为docId是连续排列的,所以只要知道当前segment的第一个docId就可以知道要查询的TargetDocId对应的Norms数据在.nvd的NormsData中是第几个,假设顺序为第n个
- NormsAddress+n*BytesPerNorm 即可得到TargetDocId对应的norms在.nvd 的NormsData中对应的位置
- 部分doc含有该field的话,这种情况理论上应该避免,因为这会导致比较大的查询损耗比较大,也就是一个doc的field有,所有的都有才合适
- 根据DocsWithFieldAddress找到.nvd文件中的DocsWithFieldData的起始位置,然后根据DocsWithFieldLength读出 DocsWithFieldData 得到docIdList,进而二分查找找到TargetDocId对应的位置n
- NormsAddress+n*BytesPerNorm 即可得到TargetDocId对应的norms在.nvd 的NormsData中对应的位置
- 可以看出来第一步比较耗时
- 当所有的doc都含有该field的话,这里存储的是-1
到此,正向索引的信息基本上是描述完了,接下来就是看看反向的信息有哪些了。
2.4 反向索引信息的存储
保存了词典到倒排表的映射:词(Term) –> 文档(Document)
包含反向信息的文件有:
- XXX.tim,XXX.tip保存了词典(Term Dictionary),也即此段包含的所有的词按字典顺序的排序。
- XXX.doc保存了倒排表,也即包含每个词的文档ID列表。
- XXX.pos保存了倒排表中每个词在包含此词的文档中的位置。
- XXX.pay保存倒排表中的每个词在包含该词的文档中的offset和payload信息
反向索引信息就是我们常说的倒排表,也是实现文本近似查询的关键支撑。
2.4.1 XXX.tim,XXX.tip
在lucene7.5中对倒排表的term dictionary (词典)部分进行了比较大的改动。之前是使用跳跃表来实现的有序term的快速查询。现在改进到了FST(Finite State Transducer)有限状态转换机。FST的具体实现相对比较复杂,我们这里用类比的方式解释一下。FST其实和AC自动机很像。AC自动机是基于Trie-Tree的有限状态自动机,目的是为了实现多模式的字符串匹配。Trie树可以实现多个模式串的前缀压缩,后缀suffix是开放的结构,lucene为了进一步节约存储空间使用FST,FST基于Trie树更进一步,对后缀也做了压缩,整个数据结构类似于一个盗梦空间中的陀螺,中间粗大,两端都收于一点,而且查询效率也很高。前缀查询也很easy。
2.4.1.1 XXX.tip
XXX.tip部分存储的是FST,而且tip为每个filed都存储了一个FST结构。
总体结构 NumFields NumFields, DirOffset
- FSTIndex: 每个field都会有一个一个FST结构,因为我们的查询常常是指定field的
- IndexStartFP: vlong类型,每个field都对应有一个IndexStartFP,指明了每个field的FSTIndex 在tip文件中的位置,方便快速查找某个field的FSTIndex
- DirOffset: vlong类型,第一个IndexStartFP的位置
这样的话就可以通过DirOffset找到第一个IndexStartFP的位置,然后按照field-number就可以找到对应的IndexStartFP,这个应该很短,就是NumFields 个vlong。然后再通过IndexStartFP找到对应的FSTIndex.
这里需要强调的一点是,FST并不存储term的全部数据,而是term的前缀信息,比如有terms: abc ,abe, abfg 那么FST存储的是ab, 也就是他们的公共前缀,剩下的c,e,fg等存储在FST指向的XXX.tim文件当中。这样也避免了FST过大,同时又能够有比较好的查询效果。
2.4.1.1 XXX.tim
.tim 的主要结构 NumBlocks, FieldSummary, DirOffset,
- NodeBlock: 这个是term后缀存储的时候按照块存储的基本单位。在这里理论上也不需要再有属于哪个field的标识,因为.tip中的FST索引是按照Field来进行区分的,而FST又会指向.tim文件,也就间接完成了field的区分。NodeBlock分为两种,OuterNode | InnerNode;
- OuterNode : EntryCount, SuffixLength, SuffixLength, StatsLength, EntryCount, MetaLength, EntryCount
-
EntryCount: EntryCount描述了当前的OuterNode中包含多个entries,即包含了多少个term的信息。
-
SuffixLength、StatsLength、MetaLength: 这三个值分别描述了所有term的Suffix、TermStats、TermMetadata在.tim文件中的数据长度,在读取.tim时用来确定读取Suffix、TermStats、TermMetadata的范围区间。
-
SuffixLength: 这是一个字节序列,实际上会被解析成一个suffix数组,数组中每一个元素被分为两个部分,length,SuffixValue
- Length: term的后缀长度。
- SuffixValue: term的后缀值,之前提到按照term的大小顺序进行处理的,如果一批term具有相同的前缀并且这批term的个数超过25个,那么这批term会被处理为一个NodeBlock,并且SuffixValue只存储除去相同前缀的后缀部分。
-
TermStats: TermStats中又包含了两个元素DocFreq和TotalTermFreq
- DocFreq: DocFreq描述了包含当前term的文档个数。
- TotalTermFreq: TotalTermFreq描述了term在文档中出现的总数,实际存储了与DocFreq的差值,目的是尽可能压缩存储,即使用差值存储。
-
TermMetadata: TermMetadata中包含的信息比较多,所有和外部关联的信息都在这里,比如指向.doc,.pos , .pay文件的指针
- SingletonDocID: 如果只有一篇文档包含当前term,那么SingletonDocID被赋值这篇文档号,如果不止一篇文档包含当前term,那么SingletonDocID不会写入到.tim文件中。
- SkipOffset: SkipOffset用来描述当前term信息在.doc文件中 跳表信息的起始位置。
- DocStartFP: DocStartFP是当前term信息在.doc文件中的起始位置。
- PosStartFP: PosStartFP是当前term信息在.pos文件中的起始位置。
- LastPosBlockOffset: 如果term的词频大于BLOC_SIZE,即大于128个,那么在.pos文件中就会生成一个block,LastPosBlockOffset记录最后一个block结束位置,通过这个位置就能快速定位到term的剩余的position信息,并且这些position信息的个数肯定是不满128个.
- PayStartFP: payStartFP是当前term信息在.pay文件中的起始位置。
-
- InnerNode: 这个是为了保存哪些已经在FST中存在的term了,也就是不要后缀的term,结构类似OuterNode
- OuterNode : EntryCount, SuffixLength, SuffixLength, StatsLength, EntryCount, MetaLength, EntryCount
- FieldSummary:对tim文件中的field进行描述,相当于一些统计信息和元信息吧
- NumFields: NumFields描述了.tim文件中的有多少种域。
- FieldNumber: FieldNumber记录了当前域的编号,这个编号是唯一的,同时它是从0开始的递增值。数值越小,说明该域更早的被添加进了索引。
- NumTerms: NumTerms记录了当前域中有多少种term。
- RootCodeLength: RootCodeLength描述了当前域中的term的FST数据的长度。
- RootCodeValue: RootCodeValue描述了当前域中的term的FST数据。
- SumTotalTermFreq: sumTotalTermFreq描述了当前域中所有term在文档中的总词频。
- SumDocFreq: SumDocFreq描述了包含当前域中的所有term的文档数量。
- DocCount: DocCount描述了有多少篇文档包含了当前域。
- LongsSize: longsSize的值只能是1,2,3三种,1说明了当前域只存储了doc、frequency,2说明了存储了doc、frequency,positions,3说明存储了doc、frequency,positions、offset。
- MinTerm: 当前域中的最小的term。
- MaxTerm: 当前域中的最大的term。
2.4.2 XXX.doc
.doc文件存储的是倒排表中的每个term指向的DocIdList,当然,还有term在当前doc中出现的次数
主体结构为 <TermFreqs, SkipData?>TermCount
有一个数组的<TermFreqs, SkipData?>列表,大小为term的总数,所以这个文件还是有一定的大小的。
- TermFreqs: 存储了docId和term在该doc中出现的次数
- SkipData: 存储了docId的跳跃表:为什么需要跳跃表呢,假设这样的一个场景,我们需要合并两个term查出来的docIdList,假如两个docIdList都很长,但是相同的只有几个docId,这样的话如果是两个链表遍历求交的话就会很慢,但是使用跳跃表进行求交的话就会快很多了。
2.4.3 XXX.pos文件
.pos文件存储的主要是term在单个文档中的位置信息。有时候为了加快查询速速,也会存储一些payload信息和offset信息。
但是大部分场景实际上只需要positions信息就够了,就不会去加载.pay文件了,只需要加载.pos文件即可
主体结构是这样: TermCount
TermPositions:单个TermPositions存储了该term在每个doc的中的position信息,TermPositions之间按照term进行排序,TermPositions内部按照docId进行排序。
2.4.4 XXX.pay文件
.pay文件中主要存储的是payload信息和offset信息
主体结构是<TermPayloads, TermOffsets?> TermCount
TermPayloads: 单个TermPayloads存储了该term在每个doc中的payload信息,顺序同TermPositions
TermOffsets: 单个TermOffsets存储了该term在每个doc中的offset信息,顺序同TermPositions
2.4.5 在倒排中的查询逻辑
一个query进来,首先会被分词产生过个term,然后在.tip文件中查找对应field的FST(Finite State Transducer) 根据FST查找到的信息去.tim文件中查找具体的term信息,
在.tim中找到对应的TermStats,TermMetadata以后,可以从TermStats中拿到当前term的DF值(即出现了该term的文档数量),从TermMetadata中拿到对应的指向.doc中的跳跃表,docIdList的指针,指向.pos .pay的指针等。
假如多个term的关系是and,那么就会根据.doc中的skipList进行docIdList合并,或者有position的要求,还要判断term在各个doc中的position信息是否满足要求,然后进行打分,这个时候一般需要根据docId里面存储的TF(term frequency term在文档中出现的次数)
根据打分公司进行排序即可。
到这里lucene的倒排和正排信息基本上都梳理的差不多了。
2.5 docvalues信息的存储 .dvd .dvm
doc-values信息是为了能够根据doc-number快速的查找到该doc对应的某个属性,在实际应用中最多的场景用于提供给搜索结果一个排序规则。
lucene 使用.dvd .dvm存储doc-values信息,.dvd存储doc-values的data, .dvm存储doc-values的元数据。
lucene现有的doc-values种类有
BinaryDocValues
SortedSetDocValues
SortedDocValues
SortedNumericDocValues
NumericDocValues
实现的存储结构不同,但是支撑的功能基本上是一致的
2.6 多维数值类信息point的存储 .dim .dii
从Lucene6.0开始出现点数据(Point Value)的概念,通过将多维度的点数据生成KD-tree结构,来实现快速的单维度的范围查询(比如 IntPoint.newRangeQuery)以及N dimesional shape intersection filtering。
索引文件.dim中的数据结构由一系列的block组成,在内存中展现为一颗满二叉树(单维度可能不是,这块内容会在介绍数值类型的范围查询时候介绍),并且叶子节点描述了所有的点数据。
单个维度的数值查询一般比较容易实现,比如使用二叉搜索树(BST),红黑树(RBT),B-Tree等都可以实现。
但是多维度就没有这么容易实现了,但是基本也是基于树结构的基本形式来实现的。
对于多维数据的实现的树有
R-Tree(R树): R-tree
R±Tree(R+树): R+ tree
R*-Tree(R树): R_tree
K-D-Tree(K维树): k-d tree
K-D-B树(K维B树): K-D-B-tree
BKD-Tree
Segment-Tree(线段树): Segment tree
其实这些树具体的数据结构我也大部分都不了解,只是知道他们适用于多维空间查询
lucene 采用的是BKD-Tree来做多维空间查询索引
BKD-Tree实际上是对K-D-B树(K维B树)的一种优化实现,可以实现更加高效的修改操作。
lucene的point类型有
- IntPoint: int indexed for exact/range queries.
- LongPoint: long indexed for exact/range queries.
- FloatPoint: float indexed for exact/range queries.
- DoublePoint: double indexed for exact/range queries.
.dim 文件存储了每个filed的数据和索引index
.dii 文件存储了元信息,主要是存储了每个filed的index在.dim文件中的地址
待处理问题
标准化因子包括哪些,有doc级别的,有field级别的,是如何存储的
Field.Index 索引选项
Index.ANALYZED 进行分词和索引
Index.NOT_ANALYZED 进行索引,但是不进行分词
Index.ANALYZED_NOT_NORMS 进行分词但是不存储norms信息,这个norms信息包含了索引时间和权值等
Index.NOT_ANALYZED_NOT_NORMS 既部分词也不存储norms信息
Index.NO 不进行索引
4.0以后Field.Index废弃
StringField 不分词并索引的字段,搜索时整字段匹配
TextField 分词并索引
StoredField 仅保存,不索引
Lucene7之后,去除了Index时的boost加权操作。这里应该是指使用了BM25之后,boost的作用越来越小吧。
备忘
https://blog.csdn.net/jediael_lu/article/details/34434219
这篇博客介绍了lucene的查询过程,比较详细