Предыстория
Понадобилось мне добавить на сайт функцию поиска. Первой мыслью было — воспользоваться возможностями 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.
Всем спасибо за внимание! Думал написать небольшую статейку, а получилось… то, что получилось.