Embedding kglite in a Rust binary

This document is written for Rust embedders: anyone who wants to use kglite’s graph engine directly from a Rust binary without the Python wheel in their build. If you’re a Python user (pip install kglite), you don’t need to read this — import kglite already wraps everything for you.

Crate split

KGLite keeps the pure-Rust engine separate from protocol and Python wrappers:

Crate

Purpose

Has PyO3?

kglite (crates/kglite/)

Pure-Rust engine. Publishable on crates.io.

No

kglite-py (crates/kglite-py/)

PyO3 wrapper. Built by maturin into the kglite Python wheel.

Yes

kglite-bolt-server (crates/kglite-bolt-server/)

Bolt v5.x protocol binary. Wraps the kglite engine directly.

No

kglite-mcp-server (crates/kglite-mcp-server/)

MCP protocol binary. Depends on the pure-Rust kglite core directly — no pyo3 in the resulting binary. (Also bundled into the Python wheel, statically linked via the kglite-py crate, sharing the one engine.)

No

Rust-side wrappers depend on the engine crate directly. Non-Rust bindings use the supported C ABI described later in this guide.

Quick start

Add kglite to your Cargo.toml:

[dependencies]
# Pre-crates.io-publish: path dependency from within the workspace.
kglite = { path = "../kglite/crates/kglite" }
# Post-publish: crates.io coordinate.
# kglite = "0.14"

Then load a .kgl file written by any kglite binding and query it:

use kglite::api::io::load_file;
use kglite::api::{session, Value};
use std::collections::HashMap;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load a graph file written by ANY kglite binding —
    // Python's `kg.save("graph.kgl")`, the bolt-server's
    // `CALL db.checkpoint()` over Bolt, etc. The on-disk
    // .kgl format is the portable cross-binding contract.
    let graph = load_file("graph.kgl")?;

    // Run a Cypher query through the canonical pipeline.
    // Same path Python / Bolt / MCP all flow through.
    let params = HashMap::new();
    let opts = session::ExecuteOptions::eager(&params);
    let outcome = session::execute_read(
        &graph,
        "MATCH (n:Person) RETURN n.name LIMIT 10",
        &opts,
    )?;

    for row in &outcome.result.rows {
        if let Some(Value::String(name)) = row.first() {
            println!("{}", name);
        }
    }
    Ok(())
}

Verify the build has zero pyo3:

cargo tree -p your-crate | grep pyo3   # → (empty)

See crates/kglite/examples/embedded_*.rs for the two runnable engine examples (embedded_basic reads a .kgl and embedded_session demonstrates OCC transactions). Source-tree construction lives in the separate codingest crate.

Keeping your debug tree small

A debug (dev-profile) build of kglite produces a large rlib — measured at 336 MB on 0.16.16 (arm64 macOS, rustc 1.98). Two-thirds of that is DWARF debug info, dominated by fully-expanded generic type-name strings; only ~5% is machine code. Every kglite version bump also strands the previous generation’s artifacts in your target/ dir, because cargo never garbage-collects it.

Cargo gives a library crate no way to fix this on its end — build profiles are controlled solely by the workspace root being built. Add this to yours:

[profile.dev.package."*"]
debug = "line-tables-only"

Measured effect: the kglite rlib drops 336 MB → 191 MB (−43%), and a minimal dependent’s whole debug tree shrinks about 22%. Your own crates keep full debug info; dependency frames keep file/line in backtraces — what you lose is variable/type inspection inside dependency code under a debugger. debug = 0 goes further (rlib → 107 MB, tree −46%) at the cost of file/line in dependency backtrace frames.

Two gotchas, both measured rather than inferred:

  • The "*" wildcard matches only non-member dependencies. If kglite is a path/workspace member in your tree, name it explicitly: [profile.dev.package.kglite] debug = "line-tables-only".

  • strip and split-debuginfo settings have zero effect on rlib size — they act at link time, and the DWARF lives in the rlib’s archived object files either way.

