Lucene 6.0 实战:索引热备份及恢复

标签: IT技术 lucene | 发表时间:2016-11-24 11:28 | 作者:codepub
出处:http://blog.jobbole.com

索引备份的几个关键问题

  • 最简单的备份方式是关闭IndexWriter,然后逐一拷贝索引文件,但是如果索引比较大,那么这种备份操作会持续较长时间,而在备份期间,程序无法对索引文件进行修改,很多搜索程序是不能接受索引操作期间如此长时间停顿的
  • 那么不关闭IndexWriter又如何呢?这样也不行,因为在拷贝索引期间,如果索引文件发生变化,会导致备份的索引文件损坏
  • 另外一个问题就是如果原索引文件损坏的话,再备份它也毫无意义,所以一定要备份的是最后一次成功commit之后的索引文件
  • 每次在备份之前,如果程序将要覆盖上一个备份,需要先删除备份中未出现在当前快照中的文件,因为这些文件已经不会被当前索引引用了;如果每次都更改备份路径的话,那么就直接拷贝即可

索引热备份

从Lucene 2.3版本开始,Lucene提供了一个热备策略,就是SnapshotDeletionPolicy,这样就能在不关闭IndexWriter的情况下,对程序最近一次索引修改提交操作时的文件引用进行备份,从而能建立一个连续的索引备份镜像。那么你也许会有疑问,在备份期间,索引出现变化怎么办呢?

这就是SnapshotDeletionPolicy的牛逼之处,在使用SnapshotDeletionPolicy.snapshot()获取快照之后,索引更新提交时刻的所有文件引用都不会被IndexWriter删除,只要IndexWriter并未关闭,即使IndexWriter在进行更新、优化操作等也不会删除这些文件。如果说索引拷贝过程耗时较长也不会出现问题,因为被拷贝的文件时索引快照,在快照的有效期,其引用的文件会一直存在于磁盘上。

所以在备份期间,索引会比通常情况下占用更大的磁盘空间,当索引备份完成后,可以调用SnapshotDeletionPolicy.release (IndexCommit commit) 释放指定的某次提交,以便让IndexWriter删除这些已被关闭或下次将要更新的文件。

需要注意的是,Lucene对索引文件的写入操作是一次性完成的。这意味着你可以简单通过文件名比对来完成对索引的增量备份备份,你不必查看每个文件的内容,也不必查看该文件上次被修改的时间戳,因为一旦程序从快照中完成文件写入和引用操作,这些文件就不会改变了。

segments.gen文件在每次程序提交索引更新时都会被重写,因此备份模块必须要经常备份该文件,但是在Lucene 6.0中注意segments.gen已经从Lucene索引文件格式中移除,所以无需单独考虑segments.gen的备份策略了。在备份期间,write.lock文件不用拷贝。

SnapshotDeletionPolicy类有两个使用限制

  • 该类在同一时刻只保留一个可用的索引快照,当然你也可以解除该限制,方法是通过建立对应的删除策略来同时保留多个索引快照
  • 当前快照不会保存到硬盘,这意味着你关闭旧的IndexWriter并打开一个新的IndexWriter,快照将会被删除,因此在备份结束前是不能关闭IndexWriter的,否则也会报org.apache.lucene.store.AlreadyClosedException: this IndexWriter is closed异常。不过该限制也是很容易解除的:你可以将当前快照存储到磁盘上,然后在打开新的IndexWriter时将该快照保护起来,这样就能在关闭旧的IndexWriter和打开新IndexWriter时继续进行备份操作

索引热备份解决方案

import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.FSDirectory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Collection;

import static org.apache.lucene.document.Field.Store.YES;

/**
 * 测试Lucene索引热备
 */
public class TestIndexBackupRecovery {

