Когда мы подготавливали для Хабра свою последнюю статью о Django-батарейках, выяснилось, что про django-sphinx мы таки имеем что рассказать и наш рассказ тянет на отдельный пост. Собственно, вот он, как и обещали.
На сегодняшний день, существует несколько хороших решений для организации поиска в Django. Несколько — это два: Haystack и django-sphinx. Haystack работает с бэкендами-движками solr, whoosh и хapian и, увы, не работает со Sphinx`ом по каким-то абстрактным лицензионным причинам. django-sphinx же, как можно догадаться, работает со Sphinx`ом и только. Haystack это качественный, хорошо документированный и активно развиваемый продукт и мы, вне всяких сомнений, использовали бы именно его, если бы он хоть в какой-нибудь форме поддерживал Sphinx. Но этого, увы, пока не произошло. А Sphinx — наше всё, благодаря его скорости, гибкости и, что очень важно в наших географических широтах, способности учитывать особенности русской морфологии, чего не скажешь о его ближайших конкурентах. «Большие, но по 5… или маленькие, но по 3?»
Так как качество поисковой выдачи всё-таки имеет решающее значение, вопрос с выбором поискового движка особо не стоял. И так как кроме django-sphinx ничего «джангосфинксового» в природе больше нет, то и выбор батарейки был заранее предопределён. Итак:
Хорошо:
- полная поддержка Sphinx API <= 0.9.9
- поисковые запросы через менеджер моделей (SphinxSearch), можно уточнять такие параметры как вес полей или названия индексов прямо в описании класса модели
- на основе указанных параметров умеет автоматически генерировать sphinx-конфиг
- псевдо`queryset (SphinxQueryset) на выходе, что удобно для дальнейшей работы с выдачей
Плохо:
- цепочечные методы не генерируют новые инстансы поискового запроса (пример далее)
- несколько досадных открытых багов в оригинальном пакете django-sphinx (например, exception при использовании метода exclude), хотя они исправлены в нашем форке
- совсем нет тестов, скудная документация
- пакет не поддерживается и больше не развивается своим автором
Можно, конечно, использовать включенный в поставку самого Sphinx`а питоновский API, что как раз предлагал нам magic4x. Есть, впрочем, и третий вариант — написать собственную батарейку, с
С другой стороны, всё не так плохо. Django-sphinx успешно применяется во множестве проектах и, по большому счёту, с работой справляется. Давайте рассмотрим один пример из реального мира.
Есть некая модель, для которой мы хотим организовать поиск:
class Post(models.Model):
...
title = models.CharField(_(u'Заголовок'), max_length=1000)
teaser_text = models.TextField(_(u'Тизер'), blank=True)
text = models.TextField(_(u'Текст'))
...
# менеджер django-sphinx
search = SphinxSearch(weights={'title': 100, 'teaser_text': 80, 'text': 90})
...
Одна из главных причин, по которой мы используем django-sphinx, а не Sphinx API, как настоящие пацаны, это способность django-sphinx автоматически генерировать для нас sphinx-конфиг на основе тех данных, которые мы указали в модели. Для этого имеется специальная менеджмент-команда generate_sphinx_config. Использовать её просто:
$ ./manage.py generate_sphinx_config --all > absolute_path_to_config_file.conf
Кстати говоря, можно создать свой набор шаблонов, по которым будет формироваться конфиг. В этих шаблонах можно указать режим поиска, русский стемминг и прочее, тогда конфиг не придётся подправлять руками. Удобно.
Теперь нам нужно запустить сам демон поисковика. К этой части настройки django-sphinx уже не имеет никакого отношения, используются программы из коробки Sphinx`а.
$ sudo searchd --config absolute_path_to_our_config_file.conf
При первом запуске, searchd ругнётся, что нет индексов и делать ему нечего. Чтобы создать таблицы индексов, нам предоставляется программа indexer, которая в самом простом варианте запускается так:
$ sudo indexer --config absolute_path_to_our_config_file.conf --all --rotate
На этом всё. Разумеется, можно написать для этих нехитрых действий ещё более нехитрые менеджмент-команды, которые создавали бы для каждого разработчика свой конфиг и свой экземпляр sphinxd в системе. Лично мы так и сделали.
Так как же составлять поисковые запросы? Что умеет django-sphinx кроме формирования конфига?
Например, в какой-нибудь вьюхе нужно получить объект поискового запроса. Сделать это очень просто:
...
user_query = self.request.GET['query'] # пользовательский запрос
result = Post.search.query(user_query)
...
Получаем псевдо`queryset-объект result с результатом поиска и некоторыми полезными методами и атрибутами. Например, Sphinx умеет самостоятельно создавать сниппеты поисковой выдачи, которые даже можно немного закастомизировать.
passages_opts = {'before_match': '<span style="background-color: yellow">',
'match': '</span>',
'chunk_separator': '...',
'around': 10,
'single_passage': True,
'exact_phrase': True,
}
result = result.set_options(passages=True,
passages_opts=passages_opts)
Что делает этот код — догадаться нетрудно и в нём нет ничего необычного. Однако, если вам нужна дальнейшая фильтрация выборки (а это почти наверняка так), here be dragons. Всё начинает работать совершенно неожиданным образом.
БАГОФИЧА №1
Для применения методов exclude и filter, необходимо заранее собрать id`шники фильтруемых объектов и передать их в виде распакованного словаря атрибутов (проще показать на примере):
excluded_obj_id_list = [post.id for post in result if post.is_published]
filtered_result = result.exclude(**{'@id__in': excluded_obj_id_list})
И самое внезапное в этом всём то, что последняя операция отработает не так, как от неё ожидается. Честно говоря, совсем не отработает, никакого exclud`а не произойдёт.
БАГОФИЧА №2
Всё работает так, как вы ожидаете только в рамках одной цепочки методов.
filtered_result = Post.search.query(user_query).exclude(**{'@id__in': excluded_obj_id_list})
И это, конечно, порождает не самый эффективный и прозрачный код.
БАГОФИЧА №3
В Сфинксе есть различные режимы поиска. Например, мы хотим установить режим 'SPH_MATCH_ANY' (matches any of the query words). Если сделать это в самой модели, всё работает хорошо.
search = SphinxSearch(weights={'title': 100, 'teaser_text': 80, 'text': 90},
mode='SPH_MATCH_ANY')
Если сделать это в логике, там где мы включаем генерирование сниппетов и их настройки, всё тоже работает хорошо…
result = Post.search\
.query(user_query)\
.exclude(**{'@id__in': excluded_obj_id_list})\
.set_options(passages=True, passages_opts=passages_opts, mode='SPH_MATCH_ANY)
… но сниппетов вы не увидите. Поэтому указывайте режим поиска только в моделях.
В шаблонах же всё достаточно тривиально. Вот так, к примеру, выводятся сниппеты:
{% for post in search_results %}
<div class="g-content">
<a href="{{ post.get_absolute_url }}" class="b-teaser__descr__snippet-link">
{{ post.sphinx.passages.text|safe }}
</a>
</div>
{% endfor %}
Упомянутые «особенности» попили немало крови и я надеюсь, что этот пост сэкономит кому-то из вас время и нервы.
И напоследок. В декабре 2011-го вышел первый за последние несколько лет новый релиз Sphinx`а — версия 2.0.3. django-sphinx же «работает» только с версиями 0.9.7, 0.9.8 и 0.9.9.
1) Sphinx — sphinxsearch.com
2) Оригинальный django-sphinx — github.com/dcramer/django-sphinx
3) Наш форк с некоторыми исправленными багами — github.com/futurecolors/django-sphinx