Lucene的IndexSearcher管理

一、场景

Lucene创建一个searcher需要先打开一个DirectoryReader,用以从目录中读取索引,而此过程的代价是比较高的。同时searcher只做查询,不涉及到索引的更新操作,自然而然我们就会想到使用单例模式,重复使用。但是我们的索引文件如果发生了更新,对应的searcher也需要同步更新,就不是普通的单例那么简单了,这里介绍几种我使用过的有效的管理方式。

二、IndexSearcher管理方案

1、自己维护

如果没有去了解过Lucene的api,我们可能会这样做:

对每个索引目录缓存一个searcher(比如缓存在map中),同时给每个目录(searcher)设置一个版本号(比如目录最近更新的时间戳),同时在一些地方(比如redis)设置每个目录的最近更新时间(如果某个目录发生了更新,则更新为当前时间戳)。使用的时候根据目录获取searcher,如果缓存中没有,则初始化,同时连带版本号缓存起来;如果缓存中有,则比较缓存中的版本号和redis的值,若缓存中版本号不是最新的,则表示需要重新初始化。

弊端:虽然能够解决问题,但是如果索引更新频繁,也会比较尴尬

2.Lucene提供DirectoryReader#openIfChanged(DirectoryReader)

调用此方法,如过索引目录在reader创建之后有了更新,则会返回一个新的reader,否则返回null。同时reader的类型和老reader一致,也就是说如果你之前创建的reader是NRT reader,返回的新reader也会是NRT reader,如果之前创建的是MultiReader,返回的新reader也会是MultiReader。此方法创建新reader要比完全重新创建一个reader资源消耗要小的多(大多数情况),因为它会共享老reader的一些资源(具体的代码示例,这里就不贴了,网上很多,也可以自行去看api,使用方法和注意事项都说的很清楚)。

弊端:openIfChanged方法每次查询都需要手动去调用,然后根据返回值来判断是否需要重新生成一个searcher,当然也可以和第一种方式结合使用(但这不是我们的目的)。最重要的原因还是,我们想要的是不要再手动去关闭一个reader,真正做到一次创建永久使用(至少在用户层面上是这样),因为既然我们想设计为单例模式,那么多线程调用是必不可少的,我们可不想在有很多其它线程正在使用的时候去close它(多线程问题)。

3.使用Lucene的SearcherManager

考虑到上述情况,Lucene为我们提供了一个管理器来管理IndexSearcher,这是一个线程安全的工具类,它为我们保证每一个searcher只有在所有使用它的线程都处理完毕后才会被关闭(也就是帮我们处理多线程问题)。

弊端:同样是索引目录更新的情况,如果使用SearcherManager,需要手动调用SearcherManager#maybeRefresh(),来确保使用的索引是最新的。你可以在每次查询之前都调用一次,当然我们不推荐这样做,通常我们会在后台单独起一个线程,去定时执行maybeRefresh()方法,最后,如果不在使用记得close。下面贴出简单的代码示例:

        ...省略
        private static Map managerMap = new HashMap<>();
        private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        /**
         * @path 索引文件目录
         * @return SearcherManager
         */
        public static SearcherManager getSearcherManager (String path) throw Exception {
            SearcherManager searcherManager = managerMap.get(path);
            if (searcherManager == null) {
                synchronized (path.intern()) {
                    if (searcherManager == null) {
                        Directory directory = FSDirectory.open(path);
                        searcherManager = new SearcherManager(directory, null);
                        managerMap.put(path, searcherManager);
                    }
                }
            }
            return searcherManager;
        }
        ...

        /**
         * 测试示例
         */
        public static void testSearch () throw Exception {
            String path = "/data/index";
            SearcherManager searcherManager = getSearcherManager(path);
            IndexSearcher indexSearcher = searcherManager.acquire();
            try {
                //这里使用indexSearcher做一些查询操作
                    ...
            } finally {
                //release和acquire成对出现
                try {
                    searcherManager.release(indexSearcher);
                } catch (Exception e) {
                    //可以做一些事儿
                }
                //release之后就不再使用这个searcher了
                indexSearcher = null;
            }
        }
        ...
        static{
            //30秒刷新一次    
            executorService.scheduleAtFixedRate(() ->
                        indexSearcherManagerMap.forEach((path, manager) -> {
                            try {
                                manager.maybeRefresh();
                            } catch (IOException e) {
                                //
                            }
                        }), 0, 30, TimeUnit.SECONDS);
        }
        ...省略

三、总结

由于很多情况下,我们使用Lucene进行全文检索,对索引更新反馈到searcher的及时性要求不是特别的高,所以通常我们都建议使用SearcherManager来管理,当然也要具体情况具体分析。

注:如果有描述错误的地方,小弟先行道歉,也希望小伙伴能指出问题,谢谢!

 

你可能感兴趣的:(java,lucene,IndexSearcher,SearcherManager,DirectoryReader,JAVA,全文检索)