Как интегрировать Elasticsearch с DRF
Из этой статьи Вы узнаете, как создать простой поисковый API с помощью Elasticsearch и Django-Rest-Framework (DRF).
Elasticsearch - это ведущая поисковая система в мире.
Многие крупные компании, например, Uber и Slack, используют именно ее, но использовать эту технологию в Django довольно сложно.
Есть три разные библиотеки, которые Вам придется использовать, если Вы не хотите реализовать этот проект с нуля.
Документация также довольно сложна для понимания, и на эту тему написано не так много.
Вот почему после нескольких дней упорной борьбы я все-таки решил написать эту статью, которая станет универсальным руководством по интеграции ElasticSearch с Django.
PS. Вы можете найти подобный проект на GitHub ниже.
Необходимые условия
- Знание основ Python
- Знание основ Django и Django-Rest-Framework (DRF)
- Знание основ ElasticSearch
- Docker или локальная установка Elasticsearch
В чем состоит суть проекта?
Чтобы не терять времени, у меня уже есть готовое приложение Django с двумя моделями.
- Статьи
- Категории
Код для моделей:
from django.db import models
class Category(models.Model):
title = models.CharField(max_length=100)class Article(models.Model):
title = models.CharField(max_length=100)
category = models.ForeignKey(
Category, related_name='category', on_delete=models.CASCADE
)
Основная цель данной статьи состоит в следующем:
- Предоставить пользователям возможность искать статьи по названию с помощью ElasticSearch.
- Предоставить пользователям возможность фильтровать статьи по категориям с помощью ElasticSearch.
- Создать API для автозаполнения статей.
Приступим.
Локальный запуск Elasticsearch
Прежде чем приступить к работе с Django, нам нужно запустить экземпляр Elasticsearch.
Если у Вас уже есть локальный экземпляр, Вы можете смело пропустить эту часть.
Я предпочитаю использовать Docker и Docker-Compose.
Вот Dockerfile, который я использую для Django:
# Dockerfile # syntax=docker/dockerfile:1 FROM python:3 ENV PYTHONUNBUFFERED=1 WORKDIR /code COPY requirements.txt /code/ RUN pip install -r requirements.txt COPY . /code/
Вот мой файл docker-compose.yml file с двумя сервисами:
- Web
- Elasticsearch
Код:
version: "3.9"services:
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- elasticsearch
networks:
- elastic
elasticsearch:
image: elasticsearch:7.14.0
volumes:
- ./data/elastic:/var/lib/elasticsearch/data
environment:
- discovery.type=single-node
ports:
- 9200:9200
networks:
- elastic
networks:
elastic:
driver: bridge
После того как эти два файла будут размещены в корневом каталоге, можно запускать контейнеры.
docker-compose up -d
Теперь идите сюда: localhost:8000. Все должно работать, как часы.
Интеграция Elasticsearch и Django
Интеграция с Elasticsearch довольно проста, Вам нужна всего одна библиотека.
Ее название - django-elasticsearch-dsl, устанавливаем.
pip install django-elasticsearch-dsl
Имейте в виду, что для разных версий ElasticSearch существуют разные версии библиотеки.
По состоянию на ноябрь 2021 года требования следующие:
- Для Elasticsearch 7.0 и более поздних версий используйте основную версию библиотеки 7 (7.x.y).
- Для Elasticsearch 6.0 и более поздних версий используйте основную версию библиотеки 6 (6.x.y).
- Для Elasticsearch 5.0 и более поздних версий используйте основную версию библиотеки 0.5 (0.5.x).
После установки библиотеки не забудьте добавить ее в список installed_apps в файле settings.py
# settings.pyINSTALLED_APPS = [
# Other apps
'django_elasticsearch_dsl',
]
Наконец, нам просто нужно указать django-elastsearch-dsl, где находится наш экземпляр ElasticSearch.
Добавьте это в файл settings.py:
# settings.py# other code....ELASTICSEARCH_DSL = {
'default': {
'hosts': 'elasticsearch:9200'
},
}
Здесь мы говорим, что ElasticSearch находится по адресу elasticsearch:9200, что эквивалентно localhost:9200, но мы используем имя сервиса при обмене данными между двумя сервисами docker-compose.
На этом этапе все должно работать как обычно.
Интеграция Elasticsearch с DRF
Теперь интегрируем Elasticsearch с DRF.
Для этого нам понадобится еще одна библиотека под названием django-elasticsearch-dsl-drf
pip install django-elasticsearch-dsl-drf
Мы должны добавить его в installed_apps в файле settings.py:
# settings.pyINSTALLED_APPS = [
# Other apps
'django_elasticsearch_dsl',
'django_elasticsearch_dsl_drf',
]
Вот и все, мы полностью интегрировали Elasticsearch с DRF.
Теперь давайте создадим несколько API.
Индексируем нашу модель в Elasticsearch
ElasticSearch работает, но он пуст.
Нам нужно сказать Django, чтобы он скопировал наши существующие записи в ElasticSearch.
ElasticSearch хранит данные в документах JSON.
Это означает, что для каждой модели, которую мы хотим добавить в ElasticSearch, мы должны создать класс документа.
Он будет храниться в файле documents.py в Вашей папке apps.
Давайте создадим документ для нашей модели статьи:
# articles/documents.pyfrom django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registryfrom articles.models import Article
@registry.register_document
class ArticleDocument(Document):
title = fields.TextField(
attr='title',
fields={
'raw': fields.TextField(),
'suggest': fields.CompletionField(),
}
)
category = fields.ObjectField(
attr='category',
properties={
'id': fields.IntegerField(),
'title': fields.TextField(
attr='title',
fields={
'raw': fields.KeywordField(),
}
)
}
) class Index:
name = 'articles' class Django:
model = Article
Это может показаться сложным, но мы просто зарегистрировали документ ArticleDocument, связанный с моделью Article. Затем мы указали поля, которые хотим индексировать в ElasticSearch.
Первое поле - это заголовок, который представляет собой текстовое поле с двумя свойствами.
- raw - обычное текстовое поле ElasticSearch, которое мы будем использовать для поиска.
- suggest — поле, которое используется для функции автозаполнения.
Следующее поле - категория, которое является полем отношения, но ElasticSearch не имеет концепции отношений.
Поэтому, чтобы сохранить весь объект категории в Elasticsearch, мы используем поле object,.
Мы указываем, что поле category - это объект, затем в свойствах указываем поля category - id и title.
Вы могли заметить, что в заголовке категории мы используем поле ключевого слова вместо текстового поля. Разница между ними заключается в том, как они анализируются в ElasticSearch.
Наконец, мы указываем имя индекса для статей.
Но прежде чем двигаться дальше, мы должны зарегистрировать наши индексы.
Поэтому добавьте этот код в файл settings.py
# settings.py ELASTICSEARCH_INDEX_NAMES = { 'articles.article': 'articles', }
Используемый мной формат в app.model: index
Но прежде чем индексировать модель, убедитесь в том, что в Вашей базе данных есть записи.
Как только Вы это сделаете, выполните команду:
python manage.py search_index --rebuild
Это позволит заполнить наш индекс статей данными из нашей базы данных.
Кажется, все работает, или нет?
Вы, наверное, заметили проблему: что если мы будем добавлять или удалять записи?
Будут ли индексы автоматически обновляться?
К сожалению, нет, но мы можем легко автоматизировать это с помощью Django Signals.
Сигналы предлагают хуки - код, который запускается после определенного события.
Нас интересуют хуки post_save и post_delete.
Поэтому создайте в своем приложении файл signals.py и добавьте в него этот код:
# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiverfrom django_elasticsearch_dsl.registries import registry
@receiver(post_save)
def update_document(sender, **kwargs):
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance'] if app_label == 'articles':
if model_name == 'article':
instances = instance.article.all()
for _instance in instances:
registry.update(_instance)
@receiver(post_delete)
def delete_document(sender, **kwargs):
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance'] if app_label == 'articles':
if model_name == 'article':
instances = instance.article.all()
for _instance in instances:
registry.update(_instance)
Этот код по сути обновляет индексы после создания или удаления статей.
Он проверяет имя приложения и имя модели для того, чтобы убедиться, что он запускается только для модели статьи.
Сериализация документа
Теперь, когда мы проиндексировали модель, нам нужно найти способ ее сериализации.
Здесь на помощь нам приходит django-elasticsearch-dsl-drf.
Библиотека предоставляет нам класс DocumentSerializer, который позволяет легко сериализовать документы.
Создайте файл serializers.py в своем приложении и добавьте в него:
from django_elasticsearch_dsl_drf.serializers import DocumentSerializerfrom articles.documents import ArticleDocumentclass ArticleDocumentSerializer(DocumentSerializer):
class Meta:
document = ArticleDocument fields = (
'title',
'category'
)
Он очень похож на ваш обычный DRF-сериализатор, вы просто передаете ему документ и указываете поля, которые Вы хотите сериализовать.
Создание API
Наконец, у нас все готово для создания наших API.
Мы будем шаг за шагом реализовывать функцию за функцией.
Давайте создадим базовое представление документа, без каких-либо функций.
В файле views.py добавьте:
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
document = ArticleDocument
serializer_class = ArticleDocumentSerializer filter_backends = []
Это простое представление документа, которое только перечисляет и извлекает статьи из нашего индекса ElascticSearch.
Опять же, это делается очень просто благодаря django-elasticsearch-dsl-drf
Давайте реализуем разные функции.
Поиск
Для реализации поиска нам нужно:
- добавить SearchFilterBackend к filter_backends
- добавить search_fields который принимает кортеж полей, по которым Вы хотите выполнить поиск.
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
document = ArticleDocument
serializer_class = ArticleDocumentSerializer filter_backends = [
SearchFilterBackend
]
search_fields = ('title',)
Фильтрация
Для реализации простой фильтрации нужно сделать следующее:
- добавить FilteringFilterBackend к filter_backends
- добавить filter_fields который принимает словарь полей, по которым Вы хотите отфильтровать информацию.
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import FilteringFilterBackend
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
document = ArticleDocument
serializer_class = ArticleDocumentSerializer filter_backends = [
FilteringFilterBackend
]
filter_fields = {
'category': 'category.id'
}
Вас может несколько удивить следующая линия:
'category': 'category.id'
По сути, это означает, что мне нужен параметр category, который принимает id, и фильтрует объект category именно по полю id.
Автозаполнение
Для реализации простого автозаполнения нужно:
- добавить SuggesterFilterBackend к filter_backends
- добавить suggester_fields который принимает словарь полей.
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import SuggesterFilterBackend
from django_elasticsearch_dsl_drf.constants import SUGGESTER_COMPLETION
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
document = ArticleDocument
serializer_class = ArticleDocumentSerializer filter_backends = [
SuggesterFilterBackend
]
suggester_fields = {
'title': {
'field': 'title.suggest',
'suggesters': [
SUGGESTER_COMPLETION,
],
},
}
Разобьем этот код на части:
suggester_fields = {
'title': {
'field': 'title.suggest',
'suggesters': [
SUGGESTER_COMPLETION,
],
},
}
Первое правило заключается в том, что мы ДОЛЖНЫ автозаполнять по полю завершения.
Если Вы помните, в файле документа мы объявили свойство «suggest» для заголовка поля, которое должно быть полем завершения.
Далее мы указываем, какой тип предложения мы используем, по состоянию на октябрь 2021 года Elasticsearch поддерживает три типа предложений:
- заполнение
- фраза
- термин
Нам достаточно заполнения.
Финальная версия представления
from django_elasticsearch_dsl_drf.constants import SUGGESTER_COMPLETION
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, FilteringFilterBackend, SuggesterFilterBackend
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSetfrom articles.documents import ArticleDocument
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
document = ArticleDocument
serializer_class = ArticleDocumentSerializer filter_backends = [
FilteringFilterBackend,
SearchFilterBackend,
SuggesterFilterBackend
] search_fields = (
'title',
) filter_fields = {
'category': 'category.id'
} suggester_fields = {
'title': {
'field': 'title.suggest',
'suggesters': [
SUGGESTER_COMPLETION,
],
},
Обязательно добавьте все это в urls.py
# urls.py
from django.contrib import admin
from django.urls import path
from rest_framework import routersfrom articles.views importArticleDocumentViewrouter = routers.SimpleRouter(trailing_slash=False)router.register(r'article-search', ArticleDocumentView, basename='article-search')urlpatterns = [
path('admin/', admin.site.urls),
]urlpatterns += router.urls
API в действии
DocumentViewSet автоматически создает три различных API.
- Один для создания списка и извлечения записей.
- Один для обычной функциональности (поиск, фильтр и т. д.).
- Один для функциональности автозаполнения.
- Один для функциональных предложений.
В нашем примере нам важна только обычная функциональность и функциональность предложений.
Перечень
URL: localhost:8000/article-search
Поиск
URL: localhost:8000/article-search?search=programming
Если Вы хотите выполнить поиск по нескольким терминам, Вы можете сделать это с помощью:
localhost:8000/article-search?search=term1&search=term2
Подробнее об этом Вы можете прочитать здесь:
Документация django-elasticsearch-dsl-drf 0.22.2
Фильтрация
URL: localhost:8000/article-search?category=2
Существует множество различных способов фильтрации.
Например, если Вы хотите отфильтровать статьи по нескольким категориям, Вы можете сделать следующее:
localhost:8000/article-search?category=id1__id2__id3
Подробнее о различных фильтрах Вы можете прочитать здесь.
Документация django-elasticsearch-dsl-drf 0.22.2
Автозаполнение
URL: localhost:8000/article-search/suggest?title__completion=how
Заключение
Прежде всего, поздравляем Вас с тем, что Вы дошли до этого места!
В завершение давайте подытожим, что же мы сегодня узнали:
- Мы создали локальный экземпляр ElasticSearch с помощью Docker и Docker-Compose.
- Мы связали Django с ElasticSearch с помощью библиотеки django-elasticsearch-dsl.
- Мы проиндексировали наши модели с помощью документов.
- Мы синхронизировали наши модели и индексы с помощью сигналов.
- Мы сериализовали наши документы.
- Мы создали базовые функции поиска, фильтрации и автозаполнения.









