Elasticsearch, mas com a conveniência do pandas
Elasticsearch para ciência de dados acabou de ficar muito mais fácil
Eland é um novo módulo python que facilita integração de Elasticsearch com o ecossistemas de data science.
Elasticsearch é um poderoso motor de busca desenvolvido em cima do Apache Lucene (uma das bibliotecas de busca textual mais importantes do mercado). Elasticsearch é conhecido pela experiência de REST API altamente versátil e expansiva, incluindo abstrações para full-text search, sorting e aggregações complexas, tornando muito mais fácil integrar tais features em backends já existentes.
Desde a sua introdução em 2010, o Elasticseach ganhou bastante notoriedade na comunidade de software de código-aberto e em 2016 se tornou o motor de busca empresarial mais popular de acordo com o agregador de conhecimento em bases de dados DB-engines, desbancando o Apache Solr (que também é construído com base em Lucene).
Uma das coisas que faz o Elasticsearch ser tão popular é o ecossistema que acabou criando. Vários desenvolvedores ao redor do mundo trabalharam em integrações e extensões do projeto base e muitos destes projetos de código-aberto foram eventualmente absorvidos pela própria Elastic (a empresa por trás do projeto do Elasticsearch) como parte de sua stack principal.
Projetos como o Logstash (usado para processamento de dados, ETL e especialmente em tratamento de arquivos baseados em texto) e o Kibana (plataforma de visualização integrada com o Elasticsearch) foram adotados como componentes primárias da suíte de software da Elastic, culminando na instituição da stack ELK (Elasticsearch, Logstash, Kibana).
Devido a sua versatilidade e aplicabilidade em diversos cenários e casos de uso, a stack da Elastic ganhou adoção também em vários outros campos de conhecimento da área de tecnologia e software, como em DevOps, Site-reliability Engineering e, mais recentemente, Data Analytics.
Mas e para ciência de dados, como fica?
É possível que, se você é um(a) cientista de dados lendo este artigo e precisa utilizar o Elasticsearch como parte de sua stack de software, você pode ter tido algum problema ou dificuldade tentando utilizar as ótimas features que a ferramenta provê para análise de dados e até mesmo para jobs de Machine Learning simples.
Cientistas de dados no geral não estão acostumados com bancos não-relacionais e estruturas orientadas a documentos, ou mesmo a utilizar REST APIs complexas para suportar análises. Lidar com volumes de dados expressivos usando o cliente baixo-nível python para Elasticsearch, por exemplo, pode ser pouco intuitivo e requer curva de aprendizado não-trivial para pessoas que não vêm do campo da Engenharia de Software.
Apesar da Elastic ter alocado esforços significativos para adaptar a stack de software ELK aos contextos de data analytics e ciência de dados, ainda faltava uma interface simples e eficiente com o ecossistema de data science já existente (bibliotecas populares pandas, numpy, scikit-learn e PyTorch).
Em 2017, a Elastic até deu um grande passo em direção ao campo da ciência de dados e, como uma respoosta ao interesse crescente em Machine Learning tecnologias preditivas, lançou o primeiro X-pack (pacote de expansão) para aprendizado de máquina, que introduziu rotinas de Detecção de Anomalias e outras rotinas de aprendizado não-supervisionado. Não muito tempo depois, também foram introduzidas rotinas de Regressãoo e Classificação (1) na stack.
Semana passada, a Elastic andou mais uma vez em direção a ganhar maior adoção no mercado de data science ao lançar o Eland, uma interface em python com sintaxe e features similares ao pacote pandas, facilitando processos de análise, ETL e modelagem com o Elasticsearch.
Eland: Elastic and Data
O pacote Eland permite a cientistas de dados usarem de forma eficiente os recursos de analytics e Machine Learning já existentes na stack da Elastic sem precisarem de um conhecimento extensivo sobre o Elasticsearch e suas particularidades.
Features e conceitos do Elasticsearch foram mapeados para um contexto mais comum no dia-a-dia de um(a) cientista de dados. Por exemplo, um índice Elasticsearch, com seus documentos, mapas e campos, se torna uma dataframe, com linhas e colunas, muito parecido com o que se está acostumado a ver em pandas.
# importando o Eland e clientes python para comparação
import eland as ed
from eland.conftest import *
from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search, Q# importando o pandas e numpy para processamento de dados
import pandas as pd
import numpy as np# para printar os documentos
import json
Casos de uso comuns em cenários de ciência de dados como ler um índice Elasticsearch inteiro e transferí-lo para um dataframe pandas para Análise Exploratória de Dados ou para realiza engenharia de features e treinar um modelo de Machine Learning normalmente necessitariam de alguns processos ineficientes.
# nome do índice que queremos estudar.
index_name = 'kibana_sample_data_ecommerce' # instanciando o cliente elasticsearch
es = Elasticsearch()# defining o objeto de busca que utilizaremos para fazer a query
search = Search(using=es, index=index_name).query("match_all") # extraindo os documentos do índice
documents = [hit.to_dict() for hit in search.scan()] # convertendo a lista de dicionários numa dataframe
df_ecommerce = pd.DataFrame.from_records(documents)# visualizando os resultados
df_ecommerce.head()['geoip']0 {'country_iso_code': 'EG', 'location': {'lon':...
1 {'country_iso_code': 'AE', 'location': {'lon':...
2 {'country_iso_code': 'US', 'location': {'lon':...
3 {'country_iso_code': 'GB', 'location': {'lon':...
4 {'country_iso_code': 'EG', 'location': {'lon':...
Name: geoip, dtype: object# resumindo as colunas da dataframe
df_ecommerce.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4675 entries, 0 to 4674
Data columns (total 23 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 category 4675 non-null object
1 currency 4675 non-null object
2 customer_first_name 4675 non-null object
3 customer_full_name 4675 non-null object
4 customer_gender 4675 non-null object
5 customer_id 4675 non-null int64
6 customer_last_name 4675 non-null object
7 customer_phone 4675 non-null object
8 day_of_week 4675 non-null object
9 day_of_week_i 4675 non-null int64
10 email 4675 non-null object
11 manufacturer 4675 non-null object
12 order_date 4675 non-null object
13 order_id 4675 non-null int64
14 products 4675 non-null object
15 sku 4675 non-null object
16 taxful_total_price 4675 non-null float64
17 taxless_total_price 4675 non-null float64
18 total_quantity 4675 non-null int64
19 total_unique_products 4675 non-null int64
20 type 4675 non-null object
21 user 4675 non-null object
22 geoip 4675 non-null object
dtypes: float64(2), int64(5), object(16)
memory usage: 840.2+ KB# calculando estatísticas em cima dos dados:
df_ecommerce.describe()
O procedimento acima é bem comum em vários Jupyter Notebooks por aí e exigiria ter tanto a lista de dicionários com os documentos quanto a pandas dataframe referente a estes dados em memória em algum momento (2). Para casos de uso de Big Data, por exemplo, tal processo não seria aplicável, já que requeriria armanezar grandes quantidades de dados em memória para processamento e até mesmo análises simples poderiam demorar.
O Eland nos permite realizar operação muito similares às descritas acima, sem a necessidade de adaptar o mesmo racional ao contexto do Elasticsearch e ao mesmo tempo usando a ferramenta para o que ela faz melhor: busca e agregação.
# lendo o índice de exemplo numa dataframe Eland
ed_ecommerce = ed.read_es('localhost', index_name)# visualizando os resultados:
ed_ecommerce.head()
Pode-se perceber, também, que o campo geoip
(que no dataset original armazena um objeto json) foi parseado sem maiores dificuldades na dataframe Eland. Podemos observar esse comportamento utilizando o método .info()
nela, da mesma que faríamos com uma dataframe pandas.
# resumindo as colunas e registros da dataframe eland
ed_ecommerce.info()<class 'eland.dataframe.DataFrame'>
Index: 4675 entries, jyzpQ3MBG9Z35ZT1wBWt to 0SzpQ3MBG9Z35ZT1yyej
Data columns (total 45 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 category 4675 non-null object
1 currency 4675 non-null object
2 customer_birth_date 0 non-null datetime64[ns]
3 customer_first_name 4675 non-null object
4 customer_full_name 4675 non-null object
5 customer_gender 4675 non-null object
6 customer_id 4675 non-null object
7 customer_last_name 4675 non-null object
8 customer_phone 4675 non-null object
9 day_of_week 4675 non-null object
10 day_of_week_i 4675 non-null int64
11 email 4675 non-null object
12 geoip.city_name 4094 non-null object
13 geoip.continent_name 4675 non-null object
14 geoip.country_iso_code 4675 non-null object
15 geoip.location 4675 non-null object
16 geoip.region_name 3924 non-null object
17 manufacturer 4675 non-null object
18 order_date 4675 non-null datetime64[ns]
19 order_id 4675 non-null object
20 products._id 4675 non-null object
21 products.base_price 4675 non-null float64
22 products.base_unit_price 4675 non-null float64
23 products.category 4675 non-null object
24 products.created_on 4675 non-null datetime64[ns]
25 products.discount_amount 4675 non-null float64
26 products.discount_percentage 4675 non-null float64
27 products.manufacturer 4675 non-null object
28 products.min_price 4675 non-null float64
29 products.price 4675 non-null float64
30 products.product_id 4675 non-null int64
31 products.product_name 4675 non-null object
32 products.quantity 4675 non-null int64
33 products.sku 4675 non-null object
34 products.tax_amount 4675 non-null float64
35 products.taxful_price 4675 non-null float64
36 products.taxless_price 4675 non-null float64
37 products.unit_discount_amount 4675 non-null float64
38 sku 4675 non-null object
39 taxful_total_price 4675 non-null float64
40 taxless_total_price 4675 non-null float64
41 total_quantity 4675 non-null int64
42 total_unique_products 4675 non-null int64
43 type 4675 non-null object
44 user 4675 non-null object
dtypes: datetime64[ns](3), float64(12), int64(5), object(25)
memory usage: 96.0 bytes# calculando estatísticos sobre os mesmos dados:
ed_ecommerce.describe()
É notável também que a utilização de memória foi de 840 Kbs na pandas dataframe para algo em torno de 96 bytes na dataframe Eland. Isso acontece porque não precisamos extrair o dataset inteiro para calcular essas informações (o Elasticsearch já possui APIs que nos trazem essa informação), fazendo com que a maior parte do workload permaneça no próprio cluster Elasticsearch (3) do qual os dados são extraídos.
Para um dataset pequeno, tais ganhos não parecem ser importantes, mas conforme escalamos este processo para GBs de dados, os benefícios de não manter os dados em memória para análises simples é notável.
Features do Elasticsearch para DataFrames
O Eland abstrai muitas das APIs existentes do Elasticsearch, sem que o(a) cientista de dados precise aprender a sintax peculiar da ferramenta. Por exemplo, é possível extrair o mapping de um índice (algo análogo a extrair os dtypes
de uma dataframe pandas), mas não é imediatamente óbvio como fazer isso. Com o dataframe Eland, podemos simples chamar dtypes
da mesma forma que faríamos com pandas.
# extraindo os dtypes da dataframe original:
df_ecommerce.dtypescategory object
currency object
customer_first_name object
customer_full_name object
customer_gender object
...
total_quantity int64
total_unique_products int64
type object
user object
geoip object
Length: 23, dtype: object# para extrair a mesma informação no Elasticsearch, precisaríamos fazer algo como o descrito abaixo:
mapping = es.indices.get_mapping(index_name) # que por sua vez já é uma abstração do GET request que seria necessário.
print(json.dumps(mapping, indent=2, sort_keys=True)){
"kibana_sample_data_ecommerce": {
"mappings": {
"properties": {
"category": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"currency": {
"type": "keyword"
},
"customer_birth_date": {
"type": "date"
},
"customer_first_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_full_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
}
...# o Eland reproduz esse mesmo conceito usando a API igual ao pandas
ed_ecommerce.dtypescategory object
currency object
customer_birth_date datetime64[ns]
customer_first_name object
customer_full_name object
...
taxless_total_price float64
total_quantity int64
total_unique_products int64
type object
user object
Length: 45, dtype: object
Com todas essas abstrações, o Eland facilita muito usar features específicas do Elasticsearch que não são implementadas atualmente em pandas (ou pelo menos não são tão eficientes quanto no Elasticsearch), como busca full-text, o caso de uso principal da stack da Elastic.
# definindo a busca full-text que testaremos:
# extraindo os produtores Elitelligence ou Primemaster
query = {
"query_string" : {
"fields" : ["manufacturer"],
"query" : "Elitelligence OR Primemaster"
}
}# usando full-text search com o eland:
text_search_df = ed_ecommerce.es_query(query)# visualizando os resultados com seletores de coluna:
text_search_df[['manufacturer','products.price']]
Mais possibilidades para integrações
Esse artigo mal tocou em todas as possibilidades que o Eland abre para cientistas de dados e outros profissionais do ramo usando Elasticsearch no dia-a-dia.
Especialmente em contextos de DevOps e AIOps, onde ferramentas e processos de Machine Learning ainda não são tão maduras, profissionais de dados podem se beneficiar amplamente do ecossistema de Machine Learning já existente em Python para analisar grandes volumes de dados de Observabilidade e métricas, assunto que será que abordado em outro artigo.
O Eland certamente representa um grande passo do Elasticsearch e estou animado para saber o que a stack nos tratá no futuro no campo de ciência de dados e machine learning.
Se você gostou deste artigo
Dê uma olhada nesse webinar em que o Seth Michael Larson (um dos criadores do Eland) fala sobre as features principais do pacote.
Se você quer ver mais conteúdo sobre Elasticsearch, Data Science e NLP no contexto de Observabilidade, sinta-se à vontade para me adicionar no LinkedIn e ler mais artigos meus sobre esses temas.
Notas:
- No momento, na versão 7.8 do ELK stack, rotinas de Regressão e Classificação ainda são experimentais.
- Existem outras formas de consumir informação do Elasticsearch usando uma sintaxe parecida com SQL, como o JDBC driver do Elasticsearch, mas ainda requeririam algum conhecimento prévio em conceitos e estruturas do Elasticsearch (como padrões de índice e paginação).
- Isso funciona de uma forma parecida como outras frameworks de processamento distribuído funcionam (como Dask, por exemplo). No caso do Eland, um query builder e gráfo de execução é mantido internamente e realiza as operações apenas no momento em que os dados são efetivamente requisitados.
Referências:
- P. Andlinger, Elasticsearch replaced Solr as the most popular search engine (2016), URL para o post no DB-engines.
- Elastic.co, Official Eland documentation (2020), URL para o website.
- Seth Michael Larson, Introduction into Eland — DataFrames and Machine Learning backed by Elasticsearch (2020), URL para o webinar.