Django + Sphinx = django-sphinx (?)

artifex 15 января 2012 в 17:00 12,3k


Когда мы подготавливали для Хабра свою последнюю статью о 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
Проголосовать:
+28
Сохранить: