ElasticSearch con Django de forma fácil

Hace un tiempo, estaba trabajando en un proyecto de Django y quería implementar una búsqueda rápida de texto libre. En lugar de utilizar una base de datos normal para esta función de búsqueda, como MySQL o PostgreSQL, decidí utilizar una base de datos NoSQL. Fue entonces cuando descubrí ElasticSearch.

ElasticSearch indexa documentos para sus datos en lugar de usar tablas de datos como lo hace una base de datos relacional normal. Esto acelera la búsqueda y ofrece muchos otros beneficios que no obtiene con una base de datos normal. También mantuve una base de datos relacional regular para almacenar detalles de usuario, inicios de sesión y otros datos que ElasticSearch no necesitaba indexar.

Después de buscar durante mucho tiempo cómo implementar correctamente ElasticSearch con Django, realmente no encontré ninguna respuesta satisfactoria. Algunas guías o tutoriales eran complicados y parecían estar dando pasos innecesarios para indexar los datos en ElasticSearch. Había bastante información sobre cómo realizar la búsqueda, pero no tanta sobre cómo se debería realizar la indexación. Sentí que debía haber una solución más simple, así que decidí intentarlo yo mismo.

Quería mantenerlo lo más simple posible, porque las soluciones simples tienden a ser las mejores en mi opinión. KISS (Keep It Simple Stupid), Menos es más y todas esas cosas son algo que me resuena mucho, especialmente cuando todas las demás soluciones son complejas. Decidí usar el ejemplo de Honza Král en este video para tener algo en lo que basar mi código. Recomiendo verlo, aunque en este momento está un poco desactualizado.

Como estaba usando Django, que está escrito en Python, fue fácil interactuar con ElasticSearch. Hay dos bibliotecas cliente para interactuar con ElasticSearch con Python. Hay elasticsearch-py, que es el cliente oficial de bajo nivel. Y está elasticsearch-dsl, que se basa en el primero pero ofrece una abstracción de nivel superior con un poco menos de funcionalidad.

Pronto veremos algún ejemplo, pero primero necesito aclarar lo que queremos lograr:

  • Configurar ElasticSearch en nuestra máquina local y asegurarse de que funcione correctamente
  • Configurando un nuevo proyecto de Django
  • Indexación masiva de datos que ya están en la base de datos
  • Indexación de cada nueva instancia que un usuario guarda en la base de datos
  • Un ejemplo de búsqueda básico

De acuerdo, eso parece bastante simple. Comencemos instalando ElasticSearch en nuestra máquina. Además, todo el código estará disponible en mi GitHub para que pueda seguir fácilmente los ejemplos.

Instalación de ElasticSearch

Dado que ElasticSearch se ejecuta en Java, debe asegurarse de tener una versión JVM actualizada. Comprueba qué versión tienes java -versionen la terminal. Luego, ejecuta los siguientes comandos para crear un nuevo directorio, descargar, extraer e iniciar ElasticSearch:

mkdir elasticsearch-example
wget //artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.1.1.tar.gz
tar -xzf elasticsearch-5.1.1.tar.gz
./elasticsearch-5.1.1/bin/elasticsearch

Cuando ElasticSearch se inicia, debería haber una gran cantidad de resultados impresos en la ventana del terminal. Para comprobar que está funcionando correctamente, abra una nueva ventana de terminal y ejecute este curlcomando:

curl -XGET //localhost:9200

La respuesta debería ser algo como esto:

