Os cientistas de dados começam a aprender SQL desde cedo. Isso é compreensível, dada a ubiquidade e a alta utilidade das informações tabulares. No entanto, existem outros formatos de banco de dados bem-sucedidos, como os bancos de dados de grafos, para armazenar dados conectados que não se encaixam em um banco de dados SQL relacional. Neste tutorial, vamos aprender sobre o Neo4j, um popular sistema de gerenciamento de banco de dados de grafos que você pode usar para criar, gerenciar e consultar bancos de dados de grafos em Python.
O Que São Bancos de Dados de Grafos?
Antes de falar sobre o Neo4j, vamos entender melhor os bancos de dados de grafos. Temos um artigo completo explicando o que são os bancos de dados de grafos, então vamos resumir os pontos chave aqui.
Bancos de dados de grafos são um tipo de banco de dados NoSQL (não usam SQL) projetados para gerenciar dados conectados. Diferentemente dos bancos de dados relacionais tradicionais que usam tabelas e linhas, os bancos de dados de grafos usam estruturas de grafos que são compostas por:
- Nós (entidades) como pessoas, lugares, conceitos
- Limites (relacionamentos) que conectam diferentes nós como pessoa MORA EM um local, ou jogador de futebol MARCOU EM um jogo.
- Propriedades (atributos para nós/arestas) como a idade de uma pessoa ou quando foi marcado o gol no jogo.
Esta estrutura faz com que os bancos de dados de grafos sejam ideais para manipular dados interconectados em campos e aplicações como redes sociais, recomendações, detecção de fraudes, etc., frequentemente superando os bancos de dados relacionais em termos de eficiência de consulta. Aqui está a estrutura de um exemplo de banco de dados de grafo para um conjunto de dados de futebol:
Ainda que este gráfico represente algo bastante intuitivo para os seres humanos, pode ficar muito complicado se desenhado em uma tela. Mas, com Neo4j, percorrer este gráfico será tão simples quanto escrever comandos de junções em SQL.
O gráfico possui seis nós: Ligação, Time, Torneio, Jogador, País e Cidade. Os retângulos listam as relações que existem entre nós. Existem também algumas propriedades de nós e relações:
- Partida: data, pontuação_mandante, pontuação_visitante
- Time: nome
- Jogador: nome
- Torneio: nome
- Cidade: nome
- País: nome
- PONTUAÇÃO, PONTUAÇÃO_EM: minuto, gol_próprio, pênalti
- TIVE_PENALTIES: vencedor, primeiro_atirador
Este esquema permite que representemos:
- Todos os jogos com suas pontuações, datas e locais
- Times participantes de cada jogo (casa e fora)
- Jogadores que marcaram gols, incluindo detalhes como o minuto, gols contra e pênaltis
- Torneios que os jogos fazem parte
- Cidades e países onde os jogos são jogados
- Informações de embate, incluindo vencedores e primeiros atiradores (quando disponíveis)
O esquema captura a natureza hierárquica dos locais (Cidade dentro do País) e as várias relações entre entidades (por exemplo, Times jogando Partidas, Jogadores marcando gols para Times em Partidas).
Esta estrutura permite consultas flexíveis, como encontrar todas as partidas entre dois times, todos os gols marcados por um jogador ou todas as partidas em um determinado torneio ou local.
Mas não vamos avançar muito cedo. Para começar, o que é o Neo4j e por que usá-lo?
O que é o Neo4j?
Neo4j, a principal referência no mundo das gerenciadoras de BD em grafos, é conhecida por suas poderosas funcionalidades e versatilidade.
No seu cerne, o Neo4j usa um armazenamento de grafos nativo, que é altamente otimizado para realizar operações de grafos. Sua eficiência em manipular relações complexas o torna superiores aos bancos de dados tradicionais para dados conectados. A escalabilidade do Neo4j é realmente impressionante: ele pode manter milhares de milhões de nós e relações com facilidade, fazendo dele adequado tanto para pequenos projetos quanto para grandes empresas.
Outro aspecto chave do Neo4j é a integridade dos dados. Ele garante plena conformidade com o ACID (Atomicity, Consistency, Isolation, Durability) (Atomismo, Consistência, Isolamento, Durabilidade), fornecendo confiabilidade e consistência nas transações.
Quanto às transações, a sua linguagem de consulta, o Cypher, oferece uma sintaxe muito intuitiva e declarativa projetada para padrões de grafos. Por esse motivo, sua sintaxe foi apelidada de “ASCII art”. O Cypher não será um problema para aprender, especialmente se estiver familiarizado com SQL.
Com o Cypher, é fácil adicionar novos nós, relações ou propriedades sem se preocupar em quebrar consultas ou esquemas existentes. É adaptável às necessidades cambiantes dos ambientes de desenvolvimento modernos.
O Neo4j tem um apoio ecossistêmico vibrante. Possui documentação extensiva, ferramentas amplas para visualizar grafos, uma comunidade ativa e integrações com outras linguagens de programação como Python, Java e JavaScript.
Configurando o Neo4j e um Ambiente Python
Antes de começarmos a trabalhar com o Neo4j, precisamos configurar nosso ambiente. Esta seção guiará você através da criação de uma instância na nuvem para hospedar bancos de dados Neo4j, configurar um ambiente Python e estabelecer uma conexão entre os dois.
Não instale o Neo4j
Se você deseja trabalhar com bancos de dados de grafos locais no Neo4j, você precisará baixar e instalá-lo localmente, juntamente com suas dependências como Java. Mas na maioria dos casos, você estará interagindo com um banco de dados Neo4j remoto existente em algum ambiente de nuvem.
Por esta razão, não iremos instalar o Neo4j no nosso sistema. Em vez disso, criaremos uma instância de banco de dados gratuita no Aura, o serviço de nuvem totalmente gerenciado do Neo4j. Em seguida, usaremos aneo4j
biblioteca de cliente em Python para se conectar a este banco de dados e preencher-lo com dados.
Criando uma instância de banco de dados Neo4j Aura
Para hospedar uma base de dados gráfica grátis no Aura DB, visite sua página de produto e clique em “Iniciar Grátis.”
Após se cadastrar, você será apresentado com os planos disponíveis e deve escolher a opção gratuita. Em seguida, você receberá uma nova instância com um nome de usuário e senha para se conectar:
Copie sua senha, nome de usuário e a URI de conexão.
Então, crie um novo diretório de trabalho e um .env
arquivo para armazenar suas credenciais:
$ mkdir neo4j_tutorial; cd neo4j_tutorial $ touch .env
Coloque o seguinte conteúdo dentro do arquivo:
NEO4J_USERNAME="YOUR-NEO4J-USERNAME" NEO4J_PASSWORD="YOUR-COPIED-NEO4J-PASSWORD" NEO4J_CONNECTION_URI="YOUR-COPIED-NEO4J-URI"
Configurando o Ambiente Python
Agora, vamos instalar a biblioteca de cliente neo4j Python em um novo ambiente Conda:
$ conda create -n neo4j_tutorial python=3.9 -y $ conda activate neo4j_tutorial $ pip install ipykernel # Para adicionar o ambiente ao Jupyter $ ipython kernel install --user --name=neo4j_tutorial $ pip install neo4j python-dotenv tqdm pandas
Os comandos também instalam ipykernel
biblioteca e usam-na para adicionar o ambiente Conda recém-criado ao Jupyter como um kernel. Em seguida, instalamos o cliente neo4j
em Python para interagir com bancos de dados Neo4j e python-dotenv
para gerenciar nossas credenciais Neo4j de forma segura.
Populando uma instância AuraDB com dados de futebol.
A ingestão de dados em um banco de dados gráfico é um processo complexo que requer conhecimento dos fundamentos do Cypher. Como ainda não aprendemos os básicos do Cypher, você vai usar um script em Python que eu preenchi para o artigo, que automaticamente fará a ingestão de dados históricos de futebol do mundo real. O script vai usar as credenciais que você armazenou para se conectar à sua instância do AuraDB.
Os dados de futebol vem de este conjunto de dados do Kaggle sobre partidas internacionais de futebol jogadas entre 1872 e 2024. Os dados estão disponíveis em formato CSV, então o script os descompõe e os converte em formato de gráfico usando Cypher e Neo4j. No final do artigo, quando tivermos confiança suficiente nestas tecnologias, vamos passar o script linha a linha para que você possa entender como converter informações tabulares em um gráfico.
Aqui estão os comandos para executar (certifique-se de ter configurado a instância do AuraDB e armazenado suas credenciais em um .env
arquivo em seu diretório de trabalho):
$ wget https://raw.githubusercontent.com/BexTuychiev/medium_stories/refs/heads/master/2024/9_september/3_neo4j_python/ingest_football_data.py $ python ingest_football_data.py
O script pode demorar alguns minutos para ser executado, dependendo da sua máquina e conexão com a Internet. No entanto, assim que terminar, sua instância do AuraDB deve exibir mais de 64k nós e 340k relações.
Conectando ao Neo4j a partir do Python
Agora, estamos prontos para conectar-nos à nossa instância de BD Aura. Primeiro, vamos ler as nossas credenciais do arquivo .env
usando dotenv
:
import os from dotenv import load_dotenv load_dotenv() NEO4J_USERNAME = os.getenv("NEO4J_USERNAME") NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD") NEO4J_URI = os.getenv("NEO4J_URI")
Agora, vamos estabelecer uma conexão:
from neo4j import GraphDatabase uri = NEO4J_URI username = NEO4J_USERNAME password = NEO4J_PASSWORD driver = GraphDatabase.driver(uri, auth=(username, password)) try: driver.verify_connectivity() print("Connection successful!") except Exception as e: print(f"Failed to connect to Neo4j: {e}")
Resultado:
Connection successful!
Aqui está a explicação do código:
- Nós importamos
GraphDatabase
doneo4j
para interagir com o Neo4j. - Usamos as variáveis de ambiente préviamente carregadas para configurar nossa conexão (
uri
,username
,password
). - Criamos um objeto de driver usando
GraphDatabase.driver()
, estabelecendo uma conexão com nosso banco de dados Neo4j. - Ao
está
dentro de umwith
bloco, nós usamos averify_connectivity()
função para verificar se uma conexão está estabelecida. Por padrão,verify_connectivity()
retorna nada se a conexão for bem-sucedida.
Assim que o tutorial terminar, chame driver.close()
para encerrar a conexão e liberar recursos. Objetos de driver são caros de criar, por isso você só deveria criar um único objeto para sua aplicação.
Essenciais da Linguagem de Consulta Cypher
A sintaxe do Cypher é projetada para ser intuitiva e visualmente representativa das estruturas de grafos. Ela depende da seguinte sintaxe do tipo ASCII-art:
(nodes)-[:CONNECT_TO]->(other_nodes)
Vamos desmontar os componentes chave deste padrão geral de consulta:
1. Nós
Em uma consulta Cypher, uma palavra-chave entre parêntesis representa o nome de um nó. Por exemplo, (Jogador
) corresponde a todos os nós de Jogador. Na maioria das vezes, os nomes de nós são referenciados com aliases para tornar as consultas mais legíveis, fáceis de escrever e compactas. Você pode adicionar um alias the nome de um nó colocando um ponto e vírgula antes dele: (m:Partida
).
Dentro dos parêntesis, você pode especificar uma ou mais propriedades de nós para um encontro preciso usando uma sintaxe semelhante a um dicionário. Por exemplo:
// Todos os nós de torneio que são a Copa do Mundo FIFA (t:Tournament {name: "FIFA World Cup"})
As propriedades do nó são escritas conforme estão, enquanto o valor que você quer que elas tenham deve ser uma string.
2. Relações
As relações conectam os nós uns aos outros e são envolvidas com colchetes e setas:
// Encontra nós que SÃO_PARTE de algum torneio (m:Match)-[PART_OF]->(t:Tournament)
Você pode adicionar apelidos e propriedades às relações também:
// Encontram que o Brasil participou de uma disputa de pênaltis e foi o primeiro a chutar (p:Player) - [r:SCORED_FOR {minute: 90}] -> (t:Team)
As relações são envolvidas com setas -[RELACAO]->
. Novamente, você pode incluir propriedades aliases dentro dos parêntesis. Por exemplo:
// Todos os jogadores que marcaram um gol contra (p:Player)-[r:SCORED_IN {own_goal: True}]->(m:Match)
3. Clausulas
Assim como COUNT(*) FROM table_name
não retornaria nada sem uma cláusula SELECT
em SQL, (nó) - [RELACIONAMENTO] -> (nó)
não traria nenhum resultado. Portanto, assim como em SQL, o Cypher tem diferentes cláusulas para estruturar sua lógica de consulta como em SQL:
MATCH
: correspondência de padrão no grafoWHERE
: filtrando os resultadosRETURN
: especificando o que incluir no conjunto de resultadosCREATE
: criando novos nós ou relaçõesMERGE
: Criando nós ou relações únicosDELETE
: Removendo nós, relações ou propriedadesSET
: Atualizando rótulos e propriedades
Aqui está uma consulta de exemplo que demonstra estes conceitos:
MATCH (p:Player)-[s:SCORED_IN]->(m:Match)-[PART_OF]->(t:Tournament) WHERE t.name = "FIFA World Cup" AND s.minute > 80 AND s.own_goal = True RETURN p.name AS Player, m.date AS MatchDate, s.minute AS GoalMinute ORDER BY s.minute DESC LIMIT 5
Essa consulta encontra todos os jogadores que marcaram gols contra na meta depois do 80º minuto em partidas da Copa do Mundo. Ela se parece quase com SQL, mas a equivalente em SQL envolve pelo menos um JOIN.
Uso do Driver Python do Neo4j para Analisar um Banco de Dados de Grafos
Executando consultas com execute_query
O driver Python do Neo4j é a biblioteca oficial que interage com uma instância do Neo4j através de aplicações em Python. Ele verifica e comunica consultas Cypher escritas em strings de Python simples com um servidor Neo4j e recupera os resultados em um formato unificado.
Começa tudo criando um objeto driver com a classe GraphDatabase
. A partir daí, podemos começar a enviar consultas usando o método execute_query
.
Para nossa primeira consulta, vamos fazer uma pergunta interessante: Qual time ganhou o maior número de partidas da Copa do Mundo?
# Retorna o time que ganhou o maior número de partidas da Copa do Mundo query = """ MATCH (t:Team)-[:WON]->(m:Match)-[:PART_OF]->(:Tournament {name: "FIFA World Cup"}) RETURN t.name AS Team, COUNT(m) AS MatchesWon ORDER BY MatchesWon DESC LIMIT 1 """ records, summary, keys = driver.execute_query(query, database_="neo4j")
Primeiro, vamos desmontar a consulta:
- O
MATCH
fechado define o padrão que queremos: Time -> Vitórias -> Partida -> parte de -> Torneio RETURN
é o equivalente ao comandoSELECT
do SQL, onde podemos retornar as propriedades dos nós e relacionamentos retornados. Nesta cláusula, você pode também usar qualquer função de agregação suportada no Cypher. Acima, estamos usandoCOUNT
.ORDER BY
cláusula funciona da mesma forma que a cláusulaORDER BY
do SQL.LIMIT
é usado para controlar o tamanho dos registros retornados.
Após definirmos a consulta como uma string de várias linhas, passamos-a para o método execute_query()
do objeto driver e especificamos o nome do banco de dados (o padrão é neo4j
). A saída sempre contém três objetos:
records
: Uma lista de objetos Record, cada um representando uma linha no conjunto de resultados. Cada Record é um objeto类似namedtuple onde você pode acessar os campos por nome ou índice.resumo
: Um objeto ResultSummary contendo metadados sobre a execução da consulta, como estatísticas de consulta e informações de tempo.chaves
: Uma lista de strings representando os nomes das colunas no conjunto de resultados.
Tocaremos brevemente sobre o resumo
objeto depois porque estamos principalmente interessados em registros
, que contêm objetos de Registro
. Podemos recuperar suas informações chamando seu método data()
:
for record in records: print(record.data())
Resultado:
{'Team': 'Brazil', 'MatchesWon': 76}
O resultado mostra corretamente que o Brasil ganhou o maior número de partidas na Copa do Mundo.
Passando parâmetros de consulta
A nossa última consulta não é reutilizável, pois só encontra a equipe mais bem sucedida na história da Copa do Mundo. E se quisermos encontrar a equipe mais bem sucedida na história da Euro?
É aí que entram os parâmetros de consulta:
query = """ MATCH (t:Team)-[:WON]->(m:Match)-[:PART_OF]->(:Tournament {name: $tournament}) RETURN t.name AS Team, COUNT(m) AS MatchesWon ORDER BY MatchesWon DESC LIMIT $limit """
Nesta versão da consulta, nós Introduzimos dois parâmetros usando o símbolo $
:
tournament
limit
Para passar valores para os parâmetros da consulta, nós usamos argumentos de palavra-chave dentro execute_query
:
records, summary, keys = driver.execute_query( query, database_="neo4j", tournament="UEFA Euro", limit=3, ) for record in records: print(record.data())
Resultado:
{'Team': 'Germany', 'MatchesWon': 30} {'Team': 'Spain', 'MatchesWon': 28} {'Team': 'Netherlands', 'MatchesWon': 23}
É sempre recomendado usar parâmetros de consulta sempre que você estiver pensando em incorporar valores mudáveis em sua consulta. Esta melhor prática protege suas consultas de injeções de Cypher e permite que o Neo4j as cache.
Escrevendo em bancos de dados com cláusulas CREATE e MERGE
Escrever nova informação em uma base de dados existente é feito de forma similar com execute_query mas usando uma CREATE
cláusula na consulta. Por exemplo, vamos criar uma função que vai adicionar um novo tipo de nó – treinadores de equipes:
def add_new_coach(driver, coach_name, team_name, start_date, end_date): query = """ MATCH (t:Team {name: $team_name}) CREATE (c:Coach {name: $coach_name}) CREATE (c)-[r:COACHES]->(t) SET r.start_date = $start_date SET r.end_date = $end_date """ result = driver.execute_query( query, database_="neo4j", coach_name=coach_name, team_name=team_name, start_date=start_date, end_date=end_date ) summary = result.summary print(f"Added new coach: {coach_name} for existing team {team_name} starting from {start_date}") print(f"Nodes created: {summary.counters.nodes_created}") print(f"Relationships created: {summary.counters.relationships_created}")
A função add_new_coach
pega cinco parâmetros:
- driver: O objeto de driver Neo4j usado para conectar à base de dados.
coach_name
: O nome do novo treinador a ser adicionado.team_name
: O nome da equipe com a qual o treinador será associado.start_date
: A data em que o treinador começa a treinar o time.end_date
: A data em que o período de trabalho do treinador com o time termina.
A consulta Cypher na função faz o seguinte:
- Compara um nó de Time existente com o nome de time dado.
- Cria um novo nó de Treinador com o nome de treinador fornecido.
- Cria uma relação COACHES entre os nós Treinador e Equipe.
- Define as propriedades
start_date
eend_date
na relaçãoCOACHES
.
A consulta é executada usando o método execute_query
, que recebe a string de consulta e um dicionário de parâmetros.
Após a execução, a função imprime:
- Uma mensagem de confirmação com os nomes do treinador e da equipe e a data de início.
- O número de nós criados (deve ser 1 para o novo nó de Treinador).
- O número de relações criadas (deve ser 1 para a nova relação
COACHES
).
Vamos colocá-lo em prática para um dos treinadores de futebol internacionais mais bem sucedidos da história, Lionel Scaloni, que ganhou três torneios internacionais consecutivos de grande importância (Copa do Mundo e duas Copas América):
from neo4j.time import DateTime add_new_coach( driver=driver, coach_name="Lionel Scaloni", team_name="Argentina", start_date=DateTime(2018, 6, 1), end_date=None )
Output: Added new coach: Lionel Scaloni for existing team Argentina starting from 2018-06-01T00:00:00.000000000 Nodes created: 1 Relationships created: 1
No trecho acima, estamos usando a classe DateTime
do módulo neo4j.time
para passar uma data corretamente para nossa consulta Cypher. O módulo contém outros tipos de dados temporais úteis que você pode querer conferir.
Além de CREATE
, também existe a MERGE
cláusula para criar novos nós e relações. Sua principal diferença é:
CREATE
sempre cria novos nós/relações, potencialmente levando a duplicações.MERGE
cria apenas nós/relacionamentos se eles ainda não existirem.
Por exemplo, no nosso script de ingestão de dados, como você verá mais tarde:
- Nós usamos
MERGE
para times e jogadores para evitar duplicatas. - Usamos
CREATE
paraSCORED_FOR
eSCORED_IN
relações, porque um jogador pode marcar várias vezes em um único jogo. - Esses não são verdadeiros duplicatas, já que eles têm propriedades diferentes (por exemplo, minuto de gol).
Esta abordagem garante a integridade dos dados enquanto permite múltiplas relações semelhantes, mas distintas.
Executando suas próprias transações
Quando você executa execute_query
, o driver cria uma transação internamente. Uma transação é uma unidade de trabalho que é executada por completo ou revertida em caso de falha. Isso significa que, quando você está criando milhares de nós ou relações em uma única transação (é possível) e ocorre algum erro no meio, toda a transação falha sem escrever quaisquer novos dados no grafo.
Para ter um controle mais refinado sobre cada transação, você precisa criar objetos de sessão. Por exemplo, vamos criar uma função para encontrar as maiores pontuações de gol de um dado torneio usando um objeto de sessão:
def top_goal_scorers(tx, tournament, limit): query = """ MATCH (p:Player)-[s:SCORED_IN]->(m:Match)-[PART_OF]->(t:Tournament) WHERE t.name = $tournament RETURN p.name AS Player, COUNT(s) AS Goals ORDER BY Goals DESC LIMIT $limit """ result = tx.run(query, tournament=tournament, limit=limit) return [record.data() for record in result]
Primeiro, criamostop_goal_scorers
função que aceita três parâmetros, sendo o mais importante otxobjeto de transação que será obtido usando um objeto de sessão.
with driver.session() as session: result = session.execute_read(top_goal_scorers, "FIFA World Cup", 5) for record in result: print(record)
Saída:
{'Player': 'Miroslav Klose', 'Goals': 16} {'Player': 'Ronaldo', 'Goals': 15} {'Player': 'Gerd Müller', 'Goals': 14} {'Player': 'Just Fontaine', 'Goals': 13} {'Player': 'Lionel Messi', 'Goals': 13}
Em seguida, dentro de um gerenciador de contexto criado com asession()
método, nós usamosexecute_read()
, passando atop_goal_scorers()
função, juntamente com quaisquer parâmetros que a consulta necessita.
A saída de execute_read
é uma lista de objetos Record que corretamente mostra os 5 maiores artilheiros da história da Copa do Mundo, incluindo nomes como Miroslav Klose, Ronaldo Nazário e Lionel Messi.
O counterparte de execute_read()
para ingestão de dados é execute_write()
.
Com isso dito, vamos agora olhar para o script de ingestão que usamos anteriormente para entender como a ingestão de dados funciona com o driver Python do Neo4j.
Ingestão de Dados Usando o Driver Python do Neo4j
O ingest_football_data.py arquivo começa com declarações de importação e carregamento dos arquivos CSV necessários:
import pandas as pd import neo4j from dotenv import load_dotenv import os from tqdm import tqdm import logging # Caminhos dos arquivos CSV results_csv_path = "https://raw.githubusercontent.com/martj42/international_results/refs/heads/master/results.csv" goalscorers_csv_path = "https://raw.githubusercontent.com/martj42/international_results/refs/heads/master/goalscorers.csv" shootouts_csv_path = "https://raw.githubusercontent.com/martj42/international_results/refs/heads/master/shootouts.csv" # Configurar registro de logs logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) logger.info("Loading data...") # Carregar dados results_df = pd.read_csv(results_csv_path, parse_dates=["date"]) goalscorers_df = pd.read_csv(goalscorers_csv_path, parse_dates=["date"]) shootouts_df = pd.read_csv(shootouts_csv_path, parse_dates=["date"])
Este bloco de código também configura um registrador. As próximas linhas de código leem minhas credenciais Neo4j usando dotenv
e cria um objeto Driver:
uri = os.getenv("NEO4J_URI") user = os.getenv("NEO4J_USERNAME") password = os.getenv("NEO4J_PASSWORD") try: driver = neo4j.GraphDatabase.driver(uri, auth=(user, password)) print("Connected to Neo4j instance successfully!") except Exception as e: print(f"Failed to connect to Neo4j: {e}") BATCH_SIZE = 5000
Como há mais de 48 mil correspondências em nosso banco de dados, definimos um parâmetro BATCH_SIZE
para ingestão de dados em amostras menores.
Então, definimos uma função chamada create_indexes
que aceita um objeto de sessão:
def create_indexes(session): indexes = [ "CREATE INDEX IF NOT EXISTS FOR (t:Team) ON (t.name)", "CREATE INDEX IF NOT EXISTS FOR (m:Match) ON (m.id)", "CREATE INDEX IF NOT EXISTS FOR (p:Player) ON (p.name)", "CREATE INDEX IF NOT EXISTS FOR (t:Tournament) ON (t.name)", "CREATE INDEX IF NOT EXISTS FOR (c:City) ON (c.name)", "CREATE INDEX IF NOT EXISTS FOR (c:Country) ON (c.name)", ] for index in indexes: session.run(index) print("Indexes created.")
Índices Cypher são estruturas de banco de dados que melhoram o desempenho de consulta no Neo4j. Eles aceleram o processo de encontrar nós ou relações com base em propriedades específicas. Nós precisamos deles para:
- Execução mais rápida de consultas
- Melhora no desempenho de leitura em grandes conjuntos de dados
- Correspondência de padrões eficiente
- Aplicando restrições de unicidade
- Melhora na escalaabilidade à medida que o banco de dados cresce
No nosso caso, índices nos nomes das equipes, IDs de jogos e nomes de jogadores ajudarão nossas consultas a serem executadas mais rapidamente ao buscar entidades específicas ou ao realizar junções entre diferentes tipos de nós. É uma boa prática criar tais índices para seus próprios bancos de dados.
A seguir, temos a função ingest_matches
. Ela é grande, então vamos quebrá-la bloco por bloco:
def ingest_matches(session, df): query = """ UNWIND $batch AS row MERGE (m:Match {id: row.id}) SET m.date = date(row.date), m.home_score = row.home_score, m.away_score = row.away_score, m.neutral = row.neutral MERGE (home:Team {name: row.home_team}) MERGE (away:Team {name: row.away_team}) MERGE (t:Tournament {name: row.tournament}) MERGE (c:City {name: row.city}) MERGE (country:Country {name: row.country}) MERGE (home)-[:PLAYED_HOME]->(m) MERGE (away)-[:PLAYED_AWAY]->(m) MERGE (m)-[:PART_OF]->(t) MERGE (m)-[:PLAYED_IN]->(c) MERGE (c)-[:LOCATED_IN]->(country) WITH m, home, away, row.home_score AS hs, row.away_score AS as FOREACH(_ IN CASE WHEN hs > as THEN [1] ELSE [] END | MERGE (home)-[:WON]->(m) MERGE (away)-[:LOST]->(m) ) FOREACH(_ IN CASE WHEN hs < as THEN [1] ELSE [] END | MERGE (away)-[:WON]->(m) MERGE (home)-[:LOST]->(m) ) FOREACH(_ IN CASE WHEN hs = as THEN [1] ELSE [] END | MERGE (home)-[:DREW]->(m) MERGE (away)-[:DREW]->(m) ) """ ...
A primeira coisa que você notará é oUNWIND
palavra-chave, que é usada para processar um lote de dados. Ela pega o$batch
parâmetro (que será as nossas linhas de DataFrame) e percorre cada linha, permitindo que criemos ou atualizem vários nós e relacionamentos em uma única transação. Esta abordagem é mais eficiente do que processar cada linha individualmente, especialmente para grandes conjuntos de dados.
O resto da consulta é familiar, pois utiliza várias cláusulas MERGE
. Logo, chegamos à cláusula WITH
, que utiliza construções FOREACH
com IN CASE
statements. Estas são usadas para criar relações condicionalmente com base no resultado da partida. Se o time de casa ganhar, cria-se uma relação ‘WON’ para o time de casa e uma relação ‘LOST’ para o time visitante e vice-versa. Em caso de empate, ambos os times obtêm uma relação ‘DREW’ com a partida.
O resto da função divide o DataFrame recebido em correspondências e constrói os dados que serão passados para a$batch
parâmetro da consulta:
def ingest_matches(session, df): query = """...""" for i in tqdm(range(0, len(df), BATCH_SIZE), desc="Ingesting matches"): batch = df.iloc[i : i + BATCH_SIZE] data = [] for _, row in batch.iterrows(): match_data = { "id": f"{row['date']}_{row['home_team']}_{row['away_team']}", "date": row["date"].strftime("%Y-%m-%d"), "home_score": int(row["home_score"]), "away_score": int(row["away_score"]), "neutral": bool(row["neutral"]), "home_team": row["home_team"], "away_team": row["away_team"], "tournament": row["tournament"], "city": row["city"], "country": row["country"], } data.append(match_data) session.run(query, batch=data)
ingest_goals
e ingest_shootouts
funções usam estruturas semelhantes. No entanto, ingest_goals
tem algumas rotinas de tratamento de erros e valores faltantes adicionais.
No final do script, temos a função main()
que executa todas nossas funções de ingestão com um objeto de sessão:
def main(): with driver.session() as session: create_indexes(session) ingest_matches(session, results_df) ingest_goals(session, goalscorers_df) ingest_shootouts(session, shootouts_df) print("Data ingestion completed!") driver.close() if __name__ == "__main__": main()
Conclusão e Próximas etapas
Cobrimos os aspectos chave de trabalhar com bases de dados de grafos Neo4j usando Python:
- Conceitos e estrutura de bases de dados de grafos
- Configurando Neo4j AuraDB
- Básicos do linguagem de consulta Cypher
- Usando o driver Python do Neo4j
- Ingestão de dados e otimização de consultas
Para avançar no seu percurso com Neo4j, explore esses recursos:
- Documentação do Neo4j
- Biblioteca de Ciência de Dados do Neo4j
- Manual do Neo4j Cypher
- Documentação do Driver Python para Neo4j
- Certificação de Carreira em Engenharia de Dados
- Introdução ao NoSQL
- Um tutorial completo de NoSQL usando MongoDB
Lembre-se, o poder dos bancos de dados de grafos reside na representação e consulta de relações complexas. Continue a experimentar com diferentes modelos de dados e a explorar recursos avançados do Cypher.