Django REST Framework и Elasticsearch

В этом руководстве мы рассмотрим, как интегрировать Django REST Framework (DRF) с Elasticsearch. Мы будем использовать Django для моделирования наших данных, а DRF для их сериализации и передачи. Наконец, мы проиндексируем данные с помощью Elasticsearch и сделаем их доступными для поиска.

Что такое Elasticsearch?

Elasticsearch - это распределенная, бесплатная и открытая поисковая и аналитическая система для всех типов данных, включая текстовые, числовые, геопространственные, структурированные и неструктурированные. Он известен своими простыми RESTful API, распределенным характером, скоростью и масштабируемостью. Elasticsearch является центральным компонентом Elastic Stack (также известного как ELK Stack), набора бесплатных и открытых инструментов для ввода, обогащения, хранения, анализа и визуализации данных.

Их использование включает в себя:

  1. Поиск по сайту и поиск приложений
  2. Мониторинг и визуализация системных показателей
  3. Безопасность и бизнес-аналитика
  4. Запись и анализ журналов

Чтобы узнать больше об Elasticsearch, ознакомьтесь с Что такое Elasticsearch? из официальной документации.

Структура и концепции Elasticsearch

Перед тем, как начать работать с Elasticsearch, мы должны познакомиться с основными концепциями Elasticsearch. Они перечислены в порядке убывания размера:

  1. Кластер (Cluster) - это набор из одного или нескольких узлов.
  2. Узел (Node) - это отдельный экземпляр сервера, на котором выполняется Elasticsearch. При общении с кластером он:
    1. Хранит и индексирует ваши данные.
    2. Обеспечивает поиск.
  3. Индекс (Index) используется для хранения документов в выделенных структурах данных, соответствующих типу данных полей (аналогично базе данных SQL). Каждый индекс имеет один или несколько сегментов и реплик.
  4. Тип (Type) - это набор документов, у которых есть что-то общее (например, таблица SQL).
  5. Шард (Shard) - это индекс Apache Lucene. Он используется для разделения индексов и обеспечения управляемости больших объемов данных.
  6. Реплика (Replica) - это отказоустойчивый механизм, который по сути является копией шарда вашего индекса.
  7. Документ (Document) - это базовая единица информации, которую можно индексировать (аналогично строке SQL). Он выражается в JSON, который является повсеместным форматом обмена данными в Интернете.
  8. Поле (Field) - это наименьшая отдельная единица данных в Elasticsearch (похожая на столбец SQL).

Кластер Elasticsearch имеет следующую структуру:

Elasticsearch cluster structure

Интересно, как концепции реляционных баз данных соотносятся с концепциями Elasticsearch?

Relational Database Elasticsearch
Cluster Cluster
RDBMS Instance Node
Table Index
Row Document
Column Field

Просмотрите концепции сопоставления в SQL и Elasticsearch, чтобы узнать, как концепции в SQL и Elasticsearch связаны друг с другом.

Elasticsearch против полнотекстового поиска PostgreSQL

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

Преимущества PostgreSQL:

  1. Поддержка Django
  2. Быстрее и проще настроить
  3. Не требует обслуживания

Преимущества Elasticsearch:

  1. Оптимизирован только для поиска
  2. Elasicsearch работает быстрее (особенно при увеличении количества записей)
  3. Поддерживает различные типы запросов (Leaf, Compound, Fuzzy, Regexp и др.)

Если вы работаете над простым проектом, в котором скорость не важна, вам следует выбрать PostgreSQL. Если производительность важна и вы хотите писать сложные запросы, выбирайте Elasticsearch.

Подробнее о полнотекстовом поиске с помощью Django и Postgres см. Базовый и полнотекстовый поиск с помощью Django и Postgres

Настройка проекта

Мы создадим простое приложение для блога. Наш проект будет состоять из нескольких моделей, которые будут сериализованы и обслуживаться через Django REST Framework. После интеграции Elasticsearch мы создадим конечную точку, которая позволит нам искать разных авторов, категории и статьи.

