DevGuideTK
← Trilha Backend

Bancos de grafos

Quando os relacionamentos são o dado principal, como em redes sociais e detecção de fraude.

O código desta página foi executado nas cinco linguagens contra um Neo4j 5.26. As medições do nível Conceito usaram o mesmo Neo4j e um PostgreSQL 18.

Recomendação por amigos em comum, em cada linguagem

Cada versão monta um grafo pequeno e pede as recomendações da Ana: pessoas seguidas por quem a Ana segue, que ela ainda não segue, ordenadas pelo número de ligações em comum. A consulta é a mesma nas cinco, com parâmetros.

import neo4j from "neo4j-driver";

const driver = neo4j.driver(process.env.NEO4J_URL!,
  neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD!));

// Quem as pessoas que eu sigo seguem, e eu ainda não sigo, pelo número de ligações em comum
const RECOMENDAR = `
  MATCH (eu:Usuario {nome: $nome})-[:SEGUE]->(:Usuario)-[:SEGUE]->(sugestao:Usuario)
  WHERE sugestao <> eu AND NOT (eu)-[:SEGUE]->(sugestao)
  RETURN sugestao.nome AS nome, count(*) AS em_comum
  ORDER BY em_comum DESC, nome
  LIMIT $limite`;

async function recomendar(nome: string, limite = 5) {
  const { records } = await driver.executeQuery(RECOMENDAR, { nome, limite: neo4j.int(limite) });
  return records.map((r) => ({ nome: r.get("nome"), emComum: r.get("em_comum").toNumber() }));
}

// Um grafo pequeno para o exemplo
await driver.executeQuery("MATCH (u:Usuario) DETACH DELETE u");
await driver.executeQuery(`
  UNWIND $ligacoes AS l
  MERGE (a:Usuario {nome: l[0]}) MERGE (b:Usuario {nome: l[1]}) MERGE (a)-[:SEGUE]->(b)`,
  { ligacoes: [["ana", "bia"], ["ana", "caio"], ["bia", "davi"], ["bia", "eva"],
               ["bia", "caio"], ["caio", "davi"], ["caio", "fabio"]] });
console.log(await recomendar("ana"));
await driver.close();

O resultado, nas cinco linguagens

O grafo do exemplo:

ana  → bia, caio
bia  → davi, eva, caio
caio → davi, fabio
DriverRecomendações para a Ana
TypeScriptneo4j-driver 6.3davi (2), eva (1), fabio (1)
Pythonneo4j 6.4davi (2), eva (1), fabio (1)
Goneo4j-go-driver/v5 5.28davi (2), eva (1), fabio (1)
Javaneo4j-java-driver 5.28davi (2), eva (1), fabio (1)
C#Neo4j.Driver 6.5davi (2), eva (1), fabio (1)

O caio não aparece, embora a bia o siga: a Ana já o segue. O davi vem primeiro porque é seguido pela bia e pelo caio.

O que muda de uma linguagem para outra

Executar a consultaInteiros
TypeScriptdriver.executeQueryO Cypher usa inteiros de 64 bits: neo4j.int(5) na ida e .toNumber() na volta
Pythondriver.execute_query, com parâmetros nomeadosint comum
Goneo4j.ExecuteQuery com EagerResultTransformerint64, com conversão de tipo na leitura
Javadriver.executableQuery(...).withParameters(...).execute()asLong()
C#driver.ExecutableQuery(...).WithParameters(...).ExecuteAsync()As<long>()

O ponto mais traiçoeiro é o do JavaScript: os números do JavaScript não representam todos os inteiros de 64 bits do Neo4j, e o driver usa um tipo Integer próprio. Por isso o exemplo converte nos dois sentidos: neo4j.int(limite) ao enviar o LIMIT, que o Cypher exige inteiro, e .toNumber() ao ler em_comum. A documentação do driver descreve as opções, como disableLosslessIntegers, para receber números comuns.