If you build the same workspace through several entry points (e.g. cargo build --workspace, cargo test --workspace, and a cargo test -p <one-crate> --test <name>), note that each distinct package selection can re-unify features and flags differently and leave an additional full-size kglite rlib in target/ per shape. Preferring --workspace (with --all-targets on the build step, or --test <name> to scope a test run) keeps every invocation on one shared dependency build.

The stable API surface

kglite::api::* is the curated, documented surface. Pre-1.0 that is not a no-break promise: any release, including a patch, may ship a documented breaking change, announced in CHANGELOG.md. Pin an exact version (kglite = "=X.Y.Z") and upgrade against the changelog. Everything else (kglite::graph::*, kglite::datatypes::*, etc.) is an implementation detail that may move in any release.

Engine types

use kglite::api::{DirGraph, KnowledgeGraph, Value, KgError, KgErrorCode};
use kglite::api::{NodeValue, PathValue, RelValue};
use kglite::api::Embedder;
  • DirGraph — the in-memory graph. Built from blueprint, loaded from a .kgl, or constructed via the codingest builder. Owned by your binding’s “graph handle” type.

  • KnowledgeGraph — the thin pure-Rust handle around Arc<DirGraph> plus an optional Arc<dyn Embedder>. Use it when those two lifecycle values are all your Rust application needs; bindings can still wrap DirGraph directly when they own richer language-specific state.

  • Value — every value a Cypher query can return. Variants include scalars (Int64, Float64, String, Bool, NaiveDate), compound (List, Map), and graph-specific (Node, Relationship, Path).

  • KgError — typed error enum every engine function can return. Map to your binding’s error idiom at the boundary. File I/O surfaces as FileError (not found), FileFormatError (corrupt / wrong-format .kgl — what load raises on a bad file), and FileIoError (permission / mid-read).

  • Embedder trait — pluggable text-embedding backend. Bind via kglite::api::FastEmbedAdapter (with the fastembed feature) or implement your own (dimension, embed, and optional model_id for store provenance + load/unload).

Cypher pipeline

use kglite::api::cypher::{parse_cypher, CypherExecutor, validate_schema};
use kglite::api::cypher::{is_mutation_query, generate_explain_result};
use kglite::api::cypher::{mark_lazy_eligibility, rewrite_text_score, planner};

Use these if you’re building a custom Cypher pipeline (e.g. a custom GraphQL adapter that compiles to Cypher). For the canonical pipeline, use session instead.

Session (canonical query + transaction surface)

use kglite::api::session::{Session, Transaction, CommitOutcome};
use kglite::api::session::{ExecuteOptions, execute_read, execute_mut};

This is the single source of truth all Rust-side bindings flow through. Cypher pipeline orchestration + snapshot/working CoW + OCC live here exactly once.

  • execute_read(&dir, query, &opts) — run a read query against &DirGraph.

  • execute_mut(&mut dir, query, &opts) — run a mutation against &mut DirGraph.

  • Session::new(dir) + session.begin() / session.commit(tx, true) — the snapshot/working CoW transaction model. OCC is opt-in per commit; pass true for production semantics.

  • Session::open_durable(dir, path, level) — the same session with a write-ahead log under it: the sidecar is recovered before the session serves anyone, and each commit’s frame is appended before the publish.

  • CommitOutcome::{NoWritesNoOp, Committed { new_version }, ConflictDetected { current_version, base_version }, DurabilityFailed { error }} — what your binding maps to its consumer-facing error type. The enum is #[non_exhaustive], so match a catch-all arm as well.

See docs/rust/session.md for the full session abstraction guide, including the durable-session contract.

Dataset loaders

The pre-packaged dataset loaders (SEC EDGAR, Sodir, Wikidata) are no longer part of the kglite core — they live in the separate kglite-datasets project, and the sec / sodir / wikidata Cargo features and kglite::datasets::* modules have been removed. kglite loads the graphs those loaders produce via the ordinary lifecycle API (kglite::api::io::load_file, etc.). To ingest RDF directly, use the kept RDF/N-Triples loaders instead.

Thin Rust handle vs binding-specific handles

