【问题标题】:Writing to Lucene index, one document at a time, slows down over time写入 Lucene 索引,一次一个文档,随着时间的推移变慢
【发布时间】:2015-11-23 00:45:31
【问题描述】:

我们有一个程序,它不断运行,做各种事情,并更改我们数据库中的一些记录。这些记录使用 Lucene 进行索引。因此,每次我们更改实体时,我们都会执行以下操作:

  1. 打开db事务,打开Lucene IndexWriter
  2. 在事务中对数据库进行更改,并在 Lucene 中使用indexWriter.deleteDocuments(..) 然后indexWriter.addDocument(..) 更新该实体。
  3. 如果一切顺利,提交 db 事务并提交 IndexWriter。

这很好用,但随着时间的推移,indexWriter.commit() 会花费越来越多的时间。最初它需要大约 0.5 秒,但在几百个这样的交易之后,它需要超过 3 秒。如果脚本运行时间更长,我不怀疑它会花费更长的时间。

到目前为止,我的解决方案是注释掉 indexWriter.addDocument(..)indexWriter.commit(),并不时地重新创建整个索引,首先使用 indexWriter.deleteAll() 然后在一个 Lucene 事务/IndexWriter 中重新添加所有文档(大约 14 秒内大约 250k 个文档)。但这显然违背了数据库和 Lucene 提供的事务方法,后​​者使两者保持同步,并使使用 Lucene 进行搜索的我们工具的用户可以看到数据库的更新。

我可以在 14 秒内添加 250k 个文档,但添加 1 个文档需要 3 秒,这似乎很奇怪。我做错了什么,我该如何改善这种情况?

【问题讨论】:

  • 你能用后台任务解决它吗?您可能会受到 10 秒的惩罚,但这对于许多应用程序来说都可以
  • @AdamSkywalker - 但它越来越慢,如果需要 1 小时、10 小时或 2 天呢?

标签: java performance indexing lucene


【解决方案1】:

您做错了什么是假设 Lucene 的 built-in transactional capabilities 具有与典型关系数据库相当的性能和保证,而 they really don't。更具体地说,在您的情况下,提交将所有索引文件与磁盘同步,使提交时间与索引大小成正比。这就是为什么随着时间的推移您的indexWriter.commit() 会花费越来越多的时间。 IndexWriter.commit()Javadoc 甚至警告说:

这可能是一项昂贵的操作,因此您应该在您的 应用程序并仅在真正需要时才执行。

你能想象数据库文档告诉你避免提交吗?

由于您的主要目标似乎是通过 Lucene 搜索及时保持数据库更新可见,为了改善这种情况,请执行以下操作:

  1. 在成功提交数据库后触发indexWriter.deleteDocuments(..)indexWriter.addDocument(..),而不是之前
  2. 定期执行 indexWriter.commit() 而不是每次事务,以确保您的更改最终写入磁盘
  3. 使用SearcherManager 进行搜索并定期调用maybeRefresh() 以在合理的时间范围内查看更新的文档

以下是一个示例程序,它演示了如何通过定期执行maybeRefresh() 来检索文档更新。它构建了一个包含 100000 个文档的索引,使用 ScheduledExecutorService 设置定期调用 commit()maybeRefresh(),提示您更新单个文档,然后重复搜索直到更新可见。所有资源都在程序终止时正确清理。请注意,更新何时可见的控制因素是调用maybeRefresh(),而不是commit()

import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
import java.util.concurrent.*;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;

public class LucenePeriodicCommitRefreshExample {
    ScheduledExecutorService scheduledExecutor;
    MyIndexer indexer;
    MySearcher searcher;

    void init() throws IOException {
        scheduledExecutor = Executors.newScheduledThreadPool(3);
        indexer = new MyIndexer();
        indexer.init();
        searcher = new MySearcher(indexer.indexWriter);
        searcher.init();
    }

    void destroy() throws IOException {
        searcher.destroy();
        indexer.destroy();
        scheduledExecutor.shutdown();
    }

    class MyIndexer {
        IndexWriter indexWriter;
        Future commitFuture;