    public static void main(String[] args) throws IOException, InterruptedException {
        String f = "D:/index_test";
        String d = "D:/index_back";
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
        indexWriterConfig.setIndexDeletionPolicy(new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()));
        IndexWriter writer = new IndexWriter(FSDirectory.open(Paths.get(f)), indexWriterConfig);
        Document document = new Document();
        document.add(new StringField("ID", "111", YES));
        document.add(new IntPoint("age", 111));
        document.add(new StoredField("age", 111));
        writer.addDocument(document);
        writer.commit();
        document = new Document();
        document.add(new StringField("ID", "222", Field.Store.YES));
        document.add(new IntPoint("age", 333));
        document.add(new StoredField("age", 333));
        writer.addDocument(document);
        document.add(new StringField("ID", "333", Field.Store.YES));
        document.add(new IntPoint("age", 555));
        document.add(new StoredField("age", 555));
        for (int i = 0; i 1000; i++) {
            document = new Document();
            document.add(new StringField("ID", "333", YES));
            document.add(new IntPoint("age", 1000000 + i));
            document.add(new StoredField("age", 1000000 + i));
            document.add(new StringField("desc", "ABCDEFG" + i, YES));
            writer.addDocument(document);
        }
        writer.deleteDocuments(new TermQuery(new Term("ID", "333")));
        writer.commit();
        backupIndex(writer, f, d);
        writer.close();
    }

    public static void backupIndex(IndexWriter indexWriter, String indexDir, String
            backupIndexDir) throws IOException {
        IndexWriterConfig config = (IndexWriterConfig) indexWriter.getConfig();
        SnapshotDeletionPolicy snapshotDeletionPolicy = (SnapshotDeletionPolicy) config.getIndexDeletionPolicy();
        IndexCommit snapshot = snapshotDeletionPolicy.snapshot();
        //设置索引提交点,默认是null,会打开最后一次提交的索引点
        config.setIndexCommit(snapshot);
        Collection fileNames = snapshot.getFileNames();
        File[] dest = new File(backupIndexDir).listFiles();
        String sourceFileName;
        String destFileName;
        if (dest != null && dest.length > 0) {
            //先删除备份文件中的在此次快照中已经不存在的文件
            for (File file : dest) {
                boolean flag = true;
                //包括文件扩展名
                destFileName = file.getName();
                for (String fileName : fileNames) {
                    sourceFileName = fileName;
                    if (sourceFileName.equals(destFileName)) {
                        flag = false;
                        break;//跳出内层for循环
                    }
                }
                if (flag) {
                    file.delete();//删除
                }
            }
            //然后开始备份快照中新生成的文件
            for (String fileName : fileNames) {
                boolean flag = true;
                sourceFileName = fileName;
                for (File file : dest) {
                    destFileName = file.getName();
                    //备份中已经存在无需复制,因为Lucene索引是一次写入的,所以只要文件名相同不要要hash检查就可以认为它们的数据是一样的
                    if (destFileName.equals(sourceFileName)) {
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    File from = new File(indexDir + File.separator + sourceFileName);//源文件
                    File to = new File(backupIndexDir + File.separator + sourceFileName);//目的文件
                    FileUtils.copyFile(from, to);
                }
            }
        } else {
            //备份不存在,直接创建
            for (String fileName : fileNames) {
                File from = new File(indexDir + File.separator + fileName);//源文件
                File to = new File(backupIndexDir + File.separator + fileName);//目的文件
                FileUtils.copyFile(from, to);
            }
        }
        snapshotDeletionPolicy.release(snapshot);
        //删除已经不再被引用的索引提交记录
        indexWriter.deleteUnusedFiles();
    }
}

索引文件格式

首先索引里都存了些什么呢?一个索引包含一个documents的序列,一个document是一个fields的序列,一个field是一个有名的terms序列,一个term是一个比特序列。

根据 Summary of File Extensions的说明,目前Lucene 6.0中存在的索引格式如下

