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
    Поделиться публикацией

    Похожие публикации

    Комментарии 50
      +9
      А где пулл реквест от вас в оригинальный django-сфинкс?
        0
        Еще как альтернатива github.com/linkedin/indextank-engine & github.com/linkedin/indextank-service Linked in недавно открыли их исходный код.
        Сам же пользуюсь django-haystack(из мастера на github — он сильно отличается от стабильных) + solr.
          0
          Вернее сильно отличается от зарелизенных версий)
          Вот кстати еще github.com/flaptor/indextank-py
          я пока их особо не смотрел, хотелось бы послушать мнения по поводу Indextank.
          0
          В solr хорошо с русскоязычным поиском? Я так понял поддержка стемминга на русском там заявлена.
          • НЛО прилетело и опубликовало эту надпись здесь
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Solr на джаве, джава — тяжела. Например для VPS
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                То есть вы предполагаете, что Solr может потреблять меньше памяти, чем Сфинкс? Именно память на VPSе самый ценный и ограниченный ресурс. А уж про внезапную прожорливость джавы до памяти ходят былины ;)
                Лицензия — вас никто не заставляет встраивать код Sphinx в свои закрытые приложения, я слабо представляю себе, кому это могло бы понадобится, а использовать его в коммерческих целях как отдельный продукт GPL не запрещает.
                • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  > отсутствие фасетного поиска из коробки

                  Смотрю, миф конкретно прилип.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Да вот же:
                      habrahabr.ru/blogs/django/136168/#comment_4529776
                      В сфинксе это не «фасетки» это группировка — разница только в терминах.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Вот вам без мульти-запроса:
                          s = SphinxClient()
                          s.SetGroupBy('tags', SPH_GROUPBY_ATTR, "@count desc" )
                          tags = s.Query('', index=index)
                          

                          Будет один запрос, а группировка тут и есть add_facet, например, в pyes, просто оно тут так называется.
                          Группировка в документации описана. «Реализуется напрямую» — даже боюсь спросить что это значит.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  +1
                                  Правильно ли я понимаю, что если добавить в API новый метод FacetQuery с одним дополнительным параметром (список атрибутов, по которым нужна группировка) — то в Сфинксе, с вашей точки зрения, немедленно «появится» труевая поддержка «фасеточного поиска»?
                                    0
                                    Если честно, для «трушности» не хватает стринговых mva и «правильно» считать все тот же MVA (мой коммент ниже). Да, все честно, это groupby и если в группе два значения, то это все равно группа, но это как раз и отличает фасетки от группировок. Второе может быть и есть в самом поиске, но в питон апи я его не нашел.
                                      0
                                      Раз уж такая пьянка.
                                      Вот прямо сейчас как можно «закостылить» текстовые MVA? Ничего умнее списка md5-хешей я ничего не придумал, например, для тех же тегов: django, django sphinx.
                                        0
                                        Пока никак, только сконвертировать в список номеров, действительно. Solr умеет? Где почитать (я плохо ориентируюсь в их документации)? Ну и заодно, наверное про его актуальный набор поддерживаемых сегодня типов где почитать?
                                          0
                                          >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
                                            Ага, спасибо! Будем изучать ;)
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                        0
                                        Почему запрос-то? Вы видите чтобы я сохранял результат? Вот тут 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
                                          > К сожалению я не нашел способа делать группировку для MVA атрибутов, например, если статья содержит тег 1, а вторая 1&2, то мы получим такой результат: {1: 1, (1, 2): 1}, вместо {1: 2, 2: 1}

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

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

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

                                                  В атрибуте tags два тега, но в groupby записан айди тега, по которому шла группировка. В общем все хорошо.
                                                    0
                                                    А, речь перескочила на группировку по строковым атрибутам (колонкам), что ли? Да, там должно вернуть некий непонятный внутренний группировочный ключ. Однако сам *атрибут* при этом должен быть вполне корректным.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Тоже не понимаю почему очень много людей боятся solr. Русский стеминг есть, фасетный поиск из коробки. Индексировать умеет почти все, удобно и прозрачно умеет делать дельта индексы. И много приятных вещей как по типу mlt (кстати а на Sphinx есть что-то на подобии mlt?).
                              • НЛО прилетело и опубликовало эту надпись здесь
                              +1
                              Исходя из заголовка:
                              jango+sphinx = jango-sphinx
                              можно предположить что sphinx = 0
                                0
                                Интересно, что Haystack не поддерживает Sphinx из-за проблем с лицензией.
                                Пришлось использовать Sorl — работает отлично.
                                  +5
                                  Парням из haystack достаточно было тупо написать мне письмо, и получить официальное разрешение использовать клиентскую библиотеку. Напишу-ка я им сам, раз они стеснительные.
                                    0
                                    Daniel молчит. Нашел другой email адрес, пробую.
                                      +1
                                      Посмотрите тут: github.com/toastdriven/django-haystack/issues/485
                                      кто-то уже написал бэкенд для Sphinx и Даниэль рассказывает о минусах Sphinx и почему он не добавляет его.
                                      Возможно он так и не получил ваши сообщения. Но там я думаю он точно увидит его. Либо увидят заинтересованные и донесут.

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

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

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

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

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

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

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

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

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

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

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

                                          Самое читаемое