Чтобы наш код оставался чистым и модульным, мы разделим наш проект на следующие два приложения:

  1. blog  - для наших моделей Django, сериализаторов и ViewSets.
  2. search - для документов, индексов и запросов Elasticsearch.

Начните с создания нового каталога и настройки нового проекта Django:

$ mkdir django-drf-elasticsearch && cd django-drf-elasticsearch
$ python3.9 -m venv env
$ source env/bin/activate

(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject core .

После этого создайте новое приложение под названием blog:

(env)$ python manage.py startapp blog

Зарегистрируйте приложение в core/settings.py в INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig', # new
]

Модели баз данных

Затем создайте модели Category и Article в blog/models.py:

# blog/models.py

from django.contrib.auth.models import User
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=32)
    description = models.TextField(null=True, blank=True)

    class Meta:
        verbose_name_plural = 'categories'

    def __str__(self):
        return f'{self.name}'


ARTICLE_TYPES = [
    ('UN', 'Unspecified'),
    ('TU', 'Tutorial'),
    ('RS', 'Research'),
    ('RW', 'Review'),
]


class Article(models.Model):
    title = models.CharField(max_length=256)
    author = models.ForeignKey(to=User, on_delete=models.CASCADE)
    type = models.CharField(max_length=2, choices=ARTICLE_TYPES, default='UN')
    categories = models.ManyToManyField(to=Category, blank=True, related_name='categories')
    content = models.TextField()
    created_datetime = models.DateTimeField(auto_now_add=True)
    updated_datetime = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f'{self.author}: {self.title} ({self.created_datetime.date()})'

Примечания:

  1. Category представляет категорию статьи, т.е. программирование, Linux, тестирование.
  2. Article представляет собой отдельную статью. Каждая статья может иметь несколько категорий. Статьи имеют определенный тип - Tutorial, Research, Review или Unspecified.
  3. Авторы представлены пользовательской моделью Django по умолчанию.

Запустить миграции

Сделайте миграции, а затем примените их:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Зарегистрируйте модели в blog/admin.py:

# blog/admin.py

from django.contrib import admin

from blog.models import Category, Article


admin.site.register(Category)
admin.site.register(Article)

Заполнить базу данных

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

Создайте новую папку в "blog" под названием "management", а затем внутри этой папки создайте другую папку под названием "commands". Внутри "commands" папку, создайте новый файл с именем populate_db.py.

management
└── commands
    └── populate_db.py

Скопируйте содержимое файла из populate_db.py и вставьте его в свой populate_db.py.

Выполните следующую команду, чтобы заполнить БД:


(env)$ python manage.py populate_db

Если все прошло хорошо, вы должны увидеть сообщение Успешно заполнено базу данных. в консоли, а в вашей базе данных должно быть несколько статей.

Фреймворк Django REST

Теперь давайте установим djangorestframework с помощью pip:

(env)$ pip install djangorestframework==3.12.4

Зарегистрируйте его в нашем settings.py вот так:

# core/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig',
    'rest_framework', # new
]

Добавьте следующие настройки:

# core/settings.py

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 25
}

Эти настройки нам понадобятся для реализации нумерации страниц.

Создать сериализаторы

Чтобы сериализовать наши модели Django, нам нужно создать сериализатор для каждой из них. Самый простой способ создать сериализаторы, зависящие от моделей Django, - использовать класс ModelSerializer.

blog/serializers.py:

# blog/serializers.py

from django.contrib.auth.models import User
from rest_framework import serializers

from blog.models import Article, Category


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'first_name', 'last_name')


class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'


class ArticleSerializer(serializers.ModelSerializer):
    author = UserSerializer()
    categories = CategorySerializer(many=True)

    class Meta:
        model = Article
        fields = '__all__'

Примечания:

  1. UserSerializer и CategorySerializer довольно просты: мы просто предоставили поля, которые хотим сериализовать.
  2. В ArticleSerializer нам нужно было позаботиться об отношениях, чтобы убедиться, что они также сериализованы. Вот почему мы предоставили UserSerializer и CategorySerializer.