Name Extension Brief Description
Segments File segments_N Stores information about a commit point
Lock File write.lock The Write lock prevents multiple IndexWriters from writing to the same file
Segment Info .si Stores metadata about a segment
Compound File .cfs, .cfe An optional “virtual” file consisting of all the other index files for systems that frequently run out of file handles
Fields .fnm Stores information about the fields
Field Index .fdx Contains pointers to field data
Field Data .fdt The stored fields for documents
Term Dictionary .tim The term dictionary, stores term info
Term Index .tip The index into the Term Dictionary
Frequencies .doc Contains the list of docs which contain each term along with frequency
Positions .pos Stores position information about where a term occurs in the index
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads
Norms .nvd, .nvm Encodes length and boost factors for docs and fields
Per-Document Values .dvd, .dvm Encodes additional scoring factors or other per-document information
Term Vector Index .tvx Stores offset into the document data file
Term Vector Documents .tvd Contains information about each document that has term vectors
Term Vector Fields .tvf The field level info about term vectors
Live Documents .liv Info about what files are live
Point values .dii, .dim Holds indexed points, if any

在Lucene索引结构中,既保存了正向信息,也保存了反向信息。
正向信息存储在:段(segments_N)->field(.fnm/.fdx/.fdt)->term(.tvx/.tvd/.tvf)
反向信息存储在:词典(.tim)->倒排表(.doc/.pos)

如果想要查看中文,可以参考 这里

恢复索引

恢复索引步骤如下

  • 关闭索引目录下的全部reader和writer,这样才能进行文件恢复。对于Windows系统来说,如果还有其它进程在使用这些文件,那么备份程序仍然不能覆盖这些文件
  • 删除当前索引目录下的所有文件,如果删除过程出现“访问被拒绝”(Access is denied)错误,那么再次检查上一步是否已完成
  • 从备份目录中拷贝文件至索引目录。程序需要保证该拷贝操作不会碰到任何错误,如磁盘空间已满等,因为这些错误会损坏索引文件
  • 对于损坏索引,可以使用CheckIndex(org.apache.lucene.index)进行检查并修复

通常Lucene能够很好地避免大多数常见错误,如果程序遇到磁盘空间已满或者OutOfMemoryException异常,那么它只会丢失内存缓冲中的文档,已经编入索引的文档将会完好保存下来,并且索引也会保持原样。这个结果对于以下情况同样适用:如出现JVM崩溃,或者程序碰到无法控制的异常,或者程序进程被终止,或者操作系统崩溃,或者计算机突然断电等。

/**
 * @param source 索引源
 * @param dest 索引目标
 * @param indexWriterConfig 配置相关
 */public static void recoveryIndex(String source, String dest, IndexWriterConfig indexWriterConfig) {
    IndexWriter indexWriter = null;
 try {
        indexWriter = new IndexWriter(FSDirectory.open(Paths.get(dest)), indexWriterConfig);
  } catch (IOException e) {
        log.error("", e);
  } finally {
        //说明IndexWriter正常打开了,无需恢复
  if (indexWriter != null && indexWriter.isOpen()) {
            try {
                indexWriter.close();
  } catch (IOException e) {
                log.error("", e);
  }
        } else {
            //说明IndexWriter已经无法打开,使用备份恢复索引
 //此处简单操作,先清空损坏的索引文件目录,如果索引特别大,可以比对每个文件,不必全部删除  try {
                FileUtils.deleteDirectory(new File(dest));
  FileUtils.copyDirectory(new File(source), new File(dest));
  } catch (IOException e) {
                log.error("", e);
  //使用备份恢复出错,那么就使用最后一招修复索引
  log.info("Check index {} now!", dest);
 try {
                    IndexUtils.checkIndex(dest);
  } catch (IOException | InterruptedException e1) {
                    log.error("Check index error!", e1);
  }
            }
        }
    }
}

修复索引

