Использование Lucene.Net в потоковом режиме из веб-приложения asp.net

Итак, я делал некоторые исследования по наилучшему способу реализации поиска Lucene.Net и записи из веб-приложения. Я изложил следующие требования:

  • Необходимо разрешить параллельный поиск и доступ к индексу (запросы выполняются параллельно)
  • будет несколько индексов
  • имеющий индексный поиск, полностью обновлен ( "в режиме реального времени" ) НЕ является обязательным требованием
  • запускать задания для обновления индексов на некоторой частоте (частота для каждого индекса различна)
  • очевидно, хотел бы сделать все это таким образом, который следует за "лучшими практиками" lucene и может хорошо выполнять и масштабировать.

Я нашел полезные ресурсы и пару хороших вопросов здесь, как этот

После этой публикации в качестве руководства я решил попробовать шаблон singleton с параллельным словарем оболочки, созданной для управления индексом.

Чтобы сделать вещи проще, я буду притворяться, что я управляю только одним индексом, и в этом случае оболочка может стать синглом. Это выглядит следующим образом:

public sealed class SingleIndexManager
{
    private const string IndexDirectory = "C:\\IndexDirectory\\";
    private const string IndexName = "test-index";
    private static readonly Version _version = Version.LUCENE_29;

    #region Singleton Behavior
    private static volatile SingleIndexManager _instance;
    private static object syncRoot = new Object();

    public static SingleIndexManager Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (syncRoot)
                {
                    if (_instance == null)
                        _instance = new SingleIndexManager();
                }
            }

            return _instance;
        }
    }
    #endregion

    private IndexWriter _writer;
    private IndexSearcher _searcher;

    private int _activeSearches = 0;
    private int _activeWrites = 0;

    private SingleIndexManager()
    {
        lock(syncRoot)
        {
            _writer = CreateWriter(); //hidden for sake of brevity
            _searcher = new IndexSearcher(_writer.GetReader());
        }
    }

    public List<Document> Search(Func<IndexSearcher,List<Document>> searchMethod)
    {
        lock(syncRoot)
        {
            if(_searcher != null && !_searcher.GetIndexReader().IsCurrent() && _activeSearches == 0)
            {
                _searcher.Close();
                _searcher = null;
            }
            if(_searcher == null)
            {
                _searcher = new IndexSearcher((_writer ?? (_writer = CreateWriter())).GetReader());
            }
        }
        List<Document> results;
        Interlocked.Increment(ref _activeSearches);
        try
        {
            results = searchMethod(_searcher);
        } 
        finally
        {
            Interlocked.Decrement(ref _activeSearches);
        }
        return results;
    }

    public void Write(List<Document> docs)
    {
        lock(syncRoot)
        {
            if(_writer == null)
            {
                _writer = CreateWriter();
            }
        }
        try
        {
            Interlocked.Increment(ref _activeWrites);
            foreach (Document document in docs)
            {
                _writer.AddDocument(document, new StandardAnalyzer(_version));
            }

        } 
        finally
        {
            lock(syncRoot)
            {
                int writers = Interlocked.Decrement(ref _activeWrites);
                if(writers == 0)
                {
                    _writer.Close();
                    _writer = null;
                }
            }
        }
    }
}

Теоретически это должно позволить потокобезопасный экземпляр singleton для индекса (здесь называется "index-test" ), где у меня есть два открытых метода, Search() и Write(), которые могут быть вызваны изнутри Веб-приложение ASP.NET без каких-либо проблем с безопасностью потоков? (если это неверно, сообщите мне).

Была одна вещь, которая сейчас немного беспокоит меня:

Как изящно закрыть эти экземпляры на Application_End в файле Global.asax.cs, чтобы, если я хочу перезапустить мое веб-приложение в IIS, я не собираюсь получать кучу ошибок write.lock и т.д.

Все, что я могу догадываться до сих пор, это:

public void Close()
{
    lock(syncRoot)
    {
        _searcher.Close();
        _searcher.Dispose();
        _searcher = null;

        _writer.Close();
        _writer.Dispose();
        _writer = null;
    }
}

