войти зарегистрироваться

Sphinx whois

индекс
0,00

Организуем релевантный поиск по разнородным данным с помощью Sphinx

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

Постановка задачи


В проекте есть на данный момент 2 зоны:
  1. географическая зона, реализованная на базе Google Maps, которая отображает нанесенные пользователями на карту географические объекты (маркеры, маршруты и области);
  2. информационная зона, которая представляет собой большой иерархически организованный каталог, содержащий информационные материалы.
Необходимо было решить задачу одновременного текстового поиска по 3-м типам объектов: географическим объектам, категориям информационной зоны и материалам информационной зоны — c возможностью фильтрации по дате публикации объектов и категориям, к которым они относятся.

Решение задачи


Все решение описано для связки PHP5 (Symfony), MySQL, Sphinx. Как ставить Sphinx, я описывать не буду, эту информацию можно прочитать на официальном сайте. Скажу лишь, что под Mac OS X он легко ставиться с помощью macports.
Имеем такую модель БД (я ее упростил, чтобы было поближе к сути) с каким-то набором записей:
Конфигурируем sphinx для индексации и выдачи результатов поиска:
  1. #articles
  2. source article
  3. {
  4.      type              = mysql
  5.      sql_host          = localhost
  6.      sql_user          = root
  7.      sql_pass          = root
  8.      sql_db             = ili_lv
  9.      sql_sock          = /tmp/mysql/mysql.sock
  10.      sql_query_range    = SELECT MIN(id), MAX(id) FROM article
  11.      sql_range_step     = 500
  12.      sql_query_pre     = SET NAMES utf8
  13.      sql_query         = \
  14.             SELECT id * 10 + 1 as id, category_id, 1 as row_type,\
  15.             UNIX_TIMESTAMP(created_at) as created_at, title, descr \
  16.             FROM article WHERE id >= $start AND id <= $end
  17.      sql_attr_uint     = category_id
  18.      sql_attr_uint     = row_type
  19.      sql_attr_timestamp = created_at
  20.      sql_query_info     = SELECT title, descr \
  21.                          FROM article WHERE id = ($id - 1) / 10
  22. }
  23. #categories
  24. source category
  25. {
  26.   #аналогичный блок параметров подключения к БД
  27.   #...
  28.      sql_query_range    = SELECT MIN(id), MAX(id) FROM category
  29.      sql_range_step     = 500
  30.      sql_query_pre     = SET NAMES utf8
  31.      sql_query         = \
  32.             SELECT id * 10 + 2 as id, tree_parent as category_id, 2 as row_type,\
  33.             UNIX_TIMESTAMP(created_at) as created_at, title, descr \
  34.             FROM category WHERE id >= $start AND id <= $end
  35.      sql_attr_uint     = category_id
  36.      sql_attr_uint     = row_type
  37.      sql_attr_timestamp = created_at
  38.      sql_query_info     = SELECT title, descr \
  39.                          FROM category WHERE id = ($id - 2) / 10
  40. }
  41. #geo_objects
  42. source geo_object
  43. {
  44.   #аналогичный блок параметров подключения к БД
  45.   #...
  46.      sql_query_range    = SELECT MIN(id), MAX(id) FROM geo_object
  47.      sql_range_step     = 500
  48.      sql_query_pre     = SET NAMES utf8
  49.      sql_query         = \
  50.             SELECT id * 10 + 3 as id, 0 as category_id, 3 as row_type,\
  51.             UNIX_TIMESTAMP(created_at) as created_at, title, descr \
  52.             FROM geo_object WHERE id >= $start AND id <= $end
  53.      sql_attr_uint     = category_id
  54.      sql_attr_uint     = row_type
  55.      sql_attr_timestamp = created_at
  56.      sql_query_info     = SELECT title, descr \
  57.                          FROM geo_object WHERE id = ($id - 3) / 10
  58. }
  59. index site_search
  60. {
  61.      source             = category
  62.      source             = geo_object
  63.      source             = article
  64.     
  65.      path              = /var/data/sphinx/site_search
  66.      docinfo            = extern
  67.      morphology         = stem_en, stem_ru
  68.      html_strip         = 0
  69.      charset_type      = utf-8
  70.      min_word_len      = 2
  71. }
* This source code was highlighted with Source Code Highlighter.