当其它所有方法都无法解决索引损坏问题时,你的最后一个选项就是使用CheckIndex工具了。该工具除了能汇报索引细节状况以外,还能完成修复索引的工作。该工具会强制删除索引中出现问题的段,需要注意的是,该操作还会全部删除这些段包含的文档,该工具的使用目标应主要着眼于能够在紧急状况下让搜索程序再次运行起来,一旦我们进行了索引备份,并且备份完好,应优先使用恢复索引,而不是修复索引。

/**
 * CheckIndex会检查索引中的每个字节,所以当索引比较大时,此操作会比较耗时
 *
 * @throws IOException
 * @throws InterruptedException
 */
public void checkIndex(String indexFilePath) throws IOException, InterruptedException {
    CheckIndex checkIndex = new CheckIndex(FSDirectory.open(Paths.get(indexFilePath)));
    checkIndex.setInfoStream(System.out);
    CheckIndex.Status status = checkIndex.checkIndex();
    if (status.clean) {
        System.out.println("Check Index successfully!");
    } else {
        //产生索引中的某个文件之后再次测试
        System.out.println("Starting repair index files...");
        //该方法会向索引中写入一个新的segments文件,但是并不会删除不被引用的文件,除非当你再次打开IndexWriter才会移除不被引用的文件
        //该方法会移除所有存在错误段中的Document索引文件
        checkIndex.exorciseIndex(status);
        checkIndex.close();
        //测试修复完毕之后索引是否能够打开
        IndexWriter indexWriter = new IndexWriter(FSDirectory.open(Paths.get(indexFilePath)), new IndexWriterConfig(new
                StandardAnalyzer()));
        System.out.println(indexWriter.isOpen());
        indexWriter.close();
    }
}

如果索引完好,输出如下信息:

Segments file=segments_2 numSegments=2 version=6.0.0 id=2jlug2dgsc4tmgkf5rck1xgm4
1 of 2: name=_0 maxDoc=1
version=6.0.0
id=2jlug2dgsc4tmgkf5rck1xgm1
codec=Lucene60
compound=true
numFiles=3
size (MB)=0.002
diagnostics = {java.runtime.version=1.8.0_45-b15, java.vendor=Oracle Corporation, java.version=1.8.0_45, java.vm.version=25.45-b02, lucene.version=6.0.0, os=Windows 7, os.arch=amd64, os.version=6.1, source=flush, timestamp=1464159740092}
no deletions
test: open reader………OK [took 0.051 sec]
test: check integrity…..OK [took 0.000 sec]
test: check live docs…..OK [took 0.000 sec]
test: field infos………OK [2 fields] [took 0.000 sec]
test: field norms………OK [0 fields] [took 0.000 sec]
test: terms, freq, prox…OK [1 terms; 1 terms/docs pairs; 0 tokens] [took 0.007 sec]
test: stored fields…….OK [2 total field count; avg 2.0 fields per doc] [took 0.008 sec]
test: term vectors……..OK [0 total term vector count; avg 0.0 term/freq vector fields per doc] [took 0.000 sec]
test: docvalues………..OK [0 docvalues fields; 0 BINARY; 0 NUMERIC; 0 SORTED; 0 SORTED_NUMERIC; 0 SORTED_SET] [took 0.000 sec]
test: points…………..OK [1 fields, 1 points] [took 0.001 sec]
2 of 2: name=_1 maxDoc=1001
version=6.0.0
id=2jlug2dgsc4tmgkf5rck1xgm3
codec=Lucene60
compound=true
numFiles=4
size (MB)=0.023
diagnostics = {java.runtime.version=1.8.0_45-b15, java.vendor=Oracle Corporation, java.version=1.8.0_45, java.vm.version=25.45-b02, lucene.version=6.0.0, os=Windows 7, os.arch=amd64, os.version=6.1, source=flush, timestamp=1464159740329}
has deletions [delGen=1]
test: open reader………OK [took 0.003 sec]
test: check integrity…..OK [took 0.000 sec]
test: check live docs…..OK [1000 deleted docs] [took 0.000 sec]
test: field infos………OK [3 fields] [took 0.000 sec]
test: field norms………OK [0 fields] [took 0.000 sec]
test: terms, freq, prox…OK [1 terms; 1 terms/docs pairs; 0 tokens] [took 0.008 sec]
test: stored fields…….OK [2 total field count; avg 2.0 fields per doc] [took 0.012 sec]
test: term vectors……..OK [0 total term vector count; avg 0.0 term/freq vector fields per doc] [took 0.000 sec]
test: docvalues………..OK [0 docvalues fields; 0 BINARY; 0 NUMERIC; 0 SORTED; 0 SORTED_NUMERIC; 0 SORTED_SET] [took 0.000 sec]
test: points…………..OK [1 fields, 1001 points] [took 0.001 sec]
No problems were detected with this index.
Took 0.159 sec total.
Check Index successfully!

