MS SQL Full-Text Search vs Sphinx

Программирование

Tagged Under : , , , , , , , ,

Собственно, к сравнению меня подвигла довольно длинная предыстория, если кратко, то в сети достаточно просто найти сравнения различных средств организации полнотекстового поиска, но я так и не увидел в кандидатах подобных «соревнований» полнотекстовый поиск от Microsoft (он же MS SQL Full-Text Search), который я успешно использую для решения задач.

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

Итак, начнём. Прежде всего поставим задачу: нужно в таблице улиц найти все записи соответствующие запросам. Текст запроса может быть полным названием улицы или же частью названия.

Что имеется в наличии:

  • Машина с ОС Windows 7 Ultimate 64x;
  • ОЗУ: 2Гб, ЦП: Intel Core 2 Duo T5470 1.6ГГц;
  • MS SQL Server 2008 SP1;
  • База (citiesdb) с таблицей улиц (Streets), количество записей – 823276;
  • На таблице висит полнотекстовый индекс на поле Name (размер индекса 23 Мб).

Итак, прежде всего мне потребовалось установить сам Sphinx:

  • Качаем версию 2.0.1-beta (Win32 binaries w/MySQL+PgSQL+libstemmer+id64 support)
  • Распаковываем и содержимое переносим в папку c:sphinx (по крайней мере у меня такая)
  • Настраиваем конфиг
  • source cities
    {
        type            = mssql
        sql_host        = KOSFIZ-PC
        sql_user        = test
        sql_pass        = ******
        sql_db          = citiesdb
        sql_port        = 1433

        mssql_unicode       = 1

        sql_query       =
            SELECT [ID], [Name]
            FROM Streets
    }

    index StreetsIndex2
    {
        source          = cities
        path            = f:indexiesstreets2
        morphology      = stem_enru
        min_word_len        = 2
        charset_type        =  utf-8
        min_prefix_len      = 0
        enable_star     = 1
        html_strip      = 0
    }

    searchd
    {
            address                         = 127.0.0.1
        port                            = 3312
        log                             = c:sphinxlogsearchd.log
        query_log                       = c:sphinxlogquery.log
        read_timeout                    = 5
            max_children                    = 0
            pid_file                        = c:sphinxlogsearchd.pid
    }
  • Строим индекс: indexer.exe –config c:sphinxsphinx.conf.in –all (если сервис searchd уже установлен и запущен добавляем –rotate)
  • Устанавливаем сервис searchd: searchd.exe –install –config c:sphinxsphinx.conf.in –servicename sphinx
  • Запускаем сервис из консоли: net start sphinx

Размер индекса составил 15Мб с небольшим. Время ~ 6 с.

Для тестирования MS SQL Full-Text Search’а таблица была предварительно подготовлена: добавлен полнотекстовый индекс. И хранимая процедура:

CREATE PROCEDURE [dbo].[GetStreets]
    @query nvarchar(400)
AS
BEGIN
    select top 1000 ID, Name from Streets with(nolock)
    where CONTAINS(Name, @query)
END

Отмечу, что при тестировании выборка будет из 1000 строк. FREETEXT я решил не использовать поскольку использование * с ним невозможно, да и к CONTAINS я прикипел всей душой.

Основные методы, используемые в тесте, выглядят следующим образом:

private static void Sphinx(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    using (ConnectionBase connection = new PersistentTcpConnection("localhost", 3312))
    {
        SearchQuery searchQuery = new SearchQuery(query);
        searchQuery.MatchMode = MatchMode.Extended2;
        searchQuery.Indexes.Add("StreetsIndex2");
        searchQuery.Limit = 1000;
        SearchCommand searchCommand = new SearchCommand(connection);
        searchCommand.QueryList.Add(searchQuery);
        searchCommand.Execute();
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);
}

private static void MSFullText(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    DataTable dt = null;

    using (SqlConnection conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "GetStreets";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@query", query);
            SqlDataAdapter adapter = new SqlDataAdapter(cmd);
            dt = new DataTable();
            adapter.Fill(dt);
        }
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);            
}

private static string PrepareString(string query)
{
    return string.Format("FORMSOF(INFLECTIONAL, {0})", query);
}

private static string PrepareStringWithWildcard(string query)
{
    return string.Format(""{0}"", query);
}

Для работы со Sphinx’ом использовал sphinx-dotnet-client.