Чуть по-подробнее о параметрах конфигурации. Разделы source, как понятно из названия, задают хранилища данных, откуда будет извлекаться индексируемая Sphinx информация. Такими хранилищами могут быть базы данных, текстовые файлы, html-файлы, xml и даже почтовые ящики. Этот раздел также описывает, какие поля хранилища будут индексироваться, в каком формате будет производиться индексация (выборка разовая или порционная) и ряд других параметров. В моем случае описано 3 source, все они ведут в одну и ту же базу данных MySQL, но в разные таблицы.
Форматы конфигураций похожи, я опишу source article.
  1.      sql_query_range    = SELECT MIN(id), MAX(id) FROM article
  2.      sql_range_step     = 500
* This source code was highlighted with Source Code Highlighter.
Этими строками мы «указываем» Sphinx делать выборку из таблицы не полным select-ом, а порциями по 500 записей, чтобы не создавать избыточную нагрузку при индексации.

  1.      sql_query         = \
  2.             SELECT id * 10 + 1 as id, category_id, 1 as row_type,\
  3.             UNIX_TIMESTAMP(created_at) as created_at, title, descr \
  4.             FROM article WHERE id >= $start AND id <= $end
* This source code was highlighted with Source Code Highlighter.
 Это маска запроса, отправляемого Sphinx при индексации данных. Здесь важно 3 момента:
  • Определяется набор полей для индексации, в нашем случае это id, текстовые поля и поля-фильтры;
  • Первое поле используется Sphinx-ом как id в формируемом индексе. Т.к. id из разных таблиц могут совпадать, то применен такой метод формирования уникального id;
  • Поле row_type дает возможность определить, какого типа каждая из сохраненных записей в индексе Sphinx.

Далее идет описание атрибутов, которые можно использовать в качестве фильтров
  1.      sql_attr_uint     = category_id
  2.      sql_attr_uint     = row_type
  3.      sql_attr_timestamp = created_at
* This source code was highlighted with Source Code Highlighter.

Ну и последний параметр — это маска запроса, который будет извлекать нужную нам информацию по найденным id:
  1.     sql_query_info     = SELECT title, descr \
  2.                          FROM geo_object WHERE id = ($id - 1) / 10
* This source code was highlighted with Source Code Highlighter.

Далее в конфигурационном файле описывается самое важное — параметры индексации указанных нами source-ов с помощью секции index.
  1.      source             = category
  2.      source             = geo_object
  3.      source             = article
* This source code was highlighted with Source Code Highlighter.

Очень важный момент — индекс может формироваться из нескольких source. Как показано выше, в индекс сливаются данные из трех таблиц. Представьте, как пришлось бы попотеть, чтобы организовать такой поиск с помощью БД! Здесь же мы просто можем делать запрос к данному индексу, получая при этом его отранжированные результаты.

Строчками
  1.      path             = /var/data/sphinx/site_search
  2.      docinfo            = extern
* This source code was highlighted with Source Code Highlighter.
указываются параметры хранения индекса и полный путь к нему.

В чем еще одна прелесь Sphinx — он «из коробки» поддерживает английскую и русскую морфологию, позволяя приводить слова запроса к нормальной форме. При необходимости эту функциональность можно расширить
  1.     morphology         = stem_en, stem_ru
* This source code was highlighted with Source Code Highlighter.

Оставшиеся три параметра отвечают за вырезание html-тегов, кодировку индекса и минимальную длину слова соответственно.