在破坏索引之后(删除了一个cfe文件),再次运行输出

Segments file=segments_2 numSegments=2 version=6.0.0 id=2jlug2dgsc4tmgkf5rck1xgm4
1 of 2: name=_0 maxDoc=1
version=6.0.0
id=2jlug2dgsc4tmgkf5rck1xgm1
codec=Lucene60
compound=true
numFiles=3
FAILED
WARNING: exorciseIndex() would remove reference to this segment; full exception:
java.nio.file.NoSuchFileException: D:index_test_0.cfe
at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)
…
…
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
2 of 2: name=_1 maxDoc=1001
version=6.0.0
id=2jlug2dgsc4tmgkf5rck1xgm3
codec=Lucene60
compound=true
numFiles=4
size (MB)=0.023
diagnostics = {java.runtime.version=1.8.0_45-b15, java.vendor=Oracle Corporation, java.version=1.8.0_45, java.vm.version=25.45-b02, lucene.version=6.0.0, os=Windows 7, os.arch=amd64, os.version=6.1, source=flush, timestamp=1464159740329}
has deletions [delGen=1]
test: open reader………OK [took 0.059 sec]
test: check integrity…..OK [took 0.000 sec]
test: check live docs…..OK [1000 deleted docs] [took 0.001 sec]
test: field infos………OK [3 fields] [took 0.000 sec]
test: field norms………OK [0 fields] [took 0.000 sec]
test: terms, freq, prox…OK [1 terms; 1 terms/docs pairs; 0 tokens] [took 0.013 sec]
test: stored fields…….OK [2 total field count; avg 2.0 fields per doc] [took 0.016 sec]
test: term vectors……..OK [0 total term vector count; avg 0.0 term/freq vector fields per doc] [took 0.000 sec]
test: docvalues………..OK [0 docvalues fields; 0 BINARY; 0 NUMERIC; 0 SORTED; 0 SORTED_NUMERIC; 0 SORTED_SET] [took 0.000 sec]
test: points…………..OK [1 fields, 1001 points] [took 0.002 sec]
WARNING: 1 broken segments (containing 1 documents) detected
Took 0.165 sec total.
Starting repair index files…
true

Lucene 6.0 实战:索引热备份及恢复,首发于 文章 - 伯乐在线

相关 [lucene 索引 备份] 推荐:

Lucene 6.0 实战:索引热备份及恢复

- - 文章 – 伯乐在线
最简单的备份方式是关闭IndexWriter,然后逐一拷贝索引文件,但是如果索引比较大,那么这种备份操作会持续较长时间,而在备份期间,程序无法对索引文件进行修改,很多搜索程序是不能接受索引操作期间如此长时间停顿的. 那么不关闭IndexWriter又如何呢. 这样也不行,因为在拷贝索引期间,如果索引文件发生变化,会导致备份的索引文件损坏.

有关Lucene的问题(7):用Lucene构建实时的索引