Хотите узнать больше о сериализаторах DRF? Ознакомьтесь с разделом «Эффективное использование сериализаторов Django REST Framework».

Создать ViewSets

Давайте создадим ViewSet для каждой из наших моделей в blog/views.py:

# blog/views.py

from django.contrib.auth.models import User
from rest_framework import viewsets

from blog.models import Category, Article
from blog.serializers import CategorySerializer, ArticleSerializer, UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    serializer_class = UserSerializer
    queryset = User.objects.all()


class CategoryViewSet(viewsets.ModelViewSet):
    serializer_class = CategorySerializer
    queryset = Category.objects.all()


class ArticleViewSet(viewsets.ModelViewSet):
    serializer_class = ArticleSerializer
    queryset = Article.objects.all()

В этом блоке кода мы создали ViewSets, предоставив serializer_class и queryset для каждого ViewSet.

Определение URL

Создайте URL-адреса на уровне приложения для ViewSets:

# blog/urls.py

from django.urls import path, include
from rest_framework import routers

from blog.views import UserViewSet, CategoryViewSet, ArticleViewSet

router = routers.DefaultRouter()
router.register(r'user', UserViewSet)
router.register(r'category', CategoryViewSet)
router.register(r'article', ArticleViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Затем подключите URL-адреса приложений к URL-адресам проекта:

# core/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls')),
    path('admin/', admin.site.urls),
]

У нашего приложения теперь есть следующие URL-адреса:

  1. /blog/user/ перечисляет всех пользователей
  2. /blog/user/<USER_ID>/выбирает конкретного пользователя
  3. /blog/category/ перечисляет все категории
  4. /blog/category/<CATEGORY_ID>/ выбирает определенную категорию
  5. /blog/article/ перечисляет все статьи
  6. /blog/article/<ARTICLE_ID>/ выбирает конкретную статью

Тестирование

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

Запускаем сервер разработки:

(env)$ python manage.py runserver

Затем в выбранном браузере перейдите по адресу http://127.0.0.1:8000/blog/article/. Ответ должен выглядеть примерно так:

{
    "count": 4,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": 1,
            "author": {
                "id": 3,
                "username": "jess_",
                "first_name": "Jess",
                "last_name": "Brown"
            },
            "categories": [
                {
                    "id": 2,
                    "name": "SEO optimization",
                    "description": null
                }
            ],
            "title": "How to improve your Google rating?",
            "type": "TU",
            "content": "Firstly, add the correct SEO tags...",
            "created_datetime": "2021-08-12T17:34:31.271610Z",
            "updated_datetime": "2021-08-12T17:34:31.322165Z"
        },
        {
            "id": 2,
            "author": {
                "id": 4,
                "username": "johnny",
                "first_name": "Johnny",
                "last_name": "Davis"
            },
            "categories": [
                {
                    "id": 4,
                    "name": "Programming",
                    "description": null
                }
            ],
            "title": "Installing latest version of Ubuntu",
            "type": "TU",
            "content": "In this tutorial, we'll take a look at how to setup the latest version of Ubuntu. Ubuntu (/ʊˈbʊntuː/ is a Linux distribution based on Debian and composed mostly of free and open-source software. Ubuntu is officially released in three editions: Desktop, Server, and Core for Internet of things devices and robots.",
            "created_datetime": "2021-08-12T17:34:31.540628Z",
            "updated_datetime": "2021-08-12T17:34:31.592555Z"
        },
        ...
    ]
}

Также проверьте вручную и другие конечные точки.

Настройка Elasticsearch

Начните с установки и запуска Elasticsearch в фоновом режиме.

Нужна помощь в настройке и запуске Elasticsearch? Ознакомьтесь с руководством по установке Elasticsearch. Если вы знакомы с Docker, вы можете просто запустить следующую команду, чтобы получить официальный образ и запустить контейнер с запущенным Elasticsearch:

$ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.14.0

