ESEngine – Um jeito Pythonico de se comunicar com o Elasticsearch

Hoje vamos compartilhar um pouco do nosso Python way of life. Vamos falar um pouco de como armazenamos e lidamos com um volume considerável de dados em nossos projetos de Inteligência artificial. Além disso, vamos apresentar nossa biblioteca python de acesso ao Elasticsearch, o Esengine.

Aqui no Catholabs utilizamos como DataStore principal o Elasticsearch, que foi escolhido por ser uma máquina de busca robusta, rápida, fácil de indexar e de consultar usando a sua API REST e com features avançadas como scripting language e analisadores personalizáveis.

O Elasticsearch tem se saído muito bem como um único ponto de acesso a dados, principalmente por sua facilidade de uso tanto do ponto de vista de desenvolvimento quanto de infra, já que manter um cluster Elasticsearch é bem simples.

Nossa atual stack:

ESENGINE
Uma vez que os dados estejam indexados, acessá-los, via a API REST do ElasticSearch, é bastante natural através da biblioteca python Requests, como é mostrado no exemplo abaixo:

import requests
results = requests.get(“http://elastichost:9200/index/doctype/_search”, data={“query”: {...}})
results.content
“{...... pure json string …. }”

É claro que o retorno da requisição REST tem que ser parseado. A própria Elastic Inc. mantém uma biblioteca de mais alto nível chamada Elasticsearch-py que, além de cuidar da requisição, faz o parsing da resposta por nós. Entretanto, ainda temos que montar um “payload” data com os nossos argumentos e queries conforme o seguinte exemplo:

from elasticsearch import Elasticsearch
es = Elasticsearch(“elastichost”)
result = es.search(index=”index”, doctype=”doctype”, body={“query”: {...}})
result
{ a parsed python dictionary with [‘hits’]... }

Para uma aplicação simples isso seria suficiente. Entretanto o exemplo acima não segue o conceito DRY (Don’t Repeat Yourself), já que sempre precisaremos informar a query, o doctype, o index. Assim, podemos simplificar nosso código ainda mais usando uma biblioteca de mais alto nível que implemente esse conceito.

Para este fim a Elastic Inc desenvolveu a Elasticsearch-DSL que é bastante simples e completa. Nós chegamos a utilizar esta biblioteca porém tivemos os seguintes problemas:

  • Construção de queries complicadas utilizando muita sobrecarga de operadores;
  • Bugs no acesso a membros dos nossos models por conta do uso excessivo de metaprogramação;
  • Problemas de performance;
  • Necessidade de utilizar “gambiarras” para garantir a formação de algumas queries. (exemplo: Necessidade de usar ~Q("match", _id=0) no começo de uma query para garantir que ela seria montada corretamente) OBS: esse caso já foi atualmente resolvido no DSL.

Apesar da maioria dos problemas do DSL terem sido atualmente resolvidos, decidimos que iríamos escrever nosso próprio ODM (Object Doctype Mapper) e então começamos a desenvolver o ESEngine inspirados pela experiência que tivemos com o uso do MongoEngine. Estamos agora abrindo o código do ESEngine e disponibilizando para a comunidade.

O elasticserarch-dsl prega o lema da abstração “Pense apenas em objetos Python e não se preocupe com as queries do Elasticsearch”. Entretanto, aqui no Catholabs acreditamos muito no domínio das tecnologias, bem como da nossa stack de código. Assim o ESEngine foi escrito com a seguinte filosofia “Conheça muito bem as suas queries Elasticsearch e então as escreva de maneira Pythonica”.

O ESEngine é um mapeador de documentos e índices Elasticsearch em forma de classes Python e, além disso, oferece algumas facilidades para a construção de queries de uma forma um pouco menos abstrata.

Abrimos o ESEngine sob a licenca MIT e o código está disponível no Github,https://github.com/catholabs/esengine

A documentação por enquanto se apresenta na forma de um README bastante completo emhttp://catholabs.github.io/esengine/ e também as Docstrings que podem ser lidas através do Epydochttp://catholabs.github.io/esengine/docs/

Como o ESEngine funciona?

Primeiro você precisa de um client do Elasticsearch e recomendamos o uso do client oficial, e então utilizando a classe base Document define o modelo de um documento.

# myproject/models.py
from elasticsearch import Elasticsearch
from esengine import Document, StringField, BooleanField

class Person(Document):
    # definição dos meta atributos
    _index = 'myproject'
    _doctype = 'person'

    # cliente default para conexões
    _es = Elasticsearch()  # opcional, pode ser passado como argumento depois.

    # definição dos campos
    name = StringField()
    active = BooleanField()

Person.init()

OBS: A chamada ao método init() irá inicializar o documento, este procedimento garante que o mapping seja corretamente carregado no Elasticsearch, esta parte pode ser ignorada e então o Elasticsearch criará o mapping via introspecção de valores quando o primeiro documento for salvo.

Com as definições acima em um arquivo chamado por exemplo myproject/models.py podemos então utilizar a classe Person para inserir, editar, excluir e pesquisar documentos.

Em um terminal iPython por exemplo:

>>> from myproject.models import Person

Indexando:

>>> user = Person(name=”Bruno”, active=True)
>>> user.save()
# ou simplesmente
>>> user =  Person.create(name=”Bruno”, active=True)

Atualizando

>>> user.active = False
>>> user.save()
# ou simplesmente
>>> user.update(active=False)

Buscando múltiplos documentos utilizando filter

>>> users = Person.filter(active=True)
[ ResultSet generator… a list of active users ]

Atualizando em Bulk

>>> users.update(active=False)

Efetuando raw queries (recomendado)

>>> query = {“query”: {“match_all”: {}}, “sort”: “name”} 
>>> Person.search(query=query, size=10)

Efetuando search queries utilizando Payload helpers (melhor para compor queries dinâmicas)

>>> from esengine import Payload, Query
>>> query = Query.match_all() 
>>> Payload(model=Person, query=query, sort=”name”).search(size=10)

Excluindo documentos

>>> user = Person.get(id=123)
>>> user.delete()
# ou simplesmente
>>> Person.delete_by_id(123)
# ou em bulk
>>> users = Person.filter(active=False)
>>> Person.delete_all(users)
# ou simplesmente
>>> Person.delete_by_query({“query”: …. })

Maiores exemplos de uso estão no repositório https://github.com/catholabs/esengine

Atualmente, o Esengine é usado em três de nossos projetos (dois deles com a versão mais madura da biblioteca e o outro uma versão preliminar) e tem alcançado níveis de performance e abstração adequados às nossas necessidades.

Se você usa o Elasticsearch o Esengine pode ser uma adição interessante a sua stack. Confira la no github e fique a vontade para abrir issues sugerindo melhorias ou reportando bugs, além disso Pull Requests também serão muito bem vindos 🙂

 

Autores:  Bruno Rocha, Eder F. Martins

Leave a Reply

Your email address will not be published. Required fields are marked *