Lucene近实时搜索应用总结

最近因工作需要,用到了Lucene,在需求中,需要对生成的索引文件不断的更新、新增、删除等操作,同时还要实时的看到索引改动后的数据,在使用过程中碰到了Lucene里几个比较常见的问题,特来总结记录下。

我使用的是Lucene4.3,本来是想使用最高的版本Lucene4.9的(不知道现在又有么有新的版本出现),但是因为公司项目的JDK都是JDK6的,而Lucene4.9的支持的最低JDK版本是7+的,所以最后选择了这个版本。第一次碰这个东东,也是在网上搜罗各种资料,当然官网是少不了的,还有一个网址,这个里面的版本更全面,包含了Lucene所有的版本,有需要的可以去下载自己想要的版本。

OK,进入正题。

  1. 在读取索引文件内容时,索引文件的打开操作 new IndexSearcher(DirectoryReader.open(FSDirectory.open(new File(indexPath)))) 是个非常耗时耗资源的操作,所以在搜索索引数据时把IndexSearcher对象给缓存起来可以提高搜索性能,这个地方可以将索引目录对应的IndexSearcher对象做成一个单例模式进行获取。

  2. 在问题1的操作基础上,我对索引进行了更新操作,紧接着我就发现了一个问题,索引更新后我查询出来的结果和我更新后的结果对不上号。再网上查了半天没找到原因,后来在一个群里请教之后,才知道更新完索引之后,索引文件需要重新打开,否则搜索得到的还是原来索引的数据,Lucene里面的这个原理非常重要。

  3. 因为当时对Lucene了解的不是很多,所以为了每次更新后能搜索到正确的数据,我的做法是每次更新完索引就将我之前缓存的IndexSearcher对象和Reader对象给移除掉,下次搜索时重新打开索引,这样来保证搜索结果的正确性。

索引的更新代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//获取索引的写对象
public static IndexWriter getIndexWriter(String indexPath) throws Exception {
Directory dir = FSDirectory.open(new File(indexPath));
if(IndexWriter.isLocked(dir)) {
IndexWriter.unlock(dir);
}
IndexWriterConfig iwriterConfig = new IndexWriterConfig(Version.LUCENE_43, new ComplexAnalyzer());
IndexWriter indexWriter=new IndexWriter(dir, iwriterConfig);
return indexWriter;
}
//索引更新操作,索引中的更新逻辑是将旧的文档删除,再将新的文档新增进去
public static void updateLucene(String indexPath,要更新的数据参数) {
IndexSearcher indexSearcher = getIndexSearcher(indexPath);
Document oldDoc = ....//根据IndexSearcher查询得到被更新的旧文档--1处
Document newDoc = ....//根据旧文档和要更新的具体数据得到要更新的新文档
indexWriter.updateDocument(doc);//更新操作--3处
removeReader(indexPath);//移除IndexSearcher缓存对象--2处
}

  1. 问题3中的这个跟新操作写完之后实际应用的时候,出了好几个问题,都是Lucene里面比较常见,容易犯错的问题。
    异常1:org.apache.lucene.store.AlreadyClosedException: this IndexReader is closed
    经过分析之后发现是因为多并发情况下时,同一个索引文件对应的Reader对象别多个线程持有,在我这个代码里存在线程1刚好执行完2处代码将Reader缓存清理掉(同时关掉了该Reader对象),而刚好,线程2在执行代码1处的查询操作,这个时候就会出现这个异常。在我这个代码里的更新操作总共分3部,首先查询出旧的文档,紧接着构造新文档并执行更新操作,第三步是移除缓存,所以这三步应该归结为一个原子操作,所以我很自然的就想到了同步锁synchronized,在该方法上添加Class级的同步锁,由于我的代码里有多个这种更新索引的方法,所以每个方法都加一个同步锁,后来证明这种方式也不行仍然出现了该异常,具体代码的问题出在哪我现在真记不起来了,而且这种多个静态方法都加Class级的同步锁性能注定不怎么样,后续的解决办法下面问题中继续说。
    异常2:LockObtainFailedException
    经过查询资料知道这是因为在IndexWriter的构造函数在试图获取另外一个IndexWriter已经加锁的索引目录时抛出的错误,这是因为在Lucene中只能允许一个线程去进行写操作,当该线程获取到这个写对象后,会在索引目录中生成一个write.lock文件,所以在这个线程没有释放该索引目录的锁对象前,其他线程无法获取该目录的写对象,根据这个write.lock文件,我们可以很方便的判断当前索引目录有没有被写对象占用,改造后的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    private Object synchronized_w = new Object();
    private IndexWriter getIndexWriter() {
    synchronized (synchronized_w) {
    try {
    Directory dir = null;
    while (true) {
    dir = FSDirectory.open(new File(indexPath));
    if(!IndexWriter.isLocked(dir)) {
    if(indexWriter == null) {
    indexWriter = new IndexWriter(dir, new IndexWriterConfig(Version.LUCENE_43, LuceneManager.getAnalyzer()));
    }
    break;
    } else {
    try {
    dir.close();
    Thread.sleep(100);
    Thread.yield();
    } catch (InterruptedException e) {
    logger.error("获取索引" + indexPath + "写对象IndexWriter失败", e);
    }
    }
    }
    } catch (Exception e) {
    logger.error("获取索引" + indexPath + "写对象IndexWriter失败", e);
    }
    }
    return indexWriter;
    }

