In-Memory-Graphdatenbank mit Labels, typisierten Properties, f64-Vektoren und drei Indizes (temporal, typ-basiert, Vektor-Ähnlichkeit). Persistenz über ein kompaktes Binärformat (LYXGDB). Enthält eine Graph-RAG-Funktion für KI-gestützte Kontextsuche (Vektorsuche + BFS-Traversal).
Einsatzbereiche: Wissensgraphen, semantische Suche, Empfehlungssysteme, temporale Ereignisgraphen, KI-Kontext (RAG).
→ Standard Library · std.db · std.hash · std.alloc · std.ml
std.graphdb.core IDs, Timestamps
│
┌────┴────┐
node edge Knoten + Kanten mit TLV-Properties
└────┬────┘
mem In-Memory-Store + Adjazenzliste
│
index TemporalIndex · TypeIndex · VectorIndex
│
┌────┴────┐
query file High-Level Query-API · Datei-Persistenz
Jedes Sub-Modul wird separat importiert:
import std.graphdb.core;
import std.graphdb.node;
import std.graphdb.edge;
import std.graphdb.mem;
import std.graphdb.index;
import std.graphdb.file;
import std.graphdb.query;
import std.graphdb.core;
| Signatur | Beschreibung |
|---|---|
GraphIDNew(): int64 | Neue eindeutige ID (Zeit XOR LCG, immer > 0) |
GraphIDFromStr(s: pchar, slen: int64): int64 | Deterministische ID aus String (FNV-1a-64, immer > 0) |
GraphNow(): int64 | Unix-Millisekunden via CLOCK_REALTIME |
GRAPH_ID_INVALID = 0 — ungültige/leere ID.
GraphIDFromStr erzeugt immer denselben Wert für denselben String — geeignet für stabile Knoten-IDs aus externen Bezeichnern.
import std.graphdb.node;
Ein Node ist ein alloc(GNODE_SIZE) Puffer (80 Bytes). Der Caller verwaltet das Speicherlayout und setzt die ID manuell:
var n: int64 := alloc(GNODE_SIZE);
GraphNodeInit(n);
poke64(n + GNODE_OFF_ID, GraphIDNew()); // ID muss explizit gesetzt werden
| Signatur | Beschreibung |
|---|---|
GraphNodeInit(n: int64) | Initialisiert alle Felder; alloziert Label- und Prop-Puffer |
GraphNodeFree(n: int64) | Gibt Label-Puffer, Prop-Puffer und Vektor-Puffer frei |
Labels sind kurze Typbezeichner (z.B. Person, Article). Intern \\0-getrennt in einem wachsenden Puffer.
| Signatur | Beschreibung |
|---|---|
GraphNodeAddLabel(n: int64, label: pchar, llen: int64) | Label hinzufügen |
GraphNodeHasLabel(n: int64, label: pchar, llen: int64): int64 | 1 wenn Label vorhanden, sonst 0 |
Properties sind typisierte Schlüssel-Wert-Paare, intern als TLV-Stream kodiert.
| Signatur | Typ | Beschreibung |
|---|---|---|
GraphNodeSetInt(n, key, klen, val: int64) | int64 | Ganzzahl-Property setzen |
GraphNodeSetF64(n, key, klen, bits: int64) | f64 | Float-Property setzen (bits = IEEE-754-Bitmuster) |
GraphNodeSetBool(n, key, klen, val: int64) | bool | Bool-Property setzen (0 oder 1) |
GraphNodeSetStr(n, key, klen, val: pchar, vlen: int64) | pchar | String-Property setzen |
Für GraphNodeSetF64 muss der f64-Wert als int64-Bitmuster übergeben werden:
var bits: int64 := 3.14 as int64; // IEEE-754-Bits von 3.14
GraphNodeSetF64(n, "score"c, 5, bits);
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphNodeGetInt(n, key, klen): int64 | Wert oder 0 | Ganzzahl-Property lesen |
GraphNodeGetF64(n, key, klen): int64 | Bitmuster oder 0 | Float-Property lesen (als int64-Bitmuster) |
GraphNodeGetBool(n, key, klen): int64 | 0 oder 1 | Bool-Property lesen |
GraphNodeGetStr(n, key, klen, outLen: int64): pchar | Pointer oder nil | String-Property lesen; outLen = Pointer auf int64 für Länge (0 = ignorieren) |
GraphNodeGetStr gibt einen direkten Pointer in den internen Prop-Puffer zurück — gültig bis zum nächsten GraphNodeSetStr-Aufruf für denselben Node.
| Signatur | Beschreibung |
|---|---|
GraphNodeSetVec(n: int64, dims: int64, data: int64) | f64-Embedding-Vektor setzen (kopiiert dims × 8 Bytes aus data) |
GraphNodeGetVecDim(n: int64, i: int64): f64 | Einzelne Dimension lesen |
data muss ein Puffer mit dims × 8 Bytes (f64-Array) sein. Vorhandener Vektor wird ersetzt.
| Offset | Konstante | Inhalt |
|---|---|---|
| 0 | GNODE_OFF_ID | Node-ID (int64) |
| 8 | GNODE_OFF_LABELSBUF | Pointer auf Label-Puffer |
| 16 | GNODE_OFF_LABELSLEN | Belegter Bereich (Bytes) |
| 24 | GNODE_OFF_LABELSCAP | Kapazität (Bytes) |
| 32 | GNODE_OFF_LABELSCOUNT | Anzahl Labels |
| 40 | GNODE_OFF_PROPSBUF | Pointer auf TLV-Prop-Puffer |
| 48 | GNODE_OFF_PROPSLEN | Belegter Bereich (Bytes) |
| 56 | GNODE_OFF_PROPSCAP | Kapazität (Bytes) |
| 64 | GNODE_OFF_VECBUF | Pointer auf f64-Vektor (0 = kein Vektor) |
| 72 | GNODE_OFF_VECDIMS | Vektor-Dimensionen |
import std.graphdb.edge;
Eine Kante verbindet zwei Nodes per ID und hat einen Typ-String sowie TLV-Properties.
var e: int64 := alloc(GEDGE_SIZE);
GraphEdgeInit(e);
poke64(e + GEDGE_OFF_ID, GraphIDNew());
poke64(e + GEDGE_OFF_SOURCE, srcNodeId);
poke64(e + GEDGE_OFF_TARGET, tgtNodeId);
poke64(e + GEDGE_OFF_CREATEDAT, GraphNow()); // Pflicht vor AddEdge!
GraphEdgeSetType(e, "KNOWS"c, 5);
| Signatur | Beschreibung |
|---|---|
GraphEdgeInit(e: int64) | Initialisiert alle Felder; alloziert Prop-Puffer |
GraphEdgeFree(e: int64) | Gibt Typ-Puffer und Prop-Puffer frei |
| Signatur | Beschreibung |
|---|---|
GraphEdgeSetType(e: int64, etype: pchar, elen: int64) | Kanten-Typ setzen (z.B. KNOWS, AUTHORED) |
GraphEdgeTypeEq(e: int64, etype: pchar, elen: int64): int64 | 1 wenn Typ übereinstimmt, sonst 0 |
Identische Signaturstruktur wie bei Nodes:
| Signatur | Beschreibung |
|---|---|
GraphEdgeSetInt(e, key, klen, val) | Ganzzahl-Property |
GraphEdgeSetF64(e, key, klen, bits) | Float-Property (IEEE-754-Bitmuster) |
GraphEdgeSetBool(e, key, klen, val) | Bool-Property |
GraphEdgeSetStr(e, key, klen, val, vlen) | String-Property |
GraphEdgeGetInt(e, key, klen) | Ganzzahl-Property lesen |
GraphEdgeGetF64(e, key, klen) | Float-Property lesen |
GraphEdgeGetBool(e, key, klen) | Bool-Property lesen |
GraphEdgeGetStr(e, key, klen, outLen) | String-Property lesen |
| Offset | Konstante | Inhalt |
|---|---|---|
| 0 | GEDGE_OFF_ID | Kanten-ID (int64) |
| 8 | GEDGE_OFF_SOURCE | Source-Node-ID |
| 16 | GEDGE_OFF_TARGET | Target-Node-ID |
| 24 | GEDGE_OFF_ETYPEBUF | Pointer auf Typ-String |
| 32 | GEDGE_OFF_ETYPELEN | Typ-String-Länge |
| 40 | GEDGE_OFF_CREATEDAT | Erstellungszeitpunkt (Unix-ms) |
| 48 | GEDGE_OFF_PROPSBUF | Pointer auf TLV-Prop-Puffer |
| 56 | GEDGE_OFF_PROPSLEN | Belegter Bereich (Bytes) |
| 64 | GEDGE_OFF_PROPSCAP | Kapazität (Bytes) |
import std.graphdb.mem;
Verwaltet Nodes und Kanten in parallelen ID/Pointer-Arrays und einer Adjazenzliste (source-ID → edge-ID).
var s: int64 := alloc(GMEM_SIZE);
GraphMemStoreInit(s);
| Signatur | Beschreibung |
|---|---|
GraphMemStoreInit(s: int64) | Alloziert alle internen Arrays (Node-IDs, Kanten-IDs, Adjazenz) |
GraphMemStoreFree(s: int64) | Gibt alle internen Arrays frei (Nodes/Edges selbst müssen separat freigegeben werden) |
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphMemStoreAddNode(s, n): int64 | 1 = OK, 0 = Fehler | Node in Store aufnehmen. Schlägt fehl wenn ID = 0 oder bereits vorhanden |
GraphMemStoreAddEdge(s, e): int64 | 1 = OK, 0 = Fehler | Kante in Store aufnehmen. Erfordert: ID ≠ 0, createdAt ≠ 0, source + target im Store vorhanden |
createdAtmuss vorGraphMemStoreAddEdgegesetzt sein — der Store prüftpeek64(e + GEDGE_OFF_CREATEDAT) != 0.
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphMemStoreGetNode(s, id): int64 | Node-Pointer oder 0 | Node per ID suchen |
GraphMemStoreGetEdge(s, id): int64 | Edge-Pointer oder 0 | Kante per ID suchen |
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphMemStoreOutEdges(s, nodeId, outBuf, maxOut): int64 | Anzahl | Kanten-IDs ausgehend von nodeId → int64-Array in outBuf |
GraphMemStoreNeighbors(s, nodeId, outBuf, maxOut): int64 | Anzahl | Target-Node-IDs über ausgehende Kanten → int64-Array in outBuf |
outBuf muss maxOut × 8 Bytes groß sein. Zurückgegeben wird die tatsächliche Anzahl gefundener Einträge.
| Offset | Konstante | Inhalt |
|---|---|---|
| 0/8 | GMEM_OFF_NODEIDS / NODEPTRS | Parallel-Arrays: Node-IDs + Node-Pointer |
| 16/24 | GMEM_OFF_NODECOUNT / NODECAP | Anzahl + Kapazität |
| 32/40 | GMEM_OFF_EDGEIDS / EDGEPTRS | Parallel-Arrays: Kanten-IDs + Kanten-Pointer |
| 48/56 | GMEM_OFF_EDGECOUNT / EDGECAP | Anzahl + Kapazität |
| 64/72/80 | GMEM_OFF_ADJBUF / ADJCOUNT / ADJCAP | Adjazenzliste: [sourceId:8][edgeId:8] pro Eintrag |
import std.graphdb.index;
Drei unabhängige Indizes, die nach dem Befüllen des Stores mit *Build aufgebaut werden.
Sortierter Index über createdAt-Timestamps. Ermöglicht Zeitfenster-Abfragen.
| Signatur | Beschreibung |
|---|---|
GraphTemporalIndexInit(idx: int64) | Index initialisieren (GTIDX_SIZE = 24 Bytes) |
GraphTemporalIndexFree(idx: int64) | Index freigeben |
GraphTemporalIndexBuild(idx, store: int64) | Index aus Store aufbauen (nach AddEdge aufrufen) |
GraphTemporalIndexQuery(idx, tsFrom, tsTo, outBuf, maxOut): int64 | Kanten-IDs im Zeitfenster → int64-Array |
Hash-basierter Index über den Kanten-Typ-String.
| Signatur | Beschreibung |
|---|---|
GraphTypeIndexInit(idx: int64) | Index initialisieren (GXIDX_SIZE = 24 Bytes) |
GraphTypeIndexFree(idx: int64) | Index freigeben |
GraphTypeIndexBuild(idx, store: int64) | Index aus Store aufbauen |
GraphTypeIndexQuery(idx, etype, elen, outBuf, maxOut): int64 | Kanten-IDs dieses Typs → int64-Array |
Cosinus-Ähnlichkeitssuche über Node-Embedding-Vektoren. Nur Nodes mit Vektor (GraphNodeSetVec) werden indiziert.
| Signatur | Beschreibung |
|---|---|
GraphVectorIndexInit(idx: int64, dims: int64) | Index initialisieren (GVIDX_SIZE = 32 Bytes); dims = Vektor-Dimensionen |
GraphVectorIndexFree(idx: int64) | Index freigeben |
GraphVectorIndexBuild(idx, store: int64) | Index aus Store aufbauen (nur Nodes mit passender Vektor-Dimension) |
GraphVectorIndexSearch(idx, queryVec, topK, outNodeIds, outScores): int64 | Top-k ähnlichste Nodes; outScores = Cosinus-Score × 1 000 000 (absteigend sortiert) |
queryVec ist ein dims × 8 Byte großer int64-Puffer mit den f64-Rohdaten des Query-Vektors (als int64-Bitmuster).
outNodeIds und outScores müssen je topK × 8 Bytes groß sein.
import std.graphdb.file;
Binäres Dateiformat mit Magic LYXGDB\x01\x00. Speichert alle Nodes und Kanten inklusive Labels, Properties und Vektoren.
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphFileSave(store, path: pchar, plen, compress: int64): int64 | 1 = OK, 0 = Fehler | Store in Datei serialisieren; compress derzeit ignoriert (Flags = 0) |
GraphFileLoad(store, path: pchar, plen: int64): int64 | 1 = OK, 0 = Fehler | Datei in Store laden; schlägt fehl bei falscher Magic |
Dateiformat:
[Magic: 8B "LYXGDB\x01\x00"][nodeCount:8][edgeCount:8][flags:8]
pro Node: [id:8][labelsLen:8][labelsCount:8][propsLen:8][vecDims:8] + Daten
pro Edge: [id:8][source:8][target:8][edgeTypeLen:8][createdAt:8][propsLen:8] + Daten
GraphFileLoadruft internGraphMemStoreAddNodeundGraphMemStoreAddEdgeauf — der Store muss vorher mitGraphMemStoreInitinitialisiert sein.
import std.graphdb.query;
High-Level Query-Kontext kombiniert Store + Indizes. Ein GraphQueryCtx (GQCTX_SIZE = 32 Bytes) verweist auf alle Komponenten.
var ctx: int64 := alloc(GQCTX_SIZE);
GraphQueryCtxInit(ctx, store);
GraphQueryCtxSetTemporalIdx(ctx, tidx); // optional
GraphQueryCtxSetTypeIdx(ctx, typeidx); // optional
GraphQueryCtxSetVectorIdx(ctx, vidx); // optional
| Signatur | Beschreibung |
|---|---|
GraphQueryCtxInit(ctx, store: int64) | Kontext initialisieren; alle Indizes auf 0 (deaktiviert) |
GraphQueryCtxSetTemporalIdx(ctx, tidx: int64) | TemporalIndex einbinden |
GraphQueryCtxSetTypeIdx(ctx, typeidx: int64) | TypeIndex einbinden |
GraphQueryCtxSetVectorIdx(ctx, vidx: int64) | VectorIndex einbinden |
| Signatur | Rückgabe | Beschreibung |
|---|---|---|
GraphQueryEdgesInWindow(ctx, tsFrom, tsTo, outBuf, maxOut): int64 | Anzahl Kanten-IDs | Zeitfenster-Abfrage (benötigt TemporalIndex) |
GraphQueryEdgesByType(ctx, etype, elen, outBuf, maxOut): int64 | Anzahl Kanten-IDs | Typ-Abfrage (benötigt TypeIndex) |
GraphQueryPattern(ctx, pat, outBuf, maxOut): int64 | Anzahl Target-Node-IDs | Pattern-Match: source → etype → target (optional: Zeitfilter) |
Um die 7-Argument-Grenze des Compilers zu umgehen, werden Pattern-Parameter in einem GQPAT_SIZE-Struct (32 Bytes) übergeben:
var pat: int64 := alloc(GQPAT_SIZE);
GraphQueryPatternInit(pat, srcNodeId, "KNOWS"c, 5);
GraphQueryPatternSetTs(pat, GraphNow() - 86400000); // nur Kanten der letzten 24h (optional)
var out: int64 := alloc(100 * 8);
var found: int64 := GraphQueryPattern(ctx, pat, out, 100);
Kombiniert Vektorsuche mit BFS-Traversal für KI-Kontext-Retrieval:
// topK ähnlichste Knoten finden + depth Hops Umfeld einsammeln
var out: int64 := alloc(maxOut * 8);
var cnt: int64 := GraphGetContextForAI(ctx, queryVec, topK, depth, out, maxOut);
| Signatur | Beschreibung |
|---|---|
GraphGetContextForAI(ctx, queryVec, topK, depth, outBuf, maxOut): int64 | Vektor-Ähnlichkeitssuche (topK Seeds) + BFS bis depth Hops; gibt alle Node-IDs zurück; benötigt VectorIndex |
Ablauf intern:
GraphVectorIndexSearch → topK semantisch ähnliche Start-NodesGraphMemStoreNeighbors → Nachbarn einsammelndepth Mal; bereits besuchte Nodes werden nicht doppelt ausgegeben
import std.graphdb.core;
import std.graphdb.node;
import std.graphdb.edge;
import std.graphdb.mem;
import std.graphdb.index;
import std.graphdb.file;
import std.graphdb.query;
import std.alloc;
fn main(): int64 {
// Store anlegen
var s: int64 := alloc(GMEM_SIZE);
GraphMemStoreInit(s);
// Nodes anlegen
var n1: int64 := alloc(GNODE_SIZE);
GraphNodeInit(n1);
poke64(n1 + GNODE_OFF_ID, GraphIDFromStr("alice"c, 5));
GraphNodeAddLabel(n1, "Person"c, 6);
GraphNodeSetStr(n1, "name"c, 4, "Alice"c, 5);
GraphNodeSetInt(n1, "age"c, 3, 30);
GraphMemStoreAddNode(s, n1);
var n2: int64 := alloc(GNODE_SIZE);
GraphNodeInit(n2);
poke64(n2 + GNODE_OFF_ID, GraphIDFromStr("bob"c, 3));
GraphNodeAddLabel(n2, "Person"c, 6);
GraphNodeSetStr(n2, "name"c, 4, "Bob"c, 3);
GraphMemStoreAddNode(s, n2);
// Kante anlegen
var e: int64 := alloc(GEDGE_SIZE);
GraphEdgeInit(e);
poke64(e + GEDGE_OFF_ID, GraphIDNew());
poke64(e + GEDGE_OFF_SOURCE, peek64(n1 + GNODE_OFF_ID));
poke64(e + GEDGE_OFF_TARGET, peek64(n2 + GNODE_OFF_ID));
poke64(e + GEDGE_OFF_CREATEDAT, GraphNow());
GraphEdgeSetType(e, "KNOWS"c, 5);
GraphEdgeSetInt(e, "since"c, 5, 2020);
GraphMemStoreAddEdge(s, e);
// TypeIndex aufbauen und abfragen
var tidx: int64 := alloc(GXIDX_SIZE);
GraphTypeIndexInit(tidx);
GraphTypeIndexBuild(tidx, s);
var out: int64 := alloc(64 * 8);
var found: int64 := GraphTypeIndexQuery(tidx, "KNOWS"c, 5, out, 64);
// found = 1 (eine KNOWS-Kante)
// Traversal: Nachbarn von n1
var nb: int64 := alloc(64 * 8);
var ncnt: int64 := GraphMemStoreNeighbors(s, peek64(n1 + GNODE_OFF_ID), nb, 64);
// ncnt = 1 (Bob)
// Persistenz
GraphFileSave(s, "graph.lyxgdb"c, 12, 0);
// Aufräumen
GraphTypeIndexFree(tidx);
free(tidx, GXIDX_SIZE);
GraphNodeFree(n1); free(n1, GNODE_SIZE);
GraphNodeFree(n2); free(n2, GNODE_SIZE);
GraphEdgeFree(e); free(e, GEDGE_SIZE);
free(nb, 64 * 8);
free(out, 64 * 8);
GraphMemStoreFree(s);
free(s, GMEM_SIZE);
return 0;
}
alloc-Puffer — kein automatisches GC. GraphNodeFree/GraphEdgeFree geben die internen Puffer frei; der Struct-Puffer selbst muss mit free(ptr, GNODE_SIZE) etc. separat freigegeben werden. GraphMemStoreFree gibt nur die internen Arrays frei, nicht die Nodes/Kanten selbst.GraphNodeSetF64 und GraphEdgeSetF64 erwarten IEEE-754-Bitmuster als int64. Konvertierung: var bits: int64 := myFloat as int64;GraphNodeSetStr mit einem Hash-Key zu bevorzugen.GraphTemporalIndexBuild, GraphTypeIndexBuild und GraphVectorIndexBuild erzeugen einen Snapshot. Nach weiteren AddNode/AddEdge-Aufrufen muss der Index neu aufgebaut werden.GraphVectorIndexSearch gibt Cosinus-Ähnlichkeit × 1 000 000 als int64 zurück (absteigend sortiert). Score 1 000 000 = identische Vektoren.GraphMemStoreAddEdge bereits im Store sein. createdAt muss ≠ 0 sein.Letzte Aktualisierung: 2026-06-14