и вызывая это в Application_End, но если у меня есть активные поисковые машины или писатели, это приведет к поврежденному индексу?

Любая помощь или предложения очень ценятся. спасибо.

Ответ 1

Lucene.NET очень потокобезопасен. Я могу с уверенностью сказать, что все методы классов IndexWriter и IndexReader являются потокобезопасными, и вы можете использовать их, не беспокоясь о синхронизации. Вы можете избавиться от всего вашего кода, который включает синхронизацию вокруг экземпляров этих классов.

Тем не менее, большая проблема заключается в использовании Lucene.NET из ASP.NET. ASP.NET перерабатывает пул приложений по нескольким причинам, однако при выключении одного домена приложения он вызывает еще один для обработки новых запросов на сайт.

Если вы попытаетесь получить доступ к тем же физическим файлам (при условии, что вы используете файловую систему на основе FSDirectory) с другим IndexWriter/IndexReader, тогда вы получите сообщение об ошибке в виде блокировки файлов не был выпущен доменом приложения, который еще не был закрыт.

С этой целью рекомендуемая рекомендация - управлять процессом, который обрабатывает доступ к Lucene.NET; обычно это означает создание службы, в которой вы могли бы открыть свои операции с помощью Remoting или WCF (предпочтительно, с последним).

Это больше работает так, как вам придется создавать все абстракции для представления ваших операций), но вы получаете следующие преимущества:

  • Процесс обслуживания всегда будет вверх, а это значит, что клиентам (приложение ASP.NET) не придется беспокоиться о том, чтобы бороться за файлы, которые требуется FSDirectory. Им просто нужно позвонить в службу.

  • Вы абстрагируете свои поисковые операции на более высоком уровне. Вы не получаете доступ к Lucene.NET напрямую, а скорее определяете операции и типы, необходимые для этих операций. Если у вас есть отвлеченный, если вы решите перейти от Lucene.NET к другому механизму поиска (скажем RavenDB), то это вопрос об изменении исполнения контракта.

Ответ 2

  • Открытие IndexWriter может быть тяжелой. Вы можете его повторно использовать.
  • В записи Write (...) для обеспечения транзакционного поведения все документы добавляются и записываются на диск перед возвратом метода. Вызов функции Commit() может быть длительной (это может привести к слиянию сегментов). Вы можете переместить это в фоновый поток, если хотите (который вводит сценарии, в которых некоторые из добавленных документов записываются в commit, а некоторые в другом).
  • В вашем методе поиска (...) нет необходимости в безусловной блокировке. Вы можете проверить, есть ли у вас экземпляр _searcher и использовать его. Он установлен в null в Write (...), чтобы заставить нового искателя.
  • Я не уверен в том, что вы используете метод searchMethod, это похоже на то, что лучше подходит коллекционеру.


public sealed class SingleIndexManager {
    private static readonly Version _version = Version.LUCENE_29;
    private readonly IndexWriter _writer;
    private volatile IndexSearcher _searcher;
    private readonly Object _searcherLock = new Object();

    private SingleIndexManager() {
        _writer = null; // TODO
    }

    public List<Document> Search(Func<IndexSearcher, List<Document>> searchMethod) {
        var searcher = _searcher;
        if (searcher == null) {
            lock (_searcherLock) {
                if (_searcher == null) {
                    var reader = _writer.GetReader();
                    _searcher = searcher = new IndexSearcher(reader);
                }
            }
        }

        return searchMethod(searcher);
    }

    public void Write(List<Document> docs) {
        lock (_writer) {
            foreach (var document in docs) {
                _writer.AddDocument(document, new StandardAnalyzer(_version));
            }

            _writer.Commit();
            _searcher = null;
        }
    }
}

Ответ 3

Вы также можете отключить настройку перекрытия пула приложений в IIS, чтобы избежать проблем с Lucene write.lock, когда один пул приложений закрывается (но все еще держит write.lock), а IIS готовит еще один для новых запросов.