Django REST Framework и Elasticsearch
В этом руководстве мы рассмотрим, как интегрировать Django REST Framework (DRF) с Elasticsearch. Мы будем использовать Django для моделирования наших данных, а DRF для их сериализации и передачи. Наконец, мы проиндексируем данные с помощью Elasticsearch и сделаем их доступными для поиска.
Что такое Elasticsearch?
Elasticsearch - это распределенная, бесплатная и открытая поисковая и аналитическая система для всех типов данных, включая текстовые, числовые, геопространственные, структурированные и неструктурированные. Он известен своими простыми RESTful API, распределенным характером, скоростью и масштабируемостью. Elasticsearch является центральным компонентом Elastic Stack (также известного как ELK Stack), набора бесплатных и открытых инструментов для ввода, обогащения, хранения, анализа и визуализации данных.
Их использование включает в себя:
- Поиск по сайту и поиск приложений
- Мониторинг и визуализация системных показателей
- Безопасность и бизнес-аналитика
- Запись и анализ журналов
Чтобы узнать больше об Elasticsearch, ознакомьтесь с Что такое Elasticsearch? из официальной документации.
Структура и концепции Elasticsearch
Перед тем, как начать работать с Elasticsearch, мы должны познакомиться с основными концепциями Elasticsearch. Они перечислены в порядке убывания размера:
- Кластер (Cluster) - это набор из одного или нескольких узлов.
- Узел (Node) - это отдельный экземпляр сервера, на котором выполняется Elasticsearch. При общении с кластером он:
- Хранит и индексирует ваши данные.
- Обеспечивает поиск.
- Индекс (Index) используется для хранения документов в выделенных структурах данных, соответствующих типу данных полей (аналогично базе данных SQL). Каждый индекс имеет один или несколько сегментов и реплик.
- Тип (Type) - это набор документов, у которых есть что-то общее (например, таблица SQL).
- Шард (Shard) - это индекс Apache Lucene. Он используется для разделения индексов и обеспечения управляемости больших объемов данных.
- Реплика (Replica) - это отказоустойчивый механизм, который по сути является копией шарда вашего индекса.
- Документ (Document) - это базовая единица информации, которую можно индексировать (аналогично строке SQL). Он выражается в JSON, который является повсеместным форматом обмена данными в Интернете.
- Поле (Field) - это наименьшая отдельная единица данных в Elasticsearch (похожая на столбец SQL).
Кластер Elasticsearch имеет следующую структуру:
Интересно, как концепции реляционных баз данных соотносятся с концепциями Elasticsearch?
Relational Database | Elasticsearch |
---|---|
Cluster | Cluster |
RDBMS Instance | Node |
Table | Index |
Row | Document |
Column | Field |
Просмотрите концепции сопоставления в SQL и Elasticsearch, чтобы узнать, как концепции в SQL и Elasticsearch связаны друг с другом.
Elasticsearch против полнотекстового поиска PostgreSQL
Что касается полнотекстового поиска, у Elasticsearch и PostgreSQL есть свои преимущества и недостатки. При выборе между ними следует учитывать скорость, сложность запроса и бюджет.
Преимущества PostgreSQL:
- Поддержка Django
- Быстрее и проще настроить
- Не требует обслуживания
Преимущества Elasticsearch:
- Оптимизирован только для поиска
- Elasicsearch работает быстрее (особенно при увеличении количества записей)
- Поддерживает различные типы запросов (Leaf, Compound, Fuzzy, Regexp и др.)
Если вы работаете над простым проектом, в котором скорость не важна, вам следует выбрать PostgreSQL. Если производительность важна и вы хотите писать сложные запросы, выбирайте Elasticsearch.
Подробнее о полнотекстовом поиске с помощью Django и Postgres см. Базовый и полнотекстовый поиск с помощью Django и Postgres
Настройка проекта
Мы создадим простое приложение для блога. Наш проект будет состоять из нескольких моделей, которые будут сериализованы и обслуживаться через Django REST Framework. После интеграции Elasticsearch мы создадим конечную точку, которая позволит нам искать разных авторов, категории и статьи.
Чтобы наш код оставался чистым и модульным, мы разделим наш проект на следующие два приложения:
blog
- для наших моделей Django, сериализаторов иViewSets
.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()})'
Примечания:
Category
представляет категорию статьи, т.е. программирование, Linux, тестирование.Article
представляет собой отдельную статью. Каждая статья может иметь несколько категорий. Статьи имеют определенный тип -Tutorial
,Research
,Review
илиUnspecified
.- Авторы представлены пользовательской моделью 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__'
Примечания:
UserSerializer
иCategorySerializer
довольно просты: мы просто предоставили поля, которые хотим сериализовать.- В
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-адреса:
/blog/user/
перечисляет всех пользователей/blog/user/<USER_ID>/
выбирает конкретного пользователя/blog/category/
перечисляет все категории/blog/category/<CATEGORY_ID>/
выбирает определенную категорию/blog/article/
перечисляет все статьи/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, нам необходимо установить следующие пакеты:
- elasticsearch - официальный низкоуровневый клиент Python для Elasticsearch
- elasticsearch-dsl-py - высокоуровневая библиотека для написания и выполнения запросов к Elasticsearch
- 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',
]
Примечания:
- Чтобы преобразовать тип статьи, мы добавили атрибут type в
ArticleDocument
. - Поскольку наша модель
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, попробуйте реализовать:
- Ваш собственный анализатор.
- Подсказка завершения - когда пользователь запрашивает "j", ваше приложение должно предлагать "johnny" или "jess_".
- Выделение - когда пользователь делает опечатку, выделите ее (например, 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)
Примечания:
- Чтобы использовать класс, мы должны предоставить наши
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. Есть еще несколько библиотек, которые вы, возможно, захотите проверить:
- django-elasicsearch-dsl-drf - это оболочка для Elasticsearch и Django REST Framework. Он предоставляет представления, сериализаторы, бэкэнды фильтров, разбиение на страницы и многое другое. Он работает хорошо, но для небольших проектов может оказаться излишним. Я бы рекомендовал использовать его, если вам нужны расширенные функции Elasticsearch.
- Haystack - это оболочка для ряда серверов поиска, таких как Elasticsearch, Solr и Whoosh. Это позволяет вам написать код поиска один раз и повторно использовать его с различными механизмами поиска. Он отлично подходит для реализации простого окна поиска. Поскольку Haystack - это еще один уровень абстракции, это связано с большими накладными расходами, поэтому вы не должны использовать его, если производительность действительно важна или если вы работаете с большими объемами данных. Это также требует некоторой настройки.
- 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.
Вернуться на верх