Поиск в Django REST Framework с помощью Elasticsearch

Пользователи ищут товары в интернет-магазине, ищут стати, поиск это неотъемлемый компонент сайта. Быстрый и гибкий поиск сложно реализовать средствами реляционных баз данных. Для таких задач используют поисковые движки, один из которых 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

    0
    Чем эта библиотека лучше/хуже чем haystack?
      0
      haystack в данный момент не поддерживает Elasticsearch версии 5.x.
        0
        Он, по-моему, и вторую-то не очень.
          0

          рабочие бекэнды под пятый эластик есть, использую этот для поиска и mlt

        0
        Спасибо за очень полезную статью, хотелось бы побольше таких дайджестов!)
          0
          Я рекомендую для описания документов и прочего использовать сам elasticsearch-dsl. Он позволяет более гибко задавать настройки аналайзеров и маппингов и много других нативных возможностей. Не понятно, как это сделать в вашем варианте, по-моему, без использования специфических возможностей ES прекрасно можно и по базе искать. Не понял смысла создания еще одного слоя абстракции.
            0
            Для описание документа как раз используется elasticsearch-dsl. Текущая библиотека позволяет просто преобразовывать django объекты в документы elasticsearch. И использовать elasticsearch с Django REST Framework — ом
            0
            Интересено было бы глянуть на ситуацию, когда Blog представлет из себя развесистую сущность с несколькими foreign key.
              +1
              мне больше интересно, как будет выглядеть переиндексация имеющейся здоровой базы. Для Foreign Key есть ObjectField
                0
                Два варианта переиндексации:
                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-у на AWS
                  0
                  Что касается упавшего балка. Таймаут выставить побольше, выбрать оптимальный размер. Опять же, если индексируем новый кластер, имеет смысл выключить рефреш и реплику на время индекса. Но это только на свежем кластере.
                    0
                    Опять же сам helpers.bulk поддерживает чанки и еще ограничение на размер чанка.
                  0
                  Для 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')
                  
                  0

                  обновление индекса по сигналу хорошо только для тестирования или если сохранения модели редки. в остальных случаях чаще используют очередь фоновых задач, которая запускается по сигналу, как например сделано в celery-haystack и подобных пакетах

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

                  Самое читаемое