
Speicherschonende graphdb mit Bolt+tls-Unterstützung, Verschlüsselung im Ruhezustand und Vektoren, entwickelt für lokale Replikat-Graph-Anwendungsfälle.
Aktuelle Version: v0.25.2 — alle Releases.
In einem Satz: Slater bedient Graphen, die nicht in den Speicher passen — Hunderte Millionen Knoten und Milliarden Kanten in wenigen hundert MB RAM — über standardmäßiges Bolt, sodass jeder neo4j-Treiber einfach funktioniert, mit disk-nativem Vektor-Search direkt neben dem Graphen, und es nimmt live, dauerhafte Schreibvorgänge entgegen, ohne darauf zu verzichten. Der residente Speicher wird durch ein Cache-Budget bestimmt, das Sie wählen, nicht durch die Größe des Graphen.
Verknüpfungen
Eine Graphdatenbank speichert Daten als Dinge (Knoten) und die Beziehungen zwischen ihnen (Kanten), wobei die Beziehungen erstklassige Bürger sind. Das ist genau das, was Sie brauchen, wenn Ihre Fragen sich um Verbindungen drehen und nicht um Zeilen — „Wer ist innerhalb von drei Hops von diesem Konto entfernt?“, „Was ist die vollständige Abhängigkeitskette hinter diesem Build?“, „Welche Konten teilen sich ein Gerät, eine Adresse und eine Karte?“ — die Abfragen, die in SQL zu einem Sumpf rekursiver Joins werden, sich in einem Graphen aber natürlich ergeben.
Die häufigste Beschwerde über Graphdatenbanken ist, dass sie nicht über das hinaus skalieren, was Sie im RAM halten können. Viele von ihnen (z. B. neo4j, Memgraph, FalkorDB usw.) halten den gesamten Graphen resident: Ein 40 GB-Graph benötigt 40 GB Speicher — pro Instanz. Möchten Sie eine Replik pro Region, pro Mandant oder pro Pod? Multiplizieren Sie die Rechnung. Und ab einer bestimmten Größe laden sie einfach nicht mehr: Der Wikidata-Graph mit 90 Millionen Knoten / 1,5 Milliarden Kanten benötigt ~64–128 GiB resident, sodass die In-Memory-Engines ihn überhaupt nicht öffnen können.
Slater ist die Widerlegung. Anstatt den Graphen in den Speicher zu laden, kompiliert es ihn einmal, offline: slater-build verwandelt Ihre Daten in ein content-addressed, unveränderliches On-Disk-Image, und beliebig viele Slater-Server bedienen dieses Image dann über Bolt (sodass Ihre vorhandenen neo4j-Treiber einfach funktionieren), pagen Blöcke bei Bedarf ein und halten nur ein festes Cache-Budget resident. So wird derselbe 90M-Knoten-Graph aus ein paar hundert MB RAM bedient — Graphgröße und Speicherrechnung sind entkoppelt. Ein 4 GB-Graph und ein 400 GB-Graph kosten denselben RAM zum Bedienen, sodass Sie günstige, zustandslose Lese-Repliken ausrollen und den Store, nicht den Heap, den Graphen halten lassen.
Das macht ihn zu einer natürlichen Wahl für Wissensgraphen hinter RAG, Empfehlungs- und Identitätsgraphen, Abhängigkeitsgraphen — alles Große und Verbundene, das Sie günstig und häufig abfragen möchten. Disk-nativer Vektor-Search lebt direkt neben dem Graphen, sodass dieselbe Engine auch die Retrieval-Schicht für Embeddings ist.
Einmal kompiliert bedeutet jedoch nicht eingefroren. Dieses Image ist eine Basis, kein Endzustand: Eine optionale Schreibschicht sitzt darüber, sodass ein Live-Graph korrigiert und erweitert werden kann, ohne etwas neu zu bauen.
Der Kern ist unveränderlich; der Graph ist es nicht. Aktivieren Sie die beschreibbare Schicht (delta.enabled) und Sie schreiben über Bolt — korrigieren Sie eine Eigenschaft, fügen Sie einen Knoten hinzu, ziehen Sie eine Kante zurück — und die Änderung landet dauerhaft, ohne Neubau des Images. Was es auf der Leseseite günstig hält, ist wo die Schreibvorgänge leben.
Schreibvorgänge sammeln sich in einer Log-Structured-Merge (LSM)-Schicht über dem unveränderlichen Kern: ein Write-Ahead-Log und eine In-Memory-Tabelle, die in unveränderliche Delta-Segmente überläuft und durch eine periodische Konsolidierung in einen frischen Kern zurückgefaltet wird. Was Ihnen das bringt:
count(*), die Label- und Beziehungstyp-Marginalien — bleiben Metadaten-Lesevorgänge, selbst bei ausstehenden Schreibvorgängen: Das Delta führt seine eigenen Zähler, sodass ein count(*) über einen 91,6M-Knoten-Kern mit einer halben Million ausstehender Schreibvorgänge immer noch in zig Millisekunden antwortet, ohne einen einzigen Block zu berühren.SUCCESS erst nach dem fsync zurück, das den Schreibvorgang abdeckt. Bündeln Sie Ihre Schreibvorgänge und sie sind günstig — ein Schreib-UNWIND committet ein fsync pro Batch statt pro Zeile.MERGE / MATCH … SET / DELETE (und CREATE / , Detach-Delete, Beziehungsschreibvorgänge), die auf die Identitätseigenschaft eines Knotens schlüsseln — oder die äquivalenten ISO-GQL-Datenmodifikationsanweisungen ( / / / ), die auf denselben Pfad herunterbrechen. Korrigieren, einfügen, upserten und zurückziehen, über Knoten und Kanten, adressiert so, wie Ihre Daten bereits sind.Bei deaktivierter Schicht — dem Standard — bedient Slater den reinen unveränderlichen Kern und verweigert Schreibvorgänge. Siehe Die beschreibbare Schicht für das vollständige Modell.
Zum Namen. Slater ist nach dem CIA-Agenten in Archer (einer großartigen Serie) benannt, der darauf besteht, nur mit einem Namen genannt zu werden — „Just… Slater“ — und einer meiner Lieblingsfiguren darin. Siehe die Charakter-Wiki-Seite.
MERGE / SET / DELETE über Knoten und Kanten, gruppen-committet und fsync-dauerhaft, durch Konsolidierung in einen frischen Kern zurückgefaltet. Lesevorgänge zahlen nicht dafür.current-Zeiger atomar um, und Server nehmen sie auf. Jeder Block ist checksummiert, sodass ein halb kopiertes Image verweigert statt bedient wird.Zwei Binaries bilden den Workspace:
| Binary | Rolle |
|---|---|
slater | Der Online-Bolt-Server (der Container-ENTRYPOINT): bedient Lesevorgänge und, mit delta.enabled, den Single-Writer-dauerhaften Schreibpfad. |
slater-build | Der Offline-Compiler: verwandelt einen primitiven Cypher-Dump in ein unveränderliches, content-gehashtes Generationsverzeichnis. |
Slater trennt Massen-Building vom Bedienen: slater-build erledigt die schwere Arbeit
offline — nimmt Ihre Daten auf und kompiliert sie in eine unveränderliche Generation — sodass
ein kalter Graph nie auf dem heißen Serving-Pfad zusammengesetzt wird. Innerhalb des Servers
beantwortet die Leseoberfläche eine breite Cypher-Scheibe — Pattern-Matching, WITH/UNION/CALL {…}-
Subqueries, 70+ Skalar- & Aggregatfunktionen, temporale & geospatiale Werte, Graph-
Algorithmen (algo.*) und disk-natives Vektor-KNN (db.idx.vector.queryNodes) —
während das Delta-Overlay der beschreibbaren Schicht unter dieser Oberfläche sitzt und bei
Leere null Kosten hat, sodass Lesevorgänge nie die Schreibseiten-Mechanik tragen. Sie können einen Graphen
auf zwei Arten aktualisieren: live über Bolt darauf schreiben (siehe Die beschreibbare Schicht),
oder eine neue Generation offline bauen und den current-Zeiger atomar tauschen, den
der laufende Server über seine Generationswache aufnimmt (siehe
Generationswache).
Das vollständige Benutzerhandbuch lebt in docs/manual/ —
ein funktionsweiser Leitfaden, der für jede Fähigkeit erklärt, was sie ist, warum sie
existiert und wie man sie nutzt, mit Arbeitsbeispielen, die Sie gegen einen gebündelten
Beispielgraphen ausführen können. Beginnen Sie dort für alles über diese Übersicht hinaus.
graphiti-slater ist ein Adapter,
der es Graphiti ermöglicht, seinen temporalen Wissens-
graphen in Slater zu speichern, mit einem ausführbaren
docker-example/
— einschließlich der Bereitstellung für Claude Code als MCP-Server. Siehe dieses Repository für wie es
funktioniert und wie man es ausführt.
Slater ist dafür ausgelegt, als Docker-Bereitstellung ausgeführt zu werden — das ist der erwartete Weg,
es zu nutzen. Vorgebaute Multi-Arch-Images (linux/amd64 + linux/arm64) werden
auf Docker Hub unter
hikarisystems/slater
veröffentlicht, mit :latest und :vX.Y.Z bei jedem Release getaggt:```sh
docker pull hikarisystems/slater:latest
Eine Anleitung zur reinen Docker-Befehlsnutzung, -konfiguration und zum Betrieb findest du in
[`DOCKERHUB.md`](https://github.com/hikari-systems/slater/blob/main/DOCKERHUB.md) (und wird auf der Docker-Hub-Übersichtsseite gespiegelt) —
**beginne dort, wenn du bereitstellst.** Kurz gesagt:```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
Um das Image stattdessen lokal zu bauen (z. B. für die Entwicklung):```sh
docker compose build
docker compose up slater
build):docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data
Der Builder-Stage installiert `cmake`, `clang` und `libclang-dev` für das rustls-`aws-lc-rs`-Backend; `git` (bereits im Basis-Image enthalten) wird für die `hs-utils`-git+tag-Abhängigkeit benötigt, die `.cargo/config.toml` über die git-CLI abruft.
Die folgenden Abschnitte behandeln das On-Disk-Format, die Konfiguration, ACLs und ein lokales (nicht-Docker-)Arbeitsbeispiel.
## So funktioniert es```
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 (Symboltabellen,
Indexdeskriptoren, ein optionaler Verschlüsselungsheader), spaltenbasierte Blockdateien
(node_props.blk, node_labels.blk, edge_props.blk, topology.csr.blk,
vectors.f32.blk), Bereichsindizes (range/<name>.isam), ANN-Indizes über dem Schwellenwert
(vector/<label>.<prop>.{vamana,pq}) sowie ein current-Textzeiger.--encrypt wird
jeder Block zusätzlich mit XChaCha20-Poly1305 versiegelt (AEAD im Ruhezustand).Mit delta.enabled wird die unveränderliche Generation zur vollständig kompaktierten untersten
Ebene (dem „Kern") eines kleinen log-strukturierten Merge-Baums, und Live-Schreibvorgänge
liegen darüber:```
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)
└─────────────┘
* **Haltbarkeits-Boden — das WAL.** Jede Mutation wird hinter einem einzelnen
Writer pro Graph serialisiert, an ein pro-Graph-Write-Ahead-Log angehängt und
`fsync`-ed, bevor der Bolt-`SUCCESS` zurückgegeben wird — also *bestätigt ⇒
dauerhaft*, und ein abgerissener Schwanz wird beim Replay verworfen. Ein
gebatchtes Write-`UNWIND` hängt seine Zeilen an und committet **ein**
`fsync` für den gesamten Batch. Das WAL ist **nur lokaler Datenträger** (es wird
nicht über das Storage-Backend geleitet), was einen *Writer*-Knoten zustandsbehaftet
macht: Er benötigt ein dauerhaftes lokales Volume unter `delta.walDir`.
Read-Replicas bleiben zustandslos.
* **Memtable → L0 → Konsolidierung.** Schreibvorgänge sammeln sich in einer
In-RAM-Memtable (begrenzt durch `delta.memtableBytes`); wenn sie voll ist,
spült sie in ein unveränderliches L0-Delta-Segment. Eine **Konsolidierung**
faltet `{core + delta}` in einen frischen Core, indem sie die zusammengeführte
Ansicht zurück durch `slater-build` serialisiert und `current` atomar austauscht
— derselbe Content-Hash-Schutz wie bei jeder veröffentlichten Generation. Löse
sie von Hand mit `CALL slater.consolidate()` aus, automatisch bei
`delta.deltaCorePercent` der Core-Größe (optional begrenzt auf ein
Off-Peak-`delta.consolidateWindow`), oder lass den
`delta.deltaHardBytes`-Drossel als Rückhalt gegen unkontrolliertes Wachstum
dienen.
* **Das Overlay sitzt unterhalb der Leseoberfläche.** Der Executor liest durch
eine `ReadView`, die entweder der nackte Core ist (Delta immer leer) oder eine
zusammengeführte `(core, delta)`-Ansicht; die Engine ist darüber monomorphisiert,
sodass ein leeres Delta zu einem einzigen vorhersagbaren Zweig kompiliert und der
Nur-Lese-Pfad byte-identisch ist. Graphweite Zähler (`count(*)`,
Label-/Reltype-Marginalien) werden aus den eigenen Live-Zählern des Deltas
bedient, sodass sie auch bei ausstehenden Schreibvorgängen Metadaten-Lesevorgänge
bleiben.
* **Eine Abfrage sieht einen stabilen Snapshot.** Sie pinnt ein `(core, delta)`-Tupel
für ihre gesamte Lebensdauer. Es gibt keine Multi-Statement-Transaktionen und kein
Rollback — ein Schreibvorgang ist eine dauerhafte, über den Business-Key
adressierte Korrektur, keine OLTP-Transaktion.
Die genaue Schreibgrammatik und die Stellschrauben stehen in der Tabelle
[Konfiguration](#environment--configuration) (`delta.*`) und im
[Arbeitsbeispiel](#worked-example) unten.
### Bereichsindizes (ISAM)
Ein Bereichsindex (`range/<name>.isam`, einer pro indiziertem `(label, property)`)
ermöglicht es einem `MATCH (n:Label {prop: v})` oder `WHERE n.prop <op> v`, die
passenden Knoten-IDs **ohne Scannen des Labels** aufzulösen. Es ist eine
**[ISAM](https://en.wikipedia.org/wiki/ISAM)**-Struktur (Indexed Sequential Access
Method) — der klassische *statische, sortierte, blockstrukturierte* Index, der
genau die richtige Form für eine unveränderliche Generation ist: Es gibt keine
Einfügungen, die neu ausbalanciert werden müssten, sodass die Einfachheit von ISAM
das erkauft, was die Mutationsmechanik eines B-Baums nur verkomplizieren würde.
* Einträge `(value, entity_id)` sind nach Wert sortiert und in dieselben
zstd-komprimierten 256-KiB-Blöcke gepackt wie alles andere.
* Eine kleine **residente oberste Ebene** hält den ersten Schlüssel jedes Blocks
(ein Sparse-Index). Eine Suche binärsucht diese In-Memory-Oberebene, um den
*einen* Block zu finden, in dem ein Schlüssel sein kann, liest + dekomprimiert
diesen Block und scannt ihn — eine Gleichheitssuche ist also **ein Block-Lesen**,
und ein Bereichsscan läuft durch den zusammenhängenden Blocklauf, den er
überspannt. (Deshalb ist eine `meshUi`-indizierte Suche im einstelligen
Millisekundenbereich, während derselbe Match auf einer nicht indizierten
Eigenschaft das gesamte Label scannt.)
* Der Planner wählt ihn über `NodeScan::RangeEq` / `RangeRange`; ein nicht
indiziertes Prädikat fällt auf einen Label-Sweep oder Vollscan zurück, wobei der
Executor jedes Prädikat in beiden Fällen erneut prüft.
### Vektorsuche (Vamana + PQ) — Kosinus, L2 und Punktprodukt, Lesen *und* Schreiben
Vektor-KNN (`db.idx.vector.queryNodes`) läuft über **Kosinus-, L2- oder
Punktprodukt- (MIPS)**-Indizes. Der Basisindex wird offline mit zwei
Ausführungspfaden erstellt, die pro Index durch den `--ann-threshold` (Standard
50 000 Vektoren) gewählt werden:
* **Unterhalb des Schwellenwerts — Brute Force.** Die vollständigen `f32`-Vektoren
liegen in `vectors.f32.blk`; eine Abfrage scannt die Gruppe des Index und
berechnet die exakte Distanz in der Metrik des Index. Einfach und exakt; in
Ordnung, wenn der Vektorsatz klein ist.
* **Bei oder oberhalb des Schwellenwerts — Vamana + PQ**, der
disk-native-ANN-Pfad, der den residenten Speicher unabhängig von der Anzahl der
Vektoren begrenzt hält:
* **[Vamana](https://arxiv.org/pdf/2401.11324)** ist der Graphindex aus der
DiskANN-Arbeitslinie: ein einzelner Näherungsgraph, dessen Kanten beschnitten
sind (der `--vamana-r`-Ausgangsgrad und der `--vamana-alpha`-Langfaktor), sodass
eine *gierige Beam-Suche* — Start am Medoid, wiederholtes Springen in Richtung
der Abfrage, wobei eine Kandidatenliste der Breite `vectorQuery.beamWidth`
gehalten wird — die wahren Nachbarn eines Knotens in wenigen Sprüngen erreicht,
d. h. **wenige zufällige Block-Lesevorgänge pro Abfrage**. Die Graphblöcke
(`vector/<label>.<prop>.vamana`) werden über den Vektor-Cache eingepagert,
nicht vollständig gehalten.
* **[Produktquantisierung (PQ)](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)**
komprimiert jeden Vektor in einen kurzen Code (`--pq-subspaces` × `--pq-bits`):
Die Dimensionen werden in Unterräume aufgeteilt, jeder wird unabhängig
k-means-geclustert, und der Vektor wird als Tupel der nächsten
Zentroid-IDs gespeichert. Diese Codes (`vector/<label>.<prop>.pq`) sind klein
genug, um **resident** zu bleiben, sodass die Beam-Suche Kandidaten aus dem RAM
bewertet und nur die ausgewählten wenigen vollständigen Vektoren von der Platte
gelesen werden. Diese residente PQ-Menge ist es, die der
`cache.vectorCacheBytes`-Pool pinnt.
**Beschreibbare Einbettungen — die Vektor-Schreibleiter (im
[FreshDiskANN](https://arxiv.org/abs/2105.09613)-Stil).** Eine indizierte
Einbettung ist ein erstklassiger beschreibbarer Wert. `SET n.embedding = vecf32([…])`
(und `REMOVE`) landet im Schreib-Delta und ist **sofort KNN-sichtbar mit exaktem
Rang**, überlebt dann einen Segment-Flush, einen Merge und eine Konsolidierung.
Eine Abfrage führt bis zu drei Ebenen zusammen — den versiegelten Basisindex, einen
versiegelten Pro-Segment-Index und einen In-Memory-**RW-Index** (ein lebendiges
mutierbares Vamana über dem Schreib-Delta) — sodass die Latenz flach bleibt, wenn
Schreibvorgänge sich ansammeln, statt mit der Anzahl ausstehender Schreibvorgänge
zu wachsen. Ein Löschen hinterlässt ein *Loch*: Der Knoten wird nicht mehr
zurückgegeben, bleibt aber ein Navigations-Wegpunkt, bis eine Hintergrund-
**Lösch-Konsolidierung** ihn aus dem Graph herausspleißt, sodass Löschvorgänge
keine Abfrage-IO mehr kosten. Und weil der On-Disk-Graph seine Nachbarn über die
Layout-Position statt über die Knoten-ID adressiert, trägt
`CALL slater.consolidate()` das Vamana **per Referenz** — hart-verlinkt,
byte-identisch — und schreibt nur eine kleine ID-Spalte neu, wodurch
Vektor-Schreibvorgänge **ohne** den O(N·R·L)-Graph-Rebuild in die Basis gefaltet
werden. Gemessene Zahlen, mit Einschränkungen, stehen im
[Leistungsbericht](https://github.com/hikari-systems/slater/blob/main/docs/PERF-REPORT.md).
## Storage-Backends (Dateisystem / S3 / GCS)
Jede Generationsdatei wird über eine **`ObjectStore`**-Abstraktion geöffnet und
nicht direkt über `std::fs`, sodass *dasselbe* On-Disk-Byteformat — Blöcke,
Indizes, Manifest, `current`-Zeiger — unverändert von jedem Backend bedient wird;
nur *woher die Bytes kommen* unterscheidet sich, niemals die Leser, die
Abfrage-Engine oder die Integritätsprüfungen. Der heiße Pfad sind positionelle
Lesevorgänge (`read_exact_at`), die auf ein `pread` auf einer lokalen Datei und
eine HTTP-Byte-Range-Anfrage auf einem Objekt-Store abgebildet werden — Slater
verwendet nie mmap, sodass das explizite, begrenzte Lesemodell überall identisch
ist.
**Drei erstklassige Backends**, ausgewählt durch `dataBackend.kind`. Das
Dateisystem ist die einfache Standardoption; **Amazon S3 und Google Cloud Storage
sind gleichwertige, vollständig unterstützte Objekt-Store-Backends** — das
veröffentlichte Image wird mit beiden kompiliert ausgeliefert, sodass jedes nur
Konfiguration ist, und eine einmal erstellte Generation kann von jedem von ihnen
bedient werden (sogar migriert `fs` → S3 → GCS) ohne Neubuild.
| `dataBackend.kind` | Positionelles Lesen | Integrität beim Öffnen | Anmeldedaten |
| --- | --- | --- | --- |
| `fs` *(Standard)* | `pread` | vollständiger BLAKE3-Re-Hash jeder Datei | — |
| `s3` | HTTP `Range` GET | Server-**SHA-256** über `HEAD` (→ BLAKE3-Body-Re-Hash, falls nicht vorhanden) | Konfig-Schlüssel, AWS-Kette oder IAM-Rolle |
| `gcs` | HTTP-Bereichslesen | Server-**CRC32C** über `get_object` (→ BLAKE3-Body-Re-Hash, falls nicht vorhanden) | ADC / Workload Identity oder Service-Account-JSON |
Beide Objekt-Stores verifizieren die Integrität über die **Prüfsumme, die der Store
bereits berechnet und aufbewahrt**, abgerufen als Objektmetadaten: `slater-build`
sendet die Prüfsumme beim Upload (der Store validiert die Bytes dagegen und
speichert sie), und der Server liest sie beim Öffnen zurück und vergleicht sie mit
dem Manifest — eine Metadatenanfrage pro Datei, kein Body-Download. Sie ist
inhaltsgradig und im Geiste identisch über S3 (SHA-256) und GCS (CRC32C). Wenn ein
Objekt **keine** serverseitig gespeicherte Prüfsumme trägt (out-of-band kopiert
oder mit einem anderen Standard hochgeladen), **re-hasht der Server den Objekt-Body
gegen das Manifest-BLAKE3**, statt seiner Bytelänge zu vertrauen — eine angeforderte
Integritätsprüfung wird nie stillschweigend zu einem Größenvergleich herabgestuft.
Von Slater veröffentlichte Generationen tragen immer die Prüfsumme, sodass sie auf
dem günstigen Metadatenpfad bleiben.
Was diese Spalte auf jedem Backend prüft, ist, dass die Dateien **mit dem Manifest
übereinstimmen**. Ob das Manifest selbst vertrauenswürdig ist, ist eine separate
Frage, und der Master-Schlüssel ist es, der sie beantwortet: Mit einem
konfigurierten Schlüssel trägt das Manifest einen Schlüssel-MAC, den der Server
verifiziert, bevor er irgendeinem Feld vertraut (einschließlich dieser Hashes),
sodass ein umgeschriebenes Manifest, das manipulierte Dateien beschreibt,
abgelehnt wird; ohne Schlüssel ist der Vergleich durchgehend ungeschlüsselt, und
jemand, der in das Datenverzeichnis schreiben kann, kann eine Datei und das
Manifest zusammen umschreiben. Siehe
[Was Integrität in jeder Konfiguration bedeutet](https://github.com/hikari-systems/slater/blob/main/THREAT_MODEL.md#what-integrity-means-in-each-configuration).
Die Prüfung selbst kann mit `dataBackend.verifyIntegrity: false` deaktiviert
werden, was sie gegen ein schnelleres Öffnen eintauscht.
### Dateisystem (`fs`)
Der Standard, verwurzelt in `dataBackend.fs.dir`. Die richtige Wahl für die meisten
Bereitstellungen: eine Generation auf einer lokalen SSD (oder einem NFS/EBS-Mount),
die schreibgeschützt bedient wird. Integrität ist ein vollständiger BLAKE3-Re-Hash
jeder Datei beim Öffnen.
### Amazon S3 (`s3`)
Ein S3- oder S3-kompatibler Bucket (AWS, MinIO, localstack). Anmeldedaten kommen
**zuerst** aus der Konfiguration (`dataBackend.s3.awsAccessKey` / `awsSecretKey`,
plus `awsSessionToken` für temporäre STS-Anmeldedaten) und fallen auf die
Standard-AWS-Kette (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`-Env,
gemeinsames Profil oder Instanz-/IRSA-Rolle) zurück, wenn sie leer gelassen werden.```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)Ein GCS-Bucket, der über die JSON-API erreicht wird. Die Autorisierung erfolgt GCP-nativ: Standardmäßig werden
Application Default Credentials aufgelöst — GKE Workload Identity, der GCE-
Metadatenserver oder ein gcloud- / GOOGLE_APPLICATION_CREDENTIALS-Schlüssel. Setze
dataBackend.gcs.credentialsPath (eine Service-Account-JSON-Schlüsseldatei) oder inline
credentialsJson für einen expliziten Schlüssel. dataBackend.gcs.endpoint zeigt auf einen
fake-gcs-server-Emulator, und dataBackend.gcs.anonymous=true ermöglicht
unauthentifizierten Zugriff nur für diesen Emulator — niemals gegen echtes GCS.```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
In allen Fällen schreibt slater-build die fertige Generation zuerst in --data-dir
(seinen lokalen Staging-Bereich) und lädt sie zusätzlich in den Bucket hoch; der
entfernte current-Zeiger wird zuletzt geschrieben, sodass ein Serving-Knoten nie eine
halb veröffentlichte Generation sieht.
Greifen Sie zu s3 oder gcs, wenn Sie Generationen in dauerhaftem, zentralem Objekt-
Speicher statt auf dem Datenträger eines Knotens wünschen — typischerweise: einmal
veröffentlichen und an viele zustandslose, plattenlose Server-Replikate verteilen, die
alle denselben Bucket lesen; den Build-Host von den Serve-Hosts entkoppeln; oder sich
auf die Haltbarkeit/Versionierung/Lebenszyklus des Stores verlassen, statt Volumes zu
verwalten. Der Kompromiss ist Latenz: Ein kalter Block ist ein Netzwerk-Roundtrip
(~10–50 ms) statt eines lokalen Lesevorgangs (~0,1 ms). Slater verdeckt den Großteil
davon mit dem In-Memory-Block-Cache, konkurrierendem Read-Ahead und dem optionalen
Disk-Cache darunter. Wenn Ihre Generationen bereits auf schnellem lokalem Speicher
liegen und Sie das zentrale Bucket-Modell nicht benötigen, ist fs einfacher und
schneller.
Der In-Memory-BlockCache ist bewusst klein gehalten (begrenzter RSS ist das
Hauptversprechen), sodass bei einem Arbeitsdatensatz, der größer als der RAM ist,
dieselben Blöcke bei jedem Spill erneut aus dem Objekt-Store geholt würden. Ein
optionaler lokaler SSD-Zweit-Cache behebt das: Ein aus dem RAM verdrängter Block
wird vom lokalen Datenträger (~0,1 ms) statt über einen frischen Objekt-GET bedient,
übersteht die In-Memory-Verdrängung und reduziert Anzahl/Kosten der Objekt-Store-
Anfragen — und bringt einen Objekt-Store-gestützten Knoten nach dem Aufwärmen nahe an
die Leistung eines lokalen Dateisystems. Er ist für beide s3 und gcs opt-in
und wird durch Setzen von dataBackend.<s3|gcs>.diskCacheBytes > 0 und ein
beschreibbares diskCacheDir aktiviert.
--encrypt-Generationen) weiterhin AEAD-versiegelt — unterhalb von
Entschlüsselung/Dekomprimierung. Die Cache-Ebene hält nie den Verschlüsselungsschlüssel
und verschlüsselt nie erneut, sodass der Ruhezustand kostenlos erhalten bleibt: Eine
verschlüsselte Generation landet weiterhin versiegelt auf dem Datenträger.diskCacheDir muss auf ein echtes beschreibbares Volume zeigen — niemals tmpfs
(tmpfs ist RAM und würde die begrenzte-RSS-Garantie zunichtemachen). Der In-Memory-
Index, der ihn verfolgt, kostet etwas RAM (~zig Bytes pro gecachtem Block), was gegen
Ihre RSS-Obergrenze zählt — dimensionieren Sie das Verzeichnis ≫ den In-Memory-Block-
Cache.blockCacheBytes / 8
begrenzt (durch diskCacheBytes nach unten begrenzt) — 8 MiB beim Standardwert — und
verwirft statt zu wachsen, sodass ein kalter Scan sie nicht aufblähen kann; ein
verworfener Block wird einfach beim nächsten Fehlgriff erneut abgerufen. Sie benötigt
keine Konfiguration: Sie skaliert mit blockCacheBytes, sodass die Datenträger-Ebene
dem RSS-Budget außer ihrem Index keine neue Zahl hinzufügt.Ein Read-Replica läuft mit einem schreibgeschützten Root-Dateisystem und einem
Nicht-Root-Benutzer (appuser:1000) — alles, was es benötigt, ist schreibgeschützt
gemountet. Ein Writer (delta.enabled) benötigt zusätzlich ein dauerhaftes,
beschreibbares Volume für sein WAL.
Die Konfiguration wird vom hausüblichen geschichteten Loader geladen: das eingebackene
config.json, dann /sandbox/config.json tief darüber gemergt, dann KEY__sub-
Umgebungs-Overrides (doppelter Unterstrich für Verschachtelung; Schlüssel entsprechen
der camelCase-Konfiguration).
Jeder Konfigurationsknopf — sein camelCase-Schlüssel, das KEY__sub-Umgebungs-Override,
sein Standardwert und was er tut — ist in der Konfigurationsreferenz tabellarisch aufgeführt. Die am häufigsten
abgestimmten Knöpfe sind die Cache-Budgets (cache.*), die Abfrage-Guards (query.*),
die Verbindungs-Obergrenzen (server.*), das Storage-Backend (dataBackend.*) und die
beschreibbare Ebene (delta.*).
Residenter Speicher verfolgt
blockCacheBytes + vectorCacheBytes + resultCacheBytes bis auf begrenzten
Pro-Eintrag- und Allokator-Overhead — jeder Pool wiegt seinen eigenen Inhalt (Strings
und Container nach allokierter Kapazität) und verdrängt, um unter dem Budget zu bleiben,
aber die Pro-Eintrag-Buchhaltung und die Größenklassen-Rundung des Allokators sitzen
obendrauf auf der von Ihnen gesetzten Zahl — plus ein kleiner fester Overhead (und bis zu
degreeColumnBytes für die lazy-Grad-Spalte, sobald der Grad-Summen-count(endpoint)-
Schnellpfad ausgeführt wird). Er ist unabhängig von der Graphgröße — das ist das
Hauptversprechen, das vom Integrationstest rss_stays_bounded_under_sustained_knn_load
ausgeführt wird, der das Peak-vs.-Warm-RSS-Wachstum gut innerhalb der summierten Budgets
hält. Pro-Verbindungs-Puffer liegen außerhalb der Cache-Budgets, sodass die Garantie
unter widriger Last nur gilt, weil server.maxConnections begrenzt, wie viele gleichzeitig
existieren können.
Slater ist ein Read-Replica-Handle; die primäre Verbindungssicherheitskontrolle ist
das Netzwerk, nicht das Binary. Binden Sie es an eine private Schnittstelle, beschränken
Sie Quellbereiche auf der Netzwerkebene (Security Groups / NetworkPolicy) und — wenn es
etwas anderem als vertrauenswürdigen Clients gegenübersteht — stellen Sie ihm einen
verbindungsbegrenzenden L4-Proxy voran (HAProxy maxconn + eine
Pro-Quelle-stick-table, oder nftables connlimit + hashlimit). Das sitzt vor dem
Dateideskriptor, der jemals an den Prozess übergeben wird, und ist daher die robusteste
Begrenzung.
Die oben genannten In-Binary-Begrenzungen (maxConnections, maxPreAuthConnections,
maxConnectionsPerIp, die differenziellen Byte-Obergrenzen und loginTimeoutMs) sind
Defence-in-Depth: Sie sind standardmäßig aktiviert und großzügig, sodass sie für eine
legitime Client-Population unsichtbar sind, aber sie lassen die begrenzte-RSS-Garantie
auch dann gelten, wenn der Proxy vergessen wird. Siehe docs/HARDENING.md für die vollständige
defensive Posture und THREAT_MODEL.md / SECURITY_WORKLIST.md für die kanonischen
Details.
Slater pollt den current-Zeiger jedes Graphs alle generationPollMs
(Poll, nicht inotify — das Datenverzeichnis kann entfernt/Netzwerk-Speicher wie NFS
sein, wo Dateisystem-Änderungsereignisse unzuverlässig sind). Wenn er sich ändert:
reloadStrategy=exit (Standard): Der Server protokolliert fatal und beendet sich mit
Nicht-Null-Exitcode, sodass der Orchestrator ihn sauber gegen die neue Generation neu
startet.reloadStrategy=swap: Der Server öffnet und validiert die neue Generation
(derselbe Content-Hash-Guard wie beim Boot), tauscht sie atomar ein und lässt
laufende Abfragen auf der alten beenden. Ein beschädigtes/unvollständiges neues Image
wird abgelehnt und die alte Generation bedient weiter.acl.json ordnet Benutzern argon2id-Passwort-Hashes und Pro-Graph-read-/write-
Grants zu. Prägen Sie einen Hash (speichern Sie niemals Klartext) mit:```sh
slater hash-password 's3cret' # prints a $argon2id$… string for acl.json
A starter `acl.json` ships at the repo root; its shape is:```json
{
"users": {
"reporting": {
"passwordArgon2id": "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>",
"grants": {
"people": ["read"],
"products": ["read", "write"]
}
}
}
}
users — ein Eintrag pro Login, nach Benutzername verschlüsselt.
passwordArgon2id — der $argon2id$…-String aus slater hash-password
(niemals im Klartext; die Datei selbst ist einfaches JSON und liegt auf gemeinsamem Speicher).
grants — Fähigkeitslisten pro Graph. Zwei Berechtigungen sind von Bedeutung:
read — den Graph abfragen. Ein Graph, der in den Grants eines Benutzers fehlt, ist für ihn unsichtbar.write — den Graph über die beschreibbare Ebene (delta.enabled) verändern: die
MERGE / SET / DELETE-Anweisungen und CALL slater.consolidate().Mounten Sie sie schreibgeschützt unter dem Pfad, der durch aclPath benannt ist (Standard /config/acl.json).
Der Server lädt sie bei jedem Generations-Hot-Swap neu, und der ACL-Stempel im Ruhezustand wird
bei jedem Neuladen erneut geprüft (siehe requireAclStamp).
Die slater-Binärdatei dient zugleich als eigene Liveness-Sonde: slater healthcheck [host] [port] führt einen Bolt-Handshake (keine HTTP-Anfrage) gegen den Server durch und
beendet sich mit 0, wenn eine Protokollversion ausgehandelt wird, andernfalls mit 1 — standardmäßig
localhost und dem konfigurierten Bolt-Port. Das ist es, was der Container-
HEALTHCHECK ausführt, sodass Orchestratoren einen wirklich Bolt-bereiten Server sehen, nicht nur einen
offenen Socket:```sh
slater healthcheck localhost 7687 # exit 0 = healthy
docker exec slater /app/slater healthcheck # inside the container
## One-shot-Abfrage
Für Skripting, CI-Prüfungen und schnelle Nachschlagevorgänge mountet `slater query` die
aktuelle Generation eines Graphen, führt eine einzelne schreibgeschützte Cypher-Abfrage prozessintern aus, gibt das
Ergebnis als JSON-Objekt aus und beendet sich — ohne Server, ohne Bolt-Verbindung. Es berücksichtigt
dieselbe Konfiguration wie der Server (Speicher-Backend, Verschlüsselungsschlüssel, Abfragebudgets):```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"]]}
Knoten und Beziehungen werden zu ihren Labels/Typen und Eigenschaften erweitert. Verwenden Sie -q,
wenn Sie maschinenlesbare Ausgabe wünschen (das Ergebnis-JSON ist das einzige, was auf
stdout ausgegeben wird); lassen Sie es weg für einen operator-orientierten Lauf mit Logs. Ohne -q wird
nach jedem Lauf eine nur-Metriken-Zusammenfassung protokolliert — z. B.```text
INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10
carrying the query `cost` (elements charged), `resultCount`, `execMs`, and
`limitRowCount` (only when the query specifies a `LIMIT`) — never the query text
or any result value. Exit status is `0` on success, `1` on a parse/open/execute
error (message on stderr).
## Export a graph (`slater dump`)
`slater dump` exports a graph from a **running** server as business-key `MERGE`
Cypher — the same dialect `slater-build` ingests — so a graph round-trips
(dump → `slater-build` → new generation) for migration or text backup. Unlike
`slater query`, it connects over **Bolt**, authenticates, and honours per-graph
ACLs, so it needs no disk access to the server. The password is read from
`SLATER_DUMP_PASSWORD` or stdin (never a flag, keeping it out of `ps`/history).```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
Jeder Identitätsschlüssel eines Labels ist die Eigenschaft, die sein Range-Index trägt; überschreiben
mit --key Label=prop (wiederholbar) oder einem globalen --pk <Feld>. CREATE INDEX
DDL wird zuerst ausgegeben, damit der Rebuild die Indizes neu erstellt. Ein Multi-Label-Knoten
behält jedes Label — er wird als MERGE (n:Ident:Other {key: v}) ausgegeben, mit
dem Identitätslabel (dem, das den Geschäftsschlüssel liefert) zuerst und dem Rest
sortiert; der Merge ist nur auf das Identitätslabel gestützt, sodass die nachfolgenden Labels
auf den Knoten geschrieben werden, ohne einen weiteren zu erzeugen. Labels, Beziehungstypen
und Eigenschaftsschlüssel mit Sonderzeichen werden bei der Ausgabe in Backticks gesetzt,
damit ungewöhnliche Namen originalgetreu hin- und zurückübertragen werden und kein Cypher in den
Rebuild injiziert werden kann. Vektoren (und andere
Werte ohne Cypher-Literal-Schreibweise) können keinen MERGE-Dump nutzen und werden
mit einer Warnung auf stderr verworfen. Der Exit-Status ist 0 bei Erfolg, 1 bei Fehler.
Ein vollständiger, ausführbarer Durchlauf — einen Graphen erstellen, ihn bereitstellen, mit den neo4j JavaScript- und Python-Treibern verbinden und in ihn schreiben — befindet sich in den Seiten Quickstart und Daten schreiben des Handbuchs, unter Verwendung des gebündelten Beispielgraphen in 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
### Objekt-Store-Backends sind optionale Cargo-Features
Ein einfaches `cargo build` erzeugt ein **nur-Dateisystem**-Binary — die `s3`- und `gcs`-Backends
sind hinter Cargo-Features verborgen, sodass der Standard-Build klein bleibt (kein AWS-
oder Google-SDK, keine asynchrone Laufzeit). Aktiviere das jeweilige Backend, das du benötigst, auf **beiden** `slater`
(serve) und `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
Jede Crate stellt passende s3-/gcs-Features bereit, die an graph-format/{s3,gcs} weiterreichen. Wird ein Backend zur Laufzeit angefordert (dataBackend.kind=s3|gcs oder slater-build --publish-{s3,gcs}-*), ohne dass das Feature einkompiliert ist, schlägt dies schnell mit einer klaren „built without the … feature“-Fehlermeldung fehl. Das veröffentlichte Docker-Image aktiviert beide (Dockerfile CARGO_FEATURES), daher benötigen vorgefertigte Images keine zusätzlichen Flags – das ist nur beim Bauen aus dem Quellcode relevant. Die Integrationstests sind ebenso abgesichert: --features s3 --test s3_minio, --features gcs --test gcs_emulator (ein fake-gcs-server) und --features gcs --test gcs_real (echtes GCS über ADC); jeder wird übersprungen, sofern die jeweiligen SLATER_*-Umgebungsvariablen nicht gesetzt sind.
Siehe docs/PLAN.md, docs/PROGRESS.md und docs/DECISIONS.md für das Design, das Meilenstein-Register und das Entscheidungsprotokoll.
Bis zu sechs Engines, eine Single-Client-Suite, Graphen von einem 62k-Knoten-Spielzeug bis zu Wikidata 91,6 Mio. Knoten / 1,5 Mrd. Kanten. Jede Engine wird isoliert gemessen (jeder andere Container gestoppt – RSS und Latenz sind ihr eigener Fußabdruck). Die Latenztabellen unten wurden auf Slater 0.21.0 neu gemessen (der beschreibbare Build): die kleinen/mittleren Graphen (MeSH, EU-AI-Act) frisch und der 91,6-Mio.-Graph als frischer Same-Box-, Shared-Anchor- Slater-gegen-Neo4j-Durchlauf (siehe diese Tabelle). Die Resident-Memory-Werte werden aus dem früheren Durchlauf übernommen (gemessen über Container-Cgroup; der Lesepfad ist byte-identisch, während die beschreibbare Schicht im Leerlauf ist). Die Zahlen der anderen Engines stammen aus dem etablierten Cross-Engine-Lauf (ihre Versionen/Leistung sind unverändert). Alle Werte sind Mediane (ms) oder Spitzen-Resident-Speicher (MiB). Überall gilt: niedriger ist besser; fett = bester Wert in der Zeile. Slater wurde auf seinem lokalen Dateisystem- (fs) Backend ausgeführt; die S3- und GCS-Backends tauschen lokale Lese-Latenz gegen Objektstore-Roundtrips (abgemildert durch die In-Memory-Caches und die optionale lokale Disk-Cache-Ebene), daher charakterisieren diese Werte die Engine, nicht eine Netzwerk-Speicher-Bereitstellung.
Die drei Engines, die von der Platte auslagern – slater, Neo4j 5 und LadybugDB – laden alle fünf Graphen. Das In-Memory-Trio (Memgraph · FalkorDB · ArcadeDB) kann den 1,5-Mrd.-Kanten-Graphen überhaupt nicht halten (er benötigt ~64–128 GiB resident), und ArcadeDBs Importer kann ihn ebenfalls nicht fertigstellen.
Jede Zahl ist zugesagter Arbeitsspeicher – was das Betriebssystem nicht zurückfordern kann. Jede Engine außer slater hält ihren Graphen im zugesagten anonymen Speicher (eigener Heap, Neo4js Off-Heap-Page-Cache oder ein Buffer-Pool), daher ist ihr Spitzen-RSS ihr zugesagter Fußabdruck. Nur slater bedient aus dem zurückforderbaren OS-Page-Cache seines On-Disk-Stores, daher ist seine Zahl der anonyme Arbeitssatz; der Page-Cache des Stores (unter Druck entfernbar – slater bedient weiter) ist ausgeschlossen und für den 91,6-Mio.-Graphen als gesamt in Klammern angegeben. Fett = niedrigster Wert.
slater ist auf jeder Skala am niedrigsten und wächst ~50×, während der Graph ~1.500× wächst – sein Fußabdruck folgt dem Abfrage-Arbeitssatz, nicht dem Graphen (im Leerlauf durchgehend ~16–71 MiB). Das In-Memory-Trio wächst ~linear und kann den 1,5-Mrd.-Graphen nicht laden; Neo4j sagt einen ~2-GiB-Heap unabhängig von der Abfrage zu. († LadybugDB nur auf den begrenzten Formen – seine Hub-/var-length-/shortestPath-Traversalen bei 1,5 Mrd. Kanten benötigen einen auf ≥2 GiB erhöhten Lesepool, gegenüber slaters automatischer maxIntermediate-Begrenzung.) Die Build-Zeit-Wert→Zähler-Histogramme fügen vernachlässigbaren Resident-Speicher hinzu – ein paar KB für eine indizierte Spalte mit niedriger Kardinalität und null für Unique-Key-Graphen wie Wikidata (wikidata_id überschreitet die Kardinalitätsgrenze des Histogramms, daher wird keines gespeichert) – daher bleiben diese Werte durch dieses Feature unverändert.
slater besitzt die Metadaten-/Index-/Scan- Formen (count, label, idx-eq, scan – ~0,4 ms, 10–200× die Service-Engines), den indizierten Punkt-Lookup (0,43 ms, nun knapp vor den 0,48 ms des In-Memory-Paars), den unverankerten Multi-Hop (2-Hop 1,40 ms über den Beziehungstyp-Scan, am schnellsten im Feld) und – über ein Build-Zeit-Wert→Zähler-Histogramm auf dem indizierten Gruppierungsschlüssel – das Label-weite group-by / count(DISTINCT) (0,45 ms, vor LadybugDBs spaltenorientierten 5,3 ms). Die In-Memory-Server behalten nur rohen 1-Hop (Memgraph 1,21 ms gegenüber slaters 1,28 ms). (pole 62k/106k sieht gleich aus: slater allein am schnellsten bei count/scan ~0,4 ms, ~1,3–2,6 ms bei Hops.)
slater beantwortet kNN mit einem exakten Brute-Force- Scan (diese Mengen liegen unter seiner 50k-Vektor-ANN-Schwelle), während die anderen ein approximatives residentes HNSW verwenden – daher sind slaters Ergebnisse exakt (Recall 1,0). Ein SIMD-Distanz-Kernel plus eine residente, vor-normalisierte Vektormatrix brachte Concept von ~23 → ~2,9 ms und Chunk von ~10 → ~2,4 ms, sodass slater nun Neo4j und LadybugDB schlägt und innerhalb von ~1,4× von Memgraph liegt, nur hinter FalkorDB – und dabei exakt.
Die obigen Tabellen sind Cross-Engine-Lese-Vergleiche. Der Vektor-Schreib-Pfad (die FreshDiskANN-artige Schreib-Leiter über der statischen Vamana-Basis) hat kein Cross-Engine-Pendant – keine andere Engine hier macht disk-natives, beschreibbares ANN – daher sind die Zahlen unten Single-Engine-Komponenten-Benchmarks über einer synthetischen, embedding-artigen Fixture (eine Low-Rank-Mannigfaltigkeit, dim 768, ungleiche Normen), festgehalten unter crates/slater/benches/ und vollständig – mit Methodik und allen Einschränkungen – in docs/PERF-REPORT.md beschrieben. Recall wird immer gegen eine exakte Brute Force über die Live-Menge gemessen, nie ein Index gegen einen anderen. Die Skala hier ist repräsentativ und nur dort extrapoliert, wo die Metrik größenlinear ist.
Die eine Zahl, die die dedizierte Leistungsbox verdient, ist der Slow-Path-Konsolidierungs-Rewrite-Durchsatz – wenn eine Konsolidierung Löschungen oder neue Vektoren trägt statt einer reinen Permutation, ist es eine sequenzielle Rekompression, begrenzt durch Single-Thread-zstd und lokale Platte, daher ist das absolute MiB/s umgebungsspezifisch (der Bericht zeigt die Form und erklärt die Umgebungsrange).
Die In-Memory-Engines (Memgraph / FalkorDB / ArcadeDB) können diesen Graphen überhaupt nicht laden (~64–128 GiB resident). Nur slater und Neo4j 5 schaffen das. Dies ist ein frischer Same-Box-, Same-Day-Durchlauf gegen einen gemeinsamen, festen Anker-Satz – jede Abfrage trifft auf beiden Engines die identischen Knoten, daher ist der direkte Vergleich Äpfel-mit-Äpfeln (ein gemeinsamer wikidata_id-Pool aus Ankern mit moderatem Grad; siehe die Anmerkung unten, warum das wichtig ist). slater wird bei beiden Fanouts gezeigt (query.maxFanout 1 = Durchsatz-Standard, 8 = der Latenz-Regler, der kalte Block-Reads überlappt). Fett = bester Wert in der Zeile.
Das ehrliche Bild: slater dominiert die Metadaten-/Index-Formen – count(*) wird über Metadaten bedient (0,41 ms gegenüber Neo4js 3,6-s-Disk-Scan, ~8800×), und Punkt-Lookup / Grad / 3-Hop laufen ~2–10× schneller – ist bei 1–2-Hop mit Neo4j gleichauf (Fanout 8 zieht bei kalten Reads voraus), verliert aber var-length *1..2 distinct deutlich (≈1 s gegenüber Neo4js 47 ms): slaters variable-length-distinct-Expansion ist hier materiell langsamer, eine echte Schwäche, die eine eigene Untersuchung verdient. All das bei ein paar hundert MB RSS gegenüber Neo4js zugesagtem ~2-GiB-Heap.
Zu den Ankern. Diese Traversal-Zahlen hängen stark davon ab, von welchen Knoten man startet – ein Knoten eine Verbindung von einem Wikidata-Mega-Hub („human“, „country“) hat eine Millionen-starke 2-Hop-Nachbarschaft, daher schwanken var-length/Hop-Kosten um Größenordnungen mit der Ankerwahl. Die frühere Ausgabe dieser Tabelle sampelte die eigene „ersten N per Scan“ jeder Engine, was weder stabil noch vergleichbar ist; dieser Durchlauf fixiert einen einzigen gemeinsamen, grad-begrenzten Anker-Satz für beide Engines. (shortestPath ist in diesem Durchlauf weggelassen – zwischen zwei beliebigen Ankern ist er pfad-existenzabhängig und zu stark schwankend, um sinnvoll einen Median zu bilden.)
count(*) – Speicher vom Ergebnisumfang entkoppeltUnbegrenztes Multi-Hop RETURN count(*) zählt während der Expansion, statt die übereinstimmenden Zeilen zu materialisieren. Gleiche Hub-Anker auf dem 91,6-Mio.-Graphen, maxIntermediate=20M:
| 3-Hop count(*) @ 91,6 Mio. | fanout=1 | fanout=8 |
|---|---|---|
| Latenz / Spitzen-Arbeitssatz | 554 ms / 0,66 GiB | 298 ms / 1,9 GiB |
Die Zählung hält O(1) Zeilen. Die Abrechnung ist unverändert, daher stößt eine Mega-Hub-Zählung weiterhin maxIntermediate auf Compute (Adjazenz-Reads), wie zuvor begrenzt.
maxFanout)Das Erhöhen von query.maxFanout überlappt die kalten, I/O-gebundenen Block-Reads einer Abfrage über Kerne – es hilft bei plattengebundenen Formen mit großem kaltem Arbeitssatz und ist bei warmen Formen flach. Auf dem 1,5-Mrd.-Graphen: shortestPath ≤6 918 → 608 ms (1,5×, größte Suche 6.269 → 2.350 ms, 2,7×); 3-Hop-Zählung 547 → 298 ms. maxFanout=1 ist der Standard (durchsatzorientiert); 8 ist der Latenz-Regler, bei mehr transientem Worker-Speicher.
Vollständige Per-Engine-Tabellen (pole, MeSH, EU-AI-Act + der blockCacheBytes-RAM↔Latenz-Regler, Wikidata 1M & 91,6 Mio.) finden sich in perf/cross-engine-hs/README.md; der frische Slater-only-Durchlauf (beide Fanouts, jeder Datensatz) in perf/PERF_CURRENT_STATUS.md.
Die obigen Benchmarks sind Single-Client. Die komplementäre Achse – Verhalten unter vielen gleichzeitigen Clients – hat ihre eigene Testumgebung, perf/loadtest/: ein Locust-Treiber über Bolt plus ein Koordinator, der die Last hochfährt, CALL slater.diagnostics() liest, das Kapazitäts-Knie findet und den Begrenzer benennt (vollständige Methode in docs/LOAD-TESTING.md). Schlagzeilen aus einem Lauf mit 256-MiB-Cache auf dem Wikidata-1M-Graphen (eine 16-Kern-Box):
Beide Speicherprobleme, die der Lasttest aufdeckte, sind nun behoben; alles im Lasttest-Dokument nachverfolgt.
Lizenziert unter der Apache License, Version 2.0. Siehe LICENSE für den vollständigen Text und NOTICE für die Namensnennung. Sofern Sie nicht ausdrücklich etwas anderes angeben, wird jeder Beitrag, der absichtlich zur Aufnahme in dieses Werk eingereicht wird, wie in der Apache-2.0-Lizenz definiert, wie oben lizenziert, ohne zusätzliche Bedingungen.
SPDX-Lizenzkennung: Apache-2.0
REMOVEINSERTSETREMOVEDELETE| Funktion | Was sie für Sie bedeutet |
|---|
| Begrenzter, vorhersehbarer Speicher | Residenter Speicher folgt drei Cache-Budgets, die Sie festlegen, innerhalb begrenzter Pro-Eintrag- und Allokator-Overheads — er wächst nicht mit der Graphgröße; Sie stimmen den Leistungs-/RAM-Kompromiss ab, statt für den gesamten Graphen zu provisionieren. Ein jemalloc-Allokator mit Hintergrund-Purge gibt freigegebenen Speicher nach schweren Abfrage-Bursts an das OS zurück, sodass die residente Größe auf ihren Leerlauf-Boden zurückfällt, statt am Post-Burst-Hochwasserstand festzunageln. |
| Multi-Tenant out of the box | Ein Server hostet viele Graphen mit Leseberechtigungen pro Benutzer — Multi-Datenbank-Isolation, die die meisten Graph-DBs für eine kostenpflichtige/Enterprise-Stufe reservieren. |
| Verschlüsselung im Ruhezustand & in Transit | Pro-Block-XChaCha20-Poly1305-Versiegelung (der Schlüssel wird nie auf die Festplatte geschrieben) plus optionales TLS (bolt+s://). Von Natur aus DSGVO-freundlich. Verschlüsselung ist auch das, was authentifizierte Integrität kauft: Der Builder versiegelt das Manifest mit einem keyed MAC, und ein Server, der den Schlüssel hält, verifiziert es und weigert sich, eine Generation zu bedienen, deren Manifest gefälscht, verändert oder dessen MAC entfernt wurde. Ein unverschlüsseltes (Klartext-)Image ist nur durch den unverschlüsselten Content-Hash geschützt — Vollständigkeit und Korruption, nicht Manipulation. Siehe Was Integrität in jeder Konfiguration bedeutet. |
| Winzige Installation | Ein kleines gestripptes Binary auf einer distroless-glibc-Basis (keine Shell/apt) — das Multi-Arch (amd64/arm64)-Image zieht ~22 MB, oder ~12 MB für den Server-only-slater:latest-lite-Tag; reines Rust-TLS, kein OpenSSL. Ziehen und ausführen. |
| Gebaut für periodisches Publizieren | Bauen Sie einen Graphen offline, bedienen Sie ihn unveränderlich, dann tauschen Sie atomar eine neue Version mit null Ausfallzeit ein — ideal für Data-Warehouse-/geplante-Refresh-Workloads. |
| Robust unter Last | Server und Offline-Builder kompilieren beide mit #![forbid(unsafe_code)] — das einzige unsafe der Engine lebt in der geprüften jemalloc-Allokator-Crate. Der Kern ist unveränderlich, sodass Lesevorgänge keine Sperren nehmen und nie auf einen Schreiber warten; ein einzelner Schreiber serialisiert Mutationen hinter dem Schreibpfad allein. Keine GC-Pausen, keine Datenrennen. Eine schlechte Abfrage kann den Server nicht herunterfahren. |
| Funktioniert mit Ihren neo4j-Tools | Spricht Bolt 5.4 / 4.4 / 4.1 — verwenden Sie die Standard-neo4j-Treiber (JS, Python, Go, Java…), cypher-shell oder Graph-Browser unverändert. |
| Reiche Cypher-Abfragefläche | Eine breite Leseoberfläche: MATCH/WHERE/WITH/UNION, CALL {…}-Subqueries, 70+ Funktionen & Aggregationen, temporale & geospatiale Werte und Regex. |
| Live, dauerhafte Schreibvorgänge | Eine optionale Single-Writer-LSM-Schicht über dem unveränderlichen Kern (delta.enabled): Business-Key-MERGE / SET / DELETE / CREATE / REMOVE über Knoten und Beziehungen, gebatchtes Schreib-UNWIND (ein fsync pro Batch) und CALL slater.consolidate() — gruppen-committet, fsync-dauerhaft und durch Konsolidierung in einen frischen Kern zurückgefaltet. Der Lesepfad ist byte-identisch, wenn das Delta leer ist. |
| ISO GQL, Lesen und Schreiben | Spricht eine Teilmenge von ISO GQL (ISO/IEC 39075) über dieselbe Bolt-Verbindung — quantifizierte Pfade, Pfad-Restriktoren, Shortest-Path-Selektoren, Label/Typ-Boolesche-Ausdrücke, FOR, CAST, ein optionales GQL/CYPHER-Dialekt-Präfix — und, mit aktivierter beschreibbarer Schicht, GQLs Datenmodifikationsanweisungen (INSERT / SET / REMOVE / [DETACH] DELETE), die auf denselben dauerhaften Schreibpfad herunterbrechen. Cypher und GQL, Lesen und Schreiben, in einer Engine. |
| Vektoren + Graph in einer Engine | Disk-nativer ANN-Vektor-Search (Vamana + PQ; Cosinus / L2 / Dot) für Embeddings/RAG, plus Graph-Algorithmen (PageRank, BFS, Betweenness, WCC…) — begrenzter Speicher selbst mit Millionen von Vektoren. Embeddings sind beschreibbar (eine FreshDiskANN-artige Schreibleiter): Vektor einfügen / aktualisieren / löschen, sofort KNN-sichtbar, ohne Neubau in die Basis gefaltet. |
| Sicher auf Netzwerkspeicher | Jede Datei ist BLAKE3-content-gehasht und beim Öffnen verifiziert; zerrissene oder halb kopierte Images werden verweigert, nicht bedient. Entwickelt für NFS/Remote-Volumes (keine mmap-Überraschungen). |
| Plugbare Speicher-Backends | Bedienen Sie dasselbe Generationsformat von einem lokalen Dateisystem, einem S3 (S3-kompatiblen) Bucket oder einem Google-Cloud-Storage-Bucket — einmal publizieren, auf zustandslose Repliken ausrollen — mit einer optionalen lokalen SSD-Cache-Stufe vor dem Objekt-Store. Siehe Speicher-Backends. |
| Pfad | Zweck | Hinweise |
|---|
/data | Die Graph-Generationen (<graph>/<uuid>/… + current). | Schreibgeschützt für Replikate; von slater-build erzeugt. Kann auf entferntem/Netzwerk-Speicher (z. B. NFS) liegen, daher werden Lesevorgänge nicht als schnelle lokale SSD-Latenzen angenommen. |
/sandbox | Pro-Umgebung-Konfigurations-Overlay + Geheimnisse. | /sandbox/config.json wird tief über das eingebackene config.json gemergt; enthält auch acl.json, TLS-PEM-Material, die At-Rest-Schlüsseldatei. |
/tmp, /run | Scratch (tmpfs). | Ein Read-Replica schreibt standardmäßig nie auf den Datenträger. |
(Writer) delta.walDir | Das Write-Ahead-Log + L0-Delta-Segmente, wenn delta.enabled. | Beschreibbar und ein dauerhaftes, echtes Volume — niemals tmpfs (es ist die Haltbarkeits-Untergrenze). Ein relativer Pfad wird unter dem Datenverzeichnis aufgelöst; geben Sie einem Writer hier ein eigenes persistentes Volume. |
| (optional) Disk-Cache | Der lokale Disk-Block-Cache, wenn dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0. | Beschreibbar und ein echtes Volume — nicht tmpfs. Von den s3- und gcs-Backends verwendet; siehe Storage-Backends. |
Sie sind unabhängig: eine read-Berechtigung gewährt keinen Schreibzugriff. Das Aktivieren der
beschreibbaren Ebene kann daher Ihre bestehenden Leser nicht zu Schreibern machen. Ein Schreiber benötigt
beides — ["read", "write"] — denn das Auflösen eines Geschäftsschlüssels zum Schreiben ist ein Lesevorgang.
Nicht erkannte Berechtigungszeichenfolgen werden ignoriert (sie gewähren nichts).
| Engine | Klasse | Speichergrenze |
|---|
| slater | diskgestützt, ausgelagert | query.maxIntermediate begrenzt den Arbeitssatz automatisch |
| Neo4j 5 | diskgestützt, JVM | ~2 GiB Heap + Off-Heap, unabhängig von der Abfrage zugesagt |
| Memgraph · FalkorDB | In-Memory | gesamter Graph im RAM |
| ArcadeDB | In-Memory, JVM | gesamter Graph resident; am schwersten |
| LadybugDB | eingebettet, spaltenorientiert | manueller Buffer-Pool, der die Abfrage übertreffen muss |
| Graph (Knoten / Kanten) | 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,6 Mio. / 1,5 Mrd. | 584 (4.595 gesamt) | ~2.900 | kann nicht laden | kann nicht laden | kann nicht laden | ~652 † |
| Form | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| count(*) alle Knoten | 0,41 | 15,0 | 23,8 | 16,4 | 82,0 | 2,2 |
| Label-Zählung | 0,42 | 4,2 | 20,7 | 1,1 | 4,4 | 4,3 |
| indizierter Punkt-Lookup | 0,43 | 3,9 | 0,48 | 0,48 | 0,65 | 8,8 |
| idx-eq-Zählung | 0,42 | 4,9 | 5,0 | 2,0 | 381 | 2,5 |
| 1-Hop (indizierter Anker) | 1,28 | 5,8 | 1,21 | 4,1 | 390 | 4,9 |
| 2-Hop (unverankert) | 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 |
Vollscan CONTAINS | 0,43 | 5,4 | 24,1 | 1,7 | 16,3 | 4,1 |
| Form | 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 |
| Eigenschaft | gemessen | warum es wichtig ist |
|---|
| KNN-Latenz vs. ausstehende Schreibvorgänge | RW-Index ~1,5–2 ms, flach bis 50k ausstehend; die vor-indizierte Brute-Force-Overlay 1,9 → 115 ms (linear im Delta) – 61× bei 50k | Abfragelatenz verschlechtert sich nicht, während sich Schreibvorgänge zwischen Konsolidierungen ansammeln |
| Embedding-Einfügung | ~1,5–2 ms pro Vektor in den Live-Index | ein Schreibvorgang ist sofort KNN-sichtbar; das Delta-Rebuild-Budget beträgt ≈ 2 ms × die Delta-Grenze |
| Lösch-IO bei Iso-Recall | 2,9× weniger Knoten-Abrufe pro Abfrage bei 67 % gelöscht, 5,2× bei 80 % (Recall ≥ 0,90) | ein konsolidierter Graph zahlt keine Lesesteuer für gelöschte Vektoren |
| Konsolidierung, reine Permutation | O(1) – die .vamana ist hart-verlinkt byte-identisch, nur die ID-Spalte wird neu geschrieben | das Falten von Vektor-Schreibvorgängen in die Basis überspringt den O(N·R·L)-Rebuild |
| Recall über die Leiter | konsolidiert ≥ Basis für Kosinus, L2 und Punktprodukt | die Schreib-Leiter erhält den Recall auf jeder Sprosse |
| Form | slater (fan 1) | slater (fan 8) | Neo4j 5 |
|---|
| count(*) alle Knoten | 0,41 | 0,41 | 3606 |
| Punkt-Lookup (indiziert) | 0,72 | 0,49 | 6,3 |
| Grad (1-Hop-Zählung) | 0,43 | 0,44 | 6,0 |
| 1-Hop-Nachbarn | 9,8 | 4,5 | 10,1 |
| 2-Hop | 37 | 23 | 34,5 |
| 3-Hop | 32 | 25 | 74 |
var-length *1..2 distinct | 985 | 1056 | 47 |
| Dimension | slater | Bestes des Feldes | Urteil |
|---|
| Resident-Speicher, jede Skala | 11–584 MiB (62k → 91,6 Mio.) | In-Memory 1,5–2,7 GiB; kann 1,5 Mrd. nicht laden | slater |
| count / Metadaten / Scan | ~0,4 ms | Service-Engines 5–80 ms | slater (10–200×) |
| indizierter Punkt-Lookup | 0,43 ms (MeSH) | Memgraph · FalkorDB 0,48 ms | slater (knapp vor dem In-Memory-Paar) |
| unverankerter Multi-Hop (Zeilen) | 1,40 ms (MeSH 2-Hop) | Neo4j 5,6 ms | slater (Beziehungstyp-Scan) |
| Aggregation (group-by / DISTINCT) | 0,45 ms | LadybugDB 5 ms (spaltenorientiert) | slater (Build-Zeit-Histogramm) |
| kNN | 2,4–2,9 ms (exakt) | FalkorDB 1,2 ms (HNSW) | schlägt Neo4j/Ladybug; ~1,4× hinter Memgraph; exakt |
| 91,6 Mio. Metadaten / Punkt / Grad / 3-Hop | 0,4–32 ms | Neo4j 6–3.600 ms | slater (2–8800×) |
| 91,6 Mio. 1–2-Hop | 4,5–23 ms (fan 8) | Neo4j 10–35 ms | ~gleichauf |
91,6 Mio. var-length *1..2 distinct | ~1 s | Neo4j 47 ms | Neo4j (eine echte slater-Schwachstelle) |
Multi-Hop count(*) bei Skala | 0,3–0,6 GiB | In-Memory-Engines materialisieren den Zeilensatz | slater, begrenzt |
| Ergebnis | Messung |
|---|
| Hält 1000 gleichzeitige Clients, null Fehler | Durchsatz erreicht Spitze ~2,5k rps; das Latenz-Knie setzt bei etwa 750 Clients ein (p99 51 → 750 ms) – Warteschlangen unter Kern-Konkurrenz, keine harte Grenze (Einzellauf, WSL2) |
| Block-Cache begrenzt und wirksam | 100 % Trefferquote, 0 Verdrängungen, 50 MB resident für einen cache-passenden Arbeitssatz |
| RSS unter Dauerlast gehalten | der jemalloc-Allokator hält RSS bei ~0,6 GB über eine 100→500-Client-wiki_cache_churn-Rampe – cache-gebunden und stabil, ohne MALLOC_*-Tuning (die frühere MALLOC_ARENA_MAX=2 + Trim-Schwelle ist ausgemustert); sein Hintergrund-Purge gibt auch den Hochwasserstand nach dem Burst zurück, statt ihn fixiert zu lassen |
| Aggregierter Speicher begrenzt | serverweites query.maxIntermediateGlobal + adjazenz-belastete Expansion halten die wiki_budget-2-Hop-Flut bei 1000 Clients ohne OOM (RSS ~0,6 GB; die Wache verwirft ~60 % der Hub-Abfragen als wiederholbare Budget-Fehler) |