Пользователи ищут товары в интернет-магазине, ищут стати, поиск это неотъемлемый компонент сайта. Быстрый и гибкий поиск сложно реализовать средствами реляционных баз данных. Для таких задач используют поисковые движки, один из которых Elasticsearch. Elasticsearch хорошо документирован и доступен из коробки на AWS.
Для работы с elasticsearch используется библиотека elasticsearch-py или elasticsearch-dsl-py. elasticsearch-dsl-py это надстройка над elasticsearch-py, она проста в использовании и поддерживает elasticsearch версии 5.x. На базе этой библиотеки была создана библиотека django-rest-elasticsearch, которая основана на идеологии существующего поиска в Django REST Framework. Ниже я детально распишу как реализовать поиск в Django REST Framework с помощью elasticsearch используя данную библиотеку.
В качестве примера рассмотрим реализацию простого блога с фильтрацией по тегам и поиском по заголовку статей.
Установка
Процесс установки elasticsearch и django детально расписан в официальных документациях. С установкой пакета все достаточно просто
pip install django-rest-elasticsearch
Создание модели и индекса
Создадим модель
class Blog(models.Model):
title = models.CharField(_('Title'), max_length=1000)
created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
body = models.TextField(_('Body'))
tags = ArrayField(models.CharField(max_length=200), blank=True, null=True)
is_published = models.BooleanField(_('Is published'), default=False)
def __str__(self):
return self.title
После создания модели опишем нашу модель в виде elasticsearch документа.
class BlogIndex(DocType):
pk = Integer()
title = Text(fields={'raw': Keyword()})
created_at = Date()
body = Text()
tags = Keyword(multi=True)
is_published = Boolean()
class Meta:
index = 'blog'
Теперь можно создать индекс в elasticsearch
BlogIndex.init()
Автоматическое обновление документов в elasticsearch
После создания модели и индекса, нужно что бы любые изменения в нашей модели были отображены в elasticsearch. Наилучшим средством это сделать добавить django сигнал, который отправить уведомления когда будут происходит изменения в модели. Прежде чем создать сигнал создадим сериалайзер для преобразования django объекта в документ elasticsearch
from rest_framework_elasticsearch.es_serializer import ElasticModelSerializer
from .models import Blog
from .search_indexes import BlogIndex
class ElasticBlogSerializer(ElasticModelSerializer):
class Meta:
model = Blog
es_model = BlogIndex
fields = ('pk', 'title', 'created_at', 'tags', 'body', 'is_published')
Теперь добавим сигнал
from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver
from .serializers import Blog, ElasticBlogSerializer
@receiver(pre_save, sender=Blog, dispatch_uid="update_record")
def update_es_record(sender, instance, **kwargs):
obj = ElasticBlogSerializer(instance)
obj.save()
@receiver(post_delete, sender=Blog, dispatch_uid="delete_record")
def delete_es_record(sender, instance, *args, **kwargs):
obj = ElasticBlogSerializer(instance)
obj.delete(ignore=404)
После добавления сигнала, любые изменения в модели моментально будут сделаны в elasticsearch
Создание view
Приступим к создания view для поиска и фильтрации
from elasticsearch import Elasticsearch, RequestsHttpConnection
from rest_framework_elasticsearch import es_views, es_filters
from .search_indexes import BlogIndex
class BlogView(es_views.ListElasticAPIView):
es_client = Elasticsearch(hosts=['elasticsearch:9200/'],
connection_class=RequestsHttpConnection)
es_model = BlogIndex
es_filter_backends = (
es_filters.ElasticFieldsFilter,
es_filters.ElasticSearchFilter
)
es_filter_fields = (
es_filters.ESFieldFilter('tag', 'tags'),
)
es_search_fields = (
'tags',
'title',
)
Вот и все, примеры поиска
http://example.com/blogs/api/list?search=elasticsearch
http://example.com/blogs/api/list?tag=opensource
http://example.com/blogs/api/list?tag=opensource,aws
Полный код примера доступен на github. Надеюсь что статья поможет Вам реализовать поиск в Вашем проекте.
Комментарии (14)
random1st
02.05.2017 15:11Я рекомендую для описания документов и прочего использовать сам elasticsearch-dsl. Он позволяет более гибко задавать настройки аналайзеров и маппингов и много других нативных возможностей. Не понятно, как это сделать в вашем варианте, по-моему, без использования специфических возможностей ES прекрасно можно и по базе искать. Не понял смысла создания еще одного слоя абстракции.
myarik
02.05.2017 18:19Для описание документа как раз используется elasticsearch-dsl. Текущая библиотека позволяет просто преобразовывать django объекты в документы elasticsearch. И использовать elasticsearch с Django REST Framework — ом
tbicr
02.05.2017 15:53Интересено было бы глянуть на ситуацию, когда Blog представлет из себя развесистую сущность с несколькими foreign key.
random1st
02.05.2017 16:42+1мне больше интересно, как будет выглядеть переиндексация имеющейся здоровой базы. Для Foreign Key есть ObjectField
myarik
02.05.2017 18:14Два варианта переиндексации:
from elasticsearch.client import IndicesClient from .serializers import Blog, ElasticBlogSerializer # Удаляем индекс indices_client = IndicesClient(client=es_client) if indices_client.exists('blog'): indices_client.delete(index='blog') # Вариант 1 # Создаем запись в elasticsearch для каждого объекта for instance in Blog.object.all().iterator(): obj = ElasticBlogSerializer(instance) obj.save() # Вариант 2 c использованием bulk-а from elasticsearch.helpers import bulk actions = [] for item in Blog.object.all().iterator(): actions.append(ElasticBlogSerializer.es_repr(item).to_dict(include_meta=True)) bulk(client=es_client, actions=actions)
bulk работает быстрее но учтите что может упасть по timeout-у на AWSrandom1st
02.05.2017 18:41Что касается упавшего балка. Таймаут выставить побольше, выбрать оптимальный размер. Опять же, если индексируем новый кластер, имеет смысл выключить рефреш и реплику на время индекса. Но это только на свежем кластере.
random1st
02.05.2017 18:45Опять же сам helpers.bulk поддерживает чанки и еще ограничение на размер чанка.
myarik
02.05.2017 18:32Для Foreign Key нужно использовать ObjectField.
Описание документа
from elasticsearch_dsl import Object class BlogIndex(DocType): pk = Integer() ... author = Object( properties={ 'name': Text(fields={'raw': Keyword()}), 'pk': Integer(), } )
Пример сериалайзера
class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = ('pk', 'name') class ElasticBlogSerializer(ElasticModelSerializer): author = AuthorSerializer() class Meta: model = Blog es_model = BlogIndex fields = ('pk', 'title', 'created_at', 'tags', 'body', 'is_published', 'author')
shurashov
02.05.2017 19:02обновление индекса по сигналу хорошо только для тестирования или если сохранения модели редки. в остальных случаях чаще используют очередь фоновых задач, которая запускается по сигналу, как например сделано в celery-haystack и подобных пакетах
MadWombat
Чем эта библиотека лучше/хуже чем haystack?
myarik
haystack в данный момент не поддерживает Elasticsearch версии 5.x.
random1st
Он, по-моему, и вторую-то не очень.
shurashov
рабочие бекэнды под пятый эластик есть, использую этот для поиска и mlt