索引是什么?索引是一种数据存储和组织结构。
逆常人之思维,lucene索引采用倒排文件索引构造索引系统。具体实现原理举例说明:
假设有3篇文章,file1,file2,file3,文件内容如下:
file1 (单词1,单词2,单词3,单词4....)
file2 (单词a,单词b,单词c,单词d....)
file3 (单词1,单词a,单词3,单词d....)
建立的倒排索引就是这个样子:
单词1 (file1,file3)
单词2 (file1)
单词3 (file1,file3)
单词a (file2, file3)
....
这就是倒排索引,很简单吧。
补充:
“倒排”是与“正排”相对的概念,“正排”如同我们通常的阅读过程:先看到文档后看到单词,而倒排则像我们翻看《辞源》的过程:从单词找到文档。所以正排与倒排可以从文档与单词的指向关系上做区分:正排是文档指向单词,倒排是单词指向文档。目前几乎所有的商业搜索引擎都采用倒排索引方式,开源Lucene也不例外,如图3.1所示。
注:一般情况下,一个文件要建立索引,就先把它变成纯文本的格式。
索引文件的逻辑视图
在lucene 中有索引块的概念,每个索引块包含了一定数目的文档。我们能够对单独的索引块进行检索。图 2 显示了 lucene 索引结构的逻辑视图。索引块的个数由索引的文档的总数以及每个索引块所能包含的最大文档数来决定。
图2:索引文件的逻辑视图
lucene 中的关键索引文件
下面的部分将会分析lucene中的主要的索引文件,可能分析有些索引文件的时候没有包含文件的所有的字段,但不会影响到对索引文件的理解。
1.索引块文件
这个文件包含了索引中的索引块信息,这个文件包含了每个索引块的名字以及大小等信息。表 2 显示了这个文件的结构信息。
表2:索引块文件结构
2.域信息文件
我们知道,索引中的文档由一个或者多个域组成,这个文件包含了每个索引块中的域的信息。表 3 显示了这个文件的结构。
表3:域信息文件结构
3.索引项信息文件
这是索引文件里面最核心的一个文件,它存储了所有的索引项的值以及相关信息,并且以索引项来排序。表 4 显示了这个文件的结构。
表4:索引项信息文件结构
4.频率文件
这个文件包含了包含索引项的文档的列表,以及索引项在每个文档中出现的频率信息。如果lucene在索引项信息文件中发现有索引项和搜索词相匹配。那么 lucene 就会在频率文件中找有哪些文件包含了该索引项。表5显示了这个文件的一个大致的结构,并没有包含这个文件的所有字段。
表5:频率文件的结构
5.位置文件
这个文件包含了索引项在每个文档中出现的位置信息,你可以利用这些信息来参与对索引结果的排序。表 6 显示了这个文件的结构
表6:位置文件的结构
从上所述:
索引结构可以分为索引、段索引、文档索引、域索引、项索引几个层次。lucene每个索引的结构有一个或多个段组成(索引在内存和磁盘上具有相同的逻辑结构);每个段包含一个或多个文档(索引段相当于子索引);每个文档管理一个或多个域;每个域有一个或多个项组成;每个索引项是一个索引数据(索引项是索引管理的最小的单位)。
在Lucene中采用段索引的生成方式,如图3.2所示。合并阈值(mergeFactor)影响着内存与硬盘中索引文件的个数,每添加一个document将生成单一段索引(single segment index)被内存持有,当单一段索引的个数超过合并阈值时,就会通过merge(合并)的过程将单一段索引合并为段索引(segment index)。段索引的个数超过合并阈值时也会触发合并过程,合并为一个索引。单一段索引,段索引,索引文件格式(file format)是相同的,三者只是规模不同。
图3.2 增量索引过程
下面来看一下IndexWriter()是如何实现的了。
IndexWriter()的实例化过程(包含初始化);
- private void init(Directory d, Analyzer a, final boolean create, boolean closeDir)
- throws IOException {
- this.closeDir = closeDir;
- directory = d;
- analyzer = a;
- if (create) {
- //清理遗留下来的写入锁:
- directory.clearLock(IndexWriter.WRITE_LOCK_NAME);//IndexWriter.WRITE_LOCK_NAME="write.lock".
- }
- Lock writeLock = directory.makeLock(IndexWriter.WRITE_LOCK_NAME);
- if (!writeLock.obtain(writeLockTimeout)) // obtain write lock
- throw new IOException("Index locked for write: " + writeLock);
- this.writeLock = writeLock; // save it
- try {
- if (create) {
- try {
- segmentInfos.read(directory); @1
- segmentInfos.clear();
- } catch (IOException e) {
- }
- segmentInfos.write(directory); @2
- } else {
- segmentInfos.read(directory);
- }
- /**
- * 创建一个删除器用以记录哪些文件可以被删除。
- */
- deleter = new IndexFileDeleter(segmentInfos, directory);
- deleter.setInfoStream(infoStream);
- deleter.findDeletableFiles();
- deleter.deleteFiles();
- } catch (IOException e) {
- this.writeLock.release();
- this.writeLock = null;
- throw e;
- }
- }
@1:表示从索引目录中读取旧段号,然后把后来创建的段号与旧段号连接起来;
@2:在索引目录中写入segment段信息文件。
注:下一篇将介绍SegmentInfos的实现。
IndexWriter中几个重要的属性:
public final static int DEFAULT_MERGE_FACTOR = 10;//对内存和磁盘中的索引都有作用。
public final static int DEFAULT_MAX_BUFFERED_DOCS = 10;//minMergeDocs默认值为10,控制内存中索引,不影响磁盘
public final static int DEFAULT_MAX_MERGE_DOCS = Integer.MAX_VALUE;//maxMergeDocs默认值2147483647,合并后的document数目如果超出此值就会保持不变。
以上三个全部都是合并阀值,只是作用区域不同。
public final static int DEFAULT_MAX_BUFFERED_DELETE_TERMS = 1000;
public final static int DEFAULT_MAX_FIELD_LENGTH = 10000;
maxFieldLength指一个可以为多少个Field建立索引,在Lucene中指定的默认的值为10000
public final static int DEFAULT_TERM_INDEX_INTERVAL = 128;
termIndexInterval是词条索引区间,与在内存中处理词条相关。如果该值设置的越大,则导致IndexReader使用的内存空间就越小,也就减慢了词条Term的随机存储速度。该参数决定了每次查询要求计算词条Term的数量。在Lucene中的默认值为128。
private Similarity similarity = Similarity.getDefault(); // how to normalize
DefaultSimilarity类继承自Similarity抽象类,该类是用来处理有关“相似性”的,与检索密切相关,其实就是对一些数据在运算过程中可能涉及到数据位数的舍入与进位。具体地,Similarity类的定义可查看org.apache.lucene.search.Similarity。
【小知识】上面的minMergeDocs与maxMergeDocs同mergeFactor都是合并阈值,只是作用区域不同,minMergeDocs控制内存中的索引段数,超过该阈值时就会合并,但不影响磁盘上的索引段。maxMergeDocs的数值决定合并后段中包含的document数目,如果要超过该值,就不会合并而维持原有段不变。mergeFactor对内存与磁盘中的段都起作用,内存与磁盘中段索引个数总和超过该阈值就合并索引。
写入索引时,无论是IndexWriter、DocumentWriter、FieldWriter、TermInfosWriter,都含有addDocument()方法。IndexWriter写入时,通常每一个document添加时都作为单一段索引添加到内存中,达到合并阈值时才会合并索引并保存到磁盘中,代码如下:
- public void addDocument(Document doc, Analyzer analyzer) throws IOException {
- /**写入内存*/
- SegmentInfo newSegmentInfo = buildSingleDocSegment(doc, analyzer); @1
- synchronized (this) {
- ramSegmentInfos.addElement(newSegmentInfo); //向SegmentInfos中添加一个SegmentInfo
- maybeFlushRamSegments(); /**将内存中的索引合并后保存到硬盘*/ @2
- }
- }
其实,在执行@1前,首先首先调用了ensureOpen()方法,该方法根据一个closed标识来保证当前实例化的IndexWriter是否处于打开状态。关于closed的标识的设置,当一个IndexWriter索引器实例化的时候,该值就已经初始化为false了,表示索引器writer已经处于打开状态。如果想要关闭writer,直接调用IndexWriter类的close()方法,可以设置closed的标识为true,表示索引器被关闭了,不能进行有关建立索引的操作了。
@1 添加一个document产生一个singleDocSegmentIndex的过程,该索引只写入到内存中。
@2 将内存中的索引合并后保存到硬盘。该方法在IndexWriter类中定义用来监测当前缓冲区,及时将缓冲区中的数据flush到索引目录中。其中可能存在索引段合并的问题。
@1,@2涉及了在内存中建立索引和合并到磁盘两块重量级的知识,在后续篇慢慢研读。……