博客链接:Cs XJH’s Blog
Lucene近实时搜索
近实时搜索用于对数据有实时性要求的场景。Lucene和数据库类似,也有隔离性,即IndexWriter写入的数据,只有commit之后,对搜索端来说才是可见的。并且,如果搜索端持有IndexWriter,那么即使commit之后,搜索端对于新的数据也是不可见的。
而且,在数据可见性上,与数据库不同的是,IndexReader对于索引的读取是以类似快照的方式。于是,如果在IndexReader读取索引之后,IndexWriter有新的数据commit,那对于IndexReader也是不可见的。只有重新打开IndexReader,那么才可以访问到新提交的数据。
所以,综上所述,如果要实现近实时搜索,那么关键在于两点:一是定期地commit,二是定期地打开IndexReader。
搜索端持有IndexWriter
对于集群或者分布式应用来说,一般都是读写分离的。所以,生成环境中,一般较少采用搜索端持有IndexWriter的方式用于检索。不过,对于单点项目来说,还是可以使用的。而且,这也有助你全面地理解Lucene。
Lucene提供ControlledRealTimeReopenThread 线程工具类来负责周期性的打开 ReferenceManager(调用ReferenceManager.maybeRefresh)。该线程类控制打开间隔比较灵活,当有外部用户在等待指定的 generation 时就按最小时间间隔等待,如果没有用户着急获取最新的Searcher,则等待最大时间间隔后再打开。其构造函数如下:
ControlledRealTimeReopenThread(TrackingIndexWriter writer, ReferenceManager manager,
double targetMaxStaleSec, double targetMinStaleSec)//单位秒
如何判断是否有人等待?
在调用 addDocument 的时候,IndexWriter 为每次更新索引的操作赋予一个标记(generation,代数),递增变化。用户使用 ControlledRealTimeReopenThread.waitForGeneration(generation) 告诉其期望获得更新代数,ControlledRealTimeReopenThread 记录了当前已打开的代数,当期望更新代数大于已打开代数时,就表示有用户期望获得最新的 Searcher。
long generation = indexWriter.addDocument(document);
try {
//当有调用者等待某个generation的时候,只需要0.25s即可重新打开
controlledRealTimeReopenThread.waitForGeneration(generation);
}catch (InterruptedException e) {
e.printStackTrace();
}
Springboot下使用近实时搜索,并将IndexWriter和SearchManager注入IOC容器
@Bean
public IndexWriter getSearchIndexWriter() throws IOException {
FSDirectory directory = FSDirectory.open(FileSystems.getDefault().getPath(SEARCH_INDEX_PATH));
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new IKAnalyzer());
indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
return new IndexWriter(directory, indexWriterConfig);
}
@Bean
public SearcherManager getSearchManager() throws Exception {
SearcherManager searcherManager = new SearcherManager(searchIndexWriter, false,
false, new SearcherFactory());
ControlledRealTimeReopenThread realTimeReopenThread = new ControlledRealTimeReopenThread(
searchIndexWriter, searcherManager, 5.0, 0.25); // 最长5秒刷新一次,最短每0.25秒刷新一次
realTimeReopenThread.setDaemon(true);
realTimeReopenThread.setName("reopen indexReader");
realTimeReopenThread.start();
return searcherManager;
}
搜索端不持有IndexWriter
在实际应用中,会并行的进行搜索、建索引、打开新 reader、关闭老 reader,操作比较复杂还有线程安全问题。为了简化使用流程,Lucene 提供了 SearcherManager extends ReferenceManager 管理 IndexReader 的重建和关闭,保证了线程安全,封装了 IndexSearcher 的生成。
SearcherManager的主要职责如下,主要提供如下三个接口
- acquire:获取当前已打开的最新 IndexSearcher,同时将对应的 IndexReader 引用计数加1
- release:释放之前通过 acquire 方法获取的引用,本质调的是 IndexSearcher.getIndexReader().decRef();,当一个 IndexReader 的引用计数为0时,会将之关闭同时释放其持有的资源
- maybeRefresh:尝试打开新的 IndexReader,本质调用的是 DirectoryReader.openIfChanged() 方法,如果多个线程同时调用该方法,那么只有第一个线程会去尝试刷新,后来的或者说随后的线程看到有其它线程在处理刷新,那么会立即返回;注意这意味着,如果有一个线程在处理刷新操作,那么后续的线程会立即返回而不是等待它刷新完成,所以后续调用该方法的线程可能是已经刷新了的或者是没有任何改变的(没有任何改变意味着通过 acquire 获取的 IndexSearcher 无法搜索到最新的数据)
- maybeRefreshBlocking:功能同 maybeRefresh(),但是不像 maybeRefresh(),如果有一个线程正在刷新,后续的线程将阻塞,直到前面的线程刷新完成,然后后续的线程再继续刷新;这非常有用对于想要保证下一次 acquire() 的调用将返回一个刷新的实例,否则考虑使用 maybeRefresh()
SearcherManager 的一个注意点是,如果调用了 acquire,那么一定要调用 release,在该方法内部会通过 IndexSearcher 获取IndexReader,然后将 IndexReader 的引用计数减1,当这个引用计数为0的时候,这个 IndexReader 就会被关闭,从而可以释放资源。
try {
RAMDirectory ramDirectory = new RAMDirectory();
SearcherManager searcherManager = new SearcherManager(ramDirectory, null);
searcherManager.maybeRefresh(); // 在每次获取indexSearch之前调用
IndexSearcher indexSearcher = searcherManager.acquire();
int count = indexSearcher.count(new MatchAllDocsQuery());
if (count > 0) {
TopDocs topDocs = indexSearcher.search(new MatchAllDocsQuery(), count);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
System.out.println(indexSearcher.doc(scoreDoc.doc));
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
searcherManager.release(indexSearcher);
}
Lucene非实时搜索
其实,也有很多一部分应用不需要实时搜索的需求。那么,代码其实和实时搜索类似。
使用SearchManager管理IndexSearcher,并且,建议将IndexWriter和SearchManager注入容器,避免重新实例化损耗性能。由于不需要定期重新打开IndexReader,只需要在每次IndexWriter写入索引之后,调用searcherManager.maybeRefresh()即可。
PS:文章大部分内容来自IT草根。博主内容写的十分好,忍不住转载,稍微加了些自己的学习理解。