【问题标题】:Making Lucene.Net thread safe in the code在代码中使 Lucene.Net 线程安全
【发布时间】:2017-01-23 18:55:12
【问题描述】:

我正在使用 Lucene.Net 进行搜索,想知道如何处理这个线程问题。

我有一个Test类实例,但在这种情况下搜索器不是线程安全的,因为计时器线程可以在处理请求的同时更新索引,因此我确实看到了异常。关于如何使其线程安全的任何指针。

public class Test 
{
    private static object syncObj = new object();

    private System.Threading.Timer timer;

    private Searcher searcher;

    private RAMDirectory idx = new RAMDirectory();

    public Test()
    {
        this.timer = new System.Threading.Timer(this.Timer_Elapsed, null, TimeSpan.Zero, TimeSpan.FromMinutes(3));
    }


    private Searcher ESearcher
    {
        get
        {
            return this.searcher;
        }

        set
        {
            lock (syncObj)
            {
                this.searcher = value;
            }
        }
    }

    public Document CreateDocument(string title, string content)
    {
        Document doc = new Document();
        doc.Add(new Field("A", title, Field.Store.YES, Field.Index.NO));
        doc.Add(new Field("B", content, Field.Store.YES, Field.Index.ANALYZED));
        return doc;
    }

    public List<Document> Search(Searcher searcher, string queryString)
    {
        List<Document> documents = new List<Document>();
        QueryParser parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "B", new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30));
        Query query = parser.Parse(queryString);
        int hitsPerPage = 5;
        TopScoreDocCollector collector = TopScoreDocCollector.Create(2 * hitsPerPage, true);
        this.ESearcher.Search(query, collector);

        ScoreDoc[] hits = collector.TopDocs().ScoreDocs;

        int hitCount = collector.TotalHits > 10 ? 10 : collector.TotalHits;
        for (int i = 0; i < hitCount; i++)
        {
            ScoreDoc scoreDoc = hits[i];
            int docId = scoreDoc.Doc;
            float docScore = scoreDoc.Score;
            Document doc = searcher.Doc(docId);
            documents.Add(doc);
        }

        return documents;
    }

    private void Timer_Elapsed(object sender)
    {
        this.Log("Started Updating the Search Indexing");
        // Get New data to Index
        using (IndexWriter writer = new IndexWriter(this.idx, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), true, IndexWriter.MaxFieldLength.LIMITED))
        {
            foreach (var e in es)
            {
                writer.AddDocument(this.CreateDocument(e.Value.ToString(), e.Key));
            }

            writer.Optimize();
        }

        this.ESearcher = new IndexSearcher(this.idx);
        this.Log("Completed Updating the Search Indexing");
    }

    public Result ServeRequest()
    {
        var documents = this.Search(this.EntitySearcher, searchTerm);
        //somelogic
        return result;

    }

}

【问题讨论】:

  • 为什么要使用单实例开始?
  • 在timer_elapsed中,关闭定时器,做工作,重新开启定时器
  • @CodeCaster ServiceFabric 将缓存实例,因此它将重用相同的索引。每次为新请求创建索引还有什么意义。
  • 好吧,让我澄清一下:这段代码的哪一部分不是线程安全的?我怀疑ESearcher 你似乎在重用Searcher 实例。
  • @CodeCaster timerThread 正在设置搜索器 (this.searcher = value;) 并且请求服务方法 (public Result ServeRequest()) 正在使用 this.ESearcher 进行搜索。

标签: c# multithreading search lucene.net


【解决方案1】:

这有很多“错误”。

如前所述,锁定并不安全(您需要锁定读取和写入)。

更重要的是,在 Lucene 中有更好的方法来处理这个问题。首先,IndexWriter 本身就是线程安全的。它应该是Directory 的所有者。让不同部分打开/关闭目录通常是“不好的做法”。

NRT(近实时)索引有一种样式,它涉及从 IW 获取 IndexReader,而不是包装目录。

如果索引本质上是只读的并且可能每天/每周等批量重新生成,那么您示例中使用的样式才真正“好”。

我重写了这个例子来展示一些方法。显然,由于这只是测试代码,因此会有细微差别,需要根据用例进行重构/增强...

public class Test
{
    private static object syncObj = new object();

    private System.Threading.Timer timer;

    private Searcher searcher;

    private IndexWriter writer;
    private IndexReader reader;