Чтобы интегрировать Elasticsearch с Django, нам необходимо установить следующие пакеты:

  1. elasticsearch - официальный низкоуровневый клиент Python для Elasticsearch
  2. elasticsearch-dsl-py - высокоуровневая библиотека для написания и выполнения запросов к Elasticsearch
  3. django-elasticsearch-dsl - оболочка вокруг elasticsearch-dsl-py, которая позволяет индексировать модели Django в Elasticsearch

Установка:

(env)$ pip install elasticsearch==7.14.0
(env)$ pip install elasticsearch-dsl==7.4.0
(env)$ pip install django-elasticsearch-dsl==7.2.0

Запустите новое приложение под названием search, в котором будут храниться наши документы, индексы и запросы Elasticsearch:

(env)$ python manage.py startapp search

Зарегистрируйте поиск и django_elasticsearch_dsl в core/settings.py в INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_elasticsearch_dsl', # new
    'blog.apps.BlogConfig',
    'search.apps.SearchConfig', # new
    'rest_framework',
]

Теперь нам нужно сообщить Django, где работает Elasticsearch. Мы делаем это, добавляя следующее в наш файл core/settings.py:

# core/settings.py

# Elasticsearch
# https://django-elasticsearch-dsl.readthedocs.io/en/latest/settings.html

ELASTICSEARCH_DSL = {
    'default': {
        'hosts': 'localhost:9200'
    },
}

Если ваш Elasticsearch работает на другом порту, обязательно измените вышеуказанные настройки соответствующим образом.

Мы можем проверить, может ли Django подключиться к Elasticsearch, запустив наш сервер:

(env)$ python manage.py runserver

Если ваш сервер Django выходит из строя, возможно, Elasticsearch работает некорректно.

Создание документов

Перед созданием документов нам необходимо убедиться, что все данные будут сохранены в правильном формате. Мы используем CharField(max_length=2) для type нашей статьи, что само по себе бессмысленно. Вот почему мы преобразуем его в читабельный текст.

Мы добьемся этого, добавив метод type_to_string() в нашу модель следующим образом:

# blog/models.py

class Article(models.Model):
    title = models.CharField(max_length=256)
    author = models.ForeignKey(to=User, on_delete=models.CASCADE)
    type = models.CharField(max_length=2, choices=ARTICLE_TYPES, default='UN')
    categories = models.ManyToManyField(to=Category, blank=True, related_name='categories')
    content = models.TextField()
    created_datetime = models.DateTimeField(auto_now_add=True)
    updated_datetime = models.DateTimeField(auto_now=True)

    # new
    def type_to_string(self):
        if self.type == 'UN':
            return 'Unspecified'
        elif self.type == 'TU':
            return 'Tutorial'
        elif self.type == 'RS':
            return 'Research'
        elif self.type == 'RW':
            return 'Review'

    def __str__(self):
        return f'{self.author}: {self.title} ({self.created_datetime.date()})'

Без type_to_string() наша модель была бы сериализована следующим образом:

{
    "title": "This is my article.",
    "type": "TU",
    ...
}

После реализации type_to_string() наша модель сериализуется следующим образом:

{
    "title": "This is my article.",
    "type": "Tutorial",
    ...
}

Теперь создадим документы. Каждый документ должен иметь Index и класс Django. В классе Index нам нужно указать имя индекса и настройки индекса Elasticsearch. В классе Django мы сообщаем документу, с какой моделью Django его связать, и предоставляем поля, которые мы хотим проиндексировать.

blog/documents.py:

# blog/documents.py

from django.contrib.auth.models import User
from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry

from blog.models import Category, Article


@registry.register_document
class UserDocument(Document):
    class Index:
        name = 'users'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = User
        fields = [
            'id',
            'first_name',
            'last_name',
            'username',
        ]


@registry.register_document
class CategoryDocument(Document):
    id = fields.IntegerField()

    class Index:
        name = 'categories'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = Category
        fields = [
            'name',
            'description',
        ]


