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();import os
from neo4j import GraphDatabase
driver = GraphDatabase.driver(os.environ["NEO4J_URL"], auth=("neo4j", os.environ["NEO4J_PASSWORD"]))
# Quem as pessoas que eu sigo seguem, e eu ainda não sigo, pelo número de ligações em comum
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"""
def recomendar(nome: str, limite: int = 5) -> list[tuple[str, int]]:
registros, _, _ = driver.execute_query(RECOMENDAR, nome=nome, limite=limite)
return [(r["nome"], r["em_comum"]) for r in registros]
# Um grafo pequeno para o exemplo
driver.execute_query("MATCH (u:Usuario) DETACH DELETE u")
driver.execute_query(
"""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"]],
)
print(recomendar("ana"))
driver.close()package main
import (
"context"
"fmt"
"os"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
// Quem as pessoas que eu sigo seguem, e eu ainda não sigo, pelo número de ligações em comum
const recomendarCypher = `
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`
type Sugestao struct {
Nome string
EmComum int64
}
func recomendar(
ctx context.Context, d neo4j.DriverWithContext, nome string, limite int,
) ([]Sugestao, error) {
resultado, err := neo4j.ExecuteQuery(ctx, d, recomendarCypher,
map[string]any{"nome": nome, "limite": limite}, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
var sugestoes []Sugestao
for _, r := range resultado.Records {
n, _ := r.Get("nome")
c, _ := r.Get("em_comum")
sugestoes = append(sugestoes, Sugestao{n.(string), c.(int64)})
}
return sugestoes, nil
}
func main() {
ctx := context.Background()
d, err := neo4j.NewDriverWithContext(os.Getenv("NEO4J_URL"),
neo4j.BasicAuth("neo4j", os.Getenv("NEO4J_PASSWORD"), ""))
if err != nil {
panic(err)
}
defer d.Close(ctx)
// Um grafo pequeno para o exemplo
executar := func(cypher string, p map[string]any) {
if _, err := neo4j.ExecuteQuery(ctx, d, cypher, p, neo4j.EagerResultTransformer); err != nil {
panic(err)
}
}
executar("MATCH (u:Usuario) DETACH DELETE u", nil)
executar(`UNWIND $ligacoes AS l
MERGE (a:Usuario {nome: l[0]}) MERGE (b:Usuario {nome: l[1]}) MERGE (a)-[:SEGUE]->(b)`,
map[string]any{"ligacoes": [][]string{{"ana", "bia"}, {"ana", "caio"}, {"bia", "davi"},
{"bia", "eva"}, {"bia", "caio"}, {"caio", "davi"}, {"caio", "fabio"}}})
sugestoes, err := recomendar(ctx, d, "ana", 5)
fmt.Println(sugestoes, err)
}import java.util.List;
import java.util.Map;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
public class Recomenda {
record Sugestao(String nome, long emComum) {}
// Quem as pessoas que eu sigo seguem, e eu ainda não sigo, pelo número de ligações em comum
static final String 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""";
static List<Sugestao> recomendar(Driver driver, String nome, int limite) {
var resultado = driver.executableQuery(RECOMENDAR)
.withParameters(Map.of("nome", nome, "limite", limite))
.execute();
return resultado.records().stream()
.map(r -> new Sugestao(r.get("nome").asString(), r.get("em_comum").asLong()))
.toList();
}
public static void main(String[] args) {
try (var driver = GraphDatabase.driver(System.getenv("NEO4J_URL"),
AuthTokens.basic("neo4j", System.getenv("NEO4J_PASSWORD")))) {
// Um grafo pequeno para o exemplo
driver.executableQuery("MATCH (u:Usuario) DETACH DELETE u").execute();
driver.executableQuery("""
UNWIND $ligacoes AS l
MERGE (a:Usuario {nome: l[0]}) MERGE (b:Usuario {nome: l[1]})
MERGE (a)-[:SEGUE]->(b)""")
.withParameters(Map.of("ligacoes", List.of(
List.of("ana", "bia"), List.of("ana", "caio"), List.of("bia", "davi"),
List.of("bia", "eva"), List.of("bia", "caio"), List.of("caio", "davi"),
List.of("caio", "fabio"))))
.execute();
System.out.println(recomendar(driver, "ana", 5));
}
}
}using Neo4j.Driver;
await using var driver = GraphDatabase.Driver(Environment.GetEnvironmentVariable("NEO4J_URL"),
AuthTokens.Basic("neo4j", Environment.GetEnvironmentVariable("NEO4J_PASSWORD")));
// Quem as pessoas que eu sigo seguem, e eu ainda não sigo, pelo número de ligações em comum
const string 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 Task<List<(string Nome, long EmComum)>> RecomendarPara(string nome, int limite = 5)
{
var resultado = await driver.ExecutableQuery(Recomendar)
.WithParameters(new { nome, limite })
.ExecuteAsync();
return resultado.Result
.Select(r => (r["nome"].As<string>(), r["em_comum"].As<long>()))
.ToList();
}
// Um grafo pequeno para o exemplo
await driver.ExecutableQuery("MATCH (u:Usuario) DETACH DELETE u").ExecuteAsync();
await driver.ExecutableQuery("""
UNWIND $ligacoes AS l
MERGE (a:Usuario {nome: l[0]}) MERGE (b:Usuario {nome: l[1]}) MERGE (a)-[:SEGUE]->(b)
""")
.WithParameters(new
{
ligacoes = new[]
{
new[] { "ana", "bia" }, new[] { "ana", "caio" }, new[] { "bia", "davi" },
new[] { "bia", "eva" }, new[] { "bia", "caio" }, new[] { "caio", "davi" },
new[] { "caio", "fabio" },
},
})
.ExecuteAsync();
Console.WriteLine(string.Join(", ", await RecomendarPara("ana")));O resultado, nas cinco linguagens
O grafo do exemplo:
ana → bia, caio
bia → davi, eva, caio
caio → davi, fabio| Driver | Recomendações para a Ana | |
|---|---|---|
| TypeScript | neo4j-driver 6.3 | davi (2), eva (1), fabio (1) |
| Python | neo4j 6.4 | davi (2), eva (1), fabio (1) |
| Go | neo4j-go-driver/v5 5.28 | davi (2), eva (1), fabio (1) |
| Java | neo4j-java-driver 5.28 | davi (2), eva (1), fabio (1) |
| C# | Neo4j.Driver 6.5 | davi (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 consulta | Inteiros | |
|---|---|---|
| TypeScript | driver.executeQuery | O Cypher usa inteiros de 64 bits: neo4j.int(5) na ida e .toNumber() na volta |
| Python | driver.execute_query, com parâmetros nomeados | int comum |
| Go | neo4j.ExecuteQuery com EagerResultTransformer | int64, com conversão de tipo na leitura |
| Java | driver.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 RECURSIVEnã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]