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


  1. MadWombat
    02.05.2017 12:42

    Чем эта библиотека лучше/хуже чем haystack?


    1. myarik
      02.05.2017 12:51

      haystack в данный момент не поддерживает Elasticsearch версии 5.x.


      1. random1st
        02.05.2017 16:03

        Он, по-моему, и вторую-то не очень.


      1. shurashov
        02.05.2017 18:33

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


  1. daake
    02.05.2017 12:42

    Спасибо за очень полезную статью, хотелось бы побольше таких дайджестов!)


  1. random1st
    02.05.2017 15:11

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


    1. myarik
      02.05.2017 18:19

      Для описание документа как раз используется elasticsearch-dsl. Текущая библиотека позволяет просто преобразовывать django объекты в документы elasticsearch. И использовать elasticsearch с Django REST Framework — ом


  1. tbicr
    02.05.2017 15:53

    Интересено было бы глянуть на ситуацию, когда Blog представлет из себя развесистую сущность с несколькими foreign key.


    1. random1st
      02.05.2017 16:42
      +1

      мне больше интересно, как будет выглядеть переиндексация имеющейся здоровой базы. Для Foreign Key есть ObjectField


      1. 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-у на AWS


        1. random1st
          02.05.2017 18:41

          Что касается упавшего балка. Таймаут выставить побольше, выбрать оптимальный размер. Опять же, если индексируем новый кластер, имеет смысл выключить рефреш и реплику на время индекса. Но это только на свежем кластере.


        1. random1st
          02.05.2017 18:45

          Опять же сам helpers.bulk поддерживает чанки и еще ограничение на размер чанка.


    1. 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')
      


  1. shurashov
    02.05.2017 19:02

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