{ "name" : "6xIrzqq", "cluster_name" : "elasticsearch", "cluster_uuid" : "eUH9REKyQOy4RKPzkuRI1g", "version" : { "number" : "5.1.1", "build_hash" : "5395e21", "build_date" : "2016-12-06T12:36:15.409Z", "build_snapshot" : false, "lucene_version" : "6.3.0" }, "tagline" : "You Know, for Search"

Genial, ahora tiene ElasticSearch ejecutándose en su máquina local. Es hora de configurar su proyecto Django.

Configurando un proyecto de Django

Primero crea un entorno virtual con virtualenv venvy lo ingresa con source venv/bin/activatepara mantener todo contenido. Luego instalas algunos paquetes:

pip install djangopip install elasticsearch-dsl

Para iniciar un nuevo proyecto de Django, ejecuta:

django-admin startproject elasticsearchprojectcd elasticsearchprojectpython manage.py startapp elasticsearchapp

Después de crear sus nuevos proyectos de Django, debe crear un modelo que utilizará. Para esta guía, elegí ir con un buen ejemplo de publicación de blog pasada de moda. En models.pyusted coloca el siguiente código:

from django.db import modelsfrom django.utils import timezonefrom django.contrib.auth.models import User# Create your models here.# Blogpost to be indexed into ElasticSearchclass BlogPost(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="blogpost") posted_date = models.DateField(default=timezone.now) title = models.CharField(max_length=200) text = models.TextField(max_length=1000)

Bastante sencillo, hasta ahora. No se olvide de añadir elasticsearchappa INSTALLED_APPSen settings.pyy registrar su nuevo modelo BlogPost de admin.pyla siguiente manera:

from django.contrib import adminfrom .models import BlogPost# Register your models here.# Need to register my BlogPost so it shows up in the adminadmin.site.register(BlogPost)

También debe python manage.py makemigrations, python manage.py migrate y python manage.py createsuperuserpara crear la base de datos y una cuenta de administrador. Ahora, python manage.py runservervaya a //localhost:8000/admin/e inicie sesión. Ahora debería poder ver el modelo de publicaciones de su blog allí. Continúe y cree su primera publicación de blog en el administrador.

¡Felicitaciones, ahora tiene un proyecto de Django en funcionamiento! Finalmente es hora de entrar en lo divertido: conectar ElasticSearch.

Conectando ElasticSearch con Django

Empiece creando un nuevo archivo llamado search.pyen nuestro elasticsearchappdirectorio. Aquí es donde vivirá el código ElasticSearch. Lo primero que debe hacer aquí es crear una conexión desde su aplicación Django a ElasticSearch. Haz esto en tu search.pyarchivo:

from elasticsearch_dsl.connections import connectionsconnections.create_connection()

Ahora que tiene una conexión global con su configuración de ElasticSearch, necesita definir qué desea indexar en ella. Escribe este código:

from elasticsearch_dsl.connections import connectionsfrom elasticsearch_dsl import DocType, Text, Dateconnections.create_connection()class BlogPostIndex(DocType): author = Text() posted_date = Date() title = Text() text = Text() class Meta: index = 'blogpost-index'

Se parece bastante a tu modelo, ¿verdad? El DocTypefunciona como un contenedor para permitirle escribir un índice como un modelo, y Texty Dateson los campos para que obtengan el formato correcto cuando se indexan.

Dentro del Meta le dice a ElasticSearch cómo quiere que se llame el índice. Este será un punto de referencia para ElasticSearch para que sepa con qué índice está tratando al inicializarlo en la base de datos y guardar cada nueva instancia de objeto creada.

Ahora necesita crear el mapeo de su recién creado BlogPostIndexen ElasticSearch. Puede hacer esto y también crear una forma de hacer la indexación masiva al mismo tiempo, ¿qué tan conveniente, verdad?

Indexación masiva de datos

El bulkcomando se encuentra en el elasticsearch.helpersque se incluye cuando lo instaló, elasticsearch_dslya que está construido sobre esa biblioteca. Haz lo siguiente en search.py:

...from elasticsearch.helpers import bulkfrom elasticsearch import Elasticsearchfrom . import models...
...def bulk_indexing(): BlogPostIndex.init() es = Elasticsearch() bulk(client=es, actions=(b.indexing() for b in models.BlogPost.objects.all().iterator()))

"¿Que esta pasando aqui?" podrías estar pensando. En realidad, no es tan complicado.

Since you only want to do bulk indexing whenever you change something in our model you init() the model which maps it into ElasticSearch. Then, you use the bulk and pass it an instance of Elasticsearch() which will create a connection to ElasticSearch. You then pass a generator to actions= and iterate over all the BlogPost objects you have in your regular database and call the .indexing() method on each object. Why a generator? Because if you had a lot of objects to iterate over a generator would not have to first load them into memory.

There is just one problem with the above code. You don’t have an .indexing() method on your model yet. Lets fix that:

...from .search import BlogPostIndex...
...# Add indexing method to BlogPostdef indexing(self): obj = BlogPostIndex( meta={'id': self.id}, author=self.author.username, posted_date=self.posted_date, title=self.title, text=self.text ) obj.save() return obj.to_dict(include_meta=True)

You add the indexing method to the BlogPost model. It returns a BlogPostIndex and gets saved to ElasticSearch.

Lets try this out now and see if you can bulk index the blog post you previously created. By running python manage.py shell you go into the Django shell and import your search.py with from elasticsearchapp.search import * and then run bulk_indexing() to index all the blog posts in your database. To see if it worked you run the following curl command:

curl -XGET 'localhost:9200/blogpost-index/blog_post_index/1?pretty'

You should get back your first blog post in the terminal.

Indexing of newly saved instance

Next you need to add a signal that fires the .indexing() on each new instance that is saved every time a user saves a new blog post. In elasticsearchapp create a new file called signals.py and add this code:

from .models import BlogPostfrom django.db.models.signals import post_savefrom django.dispatch import [email protected](post_save, sender=BlogPost)def index_post(sender, instance, **kwargs): instance.indexing()

The post_save signal will ensure that the saved instance will get indexed with the .indexing() method after it is saved.

In order for this to work we also need to register Django that we’re using signals. We do this opening apps.py and adding the following code:

from django.apps import AppConfigclass ElasticsearchappConfig(AppConfig): name = 'elasticsearchapp' def ready(self): import elasticsearchapp.signals

To to complete this we also need to tell Django that we’re using this new configuration. We do this inside the __init__.py inside our elasticsearchapp directory by adding:

default_app_config = 'elasticsearchapp.apps.ElasticsearchappConfig'

Now the post_save signal is registered with Django and is ready to listen for whenever a new blogpost is being saved.

Try it our by going into the Django admin again and saving a new blogpost. Then check with a curl command if it was successfully indexed into ElasticSearch.

Simple search

Now lets make a simple search function in search.py to find all posts filtered by author:

...from elasticsearch_dsl import DocType, Text, Date, Search...
...def search(author): s = Search().filter('term', author=author) response = s.execute() return response

Lets try the search out. In the shell: from elasticsearchapp.search import * and run print(search(author="gt;")) :

>>> print(search(author="home"))

There you have it! You have now successfully indexed all your instances into ElasticSearch, created a post_save signal that indexes each newly saved instance, and created a function to search our ElasticSearch database for your data.

Conclusion

This was a quite lengthy article but I hope it is written simple enough for even the beginner to be able to understand.

I explained how to connect a Django model to ElasticSearch for indexing and searching, but there is so much more that ElasticSearch can do. I recommend reading on their website and exploring what other possibilities exist, such as spatial operations and full text search with intelligent highlighting. Its a great tool and I will be sure to use it in future projects!

If you liked this article or have a comment or suggestion, please feel free to leave a message below. And stay tuned for more interesting stuff!

Original text