Os cinco drivers fazem a mesma coisa por baixo: abrem conexões pelo protocolo Bolt, roteiam leituras e escritas num cluster e reaproveitam o plano das consultas parametrizadas.

A carga do grafo de teste

O grafo de 100 mil pessoas e 1 milhão de relações foi carregado a partir de arquivos CSV, em lotes, em 25 segundos:

CREATE CONSTRAINT pessoa_id IF NOT EXISTS FOR (p:Pessoa) REQUIRE p.id IS UNIQUE;

LOAD CSV WITH HEADERS FROM 'file:///segue.csv' AS l
CALL (l) {
  MATCH (a:Pessoa {id: toInteger(l.src)}), (b:Pessoa {id: toInteger(l.dst)})
  CREATE (a)-[:SEGUE]->(b)
} IN TRANSACTIONS OF 20000 ROWS;

A restrição de unicidade vem antes da carga: ela cria o índice que o MATCH usa para achar as duas pontas de cada relação. Sem ela, cada linha do arquivo varreria todos os nós.

E a consulta recursiva usada no PostgreSQL, com índices em segue (src, dst) e segue (dst):

WITH RECURSIVE alcance(id, d) AS (
  SELECT 1, 0
  UNION                                   -- remove repetidos a cada nível
  SELECT s.dst, a.d + 1
    FROM alcance a JOIN segue s ON s.src = a.id
   WHERE a.d < 5                          -- limite de profundidade
)
SELECT count(DISTINCT id) - 1 FROM alcance;

Lista de revisão

Escolha

  • As consultas principais são sobre caminhos, e não listagens e somas?
  • Uma tabela de arestas com WITH RECURSIVE não resolveria?
  • Está definido qual banco é a fonte da verdade, e como o grafo é sincronizado?

Modelo

  • Valores compartilhados que ligam entidades (aparelho, cartão, endereço) são nós?
  • Os tipos de aresta são específicos?
  • Há restrição de unicidade nas chaves de busca, e o código usa MERGE?
  • Há supernós, e as consultas evitam passar por eles?

Consultas

  • Todo caminho variável tem limite de profundidade?
  • As consultas sobre nós usam DISTINCT?
  • As consultas usam parâmetros, e não texto concatenado?
  • No JavaScript, os inteiros são convertidos na ida e na volta?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar consultas de grafo na minha aplicação

    Quero usar um banco de grafos na minha aplicação.
    
    Pergunte qual linguagem, qual banco de grafos e qual é a funcionalidade (recomendação, fraude, dependências).
    
    Vamos em etapas:
    1. Configurar o driver e as restrições de unicidade.
    2. Gravar nós e arestas com MERGE, em lotes.
    3. Escrever as consultas com parâmetros e limite de profundidade.
    4. Conferir o plano com PROFILE.
    5. Testes com um grafo pequeno, de resultado conhecido.
  • Grafo no PostgreSQL com recursão

    Quero guardar um grafo no PostgreSQL e consultá-lo com WITH RECURSIVE.
    
    Pergunte qual é o grafo (hierarquia, rede social, dependências), o volume e as consultas.
    
    Me ajude com:
    1. A tabela de arestas e os índices.
    2. A consulta de alcance com limite de profundidade, removendo repetidos.
    3. A detecção de ciclos.
    4. O menor caminho, e quando ele fica caro demais.
    5. Como medir com EXPLAIN ANALYZE e quando migrar para um banco de grafos.
  • Revisar consultas e modelo de grafo

    Vou colar abaixo o modelo de um grafo e as consultas (Cypher ou SQL recursivo) de uma aplicação. Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Os caminhos variáveis têm limite de profundidade?
    2. As consultas sobre nós usam DISTINCT?
    3. As consultas usam parâmetros?
    4. Há restrições de unicidade, e o código usa MERGE onde precisa?
    5. Valores compartilhados estão modelados como nós?
    6. Há supernós no caminho das consultas frequentes?
    
    Para cada problema, descreva o efeito em produção e mostre a correção.
    
    Modelo e consultas:
    [cole aqui]