Были проведены следующие тесты:

  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Тестовая строка «ленина». Приведено среднее время за 1 вызов.
    MS SQL (CONTAINS) = 6,4 мс
    Sphinx = 182,4 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Тестовая строка «ленинск*». Приведено среднее время за 1 вызов.
    MS SQL (CONTAINS) = 4,5 мс
    Sphinx = 189,3 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, поток 1. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 6,3 мс
    Sphinx = 23,6 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, поток 1. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 7,9 мс
    Sphinx = 23,4 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 43 мс
    Sphinx = 41,5 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 30,6 мс
    Sphinx = 40,9 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. На каждом вызове строка случайна из набора в 10000 строк, набор состоит из названий улиц, имеющихся в базе.
    MS SQL (CONTAINS) = 41,7 мс
    Sphinx = 44,2 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. На каждом вызове строка случайна из набора в 10000 строк, набор состоит из названий улиц с добавлением * в конец слова, имеющихся в базе.
    MS SQL (CONTAINS) = 43 мс
    Sphinx = 47 мс

Проведём анализ полученных в ходе тестирования результатов.
В тестах 1 и 2 Sphinx проявил себя крайне плохо, вызвано это тем, что при первом запросе файл индекса помещается в память, т.е. попросту кешируется, но на это требуется значительное время. Хранимая процедура, используемая для Full-Text Search’а, будучи вызванной однажды уже скомпилирована и план запроса закэширован (правда сколько план выполнения пролежит в кэше заранее неизвестно), поэтому на вызов и получение данных затрачено минимальное время.

В тестах 3 и 4 явно видно снижение среднего времени запросов за результатами к Sphinx’у, что объясняется длительным первым запросом и быстрыми остальными. CONTAINS держится на том же временном уровне.

Оставшиеся тесты явно свидетельствуют о том, что при увеличении числа потоков начинает деградировать величина скорость обработки запросов, т.е. поиск начинает подтормаживать. Причём деградация наблюдается и для MS SQL Full-Text Search’а и для Sphinx’а. На коэффициент разный, скорость поиска у первого более сильно зависит от кол-ва потоков, примерно в два раза.

Далее я пробовал увеличивать кол-во потоков, что привело к ещё большему снижению скорости получения результатов, но Sphinx справлялся гораздо лучше с поставленной задачей.

И в тот момент, когда я уже начал смотреть в сторону Sphinx’а как в сторону поискового движка будущих проектов мне в голову пришла простая идея: раз полнотекстовый поиск от MS начинает загинаться при больших кол-вах одновременных запросов, но при этом показывает хорошие результаты, выполняя их по очереди, то почему бы не реализовать своеобразную многозадачность по известному сценарию: выполнять запросы от всех потоков по очереди, т.е. сначала один запрос от первого потока, потом от второго, затем третьего и т.д., потом по второму кругу, третьему и до конца.

В итоге такой подход оправдал себя: среднее время на 10 запросов вернулось к значениям 1 – 4 тестов. Добился я этого изменив метод MSFullText следующим образом:

static object locker = new object();
private static void MSFullText(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    DataTable dt = null;

        lock(locker)
    using (SqlConnection conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "GetStreets";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@query", query);
            SqlDataAdapter adapter = new SqlDataAdapter(cmd);
            dt = new DataTable();
            adapter.Fill(dt);
        }
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);            
}

Поступив аналогично для случая с Sphinx’ом, тоже удастся сократить среднее время поиска и приблизить его к среднему времени выдаваемому полнотекстовым поиском от MS.

Собственно, на этом я прекратил тестирование.