Core exports kglite::api::KnowledgeGraph, a deliberately small pure-Rust handle containing Arc<DirGraph> plus an optional Arc<dyn Embedder>. The Python wrapper has its own separate KnowledgeGraph PyClass with selection, reports, temporal context, and Python-facing ergonomics. A new binding should use the core handle only when that minimal shape fits; otherwise hold DirGraph and the embedder directly and add its own native state. The bolt server is a working example: it wraps Session and adds Bolt protocol state.

The .kgl file format is portable

A .kgl written by any kglite binding loads cleanly in any other:

  • Python kg.save("graph.kgl") → Bolt server reads via kglite::api::io::load_file(path)

  • Rust embedder kglite::api::io::save_graph(&mut arc, path) → Python loads via kglite.load("graph.kgl")

  • Future Go binding writes → TypeScript binding reads, etc.

The current writer emits RGF v6 with an explicit Postcard codec tag. The reader accepts v5/Postcard and rejects v4/bincode and older containers with a migration/rebuild message. Format drift is tracked via tests/test_phase4_parity.py::GOLDEN_V3_DIGEST etc. (see CLAUDE.md → “Captured-constant refresh at release time”).

The format does NOT bundle binding-ergonomic state (Python’s selection cache, default timeouts, etc.). Each binding sets those fresh on load.

Wrapping the kglite engine in a new language

Rust-side wrappers call kglite::api::* directly. Non-Rust bindings (Go, JavaScript, JVM, .NET, Swift) use the supported kglite-c boundary rather than binding internal Rust structs. In both cases the engine owns the synchronous Cypher pipeline and graph semantics; the wrapper owns marshalling, runtime idioms, logging, error presentation, and teardown.

Non-Rust bindings via the C ABI

The kglite-c crate (crates/kglite-c/) is the canonical entry point for non-Rust language bindings — Go via cgo, JavaScript via napi, JVM via JNI, .NET via P/Invoke. It exposes the supported lifecycle, session, query, result, persistence, and embedder surface through a cbindgen-generated kglite.h header. The generated header, not a prose function count, is the signature authority.

A minimal cgo binding looks like this:

package kglite

/*
#cgo LDFLAGS: -lkglite_c
#include <stdlib.h>
#include "kglite.h"
*/
import "C"

import "unsafe"

type Graph struct{ h *C.KgliteGraph }

func LoadFile(path string) (*Graph, error) {
    cpath := C.CString(path)
    defer C.free(unsafe.Pointer(cpath))
    var g *C.KgliteGraph
    var errMsg *C.char
    rc := C.kglite_load_file(cpath, &g, &errMsg)
    if rc != C.KGLITE_STATUS_CODE_OK {
        defer C.kglite_free_string(errMsg)
        return nil, errors.New(C.GoString(errMsg))
    }
    return &Graph{h: g}, nil
}

For the full cgo / napi / JNI worked examples, the C ABI design conventions, and the binding-author cookbook, see implementing-a-binding.md and c-abi.md. The bridge is mechanical; no new core development needed per binding.

What’s stable vs internal

Item

Stability

kglite::api::*

The documented surface, CI-baseline-locked. Pre-1.0, any release — patch included — may ship a documented break, announced in CHANGELOG.md.

kglite::error::*

Same policy as api::*: documented breaks may land in any pre-1.0 release.

kglite::graph::* (raw module path)

Internal. Subject to reorganization. Always go through api::* re-exports.

kglite::datatypes::* (raw module path)

Internal — use api::{Value, NodeValue, PathValue, RelValue, PropMap} and, for bulk ingest, api::mutation::{DataFrame, ColumnType, ColumnData}.

Public items outside the curated api::* boundary

Unstable implementation detail; do not bind them.

If you depend on something outside api::*, you’re on your own for compatibility across every release.

See also

  • docs/rust/session.md — full session/transaction abstraction reference.

  • docs/python/transactions.md — Python-API-flavored transaction guide.

  • docs/operators/bolt-server.md — Bolt server operator guide (an example of a sibling-crate binding).

  • CYPHER.md — Cypher language reference.