@registry.register_document
class ArticleDocument(Document):
    author = fields.ObjectField(properties={
        'id': fields.IntegerField(),
        'first_name': fields.TextField(),
        'last_name': fields.TextField(),
        'username': fields.TextField(),
    })
    categories = fields.ObjectField(properties={
        'id': fields.IntegerField(),
        'name': fields.TextField(),
        'description': fields.TextField(),
    })
    type = fields.TextField(attr='type_to_string')

    class Index:
        name = 'articles'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = Article
        fields = [
            'title',
            'content',
            'created_datetime',
            'updated_datetime',
        ]

Примечания:

  1. Чтобы преобразовать тип статьи, мы добавили атрибут type в ArticleDocument.
  2. Поскольку наша модель Article находится во взаимосвязи «многие ко многим» (M: N) с Category и в отношении «многие к одному» (N: 1) с User, нам необходимо было позаботиться об отношениях. Мы сделали это, добавив атрибуты ObjectField.

Заполнение Elasticsearch

Чтобы создать и заполнить индекс и сопоставление Elasticsearch, используйте команду search_index:

(env)$ python manage.py search_index --rebuild

Deleting index 'users'
Deleting index 'categories'
Deleting index 'articles'
Creating index 'users'
Creating index 'categories'
Creating index 'articles'
Indexing 3 'User' objects
Indexing 4 'Article' objects
Indexing 4 'Category' objects

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

django-elasticsearch-dsl создал соответствующие сигналы базы данных, чтобы ваше хранилище Elasticsearch обновлялось каждый раз, когда экземпляр модели создается, удаляется или редактируется.

Запросы Elasticsearch

Прежде чем создавать соответствующие представления, давайте посмотрим, как работают запросы Elasticsearch.

Сначала нам нужно получить экземпляр Search. Мы делаем это, вызывая search() в нашем документе следующим образом:

from blog.documents import ArticleDocument

search = ArticleDocument.search()

Feel free to run these queries within the Django shell.

Когда у нас есть экземпляр Search, мы можем передавать запросы методу query() и получать ответ:

from elasticsearch_dsl import Q
from blog.documents import ArticleDocument


# Выполняет поиск всех статей, в названии которых есть «How to».
query = 'How to'
q = Q(
     'multi_match',
     query=query,
     fields=[
         'title'
     ])
search = ArticleDocument.search().query(q)
response = search.execute()

# распечатать все хиты
for hit in search:
    print(hit.title)

Мы также можем комбинировать несколько операторов Q следующим образом:

from elasticsearch_dsl import Q
from blog.documents import ArticleDocument

"""
Ищет все статьи, которые:
1) Содержите слово "language" в "title".
2) Не используйте "ruby" или "javascript" в "title".
3) И содержать запрос в "title" или "description".
"""
query = 'programming'
q = Q(
     'bool',
     must=[
         Q('match', title='language'),
     ],
     must_not=[
         Q('match', title='ruby'),
         Q('match', title='javascript'),
     ],
     should=[
         Q('match', title=query),
         Q('match', description=query),
     ],
     minimum_should_match=1)
search = ArticleDocument.search().query(q)
response = search.execute()

# распечатать все хиты
for hit in search:
    print(hit.title)

Еще одна важная вещь при работе с запросами Elasticsearch - это нечеткость Fuzzy. Нечеткие запросы Fuzzy - это запросы, которые позволяют нам обрабатывать опечатки. Они используют алгоритм расстояния Левенштейна, который вычисляет расстояние между результатом в нашей базе данных и запросом.

Давайте посмотрим на пример.

Выполнив следующий запрос, мы не получим никаких результатов, потому что пользователь неправильно написал «django».

from elasticsearch_dsl import Q
from blog.documents import ArticleDocument

query = 'djengo'  # notice the typo
q = Q(
     'multi_match',
     query=query,
     fields=[
         'title'
     ])
search = ArticleDocument.search().query(q)
response = search.execute()

# print all the hits
for hit in search:
    print(hit.title)

Если мы включим нечеткость так:

