关于SegmentInfos类的具体实现大致已经在文章 Lucene-2.2.0 源代码阅读学习(17) 中有了一个简单的印象,可以在文章 Lucene-2.2.0 源代码阅读学习(17) 中的末尾部分看到一点非常有用的总结。
然而,到底SegmentInfos类能够实现哪些功能,让我们能够亲眼看到它产生了哪些东西呢?我们可以从SegmentInfos类的一些重要的成员方法中开始追踪一些真实存在的东西到底去向如何,比如segmentName,以及version和gen等等,他们都是有值的,那么,这些值应该被怎样地输出呢,又输出到哪里去了呢,下面仔细学习研究。
先做个引子:
在前面的文章 Lucene-2.2.0 源代码阅读学习(4) 中,我们做了一个小例子,对指定目录中的一些txt文本进行索引,然后做了一个简单的检索关键字的测试。
就从这里说起,在文章 Lucene-2.2.0 源代码阅读学习(4) 中,没有学习到内在的机制,而只是为学习分词做了一个引导。现在,也不对如果构建Document和Field进行解释,因为这一块也非常地复杂,够学习一阵子了。但是,我们要看的就是,在这个过程中都有哪些产物,即产生了哪些文件,知道了建立索引过程中产生了哪些文件,有助于我们在SegmentInfos类中对一些与维护索引文件相关的信息的去向进行追踪。
在文章 Lucene-2.2.0 源代码阅读学习(4) 中,运行测试程序后,在指定的索引文件目录(测试程序中指定为E:\Lucene\myindex)生成了很多文件(因为指定的要建立索引的txt文件具有一定的量,如果只有一两个txt文本文件,而且它们的大小都不是很大,则产生的索引文件数量会很少,可能只有3个或者4个),如图所示:
图中segments.gen和segments_f文件都不陌生了。看看他们是怎么生成的,又保存有哪些信息。
segments_N文件和segments.gen文件的生成
1、先看segment_N文件:
在将文件写入到磁盘的目录中之前,一般来说首先要创建一个输出流。在SegmentInfos类中,有一个成员方法write(),在该方法中:
IndexOutput output = directory.createOutput(segmentFileName);
根据指定的索引段文件的名称segmentFileName,创建一个指向索引目录directory的输出流output。
关于这个segmentFileName,是要先从指定的索引目录中读取出来的,在write()方法中第一行代码中就获取了这个索引段的文件名:
String segmentFileName = getNextSegmentFileName();
这里的 getNextSegmentFileName()方法是SegmentInfos类的一个成员方法,了解它有助于我们继续追踪:
public String getNextSegmentFileName() { long nextGeneration;
if (generation == -1) { nextGeneration = 1; } else { nextGeneration = generation+1; } return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",nextGeneration);}
该方法返回的就是我们将要处理的一个索引段文件的名称。最后一句return返回,调用了IndexFileNames类的fileNameFromGeneration()方法,它也很重要,因为要使用文件名称作为参数获取索引目录下的维护索引的文件都要从这里获得。
关注一下IndexFileNames类的实现:
package org.apache.lucene.index;
// 该类主要是对索引文件的命名进行管理final class IndexFileNames {
/** 索引段文件名 */static final String SEGMENTS = "segments";
/** generation reference文件名*/static final String SEGMENTS_GEN = "segments.gen";/** Name of the index deletable file (only used in pre-lockless indices) */static final String DELETABLE = "deletable"; /** norms file的扩展名 */static final String NORMS_EXTENSION = "nrm";
/** 复合文件扩展名*/static final String COMPOUND_FILE_EXTENSION = "cfs";
/** 删除文件扩展名 */static final String DELETES_EXTENSION = "del";
/** plain norms扩展名 */static final String PLAIN_NORMS_EXTENSION = "f";
/** Extension of separate norms */static final String SEPARATE_NORMS_EXTENSION = "s";
/** * Lucene的全部索引文件扩展名列表 */static final String INDEX_EXTENSIONS[] = new String[] { "cfs", "fnm", "fdx", "fdt", "tii", "tis", "frq", "prx", "del", "tvx", "tvd", "tvf", "gen", "nrm" };
/** 被添加到复合索引文件上的文件扩展名 */static final String[] INDEX_EXTENSIONS_IN_COMPOUND_FILE = new String[] { "fnm", "fdx", "fdt", "tii", "tis", "frq", "prx", "tvx", "tvd", "tvf", "nrm" };/** old-style索引文件扩展名 */static final String COMPOUND_EXTENSIONS[] = new String[] { "fnm", "frq", "prx", "fdx", "fdt", "tii", "tis"};/** 词条向量支持的文件扩展名 */static final String VECTOR_EXTENSIONS[] = new String[] { "tvx", "tvd", "tvf"};
/** * 根据基础文件名(不包括后缀,比如segments.gen文件,segments部分为基础文件名)、扩展名和generarion计算指定文件的完整文件名 */static final String fileNameFromGeneration(String base, String extension, long gen) { if (gen == SegmentInfo.NO) { return null; } else if (gen == SegmentInfo.WITHOUT_GEN) { return base + extension; } else { return base + "_" + Long.toString(gen, Character.MAX_RADIX) + extension; }}}
fileNameFromGeneration实现的功能:根据传进来的base(比如segments)、扩展名、gen来生成一个新的文件名,并返回。
在SegmentInfos类中getNextSegmentFileName() 方法调用了fileNameFromGeneration,如下所示:
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",nextGeneration);
第一个参数值为"segments",第二个参数值为"",第三个是一个gen(它是一个Long型的数字),如果假设这里的nextGeneration=5,调用fileNameFromGeneration()方法后,返回的是一个索引段文件名:segments_5。
这样,就可以根据生成的segments_N文件名,创建一个输出流,将需要的信息写入到该文件中。
2、再看segments.gen文件:
仔细观察,其实SegmentInfos类的write方法就是对segments_N文件和segments.gen文件进行写入操作的。
在写入segments_N文件以后,紧接着就是处理segments.gen文件:
output = directory.createOutput(IndexFileNames.SEGMENTS_GEN);
因为在一个索引目录下,属于同一个索引段的索引文件就是通过一个segments.gen文件来维护的,segments.gen文件的文件名自然不需要那么麻烦地去获取。直接使用IndexFileNames.SEGMENTS_GEN = segments.gen作为参数构造一个输出流,进行输出,写入到索引目录中即可。
关于segments_N文件和segments.gen文件保存的信息
同样在SegmentInfos类的write方法中能够看到,这两个文件中都加入了哪些信息。
■ 关于segments_N文件
如下所示:
output.writeInt(CURRENT_FORMAT); // write FORMAT output.writeLong(++version); // every write changes the index output.writeInt(counter); // write counter output.writeInt(size()); // write infos for (int i = 0; i < size(); i++) { info(i).write(output); }
(1) CURRENT_FORMAT
其中,CURRENT_FORMAT是SegmentInfos类的一个成员:
/* This must always point to the most recent file format. */private static final int CURRENT_FORMAT = FORMAT_SINGLE_NORM_FILE;
上面CURRENT_FORMAT的值就是FORMAT_SINGLE_NORM_FILE的值-3:
/** This format adds a "hasSingleNormFile" flag into each segment info. * See <a href="http://issues.apache.org/jira/browse/LUCENE-756">LUCENE-756</a> for details. */public static final int FORMAT_SINGLE_NORM_FILE = -3;
(2) version
version是SegmentInfos类的一个成员,版本号通过系统来获取:
/** * counts how often the index has been changed by adding or deleting docs. * starting with the current time in milliseconds forces to create unique version numbers. */private long version = System.currentTimeMillis();
(3) counter
用于为当前待写入索引目录的索引段文件命名的,即segments_N中的N将使用counter替换。
counter也是SegmentInfos类的一个成员,初始化是为0:
public int counter = 0;
在read()方法中,使用从索引目录中已经存在的segment_N中读取的出format的值,然后根据format的值来指派counter的值,如下所示:
int format = input.readInt(); if(format < 0){ // file contains explicit format info // check that it is a format we can understand if (format < CURRENT_FORMAT) throw new CorruptIndexException("Unknown format version: " + format); version = input.readLong(); // read version counter = input.readInt(); // read counter } else{ // file is in old format without explicit format info counter = format; }
(4) size()
size()就是SegmentInfos的大小,SegmentInfos中含有多个SegmentInfo,注意:SegmentInfos类继承自Vector。
(5) info(i)
info()方法的定义如下所示:
public final SegmentInfo info(int i) { return (SegmentInfo) elementAt(i);}
可见,SegmentInfos是SegmentInfo的一个容器,它只把当前这个索引目录中的SegmentInfo装进去,以便对他们管理维护。
这里,info(i).write(output);又调用了SegmentInfo类的write()方法,来向索引输出流output中加入信息。SegmentInfo类的write()方法如下所示:
/** * Save this segment's info. */void write(IndexOutput output) throws IOException { output.writeString(name); output.writeInt(docCount); output.writeLong(delGen); output.writeByte((byte) (hasSingleNormFile ? 1:0)); if (normGen == null) { output.writeInt(NO); } else { output.writeInt(normGen.length); for(int j = 0; j < normGen.length; j++) { output.writeLong(normGen[j]); } } output.writeByte(isCompoundFile);}
从上可以看到,还写入了SegmentInfo的具体信息:name、docCount、delGen、(byte)(hasSingleNormFile ? 1:0)、NO/normGen.length、normGen[j]、isCompoundFile。
■ 关于segments.gen文件
通过SegmentInfos类的write()方法可以看到:
output.writeInt(FORMAT_LOCKLESS); output.writeLong(generation); output.writeLong(generation);
segments.gen文件中只是写入了两个字段的信息:FORMAT_LOCKLESS和generation。
因为segments.gen文件管理的就是segments_N文件中的N的值,与该文件相关就只有一个generation,和一个用于判断是否是无锁提交的信息:
/** This format adds details used for lockless commits. It differs * slightly from the previous format in that file names * are never re-used (write once). Instead, each file is * written to the next generation. For example, * segments_1, segments_2, etc. This allows us to not use * a commit lock. See <a * href="http://lucene.apache.org/java/docs/fileformats.html">file * formats</a> for details. */public static final int FORMAT_LOCKLESS = -2;
最后,总结一下:
现在知道了segments_N文件和segment.gen文件都记录了什么内容。
其中,segments_N文件与SegmentInfo类的关系十分密切,接下来要学习SegmentInfo类了。
