Django Framework

индекс
177,76

Django + Sphinx = django-sphinx (?)



Когда мы подготавливали для Хабра свою последнюю статью о 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
15 января 2012, 17:00
91

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

+9
zzeus #
А где пулл реквест от вас в оригинальный django-сфинкс?
0
MechanisM #
Еще как альтернатива github.com/linkedin/indextank-engine & github.com/linkedin/indextank-service Linked in недавно открыли их исходный код.
Сам же пользуюсь django-haystack(из мастера на github — он сильно отличается от стабильных) + solr.
0
MechanisM #
Вернее сильно отличается от зарелизенных версий)
Вот кстати еще github.com/flaptor/indextank-py
я пока их особо не смотрел, хотелось бы послушать мнения по поводу Indextank.
0
MechanisM #
Забыл эту ссылочку еще github.com/flaptor/django-indextank-py-demo
0
Prophet #
В solr хорошо с русскоязычным поиском? Я так понял поддержка стемминга на русском там заявлена.
0
w999d #
хорошо, проверено
+1
w999d #
>способности учитывать особенности русской морфологии, чего не скажешь о его ближайших конкурентах
а solr чем не угодил? wiki.apache.org
0
BasilioCat #
Solr на джаве, джава — тяжела. Например для VPS
0
w999d #
без реальных тестов это не больше чем просто слова. (хотя думаю, что да, скорость индексирования будет на порядок ниже за счет сложности solr)
Но и у сфинкса есть свои минусы, такие как коммерческая лицензия/GPL и отсутствие фасетного поиска из коробки.
0
BasilioCat #
То есть вы предполагаете, что Solr может потреблять меньше памяти, чем Сфинкс? Именно память на VPSе самый ценный и ограниченный ресурс. А уж про внезапную прожорливость джавы до памяти ходят былины ;)
Лицензия — вас никто не заставляет встраивать код Sphinx в свои закрытые приложения, я слабо представляю себе, кому это могло бы понадобится, а использовать его в коммерческих целях как отдельный продукт GPL не запрещает.
0
w999d #
Не знаю даже с какого бока подойти. Насколько ценный ресурс? если лишние 100Мб будут критичными, то да, наверное Sphinx.
0
shodan #
> отсутствие фасетного поиска из коробки

Смотрю, миф конкретно прилип.
0
w999d #
почему миф? покажите как он делается в sphinx родными методами. в solr это сводится к указанию facet=true&facet.field=XXX в запросе
0
magic4x #
Да вот же:
habrahabr.ru/blogs/django/136168/#comment_4529776
В сфинксе это не «фасетки» это группировка — разница только в терминах.
0
w999d #
Ну, не только в терминах. «Из коробки» — я имел в виду, что оно описано в документации и реализуется напрямую, а не с помощью «Мульти-запросов с группировками».
0
magic4x #
Вот вам без мульти-запроса:
s = SphinxClient()
s.SetGroupBy('tags', SPH_GROUPBY_ATTR, "@count desc" )
tags = s.Query('', index=index)

Будет один запрос, а группировка тут и есть add_facet, например, в pyes, просто оно тут так называется.
Группировка в документации описана. «Реализуется напрямую» — даже боюсь спросить что это значит.
0
w999d #
>, SPH_GROUPBY_ATTR, "@count desc" )
> tags = s.Query('', index=index)
достаточно избыточно.
Тем не менее я убедился в том, что есть поддержка в sphinx.
а в tags попадает количество совпадений? или это тоже надо вручную задавать? )
0
w999d #
совпадений в смысле с каждым тегом
0
w999d #
и да, тут опять же был отдельный запрос (s.Query), верно? в solr все можно сделать одним запросом.
+1
shodan #
Правильно ли я понимаю, что если добавить в API новый метод FacetQuery с одним дополнительным параметром (список атрибутов, по которым нужна группировка) — то в Сфинксе, с вашей точки зрения, немедленно «появится» труевая поддержка «фасеточного поиска»?
0
magic4x #
Если честно, для «трушности» не хватает стринговых mva и «правильно» считать все тот же MVA (мой коммент ниже). Да, все честно, это groupby и если в группе два значения, то это все равно группа, но это как раз и отличает фасетки от группировок. Второе может быть и есть в самом поиске, но в питон апи я его не нашел.
0
magic4x #
Раз уж такая пьянка.
Вот прямо сейчас как можно «закостылить» текстовые MVA? Ничего умнее списка md5-хешей я ничего не придумал, например, для тех же тегов: django, django sphinx.
0
shodan #
Пока никак, только сконвертировать в список номеров, действительно. Solr умеет? Где почитать (я плохо ориентируюсь в их документации)? Ну и заодно, наверное про его актуальный набор поддерживаемых сегодня типов где почитать?
0
magic4x #
>Solr умеет?
К сожалению я не помню, делал я такое или нет сам. Солр у меня долго не прожил.
Вот тут речь о multiValued field, в примере:
doc {
    id : 1
    keywords: [ hello, world ]
    ...
}