        void init() throws IOException {
            indexWriter = new IndexWriter(FSDirectory.open(Paths.get("C:\\Temp\\lucene-example")), new IndexWriterConfig(new StandardAnalyzer()));
            indexWriter.deleteAll();
            for (int i = 1; i <= 100000; i++) {
                add(String.valueOf(i), "whatever " + i);
            }
            indexWriter.commit();
            commitFuture = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    indexWriter.commit();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }, 5, 5, TimeUnit.MINUTES);
        }

        void add(String id, String text) throws IOException {
            Document doc = new Document();
            doc.add(new StringField("id", id, Field.Store.YES));
            doc.add(new StringField("text", text, Field.Store.YES));
            indexWriter.addDocument(doc);
        }

        void update(String id, String text) throws IOException {
            indexWriter.deleteDocuments(new Term("id", id));
            add(id, text);
        }

        void destroy() throws IOException {
            commitFuture.cancel(false);
            indexWriter.close();
        }
    }

    class MySearcher {
        IndexWriter indexWriter;
        SearcherManager searcherManager;
        Future maybeRefreshFuture;

        public MySearcher(IndexWriter indexWriter) {
            this.indexWriter = indexWriter;
        }

        void init() throws IOException {
            searcherManager = new SearcherManager(indexWriter, true, null);
            maybeRefreshFuture = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    searcherManager.maybeRefresh();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }, 0, 5, TimeUnit.SECONDS);
        }

        String findText(String id) throws IOException {
            IndexSearcher searcher = null;
            try {
                searcher = searcherManager.acquire();
                TopDocs topDocs = searcher.search(new TermQuery(new Term("id", id)), 1);
                return searcher.doc(topDocs.scoreDocs[0].doc).getField("text").stringValue();
            } finally {
                if (searcher != null) {
                    searcherManager.release(searcher);
                }
            }
        }

        void destroy() throws IOException {
            maybeRefreshFuture.cancel(false);
            searcherManager.close();
        }
    }

    public static void main(String[] args) throws IOException {
        LucenePeriodicCommitRefreshExample example = new LucenePeriodicCommitRefreshExample();
        example.init();
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    example.destroy();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("Enter a document id to update (from 1 to 100000): ");
            String id = scanner.nextLine();
            System.out.print("Enter what you want the document text to be: ");
            String text = scanner.nextLine();
            example.indexer.update(id, text);
            long startTime = System.nanoTime();
            String foundText;
            do {
                foundText = example.searcher.findText(id);
            } while (!text.equals(foundText));
            long elapsedTimeMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
            System.out.format("it took %d milliseconds for the searcher to see that document %s is now '%s'\n", elapsedTimeMillis, id, text);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.exit(0);
        }
    }
}

本示例已使用 Lucene 5.3.1 和 JDK 1.8.0_66 成功测试。

【讨论】:

  • 很好的解释和很好的例子!谢谢!
【解决方案2】:

我的第一个方法:不要经常提交。当您删除并重新添加文档时,您可能会触发合并。合并有点慢。

如果您使用近乎实时的 IndexReader,您仍然可以像以前一样进行搜索(它不会显示已删除的文档),但您不会受到提交惩罚。您始终可以稍后提交,以确保文件系统与您的索引同步。您可以在使用索引时执行此操作,因此您不必阻止所有其他操作。

另请参阅这个有趣的blog post(也请阅读其他帖子,它们提供了很好的信息)。

【讨论】:

  • 我可以理解触发合并可能会很慢,但是您是否希望提交会随着时间的推移而变慢? (如果我不那么频繁地提交,那只会推迟提交变得不可接受的缓慢时间(1 分钟?10 分钟?),并且由于这个脚本永远运行,它最终会达到那个点。)
  • 我使用过大约 10M 文档的索引大小。在我的笔记本电脑上,commit() 最多可能需要 10 秒。但是,如果您使用 NTR 解决方案,这并不重要,因为您不必等待 commit() 完成。
猜你喜欢
  • 2012-11-25
  • 2014-12-08
  • 2016-02-13
  • 1970-01-01
  • 2014-02-04
  • 2018-09-10
  • 1970-01-01
  • 1970-01-01
  • 2021-07-22
相关资源
最近更新 更多