Сегодня мы поговорим о том, как реализовать быстрый полнотекстовый поиск в
своем приложении, а также замерим производительность Lucene.Net, Full-Text Search in SQL Server 2008 с обычным
оператором Like. Недавно мне на
работе досталось достаточно интересная задача: рассмотреть, как можно
реализовать “google-like search” для рабочей БД.
После долгого анализа и сравнения производительности в качестве
основного движка был выбран Lucene.Net. Этот движок является портом с Java полнотекстового
поискового движка на язык C#. О том, как использовать данный движок, я
написал статью, которая называется "Используем Lucene.Net для высокоточного полнотекстового поиска". Также я рассмотрел возможность использовать в
нашем рабочем проекте Full-Text Search в SQL Server 2008 и внес свои небольшие
свои заметки в статью "Use Full-Text Search in SQL Server 2008 with EntityFramework". Сегодняшняя статья
будет показывать больше характеристики того или иного подхода. Ну что же, давайте
приступим к реализации. Первым делом я решил сопоставить работу движка Lucene.Net с
обычным sql оператором LIKE. Это немного
несравнимые вещи, но я решил дать хотя бы небольшой шанс SQL серверу и проверял его не на холодном
старте. Основной задачей была проверка, как отработает Lucene.NET и поиск с помощью Like на большом объеме
данных. Для замера производительности я написал простенький тест, который
замерял производительность и выводил ее в Output.
private void Get_Search_Score_By_Lucene(string funcName, string text)
{
var stopWatch = new Stopwatch();
Debug.WriteLine("Start : {0} at {1}", funcName, DateTime.Now);
stopWatch.Start();
var res = _luceneService.Search<OrderDocument>("CustomFields", text);
Console.WriteLine(res.SearchResultItems);
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{2:000000}ms ({0:00}.{1:000} sec)", ts.Seconds, ts.Milliseconds, ts.TotalMilliseconds);
Debug.WriteLine("Finish: {0} at {1} [{2}]", funcName, DateTime.Now, elapsedTime);
}
private void Get_Search_Score_By_Like_in_Database(string funcName, string text)
{
var stopWatch = new Stopwatch();
Debug.WriteLine("Start : {0} at {1}", funcName, DateTime.Now);
stopWatch.Start();
_service.SearchOrderInDatabaseByLike(text);
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{2:000000}ms ({0:00}.{1:000} sec)", ts.Seconds, ts.Milliseconds, ts.TotalMilliseconds);
Debug.WriteLine("Finish: {0} at {1} [{2}]", funcName, DateTime.Now, elapsedTime);
}
Давайте немного о
том, что же мы все-таки проверяем. Я создал на SQL сервере таблицу на 20 полей, именуя их в
формате [Feild0…Field19]. Затем создал индекс
в Lucene.Net
и заполнил такую же
структуру. Выглядит это для Lucene.NET как-то так:
В индекс мы
добавляем кучу документов, и каждый из них представляет собой поле в формате "ключ-значение".
Затем я написал тест, который динамически создает
сначала 10 элементов, затем 1000 и, наконец, 10000 тысяч. Больше попросту не
было смысла создавать, так как на таком количестве уже видно было разницу в
поиске. Да и другая причина, по которой я это не сделал, – это ограничение со
стороны реализации на количество элементов, которые можно создать.
Начнем с реализации теста на 10 элементов.
[TestMethod, TestCategory("Integration")]
public void Search_contains_text_for_10_items()
{
CreateDafaultData(10);
//Search full equals
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 10 items (text: simple)", "simple");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 10 items (text: simple)", "simple");
//search part of text
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 10 items for part text (text: %simple%)", "%simple%");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 10 items for part text (text: %simple%)", "%simple%");
//search for part of word
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 10 items for part of word (text: %imple%)", "%imple%");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 10 items for part of word (text: %imple%)", "%imple%");
//search if data not found
var search = "%" + Guid.NewGuid() + "%";
Get_Search_Score_By_Like_in_Database(string.Format("Get_Search_Score_By_Like_in_Database with 10 items for part of word (text: {0})", search)
, search);
Get_Search_Score_By_Lucene(string.Format("Get_Search_Score_By_Lucene with 10 items for part of word (text: {0})", search), search);
}
Его цель заключалась в поиске целого
слова, слова в середине, слово в начале текста и самый сложный вариант – когда
мы не можем найти искомую информацию. Для того чтобы тестировать поиск по
большим тестовым данным, я взял какой-то топик со stackoverflow.
public string BigTestData()
{
return @"Hi I'm creating a simple library system for charity. Now I have a form that allows the users to search for books. To do this work I get a text write in a imput text and use inside a book => book.Name.Contains(text) expression (LINQ+EF).
But I want to go a little further. I'm thinking in mix it up a little big and use the same text to find book title, subtitle, author, publishing house, and serie.
Now I have 3 problems:
Search all columns
Disregarding the order of the input text words
Sort the results for the book that most fit into the input text.
I know these topics are a little vague. But I don't know how or where to started. Someone can get me some help?";
}
Результат
выполнения теста можно увидеть в виде таблицы ниже.
Не удивляйтесь тем
результатам, в которых Like вернул 0.004 сек. Чем больше данных, тем
медленнее работает Like. Особенно плохо, когда он ничего не может найти и
сканирует всю таблицу, как в последнем варианте с поиском GUID-а.
Давайте сразу
сравним на 1000 записей и покажем все в такой же таблице.
Тест идентичный, за
исключением создания вместо 10 элементов – одной тысячи.
public void Search_contains_text_for_1000_items()
{
CreateDafaultData(1000);
//Search full equals
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 1000 items (text: simple)", "simple");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 1000 items (text: simple)", "simple");
//search part of text
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 1000 items for part text (text: %simple%)", "%simple%");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 1000 items for part text (text: %simple%)", "%simple%");
//search for part of word
Get_Search_Score_By_Like_in_Database("Get_Search_Score_By_Like_in_Database with 1000 items for part of word (text: %imple%)", "%imple%");
Get_Search_Score_By_Lucene("Get_Search_Score_By_Lucene with 1000 items for part of word (text: %imple%)", "%imple%");
//search if data not found
var search = "%" + Guid.NewGuid() + "%";
Get_Search_Score_By_Like_in_Database(string.Format("Get_Search_Score_By_Like_in_Database with 1000 items for part of word (text: {0})", search)
, search);
Get_Search_Score_By_Lucene(string.Format("Get_Search_Score_By_Lucene with 1000 items for part of word (text: {0})", search), search);
}
Таблица
результатов приведена ниже.
Здесь результаты уже получше. Как видите, чем сложнее выборка по тексту, тем лучше себя
показывает Lucene.NET, что видно по
последнем тесте.
И последний запуск
на 10000 тысяч элементов показан ниже.
Заметьте, чем
больше данных, тем больше просядет поиск по Like. На ста тысячах записей он достигал у меня
в районе 15-20 секунд. На миллионе, думаю, время пойдет измеряться минутами, что
точно не соответствует “google-like” поиску. Если посмотреть на Lucene.Net, то у него
стабильные результаты. А подумайте, что будет с Like, если вам нужно будет сделать поиск текста
по разным таблицам, которые перед поиском нужно скомбинировать. Результат стает
еще более печальным.
Сама по себе идея
сравнивать полнотекстовый движок с оператором Like была неразумной, потому что и так понятно,
что полнотекстовый поиск здесь выигрывает. Но как быть, если посмотреть со
стороны, какой же все-таки движок лучше использовать: Lucene.NET или MS FTS in SQL Server? Как минимум, у них
разное поведение. Один предоставляет API для работы (Lucene), второй – чистый SQL. Второй хранит индексы на диске и
позволяет управлять ими с помощь таких утилит, как Luke и копирование индекса, это простая
операция copy-paste. В MS FTS управление
происходит через SQL код, и этим всем можно управлять через SQL Server Management Studio. Ниже на рисунке
показано сравнение MS SQL Full-Text Search и Lucene движка. Результаты
сравнения взяты с данного сайта. Я не проделывал, к сожалению, столько
работы, чтобы сравнить эти два движка, из-за нехватки времени.
Как вы понимаете, основные характеристики данных движков зависят от того, сколько места занимает
индекс от оригинальных данных, какую скорость чтения данных обеспечивает тот
или иной движок, и насколько быстро происходит поиск. Ниже показана таблица
этих характеристик в виде рисунка.
Ну и напоследок – измерение производительности
поиска с использованием данных движков на 10 потоках.
Итоги
Выбор способа решения задачи с поиском большого объема текстовых данных зависит только от вас.
Главное – не использовать при этом SQL оператор Like. В каждом из рассмотренных примеров есть
свои плюсы и минусы. Но если рассматривать Lucene.Net или MS SQL FTS, то я бы,
наверное, выбрал то, с чем удобнее работать. Так как я предпочитаю работать
с базой через ORM, то писать чистый row sql код не люблю. А
так как MS SQL Full-Text Search плохо интегрируется с Entity Framework, который очень часто использую, вопрос
отпал практически сам собой. Lucene.NET просто внедрить в ваше приложение, но во
всем есть свои недостатки. Например, по Lucene.Net не так много документации (не путать с
документацией к Lucene движку, написанному на Java, так как там кучи классов нет для C#), а большинство
примеров очень примитивные и покрывают очень малую область применения Lucene. Поэтому вам здесь
предстоит немало работы. А для себя я
открыл новую, еще не проторенную досконально область, которая мне интересна и в
которой можно проявить свои знания и умения.
No comments:
Post a Comment