Тут рассказывают как добавлять доки через CSV, там есть такая «строчка»:

# Example: for the following input
id,tags
101,"movie,spiderman,action"

#to index the 3 separate tags into a multi-valued Solr field called "tags", use
f.tags.split=true

Вот тут дока о «схеме»:
wiki.apache.org/solr/SchemaXml

Фасетки:
wiki.apache.org/solr/SolrFacetingOverview
wiki.apache.org/solr/SimpleFacetParameters

Все сразу:
wiki.apache.org/solr/

Очень наглядно для ElasticSearch (мне понравился больше чем солр, использует ту же библиотеку Lucene):
'{"title" : "One",   "tags" : ["foo"]}'
'{"title" : "Two",   "tags" : ["foo", "bar"]}'
'{"title" : "Three", "tags" : ["foo", "bar", "baz"]}'

"facets" : {
    "tags" : {
        "_type" : "terms",
        "missing" : 0,
        "total": 5,
        "other": 0,
        "terms" : [ {
            "term" : "foo",
            "count" : 2
        }, {
            "term" : "bar",
            "count" : 2
        }, {
            "term" : "baz",
            "count" : 1
        } ]
    }
}

0
shodan #
Ага, спасибо! Будем изучать ;)
0
w999d #
multi-value умеет, типы настраиваются в схеме.
0
magic4x #
Почему запрос-то? Вы видите чтобы я сохранял результат? Вот тут tags = s.Query('', index=index) мы получаем ответ от демона.
Апи собирает все наши запросы в список и только потом делает запрос.
Поэтому когда вы делаете Query на самом деле выполняется AddQuery и RunQuery и получаете results[0] — это такой враппер для ленивых.

>а в tags попадает количество совпадений? или это тоже надо вручную задавать? )
s = SphinxClient()
s.SetGroupBy('tags', SPH_GROUPBY_ATTR, "@count desc" )
tags = s.Query('', index=index)
tags = [(x['attrs']['tags'], x['attrs']['@count']) for x in tags['matches']]
print tags 
{'django': 100, 'sphinx': 30}

Это не совсем честный пример. К сожалению я не нашел способа делать группировку для MVA атрибутов, например, если статья содержит тег 1, а вторая 1&2, то мы получим такой результат: {1: 1, (1, 2): 1}, вместо {1: 2, 2: 1}
В случае пхп апи там есть костыль SetArrayResult, питоновцам не так повезло. Правда мне еще такое не пригождалось. Всякого рода «категории», «бренды» — да, это запросто.

Есть еще один момент %): сфинкс не умеет стринговые mva, а вот солр кажись умеет.
Однако я всеми руками за сфинкс, потому-как в нем есть то одно великое чего нет и в ближайшее время не будет в люцене: RT индексы — это просто железобитонный аргумент в пользу сабжа. Near-realtime, что обещают, не считается.
+1
shodan #
> К сожалению я не нашел способа делать группировку для MVA атрибутов, например, если статья содержит тег 1, а вторая 1&2, то мы получим такой результат: {1: 1, (1, 2): 1}, вместо {1: 2, 2: 1}

Группировка по MVA есть и работает. Если документ принадлежит N группам, он во все N и попадет, все агрегатные функции посчитаются правильно.

Другое дело, что в каждой группе выбирается ровно 1 представитель группы, SQL style. Расширения для выбора M представителей группы пока нет. (Есть, вроде, древний запрос в багтрекере про это даже.) Беда в этом?
0
magic4x #
(headbang)
Вы не представляете как я сглупил, даже стыдно признаться %)
Я каким-то чудом проморгал атрибут @groupby, вместо него я брал значение из нужного поля, типа tags и пока в нем было одно значение оно работало, как только там попадало два и больше я не знал для кого подсчитан @count :)
Спасибо что спросили, порой, да, бревно в глазу не замечу.
0
magic4x #
Понял почему порморгал. Если значение поля на русском (не mva), то атрибут приобретает вид '@groupby': 1174945792 и я его принял за какую-то служебную информацию. В MVA хранится числа, а в случае группировки в groupby записывается член группы, по которому шла группировка.
0
shodan #
Ничего не понял. «Значение поля на русском» это как? :)
0
magic4x #
Например, 'brand_name': 'Оптика' или 'brand_name': 'Massive'
В смысле, текстовое. Я так понимаю оно потом «конвертируется» в число. В частности при группировки по этому атрибуту я получил вот это '@groupby': -8253040684853230141L.

При группировки по tags (MVA):
'@count': 5,
'@groupby': 7,
'tags': [7, 8]

