
Graphdb de baixo consumo de memória com suporte a Bolt+tls, criptografia em repouso e vetores, projetado para casos de uso de grafos de réplica local.
Versão atual: v0.25.2 — todas as versões.
Em uma linha: o Slater serve grafos que não cabem em memória — centenas de milhões de nós e bilhões de arestas em poucas centenas de MB de RAM — via Bolt padrão, então qualquer driver neo4j funciona, com busca vetorial nativa em disco ao lado do grafo, e aceita gravações ao vivo e duráveis sem abrir mão disso. A memória residente é definida por um orçamento de cache que você escolhe, não pelo tamanho do grafo.
Atalhos
Um banco de dados de grafos armazena dados como coisas (nós) e as relações entre elas (arestas), com as relações como cidadãos de primeira classe. É isso que você quer quando suas perguntas são sobre conexões em vez de linhas — "quem está a três saltos desta conta?", "qual é a cadeia completa de dependências por trás deste build?", "quais contas compartilham um dispositivo, um endereço e um cartão?" — as consultas que viram um pântano de joins recursivos em SQL, mas que fluem naturalmente em um grafo.
A reclamação mais comum sobre bancos de dados de grafos é que eles não escalam além do que cabe na RAM. Muitos deles (por exemplo, neo4j, Memgraph, FalkorDB, etc.) mantêm o grafo inteiro residente: um grafo de 40 GB quer 40 GB de memória — por instância. Quer uma réplica por região, por locatário ou por pod? Multiplique a conta. E além de um certo tamanho eles simplesmente não carregam: por exemplo, o grafo Wikidata de 90 milhões de nós / 1,5 bilhão de arestas precisa de ~64–128 GiB residentes, então os mecanismos em memória não conseguem abri-lo de forma alguma.
O Slater é a refutação. Em vez de carregar o grafo na memória, ele o compila uma vez, offline: slater-build transforma seus dados em uma imagem imutável em disco, endereçada por conteúdo, e qualquer número de servidores Slater serve essa imagem via Bolt (então seus drivers neo4j existentes funcionam), paginando blocos sob demanda e mantendo apenas um orçamento fixo de cache residente. É assim que o mesmo grafo de 90M nós é servido a partir de algumas centenas de MB de RAM — o tamanho do grafo e a conta de memória são desacoplados. Um grafo de 4 GB e um grafo de 400 GB custam a mesma RAM para servir, então você distribui réplicas de leitura baratas e sem estado, deixando o armazenamento, não o heap, segurar o grafo.
Isso o torna um ajuste natural para grafos de conhecimento por trás de RAG, grafos de recomendação e identidade, grafos de dependência — qualquer coisa grande e conectada que você queira consultar de forma barata e frequente. A busca vetorial nativa em disco fica ao lado do grafo, então o mesmo mecanismo também é a camada de recuperação para embeddings.
Compilado uma vez não significa congelado, no entanto. Essa imagem é uma base, não um estado final: uma camada de gravação opcional fica acima dela, então um grafo ao vivo pode ser corrigido e estendido sem reconstruir nada.
O núcleo é imutável; o grafo não é. Ative a camada gravável (delta.enabled) e você grava via Bolt — corrija uma propriedade, adicione um nó, retire uma aresta — e a mudança é registrada de forma durável, sem reconstrução da imagem. O que mantém isso barato no lado da leitura é onde as gravações vivem.
As gravações se acumulam em uma camada log-structured-merge (LSM) sobre o núcleo imutável: um write-ahead log e uma tabela em memória, que transbordam para segmentos delta imutáveis, dobrados de volta em um núcleo novo por uma consolidação periódica. O que isso compra para você:
count(*), as marginais de rótulo e tipo de relacionamento — permanecem leituras de metadados mesmo com gravações pendentes: o delta mantém seus próprios contadores, então um count(*) sobre um núcleo de 91,6M nós com meio milhão de gravações pendentes ainda responde em dezenas de milissegundos sem tocar em um único bloco.SUCCESS somente após o fsync que cobre a gravação. Agrupe suas gravações e elas são baratas — um UNWIND de gravação confirma um fsync por lote, em vez de por linha.MERGE / MATCH … SET / DELETE (e CREATE / REMOVE, delete com detach, gravações de relacionamento) com chave na propriedade de identidade de um nó — ou as declarações equivalentes de modificação de dados ISO GQL ( / / / ), que descem para o mesmo caminho. Corrija, insira, faça upsert e retire, sobre nós e arestas, endereçados da maneira que seus dados já estão.Com a camada desativada — o padrão — o Slater serve o núcleo imutável puro e recusa gravações. Veja A camada gravável para o modelo completo.
Sobre o nome. Slater é nomeado em homenagem ao agente da CIA em Archer (um ótimo programa) que insiste em ser chamado por um único nome — "Apenas… Slater" — e um dos meus personagens favoritos nele. Veja a página da wiki do personagem.
MERGE / SET / DELETE por chave de negócio sobre nós e arestas, com commit em grupo e durável via fsync, dobradas de volta em um núcleo novo pela consolidação. As leituras não pagam por isso.current, e os servidores a captam. Cada bloco é verificado por checksum, então uma imagem copiada pela metade é recusada em vez de servida.Dois binários compõem o workspace:
| Binário | Função |
|---|---|
slater | O servidor Bolt online (o ENTRYPOINT do contêiner): serve leituras e, com delta.enabled, o caminho de gravação durável de gravador único. |
slater-build | O compilador offline: transforma um dump de Cypher primitivo em um diretório de geração imutável com hash de conteúdo. |
O Slater separa a construção em massa do serviço: slater-build faz o trabalho pesado
offline — ingerindo seus dados e compilando-os em uma geração imutável — então um grafo
frio nunca é montado no caminho quente do serviço. Dentro do servidor, a superfície de
leitura responde a uma ampla fatia de Cypher — correspondência de padrões, subconsultas
WITH/UNION/CALL {…}, 70+ funções escalares e de agregação, valores temporais e
geoespaciais, algoritmos de grafo (algo.*) e KNN vetorial nativo em disco
(db.idx.vector.queryNodes) — enquanto a sobreposição delta da camada gravável fica
abaixo dessa superfície e tem custo zero quando vazia, então as leituras nunca carregam
a maquinaria do lado de gravação. Você pode atualizar um grafo de duas maneiras: gravar
nele ao vivo via Bolt (veja A camada gravável), ou construir uma
nova geração offline e trocar atomicamente o ponteiro current, que o servidor em
execução capta via seu guarda de geração (veja
Guarda de geração).
O manual do usuário completo vive em docs/manual/ —
um guia recurso por recurso que explica, para cada capacidade, o que é, por que
existe e como usá-la, com exemplos práticos que você pode executar contra um
grafo de amostra incluído. Comece por lá para qualquer coisa além desta visão geral.
graphiti-slater é um adaptador
que permite ao Graphiti armazenar seu grafo de
conhecimento temporal no Slater, com um
docker-example/
executável — incluindo expô-lo ao Claude Code como um servidor MCP. Veja esse repositório
para saber como funciona e como executá-lo.
O Slater é projetado para ser executado como uma implantação Docker — essa é a forma
esperada de usá-lo. Imagens multi-arquitetura pré-construídas (linux/amd64 + linux/arm64)
são publicadas no Docker Hub em
hikarisystems/slater,
com as tags :latest e :vX.Y.Z em cada versão:```sh
docker pull hikarisystems/slater:latest
Um guia de uso, configuração e operações apenas com comandos Docker está disponível em
[`DOCKERHUB.md`](https://github.com/hikari-systems/slater/blob/main/DOCKERHUB.md) (e é espelhado na página de visão geral do Docker Hub) —
**comece por lá se você estiver implantando.** Em resumo:```sh
# Build a graph generation with the offline writer:
docker run --rm -v slater-data:/data -v "$PWD/dumps:/dumps:ro" \
--entrypoint /app/slater-build hikarisystems/slater:latest \
--input /dumps/people.cypher --graph people --data-dir /data
# Serve it over Bolt on 7687 (read-only unless `delta.enabled`):
docker run -d --name slater -p 7687:7687 \
-v slater-data:/data:ro -v "$PWD/acl.json:/config/acl.json:ro" \
hikarisystems/slater:latest
Para construir a imagem localmente (por exemplo, para desenvolvimento):```sh
docker compose build
docker compose up slater
build):docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data
A fase de construção instala `cmake`, `clang` e `libclang-dev` para o backend `aws-lc-rs` do rustls; `git` (já presente na imagem base) é necessário para a dependência git+tag do `hs-utils`, que `.cargo/config.toml` busca via CLI do git.
As seções abaixo cobrem o formato em disco, a configuração, as ACLs e um exemplo prático local (sem Docker).
## Como funciona```
slater-build slater (Bolt server)
dump.cypher ──────────▶ /data/<graph>/<uuid>/ ──────────▶ neo4j driver
(offline, atomic) MANIFEST.json, *.blk, (bolt / bolt+s)
range/*.isam, vector/*.{vamana,pq},
current → <uuid>
MANIFEST.json (tabelas de símbolos,
descritores de índice, um cabeçalho de criptografia opcional), arquivos de blocos
colunares (node_props.blk, node_labels.blk, edge_props.blk, topology.csr.blk,
vectors.f32.blk), índices de intervalo (range/<name>.isam), índices ANN acima do
limite (vector/<label>.<prop>.{vamana,pq}) e um ponteiro de texto current.--encrypt, cada
bloco é adicionalmente selado com XChaCha20-Poly1305 (AEAD em repouso).Com delta.enabled, a geração imutável torna-se o nível inferior totalmente
compactado (o "núcleo") de uma pequena árvore de mesclagem estruturada por logs,
e as gravações ativas ficam por cima dela:```
write (Bolt) read (Bolt)
│ │
▼ ▼
┌──────────────┐ flush ┌──────────────┐ ┌──────────────────────┐
│ WAL + active │ ───────▶ │ L0 delta │ │ a query pins one │
│ memtable │ │ segments │ │ (core, delta) view │
└──────────────┘ └──────┬───────┘ │ and reads the merge │
(fsync = ack) │ └──────────────────────┘
consolidation │ (folds core + delta → fresh core)
▼
┌─────────────┐
│ new core │ (atomic current swap)
└─────────────┘
* **Piso de durabilidade — o WAL.** Cada mutação é serializada atrás de um único
escritor por grafo, anexada a um log de write-ahead por grafo e submetida a `fsync`
antes de o `SUCCESS` do Bolt ser retornado — portanto, *confirmado ⇒ durável*, e uma
cauda rasgada é descartada na reprodução. Um `UNWIND` de escrita em lote anexa suas
linhas e confirma **um** `fsync` para o lote inteiro. O WAL é **apenas disco local**
(não é roteado pelo backend de armazenamento), o que torna um nó *escritor* com
estado: ele precisa de um volume local durável em `delta.walDir`. Réplicas de leitura
permanecem sem estado.
* **Memtable → L0 → consolidação.** As escritas acumulam-se numa memtable em RAM
(limitada por `delta.memtableBytes`); quando enche, descarrega para um segmento
delta L0 imutável. Uma **consolidação** dobra `{core + delta}` num core novo ao
serializar a visão mesclada de volta através de `slater-build` e trocar `current`
atomicamente — a mesma proteção de hash de conteúdo de qualquer geração publicada.
Acione-a manualmente com `CALL slater.consolidate()`, automaticamente em
`delta.deltaCorePercent` do tamanho do core (opcionalmente limitada a uma janela de
consolidação fora de pico `delta.consolidateWindow`), ou deixe o limitador
`delta.deltaHardBytes` conter o crescimento descontrolado.
* **A sobreposição fica abaixo da superfície de leitura.** O executor lê através de
uma `ReadView` que é ou o core puro (delta sempre vazio) ou uma visão mesclada
`(core, delta)`; o motor é monomorfizado sobre ela, então um delta vazio compila
para um único ramo previsível e o caminho somente-leitura é byte-idêntico.
Contadores de grafo inteiro (`count(*)`, marginais de label/reltype) são servidos
dos próprios contadores ativos do delta, então permanecem leituras de metadados
mesmo com escritas pendentes.
* **Uma consulta vê um snapshot estável.** Ela fixa uma tupla `(core, delta)` durante
toda a sua vida. Não há transações multi-instrução nem rollback — uma escrita é uma
correção durável, endereçada por chave de negócio, não uma transação OLTP.
A gramática exata de escrita e os parâmetros estão na tabela [Configuration](#environment--configuration)
(`delta.*`) e no [Worked example](#worked-example) abaixo.
### Índices de intervalo (ISAM)
Um índice de intervalo (`range/<name>.isam`, um por `(label, property)` indexado)
permite que um `MATCH (n:Label {prop: v})` ou `WHERE n.prop <op> v` resolva os ids
dos nós correspondentes **sem varrer o label**. É uma estrutura
**[ISAM](https://en.wikipedia.org/wiki/ISAM)** (Indexed Sequential Access Method) —
o clássico índice *estático, ordenado e em blocos*, que é exatamente a forma certa
para uma geração imutável: não há inserções para rebalancear, então a simplicidade
do ISAM compra o que a maquinaria de mutação de uma B-tree apenas complicaria.
* As entradas `(value, entity_id)` são ordenadas por valor e empacotadas nos mesmos
blocos de 256 KiB comprimidos com zstd que todo o resto.
* Um pequeno **nível superior residente** guarda a primeira chave de cada bloco (um
índice esparso). Uma busca faz uma pesquisa binária nesse nível superior em memória
para encontrar o *único* bloco em que uma chave pode estar, lê + descomprime esse
bloco e o varre — portanto, uma busca por igualdade é **uma leitura de bloco**, e
uma varredura de intervalo percorre a sequência contígua de blocos que abrange.
(É por isso que uma busca indexada por `meshUi` leva dígitos únicos de milissegundos
enquanto a mesma correspondência numa propriedade não indexada varre o label inteiro.)
* O planejador a escolhe via `NodeScan::RangeEq` / `RangeRange`; um predicado não
indexado recai numa varredura de label ou numa varredura completa, com o executor
re-verificando cada predicado de qualquer forma.
### Busca vetorial (Vamana + PQ) — cosseno, L2 e produto escalar, leitura *e* escrita
KNN vetorial (`db.idx.vector.queryNodes`) roda sobre índices **cosseno, L2 ou
produto escalar (MIPS)**. O índice base é construído offline com dois caminhos de
execução, escolhidos por índice pelo `--ann-threshold` (padrão 50 000 vetores):
* **Abaixo do limite — força bruta.** Os vetores `f32` completos vivem em
`vectors.f32.blk`; uma consulta varre o grupo do índice e calcula a distância exata
na métrica do índice. Simples e exato; adequado quando o conjunto de vetores é pequeno.
* **No limite ou acima — Vamana + PQ**, o caminho ANN nativo de disco que mantém a
memória residente limitada independentemente de quantos vetores existam:
* **[Vamana](https://arxiv.org/pdf/2401.11324)** é o índice de grafo da linha de
trabalho DiskANN: um único grafo de proximidade cujas arestas são podadas (o
grau de saída `--vamana-r` e o fator de aresta longa `--vamana-alpha`) para que
uma *busca gulosa por feixe* — comece no medoide, salte repetidamente em direção
à consulta, mantendo uma lista de candidatos de largura `vectorQuery.beamWidth` —
alcance os vizinhos verdadeiros de um nó em poucos saltos, ou seja, **poucas
leituras aleatórias de bloco por consulta**. Os blocos do grafo
(`vector/<label>.<prop>.vamana`) são paginados através do cache de vetores, não
mantidos inteiros.
* **[Quantização de produto (PQ)](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)**
comprime cada vetor num código curto (`--pq-subspaces` × `--pq-bits`): as
dimensões são divididas em subespaços, cada um agrupado independentemente por
k-means, e o vetor é armazenado como a tupla de ids dos centroides mais próximos.
Esses códigos (`vector/<label>.<prop>.pq`) são pequenos o suficiente para
permanecerem **residentes**, então a busca por feixe pontua candidatos a partir
da RAM e apenas os poucos vetores completos escolhidos são lidos do disco. Esse
conjunto PQ residente é o que o pool `cache.vectorCacheBytes` fixa.
**Embeddings graváveis — a escada de escrita vetorial (estilo [FreshDiskANN](https://arxiv.org/abs/2105.09613)).**
Um embedding indexado é um valor gravável de primeira classe. `SET n.embedding = vecf32([…])` (e `REMOVE`) cai no
delta de escrita e fica **imediatamente visível para KNN com classificação exata**, depois sobrevive a um
descarregamento de segmento, a uma mesclagem e a uma consolidação. Uma consulta mescla até três níveis — o índice
base selado, um índice por segmento selado e um **índice RW** em memória (um Vamana mutável ativo
sobre o delta de escrita) — então a latência permanece estável à medida que as escritas se acumulam, em vez de
crescer com a contagem de escritas pendentes. Uma exclusão deixa um *buraco*: o nó deixa de ser retornado,
mas permanece como ponto de navegação até uma **consolidação de exclusão** em segundo plano o remover
do grafo, então as exclusões deixam de custar IO de consulta. E como o grafo em disco
endereça seus vizinhos por posição de layout em vez de id de nó, `CALL slater.consolidate()`
carrega o Vamana **por referência** — hard-linked, byte-idêntico — e reescreve apenas uma
pequena coluna de ids, dobrando as escritas vetoriais no base **sem** a reconstrução
de grafo O(N·R·L). Números medidos, com ressalvas, estão no [relatório de desempenho](https://github.com/hikari-systems/slater/blob/main/docs/PERF-REPORT.md).
## Backends de armazenamento (filesystem / S3 / GCS)
Cada arquivo de geração é aberto através de uma abstração **`ObjectStore`** em vez
de `std::fs` diretamente, então o *mesmo* formato de bytes em disco — blocos, índices,
manifesto, ponteiro `current` — é servido inalterado de qualquer backend; apenas *de
onde os bytes vêm* difere, nunca os leitores, o motor de consulta ou as
verificações de integridade. O caminho quente são leituras posicionais (`read_exact_at`),
que mapeiam para um `pread` num arquivo local e uma solicitação de intervalo de bytes
HTTP num object store — o Slater nunca usa mmap, então o modelo explícito de leitura
limitada é idêntico em todos os lugares.
**Três backends de primeira classe**, selecionados por `dataBackend.kind`. O filesystem é
o padrão simples; **Amazon S3 e Google Cloud Storage são backends de object store
iguais e totalmente suportados** — a imagem publicada inclui ambos compilados,
então cada um é apenas configuração, e uma geração construída uma vez pode ser servida
de qualquer um deles (até migrada `fs` → S3 → GCS) sem reconstrução.
| `dataBackend.kind` | Leitura posicional | Integridade na abertura | Credenciais |
| --- | --- | --- | --- |
| `fs` *(padrão)* | `pread` | re-hash BLAKE3 completo de cada arquivo | — |
| `s3` | GET HTTP `Range` | **SHA-256** do servidor via `HEAD` (→ re-hash BLAKE3 do corpo se ausente) | chaves de configuração, cadeia AWS ou função IAM |
| `gcs` | leitura de intervalo HTTP | **CRC32C** do servidor via `get_object` (→ re-hash BLAKE3 do corpo se ausente) | ADC / Workload Identity ou JSON de conta de serviço |
Ambos os object stores verificam a integridade a partir do **checksum que o store já
computa e mantém**, buscado como metadados do objeto: `slater-build` envia o
checksum no upload (o store valida os bytes contra ele e o armazena), e
o servidor o lê de volta na abertura e o compara ao manifesto — uma solicitação de
metadados por arquivo, sem download do corpo. É de grau de conteúdo e idêntico em espírito
entre S3 (SHA-256) e GCS (CRC32C). Quando um objeto não carrega **nenhum** checksum
armazenado no servidor (copiado fora de banda, ou enviado com um padrão diferente), o servidor
**re-hasheia o corpo do objeto contra o BLAKE3 do manifesto** em vez de confiar no seu
comprimento de bytes — uma verificação de integridade solicitada nunca é silenciosamente
rebaixada para uma comparação de tamanho. Gerações publicadas pelo Slater sempre carregam o checksum, então permanecem no
caminho barato de metadados.
O que esta coluna verifica, em todos os backends, é que os arquivos **correspondem ao manifesto**.
Se o próprio manifesto pode ser confiável é uma questão separada, e é a
chave mestra que a responde: com uma chave configurada, o manifesto carrega um MAC com chave
que o servidor verifica antes de confiar em qualquer campo (incluindo esses hashes), então um
manifesto reescrito para descrever arquivos adulterados é recusado; sem chave, a comparação é sem chave em todo o processo, e
alguém que possa escrever no diretório de dados pode reescrever um arquivo e o manifesto
juntos. Veja
[O que integridade significa em cada configuração](https://github.com/hikari-systems/slater/blob/main/THREAT_MODEL.md#what-integrity-means-in-each-configuration).
A verificação em si pode ser desativada com `dataBackend.verifyIntegrity: false`, o que
a troca por uma abertura mais rápida.
### Filesystem (`fs`)
O padrão, enraizado em `dataBackend.fs.dir`. A escolha certa para a maioria das
implantações: uma geração num SSD local (ou um mount NFS/EBS) servida somente-leitura.
A integridade é um re-hash BLAKE3 completo de cada arquivo na abertura.
### Amazon S3 (`s3`)
Um bucket S3 ou compatível com S3 (AWS, MinIO, localstack). As credenciais vêm **primeiro**
da configuração (`dataBackend.s3.awsAccessKey` / `awsSecretKey`, além de
`awsSessionToken` para credenciais STS temporárias) e recaem na cadeia padrão da AWS
(`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env, perfil compartilhado ou
função instance/IRSA) quando deixadas vazias.```sh
# serve from S3 (env-var form; see the config table for every key)
dataBackend__kind=s3
dataBackend__s3__bucket=slater
dataBackend__s3__region=eu-west-2
dataBackend__s3__awsAccessKey=… # omit to use the AWS chain / instance role
dataBackend__s3__awsSecretKey=…
# S3-compatible (e.g. MinIO): also set
dataBackend__s3__endpoint=http://minio:9000
dataBackend__s3__pathStyle=true # required by most S3-compatible servers
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
--publish-s3-bucket slater --publish-s3-region eu-west-2 --publish-s3-prefix prod
# MinIO: add --publish-s3-endpoint http://localhost:9000 --publish-s3-path-style
gcs)Um bucket GCS, acessado pela API JSON. A autorização é nativa do GCP: por padrão,
ela resolve as Application Default Credentials — GKE Workload Identity, o
servidor de metadados do GCE ou uma chave gcloud / GOOGLE_APPLICATION_CREDENTIALS. Defina
dataBackend.gcs.credentialsPath (um arquivo de chave JSON de conta de serviço) ou
credentialsJson inline para uma chave explícita. dataBackend.gcs.endpoint aponta para um
emulador fake-gcs-server, e dataBackend.gcs.anonymous=true habilita
acesso não autenticado apenas para esse emulador — nunca contra o GCS real.```sh
dataBackend__kind=gcs dataBackend__gcs__bucket=slater dataBackend__gcs__prefix=prod dataBackend__gcs__credentialsPath=/secrets/sa.json # omit for ADC / Workload Identity
```sh
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
--publish-gcs-bucket slater --publish-gcs-prefix prod
# explicit key: add --publish-gcs-credentials /secrets/sa.json
Em todos os casos, o slater-build grava a geração finalizada em --data-dir primeiro
(sua área de staging local) e adicionalmente a envia para o bucket; o ponteiro
current remoto é gravado por último, então um nó de serviço nunca vê uma geração
meio-publicada.
Recorra a s3 ou gcs quando quiser gerações em armazenamento de objetos durável e
central em vez de no disco de um nó — tipicamente: publicar uma vez e distribuir para
muitas réplicas de servidor stateless e sem disco que leem o mesmo bucket; desacoplar
o host de build dos hosts de serviço; ou contar com a durabilidade/versionamento/ciclo
de vida do armazenamento em vez de gerenciar volumes. A contrapartida é a
latência: um bloco frio é uma ida-e-volta de rede (~10–50 ms) em vez de uma leitura
local (~0,1 ms). O Slater esconde a maior parte disso com o cache de blocos em memória,
leitura antecipada concorrente e o cache de disco opcional abaixo. Se suas gerações
já estão em armazenamento local rápido e você não precisa do modelo de bucket central,
fs é mais simples e mais rápido.
O BlockCache em memória é deliberadamente pequeno (RSS limitado é a garantia
principal), então em um conjunto de trabalho maior que a RAM os mesmos blocos seriam
buscados novamente no object store a cada spill. Uma segunda camada de cache opcional
em SSD local resolve isso: um bloco despejado da RAM é servido do disco local
(~0,1 ms) em vez de um novo GET de objeto, sobrevivendo ao despejo da memória e
reduzindo a contagem/custo de requisições ao object store — aproximando um nó apoiado
em object store do desempenho de um filesystem local quando aquecido. É opt-in
tanto para s3 quanto para gcs, habilitado definindo dataBackend.<s3|gcs>.diskCacheBytes > 0
e um diskCacheDir gravável.
--encrypt) ainda selados com AEAD — abaixo da descriptografia/
descompressão. A camada de cache nunca detém a chave de criptografia e nunca
re-criptografa, então o status em repouso é preservado de graça: uma geração
criptografada chega ao disco ainda selada.diskCacheDir deve apontar para um volume real e gravável — nunca tmpfs (tmpfs
é RAM e anularia a garantia de RSS limitado). O índice em memória que o rastreia
custa um pouco de RAM (~dezenas de bytes por bloco em cache), o que conta contra
seu teto de RSS — dimensione o diretório ≫ o cache de blocos em memória.blockCacheBytes / 8 (com piso em
diskCacheBytes) — 8 MiB no padrão — e descarta em vez de crescer, então uma
varredura fria não pode inflá-la; um bloco descartado simplesmente é buscado de
novo no próximo miss. Ela não precisa de configuração: escala com blockCacheBytes,
então a camada de disco não adiciona nenhum número novo ao orçamento de RSS além
de seu índice.Uma réplica de leitura roda com um filesystem raiz somente leitura e um usuário
não-root (appuser:1000) — tudo o que ela precisa é montado somente leitura. Um
writer (delta.enabled) adicionalmente precisa de um volume durável e gravável
para seu WAL.
A configuração é carregada pelo loader em camadas padrão da casa: o config.json
embutido, depois /sandbox/config.json deep-merged sobre ele, depois overrides de
ambiente KEY__sub (duplo underscore para aninhamento; as chaves correspondem à
configuração camelCase).
Cada botão de configuração — sua chave camelCase, o override de ambiente KEY__sub,
seu padrão e o que faz — está tabulado na Referência de configuração. Os botões mais ajustados são os orçamentos de cache (cache.*), os guards de consulta (query.*), os limites de conexão (server.*), o backend de armazenamento (dataBackend.*) e a camada gravável (delta.*).
Memória residente rastreia
blockCacheBytes + vectorCacheBytes + resultCacheBytes dentro de overhead limitado
por entrada e do alocador — cada pool pesa seu próprio conteúdo (strings e
containers pela capacidade alocada) e despeja para ficar sob o orçamento, mas a
contabilidade por entrada e o arredondamento de classe de tamanho do alocador ficam
em cima do número que você define — mais um pequeno overhead fixo (e até
degreeColumnBytes para a coluna de grau lazy, uma vez que o caminho rápido de
soma de graus count(endpoint) é exercitado). É independente do tamanho do grafo
— essa é a garantia principal, exercitada pelo teste de integração
rss_stays_bounded_under_sustained_knn_load, que mantém o crescimento de RSS
pico-vs-aquecido bem dentro dos orçamentos somados. Buffers por conexão vivem fora
dos orçamentos de cache, então a garantia se mantém sob carga adversa apenas porque
server.maxConnections limita quantos podem existir ao mesmo tempo.
O Slater é um handle de réplica de leitura; o controle primário de segurança de
conexão é a rede, não o binário. Vincule-o a uma interface privada, restrinja faixas
de origem na camada de rede (security groups / NetworkPolicy) e — se ele enfrentar
qualquer coisa além de clientes confiáveis — coloque na frente um proxy L4 com limite
de conexões (HAProxy maxconn + uma stick-table por origem, ou nftables connlimit
hashlimit). Isso fica antes de o file descriptor ser entregue ao processo, então
é o limite mais robusto.Os limites no binário acima (maxConnections, maxPreAuthConnections,
maxConnectionsPerIp, os limites diferenciais de bytes e loginTimeoutMs) são
defesa em profundidade: vêm ligados por padrão e generosos para serem invisíveis
a uma população legítima de clientes, mas fazem a garantia de RSS limitado se manter
mesmo quando o proxy é esquecido. Veja docs/HARDENING.md para a
postura defensiva completa, e THREAT_MODEL.md / SECURITY_WORKLIST.md para o
detalhe canônico.
O Slater consulta o ponteiro current de cada grafo a cada generationPollMs
(poll, não inotify — o diretório de dados pode ser armazenamento remoto/de rede
como NFS, onde eventos de mudança de filesystem não são confiáveis). Quando ele muda:
reloadStrategy=exit (padrão): o servidor registra fatal e sai com código não-zero
para que o orquestrador o reinicie limpo contra a nova geração.reloadStrategy=swap: o servidor abre e valida a nova geração (mesmo guard de
hash de conteúdo do boot), troca-a atomicamente e deixa consultas em andamento
terminarem na antiga. Uma nova imagem corrompida/incompleta é recusada e a
geração antiga continua servindo.acl.json mapeia usuários para hashes de senha argon2id e concessões por grafo de
read / write. Gere um hash (nunca armazene texto claro) com:```sh
slater hash-password 's3cret' # prints a $argon2id$… string for acl.json
Um `acl.json` inicial é fornecido na raiz do repositório; sua estrutura é:```json
{
"users": {
"reporting": {
"passwordArgon2id": "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>",
"grants": {
"people": ["read"],
"products": ["read", "write"]
}
}
}
}
users — uma entrada por login, identificada pelo nome de usuário.
passwordArgon2id — a string $argon2id$… de slater hash-password
(nunca em texto claro; o arquivo em si é JSON simples e reside em armazenamento compartilhado).
grants — listas de capacidades por grafo. Duas permissões são significativas:
read — consultar o grafo. Um grafo ausente das permissões de um usuário é invisível para ele.write — modificar o grafo por meio da camada gravável (delta.enabled): as
instruções MERGE / SET / DELETE e CALL slater.consolidate().Monte-o somente leitura no caminho indicado por aclPath (padrão /config/acl.json).
O servidor o recarrega a cada troca a quente de geração, e o carimbo ACL em repouso é
reverificado a cada recarga (veja requireAclStamp).
O binário slater também serve como sua própria sonda de atividade: slater healthcheck [host] [port] realiza um handshake Bolt (não uma solicitação HTTP) contra o servidor e
sai com 0 se negociar uma versão de protocolo, 1 caso contrário — usando como padrão
localhost e a porta Bolt configurada. É isso que o HEALTHCHECK do contêiner
executa, para que orquestradores vejam um servidor verdadeiramente pronto para Bolt, não apenas um
socket aberto:```sh
slater healthcheck localhost 7687 # exit 0 = healthy
docker exec slater /app/slater healthcheck # inside the container
## Consulta única
Para scripts, verificações de CI e consultas rápidas, `slater query` monta a
geração atual de um grafo, executa uma única consulta Cypher somente leitura
em processo, imprime o resultado como um objeto JSON e sai — sem servidor, sem
conexão Bolt. Ele respeita a mesma configuração do servidor (backend de
armazenamento, chave de criptografia, orçamentos de consulta):```sh
# GRAPH defaults to `defaultGraph`. Without -q, normal datestamped logging
# (config, "opened generation", …) is written to stdout alongside the result.
slater query mygraph 'MATCH (n) RETURN count(n) AS c'
# -q/--quiet ⇒ logging suppressed, so stdout is *only* the compact result JSON
slater query mygraph -q 'MATCH (c:Company) RETURN c.ticker AS t LIMIT 3' | jq
# {"columns":["t"],"rows":[["AUPH"],["KYMR"],["MREO"]]}
Nodes and relationships expand to their labels/type and properties. Use -q
when you want machine-parseable output (the result JSON is the only thing on
stdout); omit it for an operator-facing run with logs. Without -q a
metrics-only summary is logged after each run — e.g.```text
INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10
carregando a consulta `cost` (elementos cobrados), `resultCount`, `execMs` e
`limitRowCount` (apenas quando a consulta especifica um `LIMIT`) — nunca o texto da consulta
ou qualquer valor de resultado. O status de saída é `0` em caso de sucesso, `1` em caso de erro
de análise/abertura/execução (mensagem no stderr).
## Exportar um grafo (`slater dump`)
`slater dump` exporta um grafo de um servidor **em execução** como Cypher `MERGE` de chave de negócio
— o mesmo dialeto que o `slater-build` ingere — para que um grafo faça um ciclo completo
(dump → `slater-build` → nova geração) para migração ou backup de texto. Ao contrário
do `slater query`, ele conecta-se via **Bolt**, autentica-se e respeita os ACLs por grafo,
portanto não precisa de acesso a disco no servidor. A senha é lida de
`SLATER_DUMP_PASSWORD` ou do stdin (nunca de uma flag, mantendo-a fora do `ps`/histórico).```sh
# List the graphs the authenticated user may read.
SLATER_DUMP_PASSWORD=pw slater dump --list -u reporting
# Dump a graph to a file (identity keys inferred from range indexes).
SLATER_DUMP_PASSWORD=pw slater dump people -u reporting -o people.cypher
# Rebuild it into a fresh generation.
slater-build --input people.cypher --graph people --data-dir ./data
Cada chave de identidade de um rótulo é a propriedade carregada pelo seu índice de intervalo; substitua
com --key Label=prop (repetível) ou um --pk <campo> global. O DDL CREATE INDEX
é emitido primeiro para que a reconstrução recrie os índices. Um nó com vários rótulos
mantém todos os rótulos — ele é emitido como MERGE (n:Ident:Outro {chave: v}), com
o rótulo de identidade (aquele que fornece a chave de negócio) primeiro e o restante
ordenado; o merge é baseado apenas no rótulo de identidade, então os rótulos finais
são gravados no nó sem criar outro. Rótulos, tipos de relacionamento e chaves de propriedade contendo caracteres especiais são citados com crases na
emissão, então nomes incomuns são preservados fielmente e não podem injetar Cypher na
reconstrução. Vetores (e outros
valores sem grafia literal em Cypher) não podem acompanhar um dump MERGE e são descartados
com um aviso no stderr. O status de saída é 0 em caso de sucesso, 1 em caso de erro.
Um passo a passo completo e executável — construir um grafo, servi-lo, conectar-se com os drivers neo4j JavaScript e Python, e gravar nele — está nas páginas Quickstart e Writing data do manual, usando o grafo de exemplo incluído em docs/manual/examples/.
export PATH="$HOME/.cargo/bin:$PATH" cargo build cargo test # unit + the bounded-RSS headline integration test cargo clippy --all-targets -- -D warnings cargo fmt --all -- --check
### Backends de object-store são recursos opcionais do cargo
Um `cargo build` simples produz um binário **apenas com filesystem** — os
backends `s3` e `gcs` ficam atrás de recursos do cargo para que o build padrão
permaneça enxuto (sem AWS ou Google SDK, sem runtime assíncrono). Ative o que
precisar em **ambos** `slater` (serve) e `slater-build` (publish):```sh
# S3 only / GCS only / both
cargo build -p slater -p slater-build --features s3
cargo build -p slater -p slater-build --features gcs
cargo build -p slater -p slater-build --features s3,gcs
Cada crate expõe funcionalidades correspondentes s3 / gcs que encaminham para
graph-format/{s3,gcs}. Solicitar um backend em tempo de execução
(dataBackend.kind=s3|gcs, ou slater-build --publish-{s3,gcs}-*) sem a sua
funcionalidade compilada falha rapidamente com um erro claro de "compilado sem a funcionalidade …".
A imagem Docker publicada ativa ambas (Dockerfile CARGO_FEATURES), portanto
imagens pré-construídas não precisam de sinalizadores extras — isso só importa ao compilar a partir do código-fonte.
Os testes de integração também são limitados da mesma forma: --features s3 --test s3_minio,
--features gcs --test gcs_emulator (um fake-gcs-server), e --features gcs --test gcs_real (GCS real via ADC); cada um é ignorado a menos que suas variáveis de ambiente SLATER_* estejam
definidas.
Consulte docs/PLAN.md, docs/PROGRESS.md e docs/DECISIONS.md para o design,
o registro de marcos e o registro de decisões.
Até seis engines, uma suíte de cliente único, grafos de um brinquedo de 62 mil nós até Wikidata
91,6M de nós / 1,5B de arestas. Cada engine é medido isoladamente (todos os outros contêineres
parados — RSS e latência são sua própria pegada). As tabelas de latência abaixo foram
remedidas no Slater 0.21.0 (a build gravável): os grafos pequenos/médios (MeSH, EU-AI-Act)
recentemente, e o grafo de 91,6M como uma nova passagem mesma-máquina, âncora-compartilhada slater-vs-Neo4j (veja
essa tabela). Os números de memória residente são transportados da passagem anterior (medidos via
cgroup do contêiner; o caminho de leitura é byte-idêntico com a camada gravável ociosa). Os números dos
outros engines são da execução cross-engine estabelecida (suas versões/desempenho não mudaram).
Todos os números são medianas (ms) ou memória residente de pico (MiB). Menor é melhor em todos os lugares; negrito =
melhor na linha. O slater foi executado em seu backend de sistema de arquivos local (fs); os backends S3 e GCS trocam a latência de leitura local
por idas e voltas ao armazenamento de objetos (mitigadas pelos caches em memória e pela camada de cache em disco local
opcional), portanto esses números caracterizam o engine, não uma implantação de armazenamento em rede.
Os três engines que paginam do disco — slater, Neo4j 5 e LadybugDB — carregam todos os cinco grafos. O trio em memória (Memgraph · FalkorDB · ArcadeDB) não consegue conter o grafo de 1,5B de arestas de forma alguma (ele precisa de ~64–128 GiB residentes), e o importador do ArcadeDB também não consegue terminá-lo.
Cada número é memória de trabalho comprometida — o que o SO não pode recuperar. Todo engine exceto o slater mantém seu grafo em memória anônima comprometida (heap próprio, cache de páginas off-heap do Neo4j, ou um pool de buffers), portanto seu RSS de pico é sua pegada comprometida. Somente o slater atende a partir do cache de páginas do SO recuperável de seu armazenamento em disco, portanto seu número é o conjunto de trabalho anônimo; o cache de páginas do armazenamento (despejável sob pressão — o slater continua atendendo) é excluído e mostrado como total entre parênteses para o grafo de 91,6M. Negrito = mais baixo.
O slater é o mais baixo em todas as escalas e cresce ~50× enquanto o grafo cresce ~1.500× — sua
pegada acompanha o conjunto de trabalho da consulta, não o grafo (ocioso ~16–71 MiB durante todo o processo). O
trio em memória cresce ~linearmente e não consegue carregar o grafo de 1,5B; o Neo4j compromete um heap de ~2 GiB
independentemente da consulta. († LadybugDB apenas nas formas limitadas — suas travessias de hub / comprimento variável /
caminho mais curto em 1,5B de arestas precisam que seu pool de leitura seja elevado para ≥2 GiB, vs. o limite automático
maxIntermediate do slater.) Os histogramas valor→contagem em tempo de build adicionam memória residente insignificante —
alguns KB para uma coluna indexada de baixa cardinalidade, e zero para grafos de chave única como Wikidata
(wikidata_id excede o limite de cardinalidade do histograma, portanto nenhum é armazenado) — portanto esses números são
inalterados por essa funcionalidade.
O slater domina as formas de metadados / índice / varredura (count, rótulo, idx-eq, varredura — ~0,4 ms, 10–200× os engines de serviço), a busca pontual indexada (0,43 ms, agora superando o par em memória de 0,48 ms), o multi-salto sem âncora (2-saltos 1,40 ms via varredura por tipo de relacionamento, o mais rápido do campo), e — via um histograma valor→contagem em tempo de build na chave de agrupamento indexada — o group-by / count(DISTINCT) de rótulo inteiro (0,45 ms, à frente dos 5,3 ms colunares do LadybugDB). Os servidores em memória mantêm apenas o 1-salto bruto (Memgraph 1,21 ms vs. 1,28 ms do slater). (pole 62k/106k parece o mesmo: slater único mais rápido em count/varredura ~0,4 ms, ~1,3–2,6 ms em saltos.)
O slater responde ao kNN com uma varredura exata de força bruta (esses conjuntos estão abaixo de seu limite ANN de 50k vetores) onde os outros usam um HNSW residente aproximado — portanto os resultados do slater são exatos (recall 1,0). Um kernel de distância SIMD + uma matriz de vetores residente e pré-normalizada levou Concept de ~23 → ~2,9 ms e Chunk de ~10 → ~2,4 ms, portanto o slater agora supera Neo4j e LadybugDB e está dentro de ~1,4× do Memgraph, ficando atrás apenas do FalkorDB — enquanto é exato.
As tabelas acima são comparações de leitura entre engines. O caminho de gravação de vetores (a
escada de gravação estilo FreshDiskANN sobre a base Vamana estática)
não tem contraparte entre engines — nenhum outro engine aqui faz ANN gravável nativo em disco —
portanto os números abaixo são benchmarks de componente de engine único sobre um fixture sintético
semelhante a embeddings (uma variedade de baixo posto, dim 768, normas desiguais), comprometidos sob
crates/slater/benches/ e documentados por extenso — com a
metodologia e todas as ressalvas — em docs/PERF-REPORT.md. O recall é
sempre medido contra uma força bruta exata sobre o conjunto vivo, nunca um índice contra outro.
A escala aqui é representativa e extrapolada apenas onde a métrica é linear em tamanho.
O único número que merece a caixa de desempenho dedicada é a taxa de transferência de reescrita de consolidação do caminho lento — quando uma consolidação carrega exclusões ou novos vetores em vez de uma permutação pura, é uma recompressão sequencial limitada por zstd de thread única e disco local, portanto o MiB/s absoluto é específico do ambiente (o relatório mostra a forma e explica a faixa ambiental).
Os engines em memória (Memgraph / FalkorDB / ArcadeDB) não conseguem carregar este grafo de forma alguma
(~64–128 GiB residentes). Apenas o slater e o Neo4j 5 conseguem. Esta é uma nova passagem mesma-máquina, mesmo-dia
contra um conjunto de âncoras fixo e compartilhado — cada consulta atinge os nós idênticos em ambos os engines,
portanto o confronto direto é comparável (um pool comum de wikidata_id de âncoras de grau moderado;
veja a nota abaixo sobre por que isso importa). O slater é mostrado em ambos os fanouts (query.maxFanout 1 =
padrão de taxa de transferência, 8 = o dial de latência que sobrepõe leituras de blocos frios). Negrito = melhor na linha.
O quadro honesto: o slater domina as formas de metadados / índice — count(*) é
atendido por metadados (0,41 ms vs. a varredura de disco de 3,6 s do Neo4j, ~8800×), e busca pontual / grau /
3-saltos rodam ~2–10× mais rápido — está empatado com o Neo4j em 1–2 saltos (fanout 8 avança em leituras frias),
mas perde comprimento variável *1..2 distinto decisivamente (≈1 s vs. os 47 ms do Neo4j): a
expansão distinta de comprimento variável do slater é materialmente mais lenta aqui, uma fraqueza real que merece sua própria
investigação. Tudo isso com algumas centenas de MB de RSS versus o heap comprometido de ~2 GiB do Neo4j.
Sobre as âncoras. Esses números de travessia dependem fortemente de quais nós você começa — um nó a um link de um mega-hub do Wikidata ("humano", "país") tem uma vizinhança de 2-saltos com milhões de membros, portanto o custo de comprimento variável/salto oscila por ordens de magnitude com a escolha da âncora. A edição anterior desta tabela amostrava os próprios "primeiros N por varredura" de cada engine, o que não é nem estável nem comparável; esta passagem fixa um único conjunto de âncoras compartilhado e limitado por grau para ambos os engines. (shortestPath é omitido desta passagem — entre duas âncoras arbitrárias é dependente da existência do caminho e variância muito alta para mediana significativa.)
count(*) multi-salto — memória desacoplada do tamanho do resultadoRETURN count(*) multi-salto sem limite conta durante a expansão em vez de materializar
as linhas correspondentes. Mesmas âncoras de hub no grafo de 91,6M, maxIntermediate=20M:
| 3-saltos count(*) @ 91,6M | fanout=1 | fanout=8 |
|---|---|---|
| latência / conjunto de trabalho de pico | 554 ms / 0,66 GiB | 298 ms / 1,9 GiB |
A contagem mantém O(1) linhas. A cobrança não muda, portanto uma contagem de mega-hub ainda dispara
maxIntermediate em computação (leituras de adjacência), limitada como antes.
maxFanout)Elevar query.maxFanout sobrepõe as leituras de blocos frios e limitadas por IO de uma consulta entre núcleos —
ajuda formas limitadas por disco com conjunto de trabalho frio grande e é estável em formas quentes. No grafo de 1,5B
: shortestPath ≤6 918 → 608 ms (1,5×, maior busca 6.269 → 2.350 ms, 2,7×);
3-saltos count 547 → 298 ms. maxFanout=1 é o padrão (orientado a taxa de transferência); 8 é o
dial de latência, com mais memória de trabalho transitória.
Tabelas completas por engine (pole, MeSH, EU-AI-Act + o dial RAM↔latência blockCacheBytes,
Wikidata 1M & 91,6M) estão em
perf/cross-engine-hs/README.md; a nova passagem apenas-slater
(ambos os fanouts, todos os conjuntos de dados) está em perf/PERF_CURRENT_STATUS.md.
Os benchmarks acima são de cliente único. O eixo complementar — comportamento sob muitos
clientes concorrentes — tem seu próprio harness, perf/loadtest/: um driver
Locust sobre Bolt mais um coordenador que aumenta a carga, lê CALL slater.diagnostics(),
encontra o joelho de capacidade e nomeia o limitador (método completo em
docs/LOAD-TESTING.md). Destaques de uma execução com cache de 256 MiB no
grafo Wikidata-1M (uma máquina de 16 núcleos):
Ambos os problemas de memória que o teste de carga revelou agora estão fechados; todos rastreados no documento de teste de carga.
Licenciado sob a Apache License, Versão 2.0. Consulte LICENSE para o
texto completo e NOTICE para atribuição. A menos que você declare explicitamente
o contrário, qualquer contribuição intencionalmente submetida para inclusão neste trabalho,
conforme definido na licença Apache 2.0, será licenciada como acima, sem quaisquer
termos ou condições adicionais.
SPDX-License-Identifier: Apache-2.0
INSERTSETREMOVEDELETE| Recurso | O que significa para você |
|---|
| Memória limitada e previsível | A memória residente acompanha três orçamentos de cache que você define, dentro de overhead limitado por entrada e do alocador — ela não cresce com o tamanho do grafo; você ajusta a compensação desempenho/RAM em vez de provisionar para o grafo inteiro. Um alocador jemalloc com purga em segundo plano devolve memória liberada ao SO após rajadas pesadas de consultas, então o tamanho residente cai de volta ao seu piso ocioso em vez de permanecer fixado na marca d'água alta pós-rajada. |
| Multilocatário pronto para uso | Um servidor hospeda muitos grafos com concessões de leitura por usuário — isolamento multi-banco que a maioria dos bancos de grafos reserva para um nível pago/empresarial. |
| Criptografia em repouso e em trânsito | Selagem por bloco XChaCha20-Poly1305 (a chave nunca é gravada em disco) além de TLS opcional (bolt+s://). Compatível com GDPR por construção. A criptografia também é o que compra integridade autenticada: o construtor sela o manifesto com um MAC com chave, e um servidor que detém a chave o verifica e recusa servir uma geração cujo manifesto foi forjado, alterado ou teve seu MAC removido. Uma imagem sem chave (texto simples) é protegida apenas pelo hash de conteúdo sem chave — completude e corrupção, não adulteração. Veja O que integridade significa em cada configuração. |
| Instalação mínima | Um binário pequeno e reduzido em uma base glibc distroless (sem shell/apt) — a imagem multi-arquitetura (amd64/arm64) puxa ~22 MB, ou ~12 MB para a tag slater:latest-lite somente servidor; TLS Rust puro, sem OpenSSL. Puxe e execute. |
| Feito para publicação periódica | Construa um grafo offline, sirva-o imutável e troque atomicamente por uma nova versão com zero tempo de inatividade — ideal para cargas de trabalho de data warehouse / atualização agendada. |
| Robusto sob carga | O servidor e o construtor offline compilam com #![forbid(unsafe_code)] — o único unsafe do mecanismo vive no crate auditado do alocador jemalloc. O núcleo é imutável, então as leituras não usam locks e nunca esperam por um gravador; um único gravador serializa mutações apenas atrás do caminho de gravação. Sem pausas de GC, sem corridas de dados. Uma consulta ruim não pode derrubar o servidor. |
| Funciona com suas ferramentas neo4j | Fala Bolt 5.4 / 4.4 / 4.1 — use os drivers neo4j padrão (JS, Python, Go, Java…), cypher-shell ou navegadores de grafos sem alterações. |
| Superfície de consulta Cypher rica | Uma ampla superfície de leitura: MATCH/WHERE/WITH/UNION, subconsultas CALL {…}, 70+ funções e agregações, valores temporais e geoespaciais e regex. |
| Gravações ao vivo e duráveis | Uma camada LSM opcional de gravador único sobre o núcleo imutável (delta.enabled): MERGE / SET / DELETE / CREATE / REMOVE por chave de negócio sobre nós e relacionamentos, UNWIND de gravação em lote (um fsync por lote) e CALL slater.consolidate() — com commit em grupo, durável via fsync e dobrado de volta em um núcleo novo pela consolidação. O caminho de leitura é byte-idêntico quando o delta está vazio. |
| ISO GQL, leitura e gravação | Fala um subconjunto de ISO GQL (ISO/IEC 39075) sobre a mesma conexão Bolt — caminhos quantificados, restritores de caminho, seletores de caminho mais curto, expressões booleanas de rótulo/tipo, FOR, CAST, um prefixo de dialeto opcional GQL/CYPHER — e, com a camada gravável ativada, as declarações de modificação de dados do GQL (INSERT / SET / REMOVE / [DETACH] DELETE) descem para o mesmo caminho de gravação durável. Cypher e GQL, leituras e gravações, em um único mecanismo. |
| Vetores + grafo em um mecanismo | Busca vetorial ANN nativa em disco (Vamana + PQ; cosseno / L2 / dot) para embeddings/RAG, além de algoritmos de grafo (PageRank, BFS, betweenness, WCC…) — memória limitada mesmo com milhões de vetores. Os embeddings são graváveis (uma escada de gravação estilo FreshDiskANN): insira / atualize / exclua um vetor, visível no KNN imediatamente, dobrado na base sem reconstrução. |
| Seguro em armazenamento de rede | Cada arquivo é hashado com BLAKE3 por conteúdo e verificado na abertura; imagens rasgadas ou copiadas pela metade são recusadas, não servidas. Projetado para volumes NFS/remotos (sem surpresas de mmap). |
| Backends de armazenamento plugáveis | Sirva o mesmo formato de geração de um sistema de arquivos local, um bucket S3 (compatível com S3) ou um bucket Google Cloud Storage — publique uma vez, distribua para réplicas sem estado — com uma camada de cache SSD local opcional na frente do armazenamento de objetos. Veja Backends de armazenamento. |
| Caminho | Finalidade | Notas |
|---|
/data | As gerações do grafo (<graph>/<uuid>/… + current). | Somente leitura para réplicas; produzido pelo slater-build. Pode viver em armazenamento remoto/de rede (ex.: NFS), então leituras não são assumidas como latências rápidas de SSD local. |
/sandbox | Overlay de configuração por ambiente + segredos. | /sandbox/config.json é deep-merged sobre o config.json embutido; também contém acl.json, material PEM de TLS, o arquivo de chave em repouso. |
/tmp, /run | Scratch (tmpfs). | Uma réplica de leitura nunca grava em disco por padrão. |
(writer) delta.walDir | O write-ahead log + segmentos delta L0, quando delta.enabled. | Gravável, e um volume durável e real — nunca tmpfs (é o piso de durabilidade). Um caminho relativo resolve sob o diretório de dados; dê a um writer seu próprio volume persistente aqui. |
| (opcional) cache de disco | O cache de blocos em disco local, quando dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0. | Gravável, e um volume real — não tmpfs. Usado pelos backends s3 e gcs; veja Storage backends. |
Elas são independentes: uma permissão read não confere acesso de escrita. Ativar a camada
gravável, portanto, não pode promover seus leitores existentes a escritores. Um escritor precisa
de ambas — ["read", "write"] — porque resolver uma chave de negócio para escrevê-la é uma leitura.
Strings de permissão não reconhecidas são ignoradas (elas não concedem nada).
| engine | classe | limite de memória |
|---|
| slater | com suporte em disco, paginado | query.maxIntermediate limita o conjunto de trabalho automaticamente |
| Neo4j 5 | com suporte em disco, JVM | heap de ~2 GiB + off-heap, comprometido independentemente da consulta |
| Memgraph · FalkorDB | em memória | grafo inteiro residente na RAM |
| ArcadeDB | em memória, JVM | grafo inteiro residente; o mais pesado |
| LadybugDB | embutido, colunar | pool de buffers manual que deve exceder a consulta |
| grafo (nós / arestas) | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| pole — 62k / 106k | 11 | 746 | 114 | 140 | 1.556 | 198 |
| MeSH — 341k / 469k | 63 | 1.083 | 358 | 455 | 1.631 | 121 |
| EU-AI-Act — 21k / 45k (+55 MiB vec) | 99 | 729 | 229 | 312 | 1.948 | 286 |
| Wikidata — 91,6M / 1,5B | 584 (4.595 total) | ~2.900 | não-carregável | não-carregável | não-carregável | ~652 † |
| forma | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| count(*) todos os nós | 0,41 | 15,0 | 23,8 | 16,4 | 82,0 | 2,2 |
| contagem de rótulo | 0,42 | 4,2 | 20,7 | 1,1 | 4,4 | 4,3 |
| busca pontual indexada | 0,43 | 3,9 | 0,48 | 0,48 | 0,65 | 8,8 |
| contagem idx-eq | 0,42 | 4,9 | 5,0 | 2,0 | 381 | 2,5 |
| 1-salto (âncora indexada) | 1,28 | 5,8 | 1,21 | 4,1 | 390 | 4,9 |
| 2-saltos (sem âncora) | 1,40 | 5,6 | 8,5 | 16,7 | 444 | 6,4 |
| group-by / count(DISTINCT) | 0,45 | 47–51 | 63–64 | 31–39 | 411 | 5,3 |
varredura completa CONTAINS | 0,43 | 5,4 | 24,1 | 1,7 | 16,3 | 4,1 |
| forma | slater | Neo4j 5 | Memgraph | FalkorDB | LadybugDB |
|---|
| kNN top-10 Concept | 2,9 | 8,6 | 1,9 | 1,2 | 2,8 |
| kNN top-10 Chunk | 2,4 | 5,7 | 1,9 | 1,5 | 3,2 |
| propriedade | medido | por que importa |
|---|
| Latência KNN vs. gravações pendentes | Índice RW ~1,5–2 ms, estável até 50k pendentes; a sobreposição de força bruta pré-indexada 1,9 → 115 ms (linear no delta) — 61× em 50k | a latência de consulta não degrada conforme as gravações se acumulam entre consolidações |
| Inserção de embedding | ~1,5–2 ms por vetor no índice vivo | uma gravação é visível ao KNN imediatamente; o orçamento de reconstrução de delta é ≈ 2 ms × o limite de delta |
| IO de exclusão em recall iso | 2,9× menos buscas de nós por consulta em 67 % excluídos, 5,2× em 80 % (recall ≥ 0,90) | um grafo consolidado não paga imposto de leitura por vetores excluídos |
| Consolidação, permutação pura | O(1) — o .vamana é hard-linked byte-idêntico, apenas a coluna de id é reescrita | dobrar gravações de vetores na base ignora a reconstrução O(N·R·L) |
| Recall na escada | consolidado ≥ base para cosseno, L2 e produto escalar | a escada de gravação preserva o recall em cada degrau |
| forma | slater (fan 1) | slater (fan 8) | Neo4j 5 |
|---|
| count(*) todos os nós | 0,41 | 0,41 | 3606 |
| busca pontual (indexada) | 0,72 | 0,49 | 6,3 |
| grau (contagem de 1-salto) | 0,43 | 0,44 | 6,0 |
| vizinhos de 1-salto | 9,8 | 4,5 | 10,1 |
| 2-saltos | 37 | 23 | 34,5 |
| 3-saltos | 32 | 25 | 74 |
comprimento variável *1..2 distinto | 985 | 1056 | 47 |
| dimensão | slater | melhor do campo | veredito |
|---|
| memória residente, qualquer escala | 11–584 MiB (62k → 91,6M) | em memória 1,5–2,7 GiB; não consegue carregar 1,5B | slater |
| count / metadados / varredura | ~0,4 ms | engines de serviço 5–80 ms | slater (10–200×) |
| busca pontual indexada | 0,43 ms (MeSH) | Memgraph · FalkorDB 0,48 ms | slater (supera o par em memória) |
| multi-salto sem âncora (linhas) | 1,40 ms (MeSH 2-saltos) | Neo4j 5,6 ms | slater (varredura por tipo de relacionamento) |
| agregação (group-by / DISTINCT) | 0,45 ms | LadybugDB 5 ms (colunar) | slater (histograma em tempo de build) |
| kNN | 2,4–2,9 ms (exato) | FalkorDB 1,2 ms (HNSW) | supera Neo4j/Ladybug; ~1,4× atrás do Memgraph; exato |
| 91,6M metadados / ponto / grau / 3-saltos | 0,4–32 ms | Neo4j 6–3.600 ms | slater (2–8800×) |
| 91,6M 1–2-saltos | 4,5–23 ms (fan 8) | Neo4j 10–35 ms | ~empatado |
91,6M comprimento variável *1..2 distinto | ~1 s | Neo4j 47 ms | Neo4j (um ponto fraco real do slater) |
count(*) multi-salto em escala | 0,3–0,6 GiB | engines em memória materializam o conjunto de linhas | slater, limitado |
| resultado | medição |
|---|
| Mantém 1000 clientes concorrentes, zero falhas | taxa de transferência atinge pico de ~2,5k rps; o joelho de latência se instala em torno de 750 clientes (p99 51 → 750 ms) — fila sob contenção de núcleo, não um limite rígido (execução única, WSL2) |
| Cache de blocos limitado e eficaz | taxa de acerto de 100%, 0 despejos, 50 MB residentes para um conjunto de trabalho que cabe no cache |
| RSS mantido sob carga sustentada | o alocador jemalloc mantém o RSS em ~0,6 GB em uma rampa wiki_cache_churn de 100→500 clientes — limitado por cache e estável, sem nenhum ajuste MALLOC_* (o antigo MALLOC_ARENA_MAX=2 + limite de corte está aposentado); sua purga em segundo plano também retorna o pico pós-explosão em vez de deixá-lo fixado |
| Memória agregada limitada | query.maxIntermediateGlobal em todo o servidor + expansão cobrada por adjacência seguram a inundação de 2-saltos wiki_budget em 1000 clientes sem OOM (RSS ~0,6 GB; o guarda descarta ~60% das consultas de hub como erros de orçamento repetíveis) |