Итак, для чего же я для себя вынес из данного сравнения:

  • Полнотекстовый SQL-поиск от MS при возрастании кол-ва одновременных потоков начинает сильно деградировать в скорости получения результатов, чуть меньше деградирует Sphinx. Но последний хак относительно многопоточности показывает обе технологии поиска в выгодном свете.
  • Для редких одиночных запросов использовать Sphinx нет смысла, ибо он будет уступать полнотекстовому SQL-поиску от MS даже в случае, если план запроса не кеширован (100-140 мс);
  • Использовать поиск от MS удобно тем, что последние изменения сразу же попадают в индекс при включенном Change Tracking’е, а в случае со Sphinx’ом надо делать какое-то расписание или надстройку по отслеживанию изменений в таблице;
  • Sphinx криво работает с *. Например, по строке «лен*» ничего не найдёт, хотя в конфиге явно прописано min_prefix_len = 0. Зато для «ленинск*» отрабатывает корректно;
  • Оба метода поиска требуют предварительной обработки строки поиска, например, Sphinx падает при наличии в строке поиска /. Для учёта морфологии CONTAINS’у нужно прописать FORMSOF и INFLECTIONAL;
  • 6. При 40 потоках полнотекстовый поиск от MS сильно затормозил, Sphinx оборвал соединения и не принимал новые;

  • Sphinx при нагрузке загружает ЦП на 100% при этом много времени начинает отъедать ядро ОС, с MS SQL Full-Text Search ситуация совсем другая, что не может не радовать.


    Первый пик – MS SQL Full-Text Search, второй – Sphinx.
    Для наблюдательных отмечу: да, вы не ошиблись Sphinx пробежался быстрее и среднее время запроса составило 3 мс в то время как MS SQL Full-Text Search отработал в среднем за 5 мс на запрос. Общее кол-во запросов 4000, что соответствует интервалу времени 12-20 секунд.

HtmlAgilityPack: фильтруем html в сообщениях и страницах

Программирование

Tagged Under : , , , ,

О HtmlAgilityPack недавно писали, кстати, тут.

Я же применял библиотеку в работе дважды:

  • При работе с Search Server’ом;
  • При реализации комментирования на одном из проектов.

Причем впервые где-то полгода назад, и по наводке Steve Sanderson‘а.

Тогда передо мной стояла задача «скормить» Search Server’у только желаемый контент со страниц. Дело в том, что его поисковый бот весьма далёк от совершенства и зачастую случается так, что нужно приложить усилия (показывать боту только нужное содержимое), чтобы выдаваемые им результаты стали вполне добротными.
Читать дальше »

Search Server: There was an error generating the XML document

Программирование

Tagged Under : , ,

А если быть точнее, то ошибка имеет следуюий текст:
There was an error generating the XML document. —> The surrogate pair (0xD86E, 0×79) is invalid. A high surrogate character (0xD800 – 0xDBFF) must always be paired with a low surrogate character (0xDC00 – 0xDFFF).

Пытаясь решить данную проблему, есстественно начал гуглить и наткнулся на решение. Это конечно можно было провернуть, но хотелось понять в чем первопричина и это вскоре мне открылось: данная ошибка возникает в связи с тем, что кодировка, в которой находится содержимое страницы, и кодировка обозначенная на странице через meta-тег не совпадают. Конкретный пример: в meta-теге utf-8, а содержимое в windows-1251.
Search server при обходе страницы берёт кодировку из заголовка и помещает в индекс данные со страницы, считая что они в той кодировке, которая указана в мете. Ну, а потом полагаясь на правильность со стороны разработчиков сайта выдаёт всё как есть, ну, а дальше ошибка!!!

P.S.: а смысл прост — указывайте корректную кодировку в заголовке.

И снова поиск: Яндекс.XML + ASP.NET

Программирование

Tagged Under : , , ,

Итак, в продолжении темы использования различных поисковых движков я предпринял попытку воспользоваться сервисом Яндекс.XML и прочувствовать всю мощь российского сервиса.
В результате изучения Яндекс.XML я написал класс YandexSearch (+ парочку вспомогательных). Как видно из исходника я использовал GET-запрос: это на мой взгляд несколько проще чем составлять xml-тело для POST-запроса. Регулируется не всё, но того, что настраивается, вполне достаточно.
Итак, исходник ниже
Читать дальше »

Используем поисковый движок Bing на ASP.NET-сайте

Программирование

Tagged Under : , ,

Ну, что ж продолжим начатое мной в предыдущей статье, посвященной использованию поискового движка гугл на asp.net-сайте, только на этот раз припарировать будет творение microsoft – Bing, который выгодно отличается тем, что не лимитирует количество запросов к своему сервису, а значит может вполне быть использован даже на ресурсах, где кол-во запросов может превышать 1000 в сутки, причем бесплатно в отличие от движков google и yandex.
Читать дальше »

Поиск от google на ASP.NET сайте

Программирование

Tagged Under : , ,

Совсем недавно возникла необходимость внедрить на сайт google’овый поиск, с javascript’ом, несмотря на всю его прелесть, заморачиваться не хотелось, да и кастомизировать это всё дело (результаты и др.) не хотелось, поэтому решил написать небольшой класс.
Читать дальше »