- -
由于前一章所述的Lucene的事务性,使得Lucene可以增量的添加一个段,我们知道,倒排索引是有一定的格式的,而这个格式一旦写入是非常难以改变的,那么如何能够增量建索引呢. Lucene使用段这个概念解决了这个问题,对于每个已经生成的段,其倒排索引结构不会再改变,而增量添加的文档添加到新的段中,段之间在一定的时刻进行合并,从而形成新的倒排索引结构.

lucene索引创建的理解思路

- - ITeye博客
虽然lucene4很早就出来,但是这里仍然以lucene3.0为基础,理解lucene索引创建的思路:. field的数据,fdx,fdt,依次写每个field的即可. 词向量,tvx,tvd,tvf. tvf是真正存储的地方,tvx是每个文档一项,具体包含第一个field的位置,其他field只要记录与覅一个field的偏移量即可.

[原]Lucene系列-索引文件

- - 文武天下
本文介绍下lucene生成的索引有哪些文件组成,每个文件包含了什么信息. 基于Lucene 4.10.0. 索引(index)包含了存储的文档(document)正排、倒排信息,用于文本搜索. 索引又分为多个段(segments),每个新添加的doc都会存到一个新segment中,不同的segments又会合并成一个segment.

减小lucene索引体积大小

- - sling2007的博客
下文讲述了lucene中,如何优化索引,减小索引体积. 如果需要被搜索的数值类型,需要设置合适的precisionstep. 如果不需要搜索,只要排序即可,那么设置precisionstep为Integer.Max即可. 使用geohash算法,给每个区域编码,把编码切成term并索引,然后用于搜索.

Lucene索引升级 - rainystars' Blog - SegmentFault

- -
由于Lucene文件格式从2到3以及从3到4版本时都发生了重大的改变,造成了高版本无法读取低版本的数据,使用Lucene中的IndexUpgrader方法先将版本从2升到3,然后再从3升级到4. 从版本2升级到版本3时,需要使用lucene3的jar包,我使用的lucene3.6的jar包,我需要处理的索引是在一个文件夹中所存在的一系列索引文件,所以需要循环来遍历每个目录.

[原]基于Lucene多索引进行索引和搜索

- - 千与的专栏
Lucene支持创建多个索引目录,同时存储多个索引. 我们可能担心的问题是,在索引的过程中,分散地存储到多个索引目录中,是否在搜索时能够得到全局的相关度计算得分,其实Lucene的ParallelMultiSearcher和MultiSearcher支持全局得分的计算,也就是说,虽然索引分布在多个索引目录中,在搜索的时候还会将全部的索引数据聚合在一起进行查询匹配和得分计算.

LIRE(Lucene Image Retrieval)相似图像索引和搜索机制

- - CSDN博客云计算推荐文章
众说周知,lucene是一个开源的强大的索引工具,但是它仅限于文本索引. 基于内容的图像检索(CBIR)要求我们利用图像的一些基本特征(如颜色纹理形状以及sift,surf等等)搜索相似的图片,LIRE(Lucene Image Retrieval)是一款基于lucene的图像特征索引工具,它能帮助我们方便的对图像特征建立索引和搜索,作者也在不断加入新的特征供用户使用.

开源搜索引擎评估:lucene sphinx elasticsearch

- - 鲁塔弗的博客
lucene系,java开发,包括 solr和 elasticsearch. sphinx,c++开发,简单高性能. 搜索引擎程序这个名称不妥当,严格说来应该叫做 索引程序(indexing program),早期主要用来做中文全文搜索,但是随着互联网的深入普及,各家网站规模越来越大,索引程序在 优化网站架构上发挥了更大的作用: 替代mysql数据库 内置的索引.

用Lucene构建实时索引的文档更新问题

- - 开源软件 - ITeye博客
1、Lucene删除文档的几种方式. IndexReader.deleteDocument(int docID)是用 IndexReader 按文档号删除. IndexReader.deleteDocuments(Term  term)是用 IndexReader 删除包含此词(Term)的文档. IndexWriter.deleteDocuments(Term  term)是用 IndexWriter 删除包含此词(Term)的文档.