from elasticsearch_dsl import Q
from blog.documents import ArticleDocument

query = 'djengo'  # notice the typo
q = Q(
     'multi_match',
     query=query,
     fields=[
         'title'
     ],
     fuzziness='auto')
search = ArticleDocument.search().query(q)
response = search.execute()

# print all the hits
for hit in search:
    print(hit.title)

Пользователь получит правильный результат.

Разница между полнотекстовым поиском и точным соответствием заключается в том, что полнотекстовый поиск запускает анализатор текста, прежде чем он будет проиндексирован в Elasticsearch. Текст разбивается на разные токены, которые преобразуются в свою корневую форму (например, чтение -> чтение). Затем эти токены сохраняются в инвертированном индексе. По этой причине полнотекстовый поиск дает больше результатов, но требует больше времени для обработки.

Elasticsearch имеет ряд дополнительных функций. Чтобы ознакомиться с API, попробуйте реализовать:

  1. Ваш собственный анализатор.
  2. Подсказка завершения - когда пользователь запрашивает "j", ваше приложение должно предлагать "johnny" или "jess_".
  3. Выделение - когда пользователь делает опечатку, выделите ее (например, Linuks -> Linux).

Вы можете увидеть все API поиска Elasticsearch здесь.

Поисковые представления

На этом давайте создадим сим-представления. Чтобы сделать наш код более DRY, мы можем использовать следующий абстрактный класс в search/views.py:

# search/views.py

import abc

from django.http import HttpResponse
from elasticsearch_dsl import Q
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import APIView


class PaginatedElasticSearchAPIView(APIView, LimitOffsetPagination):
    serializer_class = None
    document_class = None

    @abc.abstractmethod
    def generate_q_expression(self, query):
        """This method should be overridden
        and return a Q() expression."""

    def get(self, request, query):
        try:
            q = self.generate_q_expression(query)
            search = self.document_class.search().query(q)
            response = search.execute()

            print(f'Found {response.hits.total.value} hit(s) for query: "{query}"')

            results = self.paginate_queryset(response, request, view=self)
            serializer = self.serializer_class(results, many=True)
            return self.get_paginated_response(serializer.data)
        except Exception as e:
            return HttpResponse(e, status=500)

Примечания:

  1. Чтобы использовать класс, мы должны предоставить наши serializer_class и document_class и переопределить generate_q_expression().
    Класс ничего не делает, кроме запуска запроса generate_q_expression(), получения ответа, разбиения его на страницы и возврата сериализованных данных.

Теперь все представления должны наследоваться от PaginatedElasticSearchAPIView:

# search/views.py

import abc

from django.http import HttpResponse
from elasticsearch_dsl import Q
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import APIView

from blog.documents import ArticleDocument, UserDocument, CategoryDocument
from blog.serializers import ArticleSerializer, UserSerializer, CategorySerializer


class PaginatedElasticSearchAPIView(APIView, LimitOffsetPagination):
    serializer_class = None
    document_class = None

    @abc.abstractmethod
    def generate_q_expression(self, query):
        """This method should be overridden
        and return a Q() expression."""

    def get(self, request, query):
        try:
            q = self.generate_q_expression(query)
            search = self.document_class.search().query(q)
            response = search.execute()

            print(f'Found {response.hits.total.value} hit(s) for query: "{query}"')

            results = self.paginate_queryset(response, request, view=self)
            serializer = self.serializer_class(results, many=True)
            return self.get_paginated_response(serializer.data)
        except Exception as e:
            return HttpResponse(e, status=500)


# views


class SearchUsers(PaginatedElasticSearchAPIView):
    serializer_class = UserSerializer
    document_class = UserDocument

    def generate_q_expression(self, query):
        return Q('bool',
                 should=[
                     Q('match', username=query),
                     Q('match', first_name=query),
                     Q('match', last_name=query),
                 ], minimum_should_match=1)


class SearchCategories(PaginatedElasticSearchAPIView):
    serializer_class = CategorySerializer
    document_class = CategoryDocument

    def generate_q_expression(self, query):
        return Q(
                'multi_match', query=query,
                fields=[
                    'name',
                    'description',
                ], fuzziness='auto')