В атрибуте tags два тега, но в groupby записан айди тега, по которому шла группировка. В общем все хорошо.
0
shodan #
А, речь перескочила на группировку по строковым атрибутам (колонкам), что ли? Да, там должно вернуть некий непонятный внутренний группировочный ключ. Однако сам *атрибут* при этом должен быть вполне корректным.
0
w999d #
Эээ… Near-realtime, «что обещают» (а на деле это существует в trunk), это Near-realtime без жестких коммитов («мягкие коммиты») при которых обновляется только индекс, а запись на диск производится позже. При ручных жестких коммитах будет тот же realtime.
0
shodan #
0
pav #
Тоже не понимаю почему очень много людей боятся solr. Русский стеминг есть, фасетный поиск из коробки. Индексировать умеет почти все, удобно и прозрачно умеет делать дельта индексы. И много приятных вещей как по типу mlt (кстати а на Sphinx есть что-то на подобии mlt?).
0
w999d #
highlight, suggest, spellcheck, статистика, отладка, более 30 фильтров, геопоиск…
действительно :3
+1
k0ldbl00d #
Исходя из заголовка:
jango+sphinx = jango-sphinx
можно предположить что sphinx = 0
0
rtm #
Интересно, что Haystack не поддерживает Sphinx из-за проблем с лицензией.
Пришлось использовать Sorl — работает отлично.
+5
shodan #
Парням из haystack достаточно было тупо написать мне письмо, и получить официальное разрешение использовать клиентскую библиотеку. Напишу-ка я им сам, раз они стеснительные.
0
shodan #
Daniel молчит. Нашел другой email адрес, пробую.
+1
MechanisM #
Посмотрите тут: github.com/toastdriven/django-haystack/issues/485
кто-то уже написал бэкенд для Sphinx и Даниэль рассказывает о минусах Sphinx и почему он не добавляет его.
Возможно он так и не получил ваши сообщения. Но там я думаю он точно увидит его. Либо увидят заинтересованные и донесут.

Кстати для тех кому интересно, вот Sphinx-бэкенд для django-haystack: github.com/btimby/sphinx-haystack
0
shodan #
Спасибо за ссылку!

Большая часть т.н. «минусов» вызывает у меня сильное офигение.

Впрочем, я давно убедился, что документацию никто не читает, зато в сложившиеся каким-то образом мифы верят многие.

Сейчас напишу ему туда.
0
MechanisM #
Да, я тоже подумал что он просто когда-то давно чуток покопался со Sphinx и у него сложилось мнение.
Прошло время Sphinx изменился, а он этого не заметил. Я собственно и подразумевал что минусы в кавычках.
0
shodan #
Дык, оно все равно полезное: что людям вот кажется важным, в какие мифы верят. Что стоит приделывать, с чем нужно бороться. ;)

Плюс тот факт, что я с Д. в итоге ведь связался почтой и *раньше*, чем появился комментарий, усиляет мою веру в человечество!!!
0
MechanisM #
Молодец, хорошо расписал там все! Надеюсь скоро займутся этим и в django-haystack бэкенд Shphinx будет «из коробки». Я на твиттере(и в некоторых комнатах IRC) с нужными хэш-тэгами выложил ссылку на твой комментарий. Те кому интересно проголосуют чтобы добавили или доработают тот что я выше по ссылке выложил. Спасибо!
0
shodan #
Ну, вообще говоря, всякие абстрагирующие прокси типа haystack это прикольно, наверное; но я лично больше верю в родные специализированные решения на своем месте, чем кучи абстракций.

Те. считаю, задачу «давайте легко прикрутим поиск к фреймворку X» в идеальном мире должен хорошо решать вообще тот или иной наш нативный интерфейс; следующее допустимое приближение это родной плагин (видимо, это django-sphinx или любой его живенький форк); и только затем обобщенные абстракции типа haystack.

Причем мы ж завсегда готовы работать с авторами подобных штук; пишите письма; обсудим, поможем, приделаем. Однако если обстоятельный комментарий «почему нет» написать человек находит время, а тупо кинуть мне ссылку уже нет, то ничего конструктивного не произойдет, очевидно. Это как бы «намек» желающим форкнуть, доработать итп.
0
EvilX #
Понадобилось сделать поиск на сфинксе. В реализации django-sphinx не понравилось в первую очередь привязка к моделям. А если контент раскидан по связным моделям?
Тогда реализовал вывод через питоновское апи и индексацию по xml.
+1
shodan #
Про варианты. Есть еще один: можно ходить напрямую в Sphinx через SphinxQL, все будет совершенно аналогично работе с обычным MySQL.

Про режимы поиска. Они, может, некоторые мелочи слегка упрощают, но это таки легаси. Я бы советовал таки везде приводиться к MATCH_EXTENDED.

Про релизы. Мы обратно совместимы, практически всегда. Ну те. любая новая версия обязана работать с любыми старыми клиентами и умеренно старыми форматами индексов.
0
Kirax #
А я для себя просто сделал «обертку» python api. То есть, например, одна функция поиска, которая принимает многочисленные опции сфинкса в качестве параметров. Другая функция возвращает объекты по классу модели и результату сфинкса.

А с конфигами ИМХО лучше всё же разбираться вручную, т.к. там есть много полезных вещей, о которых вы можете не подозревать. ;)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.