Далее осталось только запустить индексацию.
  1. muxx:~ muxx$ sudo searchd --stop
  2. Sphinx 0.9.8.1-release (r1533)
  3. Copyright © 2001-2008, Andrew Aksyonoff
  4. using config file '/usr/local/etc/sphinx.conf'...
  5. stop: succesfully sent SIGTERM to pid 5677
  6. muxx:~ muxx$ sudo indexer --all
  7. Sphinx 0.9.8.1-release (r1533)
  8. Copyright © 2001-2008, Andrew Aksyonoff
  9. using config file '/usr/local/etc/sphinx.conf'...
  10. indexing index 'site_search'...
  11. collected 759 docs, 0.0 MB
  12. sorted 0.0 Mhits, 100.0% done
  13. total 759 docs, 22171 bytes
  14. total 0.028 sec, 785871.25 bytes/sec, 26903.45 docs/sec
  15. muxx:~ muxx$ sudo searchd
  16. Sphinx 0.9.8.1-release (r1533)
  17. Copyright © 2001-2008, Andrew Aksyonoff
  18. using config file '/usr/local/etc/sphinx.conf'...
  19. creating server socket on 127.0.0.1:3312
  20. muxx:~ muxx$ search мой сложный запрос
  21. Sphinx 0.9.8.1-release (r1533)
  22. Copyright © 2001-2008, Andrew Aksyonoff
  23. using config file '/usr/local/etc/sphinx.conf'...
  24. index 'site_search': query 'мой сложный запрос ': returned 0 matches of 0 total in 0.000 sec
  25. words:
  26. 1. 'мо': 0 documents, 0 hits
  27. 2. 'сложн': 0 documents, 0 hits
  28. 3. 'запрос': 0 documents, 0 hits
  29. muxx:~ muxx$
* This source code was highlighted with Source Code Highlighter.

В листинге выше мы сначала останавливаем демон на случай, если он запущен. Затем выполняем индексацию. Можно увидеть, насколько высока скорость индексации у Sphinx.

// В комментариях подсказали, что можно производить индексирование, не останавливая демон командой sudo indexer --rotate --all.

Затем запускаем демон и выполняем пробный запрос. Sphinx показывает, как он разбивает запрос и нормализует слова в нем. В моем примере он отработал нормально, но ничего не нашел :)

После того, как удостоверились, что демон работает, можно работать со Sphinx из Symfony.
Устанавливаем плагин sfSphinxPlugin, подключаем его в конфигурациях:
  1. $this->enablePlugins(array('sfSphinxPlugin'));
* This source code was highlighted with Source Code Highlighter.

и пишем небольшой пример запроса к демону:
  1.   $sphinx = new sfSphinxClient($options);
  2.         
  3.   //устанавливаем числовые фильтры, если они заданы
  4.   if ($request->getParameter('category_id'))
  5.      $sphinx->setFilter('category_id', array($request->getParameter('category_id')));
  6.   if ($request->getParameter('row_type'))
  7.      $sphinx->setFilter('row_type', array($request->getParameter('row_type')));
  8.   $dateRange = $request->getParameter('date');
  9.   if ($dateRange['from'] || $dateRange['to'])
  10.   {
  11.      $sphinx->setFilterRange('created_at',
  12.                              !empty($dateRange['from']) ? strtotime($dateRange['from']) : '',
  13.                              !empty($dateRange['to'])  ? strtotime($dateRange['to'])  : '');
  14.   }
  15.   $this->results = $sphinx->Query($request->getParameter('s'), 'site_search');
  16.   if ($this->results === false)
  17.   {
  18.      $this->message = 'Запрос не выполнен: ' . $sphinx->GetLastError();
  19.   }
  20.   else
  21.      //если все путём, то достаем информацию по id индекса
  22.      //и выводим ее в template
  23.      $this->items = $this->retrieveResultRows($this->results);
* This source code was highlighted with Source Code Highlighter.

Надеюсь, из моего описания можно оценить все прелести Sphinx, я рассказал далеко не обо всех его возможностях, остальное при желании вы уже сможете самостоятельно изучить.

PS: Просьба к тем у кого достаточно кармы — создайте блог Sphinx, я бы перенес туда статью.

PS2: Спасибо всем! Блог создан, топик перенесен туда.

Кросс-пост в моем блоге
_________
Текст подготовлен в ХабраРедакторе

комментарии (39)

  • Статья хорошая. Но вот такой вопрос. Я бы в такой ситуации поступил так — сделал бы из трез таблиц view с дополнительным полем source=enum{article, category, geo} а потом из этого view сделал бы таблицу с полнотекстовым поиском(ну или можно было бы сделать триггеры на изменение данных в исходных таблицах). Конечно морфологии тут не было бы(не postgres ведь)). В чем приемущество сфинкса перед таким вариантом?
    • Ну вот как раз в морфологии, и с релевантностью у сфинкса будут дела по-лучше, чем у mysql.
    • Ну вот видите, сколько действий вам нужно делать, чтобы организовать поиск с помощью БД. Плюс вы столкнетесь со следующими проблемами:
      1. БД сможет использовать только полнотекстовый индекс, а если у вас еще будет фильтрация по другим параметрам, это будет производиться без индексов, т.е. медленно. В статье я поставил простую задачу, на самом деле мне нужно было фильтровать записи по десятку параметров;
      2. В Sphinx существует множество режимов поиска, в каждом из них идут свои «бонусы»;
      3. С помощью прстого указания между режимами SPH_MATCH_ALL, SPH_MATCH_ANY можно задать поиск по всех словам из фразы или по любому из слов и все это с учетом морфологии;
      4. В режиме SPH_MATCH_EXTENDED, помимо стандартных операторов AND, OR, NOT можно задавать близость слов: «example program»~5 – такое условие говорит Sphinx, что между словами example и program должно быть не более 5-ти слов; а также порог на количество слов: «Петя Пупкин пошел гулять по лесу»/3 возвращает те записи, где встречается хотя бы 3 из 6 слов в заданной фразе.
      5. В Sphinx введен режим SPH_MATCH_FULLSCAN, когда поисковая фраза пустая и заданы только фильтры и группирование. В документации пишут, что выборка записей по фильтрам идет в некоторых случаях даже быстрее, чем в MySQL. Я также перевел некоторые запросы с большими условиями на Sphinx, чтобы разгрузить БД.
      6. В индекс может понадобиться включить что-то не хранящееся в БД, а Sphinx, как я писал, умеет искать и по xml, html, почте и др.
    • На более-менее приличной по объему БД сфинкс в разы (а то и в десятки раз) сделает мускуль по производительности. Проверено.
  • Большое спасибо за статью! Как раз уже начинал копать в сторону сфинкс, скоро придется тоже поиск организовывать на сайте.
    А кармы, я думаю, у вас будет скоро много(я принял в этом участие :) ) и сможете сами создать блог)
  • Демона останавливать не обязательно, достаточно indexer'у дать параметр --rotate название_индкса или --rotate all
    • Да, спасибо, это очень ценно, упустил этот момент.
  • Создал: habrahabr.ru/blogs/sphinx

    Спасибо за статью, для своих проектов на symfony использовали lucene. Теперь, думаю стоит посмотреть в сторону Sphinx.
    • Спасибо за блог!
  • Он только под Виндовс? Жаль
    • Нет, вы что) ставится везде, где только можно — и Linux, и макось.
    • Вот список из оф. документации:
      – Linux 2.4.x, 2.6.x (various distributions)
      – Windows 2000, XP
      – FreeBSD 4.x, 5.x, 6.x
      – NetBSD 1.6, 3.0
      – Solaris 9, 11
      – Mac OS X
    • нет
    • Кто он? Sphinx кросс-платформенный
  • >> Представьте, как пришлось бы попотеть, чтобы организовать такой поиск с помощью БД!
    те же самые 3 запроса, которые использовались при индексации, объединённые через UNION.
    • Я имел ввиду в том числе и эффективность выполнения таких запросов.
    • Если записей, скажем, 4млн, сфинкс оставит далеко позади mysql по скорости, тем более при полнотекстовом поиске.
      • прочитай внимательнее его абзац :-)
        это предложение преподносится в контексте того, что «как круто, сфинкс умеет индексировать из N источников». Т.е. фичей преподносится возможность индексации кучи сорсов, а не производительность — с этой точки зрения мой комментарий вполне уместен: mysql через UNION может делать то же самое. м?

        по поводу скорости — отлично представляю насколько сфинкс куче mysql.
  • Да и для Symfony есть неплохой плагин, позволяющий работать с Sphinx
  • Создание индекса Sphinx'а надо периодически запускать, или он это делает сам?
    • Да, надо запускать, повесить в кроне задачу, например.
  • Я бы создал все-таки три индекса. Sphinx API позволяет искать по нескольким индексам сразу. Зато при наличии трех индексов вместо одного мы можем спокойно искать по каждому из них отдельно в случае надобности.
    И второе преимущество различных индексов — не приходится вводить лишнии манипуляции с id-шниками
    • Фильтр по row_type позволить искать по отдельным индексам, а со вторым в принципе согласен.
  • Чтобы не писать конфиг коннекта в каждом source, его можно определить в отдельном source (скажем, generic) и во всех остальных его наследовать. Так будет много проще
    • НЛО прилетело и опубликовало эту надпись здесь.
      • source generic {конфиг коннекта}
        source foo : generic {конфиг для нашего конктретного source}
        • НЛО прилетело и опубликовало эту надпись здесь.
        • НЛО прилетело и опубликовало эту надпись здесь.
  • По поводу источника данных.
    View — я бы не советовал использовать, так как при достаточно большом обьеме данных, даже используя частичную выборку из view вы положите базу данных ($start, $stop). Плюс если вы хотите действительно не напрягать mysql то не слудет при обращении в view делать какие либо условия на больших обьемах данных (например: where source = 'article', limit и тд, но это только пример 8) ).

    Стоит делать отдельные индексы (article, category, geo).
    Какие плюсы мы получим:
    1. индексы могут содержать разное количество полей, часть полей могут быть в будущем использованы для специфического поиска к примеру по категориям.
    2. можно гибко работать с весами для разных индесков
    3. можно производить реиндексацию только изменившегося индекса (при полной реиндексации), а это снижение нагрузки. В данном случае чаще будет обновляться article, чем category и geo.

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

      PS. Эту статью меня попросили перевести на английский, надеюсь, это поспособствует популяризации Sphinx за рубежом :)
      • когда же будет продолжение?
  • Уже приходилось прикручивать Sphinx к Symfony для организации поиска на новой версии проекта своей компании. Что я могу сказать по этому поводу. Порадовала производительность — поиск просто летает, страницы с поисковыми запросами отдаются быстрее всех на сайте! В то же время пришлось немало помучиться, наступив на неслабое количество граблей. Во-первых крайне не понравился написанный на PHP 4 и крайне неудобный в использовании Sphinx API. Я решил написать более удобную обертку, использующую всю мощь ООП в PHP 5. В не меньшей степени раздражало и необходимость применения сфинксовых костылей типа метода SetArrayResult(). Ну и самый пипец был уже после выкладки — неожидано выяснилось, что пустой поиск поиск по одному из индексов выдает довольно странную ошибку. К счастью она не я первый на нее наткнулся:

    www.sphinxsearch.com/forum/view.html?id=2070

    В индексе не было ни одного атрибута, поэтому возникла ошибка. Добавление атрибута решило проблему.

    Одним словом вывод один — для работы со Sphinx нужно хорошо уметь работать напильником :).
    • Такая же история ;) И класс sfSphinxClient толком наследовать не получается, потому как private $res, а чужой код мы стараемся не изменять ;)
      С вашей версией обёртки ознакомиться где-нибудь можно?
  • SELECT id * 10 + 1 as id — скажите пожалуйста о какой уникальности здесь идёт речь ??
    К примеру:
    21 * 10 + 1 = 211
    22 * 10 + 1 = 221
    Собственно хочется спросить — и чо? В чём разница между 21, 22 и 211, 221?
    • Понятно, что id одной таблицы уникальны. Здесь же, если вы заметили, объединяются записи из 3-х таблиц, а в этом случае у нас вполне могут попасться записи с одинаковым id=21, например. Поэтому для первой таблицы 21 * 10 + 1 = 211, для второй 21 * 10 + 2 = 212 и т.п. Таким способом можно объединить до 10-ти таблиц.
      Если этого не хватает, меняем 10 на 100, и проблема решается на долгую перспективу.
  • я просто думал, что должно быть какое то по оригинальней решение ))
    Ведь если, грубо говоря, в одной таблице записей не больше 50, а во второй больше 500, то даже делая id * 10 + 1 as id, то идентификаторы всё равно будут пересекаться… т.е. это далеко не универсальное решение. Так что эта «перспектива» весьма туманна. Я как то читал на баше пост, где тип жаловался на инфляцию и говорил, что ему в его какой то биллинговой софтине пришлось все типы дынных int заменить на long из-за того, что цены быстро растут)))

    P.S. Всё равно не ясно, что в этой комбинации решает еденица — id * 10 + 1? ))
    • с чего бы им пересекаться? все идентификаторы первой таблицы кончаются на 1, второй — на 2.
  • с чего бы им пересекаться? все идентификаторы первой таблицы кончаются на 1, второй — на 2.
  • подключайтесь к переводу мануала:
    translated.by/you/sphinx-0-9-9-reference-manual/trans/
Только авторизованные пользователи могут оставлять комментарии. Авторизуйтесь, пожалуйста.