Monday, August 17, 2015

FSDirectory vs RAMDirectory in Lucene.NET

Здравствуйте, уважаемые читатели. Сегодня мы вновь окунемся в мир Lucene.NET и рассмотрим вопросы производительности RAMDirectory vs FSDirectory для хранения данных. Небольшое предисловие к данной статье, чтобы настроить вас на размышление. Если вы работали с Lucene.NET, то вам наверняка знаком тот факт, что FSDirectory используется для хранения информации о документах в индексе на диске, а RAMDirectory используется для хранения информации о документах в RAM памяти. Теперь, собственно, сам вопрос, – что быстрее будет отрабатывать для сохранения документов: RAMDirectory или же FSDirectory? Давайте рассмотрим данный пример. Для начала создадим новое консольное приложение, назвав его “FastSavingInLucene”.
Затем сразу же поставим сюда через NuGet пакет Lucene.NET для работы с библиотекой для высокоскоростного поиска Lucene.Net.
Теперь переходим к самому примеру. Не так давно я прочитал книгу “Lucene in Action” первого издания (к сожалению, оно очень устарело), для того чтобы понять, как устроено сохранение индексов, поиск по ним и т.д., так как концепция Lucene там осталась. В итоге нашел там отличный пример, который написан на Java. Я адаптировал его на C# и хотел бы привести его вам с объяснением, почему происходит именно так, а не иначе. Создаем новый класс FsVersusRamDirectory и смотрим на его реализацию ниже.
public class FsVersusRamDirectory
{
    #region Variables
    private const string LuceneIndexFolder = "LuceneIndex";

    private FSDirectory _fsDirectory;
    private RAMDirectory _ramDirectory;
    private string _dataFolder;
    private readonly List<string> _documents = LoadDocument(100000, 5);
    #endregion

    #region Public Ctor
    public FsVersusRamDirectory()
    {
        InitializeLucene();
    }
    #endregion

    #region Public Properties
    public string DataFolder
    {
        get { return _dataFolder; }
    }

    public FSDirectory LuceneDirectory
    {
        get { return _fsDirectory; }
    }

    public RAMDirectory RamDirectory
    {
        get { return _ramDirectory; }
    }

    #endregion


    #region Public Methods
    public void TestTiming()
    {
        var ramTiming = TimeIndexerWriter(RamDirectory);
        var fsTiming = TimeIndexerWriter(LuceneDirectory);

        Console.WriteLine("RAMDirectory Time: {0} ms", ramTiming);
        Console.WriteLine("FSDirectory Time: {0} ms", fsTiming);
    }
    #endregion

    #region Private Methods
    private void InitializeLucene()
    {
        _dataFolder = Path.Combine(Environment.CurrentDirectory, LuceneIndexFolder);
        var di = new DirectoryInfo(_dataFolder);

        if (!di.Exists)
        {
            di.Create();
        }

        _fsDirectory = FSDirectory.Open(di.FullName);
        _ramDirectory = new RAMDirectory();
    }

    private static List<string> LoadDocument(int numDocs, int wordsPerDoc)
    {
        var result = new List<string>();
        for (var i = 0; i < numDocs; i++)
        {
            var doc = new StringBuilder(wordsPerDoc);
            for (var j = 0; j < wordsPerDoc; j++)
            {
                doc.Append("Test");
            }
            result.Add(doc.ToString());
        }

        return result;
    }

    private long TimeIndexerWriter(Lucene.Net.Store.Directory directory)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        AddDocuments(directory);

        stopwatch.Stop();

        return stopwatch.ElapsedMilliseconds;
    }

    private void AddDocuments(Lucene.Net.Store.Directory directory)
    {
        using (
            var writer = new IndexWriter(directory, new SimpleAnalyzer(), true,
                IndexWriter.MaxFieldLength.UNLIMITED))
        {
            foreach (var word in _documents)
            {
                var document = new Document();
                var field = new Field("Text", word, Field.Store.NO, Field.Index.NOT_ANALYZED);
                document.Add(field);
                writer.AddDocument(document);
            }
            writer.Optimize();
        }
    }
    #endregion

}
Он очень прост для понимания. У нас есть функция LoadDocuments, которая принимает первым параметром количество документов, вторым – количество слов в нем, а затем создает список документов и возвращает их. Наружу смотрит только функция TestTiming(), которая и замеряет производительность для RamDirectory и FSDirectory. Чтобы запустить этот пример, добавим в наш класс Program следующий код.
class Program
{
    static void Main(string[] args)
    {
        var testData = new FsVersusRamDirectory();
        testData.TestTiming();

        Console.ReadLine();
    } 
}
Теперь давайте соберем наш пример в релизе, запустим его несколько раз и посмотрим на то, что получилось. У меня результат на x64 получился следующим.
Если вы уже плотно работали с Lucene, то вы наверняка знаете, в чем причина такого отличия. Если же нет, тогда небольшое объяснение ниже. Этот тест проходит быстрее для FSDirectory, потому что, во-первых, Windows умеет кешировать файлы, а во-вторых, эти файлы реально маленькие. Судя по результатам, может сложиться впечатление, что RAMDirectory вообще не нужен. По большому счету, это так и есть, но есть сценарии, в которых RAMDirectory лучше походит, чем FSDirectory, – это unit tests. Дело в том, что используя RAMDirectory, мы пытаемся обезопасить себя от возможного мусора, связанного с FSDirectory. Поскольку RAMDirectory потеряла свою актуальность для последних версий Lucene.NET, примеры с RAMDirectory оптимизациями убрали со второго издания книги “Lucene in Action”. Теперь, разобравшись в RAMDirectory vs FSDirectory, пришло время рассмотреть вариант, который работает еще быстрее, чем FSDirectory. Представляю вашему вниманию MMapDirectory для x64 приложений. В Lava для x64 платформ MMapDirectory используется по умолчанию, если вы используете FSDirectory.open(File). Для Lucene.Net это, к сожалению, не актуально, что вы можете увидеть по коду ниже.
Единственное, что изменяется, если версия разная, – так это размер чанков для чтения информации.
Поэтому пока приходится использовать MMapDirectory вручную. Возможно, в более новых версиях Lucene.Net все изменится, и этот выбор будет автоматическим. MMapDirectory использует мапинг файлов (memory-mapped files) для получения доступа к индексу. Реализован в C# он криво: обычный копипаст с Java кода, вместо того чтобы реально заимплементить использование Memory-Mapped Files, доступных в .NET Framework. Использование MMapDirectory в Lucene.NET не очень популярно. Явно его используют по минимуму, а неявно он не используется. Поэтому давайте расширим наш предыдущий пример добавлением MMapDirectory и посмотрим после этого на результат.
public class FsVersusRamDirectory
{
    #region Variables
    private const string LuceneIndexFolder = "LuceneIndex";