class SearchArticles(PaginatedElasticSearchAPIView):
    serializer_class = ArticleSerializer
    document_class = ArticleDocument

    def generate_q_expression(self, query):
        return Q(
                'multi_match', query=query,
                fields=[
                    'title',
                    'author',
                    'type',
                    'content'
                ], fuzziness='auto')

Определение URL

Наконец, давайте создадим URL-адреса для наших представлений:

# search.urls.py

from django.urls import path

from search.views import SearchArticles, SearchCategories, SearchUsers

urlpatterns = [
    path('user/<str:query>/', SearchUsers.as_view()),
    path('category/<str:query>/', SearchCategories.as_view()),
    path('article/<str:query>/', SearchArticles.as_view()),
]

Затем подключите URL-адреса приложений к URL-адресам проекта:

# core/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls')),
    path('search/', include('search.urls')), # new
    path('admin/', admin.site.urls),
]

Тестирование

Наше веб-приложение готово. Мы можем протестировать наши конечные точки поиска, посетив следующие URL-адреса:

URL Description
http://127.0.0.1:8000/search/user/mike/ Возвращает пользователя 'mike13'
http://127.0.0.1:8000/search/user/jess_/ Возвращает пользователя 'jess_'
http://127.0.0.1:8000/search/category/seo/ Возвращает категорию "SEO-оптимизация"
http://127.0.0.1:8000/search/category/progreming/ Категория возврата "Программирование"
http://127.0.0.1:8000/search/article/linux/ Возвращает статью «Установка последней версии Ubuntu».
http://127.0.0.1:8000/search/article/java/ Возвращает статью «Какой язык программирования лучший?»

Обратите внимание на опечатку с четвертым запросом. Мы написали «прогрэминг», но все равно получили правильный результат благодаря нечеткости.

Альтернативные библиотеки

Выбранный нами путь - не единственный способ интегрировать Django с Elasticsearch. Есть еще несколько библиотек, которые вы, возможно, захотите проверить:

  1. django-elasicsearch-dsl-drf - это оболочка для Elasticsearch и Django REST Framework. Он предоставляет представления, сериализаторы, бэкэнды фильтров, разбиение на страницы и многое другое. Он работает хорошо, но для небольших проектов может оказаться излишним. Я бы рекомендовал использовать его, если вам нужны расширенные функции Elasticsearch.
  2. Haystack - это оболочка для ряда серверов поиска, таких как Elasticsearch, Solr и Whoosh. Это позволяет вам написать код поиска один раз и повторно использовать его с различными механизмами поиска. Он отлично подходит для реализации простого окна поиска. Поскольку Haystack - это еще один уровень абстракции, это связано с большими накладными расходами, поэтому вы не должны использовать его, если производительность действительно важна или если вы работаете с большими объемами данных. Это также требует некоторой настройки.
  3. Haystack для Django REST Framework - это небольшая библиотека, которая пытается упростить интеграцию Haystack с Django REST Framework. На момент написания проект немного устарел, а их документация написана плохо. Я потратил приличное количество времени, пытаясь заставить его работать, но безуспешно.

Заключение

В этом руководстве вы изучили основы работы с Django REST Framework и Elasticsearch. Теперь вы знаете, как их интегрировать, создавать документы и запросы Elasticsearch и обслуживать данные через RESTful API.

Перед запуском проекта в производство рассмотрите возможность использования одной из управляемых служб Elasticsearch, например Elastic Cloud, Amazon Elasticsearch Service или Elastic on Azure. Стоимость использования управляемой службы будет выше, чем управление собственным кластером, но они предоставляют всю инфраструктуру, необходимую для развертывания, защиты и запуска кластеров Elasticsearch. Кроме того, они будут обрабатывать обновления версий, регулярное резервное копирование и масштабирование.

Возьмите код из репозитория django-drf-elasticsearch на GitHub.

Вернуться на верх