    public Test()
    {
        writer = new IndexWriter(new RAMDirectory(), new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), true, IndexWriter.MaxFieldLength.LIMITED);
        reader = writer.GetReader();
        searcher = new IndexSearcher(reader);
        timer = new System.Threading.Timer(Timer_Elapsed, null, TimeSpan.Zero, TimeSpan.FromMinutes(3));
    }


    public void CreateDocument(string title, string content)
    {
        var doc = new Document();
        doc.Add(new Field("A", title, Field.Store.YES, Field.Index.NO));
        doc.Add(new Field("B", content, Field.Store.YES, Field.Index.ANALYZED));

        writer.AddDocument(doc);
    }

    public void ReplaceAll(Dictionary<string, string> es)
    {
        // pause timer
        timer.Change(Timeout.Infinite, Timeout.Infinite);

        writer.DeleteAll();
        foreach (var e in es)
        {
            AddDocument(e.Value.ToString(), e.Key);
        }

        // restart timer
        timer.Change(TimeSpan.Zero, TimeSpan.FromMinutes(3));
    }

    public List<Document> Search(string queryString)
    {
        var documents = new List<Document>();
        var parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "B", new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30));
        Query query = parser.Parse(queryString);
        int hitsPerPage = 5;
        var collector = TopScoreDocCollector.Create(2 * hitsPerPage, true);
        searcher.Search(query, collector);

        ScoreDoc[] hits = collector.TopDocs().ScoreDocs;

        int hitCount = collector.TotalHits > 10 ? 10 : collector.TotalHits;
        for (int i = 0; i < hitCount; i++)
        {
            ScoreDoc scoreDoc = hits[i];
            int docId = scoreDoc.Doc;
            float docScore = scoreDoc.Score;
            Document doc = searcher.Doc(docId);
            documents.Add(doc);
        }

        return documents;
    }

    private void Timer_Elapsed(object sender)
    {
        if (reader.IsCurrent())
            return;

        reader = writer.GetReader();
        var newSearcher = new IndexSearcher(reader);
        Interlocked.Exchange(ref searcher, newSearcher);
        Debug.WriteLine("Searcher updated");
    }

    public Result ServeRequest(string searchTerm)
    {
        var documents = Search(searchTerm);
        //somelogic
        var result = new Result();

        return result;

    }
}

注意:

  • 作者“拥有”目录
  • 如果这是一个文件基目录,那么您将拥有OpenClose 方法来创建/处置编写器(用于处理lock 文件)。 RamDirectory 可以被 GC 处理
  • 使用Interlocked.Exchange 而不是lock。所以使用searcher 成员时零成本(这里是龙!)
  • 新文档直接添加到作者中
  • 如果没有添加新文档,IsCurrent() 允许零成本。根据您添加文档的频率,您可能根本不需要计时器(只需调用 Timer_Elapsed - 显然已重命名 - 在 Search 的顶部)。
  • 不要使用Optimize(),这是以前版本的遗留问题,强烈建议不要使用它(性能和磁盘 I/O 原因)

最后,如果您使用的是 Lucene.net v4.8,那么您应该使用 SearcherManager(如另一个答案中所建议的那样)。但是使用接受IndexWriter 的ctor 并将其保持为“单例”(与writer 的范围相同)。它将为您处理锁定和获取新读者。

【讨论】:

  • 投我一票 - 尊重在这里花费的时间和精力。
  • 感谢您的回复,几个问题。创建文档在哪里调用,它是如何创建和更新的。 Iscurrent,它如何知道要索引的新数据。
  • @AndyPook 如何创建索引,因为在您的代码中,我看不到创建所需文档的调用,因为这是被索引的数据
  • 从示例中不清楚es 集合的来源。我的版本假设您不添加到es 集合,而是直接调用CreateDocument,它会创建文档并通过编写器将其添加到索引中。我的版本将索引保存在内存中,而不是每次都重新创建它,如果集合很大,这可能会很昂贵。 (即把它当作一个数据库)。这个想法是将索引管理部分与其他问题隔离开来。所以,也许需要一个Delete 方法。显然,这只是一个示例,您需要对其进行一些修改以使其适合您的需求
  • 我添加了一个ReplaceAll 方法,一些外部“客户端”可以调用该方法来替换索引的全部内容。请注意,这是您的版本不是线程安全的另一个方面。 es 看起来像一个字典 (?),它不是线程安全的,也就是说,如果你在 foreach 运行时添加一个条目,它会很糟糕
【解决方案2】:

而不是使用新的 IndexSearcher。您可以使用“SearcherManager”类。

SearcherManager _searcherManager = 新 SearcherManager(LuceneMapDirectory, null);

然后,搜索如下:

_searcherManager.ExecuteSearch(searcher =>
        {
          //Execute query using <searcher>
        }, ex => { Trace.WriteLine(ex); });

【讨论】:

  • 您应该注意 SearcherManager 仅在 v4.8 中可用
  • Lucene.net 4.8 似乎不再有 SearcherManager.ExecuteSearch。我正在使用 5 月 10 日发布的版本
猜你喜欢
  • 2012-07-06
  • 1970-01-01
  • 1970-01-01
  • 2011-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多