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? |
|---|---|---|
|
Pure-Rust engine. Publishable on crates.io. |
No |
|
PyO3 wrapper. Built by maturin into the |
Yes |
|
Bolt v5.x protocol binary. Wraps the kglite engine directly. |
No |
|
MCP protocol binary. Depends on the pure-Rust |
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(¶ms);
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".stripandsplit-debuginfosettings 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 aroundArc<DirGraph>plus an optionalArc<dyn Embedder>. Use it when those two lifecycle values are all your Rust application needs; bindings can still wrapDirGraphdirectly 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 asFileError(not found),FileFormatError(corrupt / wrong-format.kgl— whatloadraises on a bad file), andFileIoError(permission / mid-read).Embeddertrait — pluggable text-embedding backend. Bind viakglite::api::FastEmbedAdapter(with thefastembedfeature) or implement your own (dimension,embed, and optionalmodel_idfor 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; passtruefor 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 viakglite::api::io::load_file(path)Rust embedder
kglite::api::io::save_graph(&mut arc, path)→ Python loads viakglite.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 |
|---|---|
|
The documented surface, CI-baseline-locked. Pre-1.0, any release — patch included — may ship a documented break, announced in |
|
Same policy as |
|
Internal. Subject to reorganization. Always go through |
|
Internal — use |
Public items outside the curated |
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.