获取索引写对象的代码经过如此改造之后,成功运行,没有出现问题。

  1. 继续问题4中的第一个异常,分析之后觉得问题出现的最终原因还是在第三步移除缓存时关闭Reader对象这里,所以如果Lucene能不需要手动关闭Reader对象就可以解决这个问题,带着这个问题我重新去查看了Lucene4.3的API文档,看了之后发现了两个个比较重要的API,DirectoryReader.openIfChanged(dirReader)和DirectoryReader.isCurrent(),前者是个静态方法,可以判断当前Reader对象的索引有没有被修改过,如果索引文件被更新过则重新加载该索引目录,但是这个时候的重新加载则比单纯的open(indexPath)要高效很多,它只是重新加载被更新过的文档,而单纯的open则是加载全部的文档,重新加载后我们查询的时候就可以查询到最新的数据结果了。而第二个API是个实例方法,用来判断当前Reader对象所代表的索引文件是不是最开始那个,即判断当前索引文件有没有被更新过,有更新则返回false,否则返回true,则两个API结合在一起刚好可以解决我之前的Reader关闭问题。有了这种重新加载机制,我就不需要每次更新索引之后清除缓存关掉旧的Reader对象并打开新的对象。改造的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public IndexSearcher getIndexSearcher() {
    synchronized (lock_r) {
    try {
    if(indexSearcher == null && dirReader == null) {
    dirReader = DirectoryReader.open(FSDirectory.open(new File(indexPath)));
    indexSearcher = new IndexSearcher(dirReader);
    } else if(indexSearcher != null && dirReader != null && !dirReader.isCurrent()) {
    //判断有没有更新过,有更新则重新加载更新过的文档
    DirectoryReader newDirReader = DirectoryReader.openIfChanged(dirReader);
    indexSearcher = new IndexSearcher(newDirReader);
    dirReader = newDirReader;
    }
    } catch (IOException e) {
    logger.error("获取索引" + indexPath + "搜索对象IndexSearcher失败", e);
    }
    }
    return indexSearcher;
    }

这个代码里面有个问题就是旧的Reader对象没有关闭掉,如果加上这reader.close()这句话又会出现那个异常,不加的话运行没有问题,但是我总觉得不关掉不太好,感觉占着资源。然后继续查看API文档,发现了SearcherManager这个类,这个是Lucene里面提供的工具类,主要是用来了管理IndexSearcher对象的,仔细阅读了该类的说明及其源代码后觉得用这个工具类更靠谱,所以最后毫不犹豫的重新写了一个IndexSearcher对象的获取方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private IndexSearcher getIndexSearcher() throws IOException {
IndexSearcher indexSearcher = null;
synchronized (synchronized_r) {
if(searcherManager == null) {
searcherManager = new SearcherManager(FSDirectory.open(new File(indexPath)), new SearcherFactory());
}
searcherManager.maybeRefresh();//这个方法同DirectoryReader.openIfChanged(dirReader)效果一样,其实底层还是调用的该方法实现的
indexSearcher = searcherManager.acquire();//借用一个IndexSearcher对象的引用,记住该对象用完之后要归还的,有借有还再借不难
}
return indexSearcher;
}
private void closeIndexSearcher(IndexSearcher indexSearcher) throws IOException {
if(indexSearcher != null) {
searcherManager.release(indexSearcher);//归还从SearcherManager处借来的IndexSearcher对象
}
indexSearcher = null;
}

如此实现之后,就不需要我们自己管理这个旧的Reader对象,而是交由Lucene本身自己去进行管理,而且此种实现方式更简洁明了,也完美解决了我的问题。

实现了索引更新后的数据的读取实时性就可以实现一个简单的实时搜索功能。

最后为了保证项目中Lucene使用的稳定性,我对索引文件的更新和查询都添加了读写锁ReentrantReadWriteLock来进行控制,更新的时候添加写锁,查询的时候添加读锁,这样更加的保证了Lucene使用的安全性。


简单介绍下读写锁ReentrantReadWriteLock的机制(多线程并发的时候很有用):

  1. 在某个线程获取到读锁时,其他线程不能获取写锁,但是可以获取读锁
  2. 在某个线程获取到写锁时,其他线程既不能获取写锁也不能获取读锁
  3. 给个示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private ReentrantReadWriteLock w_lock = new ReentrantReadWriteLock();//读写锁
    public void updateDocument(Term term, Document doc) throws Exception {
    try {
    w_lock.writeLock().lock();//获得写锁
    getIndexWriter();
    indexWriter.updateDocument(term, doc);
    indexWriter.commit();
    } catch (IOException e) {
    throw new Exception(e);
    } finally {
    closeIndexWriter();
    w_lock.writeLock().unlock();释放写锁
    }
    }

OK,到这里我的问题基本上就写完了,有时间再去研究研究Lucene的其他的特性。


write by laohu
2014年12月11日19:12:55


原创文章,本文采用知识共享署名 2.5(中国大陆许可协议)进行许可,欢迎转载,但转载请注明来自ittiger.cn,并保证转载后文章内容的完整性。本人(laohu)保留所有版权相关权利。



评论