Как подружить Django и Sphinx?

в 10:40, , рубрики: django, django-sphinx, python, sphinx, Песочница, поиск, метки: , , , ,

Предыстория

Понадобилось мне добавить на сайт функцию поиска. Первой мыслью было — воспользоваться возможностями SQL-сервера, — но искать надо сразу по нескольким таблицам, слова и фразы, да ещё и со стеммингом. Понял, что изобретать свой велосипед будет накладно.

Решил поискать, а что же всё-таки есть из готовых решений? Оказалось, прямо скажем, не густо: django-haystack и django-sphinx. Ранее достоинства и недостатки обоих уже перечисляли, поэтому не буду повторяться.

Потратив какое-то время на чтение блогов и форумов, решил всё-таки попробовать django-sphinx, т. к. в django-haystack, насколько мне известно, с поддержкой Sphinx до сих пор не очень.

Автор же django-sphinx давно забросил свой проект, но есть множество форков, и, говорят, что пользоваться им вполне возможно. Я выбрал тот, что был, хм, посвежее и попытался подключить его к своему проекту.

История

Оказалось, что всё очень плохо там — множество ошибок, недоделок, проблемы с Python API Сфинкса.
По началу я пытался просто исправить ошибки в коде и заставить-таки его работать. У меня это даже получилось — я смог искать по одному слову (знатоки справедливо заметят, что SPH_MATCH_ANY решил бы и эту проблему), но об этом флаге я узнал чуть позднее. Да и еще много о чем узнал.

В комментариях к посту, на который я сослался ранее, ругали django-sphinx, что де то не умеет, это не поддерживает. Решил я добавить недостающие возможности — в результате родился очередной форк. Через какое-то время он уже умел индексировать MVA и поля из связанных моделей (документация Sphinx мне показалась местами запутанной — пришлось долго разбираться, что там к чему). Было исправлено множество ошибок и не меньше добавлено… а как иначе?

А затем я решил-таки прочитать раздел, посвященный SphinxQL. И почти полностью переписал django-sphinx.

На данный момент мой форк умеет работать со Sphinx повредством его диалекта SphinxQL и может похвастаться:

  • поддержкой sphinx 2.0.1-beta и выше
  • довольно большой гибкостью в настройке
  • автоматической генерацией конфигурации sphinx
  • возможностью искать как по одному индексу, так и по нескольким сразу
  • возможностью индексировать MVA и поля из связанных один-к-одному моделей в одном индексе
  • поддержкой создания сниппетов
  • привязкой документов из индекса к объектам соответствующих моделей
  • подобными Django ORM методами фильтрации поисковой выдачи (в том числе цепочки методов)

RealTime-индексы пока не поддерживаются, соответственно нет функций для работы с ними (INSERT, UPDATE, DELETE).
Не поддерживается поиск по связанным моделям. И не уверен, что оно вообще нужно. Комментаторы, кто знает, приведите примеры, где и как это можно использовать?

Часть кода уже покрыта тестами (да, попутно учусь писать юнит-тесты — раньше несколько раз пытался начать, но не понимал, с какой стороны вообще к этому занятию подходить)

Кроме того начал писать документацию — пока наброски, но в целом, надеюсь, всё понятно.

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

За основу я возьму вот такие модели:

class Related(models.Model):
    name = models.CharField(max_length=10)

    def __unicode__(self):
        return self.name

class M2M(models.Model):
    name = models.CharField(max_length=10)

    def __unicode__(self):
        return self.name


class Search(models.Model):

    name = models.CharField(max_length=10)
    text = models.TextField()
    stored_string = models.CharField(max_length=100)

    datetime = models.DateTimeField()
    date = models.DateField()
    bool = models.BooleanField()
    uint = models.IntegerField()
    float = models.FloatField(default=1.0)

    related = models.ForeignKey(Related)
    m2m = models.ManyToManyField(M2M)

    search = SphinxSearch(
        index='test_index',
        options={
            'included_fields': [
                'text',

                'datetime',
                'bool',
                'uint',
            ],
            'stored_attributes': [
                'stored_string',
            ],
            'stored_fields': [
                'name',
            ],
            'related_fields': [
                'related',
            ],
            'mva_fields': [
                'm2m',
            ]
        },
    )

В первую очередь, на основе словаря options, переданного аргументом SphinxSearch будет сгенерирован конфиг, в котором:

  • все поля из included_fields будут помещены в индекс, при чем нестроковые поля — в качестве stored-атрибутов
  • все поля из stored_attributes, как вы поняли, тоже станут stored. Этот список может быть полезен, если надо сделать stored текстовое поле
  • поля из stored_fields станут stored, но при этом будут так же доступны для полнотекстового поиска
  • поля из related_fields, Вы уже догадались?, аналогичго будут объявлены как stored. Там будут храниться ключи из связанных моделей (чуть ниже я объясню, зачем)
  • наконец, назначение mva_fields, думаю Вам уже понятно. В этот список можно поместить только названия ManyToMany-полей

Что же нам всё это даёт? А даёт это достаточно большие возможности для поиска.

Получим QuerySet для нашей модели. Это можно сделать двумя способами:

    qs = Search.search.query('query')

либо:

    qs = SphinxQuerySet(model=Search).query('query')

Оба способа дадут похожий результат, но во втором случае не будут учтены параметры, переданные SphinxSearch в описании модели (за исключением списков полей).

Теперь мы можем что-нибудь поискать:

    qs1 = qs.filter(bool=True, uint__gt=100, float__range=(1.0, 15.4)).group_by('date').order_by('-pk').group_order_by('-datetime')

Поясню, что делает этот запрос:

  • ищет в индексе модели Search слово 'query'
  • при этом в выдачу будут включены лишь результаты в которых поле bool содержит Истину, поле uint больше 100, а содержимое поля float находится в диапазоне от 1.0 до 15.4
  • группирует все результаты по дате
  • сортируя их по идентификатору документа в обратном порядке ('pk' приводится к 'id' автоматически)
  • внутри каждой группы сортирует результаты по полю datetime тоже в обратном порядке

Что еще можно сделать?

Например, предположим, что в переменной r хранится QuerySet с несколькими объектами Related, а в m — с M2M (см. модели выше). Тогда можно сделать что-то такое:

    qs2 = qs.filter(related__in=r, m2m__in=m)

    # или
    qs3 = qs.filter(related=r[0])

То есть не требуется самостоятельно подготавливать списки идентификаторов — django-sphinx сделает это за вас!

Ну и напоследок скажу, что SphinxQuerySet ведёт себя как массив.

    # можно взять любой результат по индексу
    doc = qs[5]

    # или срез
    docs = qs[3:20]
    docs = qs[:50]
    docs = qs[100:]

Наконец, чтобы получить значения stored-атрибутов (если они понадобятся по каким-то причинам) или вычисленным выражениями, необходимо обратиться к атрибуту sphinx объекта, полученного из SphinxQuerySet.

Да. Немного о выражениях.
Sphinx умеет вычислять различные формулы на лету для каждого документа (по этому же принципу работает и ранжирование) и позволяет составлять собственные:

    qs4 = qs.fields(expr1='uint*(float+100)')

Результат вычисления Вы сможете найти внутри атрибута sphinx полученных объектов.
Кроме того Sphinx позволяет сортировать выдачу не только по определённому полю, но и по этим выражениям, так что такой код тоже возможен:

    qs4 = qs.fields(expr1='uint*(float+100)').order_by('expr1')

Так о чём это я?

Я надеюсь, что обитатели хабра дадут мне полезные советы (или закидают какашками, если заслужил… и укажут, куда бы мне стоило дальше развивать django-sphinx.

Всем спасибо за внимание! Думал написать небольшую статейку, а получилось… то, что получилось.

Автор: Yuego

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js