    private FSDirectory _fsDirectory;
    private MMapDirectory _mMapDirectory;
    private RAMDirectory _ramDirectory;
    private string _dataFolder;
    private readonly List<string> _documents = LoadDocument(100000, 5);
    #endregion

    #region Public Ctor
    public FsVersusRamDirectory()
    {
        InitializeLucene();
    }
    #endregion

    #region Public Properties
    public string DataFolder
    {
        get { return _dataFolder; }
    }

    public FSDirectory LuceneDirectory
    {
        get { return _fsDirectory; }
    }

    public RAMDirectory RamDirectory
    {
        get { return _ramDirectory; }
    }

    public MMapDirectory MMapDirectory
    {
        get { return _mMapDirectory; }
    }
    #endregion


    #region Public Methods
    public void TestTiming()
    {
        var ramTiming = TimeIndexerWriter(RamDirectory);
        var fsTiming = TimeIndexerWriter(LuceneDirectory);
        var mMapTiming = TimeIndexerWriter(MMapDirectory);

        Console.WriteLine("RAMDirectory Time: {0} ms", ramTiming);
        Console.WriteLine("FSDirectory Time: {0} ms", fsTiming);
        Console.WriteLine("MMapDirectory Time: {0} ms", mMapTiming);
    }
    #endregion

    #region Private Methods
    private void InitializeLucene()
    {
        _dataFolder = Path.Combine(Environment.CurrentDirectory, LuceneIndexFolder);
        var di = new DirectoryInfo(_dataFolder);

        if (!di.Exists)
        {
            di.Create();
        }

        _fsDirectory = FSDirectory.Open(di.FullName);
        _ramDirectory = new RAMDirectory();
        _mMapDirectory = new MMapDirectory(di);
    }

    private static List<string> LoadDocument(int numDocs, int wordsPerDoc)
    {
        var result = new List<string>();
        for (var i = 0; i < numDocs; i++)
        {
            var doc = new StringBuilder(wordsPerDoc);
            for (var j = 0; j < wordsPerDoc; j++)
            {
                doc.Append("Test");
            }
            result.Add(doc.ToString());
        }

        return result;
    }

    private long TimeIndexerWriter(Lucene.Net.Store.Directory directory)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        AddDocuments(directory);

        stopwatch.Stop();

        return stopwatch.ElapsedMilliseconds;
    }

    private void AddDocuments(Lucene.Net.Store.Directory directory)
    {
        using (
            var writer = new IndexWriter(directory, new SimpleAnalyzer(), true,
                IndexWriter.MaxFieldLength.UNLIMITED))
        {
            foreach (var word in _documents)
            {
                var document = new Document();
                var field = new Field("Text", word, Field.Store.NO, Field.Index.NOT_ANALYZED);
                document.Add(field);
                writer.AddDocument(document);
            }
            writer.Optimize();
        }
    }
    #endregion

}
Теперь запустим наш проект несколько раз в релизе и посмотрим на результат. У меня он получился таким, как показано на рисунке ниже.
Как видим, на MMapDirectory немного быстрее по времени, чем FSDirectory.

Итоги
Первое, что можно вынести из данной статьи, – это то, что RAMDirectory не быстрее, чем FSDirectory в большинстве случаев. Второе – MMapDirectory быстрее, чем базовый FSDirectory. Третье – MMapDirectory не используется неявно, а использовать его явно вы можете на свой страх и риск, тем более что все может в любой момент измениться. Чтобы понять, как работает MMapDirectory и в чем его преимущества и недостатки, рекомендую прочитать статью "Use Lucene’s MMapDirectory on 64bit platforms, please!", для того чтобы все стало на свои места. Статья на английском, но очень интересная и занимательная, и если вам нравится работать с Lucene, то прочитать ее стоит хотя бы для того, чтобы расширить немного свой кругозор.