注:本文基于Lucene 8.2.0 版本。
本文讨论Lucene底层索引数据存储。对于绝大数多人来说了解Lucene的上层概念足矣,无需关注底层的存储格式。所以本文虽然是讨论底层数据存储的,但也不会深入到具体的数据结构、压缩算法等。如果你有兴趣,可以查看对应版本的Lucene Java doc(8.2.0版本的链接已经附在文末)。另外,如果你对index、document、term、segment、term vector、norm等上层概念还不清楚,建议先阅读该系列文章的前几篇。
索引文件格式
不论是Solr还是ES,底层index的存储都是完全使用Lucene原生的方式,没有做改变,所以本文会以ES为例来介绍。需要注意的是Lucene的index在ES中称为shard,本文中提到的index都指的是Lucene的index,即ES中的shard。先来看一个某个index的数据目录:
可以看到一个索引包含了很多文件,似乎很复杂。但仔细观察之后会发现乱中似乎又有些规律:很多文件前缀一样,只是后缀不同,比如有很多_3c
开头的文件。回想一下之前文章的介绍,index由若干个segment组成,而一个index目录下前缀相同表示这些文件都属于同一个segment。
那各种各样的后缀又代表什么含义呢?Lucene存储segment时有两种方式:
- multifile格式。该模式下会产生很多文件,不同的文件存储不同的信息,其弊端是读取index时需要打开很多文件,可能造成文件描述符超出系统限制。
- compound格式。一般简写为CFS(Compound File System),该模式下会将很多小文件合并成一个大文件,以减少文件描述符的使用。
我们先来介绍multifile格式下的各个文件:
write.lock
:每个index目录都会有一个该文件,用于防止多个IndexWriter同时写一个文件。segments_N
:该文件记录index所有segment的相关信息,比如该索引包含了哪些segment。IndexWriter每次commit都会生成一个(N的值会递增),新文件生成后旧文件就会删除。所以也说该文件用于保存commit point信息。
上面这两个文件是针对当前index的,所以每个index目录下都只会有1个(segments_N可能因为旧的没有及时删除临时存在两个)。下面介绍的文件都是针对segment的,每个segment就会有1个。
.si
:Segment Info的缩写,用于记录segment的一些元数据信息。.fnm
:Fields,用于记录fields设置类信息,比如字段的index option信息,是否存储了norm信息、DocValue等。.fdt
:Field Data,存储字段信息。当通过StoredField
或者Field.Store.YES
指定存储原始field数据时,这些数据就会存储在该文件中。.fdx
:Field Index,.fdt
文件的索引/指针。通过该文件可以快速从.fdt
文件中读取field数据。.doc
:Frequencies,存储了一个documents列表,以及它们的term frequency信息。.pos
:Positions,和.doc
类似,但保存的是position信息。.pay
:Payloads,和.doc
类似,但保存的是payloads和offset信息。.tim
:Term Dictionary,存储所有文档analyze出来的term信息。同时还包含term对应的document number以及若干指向.doc
,.pos
,.pay
的指针,从而可以快速获取term的term vector信息。。.tip
:Term Index,该文件保存了Term Dictionary的索引信息,使得可以对Term Dictionary进行随机访问。.nvd
,.nvm
:Norms,这两个都是用来存储Norms信息的,前者用于存储norms的数据,后者用于存储norms的元数据。.dvd
,.dvm
:Per-Document Values,这两个都是用来存储DocValues信息的,前者用于数据,后者用于存储元数据。.tvd
:Term Vector Data,用于存储term vector数据。.tvx
:Term Vector Index,用于存储Term Vector Data的索引数据。.liv
:Live Documents,用于记录segment中哪些documents没有被删除。一般不存在该文件,表示segment内的所有document都是live的。如果有documents被删除,就会产生该文件。以前是使用一个.del
后缀的文件来记录被删除的documents,现在改为使用该文件了。.dim
,.dii
:Point values,这两个文件用于记录indexing的Point信息,前者保存数据,后者保存索引/指针,用于快速访问前者。
上面介绍了很多文件类型,实际中不一定都有,如果indexing阶段不保存字段的term vector信息,那存储term vector的相关文件可能就不存在。如果一个index的segment非常多,那将会有非常非常多的文件,检索时,这些文件都是要打开的,很可能会造成文件描述符不够用,所以Lucene引入了前面介绍的CFS格式,它把上述每个segment的众多文件做了一个合并压缩(.liv
和.si
没有被合并,依旧单独写文件),最终形成了两个新文件:.cfs
和.cfe
,前者用于保存数据,后者保存了前者的一个Entry Table,用于快速访问。所以,如果使用CFS的话,最终对于每个segment,最多就只存在.cfs
, .cfe
, .si
, .liv
4个文件了。Lucene从1.4版本开始,默认使用CFS来保存segment数据,但开发者仍然可以选择使用multifile格式。一般来说,对于小的segment使用CFS,对于大的segment,使用multifile格式。比如Lucene的org.apache.lucene.index.MergePolicy
构造函数中就提供merge时在哪些条件下使用CFS:
/**
* Default ratio for compound file system usage. Set to <tt>1.0</tt>, always use
* compound file system.
*/
protected static final double DEFAULT_NO_CFS_RATIO = 1.0;
/**
* Default max segment size in order to use compound file system. Set to {@link Long#MAX_VALUE}.
*/
protected static final long DEFAULT_MAX_CFS_SEGMENT_SIZE = Long.MAX_VALUE;
/** If the size of the merge segment exceeds this ratio of
* the total index size then it will remain in
* non-compound format */
protected double noCFSRatio = DEFAULT_NO_CFS_RATIO;
/** If the size of the merged segment exceeds
* this value then it will not use compound file format. */
protected long maxCFSSegmentSize = DEFAULT_MAX_CFS_SEGMENT_SIZE;
/**
* Creates a new merge policy instance.
*/
public MergePolicy() {
this(DEFAULT_NO_CFS_RATIO, DEFAULT_MAX_CFS_SEGMENT_SIZE);
}
/**
* Creates a new merge policy instance with default settings for noCFSRatio
* and maxCFSSegmentSize. This ctor should be used by subclasses using different
* defaults than the {@link MergePolicy}
*/
protected MergePolicy(double defaultNoCFSRatio, long defaultMaxCFSSegmentSize) {
this.noCFSRatio = defaultNoCFSRatio;
this.maxCFSSegmentSize = defaultMaxCFSSegmentSize;
}
接下来让我们使用ES做一些操作来具体感受一下。
一些例子
首先在ES中创建一个索引:
PUT nyc-test
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"refresh_interval": -1
}
}
这里设置1个shard,0个副本,并且将refresh_interval设置为-1,表示不自动刷新。创建完之后就可以在es的数据目录找到该索引,es的后台索引的目录结构为:<数据目录>/nodes/0/indices/<索引UUID>/<shard>/index
,这里的shard就是Lucene的index。我们看下刚创建的index的目录:
-> % ll
总用量 4.0K
-rw-rw-r-- 1 allan allan 230 10月 11 21:45 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 21:45 write.lock
可以看到,现在还没有写入任何数据,所以只有index级别的segments_N
和write.lock
文件,没有segment级别的文件。写入1条数据并查看索引目录的变化:
PUT nyc-test/doc/1
{
"name": "Jack"
}
# 查看索引目录
-> % ll
总用量 4.0K
-rw-rw-r-- 1 allan allan 0 10月 11 22:20 _0.fdt
-rw-rw-r-- 1 allan allan 0 10月 11 22:20 _0.fdx
-rw-rw-r-- 1 allan allan 230 10月 11 22:19 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
可以看到出现了1个segment的数据,因为ES把数据缓存在内存里面,所以文件大小为0。然后再写入1条数据,并查看目录变化:
PUT nyc-test/doc/2
{
"name": "Allan"
}
# 查看目录
-> % ll
总用量 4.0K
-rw-rw-r-- 1 allan allan 0 10月 11 22:20 _0.fdt
-rw-rw-r-- 1 allan allan 0 10月 11 22:20 _0.fdx
-rw-rw-r-- 1 allan allan 230 10月 11 22:19 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
因为ES缓存机制的原因,目录没有变化。显式的refresh一下,让内存中的数据落地:
POST nyc-test/_refresh
-> % ll
总用量 16K
-rw-rw-r-- 1 allan allan 405 10月 11 22:22 _0.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:22 _0.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:22 _0.si
-rw-rw-r-- 1 allan allan 230 10月 11 22:19 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
ES的refresh操作会将内存中的数据写入到一个新的segment中,所以refresh之后写入的两条数据形成了一个segment,并且使用CFS格式存储了。然后再插入1条数据,接着update这条数据:
PUT nyc-test/doc/3
{
"name": "Patric"
}
# 查看
-> % ll
总用量 16K
-rw-rw-r-- 1 allan allan 405 10月 11 22:22 _0.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:22 _0.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:22 _0.si
-rw-rw-r-- 1 allan allan 0 10月 11 22:23 _1.fdt
-rw-rw-r-- 1 allan allan 0 10月 11 22:23 _1.fdx
-rw-rw-r-- 1 allan allan 230 10月 11 22:19 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
# 更新数据
PUT nyc-test/doc/3?refresh=true
{
"name": "James"
}
# 查看
-> % ll
总用量 32K
-rw-rw-r-- 1 allan allan 405 10月 11 22:22 _0.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:22 _0.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:22 _0.si
-rw-rw-r-- 1 allan allan 67 10月 11 22:24 _1_1.liv
-rw-rw-r-- 1 allan allan 405 10月 11 22:24 _1.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:24 _1.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:24 _1.si
-rw-rw-r-- 1 allan allan 230 10月 11 22:19 segments_2
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
可以看到,再次refresh的时候又形成了一个新的segment,并且因为update,导致删掉了1条document,所以产生了一个.liv
文件。但前面的这些流程中,segments_N文件也就是segments_2一直没有变过,这是因为一直没有Lucene概念中的commit操作发生过。ES的flush操作对应的是Lucene的commit,我们触发一次Lucene commit看下变化:
# 触发Lucene commit
POST nyc-test/_flush?wait_if_ongoing
# 查看目录
-> % ll
总用量 32K
-rw-rw-r-- 1 allan allan 405 10月 11 22:22 _0.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:22 _0.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:22 _0.si
-rw-rw-r-- 1 allan allan 67 10月 11 22:24 _1_1.liv
-rw-rw-r-- 1 allan allan 405 10月 11 22:24 _1.cfe
-rw-rw-r-- 1 allan allan 2.5K 10月 11 22:24 _1.cfs
-rw-rw-r-- 1 allan allan 393 10月 11 22:24 _1.si
-rw-rw-r-- 1 allan allan 361 10月 11 22:25 segments_3
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
# 查看segment信息
GET _cat/segments/nyc-test?v
index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound
nyc-test 0 p 10.8.4.42 _0 0 2 0 3.2kb 1184 true true 7.4.0 true
nyc-test 0 p 10.8.4.42 _1 1 1 2 3.2kb 1184 true true 7.4.0 true
触发Lucene commit之后,可以看到segments_2变成了segments_3。然后调用_cat
接口查看索引的segment信息也能看到目前有2个segment,而且都已经commit过了,并且compound是true,表示是CFS格式存储的。当然Lucene的segment是可以合并的。我们通过ES的forcemerge接口进行合并,并且将所有segment合并成1个segment,forcemerge的时候会自动调用flush,即会触发Lucene commit:
POST nyc-test/_forcemerge?max_num_segments=1
-> % ll
总用量 60K
-rw-rw-r-- 1 allan allan 69 10月 11 22:27 _2.dii
-rw-rw-r-- 1 allan allan 123 10月 11 22:27 _2.dim
-rw-rw-r-- 1 allan allan 142 10月 11 22:27 _2.fdt
-rw-rw-r-- 1 allan allan 83 10月 11 22:27 _2.fdx
-rw-rw-r-- 1 allan allan 945 10月 11 22:27 _2.fnm
-rw-rw-r-- 1 allan allan 110 10月 11 22:27 _2_Lucene50_0.doc
-rw-rw-r-- 1 allan allan 80 10月 11 22:27 _2_Lucene50_0.pos
-rw-rw-r-- 1 allan allan 287 10月 11 22:27 _2_Lucene50_0.tim
-rw-rw-r-- 1 allan allan 145 10月 11 22:27 _2_Lucene50_0.tip
-rw-rw-r-- 1 allan allan 100 10月 11 22:27 _2_Lucene70_0.dvd
-rw-rw-r-- 1 allan allan 469 10月 11 22:27 _2_Lucene70_0.dvm
-rw-rw-r-- 1 allan allan 59 10月 11 22:27 _2.nvd
-rw-rw-r-- 1 allan allan 100 10月 11 22:27 _2.nvm
-rw-rw-r-- 1 allan allan 572 10月 11 22:27 _2.si
-rw-rw-r-- 1 allan allan 296 10月 11 22:27 segments_4
-rw-rw-r-- 1 allan allan 0 10月 11 22:19 write.lock
GET _cat/segments/nyc-test?v
index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound
nyc-test 0 p 10.8.4.42 _2 2 3 0 3.2kb 1224 true true 7.4.0 false
可以看到,force merge之后只有一个segment了,并且使用了multifile格式存储,而不是compound。当然这并非Lucene的机制,而是ES自己的设计。
最后用图总结一下:
本文就介绍到这里,对于绝大多数使用者来说,只需要知道Lucene索引后台存储的组织逻辑和层次,以更好的使用Lucene及基于Lucene的产品即可。
评论已关闭