29

Lucene系列(7)——索引存储文件介绍

 5 years ago
source link: https://www.tuicool.com/articles/rAzUn2J
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

注:本文基于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的数据目录:

eyYFFbU.png!web

可以看到一个索引包含了很多文件,似乎很复杂。但仔细观察之后会发现乱中似乎又有些规律:很多文件前缀一样,只是后缀不同,比如有很多 _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个。

  • .siSegment Info 的缩写,用于记录segment的一些元数据信息。
  • .fnmFields ,用于记录fields设置类信息,比如字段的index option信息,是否存储了norm信息、DocValue等。
  • .fdtField Data ,存储字段信息。当通过 StoredField 或者 Field.Store.YES 指定存储原始field数据时,这些数据就会存储在该文件中。
  • .fdxField Index.fdt 文件的索引/指针。通过该文件可以快速从 .fdt 文件中读取field数据。
  • .docFrequencies ,存储了一个documents列表,以及它们的term frequency信息。
  • .posPositions ,和 .doc 类似,但保存的是position信息。
  • .pay :Payloads ,和 .doc 类似,但保存的是payloads和offset信息。
  • .timTerm Dictionary ,存储所有文档analyze出来的term信息。同时还包含term对应的document number以及若干指向 .doc , .pos , .pay 的指针,从而可以快速获取term的term vector信息。。
  • .tipTerm Index ,该文件保存了Term Dictionary的索引信息,使得可以对Term Dictionary进行随机访问。
  • .nvd , .nvmNorms ,这两个都是用来存储Norms信息的,前者用于存储norms的数据,后者用于存储norms的元数据。
  • .dvd , .dvmPer-Document Values ,这两个都是用来存储DocValues信息的,前者用于数据,后者用于存储元数据。
  • .tvdTerm Vector Data ,用于存储term vector数据。
  • .tvxTerm Vector Index ,用于存储Term Vector Data的索引数据。
  • .livLive Documents ,用于记录segment中哪些documents没有被删除。一般不存在该文件,表示segment内的所有document都是live的。如果有documents被删除,就会产生该文件。以前是使用一个 .del 后缀的文件来记录被删除的documents,现在改为使用该文件了。
  • .dim , .diiPoint 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_Nwrite.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自己的设计。

最后用图总结一下:

6biAVjQ.png!web

本文就介绍到这里,对于绝大多数使用者来说,只需要知道Lucene索引后台存储的组织逻辑和层次,以更好的使用Lucene及基于Lucene的产品即可。

References


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK