Часто при работе с Django и PostgreSQL возникает необходимость в дополнительных расширениях для базы данных. И если например с hstore или PostGIS (благодаря GeoDjango) всё достаточно удобно, то c более редкими расширениями — вроде pgRouting, ZomboDB и пр. — приходится либо писать на RawSQL, либо кастомизировать Django ORM. Чем я предлагаю, в данной статье, и заняться, используя в качестве примера ZomboDB и его getting started tutorial. И заодно рассмотрим как можно подключить ZomboDB к проекту на Django. У PostgreSQL есть свой полнотекстовый поиск и работает он, судя по последним бенчмаркам, довольно быстро. Но его возможности именно в поиске всё ещё оставляют желать лучшего. Вследствие чего без решений на базе Lucene — ElasticSearch, например, — приходится туго. ElasticSearch внутри имеет свою БД, по которой проводит поиск. Основное решение на текущий момент — это ручное управление консистентностью данных между PostgreSQL и ElasticSearch с помощью сигналов или ручных функций обратного вызова. ZomboDB — это расширение, которое реализует собственный тип индекса, превращая значение таблицы в указатель на ElasticSearch, что позволяет проводить полнотекстовый поиск по таблице, используя ElasticSearch DSL как часть синтаксиса SQL. На момент написания статьи поиск по сети к результатам не привел. Из статей на Хабре про ZomboDB только одна. Статьи по интеграции ZomboDB и Django отсутствуют. В описании ZomboDB сказано, что обращения в Elasticsearch идут через RESTful API, поэтому производительность вызывает сомнения, но сейчас мы ее касаться не будем. Также вопросов корректного удаления ZomboDB без потери данных. Далее все тесты будем проводить в Docker, поэтому соберем небольшой docker-compose файл Последняя версия ZomboDB работает максимум с 10-ой версией Postgres и из зависимостей требует curl (полагаю, чтобы делать запросы в ElasticSearch). Контейнер для Django типичный. В него мы поставим только последние версии Django и psycopg2. ElasticSearch в Linux не стартует с базовыми настройками vm.max_map_count, поэтому нам придется их немного увеличить (кто знает как это автоматизировать через docker — отпишитесь в комментариях). Итак, тестовое окружение готово. Можно переходить к проекту на Django. Целиком я его приводить не буду, желающие могут посмотреть его в репозитории на GitLab. Остановлюсь только на критичных моментах. Первое, что нам нужно сделать, это подключить ZomboDB как extension в PostgreSQL. Можно, конечно, подключиться к базе и включить расширение через SQL CREATE EXTENSION zombodb;. Можно даже для этого использовать docker-entrypoint-initdb.d hook в официальном контейнере для Postgres. Но раз у нас Django, то и пойдем его путем.
После создания проекта и создания первой миграции добавим в нее подключение расширения. Во-вторых, нам нужна модель, которая будет описывать тестовую таблицу. Для этого нам необходимо поле, которое бы работало с типом данных zdb.fulltext. Ну что же, напишем свое. Так как этот тип данных для django ведет себя так же, как и нативный postgresql text, то при создании своего поля мы унаследуем наш класс от models.TextField. Вдобавок нужно сделать две важных вещи: выключить возможность использовать Btree-индекс на этом поле и ограничить backend для базы данных. В конечном результате это выглядит следующим образом: В-третьих, объясним ZomboDB где искать наш ElasticSearch. В самой базе с этой целью используется кастомный индекс от ZomboDB. Поэтому если адрес поменяется, то и индекс нужно изменить.
Django именует таблицы по шаблону app_model: в нашем случае приложение называется main, а модель — article. elasticsearch — это dns-имя, которое докер присваивает по названию контейнера.
В SQL это выглядит так: В Django нам тоже нужно создать кастомный индекс. Индексы там пока еще не очень гибкие: в частности, zombodb индекс указывает не на конкретную колонку, а на всю таблицу целиком. В Django же индекс требует обязательное указание на поле. Поэтому я подменил statement.parts['columns'] на ((main_article.*)), но методы construct и deconstruct по-прежнему требуют указывать атрибут fields при создании поля. Так же нам нужно передать дополнительный параметр в params. Для чего переопределим метод __init__, deconstruct и get_with_params.
В целом, конструкция получилась рабочая. Миграции применяются и отменяются без проблем. Кому такой подход не по душе могут использовать миграции с RunSQL, напрямую добавив индекс. Только придется следить за названием таблицы и индекса самостоятельно. В итоге получилась вот такая модель. ZomboField принимает те же самые аргументы, что и TextField, с одним исключением — index_db ни на что не влияет, так же как и атрибут fields в ZomboIndex. В конечном счёте, файл миграции должен выглядеть следующим образом: Для интересующихся прилагаю SQL, который выдает Django ORM (можно посмотреть через sqlmigrate, ну, или с учетом докера: sudo docker-compose exec django python3 manage.py sqlmigrate main 0001) Итак, модель у нас есть. Осталось теперь сделать поиск через filter. Для этого опишем свой lookup и зарегистрируем его. Поиск в таком случае будет выглядеть следующим образом: Но обычно одного поиска недостаточно. Требуется еще ранжирование результата и подсветка найденных слов.
Ну, с ранжированием всё довольно просто. Пишем свою собственную функцию: Теперь можно строить довольно сложные запросы без особых проблем. Подсветка результата (highlight) оказалась несколько сложнее, красиво не получилось. Django psycopg2 backend в любых ситуациях преобразует имя_колонки в таблица.имя_колонки. Если было text, то будет "main_article"."text", чего ZomboDB категорически не приемлет. Указание колонки должно быть исключительно текстовым именем колонки. Но и здесь нам на помощь приходит RawSQL. Полную версию проекта с тестами можно посмотреть в репозитории. Все примеры из статьи оформлены там в виде тестов. Надеюсь для кого-нибудь эта статья окажется полезной и подтолкнет не писать велосипед на сигналах, с возможностью отстрелить себе всю консистентность, а использовать готовое решение не теряя все положительные стороны ORM. Дополнения и исправления также приветствуются. UPD: Появилась библиотека django-zombodb Разработчик