Lucene系列(7)——索引存储文件介绍
注:本文基于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 segments_N
上面这两个文件是针对当前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 1.0, 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///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的产品即可。