kglite

Type stubs for kglite — a high-performance knowledge graph library.

Submodules

Exceptions

ArgumentError

A user-supplied argument violated a precondition.

ConnectionNotFoundError

A connection type isn't declared in the schema.

ConstraintCreationError

Declaring a constraint failed because the stored data already violates it.

ConstraintError

Base class for declared-integrity-constraint failures.

ConstraintViolationError

A write violated a declared UNIQUE / NOT NULL / NODE KEY / IS :: TYPE

CypherError

Base for all Cypher-related errors (syntax, timeout, execution, type).

CypherExecutionError

Cypher executor failure during query evaluation.

CypherSyntaxError

Cypher parser / tokenizer rejected the query.

CypherTimeoutError

Cypher query exceeded its timeout_ms budget.

CypherTypeMismatchError

Cypher value-type mismatch (e.g. arithmetic on a String).

ExprError

Blueprint expression evaluation failure.

FileError

A file the user named doesn't exist on disk.

FileFormatError

A file's contents are malformed (bad .kgl header, truncated blueprint, etc.).

FileIoError

Generic I/O failure (permission denied, mid-read EOF, mmap failure).

InternalError

Invariant violation — kglite-internal bug. Reports the source location.

InternerCollisionError

Two distinct names collided on one persisted interner key; no mutation was applied.

KgError

Base class for typed KGLite engine failures.

LoadMemoryLimitError

A .kgl load was refused before decoding: its estimated memory exceeded

MissingArgumentError

A required argument wasn't passed.

NodeNotFoundError

A node identified by (node_type, id) doesn't exist.

PropertyNotFoundError

A property is missing from a node or relationship.

SchemaError

Schema validation failure (unknown property, type mismatch at pattern literal).

TransactionConflictError

A transaction's commit lost an optimistic-concurrency race.

ValidationError

Structural validation failure (missing required field, wrong connection endpoint).

Classes

Agg

Aggregation expression builders for add_properties().

EmbeddingModel

Protocol for embedding models passed to embed_texts / search_text.

FrozenGraph

An immutable, concurrently-readable snapshot of a graph.

KnowledgeGraph

A high-performance knowledge graph with typed nodes, connections, and

ResultIter

Iterator for ResultView. Converts one row per step.

ResultView

Lazy result container — data stays in Rust until accessed from Python.

Session

A thread-safe, shareable concurrency handle over a graph.

Spatial

Spatial compute expression builders for add_properties().

Transaction

An isolated transaction on a KnowledgeGraph.

Functions

attach_rows(→ int)

Attach a DataFrame to a parent node as row NODES plus edges.

check_file_freshness(→ list[dict[str, Any]])

Read-only drift check (binding-layer): snapshot each node's

cypher_pass_names(→ list[str])

Names of every Cypher optimizer pass, in execution order.

estimate_load_memory(→ dict[str, int])

Estimate what loading the .kgl at path would cost, without

from_blueprint(→ KnowledgeGraph)

Build a KnowledgeGraph from a JSON blueprint and its declared inputs.

from_bytes(→ KnowledgeGraph)

Load an in-memory graph from a .kgl byte buffer.

from_networkx(→ KnowledgeGraph)

Build a KnowledgeGraph from a networkx graph.

from_records(→ KnowledgeGraph)

Build a KnowledgeGraph from an inline JSON records spec.

get_query_warning_policy(→ str)

The query-warning policy currently in effect.

graphgen(→ KnowledgeGraph | dict[str, Any])

Generate a synthetic org/social knowledge graph (bundled generator).

load(→ KnowledgeGraph)

Load a graph from a binary file previously saved with save().

load_rdf(→ KnowledgeGraph)

Load an RDF file into a fresh in-memory graph.

open(→ KnowledgeGraph)

Open a graph at path — load it if it exists, create a fresh one if

open_session(→ Session)

Load a saved graph at path directly as a thread-safe Session.

outline(→ str)

Render the spanning tree from root along edge as a nested outline.

retry_on_conflict(→ Any)

Run work in a transaction, retrying the whole unit on conflict.

set_query_warning_policy(→ None)

Choose how Cypher query warnings are announced, process-wide.

stamp_file_freshness(→ int)

Capture each node's linked-file state into properties (binding-layer; the

to_neo4j(→ dict[str, Any])

Push graph data to a Neo4j database.

trim_memory(→ None)

Return allocator-retained memory to the operating system.

Package Contents

exception kglite.ArgumentError

Bases: KgError

A user-supplied argument violated a precondition.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ConnectionNotFoundError

Bases: KgError

A connection type isn’t declared in the schema.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ConstraintCreationError

Bases: ConstraintError

Declaring a constraint failed because the stored data already violates it.

Raised by KnowledgeGraph.define_schema() when the schema declares a unique tuple or primary_key that existing nodes already duplicate. Nothing is changed, so deduplicate the node type and call it again.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ConstraintError

Bases: KgError

Base class for declared-integrity-constraint failures.

Catch this to handle any constraint problem — a violated write or an uninstallable declaration — without distinguishing the two.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ConstraintViolationError

Bases: ConstraintError

A write violated a declared UNIQUE / NOT NULL / NODE KEY / IS :: TYPE constraint — on a node, or on a relationship (NOT NULL / IS :: TYPE).

The write was rejected before touching storage, so the graph is unchanged. Raised by cypher() for CREATE / MERGE / SET / REMOVE and by the bulk writers (add_nodes and everything funnelling through it).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.CypherError

Bases: KgError

Base for all Cypher-related errors (syntax, timeout, execution, type).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.CypherExecutionError

Bases: CypherError

Cypher executor failure during query evaluation.

line and col (1-indexed) are set as attributes when the failure is pinned to a source position; both are None otherwise.

Initialize self. See help(type(self)) for accurate signature.

col: int | None
line: int | None
exception kglite.CypherSyntaxError

Bases: CypherError

Cypher parser / tokenizer rejected the query.

line and col (1-indexed) are always present as attributes; both are None when the parser couldn’t pin the failure to a specific position (e.g. “expected end of input”).

Initialize self. See help(type(self)) for accurate signature.

col: int | None
line: int | None
exception kglite.CypherTimeoutError

Bases: CypherError

Cypher query exceeded its timeout_ms budget.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.CypherTypeMismatchError

Bases: CypherError

Cypher value-type mismatch (e.g. arithmetic on a String).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ExprError

Bases: KgError

Blueprint expression evaluation failure.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.FileError

Bases: KgError

A file the user named doesn’t exist on disk.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.FileFormatError

Bases: KgError

A file’s contents are malformed (bad .kgl header, truncated blueprint, etc.).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.FileIoError

Bases: KgError

Generic I/O failure (permission denied, mid-read EOF, mmap failure).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.InternalError

Bases: KgError

Invariant violation — kglite-internal bug. Reports the source location.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.InternerCollisionError

Bases: KgError

Two distinct names collided on one persisted interner key; no mutation was applied.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.KgError

Bases: Exception

Base class for typed KGLite engine failures.

Query, schema, graph-engine, transaction, and storage failures use this hierarchy. Python lookup, argument-shape, filesystem, and object-lifecycle protocols may instead raise their conventional built-in exceptions.

Every instance carries code, a stable classifier string, so an application can branch on the failure kind without matching message prose.

Initialize self. See help(type(self)) for accurate signature.

code: str | None

Stable error classifier — e.g. "ConstraintViolation", "TransactionConflict", "CypherSyntax".

Set on every raised instance, and also readable on the concrete classes themselves (kglite.ConstraintViolationError.code). It is None only on the three abstract bases — KgError, CypherError, ConstraintError — which span several codes. The same strings appear as KGLITE_STATUS_* in the C ABI and drive the Bolt Neo.* mapping.

exception kglite.LoadMemoryLimitError

Bases: KgError

A .kgl load was refused before decoding: its estimated memory exceeded the ceiling set by max_load_mb or KGLITE_MAX_LOAD_MB.

The file is valid and nothing was decompressed — this is a statement about the process’s budget, not about the data, which is why it is not FileFormatError. The message names the estimate, the ceiling, the terms it is made of, and the ways out. See estimate_load_memory().

Initialize self. See help(type(self)) for accurate signature.

exception kglite.MissingArgumentError

Bases: KgError

A required argument wasn’t passed.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.NodeNotFoundError

Bases: KgError

A node identified by (node_type, id) doesn’t exist.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.PropertyNotFoundError

Bases: KgError

A property is missing from a node or relationship.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.SchemaError

Bases: KgError

Schema validation failure (unknown property, type mismatch at pattern literal).

Initialize self. See help(type(self)) for accurate signature.

exception kglite.TransactionConflictError

Bases: KgError

A transaction’s commit lost an optimistic-concurrency race.

The graph advanced between KnowledgeGraph.begin() and Transaction.commit(), so the transaction’s working copy is stale and nothing was applied. Re-run the work against a fresh begin(); retry_on_conflict() is that loop.

Note this is a whole-graph version check, not a read/write-set intersection: a commit publishes the transaction’s working copy by pointer swap, so any concurrent commit conflicts — including one that touched entirely different nodes. See Concurrency.

Initialize self. See help(type(self)) for accurate signature.

exception kglite.ValidationError

Bases: KgError

Structural validation failure (missing required field, wrong connection endpoint).

Initialize self. See help(type(self)) for accurate signature.

class kglite.Agg

Aggregation expression builders for add_properties().

Each method returns the string expression that add_properties() already understands, making the DSL discoverable via autocomplete instead of requiring users to know the string syntax.

Example:

from kglite import Agg

graph.select('Well').traverse('HAS_BLOCK').add_properties({
    'Block': {'well_count': Agg.count(), 'avg_depth': Agg.mean('depth')}
})

Equivalent to the raw string form:

graph.select('Well').traverse('HAS_BLOCK').add_properties({
    'Block': {'well_count': 'count(*)', 'avg_depth': 'mean(depth)'}
})
static collect(prop: str) str

Comma-separated string of values — returns 'collect(prop)'.

static count() str

Count leaf nodes per ancestor — returns 'count(*)'.

static max(prop: str) str

Maximum value of a numeric property — returns 'max(prop)'.

static mean(prop: str) str

Arithmetic mean of a numeric property — returns 'mean(prop)'.

static min(prop: str) str

Minimum value of a numeric property — returns 'min(prop)'.

static std(prop: str) str

Sample standard deviation — returns 'std(prop)'.

static sum(prop: str) str

Sum a numeric property across leaves — returns 'sum(prop)'.

class kglite.EmbeddingModel

Bases: Protocol

Protocol for embedding models passed to embed_texts / search_text.

Requireddimension and embed() must be present.

Optionalload() and unload() are called automatically if present:

  • load() is called before each embed_texts() / search_text() call.

  • unload() is called after each call completes (even on error).

Optional — ``model_id`` / ``model_name`` (a str attribute): if present, its value is stamped onto the embedding store as provenance and surfaced via KnowledgeGraph.embedding_info() (model). An embedder without it works exactly the same, but the store records model=None — so a model swap can’t be detected from the store alone. Add a model_id to your embedder to light up provenance / model-swap detection.

This lets models manage heavyweight resources (GPU memory, large weights) on demand. A common pattern is to implement a cooldown in unload() so the model stays warm across rapid successive calls but eventually releases memory after a period of inactivity.

Example:

import threading
from sentence_transformers import SentenceTransformer

class Embedder:
    def __init__(self, model_name="all-MiniLM-L6-v2"):
        self._model_name = model_name
        self._model = None
        self._timer = None
        self.dimension = 384  # known ahead of time, or set in load()

    def load(self):
        if self._timer:
            self._timer.cancel()
            self._timer = None
        if self._model is None:
            self._model = SentenceTransformer(self._model_name)
            self.dimension = self._model.get_sentence_embedding_dimension()

    def unload(self, cooldown=60):
        def _release():
            self._model = None
            self._timer = None
        self._timer = threading.Timer(cooldown, _release)
        self._timer.start()

    def embed(self, texts: list[str]) -> list[list[float]]:
        return self._model.encode(texts).tolist()
embed(texts: list[str]) list[list[float]]

Embed a batch of texts, returning one vector per text.

load() None

(Optional) Load model weights / allocate resources.

Called automatically before embed() in embed_texts() and search_text(). If not defined, this step is skipped.

unload() None

(Optional) Release model weights / free resources.

Called automatically after embed_texts() and search_text() complete (including on error). A common pattern is to start a cooldown timer here instead of releasing immediately.

property dimension: int

The dimensionality of the embedding vectors.

class kglite.FrozenGraph

An immutable, concurrently-readable snapshot of a graph.

Created via KnowledgeGraph.freeze(). Shares the source graph’s data (an O(1) clone — no deep copy) and exposes only read methods, so any number of threads can query the same FrozenGraph in parallel without the single-owner borrow conflict a live KnowledgeGraph raises. Use the “build → freeze → share → swap” model: build a graph, freeze it, serve concurrent readers, and atomically swap in a new freeze() when the data changes.

cypher(query: str, to_df: bool = False, params: dict[str, Any] | None = None, timeout_ms: int | None = None, max_work_units: int | None = None, row_limit: int | None = None) Any

Run a read-only Cypher query against the snapshot.

Same read semantics as KnowledgeGraph.cypher()MATCH / WHERE / RETURN / aggregations, and semantic search via text_score() / vector_score(). A mutation query (CREATE / SET / DELETE / REMOVE / MERGE) raises ValueError — a frozen snapshot is immutable; mutate the source graph and take a fresh KnowledgeGraph.freeze().

Safe to call concurrently from many threads on the same snapshot.

row_limit caps the rows the call retains — the query still runs in full and only retention stops at the cap, so the rows kept are the first N of the uncapped answer (the genuine top-N under ORDER BY) and an explicit LIMIT m makes the effective cap min(m, row_limit). Truncation is never silent: it warns, and ResultView.diagnostics carries row_limit plus the exact pre-truncation total_rows.

node_count() int

Number of nodes in the snapshot.

property node_types: list[str]

Node type names present in the snapshot.

class kglite.KnowledgeGraph

A high-performance knowledge graph with typed nodes, connections, and a fluent query API backed by Rust.

Single-owner / threading. A KnowledgeGraph is single-owner: it is not safe to share one instance across threads while any thread mutates it (doing so raises a clear RuntimeError). For concurrent access, don’t share the graph — take a thread-safe handle off it: session() (shared reads + serialized writes, plus Session.cursor() for per-thread fluent chains) or freeze() (a lock-free read-only snapshot). See docs/concepts/concurrency.md.

Create an empty KnowledgeGraph.

Parameters:
  • storage – Storage mode. None (default) uses heap-resident storage, optimal for small-to-medium graphs. "mapped" uses mmap-backed columnar storage from the start, designed for large graphs that may approach or exceed available RAM. "disk" uses fully disk-backed storage for very large graphs (100M+ nodes). Requires path.

  • path – Directory path for disk-mode storage. Required when storage="disk". The directory IS the graph — data is written directly to disk via mmap. Load with kglite.load(path).

Note

This constructor is never durable, and takes no ``durable`` argument. It returns a detached graph — no source_path, so a bare save() asks for an explicit path and there is nowhere for a write-ahead log to live. kglite.open() is the durable entry point: it binds the graph to a path and defaults to durable="full". So KnowledgeGraph(storage="mapped") is mapped-and-unlogged while kglite.open(new_path, storage="mapped") is mapped-and-logged. The difference is structural rather than a defaulting inconsistency, but it is easy to trip over when comparing the two.

Note also that mutating statements on a "mapped" or "disk" graph do not use the cheap statement-rollback journal — see kglite.open() and the storage-mode guide.

add_connections(data: pandas.DataFrame | None, connection_type: str, source_type: str, source_id_field: str, target_type: str, target_id_field: str, source_title_field: str | None = None, target_title_field: str | None = None, columns: list[str] | None = None, skip_columns: list[str] | None = None, conflict_handling: str | None = None, column_types: dict[str, str] | None = None, query: str | None = None, extra_properties: dict[str, Any] | None = None, git_sha: str | None = None, modified_by: str | None = None, on_invalid: Literal['warn', 'error', 'skip'] = 'warn') dict[str, Any]

Add connections (edges) between existing nodes.

Two modes — supply either data (a pandas DataFrame) or query (a Cypher string whose RETURN columns provide source/target IDs).

Example (from DataFrame):

graph.add_connections(df, 'KNOWS', 'Person', 'src_id', 'Person', 'tgt_id')

Example (from Cypher query):

graph.add_connections(
    None, 'ENCLOSES', 'Play', 'play_id', 'StructuralElement', 'struct_id',
    query="""
        MATCH (p:Play), (s:StructuralElement)
        WHERE contains(p, s)
        RETURN DISTINCT p.id AS play_id, s.id AS struct_id
    """,
)

Example (query with extra properties):

graph.add_connections(
    None, 'HC_IN_FORMATION', 'Discovery', 'src', 'Stratigraphy', 'tgt',
    query='MATCH ... RETURN d.id AS src, s.id AS tgt',
    extra_properties={'hc_rank': 1},
)
Parameters:
  • data – DataFrame containing edge data, or None when using query.

  • connection_type – Label for this edge type (e.g. 'KNOWS'). Any characters are accepted here — including hyphens/dots/spaces ('supports-claim'). Such a type only needs backtick-quoting when named inside a Cypher query ([r:`supports-claim`]).

  • source_type – Node type of source nodes.

  • source_id_field – Column with source node IDs (must appear in DataFrame or query RETURN).

  • target_type – Node type of target nodes.

  • target_id_field – Column with target node IDs (must appear in DataFrame or query RETURN).

  • source_title_field – Optional title column for source nodes.

  • target_title_field – Optional title column for target nodes.

  • columns – Optional whitelist of property columns (data mode only). When omitted, every DataFrame column is preserved except those named by skip_columns, matching add_nodes().

  • skip_columns – Columns to exclude (data mode only).

  • conflict_handling'update' (default), 'replace', 'skip', 'preserve', or 'sum'. 'sum' adds numeric edge properties (Int64+Int64, Float64+Float64; mixed promotes to Float64). Non-numeric properties overwrite like 'update'.

  • column_types – Override column dtypes (data mode only).

  • query – Cypher query string (alternative to data). Must be a read-only query whose RETURN clause includes columns matching source_id_field and target_id_field.

  • extra_properties – Dict of static properties to add to every edge created from the query results (query mode only).

  • git_sha – Commit SHA stamped when the edge type has auto_timestamp=True.

  • modified_by – Actor id stamped when the edge type has auto_timestamp=True.

  • on_invalid – What to do about rows whose source or target ID is null. 'warn' (default) skips them, counts them in the report and emits a UserWarning; 'error' refuses the whole call with ArgumentError, naming the count, the first offending row and the value it holds, before anything is written; 'skip' is 'warn' without the warning. A row whose endpoint is missing rather than null is vivified as a stub node, not skipped, and is unaffected by this setting.

Returns:

Operation report dict with connections_created, connections_skipped, etc.

add_connections_bulk(connections: list[dict[str, Any]], *, git_sha: str | None = None, modified_by: str | None = None) dict[str, int]

Add multiple connection types at once.

Each dict must contain source_type, target_type, connection_name, and data (DataFrame with source_id/target_id columns). git_sha and modified_by apply to every opted-in edge type.

Returns:

Mapping of connection_name to count of connections created.

add_connections_from_source(connections: list[dict[str, Any]], *, git_sha: str | None = None, modified_by: str | None = None) dict[str, int]

Add connections, auto-filtering to types already loaded in the graph.

Same spec format as add_connections_bulk(), but silently skips connection specs whose source or target type is not in the graph. git_sha and modified_by apply to every loaded opted-in edge type.

Returns:

Mapping of connection_name to count of connections created.

add_embeddings(node_type: str, text_column: str, embeddings: dict[Any, list[float]], metric: str | None = None) dict[str, Any]

Add or update embeddings without discarding the existing store.

Differs from set_embeddings() (which replaces the store) by upserting entries into an existing (node_type, "{text_column}_emb") store. If no store exists yet, behaves like set_embeddings — the first call creates one; subsequent calls extend it.

Use this for incremental ingest workflows where multiple add_nodes + embedding batches need to coexist without a read-merge-write cycle through the user’s process.

Resolves and validates text_column exactly as set_embeddings() does — stored property, identity alias (title_field/id_field column names), id/title, or a structural alias — and keys the store by the spelling you pass. The whole batch is resolved and dimension-checked before anything is written, so a rejected call leaves the store exactly as it was.

Call save() to persist the store: embedding stores ride the checkpoint. A store records the vectors, dimension and metric you supply here; embed_texts() additionally records the model id and per-node text hashes.

Parameters:
  • node_type – The node type (e.g. 'Article').

  • text_column – Source text column name (e.g. 'summary').

  • embeddings – Dict mapping node IDs to embedding vectors. An id that matches no node of this type is counted in skipped. When a store already exists its dimension is authoritative and every vector must match it.

  • metric – Applies to the call that creates the store; a later call extends the store the first one made, whose metric stands.

Returns:

Dict with embeddings_stored (total in store after the upsert), dimension, skipped (unknown ids), and store_created (True iff this call created the store).

add_label(node_type: str, ids: list[Any], label: str) dict[str, int]

Add a secondary label to a batch of nodes by id.

Secondary labels are queryable via Cypher (MATCH (n:Label)) and surfaced by labels(n). The primary type (set via add_nodes(node_type=...)) is immutable. SET n.type writes an ordinary property; changing the primary type requires recreating or migrating the node.

Parameters:
  • node_type – Primary type of the nodes.

  • ids – Node ids (the unique_id_field values).

  • label – Secondary label to add.

Returns:

Dict with labelled (newly added) and skipped (unknown ids, or label already present).

Example:

graph.add_label('Agent', ['ag_001', 'ag_002'], 'Reviewer')
graph.cypher('MATCH (a:Reviewer) RETURN a.id').to_list()
add_nodes(data: pandas.DataFrame, node_type: str, unique_id_field: str, node_title_field: str | None = None, columns: list[str] | None = None, conflict_handling: str | None = None, skip_columns: list[str] | None = None, column_types: dict[str, str] | None = None, timeseries: dict[str, Any] | None = None, nullable_int_downcast: bool = False, labels: list[str] | None = None, managed_reload: bool = False, git_sha: str | None = None, modified_by: str | None = None, on_invalid: Literal['warn', 'error', 'skip'] = 'warn') dict[str, Any]

Add nodes from a DataFrame.

String and integer IDs are auto-detected from the DataFrame dtype. Non-contiguous DataFrame indexes (e.g. from filtering) are handled automatically.

When timeseries is provided, the DataFrame may contain multiple rows per unique ID (one per time step). Rows are deduplicated automatically — the first occurrence per ID provides static node properties, and all rows contribute to the timeseries channels.

Parameters:
  • data – DataFrame containing node data.

  • node_type – Label for this set of nodes (e.g. 'Person').

  • unique_id_field – Column used as unique identifier. Text ids are stored as strings and integer ids as a compact 32-bit key; an integer column holding any value outside 0..2**32-1 (negatives, snowflake ids, hashes) is stored as a full 64-bit key instead, so no row is dropped for being out of range.

  • node_title_field – Column used as display title. Defaults to unique_id_field.

  • columns – Whitelist of columns to include. None = all.

  • conflict_handling

    'update' (default), 'replace', 'skip', 'preserve', or 'sum'. 'sum' acts as 'update' for nodes.

    Partial-update guarantee: 'update' writes only the columns present in this call’s data — properties of an existing node that are not in the incoming columns are left untouched. This is a stable contract: it lets a batch reload re-assert a subset of fields (e.g. identity + links) without clobbering fields another writer owns (e.g. an agent’s status/notes). 'replace' instead reconciles the node to the incoming record — it overwrites the whole node, so a property absent from the new data is dropped (set to null). Use 'replace' (not 'update') when the source data is the single source of truth and you want field deletions to propagate on rebuild.

  • skip_columns – Columns to exclude.

  • column_types – Override column dtypes, e.g. {'col': 'string'}. Supported: 'string', 'integer', 'float', 'datetime', 'timestamp', 'uniqueid', 'list', 'map'. A column of Python lists/tuples is auto-detected as a native 'list' property (stored structurally, not stringified), so 'y' IN n.aliases tests membership and UNWIND n.aliases yields the elements; pass 'list' explicitly to force it. A column of Python dicts is auto-detected as a native 'map' property (n.meta['k'] / n.meta.k read back the value) rather than being stringified. A datetime64 column keeps its full time-of-day when any value has a nonzero time (stored as a Timestamp); a pure-midnight column stays date-only ('datetime'). Pass 'timestamp' to force full date+time, 'datetime' to force date-only. Also supports spatial types: 'location.lat', 'location.lon', 'geometry', 'point.<name>.lat', 'point.<name>.lon', 'shape.<name>'.

  • nullable_int_downcast – When True, Float64 columns whose non-null values are all integer-valued (e.g. pd.NA-bearing ints that pandas auto-promoted to float64) are silently downcast to Int64. Default False — explicit opt-in protects existing callers.

  • managed_reload

    When True, this call is part of a managed reload (a batch writer rebuilding from source). If node_type declares layer='runtime' in the schema (an agent-owned type), the write is skipped as a no-op and the returned report carries the normal shape plus skipped_runtime_layer=True, node_type and message. Undeclared or layer='managed' types are written normally. Pairs with the layer declaration in define_schema() and conflict_handling.

    What this does and does not guarantee. It is a guard the rebuilding side opts into, not an enforced perimeter: an add_nodes call that omits managed_reload writes a runtime type normally, nothing stops a runtime writer (Cypher, or another loader call) from mutating or deleting managed nodes, and add_connections() is not covered at all. Use write_scope on the Cypher path when the goal is to refuse out-of-role writes rather than to keep a well-behaved rebuild in its lane.

  • timeseries

    Inline timeseries configuration dict with keys:

    • time (required): column name containing date strings ('yyyy-mm', 'yyyy-mm-dd', 'yyyy-mm-dd hh:mm'), or a dict mapping year/month/day/hour/minute to column names (e.g. {'year': 'ar', 'month': 'maned'}).

    • channels (required): list of column names for timeseries data (e.g. ['oil', 'gas', 'condensate']).

    • resolution (optional): 'year', 'month', 'day', 'hour', or 'minute'. Auto-detected from time format if omitted.

    • units (optional): dict mapping channel names to unit strings (e.g. {'oil': 'MSm3'}).

  • labels – Optional secondary labels to apply to every node in the batch. add_nodes(df, 'Agent', 'id', 'name', labels=['Reviewer']) creates Agent-typed nodes that also wear the Reviewer label, queryable via MATCH (a:Reviewer) or MATCH (a:Agent:Reviewer). For per-row labels, call add_label() after.

  • git_sha – Commit SHA stamped on opted-in auto_timestamp types.

  • modified_by – Actor id stamped on opted-in auto_timestamp types.

  • on_invalid

    What to do about input rows this call cannot use — a row whose ID is null, or holds a value the declared ID type cannot store.

    • 'warn' (default) — load the usable rows, count the rest in the report, and emit a UserWarning.

    • 'error' — refuse the whole call. Raises ArgumentError naming how many rows are unusable, the first offending row’s position and what it holds. Nothing is written, so the graph is exactly as it was.

    • 'skip' — as 'warn', without the warning. The counts stay in the returned report.

    'error' also refuses an object-dtype column that would be stringified wholesale (see column_types).

Returns:

Operation report dict with keys nodes_created, nodes_updated, nodes_skipped, processing_time_ms, has_errors, and optionally errors with skip reasons.

Example:

graph.add_nodes(df, 'Production', 'field_id', 'field_name',
    timeseries={
        'time': 'date',
        'channels': ['oil', 'gas', 'condensate', 'oe'],
    })
add_nodes_bulk(nodes: list[dict[str, Any]], *, git_sha: str | None = None, modified_by: str | None = None) dict[str, int]

Add multiple node types at once.

Each dict in nodes must contain node_type, unique_id_field, node_title_field, and data (a DataFrame). git_sha and modified_by apply to every opted-in node type.

Returns:

Mapping of node_type to count of nodes added.

add_properties(properties: dict[str, list[str] | dict[str, str]], keep_selection: bool | None = None) KnowledgeGraph

Enrich selected nodes with properties from ancestor nodes in the traversal chain.

Copies, renames, aggregates, or computes spatial properties from nodes at other levels of the selection hierarchy onto the current leaf nodes.

Parameters:
  • properties

    Dict mapping source node type → property spec:

    • {'B': ['name', 'status']} — copy listed properties as-is

    • {'B': []} — copy all properties from B

    • {'B': {'new_name': 'old_name'}} — copy with rename

    • {'B': {'avg_depth': 'mean(depth)'}} — aggregate functions: count(*), sum(prop), mean(prop), min(prop), max(prop), std(prop), collect(prop)

    • {'B': {'dist': 'distance'}} — spatial compute: distance, area, perimeter, centroid_lat, centroid_lon

  • keep_selection – Preserve current selection. Default True.

Returns:

A new KnowledgeGraph with the properties added to selected nodes.

Examples:

# Copy structure name onto wells
graph.select('Structure').compare('Well', 'contains') \
    .add_properties({'Structure': ['name', 'status']})

# Rename properties
graph.select('Structure').compare('Well', 'contains') \
    .add_properties({'Structure': {'struct_name': 'name'}})

# Aggregate with Agg helpers (discoverable via autocomplete)
from kglite import Agg, Spatial
graph.select('Structure').compare('Well', 'contains') \
    .add_properties({'Well': {
        'well_count': Agg.count(),
        'avg_depth': Agg.mean('depth'),
    }})

# Spatial compute with Spatial helpers
graph.select('Structure').compare('Well', 'contains') \
    .add_properties({'Structure': {
        'dist_to_center': Spatial.distance(),
        'parent_area': Spatial.area(),
    }})

Note

Not available on a graph opened with durable=. The write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log, so it is refused rather than left unlogged — use cypher(), which expresses the same write and is logged. See kglite.open().

add_timeseries(node_type: str, *, data: Any, fk: str, time_key: list[str], channels: dict[str, str] | list[str], resolution: str | None = None, units: dict[str, str] | None = None) dict[str, Any]

Bulk-load timeseries data from a DataFrame.

Groups rows by fk, sorts by time_key, and attaches the resulting timeseries to matching nodes (found by node ID).

Time keys are combined into NaiveDate internally: - Single column: parsed as date strings ('2020-06') - Multiple columns: combined as year + month [+ day] → NaiveDate

Parameters:
  • node_type – Target node type.

  • data – Source DataFrame.

  • fk – Foreign key column in data linking to node IDs.

  • time_key – Column(s) for time keys. If single column, values are parsed as date strings. If multiple, combined as year + month [+ day].

  • channels – Either a list of column names (used as channel names) or a dict mapping {channel_name: column_name}.

  • resolution – Time granularity ('year', 'month', 'day'). Auto-detected from time_key count if not specified.

  • units – Optional channel→unit map, merged into config.

Returns:

{'nodes_loaded': N, 'total_records': M, 'total_rows': R}.

Return type:

Summary

add_ts_channel(node_id: Any, channel_name: str, values: list[float]) None

Add a timeseries channel to a node.

The node must already have a time index set (via set_time_index or add_timeseries). The values length must match the time index length. Use float('nan') for missing values.

Parameters:
  • node_id – The node’s unique ID.

  • channel_name – Channel name (e.g. 'oil', 'temperature').

  • values – Float values aligned with the time index.

all_paths(source_type: str, source_id: Any, target_type: str, target_id: Any, max_hops: int | None = None, max_results: int | None = None, connection_types: list[str] | None = None, via_types: list[str] | None = None, timeout_ms: int | None = None, direction: str | None = None) list[dict[str, Any]]

Find all paths between two nodes.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • max_hops – Maximum path length. Default 5.

  • max_results – Stop after finding this many paths. Default unlimited. Use to prevent OOM on dense graphs.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • timeout_ms – Stop searching after this many milliseconds; paths found before the deadline are returned.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

Returns:

List of path dicts, each with path, connections, length.

are_connected(source_type: str, source_id: Any, target_type: str, target_id: Any, connection_types: list[str] | None = None, via_types: list[str] | None = None, direction: str | None = None, timeout_ms: int | None = None) bool

Check if two nodes are connected (directly or indirectly).

True exactly when shortest_path_length() with the same arguments returns a distance.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

  • timeout_ms – Abort after this many milliseconds and return False.

begin(timeout_ms: int | None = None) Transaction

Begin a read-write transaction with a lazy copy-on-write snapshot.

begin() is O(1). The first mutation creates a backend-specific working fork; all transaction mutations remain isolated until commit(). Rollback (or dropping without commit) discards the fork.

Uses optimistic concurrency control: commit() will raise a typed KgError if the graph changed since begin().

Parameters:

timeout_ms – Optional transaction-level timeout in milliseconds. If set, operations after the deadline raise CypherTimeoutError.

Can be used as a context manager:

with graph.begin() as tx:
    tx.cypher("CREATE (n:Person {name: 'Alice', age: 30})")
    tx.cypher("CREATE (n:Person {name: 'Bob', age: 25})")
    # auto-commits on success, auto-rollbacks on exception
begin_read(timeout_ms: int | None = None) Transaction

Begin a read-only transaction — O(1) cost, zero memory overhead.

Returns a Transaction backed by an Arc reference to the current graph state. Mutations (CREATE, SET, DELETE, REMOVE, MERGE) are rejected.

Ideal for concurrent read-heavy workloads (e.g. MCP server agents) where you want a consistent snapshot without the cost of a full clone.

Parameters:

timeout_ms – Optional transaction-level timeout in milliseconds.

Can be used as a context manager:

with graph.begin_read() as tx:
    result = tx.cypher("MATCH (n:Person) RETURN n.name")
    # auto-closes on exit (no commit needed)
betweenness_centrality(normalized: bool | None = None, sample_size: int | None = None, connection_types: str | list[str] | None = None, top_k: int | None = None, timeout_ms: int | None = None, to_df: bool | None = None) ResultView | pandas.DataFrame

Calculate betweenness centrality.

Parameters:
  • normalized – Normalise scores to [0, 1]. Default True.

  • sample_size – Sample source nodes for faster computation on large graphs.

  • connection_types – Only traverse these relationship types (str or list).

  • top_k – Return only the top K nodes.

  • timeout_ms – Abort after this many milliseconds with an error.

  • to_df – Return a pandas DataFrame with columns type, title, id, score.

Returns:

A ResultView of rows with type, title, id, score, sorted by score descending. Or a pandas DataFrame if to_df=True.

bounds(lat_field: str | None = None, lon_field: str | None = None, as_shapely: bool = False) dict[str, float] | Any | None

Get geographic bounds of selected nodes.

Parameters:
  • lat_field – Latitude property name. Default from spatial config or 'latitude'.

  • lon_field – Longitude property name. Default from spatial config or 'longitude'.

  • as_shapely – If True, return a shapely.geometry.Polygon (box) instead of a dict.

Returns:

Dict with min_lat, max_lat, min_lon, max_lon, or a shapely box polygon when as_shapely=True, or None if no valid coordinates found.

bug_report(query: str, result: str, expected: str, description: str, path: str | None = None) str

File a Cypher bug report to reported_bugs.md.

Appends a timestamped, version-tagged report to the top of the file (creating it if needed). All inputs are sanitised against code injection (HTML tags, javascript: URIs, triple-backtick breakout).

Parameters:
  • query – The Cypher query that triggered the bug.

  • result – The actual result you got.

  • expected – The result you expected.

  • description – Free-text explanation.

  • path – Optional file path (default: reported_bugs.md in cwd).

Returns:

Confirmation message with the file path.

Raises:

IOError – If the file cannot be written.

build_id_indices(node_types: list[str] | None = None) None

Pre-build ID lookup indices for fast node() calls.

Parameters:

node_types – Types to index. None indexes all types.

build_text_index(node_type: str, property: str, auto_refresh_limit: int | None = None) dict[str, Any]

Build a BM25 lexical index over a node type’s string property, for keyword/full-text ranking.

Query it with the Cypher scalar text_bm25(n, '<property>', '<query text>'), which returns that row’s BM25 relevance — 0.0 for an indexed document sharing no word with the query, null for a row the index holds no document for, and an error when no index exists.

Opt-in and explicit, like create_index(): nothing builds one for you. After the build the index does not follow writes eagerly — it records that they happened and folds them in when a query next reads it, as long as the outstanding delta is at or under auto_refresh_limit. Past that limit it serves what it has and says so, rather than putting an open-ended catch-up inside your query; call this method again to rebuild, which replaces the index wholesale. SHOW INDEXES reports both facts, in its stale and delta columns.

What catching up costs. Folding one document in is not a constant: it inserts into the posting list of every term the document uses, and those lists grow with the corpus, so the per-document cost rises as the index does (measured 2026-08-25: 0.08 ms per document over a 20k-document corpus, 0.4 ms over a 100k one). Past roughly 1500 documents that overtakes a full rebuild, and the catch-up rebuilds instead — so a refresh costs the cheaper of the two, never more than one rebuild, and raising auto_refresh_limit well above that point buys rebuilds rather than an ever-slower fold.

Catching up costs the writes almost nothing: creations are noticed by comparing one node slot against a watermark, so bulk ingest into an indexed graph runs at the speed it would without one, and a graph with no text index at all pays a single branch.

Deletion is handled at the delete, not by catch-up: deleting a node prunes its document immediately, because the freed node slot is handed to the next node created and an orphaned document would be inherited by it.

The property is read through the same alias resolution a Cypher MATCH filter uses, so a type’s id/title column can be indexed under the name the loader gave it (add_nodes(df, "Person", "npdid", "name") makes build_text_index("Person", "name") index titles). The index is keyed by the spelling you pass, not by what it resolves to.

What is indexed. Every node of the type whose property holds a string. An empty string is a document — one with no terms, counted in the corpus statistics. A node whose property is absent or holds a non-string (a number, a list) is skipped: BM25 indexes text, and a stringified number is not text. Skipped nodes are counted in the return value.

Tokenization is the character rule text_normalize() exposes: alphanumeric runs are terms, everything else separates, and terms are lowercased per character. Unicode letters are term content (Tromsø is one token), there is no stemming and no stopword list, and there is no CJK segmentation.

The index is heap-resident and is dropped by vacuum(), which renumbers every node — rebuild after vacuuming. It is saved with the graph: save() writes it into the .kgl as its own section, carrying its staleness, and kglite.load() restores both — a reloaded index that was stale is still stale by the same delta. The section is a rebuildable cache, so a file whose index format this build does not recognise loads without the index rather than failing; rebuild it in that case. It appears in SHOW INDEXES / schema() under the canonical name '{node_type}.{property}' with type FULLTEXT, alongside any equality or range index on the same property, and DROP INDEX {node_type}.{property} removes all of them.

Storage modes: the default (in-memory) and 'mapped' backends build; the 'disk' backend refuses, because a heap-resident inverted index over a graph sized for disk mode is the memory cliff that backend exists to avoid.

Parameters:
  • node_type – The node type to index (e.g. 'Article').

  • property – The string property to index (e.g. 'body').

  • auto_refresh_limit – How many changed documents a query will fold in inline before it serves stale results and warns instead. Defaults to 1000. It bounds a document count, not a duration — see “What catching up costs” above for what a delta of that size is worth in time. A rebuild that omits this keeps whatever the existing index used, so refreshing an index does not quietly restore the default.

Returns:

{'indexed': int, 'skipped': int, 'terms': int} — documents indexed, nodes skipped as absent/non-string, and the size of the resulting vocabulary.

Return type:

dict

Raises:

ValueError – if the node type is unknown, the graph is disk-backed, or the type has nodes but not one of them carries a string for property (what a misspelled property name looks like). A type with no nodes yet builds an empty index instead, so an index can be declared before ingest.

Example:

g.build_text_index("Article", "body")
# {'indexed': 1200, 'skipped': 3, 'terms': 18422}
g.cypher(
    "MATCH (a:Article) "
    "RETURN a.title, text_bm25(a, 'body', 'low light') AS score "
    "ORDER BY score DESC LIMIT 10"
)
build_vector_index(node_type: str, text_column: str, m: int | None = None, ef_construction: int | None = None, ef_search: int | None = None, metric: str | None = None, auto_refresh_limit: int | None = None) dict[str, Any]

Build an HNSW approximate-nearest-neighbour index over an embedding store so vector search scales sub-linearly on large stores.

Opt-in, like create_index(): without it, vector search is an exact brute-force scan. Once built, vector_search() / search_text() auto-use the index for queries covering most of a large store; pass exact=True to force an exact scan.

Later vector writes do not drop the index. add_embeddings / embed_texts / set_embeddings leave the slot layout alone, so they are recorded and folded in at query entry while the outstanding delta stays at or under auto_refresh_limit; a larger delta is served by the exact scan — correct, and slower — until you rebuild or call refresh_vector_index(). SHOW INDEXES reports both facts, in its stale and delta columns.

Catch-up never embeds. A node with no vector is not part of the delta: it is counted in SHOW INDEXESunembedded column and stays invisible to vector search until you embed it. No query turns into an embedding run.

What does drop the index is a change to the slot layout the index addresses: deleting an embedded node (the delete prunes its vector), rolling that delete back, and vacuum(). Rebuild after those.

The selection does not have to be node_type: while only one node type carries text_column, a whole-graph search on a multi-type graph uses the index too. When two or more types carry the same column, only a selection of a single one of them can — a selection spanning both is ranked by exact scan so neither type’s rows are dropped.

Requires an existing embedding store; build it after ingest. The index is a rebuildable cache: a .kgl carries it, and a graph whose stored index no longer matches its vectors loads and searches exactly.

Parameters:
  • node_type – The node type (e.g. 'Article').

  • text_column – Source column name (e.g. 'summary'; the store is '{text_column}_emb').

  • m – Max neighbours per node on upper layers (default 16). Higher → better recall, larger index.

  • ef_construction – Build-time search width (default 200). Higher → better graph, slower build.

  • ef_search – Default query-time search width (default 64). Higher → better recall, slower query. Recall at the default is ≥0.99 on structured embeddings but degrades on unclustered high-dimensional vectors, where raising it helps only marginally — see Recall on hard corpora in the semantic-search guide for the measured numbers.

  • metric'cosine' (default), 'dot_product', or 'euclidean'. 'poincare' is unsupported (stays exact). If omitted, uses the store’s metric, else 'cosine'.

  • auto_refresh_limit – How many outstanding vectors a query folds into the index inline before it serves the exact scan instead (default 1000). Omit on a rebuild to keep the current value.

Returns:

{'indexed': int, 'metric': str, 'm': int}.

Return type:

dict

Raises:

ValueError – if the store doesn’t exist or the metric is unsupported. A text_column that is itself a store name ('summary_emb') is named as such, with the column that would have worked.

Example:

g.embed_texts("Article", "summary")
g.build_vector_index("Article", "summary")          # opt in
hits = g.select("Article").search_text("summary", "AI", top_k=10)
calculate(expression: str, level_index: int | None = None, store_as: str | None = None, keep_selection: bool | None = None, aggregate_connections: bool | None = None) Any

Evaluate a mathematical expression on selected nodes.

Supports property references, arithmetic operators, and aggregate functions (sum, mean, std, min, max, count).

Parameters:
  • expression – Expression string, e.g. 'price * quantity' or 'mean(age)'.

  • level_index – Target level in the hierarchy.

  • store_as – If set, stores results as this property on nodes.

  • keep_selection – Preserve selection after store. Default False.

  • aggregate_connections – Aggregate over connected nodes.

Returns:

Computation results, or a KnowledgeGraph if store_as is set.

Note

store_as= writes, and the write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log — so it is refused on a graph opened with durable= rather than left unlogged. Use cypher() there; see kglite.open().

centroid(lat_field: str | None = None, lon_field: str | None = None, as_shapely: bool = False) dict[str, float] | Any | None

Get the geographic centroid (average lat/lon) of selected nodes.

Parameters:
  • lat_field – Latitude property name. Default from spatial config or 'latitude'.

  • lon_field – Longitude property name. Default from spatial config or 'longitude'.

  • as_shapely – If True, return a shapely.geometry.Point instead of a dict.

Returns:

Dict with latitude and longitude, or a shapely Point when as_shapely=True, or None.

clear() None

Clear the current selection (resets to empty).

clear_ontology() None

Remove the declared semantic layer entirely.

Materialized labels (if any) are withdrawn first, so a store-less graph never carries managed buckets nothing can explain.

clear_schema() KnowledgeGraph

Remove the schema definition from the graph, and with it every constraint the schema declared.

Returns this same graph (not a copy), so the call can be chained.

The unique indexes a primary_key/unique declaration installed are withdrawn too — enforcement never outlives the declaration that explains it. Constraints declared through Cypher DDL (CREATE CONSTRAINT) are separate declarations and survive; drop them with DROP CONSTRAINT.

close() None

Persist the graph to its remembered origin path (the file it was opened from via kglite.open() / kglite.load(), or last saved to). No-op if the graph has no associated path. The graph stays usable after close().

closeness_centrality(normalized: bool | None = None, sample_size: int | None = None, connection_types: str | list[str] | None = None, top_k: int | None = None, timeout_ms: int | None = None, to_df: bool | None = None) ResultView | pandas.DataFrame

Calculate closeness centrality.

Parameters:
  • normalized – Adjust for disconnected components. Default True.

  • sample_size – Approximate by sampling N source nodes (faster for large graphs). If None, uses all nodes.

  • connection_types – Only traverse these relationship types (str or list).

  • top_k – Return only the top K nodes.

  • timeout_ms – Abort after this many milliseconds with an error.

  • to_df – Return a pandas DataFrame with columns type, title, id, score.

Returns:

A ResultView of rows with type, title, id, score, sorted by score descending. Or a pandas DataFrame if to_df=True.

collect(limit: int | None = None) ResultView

Materialise selected nodes as a flat ResultView.

For grouped output by parent type, use collect_grouped() instead.

Parameters:

limit – Maximum number of nodes to return.

Returns:

A ResultView containing id, title, type, and all stored properties for each selected node.

collect_children(property: str | None = None, where: dict[str, Any] | None = None, sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None, store_as: str | None = None, max_length: int | None = None, keep_selection: bool | None = None) Any

Collect child-node property values into comma-separated lists.

Parameters:
  • property – Child property to collect. Default 'title'.

  • where – Filter conditions for children.

  • sort – Sort children.

  • limit – Limit children per parent.

  • store_as – If set, stores the list as this property on parent nodes.

  • max_length – Max string length when storing.

  • keep_selection – Preserve selection after store. Default False.

Returns:

Dict of {parent_title: 'val1, val2, ...'} or a KnowledgeGraph if store_as is set.

Note

Not available on a graph opened with durable=. store_as= writes, and the write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log, so it is refused rather than left unlogged — use cypher(), which expresses the same write and is logged. See kglite.open().

collect_grouped(group_by: str, *, parent_info: bool = False, flatten_single_parent: bool = True, limit: int | None = None) dict[str, Any]

Materialise selected nodes grouped by a parent type in the traversal hierarchy.

Parameters:
  • group_by – Parent node type to group by (must exist in the traversal chain).

  • parent_info – Include parent metadata (type, id, title) in each group. Default False.

  • flatten_single_parent – If only one parent group exists, return a flat list instead of a single-key dict. Default True.

  • limit – Maximum number of nodes to return.

Returns:

Dict mapping parent title → list of node dicts. If flatten_single_parent is True and there is only one parent, returns a flat list.

Examples:

# Group wells by their parent field
graph.select('Field').traverse('HAS_WELL') \
    .collect_grouped('Field')
# → {'TROLL': [...], 'EKOFISK': [...]}

# Include parent metadata
graph.select('Field').traverse('HAS_WELL') \
    .collect_grouped('Field', parent_info=True)
compact() int

Merge a disk-mode graph’s overflow edges back into its CSR arrays.

Overflow edges accumulate when edges are added after the initial CSR build (e.g., after loading a graph and adding new connections). Compaction rebuilds the CSR to include all overflow edges, restoring optimal query performance.

Edges only — this does not reclaim anything a delete left behind. Dead columnar rows are dropped by save(), which rewrites a disk graph’s columns without them; node slots freed by a delete are not reclaimed by either, so a disk graph’s node capacity only shrinks when the directory is rebuilt from a fresh ingest.

Returns:

Number of overflow edges that were merged. Returns 0 if there are no overflow edges or the graph is not in disk mode.

compare(target_type: str | list[str], method: str | dict[str, Any], *, filter: dict[str, Any] | None = None, sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None, level_index: int | None = None, new_level: bool | None = None) KnowledgeGraph

Compare selected nodes against a target type using spatial, semantic, or clustering methods.

Parameters:
  • target_type – Node type to compare against (e.g. 'Well'). Exactly one type is supported: a bare string, or a single-element list for symmetry with traverse(). A list of two or more raises ArgumentError rather than comparing against only the first — call compare() once per type instead.

  • method

    Comparison method — a string shorthand or a dict with settings:

    Spatial methods:

    • 'contains' — point-in-polygon or polygon containment

    • 'intersects' — polygon-polygon intersection

    • {'type': 'distance', 'max_m': 5000} — geodesic distance

    Semantic methods:

    • {'type': 'text_score', 'property': 'name'} — embedding similarity

    • {'type': 'text_score', 'threshold': 0.7} — with threshold

    • {'type': 'text_score', 'metric': 'poincare'} — Poincaré distance (hierarchical data)

    Clustering methods:

    • {'type': 'cluster', 'k': 5} — K-means clustering

    • {'type': 'cluster', 'algorithm': 'dbscan', 'eps': 0.5} — DBSCAN

    Common dict keys:

    • resolve: 'centroid', 'closest', or 'geometry'

    • max_m: Maximum distance in meters (distance method)

    • threshold: Minimum similarity score (semantic methods)

    • k: Number of clusters (K-means)

    • features: Properties to cluster on

  • filter – Property conditions for target nodes.

  • sort – Sort results. Field name or [(field, ascending)] list.

  • limit – Max target nodes per source.

  • level_index – Source level in the hierarchy (advanced).

  • new_level – Add targets as new hierarchy level. Default True.

Returns:

A new KnowledgeGraph with comparison results selected.

Raises:
  • ArgumentError – If target_type is not a string or list of strings, or is a list holding more than one type; if the method dict carries an unknown resolve mode; or if the comparison itself rejects its inputs (unknown method name, a method used without its required target_type or settings such as max_m / property / features).

  • TypeError – If method is neither a string nor a dict.

Examples:

# Spatial containment: find wells inside structures
graph.select('Structure').compare('Well', 'contains')

# Distance: wells within 5km
graph.select('Well').compare('Well',
    {'type': 'distance', 'max_m': 5000})

# Semantic similarity
graph.select('Document').compare('Document',
    {'type': 'text_score', 'property': 'summary', 'threshold': 0.7})

# Clustering
graph.select('Well').compare('Well',
    {'type': 'cluster', 'k': 5, 'features': ['latitude', 'longitude']})
composite_index_stats(node_type: str, properties: list[str]) dict[str, Any] | None

Get statistics for a composite index. Returns None if not found.

connected_components(weak: bool | None = None, titles_only: bool | None = None) list[list[dict[str, Any]]]

Find connected components in the graph.

Parameters:
  • weak – If True (default), find weakly connected components. If False, find strongly connected components.

  • titles_only – If True, return lists of node titles instead of full dicts.

Returns:

List of components (largest first), each a list of node info dicts.

connection_types() list[dict[str, Any]]

Return all connection types with counts and endpoint type sets.

Returns:

List of dicts with type, count, source_types, target_types.

connections(indices: list[int] | None = None, parent_info: bool | None = None, include_node_properties: bool | None = None, flatten_single_parent: bool = True) dict[str, Any]

Get connections for selected nodes.

Parameters:
  • indices – Specific node indices to query.

  • parent_info – Include parent info in output.

  • include_node_properties – Include properties of connected nodes. Default True.

  • flatten_single_parent – Flatten when only one parent. Default True.

Returns:

Nested dict {title: {node_id, type, incoming, outgoing}}.

contains_point(lat: float, lon: float, geometry_field: str | None = None) KnowledgeGraph

Filter nodes whose WKT polygon contains a point.

Parameters:
  • lat – Query point latitude.

  • lon – Query point longitude.

  • geometry_field – WKT geometry property name. Default from spatial config or 'geometry'.

Returns:

A new KnowledgeGraph with containing nodes.

context(name: str, node_type: str | None = None, hops: int | None = None) dict[str, Any]

Get the full neighborhood of a code entity.

Returns the node’s properties and all related entities grouped by relationship type. If the name is ambiguous, returns the matches so you can refine with a qualified name.

Parameters:
  • name – Entity name or qualified name.

  • node_type – Optional node type hint.

  • hops – Max traversal depth (default 1).

Returns:

Dict with "node" (properties), "defined_in" (file path), and relationship groups (e.g. "HAS_METHOD", "CALLS", "called_by").

copy() KnowledgeGraph

Create an independent deep copy of this graph.

Returns a new KnowledgeGraph that shares no mutable state with the original. Useful for running mutations without affecting the source graph.

copy_embeddings_from(other: KnowledgeGraph) dict[str, int]

Copy every embedding store from other into this graph, by node id.

The one-call answer to the “rebuild a fresh graph from a source of truth on each load, keep the vectors” workflow: build the new graph, then new.copy_embeddings_from(old). Vectors land on the new nodes that share an id, carrying each store’s dimension, metric, model id, and per-node text hashes — so a following embed_texts(mode='changed') re-embeds only genuinely new/changed text. Vectors whose id has no matching node here are skipped (counted). Replaces the manual embeddings()add_embeddings()embed_texts() carry.

Returns:

Dict with stores_copied, vectors_copied, vectors_skipped.

Example:

new = build_graph_from_source()          # fresh, no vectors
new.copy_embeddings_from(old)             # carry vectors by id
new.embed_texts("Doc", "summary", mode="changed")  # fill only the new/changed
count(level_index: int | None = None, group_by_parent: bool | None = None, store_as: str | None = None, keep_selection: bool | None = None, group_by: str | None = None) Any

Count nodes, optionally grouped by parent or by a property.

Parameters:
  • level_index – Target level in the hierarchy.

  • group_by_parent – Group counts by parent node.

  • store_as – Store count as a property on parent nodes.

  • keep_selection – Preserve selection after store. Default False.

  • group_by – Group counts by this property instead of by parent. Returns {group_value: count}.

Returns:

An integer count, grouped counts, or a KnowledgeGraph if store_as is set.

Note

store_as= writes, and the write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log — so it is refused on a graph opened with durable= rather than left unlogged. Use cypher() there; see kglite.open().

create_composite_index(node_type: str, properties: list[str]) dict[str, Any]

Create a composite index on multiple properties.

The order of properties is not significant: the index is stored under its property names sorted, and list_composite_indexes() and SHOW INDEXES report that spelling.

Parameters:
  • node_type – Node type to index.

  • properties – List of property names for the composite key.

Returns:

Dict with node_type, properties, and unique_combinations.

Raises:

ValueError – If node_type exists only as a secondary label (indexes are keyed by primary type; see create_index).

create_connections(connection_type: str, keep_selection: bool | None = None, conflict_handling: str | None = None, properties: dict[str, list[str]] | None = None, source_type: str | None = None, target_type: str | None = None) KnowledgeGraph

Create connections from the traversal hierarchy.

By default, creates edges from the top-level ancestor (first traversal level) to the leaf nodes (last level). Use source_type / target_type to choose different levels.

Parameters:
  • connection_type – Label for the new edges (e.g. 'A_TO_C').

  • keep_selection – Preserve selection. Default False.

  • conflict_handling'update' (default), 'replace', 'skip', 'preserve', or 'sum'.

  • properties – Copy properties from intermediate nodes onto the new edges. Dict mapping node type to property names: {'TypeB': ['score', 'weight']}. An empty list copies all properties from that type.

  • source_type – Node type to use as source (default: first level).

  • target_type – Node type to use as target (default: last level).

Returns:

A new KnowledgeGraph with the connections added.

Example:

# After traversal A → B → C, create direct A → C edges
# with B's 'score' property copied onto each edge
graph.select('A') \
    .traverse('REL_AB') \
    .traverse('REL_BC') \
    .create_connections('A_TO_C',
        properties={'B': ['score']})

Note

Not available on a graph opened with durable=. The write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log, so it is refused rather than left unlogged — use cypher(), which expresses the same write and is logged. See kglite.open().

create_global_index(property: str) dict[str, Any]

Build a cross-type global index on property.

Unlike create_index, which is keyed by (node_type, property), this indexes EVERY node whose value at property is a non-empty string — regardless of node type. Enables:

  • MATCH (n {label: 'Norway'}) — untyped Cypher lookups, routed through the global index in O(log N).

  • graph.search(text) — top-k helper that returns the nodes whose label (or any indexed property) matches.

Disk-backed graphs only. On memory/mapped graphs this is a no-op that returns unique_values=0 — per-type create_index already covers the use case at in-memory scale.

Returns:

Dict with property, unique_values (count of nodes indexed), and created.

create_index(node_type: str, property: str) dict[str, Any]

Create an index on a property for O(1) equality filter lookups.

Indexes are automatically maintained by Cypher mutations (CREATE, SET, REMOVE, DELETE, MERGE).

Idempotent — re-creating an existing index rebuilds it without error; created is then False. It is True only when this call made a new index.

Parameters:
  • node_type – Node type to index.

  • property – Property name to index.

Returns:

Dict with node_type, property, unique_values, persistent, and created (False if the index already existed).

Raises:

ValueError – If node_type exists only as a secondary label — property indexes are keyed by primary type, so such an index would never be consulted. Index the nodes’ primary type.

create_range_index(node_type: str, property: str) dict[str, Any]

Create a range index (B-Tree) on a property for efficient range queries.

Enables fast >, >=, <, <=, and BETWEEN queries in filter() calls.

Parameters:
  • node_type – Node type to index.

  • property – Property name to index.

Returns:

Dict with node_type, property, unique_values, created.

Raises:

ValueError – If node_type exists only as a secondary label (indexes are keyed by primary type; see create_index).

Example:

graph.create_range_index('Person', 'age')
old = graph.select('Person').where({'age': {'>': 60}}).collect()
cypher(query: str, *, to_df: bool = False, params: dict[str, Any] | None = None, timeout_ms: int | None = None, max_work_units: int | None = None, row_limit: int | None = None, streaming: bool = True, parallel: bool = False, disable_optimizer: bool = False, disabled_passes: list[str] | None = None, write_scope: list[str] | None = None, git_sha: str | None = None, modified_by: str | None = None) ResultView | pandas.DataFrame | str

Execute a Cypher query.

Supports MATCH, WHERE, RETURN, ORDER BY, LIMIT, SKIP, WITH, OPTIONAL MATCH, UNWIND, UNION, CREATE, SET, DELETE, DETACH DELETE, REMOVE, MERGE (with ON CREATE SET / ON MATCH SET), HAVING, CASE expressions, WHERE EXISTS, shortestPath(), list comprehensions, CALL { … } read subqueries (uncorrelated + correlated; v1 excludes writes / UNION / unit subqueries in the body), CALL…YIELD (graph algorithms: pagerank, betweenness, degree, closeness, louvain, label_propagation, connected_components), parameters ($param), != operator, aggregation functions, window functions (row_number(), rank(), dense_rank() with OVER (PARTITION BY ... ORDER BY ...)), and date arithmetic (date + N, date - date, date_diff(d1, d2)).

Mutation queries (CREATE, SET, DELETE, REMOVE, MERGE, and the schema DDL below) store statistics on graph.last_mutation_stats with keys nodes_created, relationships_created, properties_set, nodes_deleted, relationships_deleted, properties_removed, indexes_added, indexes_removed, constraints_added, constraints_removed.

Schema DDL — CREATE [RANGE] INDEX [name] [IF NOT EXISTS] FOR (n:L) ON (n.p, ...), DROP INDEX <name> [IF EXISTS], and SHOW INDEXES — runs as a standalone statement. What each form builds differs from Neo4j (KGLite has separate equality, composite, and B-tree range structures) and index names are canonical rather than user-assigned; see the “Cypher index DDL” section of CYPHER.md. Index DDL counts as a mutation, so it is blocked on a read-only graph.

Constraint DDL — CREATE CONSTRAINT [name] [IF NOT EXISTS] FOR (n:L) REQUIRE n.p IS UNIQUE | IS NOT NULL | IS NODE KEY | IS :: TYPE, the relationship form FOR ()-[r:T]-() REQUIRE r.p IS NOT NULL | IS :: TYPE, DROP CONSTRAINT <name> [IF EXISTS], and SHOW CONSTRAINTS — declares constraints that are enforced on every write path, including the bulk loader. REQUIRE (n.a, n.b) IS UNIQUE constrains the tuple; IS NODE KEY is uniqueness and presence, installed atomically. Declaring a constraint the existing data already violates is rejected and changes nothing. Unlike index names, constraint names are stored, so DROP CONSTRAINT <name> works; a constraint declared without a name is addressable by its canonical descriptor (Label.property, or TYPE.property for a relationship constraint). IS :: TYPE / IS TYPED TYPE declares a per-property type, checked before a write lands; only the type names with an exact KGLite value counterpart are accepted, and the rest are rejected by name rather than approximated (use lock_schema() / validate_schema() for those). IS UNIQUE / IS RELATIONSHIP KEY on a relationship is rejected: KGLite has no single answer for when two relationships of a type are the same one. IS UNIQUE / IS NODE KEY over the identity field is rejected because it would enforce nothing, under any spelling that resolves to it (id itself or the node type’s own id column): id is a structural field rather than a stored property, so the unique secondary index never sees the write. Declare primary_key through define_schema() instead — it probes the per-type id index on every write path. IS NOT NULL on id is accepted and enforced: an omitted id is resolved by every write path and satisfies it, an explicit {id: null} violates it. SHOW CONSTRAINTS and SHOW INDEXES are reads and work on a read-only graph. See the “Cypher constraint DDL” section of CYPHER.md.

Direct mutation calls execute in place: if a later clause, timeout, or row-budget check fails, earlier mutations may remain visible. Use KnowledgeGraph.session() or KnowledgeGraph.begin() when failure must roll back. Property and composite indexes are maintained.

FORMAT CSV: Append FORMAT CSV to any query to get results as a CSV string instead of a ResultView. Good for large result transfers and token-efficient LLM consumption in MCP servers.

Before execution, the query is validated against the graph schema (known node types, connection types, and properties). Unknown identifiers raise ValueError with a Did you mean '...'? suggestion — catches typos before any scan runs.

Parameters:
  • query – Cypher query string.

  • to_df – If True, return a pandas DataFrame. A DataFrame has nowhere to carry ResultView.diagnostics, and neither does the string a FORMAT CSV query returns — so for both shapes the query’s warnings are only announced (stderr by default, see kglite.set_query_warning_policy()), never attached. Run the query without to_df / FORMAT CSV and call ResultView.to_df() when you want both the frame and rv.warnings.

  • params – Optional parameter dict for $param substitution. A parameter can supply a value or a name: labels and relationship types accept $label / $(label) too (MATCH (n:$label), -[:$type]->, CREATE (n:$label), SET n:$label). A parameter bound to a name position is a name by construction, so a caller never has to escape untrusted input into the query text. Name parameters must be strings, and a missing one is an error.

  • timeout_ms – Deadline in milliseconds. If omitted, uses set_default_timeout() when set, otherwise the built-in default of 180_000 ms (3 min). Pass 0 to disable the deadline for this call. Independently of the deadline, a long-running read can be interrupted with Ctrl-C (raises KeyboardInterrupt) on POSIX — the query aborts promptly instead of waiting for the deadline.

  • max_work_units – Work budget for this query — not a result-row cap. Charged against intermediate rows, retained collection items and scan work across every execution path, so it can far exceed the rows returned. Exceeding the budget raises an error (never truncates); use LIMIT to bound the rows you get back. Size it from a count(*) probe of the pattern with headroom — it is a hard refusal, not a soft cap — and note that with timeout_ms set too, the budget is usually what fires on a runaway pattern, since it bounds what the query holds while the deadline bounds how long it runs. Direct mutation calls are in-place; use Session/Transaction for rollback. Defaults to set_default_max_work_units().

  • row_limit – Cap on the result rows this call retains — the opposite number to max_work_units, which bounds work and raises. The query still runs to completion and still computes every row (ORDER BY sorts the whole answer, aggregation folds the whole answer); only retention stops at the cap, so the rows you keep are the first N of the uncapped answer, and the genuine top-N under ORDER BY. An explicit LIMIT m is applied first, making the effective cap min(m, row_limit). Applies to a mutation’s trailing RETURN too: every write still happens and last_mutation_stats still counts them all — the cap bounds what is reported, never what is changed. EXPLAIN is exempt. 0 is legal (“keep no rows, still tell me the total”). Truncation is never silent: it raises a query warning, and diagnostics carries row_limit plus the exact pre-truncation total_rows. Defaults to set_default_row_limit().

  • streaming – When True (default), the executor absorbs compatible clause runs (currently WITH/RETURN(group, agg) [ORDER BY ... LIMIT k]) into a streaming pipeline that builds aggregate state inline and replaces full sort + truncate with a heap-pruned top-K (O(n log k) instead of O(n log n)). Pass False to force the materialized executor — useful for debugging parity issues; behaviour should otherwise be identical.

  • parallel

    Opt this query in to the parallel runtime (default False). It is a hint, not an instruction: only operators that can partition deterministically use it, and each still applies its own runtime gate on candidate count and per-row cost — a small query stays sequential however it is flagged. Answers and row order are identical either way, so this is a throughput knob and never a semantic one.

    Worth it for one heavy analytical scan or aggregate over a large graph, and not worth it for returning many rows: scanning parallelises, building result rows does not (it is bound by the allocator). Measured on a 1M-node graph and a 10-core Apple Silicon machine, release, two agreeing runs: scan + filter + count(*) 5.2x, the grouped form 5.2x, a regex predicate 6.0x — but a 792k-row projection only 1.1x.

    It is off by default, and stays off in the Bolt and MCP servers, because a server’s cores belong to its concurrent clients: turning it on trades across-query throughput for one query’s latency. Disk-mode graphs, and graphs with a spatial configuration, ignore the flag and run sequentially rather than refusing it. See the “Parallel runtime” section of CYPHER.md for what does and does not parallelise.

  • disable_optimizer – When True, run the query with all optimizer passes skipped — schema validation still applies, but no predicate pushdown, fusion, reordering, or LIMIT-pushdown happens. Diagnostic / testing knob; normal use should leave this False. Used by the differential test harness to assert optimized and naive executions produce identical results.

  • disabled_passes – Skip a specific subset of optimizer passes by name. Names must come from kglite.cypher_pass_names() — typos raise ValueError. Useful for bisecting which pass introduces a divergence.

  • write_scope

    Role-scoped write whitelist (integrity, not secrecy — e.g. a coding role may write ["Plan", "Task"] but not research-owned Algorithm nodes). None (default) = unrestricted; [] denies every mutation. Applies per-call; also on Session.execute() and Transaction.cypher. The perimeter, exactly:

    • Node writesCREATE, MERGE’s create arm, SET n.p, SET n += {...}, SET n:Label, REMOVE n.p, REMOVE n:Label, DELETE n, DETACH DELETE n, and index/constraint DDL for a node type — are judged by the node’s stored type, never by a pattern label, so label smuggling cannot widen the scope.

    • Relationship writesCREATE (a)-[:R]->(b), DELETE r, SET r.p, REMOVE r.p — are allowed iff at least one endpoint’s stored type is in scope. Linking a node you own to an already-existing (matched) out-of-scope node is allowed, since linking does not mutate it; an edge between two out-of-scope nodes is refused, and creating a new out-of-scope endpoint node is refused by the node rule.

    • DETACH DELETE removes the incident relationships of a node you are allowed to delete, whatever type the far endpoint is — that collateral is authorized by the node delete and not re-checked per endpoint.

    • Outside the perimeter, deliberately: relationship constraint DDL, db.cdc.enable/db.cdc.disable, and the bulk loaders add_nodes() / add_connections()write_scope is a per-Cypher-execution concept and does not reach the Python loader API.

Returns:

ResultView by default, DataFrame when to_df=True, or CSV string when the query ends with FORMAT CSV. Only the ResultView carries diagnostics / warnings.

Raises:
  • KeyboardInterrupt – If a long-running read is interrupted with Ctrl-C (POSIX only). The graph is left unchanged.

  • kglite.CypherSyntaxError / kglite.SchemaError / ... – Typed kglite.KgError subclasses for query / schema faults.

Example:

rows = graph.cypher('''
    MATCH (p:Person)-[:KNOWS]->(f:Person)
    WHERE p.age > $min_age
    RETURN p.name, count(f) AS friends
    ORDER BY friends DESC LIMIT 10
''', params={'min_age': 25})
for row in rows:
    print(row['name'], row['friends'])

# As DataFrame
df = graph.cypher('MATCH (n:Person) RETURN n.name, n.age', to_df=True)

# As CSV string (good for large data transfers)
csv = graph.cypher('MATCH (n:Person) RETURN n.name, n.age FORMAT CSV')

# CREATE nodes and edges
graph.cypher("CREATE (n:Person {name: 'Alice', age: 30})")
print(graph.last_mutation_stats['nodes_created'])  # 1

# SET properties
graph.cypher('''
    MATCH (n:Person) WHERE n.name = 'Alice'
    SET n.city = 'Oslo', n.age = 31
''')

# Semantic search with text_score (requires set_embedder + embed_texts)
results = graph.cypher('''
    MATCH (n:Article)
    RETURN n.title,
           text_score(n, 'summary', 'machine learning') AS score
    ORDER BY score DESC LIMIT 10
''', to_df=True)

# text_score with parameter
graph.cypher('''
    MATCH (n:Article)
    WHERE text_score(n, 'summary', $query) > 0.8
    RETURN n.title
''', params={'query': 'artificial intelligence'})

# CALL graph algorithms
top = graph.cypher('''
    CALL pagerank() YIELD node, score
    RETURN node.title, score
    ORDER BY score DESC LIMIT 10
''')

# Community detection
graph.cypher('''
    CALL louvain() YIELD node, community
    RETURN community, count(*) AS size
    ORDER BY size DESC
''')
date(date_str: str | None = None, end_str: str | None = None) KnowledgeGraph

Set the temporal context for auto-filtering.

Returns a new KnowledgeGraph. All subsequent select() and traverse() calls on the returned graph use this context for temporal filtering.

Modes:
  • date('2013') — point-in-time (valid at 2013-01-01).

  • date('2010', '2015') — range: include everything valid at any point during 2010-01-01 to 2015-12-31 (overlap check).

  • date('all') — disable temporal filtering entirely.

  • date() — reset to today (default).

Parameters:
  • date_str – Date string, 'all', or None to reset.

  • end_str – Optional end date for range mode. End dates expand to period end ('2015' → 2015-12-31, '2015-06' → 2015-06-30).

Returns:

A new KnowledgeGraph with the given temporal context.

define_ontology(ontology_dict: dict[str, Any]) list[str]

Install the declared semantic layer (classes + relationship semantics).

Annotations, not axioms — SKOS in spirit, never OWL: the ontology never changes what a query matches. It feeds describe(), provides defaults for the rule-procedure validators (a no-argument CALL type_domain_violation() checks every declaration), and acts as a data-quality contract for blueprint builds.

Document shape (all keys optional):

g.define_ontology({
    "classes": {
        "Licensable": {"abstract": True, "description": "..."},
        "Licence": {"is_a": "Licensable"},
    },
    "relationships": {
        "HAS_OPERATOR": {
            "domain": "Licensable", "range": "Company",
            "required_properties": ["validFrom"],
            "cardinality": {"min": 0, "max": 1},
            "required": True, "enforcement": "warn",
            "exempt": {"required_properties": ["PetregLicence"]},
            "ancestry": False,
        },
    },
})

Semantics: is_a is a forest (single parent, no cycles); cardinality/required describe outgoing edges of the domain type; required_properties (per-edge presence) and property_types (per-edge type check of present values, names validated on declare) are audited per edge; inverse_name is a reading-direction alias only unless inverse_enforced: True opts into the physical-pairing check; enforcement is a severity (advisory/warn/error) or a per-check map ({"required_properties": "error"}, unlisted checks stay advisory) consumed by the blueprint gate and ontology_audit(); exempt is a per-check map of source classes whose violations are counted in ontology_audit()’s exempted column instead of against severity (a class matches when it is the edge source’s primary type or one of its declared ancestors), so one legitimately nonconforming source type cannot pin a whole rule at advisory — accepted for required_properties and property_types only, the two checks where “domain-side class” means the edge’s source type, and refused with an explanation for every other check name and for the flat exempt: [...] form; transitive and ancestry are mutually exclusive and mean different things — transitive: True enrolls transitivity_violation, which audits a stored closure (every a→b→c must have a stored a→c edge), while ancestry: True is a documentation-only annotation that the chain is meaningful and is walked with *1.., which is what a parent-pointer taxonomy (STRAT_PARENT, wdt:P279) is, so declaring that transitive reports 100% violations; by names a discriminator property and is documentation only. Class names share the label namespace: an abstract class may not shadow a live node type. This is deliberately separate from set_parent_type — that map is presentation ownership, this one is semantic “kind of”.

Replaces any previously declared ontology. Persisted by save().

Parameters:

ontology_dict – The declaration document.

Returns:

List of warnings (e.g. a concrete class naming no live node type). Empty when clean.

Raises:

ValueError – Malformed document (unknown key, cycle, dangling is_a target, class cap exceeded, both transitive and ancestry on one relationship, an exempt entry on an unexemptable check or naming an undeclared class), or an abstract class shadowing a live node type.

define_schema(schema_dict: dict[str, Any], *, replace: bool = False) KnowledgeGraph

Define the expected schema for the graph.

Merges per node/connection type. A type named in schema_dict takes the new declaration entire; a type it does not name keeps the declaration it already had. So declaring per module or per type is safe — it cannot affect the constraints of a type this call never mentions:

g.define_schema({"nodes": {"User": {"primary_key": "email"}}})
g.define_schema({"nodes": {"Task": {"required": ["title"]}}})
# User.email is still a NODE KEY and still enforced.

Merging is per type, not per field, so re-declaring a type is still how you narrow it — declare Task without a required entry it used to have and that requirement is withdrawn.

Pass replace=True for the whole-schema semantics: the incoming schema becomes the entire schema and every type it does not name loses its declarations. Because that withdraws enforcement from types the caller never mentioned, it emits a UserWarning naming each constraint it stops enforcing. clear_schema() removes everything.

Constraints declared through Cypher DDL (CREATE CONSTRAINT) are not schema declarations and are unaffected by either mode — they are withdrawn only by DROP CONSTRAINT.

Parameters:

schema_dict

Schema definition with nodes and connections keys. Each node entry may set required/optional/types, and optionally primary_key and unique to declare enforced integrity constraints:

g.define_schema({"nodes": {"Person": {
    "primary_key": "email",
    "unique": [["first", "last"]],
    "required": ["email"],
}}})

primary_key may name any property and means unique and present (NODE KEY): a CREATE that duplicates or omits it is rejected — use MERGE to upsert. A key on "id" is enforced through the O(1) identity index; any other key is backed by a unique secondary index that persists and rebuilds on load.

unique declares additional UNIQUE constraints and accepts a property name, a list of names, or a list of property tuples, so "email", ["email"] and [["first", "last"]] are all valid — the last being a composite constraint. A tuple only constrains nodes carrying every property in it, matching Neo4j, where uniqueness does not apply to nodes missing the property.

required is enforced at write time, not only by validate_schema(): a CREATE that omits the property, a SET that nulls it, and a REMOVE that drops it all raise. type is the node’s label and cannot be absent, so requiring it is a no-op; id and title are auto-supplied when omitted (so omitting them satisfies the requirement) but can be explicitly nulled, and that is rejected. Unlike CREATE CONSTRAINT ... IS NOT NULL, which verifies stored data before installing, required declares intent without re-checking what is already there — validate_schema() reports existing violations. Auto-vivified edge stubs are deferred rather than exempt — vivification may create an incomplete placeholder, but the add_nodes upsert that promotes it is a normal enforced write, and an unpromoted stub stays reportable via validate_schema() and removable via purge_provisional().

All constraints are enforced on every write path, including the bulk loaders. All are opt-in: a type declaring none keeps the permissive default, and older graphs load unchanged.

A node entry may also set layer to 'managed' (rebuilt from source by a batch writer) or 'runtime' (owned/mutated live by another writer, e.g. an agent). With layers declared, an add_nodes(..., managed_reload=True) call skips a runtime type instead of writing it — a declared lane for a well-behaved batch writer, not an enforced perimeter (the caveats are on add_nodes()’s managed_reload):

g.define_schema({"nodes": {
    "AlgorithmSpec": {"layer": "managed"},
    "Task":         {"layer": "runtime"},
}})

A node entry may also set auto_timestamp: True to opt that type into freshness provenance: every write (Cypher CREATE/SET/MERGE and add_nodes) auto-stamps an updated_at timestamp, plus the caller-supplied git_sha / modified_by when provided. It is off by default (writes stay deterministic) and independent of layer / lock_schema:

g.define_schema({"nodes": {"Task": {"auto_timestamp": True}}})

Raises:

ConstraintCreationError – A declared unique tuple or primary_key is already duplicated by existing nodes. Nothing is changed — deduplicate the node type and call again.

Returns:

Self with schema defined.

degree_centrality(normalized: bool | None = None, connection_types: str | list[str] | None = None, top_k: int | None = None, timeout_ms: int | None = None, to_df: bool | None = None) ResultView | pandas.DataFrame

Calculate degree centrality.

Parameters:
  • normalized – Normalise by (n-1). Default True.

  • connection_types – Only count these relationship types (str or list).

  • top_k – Return only the top K nodes.

  • timeout_ms – Abort after this many milliseconds with an error.

  • to_df – Return a pandas DataFrame with columns type, title, id, score.

Returns:

A ResultView of rows with type, title, id, score, sorted by score descending. Or a pandas DataFrame if to_df=True.

degrees() dict[str, int]

Get connection count for each selected node.

Returns:

{node_title: degree}.

Raises:

ArgumentError – Two selected nodes share a title. Titles are not unique — not even within one node type — so keying by title would drop a node’s degree with no signal. Use degree_centrality(), whose ResultView carries one row per node with its type and id.

dematerialize_ontology() int

Withdraw every materialized label (the managed set empties).

The declaration store itself stays — this exits materialization, not the ontology. Returns the number of label removals performed.

describe(types: list[str] | None = None, type_search: str | None = None, connections: bool | list[str] | None = None, cypher: bool | list[str] | None = None, fluent: bool | list[str] | None = None, max_pairs: int | None = None, sample_truncate: int | None = 40) str

Return an XML description of this graph for AI agents.

Five independent axes for progressive disclosure:

Node types (types parameter):

  • describe() — Inventory overview with compact type descriptors and connections carrying typed edge properties (properties="since:Int64"). Adapts to graph scale: small graphs get full inline detail, extreme-scale graphs get a statistical summary with search hints.

  • describe(types=['Field', 'Well']) — Focused detail for specific types with properties, connections, and samples. Each type carries a schema-adapted <example> query anchored on its real identifier property (its id alias, else id) with a concrete sampled value, so an agent copies a query that matches that type’s key shape. A property present on only some of the type’s nodes carries coverage="51%"; a fully populated one carries no coverage attribute at all.

Type search (type_search parameter):

  • describe(type_search='software') — Find types by name (case-insensitive substring match) with neighborhood fan-out. Returns matching types with their connections, plus one layer of connected types. Ideal for exploring large/extreme-scale graphs where listing all types is impractical.

Connections (connections parameter):

  • describe(connections=True) — All connection types with count, source/target node types, and typed property names (name:Type).

  • describe(connections=['BELONGS_TO']) — Deep-dive with per-pair counts, property stats with sample values, and sample edges. Use this to discover what data edges carry.

Cypher (cypher parameter):

  • describe(cypher=True) — Compact Cypher reference: all clauses, operators, functions, and procedures with 1-line descriptions.

  • describe(cypher=['cluster', 'MATCH']) — Detailed docs with parameters and examples for specific topics.

Fluent API (fluent parameter):

  • describe(fluent=True) — Compact fluent API reference: all methods grouped by area with signatures and descriptions.

  • describe(fluent=['traverse', 'where', 'spatial']) — Detailed docs with parameters and examples for specific topics.

When type_search, connections, cypher, or fluent is set, only those tracks are returned (no node inventory).

Parameters:
  • types – Node type names for focused detail.

  • type_search – Case-insensitive substring pattern to search type names. Returns matching types with connections + 1 layer of connected types for neighborhood discovery.

  • connections – True for overview, list for deep-dive into specific types.

  • cypher – True for compact reference, list for detailed topic docs.

  • fluent – True for compact reference, list for detailed topic docs.

  • max_pairs – Cap on (src_type, tgt_type) pairs rendered in the describe(connections=['T']) deep-dive. Defaults to 50. Sorted by count desc, so the head covers the dominant relationships; a trailing <more pairs="…" edges="…"/> marker reports the hidden tail. Raise to drill into wide fan-out connection types (e.g. Wikidata P31 has 191k distinct pairs).

  • sample_truncate – Max chars for string values in vals= attributes, sample-node ids/titles, and sample-edge attributes. Defaults to 40. Pass None to disable truncation entirely — useful when you want full titles in an LLM prompt and have context-window budget for it. Has no effect on stored data; only the rendered XML.

Raises:
  • ValueError – If any type/connection/topic is not found.

  • TypeError – If connections, cypher, or fluent has wrong type.

difference(other: KnowledgeGraph) KnowledgeGraph

Keep nodes in self but not in other (set difference).

Returns:

A new KnowledgeGraph with the difference.

drop_composite_index(node_type: str, properties: list[str]) bool

Remove a composite index. Returns True if it existed.

drop_index(node_type: str, property: str) bool

Remove an index. Returns True if it existed.

drop_range_index(node_type: str, property: str) bool

Remove a range index. Returns True if it existed.

drop_text_index(node_type: str, property: str) bool

Drop the BM25 text index for (node_type, property).

Returns True if an index was dropped, False if none existed.

drop_vector_index(node_type: str, text_column: str) bool

Drop the HNSW index for an embedding store (search reverts to exact).

The vectors are untouched — this drops the accelerator, not the data. Returns True if an index was dropped, False if none existed.

embed_texts(node_type: str, text_column: str, batch_size: int = 256, show_progress: bool = True, mode: str | None = None) dict[str, int]

Embed a text column for all nodes of a given type.

Uses the model registered via set_embedder(). Reads each node’s text_column, calls model.embed() in batches, and stores the resulting vectors as {text_column}_emb. Nodes with missing or non-string text are skipped.

text_column resolves exactly as set_embeddings() resolves it — a stored property, an identity alias (a type built with title_field='name' embeds its titles under 'name'), the canonical id/title, or a structural alias — and a column that resolves to none of those raises ValueError rather than silently embedding nothing.

The store also records, per node, a hash of the embedded text and (when the embedder names it) the model id — so a later embed_texts(mode='changed') can re-embed exactly the nodes whose text changed, and embedding_info() can report provenance.

Shows a tqdm progress bar by default (requires tqdm).

Parameters:
  • node_type – The node type to embed (e.g. 'Article').

  • text_column – The column holding the text to embed — a property, an identity alias, or id/title.

  • batch_size – Number of texts per model.embed() call (default 256).

  • show_progress – Show a tqdm progress bar (default True). Silently falls back to no bar if tqdm is not installed.

  • mode – Which nodes to embed — 'missing' (default): only nodes without an embedding; 'changed': nodes missing an embedding or whose text changed since the last embed (via the stored content hash) — the incremental re-embed; 'all': re-embed every node, rebuilding the store fresh.

Returns:

Dict with embedded, skipped, skipped_existing, reembedded_changed, and dimension.

Raises:
  • ValueError – if node_type does not exist in the graph — the same complaint set_embeddings() makes, raised before the model is loaded (an existing type with no matching rows stays a {'embedded': 0} no-op); if text_column resolves to none of the accepted spellings; or if mode is not 'missing' / 'changed' / 'all'.

  • RuntimeError – if no embedder was registered with set_embedder().

Example:

g.set_embedder(my_model)
g.embed_texts("Article", "summary")
# Embedding Article.summary: 100%|████████| 1000/1000 [00:05<00:00]

# Add/edit articles, then re-embed only what changed:
g.embed_texts("Article", "summary", mode="changed")
embedding(node_type: str, text_column: str, node_id: Any) list[float] | None

Retrieve a single node’s embedding vector.

Parameters:
  • node_type – The node type (e.g. ‘Article’).

  • text_column – Source text column name (e.g. ‘summary’).

  • node_id – The node ID to look up.

Returns:

The embedding vector as a list of floats, or None if not found.

embedding_diagnostics(node_type: str | None = None) list[dict[str, Any]]

Diagnose embedding coverage per (node_type, text_column).

Companion to list_embeddings(). Surfaces three states:

  • "embedded": a store exists and at least one node has the underlying property.

  • "embeddable": nodes have a string-typed property but no embedding store has been created or restored.

  • "store_orphan": a store exists but no node in the current graph has the underlying property — the symptom import_embeddings() warns about when keys mismatch.

Each row also carries a length_stats dict so callers can filter on string-length distribution + cardinality before committing to embed a column. ISO timestamps, status enums, and fully-unique identifiers show up with status="embeddable" but their length_stats distinguishes them from real candidates:

# keep columns averaging ≥ 20 chars and not fully-unique
candidates = [d for d in g.embedding_diagnostics()
              if d["length_stats"]["mean_length"] >= 20
              and d["length_stats"]["distinct_ratio"] < 1.0]
Parameters:

node_type – Optional filter. When set, only that node type is scanned. When None, every type in the graph is scanned — may be expensive on graphs with millions of nodes.

Returns:

node_type, text_column, embedding_key (= f"{text_column}_emb"), nodes_with_property, nodes_embedded, dimension (or None), metric (or None), status, and length_stats with mean_length / max_length / distinct_count / distinct_ratio.

Return type:

List of dicts with

embedding_dim(node_type: str, text_column: str) int | None

The vector dimension of the (node_type, text_column) embedding store, or None if none exists.

A cheap, direct way to detect an embedder/model change without bookkeeping: compare it against your model’s dimension before re-embedding. embed_texts / add_embeddings reject a dimension mismatch (re-embed with mode='all' to rebuild at a new dimension).

Example:

if g.embedding_dim("Article", "summary") not in (None, model.dimension):
    g.embed_texts("Article", "summary", mode="all")  # model changed
embedding_info(node_type: str, text_column: str) dict[str, Any] | None

Provenance for the (node_type, text_column) embedding store, or None if no store exists.

Returns a dict with dimension, count (vectors stored), model (the embedder id stamped at embed_texts time, or None for vectors supplied directly via add_embeddings), metric, and hashed (how many vectors carry a source-text hash, used by embed_texts(mode='changed') for change-detection). Detect a model swap or a partially-hashed store without external bookkeeping.

metric is the store’s effective distance metric: the one set via set_embeddings(metric=...) if any, else 'cosine' (the default search applies). It is never None for an existing store — a store with no explicit metric reports 'cosine' (consistent with list_embeddings()).

The model is populated when the embedder exposes a model_id / model_name attribute (or is the built-in fastembed backend).

embeddings(node_type: str, text_column: str) dict[Any, list[float]]
embeddings(text_column: str) dict[Any, list[float]]

Retrieve embeddings for nodes in the current selection.

With no selection active this covers the whole graph (the same never-selected rule get_nodes() follows); a selection a query emptied returns {}.

Parameters:

text_column – Source text column name (e.g. ‘summary’).

Returns:

Dict mapping node IDs to embedding vectors.

Raises:

ArgumentError – The selection spans two node types sharing an id. Ids are unique per type only, so one of the two vectors would be dropped from the dict. Call the two-arg form embeddings(node_type, text_column) once per type — it keys a single type’s id namespace.

enable_disk_mode(path: str | None = None) None

Materialize the in-memory graph as a disk-backed one.

Builds CSR (Compressed Sparse Row) edge arrays in files and switches the graph onto the disk backend. Node slots become a mmap’d array of 16 bytes per node; edges are read through the mapping.

With path, the conversion writes into that directory and publishes it: the live handle ends on the published generation (mapped edges, no mutation overlay) — the same state a fresh kglite.open(path) reads — and path becomes this graph’s save target, so a later bare save() writes back to it. Nothing transits the system temp directory on the way, so a conversion larger than /tmp still runs.

Warning

This does not shrink the process. It is a conversion, and the conversion itself adds the on-disk edge structures on top of what is already resident — expect resident memory to go up, not down, for the lifetime of this process. The in-memory structures it replaces are freed, but the allocator keeps the pages; call kglite.trim_memory() afterwards to return them to the OS.

Where the small footprint actually comes from is that directory, reopened in a fresh process: the reopened graph starts at roughly a tenth of the in-memory graph’s resident size (measured 56 MB against 492 MB on the same graph), because its edges are paged in on demand instead of built.

To run at that footprint from the start — never paying the in-memory peak at all — build into disk storage directly with KnowledgeGraph(storage="disk", path="graph.kgl").

Warning

Called without a path, the conversion materializes the edge structures into a scratch directory under the system temp location and deletes them when the graph is dropped. Nothing persists, and on a machine whose temp directory is small or RAM-backed the whole edge structure is written somewhere you did not choose — which is why that form emits a UserWarning naming the location. Keep it only for a throwaway, process-scoped conversion.

graph_info() reports the result: storage_mode becomes "disk" and edges_mapped becomes True. columnar_is_mapped is about property-column spilling and stays False here — that is disk mode’s normal shape, not a failed conversion.

All query methods (Cypher, fluent API, algorithms) work identically afterwards.

Parameters:

path – Directory to materialize the graph into and publish it at. Omit for a scratch conversion that persists nothing.

Raises:
  • ValueError – The directory cannot be written back to — a write-ahead sidecar beside it holds commits this publish would strand, or this handle is a view derived from a durable graph.

  • FileIoError – The conversion or the publish failed on the filesystem.

Example:

graph.enable_disk_mode("graph.kgl")   # convert *and* publish
kglite.trim_memory()      # hand the freed pages back to the OS

# ...in a fresh process, at ~10% of the in-memory footprint:
reopened = kglite.open("graph.kgl")
exists(node_type: str, unique_id: Any) bool

Return True if a node of node_type with that id exists. O(1).

Uses the same hash index as node() (no scan), and mirrors its id-coercion semantics: ids are integers in every storage mode, so a Python int and the stored id normalize to the same key.

Parameters:
  • node_type – The node type (e.g. 'User').

  • unique_id – The unique ID value.

Returns:

True if a matching node exists, False otherwise.

expand(hops: int | None = None) KnowledgeGraph

Expand the selection by N hops (breadth-first, undirected).

Parameters:

hops – Number of hops to expand. Default 1.

Returns:

A new KnowledgeGraph with the expanded selection.

explain() str

Return a human-readable execution plan for the fluent chain.

Fluent methods return a new handle, so the plan lives on the object they return — call explain() on the chain result, not on the graph:

graph.select("Person").where({"city": "Oslo"}).explain()
# 'SELECT Person (500 nodes) -> WHERE (42 nodes)'

Each recorded operation (SELECT, WHERE, TRAVERSE, EXPAND, VALID_AT, VALID_DURING, the spatial predicates) is shown with the node count it produced. Calling it on a graph with no recorded operations returns a message saying so.

This reports fluent chains only. For Cypher, prefix the query with EXPLAIN (plan the query without executing it) or PROFILE (execute it and return per-clause statistics on result.profile).

Returns:

The chain’s operations joined by ->.

static explain_mcp() str

Return a self-contained XML quickstart for setting up a KGLite MCP server.

Covers the bundled kglite-mcp-server console script (the default on-ramp — no fork required), the YAML manifest for adding custom tools (source_root: for file access, inline Cypher tools, and trust-gated Python hooks), and Claude Desktop / Claude Code registration config.

Example:

print(KnowledgeGraph.explain_mcp())
explore(query: str, max_entities: int = 10, max_depth: int = 2, include_source: bool = True, source_roots: list[str] | None = None) str

One-call codebase exploration over a code-tree graph.

Lexically ranks Function / Class / Interface / Struct / Trait / Protocol / Enum nodes against query (matched against name + signature + docstring), takes the top max_entities, 2-hop traverses CALLS / USES_TYPE / HAS_METHOD / DEFINES / REFERENCES_FN, and returns a markdown report with entry points, a relationship map, and grouped source slices for the entry points.

Designed for the “how does X work in this codebase” question that would otherwise require a chain of grep + read calls. Composes FTS + traversal + source-slicing into a single Rust- side call.

Parameters:
  • query – Free-text topic. Matched against name + signature + docstring. Exact name matches rank highest.

  • max_entities – Top N entry points after ranking (default 10).

  • max_depth – Hops for the neighborhood traversal (default 2).

  • include_source – Whether to include source slices for entry points (default True). Set False for a smaller, faster response when the entity list is all you need.

  • source_roots – Filesystem roots to resolve file_path properties against. Files matched literally are tried first; roots are searched in order. Default: cwd only.

Returns:

A markdown string with ## Query / ## Entry points / ## Related / ## Source sections. Empty queries and graphs with no matching entities return a clear no-match message rather than raising.

export(path: str, format: str | None = None, selection_only: bool | None = None) None

Export the graph to a file.

Supported formats: graphml, gexf, d3/json, csv, sqlite. Format is inferred from the file extension if not specified (.sqlsqlite).

sqlite writes a SQLite-dialect SQL script — node types become tables, connection types become link tables — which you ingest with the stock CLI:

graph.export("dump.sql")
# then: sqlite3 out.db < dump.sql

A script rather than a .db file keeps kglite dependency-free while still handing you a real, queryable database. See the migrations guide.

Parameters:
  • path – Output file path.

  • format – Export format. Default: inferred from the extension (unrecognised extensions fall back to graphml). export_string() has no path to infer from and defaults to 'json' instead.

  • selection_only – Export only selected nodes. Default: True if selection exists.

export_csv(path: str, selection_only: bool | None = None, verbose: bool = False) dict[str, Any]

Export to an organized CSV directory tree with blueprint.

Creates a directory structure with one CSV per node type and one per connection type, plus a blueprint.json for round-trip re-import via from_blueprint().

Output structure:

path/
├── nodes/
│   ├── Person.csv
│   ├── Company.csv
│   └── Person/          # sub-nodes nested under parent
│       └── Skill.csv
├── connections/
│   ├── WORKS_AT.csv
│   └── KNOWS.csv
└── blueprint.json

Node CSVs have columns: id, title, then all properties. Connection CSVs: source_id, source_type, target_id, target_type, then edge properties.

Only connections where both endpoints are in the selection are exported.

Parameters:
  • path – Output directory (created if it doesn’t exist).

  • selection_only – Export only selected nodes. Default: True if a selection exists, False otherwise.

  • verbose – Print progress information during export.

Returns:

Summary dict with keys output_dir, nodes (type → count), connections (type → count), files_written.

Example:

graph.export_csv('output/')
graph.select('Person').export_csv('output/', verbose=True)
export_embeddings(path: str, node_types: list[str] | dict[str, list[str]] | None = None) dict[str, int]

Export embeddings to a standalone .kgle file, keyed by node ID.

Parameters:
  • path – Output .kgle file path.

  • node_types

    Optional filter.

    • None (default): export all stores.

    • list[str]: only stores whose node_type is in the list.

    • dict[str, list[str]]: per-type list of text columns to export.

Returns:

Dict with stores (count of stores written) and embeddings (total embedding vectors written).

export_string(format: str | None = None, selection_only: bool | None = None) str

Export the graph to a string.

Supported formats: graphml, gexf, d3/json, sqlite. csv is file-only — it writes two files (nodes and edges), which one string cannot carry — so it is rejected here; use export(path, format='csv').

Parameters:
  • format – Export format. Default: 'json'. (export() infers its format from the path extension; a string return has no extension, so this side defaults to the format such a string is most often fed to.)

  • selection_only – Export only selected nodes.

extend(other: KnowledgeGraph, conflict_handling: str | None = None) dict[str, Any]

Merge another KnowledgeGraph into this one, in place.

A native alternative to round-tripping through CSV export/import when building a graph incrementally from multiple sources (or merging two .kgl files loaded into memory). The other graph is read-only and never mutated. Both graphs must use the default in-memory storage.

Example:

g1 = kglite.load('source_a.kgl')
g2 = kglite.load('source_b.kgl')
report = g1.extend(g2)              # g2 folded into g1 in place
report = g1.extend(g2, 'preserve')  # existing g1 values win

Semantics:

  • Node identity is (node_type, id) — the key the id index uses. id is the canonical integer node id in every storage mode. When a node in other matches an existing node here, the conflict is resolved by conflict_handling (same vocabulary as add_nodes):

    • 'update' (default) — merge properties, other wins on conflicts; title is overwritten.

    • 'replace' — replace all properties and the title with other’s.

    • 'skip' — leave the existing node untouched.

    • 'preserve' — merge properties, existing values win; title kept unless currently null.

    • 'sum' — adds numeric property values on edges; for node properties it acts as update (matches add_nodes / add_connections 'sum' semantics).

  • Secondary labels (multi-label, since 0.10.5) are unioned onto the matched/created node — never removed. Idempotent.

  • Property schemas merge: a property present in other but not here extends this graph’s type schema (the same path add_nodes uses for new columns).

  • Edges dedup on (connection_type, source, target): an edge that already exists here is not duplicated — its properties merge per conflict_handling. Exact-duplicate edges present in both graphs are created once, not twice (mirrors add_connections’ dedup so a merge never silently doubles shared edges).

Scope limits (v1):

  • In-memory only. Both graphs must use the default in-memory storage; storage='mapped' / 'disk' graphs raise an error suggesting the export/import path.

  • Embeddings are NOT merged. If other has any embedding stores a warning is emitted — re-run set_embeddings / add_embeddings after the merge to rebuild them here.

  • Self-extend (g.extend(g)) is a no-op for creation: every node/edge already matches itself, so the result is a property merge against self (a no-op under every mode but 'replace').

  • Locks. Like add_nodes / add_connections, this bulk path does not consult schema_locked / read_only (those gate the Cypher write path only).

Parameters:
  • other – KnowledgeGraph to merge into this one (read-only).

  • conflict_handling – ‘update’ (default), ‘replace’, ‘skip’, ‘preserve’, or ‘sum’.

Returns:

Operation report dict with nodes_created, nodes_updated, nodes_skipped, edges_created, edges_skipped, node_types_merged, connection_types_merged, labels_unioned, processing_time_ms, has_errors, and optionally errors.

find(name: str, node_type: str | None = None, match_type: str | None = None) list[dict[str, Any]]

Find code entities by name, with disambiguation context.

Code-entity search only. find() searches nodes of type Function, Struct, Class, Mixin, Enum, Trait, Protocol, Interface, Module, or Constant — the types produced by code-graph builders (e.g. codingest). On graphs that don’t contain these types (e.g. a social graph with Person nodes), find() returns an empty list. For general name lookup on other node types, use select(type).where({"name": ...}) or cypher("MATCH (n:Type) WHERE n.name = $n RETURN n", params={"n": ...}).

Parameters:
  • name – Entity name to search for (e.g. "execute").

  • node_type – Optional filter — only search this node type (e.g. "Function", "Struct").

  • match_type – Matching strategy: "exact" (default), "contains" (case-insensitive substring), or "starts_with" (case-insensitive prefix).

Returns:

type, name, qualified_name, file_path, line_number, and optionally signature and visibility.

Return type:

List of dicts with

freeze() FrozenGraph

Take an immutable, concurrently-readable snapshot of the graph.

Returns a FrozenGraph that shares this graph’s data (an O(1) clone — no deep copy) and exposes only read methods. A live KnowledgeGraph is single-owner and raises if a second thread touches it while another mutates it; a FrozenGraph has no mutating method, so any number of threads can run cypher() against the same snapshot in parallel, lock-free.

The snapshot is stable: mutating the source graph afterwards copy-on-writes a fresh copy, leaving the frozen view on the original data — the “build → freeze → share → swap” model for serving concurrent readers while a new snapshot is built in the background.

get_default_max_work_units() int | None

Get the current default per-query work budget, or None.

Returns:

The work-unit budget set by set_default_max_work_units(), or None when no default budget is set.

get_default_row_limit() int | None

Get the current default result-row cap, or None.

Returns:

The cap set by set_default_row_limit(), or None when no default cap is set.

get_default_timeout() int | None

Get the current default query timeout in milliseconds, or None.

get_properties(properties: list[str], limit: int | None = None, indices: list[int] | None = None, flatten_single_parent: bool | None = None) list[tuple[Any, ...]] | dict[str, list[tuple[Any, ...]]]

Get specific properties for selected nodes.

Without traversal (single parent group), returns a flat list of tuples. After traversal (multiple parent groups), returns {parent_title: [tuples]}.

Parameters:
  • properties – List of property names to retrieve.

  • limit – Maximum number of nodes.

  • indices – Specific node indices.

  • flatten_single_parent – Flatten single-group results to a list. Default True.

Returns:

list[tuple] when flattened, dict[str, list[tuple]] when grouped.

get_table_property(node_type: str, node_id: Any, property: str) Any

Reconstruct a table-valued property as a pandas DataFrame.

Restores the column order and dtypes recorded by set_table_property() (columns that held nulls come back as pandas nullable dtypes, e.g. Int64). A node without the property yields an empty frame with the registered columns.

Raises:

ValueError – No such node.

graph_info() dict[str, Any]

Get diagnostic information about graph storage health.

Returns a dictionary with storage metrics useful for deciding when to call vacuum() or reindex().

Returns:

  • node_count: Number of live nodes

  • node_capacity: Upper bound of node indices (includes tombstones)

  • node_tombstones: Number of wasted slots from deletions

  • edge_count: Number of live edges

  • edge_capacity: Upper bound of edge indices (includes slots freed by relationship deletes)

  • edge_tombstones: Wasted edge slots. The only garbage a relationship-only delete workload produces, and invisible to fragmentation_ratio, which is node-shaped

  • fragmentation_ratio: Ratio of wasted node storage (0.0 = clean)

  • type_count: Number of distinct node types

  • property_index_count: Number of single-property indexes

  • composite_index_count: Number of composite indexes

  • storage_mode: "memory", "mapped" or "disk" — the backend the graph is actually running on. For a graph opened from a path this is the mode its checkpoint recorded, so it is how you confirm a reopen (or a storage= conversion) landed where you expected

  • format_version: .kgl on-disk layout version (engine-owned)

  • library_version: kglite version that last saved the graph

  • user_schema_version: your data-model revision (see schema_version); 0 when unversioned

  • columnar_heap_bytes: Heap-resident bytes in the property columns

  • columnar_is_mapped: Whether any property column is file-backed rather than heap-resident (after a spill, or on a graph opened from a file). This reports column spilling — set_memory_limit(), or the "mapped" storage mode which pins that limit at 0 — not disk-mode health. A disk graph reports False here unless its columns also spilled; that is normal, see edges_mapped

  • edges_mapped: Whether the edge CSR arrays are memory-mapped from files. True on a disk graph whose CSR is materialized; always False on the memory and mapped backends, which keep edges in the heap graph and have no CSR

  • edge_property_overlay_rows: Edges whose properties are held in the disk backend’s heap mutation overlay rather than the mmap-backed base. 0 on every non-disk backend

  • memory_limit: Configured memory limit (None if unset)

  • columnar_total_rows: Total property-column rows, including rows orphaned by deleted nodes

  • columnar_live_rows: Rows backed by live nodes

  • auto_vacuum_threshold: The configured auto-vacuum threshold, or None when disabled (see set_auto_vacuum())

  • auto_vacuums_run: How many times auto-vacuum has fired on this graph object. Counts fired vacuums, not reclaimed slots — on the disk backend a vacuum reclaims nothing and still counts. Not persisted; a reopened graph starts at 0

Return type:

dict with keys

Example:

info = graph.graph_info()
if info['fragmentation_ratio'] > 0.3:
    graph.vacuum()
has_composite_index(node_type: str, properties: list[str]) bool

Check if a composite index exists.

has_index(node_type: str, property: str) bool

Check if an in-memory equality index exists.

A disk-backed persistent index (one create_index reported as persistent) reports False.

has_schema() bool

Check if a schema has been defined.

has_text_index(node_type: str, property: str) bool

Whether a BM25 text index is currently built over (node_type, property).

has_vector_index(node_type: str, text_column: str) bool

Whether an HNSW index is currently built over the (node_type, text_column) embedding store.

ids() list[Any]

Return a flat list of ID values from the current selection.

The lightest retrieval method — no dict wrapping.

import_embeddings(path: str) dict[str, int]

Import embeddings from a .kgle file.

Matches embeddings to nodes by (node_type, node_id). Embeddings whose node ID doesn’t exist in the current graph are skipped.

When all embeddings — or whole per-type stores — fail to match, a UserWarning is emitted to surface the silent-drop case. This most commonly indicates the .kgle file was exported from a different graph, or that the node ID schema has drifted (e.g. the a code-graph qualified-name format changed across builder versions).

Parameters:

path – Path to a .kgle file previously created by export_embeddings().

Returns:

  • stores: number of stores actually inserted.

  • imported: total embedding vectors matched to current nodes.

  • skipped: total entries in the file that didn’t match.

  • dropped_stores: number of per-type stores in the file that contained entries but had zero matches (so the store was not inserted).

Return type:

Dict with

index_stats(node_type: str, property: str) dict[str, Any] | None

Get statistics for an in-memory equality index.

Returns None when no such index exists — a disk-backed persistent index reports None.

indexes() list[dict[str, Any]]

Return a unified list of all indexes.

Returns:

  • node_type: the indexed node type

  • property: property name (for equality indexes)

  • properties: list of property names (for composite indexes)

  • type: 'equality' or 'composite'

Return type:

List of dicts, each with

indices() list[int]

Return raw graph indices for selected nodes.

intersection(other: KnowledgeGraph) KnowledgeGraph

Keep only nodes present in both selections (set intersection).

Returns:

A new KnowledgeGraph with only shared nodes.

intersects_geometry(query_wkt: str | Any, geometry_field: str | None = None) KnowledgeGraph

Filter nodes whose geometry intersects a WKT geometry.

Parameters:
  • query_wkt – WKT string or shapely geometry object.

  • geometry_field – Geometry property name. Default from spatial config or 'geometry'.

Returns:

A new KnowledgeGraph with intersecting nodes.

label_pair_counts() list[tuple[str, str, str, int]]

Return the label-pair edge-count cardinality cache.

Lists every (src_type, edge_type, tgt_type) triple present in the graph along with its edge count. Backs the Cypher planner’s 0.9.35 selectivity-aware reorder_match_clauses pass — picking the more-selective driving side on label-skewed patterns by consulting per-triple counts instead of edge-type totals.

Computed lazily: first access walks every edge once (O(E)); subsequent reads return a cached snapshot in O(triples) (typically <100 entries). Edge mutations — Cypher CREATE / DELETE, Python add_connections — invalidate the cache.

Returns:

A list of 4-tuples [(src_type, edge_type, tgt_type, count), ...]. Order is hash-arbitrary; sort the result if a deterministic row order matters.

Example

Iterate over the cached triples with for src, edge, tgt, count in graph.label_pair_counts(): ....

label_propagation(max_iterations: int | None = None, connection_types: list[str] | None = None, timeout_ms: int | None = None) dict[str, Any]

Detect communities using label propagation.

Parameters:
  • max_iterations – Maximum iterations. Default 100.

  • connection_types – Only consider edges of these types. Default all edge types.

  • timeout_ms – Abort after this many milliseconds with an error.

Returns:

Dict with communities, modularity, and num_communities.

last_report() dict[str, Any]

Get the most recent operation report as a dict.

Returns an empty dict if no operations have been performed.

len() int

Count selected nodes without materialising them.

Much faster than len(collect()). Also available via len(graph).

limit(max_per_group: int) KnowledgeGraph

Limit the number of nodes per parent group.

Parameters:

max_per_group – Maximum number of nodes to keep per group.

Returns:

A new KnowledgeGraph with the limited selection.

list_composite_indexes() list[dict[str, Any]]

List all composite indexes.

Each dict has node_type, properties and state; state means what it means on list_indexes().

list_embeddings() list[dict[str, Any]]

List all embedding stores in the graph.

Returns:

List of dicts with node_type, text_column, store_name, dimension, count and metric. text_column is the source column this API takes ('summary'); store_name is the store the column is held in ('summary_emb') — the spelling Cypher’s vector_score() takes. metric is the store’s own metric, or 'cosine' when it recorded none.

list_indexes() list[dict[str, str]]

List the in-memory equality indexes.

Each dict has node_type, property and state. Range, composite and disk-backed persistent indexes are not included.

state is "ONLINE" for a built index and "DEFERRED" for one a kglite.load(..., defer_index_rebuild=True) has declared but not yet built. This is a listing: has_index() answers from the built stores alone, so it reports False for a DEFERRED entry — deliberately, since a query planner that believed an unbuilt index was present would return no rows instead of scanning. Any write or index DDL builds the whole set and turns it ONLINE.

load_ntriples(path: str, *, predicates: list[str] | None = None, languages: list[str] | None = None, node_types: dict[str, str] | None = None, predicate_labels: dict[str, str] | None = None, max_entities: int | None = None, max_triples: int | None = None, verbose: bool = False, progress: Callable[[dict], None] | None = None) dict

Load an N-Triples file into the graph.

Streams the file (supports .bz2, .gz, or plain .nt) and converts RDF triples into a property graph. Designed for Wikidata truthy dumps but works with any N-Triples file.

RDF → property graph mapping:

  • Each unique Q-entity subject becomes a node.

  • rdfs:label (language-filtered) → node title.

  • schema:descriptiondescription property.

  • prop/direct/P* with a Q-entity object → edge.

  • prop/direct/P* with a literal object → node property.

  • P31 (instance of) determines the node type via node_types.

Parameters:
  • path – Path to the N-Triples file (.nt, .nt.bz2, .nt.gz).

  • predicates – Wikidata P-codes to import (e.g. ["P31", "P279"]). None imports all predicates. Labels and descriptions are always imported regardless of this filter.

  • languages – Language codes for label/description literals (e.g. ["en"]). None keeps all languages.

  • node_types – Maps a P31 target Q-code to a human-readable node type name (e.g. {"Q5": "Person", "Q6256": "Country"}). Entities whose P31 value is not in this map use the raw Q-code as their type. Entities without P31 get type "Entity".

  • predicate_labels – Maps P-codes to human-readable edge/property names (e.g. {"P31": "instance_of", "P17": "country"}). Unmapped predicates use the raw P-code.

  • max_entities – Stop after creating this many nodes. Useful for exploratory loading of large dumps.

  • max_triples – Stop after scanning this many triples (entity and non-entity lines alike). Applied alongside max_entities; whichever fires first wins. Useful for benchmarking the bz2/gz/plain decompression + parser pipeline at a fixed wall-time-friendly slice.

  • verbose – Print progress to stderr every 5M triples.

  • progress – Optional callable receiving structured per-phase events. The callback is invoked with a single dict whose "kind" is "start", "update", or "complete" and whose "phase" is one of "phase1", "phase1b", "phase2", "phase3", "finalising". See kglite.progress.TqdmBuildProgress for a tqdm-backed reporter. Errors raised by the callback are swallowed.

Returns:

A dict with load statistics:

{"entities": int, "edges": int, "edges_skipped": int,
 "triples_scanned": int, "seconds": float}

Example:

graph = KnowledgeGraph()
stats = graph.load_ntriples(
    "latest-truthy.nt.bz2",
    predicates=["P31", "P279", "P17", "P106"],
    languages=["en"],
    node_types={"Q5": "Person", "Q6256": "Country"},
    predicate_labels={"P31": "instance_of", "P17": "country"},
    max_entities=1_000_000,
    verbose=True,
)
lock_schema() KnowledgeGraph

Lock the schema: Cypher must conform to the current types.

When locked, CREATE, SET, and MERGE operations are validated against the graph’s known node types, connection types, and property types, and reads are validated too: an unknown node label in a MATCH, OPTIONAL MATCH, MERGE, WHERE EXISTS { ... } pattern predicate, or CALL { ... } subquery raises SchemaError instead of silently returning zero rows. Errors name the offending label or property, enumerate the valid set, and add a ‘did you mean?’ suggestion.

A locked graph also rejects a property name no node of the type carries, wherever it is read — WHERE p.agee = 1 (which would filter out every row) and RETURN/WITH/ORDER BY p.agee (which would produce a column of nulls beside correct-looking siblings). Unlocked, both are non-fatal warnings.

It rejects one more thing: a comparison a property’s declared type can never satisfy — WHERE p.age > 'forty' where CREATE CONSTRAINT ... REQUIRE p.age IS :: INTEGER is in force. The write path enforces that declaration, so no row can answer the predicate and the empty result is a certainty rather than data. The same mistake hidden behind a bound parameter (WHERE p.age > $cutoff with a string bound) raises too; the verdict is per call, so the same statement runs with an integer bound. A type declared only by define_schema() is not promoted: the write path never enforced it, so the mismatch stays a warning in both schema states.

The check is deliberately narrow, so a lock never rejects a valid query. It stays silent on: a sparse property (one node carrying it makes it known, however many leave it null); a property this same statement writes; a type with no recorded properties; a property the graph’s own define_schema() declares but nothing has written yet; a multi-label pattern or a variable rebound by WITH, neither of which resolves to one type; and the built-ins id, title, name, type. A relationship type the graph has never seen, a relationship arrow pointing the wrong way, and a type mismatch read from a define_schema() field type rather than an IS :: T constraint, stay warnings in both states.

This is the “catch my typos” mechanism — an empty result set is indistinguishable from “no matching data”, so a typo’d label would otherwise reach production looking like a legitimate empty state.

On an unlocked graph (the default) kglite is schemaless: an unknown label matches nothing and is reported only as a non-fatal warning: on stderr, keeping the zero-row existence-check idiom valid.

Returns:

This same graph (not a copy), so the call can be chained.

Example:

graph.lock_schema()
graph.cypher("CREATE (p:Typo {name: 'x'})")  # raises SchemaError
graph.cypher("MATCH (p:Persom) RETURN p")    # raises SchemaError
# Schema error: Unknown node type 'Persom'. Did you mean 'Person'?
#   Valid types: Paper, Person
graph.cypher("MATCH (p:Person) RETURN p.agee")  # raises SchemaError
# Schema error: Unknown property 'agee' on Person, referenced in
# RETURN. Did you mean 'age'?
graph.cypher("MATCH (p:Person) WHERE p.age > 'forty' RETURN p")
# raises SchemaError when p.age IS :: INTEGER is declared
louvain_communities(weight_property: str | None = None, resolution: float | None = None, connection_types: list[str] | None = None, timeout_ms: int | None = None) dict[str, Any]

Detect communities using the Louvain algorithm.

Parameters:
  • weight_property – Edge property to use as weight. Default all edges weight 1.0.

  • resolution – Resolution parameter (higher = more communities). Default 1.0.

  • connection_types – Only consider edges of these types. Default all edge types.

  • timeout_ms – Abort after this many milliseconds with an error.

Returns:

Dict with communities (dict of community_id to member list), modularity, and num_communities.

match_pattern(pattern: str, max_matches: int | None = None) list[dict[str, Any]]

Match a Cypher-like pattern against the graph.

Supports node patterns (a:Type {prop: val}), directed edges -[:TYPE]->, <-[:TYPE]-, and undirected -[:TYPE]-.

Parameters:
  • pattern – Pattern string, e.g. '(a:Person)-[:KNOWS]->(b:Person)'.

  • max_matches – Maximum results to return.

Returns:

List of match dicts with variable bindings.

materialize_ontology(adopt: bool = False) list[dict[str, Any]]

Materialize declared supertypes as real secondary labels.

Student is_a Person stamps the actual secondary label :Person onto every Student node (through the bulk label path, so WAL/CDC/rollback all see it) — MATCH (p:Person) then matches with today’s query semantics and today’s indexes; no new syntax, and labels(n) agrees with what queries see. Write paths maintain the closure from then on.

Each materialized label is managed, in one of two states: closed (the engine is its only writer; the bucket holds exactly the declared closure) or open (a manual SET, an adoption, or an extend-graph union touched it — still correct, but closure-reliant optimizations stay off for it). Manual REMOVE of a managed label is refused; dematerialize_ontology() is the exit.

Parameters:

adopt – Manage a label whose bucket already has members outside the declared closure (it becomes open). Without it such a collision is refused and nothing is stamped.

Returns:

label, stamped (nodes that gained it in this call), state.

Return type:

One dict per managed label

Raises:

ValueError – No ontology declared, or a collision without adopt.

near_point(center_lat: float, center_lon: float, max_distance: float, lat_field: str | None = None, lon_field: str | None = None) KnowledgeGraph

Filter nodes within a distance (in degrees) of a point.

Parameters:
  • center_lat – Center latitude.

  • center_lon – Center longitude.

  • max_distance – Maximum distance in degrees.

  • lat_field – Latitude property name. Default from spatial config or 'latitude'.

  • lon_field – Longitude property name. Default from spatial config or 'longitude'.

Returns:

A new KnowledgeGraph with nearby nodes.

near_point_m(center_lat: float, center_lon: float, max_distance_m: float, lat_field: str | None = None, lon_field: str | None = None) KnowledgeGraph

Filter nodes within a distance (in meters) using geodesic calculation.

Uses WGS84 ellipsoid for accurate Earth-surface distances. Falls back to geometry centroid when lat/lon fields are missing but a WKT geometry is configured via set_spatial.

Parameters:
  • center_lat – Center latitude.

  • center_lon – Center longitude.

  • max_distance_m – Maximum distance in meters.

  • lat_field – Latitude property name. Default from spatial config or 'latitude'.

  • lon_field – Longitude property name. Default from spatial config or 'longitude'.

Returns:

A new KnowledgeGraph with nearby nodes.

neighbors_schema(node_type: str) dict[str, list[dict[str, Any]]]

Return connection topology for a node type.

Parameters:

node_type – The node type to inspect.

Returns:

  • outgoing: list of {connection_type, target_type, count}

  • incoming: list of {connection_type, source_type, count}

Return type:

Dict with

Raises:

KeyError – If node_type does not exist.

node(node_type: str, node_id: Any) dict[str, Any] | None

Look up a single node by type and ID. O(1) via hash index.

Parameters:
  • node_type – The node type (e.g. 'User').

  • node_id – The unique ID value.

Returns:

Node property dict, or None if not found.

node_type_counts() dict[str, int]

Get node counts per type without materialising nodes.

Returns:

Dict mapping node type name to count.

offset(n: int) KnowledgeGraph

Skip the first n nodes per parent group (pagination).

Combine with limit() for pagination: graph.sort('name').offset(20).limit(10)

Parameters:

n – Number of nodes to skip.

Returns:

A new KnowledgeGraph with the offset selection.

ontology() dict[str, Any] | None

The declared semantic layer as a dict, or None if none declared.

ontology_diff() list[dict[str, Any]]

Drift report per managed label.

Returns:

label, state (closed/open), extra (members the declared closure does not explain), missing (closure members the bucket lacks).

Return type:

One dict per managed label

operation_index() int

Get the sequential index of the last operation.

pagerank(damping_factor: float | None = None, max_iterations: int | None = None, tolerance: float | None = None, connection_types: str | list[str] | None = None, top_k: int | None = None, timeout_ms: int | None = None, to_df: bool | None = None) ResultView | pandas.DataFrame

Calculate PageRank centrality.

Parameters:
  • damping_factor – Probability of following a link. Default 0.85.

  • max_iterations – Maximum iterations. Default 100.

  • tolerance – Convergence threshold. Default 1e-6.

  • connection_types – Only traverse these relationship types (str or list).

  • top_k – Return only the top K nodes.

  • timeout_ms – Abort after this many milliseconds with an error.

  • to_df – Return a pandas DataFrame with columns type, title, id, score.

Returns:

A ResultView of rows with type, title, id, score, sorted by score descending. Or a pandas DataFrame if to_df=True.

properties(node_type: str, max_values: int = 20) dict[str, dict[str, Any]]

Return property statistics for a node type.

Only properties that exist on at least one node are included.

Parameters:
  • node_type – The node type to inspect.

  • max_values – Include values list when unique count <= this threshold. Set to 0 to never include values. Default: 20.

Returns:

  • type: type string (e.g. 'str', 'int', 'float')

  • non_null: count of non-null values

  • unique: count of distinct values (a lower bound when approx is True)

  • values: (optional) sorted list of values when unique count <= max_values

  • approx: True when unique/values are not exhaustive — the type was sampled (only huge, Wikidata-scale types) or the distinct-value set hit its cap. Types at or below ~200k nodes are scanned in full and report exact stats (approx False).

Return type:

Dict mapping property name to stats dict with keys

Raises:

KeyError – If node_type does not exist.

purge_provisional() dict[str, Any]

Delete provisional stub nodes that were never promoted.

When an edge is loaded against a node that doesn’t exist, the node is auto-vivified as a provisional stub (carrying _provisional) so the edge isn’t lost. A later load of the real node row clears the marker. purge_provisional() deletes whatever is still marked — genuinely dangling references — along with their incident edges.

Important: This resets the current selection since node indices change. Call this between query chains, not in the middle of one.

Returns:

  • nodes_purged: Number of provisional stub nodes deleted

  • edges_removed: Number of incident edges removed with them

Return type:

dict with keys

Example:

result = graph.purge_provisional()
print(f"Dropped {result['nodes_purged']} dangling stubs")
read_only(enabled: bool | None = None) bool

Set or query read-only mode for the Cypher layer.

When enabled, all Cypher mutation queries (CREATE, SET, DELETE, REMOVE, MERGE) are rejected, and describe() announces the restriction in a <read-only> element (the Cypher reference it renders is unchanged). Read-only queries (MATCH, RETURN, CALL, etc.) are unaffected.

Parameters:

enabled – If True, enable read-only mode. If False, disable. If omitted, return the current state without changing it.

Returns:

The current read-only state (after applying the change, if any).

Example:

graph.read_only(True)   # lock the graph
graph.read_only()       # -> True
graph.read_only(False)  # unlock
rebuild_caches() None

Force recomputation of internal caches (edge type counts, etc.).

Call once after bulk mutations to warm the cache before save() or describe(). The cache is persisted by save() and restored by load(), so this only needs to be called once after building or mutating a graph.

Example:

g = KnowledgeGraph()
g.add_nodes(...)
g.add_connections(...)
g.rebuild_caches()   # one-time O(E) pass
g.save("graph.kgl")  # persists warm cache
rebuild_indexes() int

Rebuild the in-memory equality indexes.

Range, composite and disk-backed persistent indexes are left untouched. Returns the number of indexes rebuilt.

refresh_vector_index(node_type: str, text_column: str) int

Fold every outstanding vector into the HNSW index now.

Returns how many vectors were folded in — 0 when the index is already current, when none is built (catch-up never builds one), or on a read-only graph. Queries do this on their own while the outstanding delta stays under auto_refresh_limit; call this to pay the cost at a moment of your choosing, or to bring a larger delta back in one incremental step instead of rebuilding the whole index.

reindex() None

Rebuild all indexes from the current graph state.

Reconstructs type_indices, property_indices, and composite_indices by scanning all live nodes. Clears lazy caches (id_indices, connection_types) so they rebuild on next access.

Use after bulk mutations (especially Cypher DELETE/REMOVE) to ensure index consistency.

Example:

graph.reindex()
remove_embeddings(node_type: str, text_column: str) None

Remove an embedding store.

Parameters:
  • node_type – The node type.

  • text_column – Source text column name (e.g. 'summary').

remove_label(node_type: str, ids: list[Any], label: str) dict[str, int]

Remove a secondary label from a batch of nodes by id.

Errors if label is the primary type. Changing the primary type requires recreating or migrating the node.

Parameters:
  • node_type – Primary type of the nodes.

  • ids – Node ids.

  • label – Secondary label to remove.

Returns:

Dict with removed and skipped (unknown ids, or label not present on the node).

replace_connections(data: pandas.DataFrame | None, connection_type: str, source_type: str, source_id_field: str, target_type: str, target_id_field: str, source_title_field: str | None = None, target_title_field: str | None = None, columns: list[str] | None = None, skip_columns: list[str] | None = None, conflict_handling: str | None = None, column_types: dict[str, str] | None = None, query: str | None = None, extra_properties: dict[str, Any] | None = None, git_sha: str | None = None, modified_by: str | None = None, on_invalid: Literal['warn', 'error', 'skip'] = 'warn') dict[str, Any]

Replace a node’s outgoing edges of a given type, then add new ones — an atomic edge upsert.

Unlike add_connections() (add-only), this prunes first: for every source node present in data (or the query result), its existing edges of connection_type are removed, then the edges the input describes are added. Edges from sources not in the input, and edges of other types from the same sources, are left untouched. The prune and add happen in one call, so there is no clear-then-add window that could leave a node edgeless if a separate re-add step failed.

Use it to re-sync a derived edge set idempotently — “the current MENTIONS of exactly these documents is this list”:

# First sync: doc 1 -> [A, B]
graph.replace_connections(df_ab, 'MENTIONS', 'Doc', 'doc', 'Entity', 'ent')
# Re-sync doc 1 -> [B, C]: the stale 1->A edge is pruned, 1->C added.
graph.replace_connections(df_bc, 'MENTIONS', 'Doc', 'doc', 'Entity', 'ent')

Accepts every argument add_connections() does (including query mode and extra_properties), with identical semantics; only the prune-first behaviour differs.

Parameters:
  • data – DataFrame containing edge data, or None when using query.

  • connection_type – Edge type to replace (e.g. 'MENTIONS').

  • source_type – Node type of source nodes.

  • source_id_field – Column with source node IDs.

  • target_type – Node type of target nodes.

  • target_id_field – Column with target node IDs.

  • source_title_field – Optional title column for source nodes.

  • target_title_field – Optional title column for target nodes.

  • columns – Optional property-column whitelist (data mode only). When omitted, all non-skipped DataFrame columns are preserved.

  • skip_columns – Columns to exclude (data mode only).

  • conflict_handling'update' (default), 'replace', 'skip', 'preserve', or 'sum'.

  • column_types – Override column dtypes (data mode only).

  • query – Cypher query string (alternative to data). Must be read-only.

  • extra_properties – Static properties stamped onto every edge (query mode only).

  • git_sha – Commit SHA stamped when the edge type has auto_timestamp=True.

  • modified_by – Actor id stamped when the edge type has auto_timestamp=True.

  • on_invalid – What to do about rows whose source or target ID is null. 'warn' (default) skips them, counts them in the report and emits a UserWarning; 'error' refuses the whole call with ArgumentError, naming the count, the first offending row and the value it holds, before anything is written; 'skip' is 'warn' without the warning. A row whose endpoint is missing rather than null is vivified as a stub node, not skipped, and is unaffected by this setting.

Returns:

Operation report dict with connections_created, connections_skipped, etc.

report_history() list[dict[str, Any]]

Get all operation reports as a list of dicts.

sample(node_type: str, n: int = 5) ResultView
sample(n: int = 5) ResultView
save(path: str | None = None, *, fsync: bool = True) None

Serialise the graph to disk, atomically and durably.

For default/mapped modes: saves to a .kgl binary file. For disk mode: saves to a directory containing CSR files and compressed node/edge data. The directory IS the saved graph.

Crash-safety (default/mapped modes). The file is written to a sibling temp file and then atomically renamed over the target, so a crash mid-write can never leave a torn/truncated .kgl — a reader always sees either the previous file or the complete new one. With fsync=True (default) the file and its directory are flushed to physical storage before returning, so a committed save survives an OS/power crash. The temp name is unique per process, so two processes saving the same path won’t corrupt each other’s in-flight write (last rename wins, cleanly — keep one writer per file).

The property columns are consolidated on the way out — the same pass that reclaims rows left behind by deleted nodes — which is what makes the file compress well and load larger-than-RAM.

Load it back with kglite.load() (accepts both files and directories).

A path whose write-ahead log runs ahead of it is refused. If a <path>-wal sidecar beside the target still holds commits this graph does not contain — a durable writer that died before its next checkpoint — saving here would strand them: the sidecar outlives the new file, and the next kglite.open(path, durable=...) replays those commits back over what was just saved. save() raises ValueError instead, naming the sidecar and the two ways out: reopen the path durably (kglite.open(path, durable="full")) to replay the commits first, or move the sidecar aside to discard them deliberately. A graph opened with durable= is never affected — its own checkpoint folds its log in.

Saving does not take the writer lease. The lease belongs to kglite.open(), which holds it for as long as the graph can write back to path; save() itself writes whatever target it is given without asking for it. So a graph obtained from kglite.load() can publish over a path an open() holder — or a running MCP or Bolt server — is mid-write on: the file that results is a complete graph, it is simply this one, and whatever the holder had not yet saved is not in it. A caller that may save to a path should hold the lease across the whole read-modify-save interval, which is what kglite.open(path) does; load() + save(path) is a write that opted out of it.

Parameters:
  • path – Output file path (typically *.kgl). May be omitted if the graph was opened via kglite.open() or kglite.load(), in which case it defaults to that origin file. Passing a path updates the remembered target (“save as”). Raises ValueError if omitted and there is no remembered path.

  • fsync

    When True (default), flush the file and its parent directory to disk before returning (durable against an OS/power crash). Set False to skip the flush for speed — the write is still atomic (temp + rename, no torn file), just not guaranteed flushed to physical media when the call returns.

    For graphs opened with kglite.open(..., durable=True), fsync=False is ignored (a UserWarning is emitted and the flush happens anyway): the save is the checkpoint that truncates the fsync’d write-ahead log, so skipping the flush could lose both the checkpoint and the log on a crash.

Raises:
  • kglite.FileIoError – The write itself failed — a full disk, a read-only directory, a failing device. Carries .code == "FileIo". Not a bare OSError: the write path classifies the same way the load path does.

  • ValueError – The call was refused before the path was touched — no remembered path, or a write-ahead sidecar that runs ahead of the target (above).

save_subset(path: str) None

Save the current selection as an independent subgraph file.

Equivalent to kg.to_subgraph().save(path) in a single call. Output is a .kgl file that reloads via kglite.load(path). A .kgl is a file, never a disk-mode directory, so storage='disk' is refused on either entry point; build a disk graph with kglite.open(dir, storage='disk') and ingest into it. All edges between selected nodes are included; node and edge properties round-trip byte-for-byte.

Parameters:

path – Destination path for the subgraph file.

Example

>>> kg.select("Article").expand(hops=1).save_subset(
...     "articles_with_authors.kgl"
... )
schema() dict[str, Any]

Return a full schema overview of the graph.

Returns:

  • node_types: {type_name: {count, properties: {name: type_str}}}

  • connection_types: {conn_name: {count, source_types: list, target_types: list}}

  • indexes: list of "Type.property" strings

  • node_count: total nodes

  • edge_count: total edges

Return type:

Dict with keys

Note

Scans all edges once (O(m)) to compute accurate connection type stats.

schema_definition() dict[str, Any] | None

Get the current schema definition as a dict, or None.

schema_text() str

Return a text summary of the graph schema (node types, connections).

search(text: str, *, property: str = 'title', limit: int = 10) list[dict[str, Any]]

Find nodes matching text on property (default ‘title’).

Tries exact match first, then prefix match. Returns up to limit results as dicts with id (node index), type, title, and id_value (the node’s id-field value, e.g. a Wikidata Q-number).

Requires create_global_index(property) to have been run on a disk-backed graph. Returns an empty list otherwise.

Example:

graph.create_global_index('label')
hits = graph.search('Norway')
# [{'id': 12345, 'type': 'country', 'title': 'Norway',
#   'id_value': 'Q20'}]
search_text(text_column: str, query: str, top_k: int = 10, metric: str | None = None, to_df: bool = False, returning: list[str] | None = None, exact: bool = False) list[dict[str, Any]] | pandas.DataFrame

Search embeddings using a text query.

Uses the model registered via set_embedder() to embed the query, then performs vector search within the current selection — or the whole graph when no selection is active (see vector_search()). Refer to the text column name (e.g. "summary"); the graph resolves it to "summary_emb" internally.

Parameters:
  • text_column – Text column whose embeddings to search (e.g. 'summary').

  • query – The text query to search for.

  • top_k – Number of results (default 10).

  • metric'cosine', 'dot_product', 'euclidean', or 'poincare'. Omitted uses the same selection-aware stored metric resolution as vector_search().

  • to_df – If True, return a pandas DataFrame.

  • returning – Optional field projection — see vector_search(). Omitted → full hit (all properties); given → id + score + the named fields only.

  • exact – Force an exact brute-force scan even when an HNSW index exists — see vector_search() and build_vector_index().

Returns:

Same format as vector_search() — list of dicts or DataFrame.

Raises:

ValueError – same unknown-store contract as vector_search().

Example:

results = g.select("Article").search_text(
    "summary", "find AI articles", top_k=10
)
select(node_type: str, sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None, temporal: bool | None = None, include_secondary: bool = False) KnowledgeGraph

Select all nodes of a given type.

When a temporal config exists for this node type (via set_temporal()), nodes are auto-filtered to those valid at the reference date (today or date() context). Pass temporal=False to include all nodes.

Parameters:
  • node_type – The node type to select (e.g. 'Person').

  • sort – Optional sort spec — a property name or list of (field, ascending) tuples.

  • limit – Limit the number of selected nodes.

  • temporal – Override temporal filtering. None = auto (filter if configured), False = disable, True = require (error if not configured).

  • include_secondary – When True, also select nodes that carry node_type as a secondary label (added via add_label()), not only nodes whose primary type is node_type — the fluent equivalent of Cypher MATCH (n:node_type). Default False preserves primary-type-only selection. On a graph with no secondary labels the two are identical.

Returns:

A new KnowledgeGraph with the filtered selection.

selection() str

Return a text summary of the current selection state.

session() Session

Seed a thread-safe, shareable Session from this graph.

Unlike a live KnowledgeGraph — which is single-owner and trips a borrow guard when shared across threads mid-mutation — a Session exposes only &self methods with synchronisation in an internal lock: concurrent cypher() reads run lock-free, and execute() writes serialise behind the lock with copy-on-write + atomic swap.

The Session is an independent owner: it shares this graph’s data at creation (O(1), no copy), but once either side mutates, copy-on-write forks them and they no longer track each other. The intended model is “build / load with a KnowledgeGraph, then .session() and serve every thread through the Session” — don’t keep mutating the original graph after handing out a Session.

set_auto_vacuum(threshold: float | None) None

Configure automatic vacuum after DELETE operations.

When enabled, the graph automatically compacts itself after Cypher DELETE operations if the fragmentation ratio exceeds the threshold and there are more than 100 tombstones.

Three independent kinds of garbage feed the trigger, and the worst of them decides: free node slots, property-column rows left behind by deleted nodes, and free edge slots. The last is what a relationship-only workload (MATCH ()-[r]->() DELETE r) produces — it leaves every node alive, so the other two readings stay clean.

A held selection is carried through the compaction rather than reset; see vacuum().

Read the current setting back with graph_info()['auto_vacuum_threshold'], and how often it has fired with graph_info()['auto_vacuums_run'].

Parameters:

threshold – A float between 0.0 and 1.0, or None to disable. Default is 0.3 (30% fragmentation triggers vacuum).

Example:

graph.set_auto_vacuum(0.2)   # more aggressive — vacuum at 20%
graph.set_auto_vacuum(None)  # disable auto-vacuum
graph.set_auto_vacuum(0.3)   # restore default
set_default_max_work_units(max_work_units: int | None = None) None

Set a default per-query work budget for all cypher() calls.

This is a work budget, not a result-row cap. It is charged against intermediate rows, retained collection items and scan work — every quantity the executor holds or walks on the way to an answer — so the count can far exceed the rows a query returns. A query that exceeds the budget raises an error; it is never truncated to it. To bound the rows you get back, write LIMIT in the query.

Because it is a hard refusal rather than a soft cap, size it from a count(*) probe of the patterns you run and leave real headroom — a query sitting at 97% of its budget today fails outright on slightly larger data instead of merely slowing down. When a deadline is set too, expect the budget to be what fires on a runaway pattern: the budget bounds what a query holds and the deadline bounds how long it runs, and an explosive expansion reaches the memory ceiling long before the clock.

Parameters:

max_work_units – Positive work-unit budget, or None (default) to set no explicit budget, leaving the engine’s own 10,000,000-unit backstop in charge of materialized quantities.

Returns:

None.

Raises:
  • Nothing here — a breach surfaces later, from the cypher() call

  • that exceeds the budget, as CypherExecutionError.

Example

>>> kg.set_default_max_work_units(1_000_000)
>>> kg.cypher("MATCH (a)--(b)--(c) RETURN count(*)")
Traceback (most recent call last):
kglite.CypherExecutionError: Query produced 1000001 rows while
executing MATCH, exceeding the max_work_units budget of 1000000.
Add a LIMIT clause or raise max_work_units.

Note

Per-query max_work_units overrides this default.

set_default_row_limit(row_limit: int | None = None) None

Set a default cap on the result rows every cypher() call keeps.

This is a result-row cap, not a work budget — the deliberate opposite number to set_default_max_work_units():

knob

bounds

on overrun

max_work_units

work the executor performs

raises

row_limit

rows handed back to you

truncates

The query still runs to completion and still computes every row, so ORDER BY sorts the whole answer and aggregation folds the whole answer; only retention of the finished rows stops at the cap. The rows you keep are therefore the first N of the answer you would have got uncapped — the genuine top-N under ORDER BY. An explicit LIMIT m in the query is applied first, so the effective cap is min(m, row_limit).

Truncation is never silent. It raises a query warning, and ResultView.diagnostics carries row_limit plus the exact pre-truncation total_rows, so a “showing 5,000 of 412,003” banner is answerable from the result alone.

Parameters:

row_limit – Maximum rows to retain, or None (default) to retain everything. 0 is legal and means “keep no rows, still tell me the total”.

Returns:

None.

Example

>>> kg.set_default_row_limit(5_000)
>>> rv = kg.cypher("MATCH (n:Item) RETURN n.id ORDER BY n.id")
>>> len(rv), rv.diagnostics["total_rows"]
(5000, 412003)

Note

Per-query row_limit overrides this default, 0 included. Applies to a mutation’s trailing RETURN as well: the writes all still happen and last_mutation_stats still counts them all, because this caps what is reported, never what is changed. EXPLAIN is exempt.

set_default_timeout(timeout_ms: int | None = None) None

Set a default query timeout (milliseconds) for all cypher() calls.

  • None (default): fall through to the built-in default of 180_000 ms (3 min) for every storage mode.

  • 0: disable the deadline for every query unless per-call timeout_ms overrides.

  • Positive integer: use as the default.

Per-query timeout_ms always overrides this setting.

set_embedder(model: EmbeddingModel | None) None

Register or unbind an embedding model on the graph.

Pass a model object to register; pass None to unbind the currently-registered embedder.

After registering, embed_texts() and search_text() use the registered model automatically. The model is not serialized — call set_embedder() again after deserializing.

If the model has optional load() / unload() methods, they are called automatically around each embedding operation.

Parameters:

model – An embedding model with dimension and embed() — see EmbeddingModel. Or None to unbind.

Example:

g.set_embedder(my_model)
g.set_embedder(None)  # unbind
set_embeddings(node_type: str, text_column: str, embeddings: dict[Any, list[float]], metric: str | None = None) dict[str, int]

Store embeddings for nodes of the given type. Replaces any existing store for (node_type, "{text_column}_emb").

Embeddings are stored separately from regular node properties and are invisible to collect(), to_df(), and other property-based APIs. The embedding store key is auto-derived as {text_column}_emb.

Requires text_column to name something the node type actually has — the guard that catches passing the store name ('summary_emb') where the column name ('summary') belongs. It resolves exactly as a Cypher property reference does: a stored property, an identity alias (a type built with title_field='name' accepts 'name', one built with id_field='npdid' accepts 'npdid'), the canonical id/title, or a structural alias (name, type, node_type, label).

The store is keyed by the spelling you pass, never by what it resolves to: embedding 'name' on a title_field='name' type writes name_emb and is read back as 'name' by vector_search(), text_score() and list_embeddings(). Pick one spelling per column — 'name' and 'title' there are two stores holding the same text.

The whole batch is resolved and dimension-checked before anything is written, so a rejected call leaves the store exactly as it was.

Call save() to persist the store: embedding stores ride the checkpoint. A store records the vectors, dimension and metric you supply here; embed_texts() additionally records the model id and per-node text hashes that let a later embed_texts(mode='changed') re-embed only what changed.

For incremental ingest where you want to add to an existing store without a read-merge-write round-trip, use add_embeddings().

Parameters:
  • node_type – The node type (e.g. 'Article').

  • text_column – Source text column name (e.g. 'summary').

  • embeddings – Dict mapping node IDs to embedding vectors. An id that matches no node of this type is counted in skipped.

  • metric – Default distance metric for this store. Used when no metric is specified at query time. 'cosine' (default), 'dot_product', 'euclidean', or 'poincare'. Persisted with save().

Returns:

Dict with embeddings_stored, dimension, and skipped.

set_instructions(text: str, *, channel: str | None = None) KnowledgeGraph

Set free-text instructions/briefing rendered verbatim at the top of describe() — so an agent that opens the graph cold reads it first.

Unlike sample values in describe(), this text is shown in full (no truncation). It persists in the .kgl. Pass empty text to clear.

Parameters:
  • text – The instructions (plain text / markdown). Shown verbatim.

  • channel – Reserved for per-audience instructions; None (default) sets the single graph-level slot.

Returns:

This same graph (not a copy), so the call can be chained.

set_memory_limit(limit_bytes: int | None, spill_dir: str | None = None) None

Configure automatic memory-pressure spill for the property columns.

While a limit is set, the graph spills its largest column stores to temporary files on disk whenever their total heap usage would exceed it — checked after each mutating statement and by the consolidation pass save() and vacuum() run. unspill() brings them back to the heap; the limit itself survives that and is re-applied by the next write, so pass None first to keep them resident.

The limit governs property columns only — it does not bound nodes, edges or indexes, and enable_disk_mode() is not one of its checkpoints. graph_info()['columnar_is_mapped'] is how you see whether a spill has happened.

Parameters:
  • limit_bytes – Maximum heap bytes for column data, or None to disable the limit.

  • spill_dir – Directory for spill files. Defaults to system temp dir.

Example:

graph.set_memory_limit(500_000_000)  # 500 MB limit
graph.set_memory_limit(None)         # disable limit
set_parent_type(node_type: str, parent_type: str) None

Declare a node type as a supporting child of a parent type.

Supporting types are hidden from the describe() inventory and instead appear in the <supporting> section when the parent type is inspected. Their capabilities (timeseries, spatial, etc.) bubble up to the parent descriptor.

Parameters:
  • node_type – The supporting (child) node type.

  • parent_type – The core (parent) node type.

Raises:

ValueError – If either type does not exist in the graph.

Example:

graph.set_parent_type('ProductionProfile', 'Field')
graph.set_parent_type('FieldReserves', 'Field')
set_schema_version(version: int) KnowledgeGraph

Stamp your data-model revision on the graph.

Persisted on the next save().

Parameters:

version – The revision number. 0 marks the graph unversioned.

Returns:

This same graph (not a copy), so the call can be chained.

Example:

graph.cypher("MATCH (p:Person) SET p.email = 'unknown'")
graph.set_schema_version(1).save("graph.kgl")
set_spatial(node_type: str, *, location: tuple[str, str] | None = None, geometry: str | None = None, points: dict[str, tuple[str, str]] | None = None, shapes: dict[str, str] | None = None) None

Configure spatial properties for a node type.

Parameters:
  • node_type – The node type to configure.

  • location – Primary lat/lon pair as (lat_field, lon_field). At most one per type.

  • geometry – Primary WKT geometry field name. At most one per type.

  • points – Named lat/lon points as {name: (lat_field, lon_field)}.

  • shapes – Named WKT shape fields as {name: field_name}.

set_table_property(node_type: str, node_id: Any, property: str, data: Any) int

Store a DataFrame as a table-valued property.

The stored value is a plain list<map> — queryable with today’s Cypher (UNWIND o.line_items, o.line_items[2].qty, the table.upsert/table.delete procedures, nested SET paths). Column order, dtypes, and nullability are recorded in a per-(node_type, property) registry persisted with the graph and restored by get_table_property(); Cypher reads see map keys in sorted order as for any map. The write routes through Cypher SET, so write scope, constraints, declared shapes, WAL, and CDC all apply. Unsupported cell values raise (on_invalid='error' semantics).

Note the memory shape: a large embedded table lives in a Mixed (heap-only) column that cannot spill to disk — for independently addressable rows at scale, prefer row nodes (see attach_rows and the “embedded table vs row nodes” guide).

Parameters:
  • node_type – The parent node’s type (plain identifier).

  • node_id – The parent node’s id.

  • property – Property name to store under (plain identifier).

  • data – A pandas DataFrame.

Returns:

Number of rows stored.

Raises:

ValueError – Unknown node, empty/invalid frame, bad identifier, unsupported cell values, or a declared-shape violation.

set_temporal(type_name: str, valid_from: str, valid_to: str) None

Configure temporal validity for a node type or connection type.

After configuration, select() auto-filters temporal nodes and traverse() auto-filters temporal connections to “current” (today or the date() context).

Auto-detects whether type_name is a node type or connection type.

Parameters:
  • type_name – Node type (e.g. 'FieldStatus') or connection type (e.g. 'HAS_LICENSEE').

  • valid_from – Property name holding the start date.

  • valid_to – Property name holding the end date.

Raises:

ValueError – If type_name is not a known node or connection type.

set_time_index(node_id: Any, keys: list[str] | list[list[int]]) None

Set the sorted time index for a specific node.

If the node already has a timeseries, this replaces its time index and clears all channels.

Parameters:
  • node_id – The node’s unique ID.

  • keys – Sorted list of date strings (e.g. ['2020-01', '2020-02']) or composite integer keys for backwards compat (e.g. [[2020, 1], [2020, 2]]).

set_timeseries(node_type: str, *, resolution: str, channels: list[str] | None = None, units: dict[str, str] | None = None, bin_type: str | None = None) None

Configure timeseries metadata for a node type.

Parameters:
  • node_type – The node type to configure.

  • resolution – Time granularity — 'year', 'month', or 'day'. Determines key depth (year=1, month=2, day=3).

  • channels – Optional list of known channel names.

  • units – Optional map of channel name to unit string, e.g. {'oil': 'MSm3', 'temperature': '°C'}.

  • bin_type – What values represent — 'total', 'mean', or 'sample'. None if unspecified.

shortest_path(source_type: str, source_id: Any, target_type: str, target_id: Any, connection_types: list[str] | None = None, via_types: list[str] | None = None, weight_property: str | None = None, timeout_ms: int | None = None, direction: str | None = None) dict[str, Any] | None

Find the shortest path between two nodes.

Undirected by default; pass direction for a one-way search.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • weight_property – Edge property to use as cost. When set, the search uses Dijkstra and minimises total weight; when None, BFS minimises hop count. Edges missing the property fall back to weight 1.0 (matches Louvain’s weighted-adjacency convention). Negative weights cause the path to be reported as missing. Honours connection_types / via_types / direction.

  • timeout_ms – Abort after this many milliseconds and return None.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

Returns:

Dict with path (list of node info dicts), connections (list of edge types), and length (hop count). When weight_property is set, also includes weight (sum of edge weights). None if no path exists or timeout is reached.

Note

When several paths tie for shortest, which one is returned is unspecified and may change between releases. Use Cypher’s allShortestPaths(...) to get all of them.

shortest_path_ids(source_type: str, source_id: Any, target_type: str, target_id: Any, connection_types: list[str] | None = None, via_types: list[str] | None = None, timeout_ms: int | None = None, direction: str | None = None) list[Any] | None

Get node IDs along the shortest path.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • timeout_ms – Abort after this many milliseconds and return None.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

Returns:

List of node IDs, or None if no path exists or timeout is reached.

Note

When several paths tie for shortest, which one is returned is unspecified and may change between releases. Use Cypher’s allShortestPaths(...) to get all of them.

shortest_path_indices(source_type: str, source_id: Any, target_type: str, target_id: Any, connection_types: list[str] | None = None, via_types: list[str] | None = None, timeout_ms: int | None = None, direction: str | None = None) list[int] | None

Get raw graph indices along the shortest path.

Fastest path query — no node data lookup.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • timeout_ms – Abort after this many milliseconds and return None.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

Returns:

List of integer indices, or None if no path exists or timeout is reached.

Note

When several paths tie for shortest, which one is returned is unspecified and may change between releases. Use Cypher’s allShortestPaths(...) to get all of them.

shortest_path_length(source_type: str, source_id: Any, target_type: str, target_id: Any, weight_property: str | None = None, connection_types: list[str] | None = None, via_types: list[str] | None = None, direction: str | None = None, timeout_ms: int | None = None) int | float | None

Get just the cost of the shortest path.

Faster than shortest_path() when you only need the distance, and asks exactly the same question — same filters, same direction.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the path may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Target node type. An ID namespace, as above.

  • target_id – Target node ID.

  • weight_property – When set, uses Dijkstra and returns total weight (float). When None, BFS returns hop count (int). Honours the filters and direction either way.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the endpoints are exempt). Default all.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

  • timeout_ms – Abort after this many milliseconds and return None.

Returns:

Hop count (int) or total weight (float), or None if no path exists.

shortest_path_lengths_batch(node_type: str, pairs: list[tuple[Any, Any]], connection_types: list[str] | None = None, via_types: list[str] | None = None, direction: str | None = None, timeout_ms: int | None = None) list[int | None]

Return shortest-path lengths for ID pairs of one node type.

Results preserve input order; unreachable pairs produce None. Builds the adjacency once for the whole batch, so this is much cheaper than a loop over shortest_path_length().

Parameters:
  • node_type – The type both ids of every pair are looked up in. An ID namespace — it does not restrict which node types a path may pass through (use via_types for that; without it a Person-to-Person distance can be answered through a City).

  • pairs(source_id, target_id) tuples.

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only traverse through nodes of these types (the pair endpoints are exempt). Default all.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

  • timeout_ms – Abort after this many milliseconds; pairs not yet answered come back as None.

shortest_path_lengths_from(source_type: str, source_id: Any, target_type: str | None = None, target_ids: list[Any] | None = None, *, connection_types: list[str] | None = None, via_types: list[str] | None = None, direction: str | None = None, max_hops: int | None = None, timeout_ms: int | None = None) dict[Any, int | None]

Hop distances from ONE source to many targets, in a single BFS.

The one-to-many member of the shortest-path family: what N shortest_path_length() calls answer one pair at a time, this answers in one traversal.

At least one of target_ids, target_type or max_hops is required — an unbounded one-to-all walk would materialise a dict with one entry per reachable node, so it is refused by name.

Parameters:
  • source_type – Source node type. An ID namespace — it says which type to look source_id up in, never which node types the walk may pass through (use via_types for that).

  • source_id – Source node ID.

  • target_type – Restricts the result to nodes of this type, and is the ID namespace target_ids are looked up in (defaulting to source_type). It does not restrict the traversal: the walk may still route through any node type unless via_types says otherwise. Without it, results span every node type — and because ids are unique per type but not across types, a reached pair of different types sharing one id raises rather than silently collapsing into one dict key.

  • target_ids – Answer for exactly these ids (see Returns).

  • connection_types – Only traverse edges of these types. Default all.

  • via_types – Only route through nodes of these types. A node the filter excludes is still reported with its own distance — it can be a path end, like the exempt endpoints of the pair members — but nothing is reached through it.

  • direction'outgoing' / 'out' follows edges forwards, 'incoming' / 'in' follows them backwards, 'any' / 'both' / None (default) ignores edge direction. Anything else raises.

  • max_hops – Stop the search after this many hops. 0 returns only the source.

  • timeout_ms – Abort after this many milliseconds — and raise (see below), unlike the pair members, which return None.

Returns:

  • With target_ids: one entry per requested id, in the

    order given, and an unreachable target maps to None. You asked about it, so you get an answer for it.

  • Without target_ids (discovery mode): only the nodes

    actually reached, in non-decreasing distance order. Absent means unreachable (or beyond max_hops); there are no None values, because enumerating every unreached node is the footgun this mode exists to avoid.

The source itself is present at distance 0 whenever it is in scope.

Return type:

{node id: hop count}, in two deliberately different shapes

Raises:

ArgumentError – when none of target_ids / target_type / max_hops is given; when an id or target_type does not exist; when two reached nodes of different types share an id; or when timeout_ms expires — a partial map silently missing its far half is a wrong answer, not a missing one.

Example

>>> # Every Person within 3 hops of Alice.
>>> graph.shortest_path_lengths_from('Person', 'alice', 'Person', max_hops=3)
{'alice': 0, 'bob': 1, 'carol': 2}
>>> # An answer for each of these three, None where unreachable.
>>> graph.shortest_path_lengths_from('Person', 'alice', target_ids=['bob', 'zoe'])
{'bob': 1, 'zoe': None}
show(columns: list[str] | None = None, limit: int = 200) str

Display selected nodes with specific properties in a compact format.

Single level (no traversals): one node per line as Type(val1, val2). Multi-level (after traverse): walks the full chain as Type1(vals) -> Type2(vals) -> Type3(vals).

Parameters:
  • columns – Property names to include. Default ["id", "title"].

  • limit – Maximum output lines. Default 200.

Example:

print(graph.select("Discovery").show(["id", "title"]))
# Discovery(123, Johan Sverdrup)

print(graph.select("Discovery")
    .traverse("IN_FIELD")
    .traverse("DISCOVERY_WELLBORE")
    .show(["id"]))
# Discovery(123) -> Field(456) -> Wellbore(789)
sort(sort: str | list[tuple[str, bool]], ascending: bool | None = None) KnowledgeGraph

Sort the current selection.

Fields are applied in order: the second field breaks ties on the first, and so on. Values of different types are ordered by type rank (map < node < relationship < list < path < temporal < point < string < boolean < number), the same total order ORDER BY uses — see the “Sort order” section of the Cypher reference. A node missing the sort property is ordered as NULL: last ascending, first descending.

Parameters:
  • sort – Property name (string) or list of (field, ascending) tuples.

  • ascending – Direction when sort is a single string. Default True.

Returns:

A new KnowledgeGraph with the sorted selection.

source(name: str, node_type: str | None = None) dict[str, Any]
source(name: list[str], node_type: str | None = None) list[dict[str, Any]]
spatial(node_type: str | None = None) dict[str, Any] | None

Get spatial configuration for a node type or all types.

Parameters:

node_type – If given, return config for this type only. Otherwise return all.

Returns:

Dict with spatial config, or None if not configured.

statistics(property: str, level_index: int | None = None, group_by: str | None = None) Any

Compute descriptive statistics for a numeric property.

Returns per-parent stats including count, mean, std, min, max, sum.

Parameters:
  • property – Numeric property name.

  • level_index – Target level in the hierarchy.

  • group_by – Group results by this property instead of by parent. Returns {group_value: {count, sum, mean, min, max, std}}.

subgraph_stats() dict[str, Any]

Get statistics about the subgraph that would be extracted.

Returns:

Dict with node_count, edge_count, node_types, connection_types.

symmetric_difference(other: KnowledgeGraph) KnowledgeGraph

Keep nodes in exactly one of the selections (symmetric difference).

Returns:

A new KnowledgeGraph with nodes exclusive to each side.

sync() None

Flush every commit made so far to stable storage.

This is the barrier durable="full" performs on every commit, taken on demand — and it is what makes durable="normal" adoptable rather than merely fast. Without it, a "normal" graph’s only route to power-safety is a full save(), which republishes the entire graph: the wrong granularity for “flush at the end of a request” or “flush before shutdown”:

g = kglite.open("app.kgl", durable="normal")
handle_request(g)      # commits survive the process dying
g.sync()               # …and now survive power loss too

Behaviour by level:

  • "normal" — the real work. Everything committed before this call now survives power loss, not just process death.

  • "full" — returns immediately; every commit was already barriered, so the guarantee is already met.

  • "off" or a graph opened without a log — raises ValueError. There is nothing to flush, and silently doing nothing would leave a caller believing they had bought power-safety.

Pending mutations are folded into the log first, so this is also the safe way to force a commit boundary before an external snapshot.

Unlike save(), this writes no checkpoint and does not truncate the log — it only makes the existing log durable.

Raises:
  • kglite.FileIoError – The barrier itself failed. Carries .code == "FileIo".

  • ValueError – This graph has no log to flush (above).

time_index(node_id: Any) list[str] | None

Get the time index for a node as ISO date strings, or None.

timeseries(node_id: Any, channel: str | None = None, start: str | None = None, end: str | None = None) dict[str, Any] | None

Extract timeseries data for a node.

If channel is given, returns {'keys': [...], 'values': [...]}. Otherwise returns {'keys': [...], 'channels': {'name': [...], ...}}.

Parameters:
  • node_id – The node’s unique ID.

  • channel – Optional channel name to extract.

  • start – Optional range start as date string (e.g. '2020', '2020-2').

  • end – Optional range end as date string.

Returns:

Dict with keys and channel data, or None if no timeseries.

timeseries_config(node_type: str | None = None) dict[str, Any] | None

Get timeseries configuration for a node type or all types.

Returns a dict with resolution, channels, units, bin_type.

titles(limit: int | None = None, indices: list[int] | None = None, flatten_single_parent: bool | None = None) list[str] | dict[str, list[str]]

Get titles of selected nodes.

Without traversal (single parent group), returns a flat list of titles. After traversal (multiple parent groups), returns {parent_title: [titles]}.

Parameters:

flatten_single_parent – Flatten single-group results to a list. Default True.

Returns:

list[str] when flattened, dict[str, list[str]] when grouped.

to_bytes() bytes

Serialise the in-memory graph to a .kgl byte buffer.

Returns the same bytes save() writes to disk, so a caller can own the write — push to object storage, a pipe, a checksum, or a custom atomic-write routine — instead of being limited to a filesystem path. Round-trips through kglite.from_bytes().

Default/mapped modes only: a disk-mode graph is a directory, not a single byte stream, so this raises ValueError for disk graphs (use save('dir/') there).

to_df(*, include_type: bool = True, include_id: bool = True) pandas.DataFrame

Export current selection as a pandas DataFrame.

Each node becomes a row with columns for title, type, id, and all properties. Missing properties across different node types become None.

id, title and type come from the node’s canonical identity. A node may also store a property under one of those names — CREATE (:T {title: 'a'}) sets title both ways — in which case the canonical value wins and the property is not repeated as a second column. Opt a canonical column out to read the stored property instead: with include_type=False there is no type column to collide with, so a stored type property is returned as itself.

Parameters:
  • include_type – Include type column. Default True.

  • include_id – Include id column. Default True.

Returns:

DataFrame with one row per selected node. Column names are unique, so the frame is directly writable with to_parquet() / to_csv().

to_networkx(*, node_key: str = 'id') Any

Convert the graph to a networkx.MultiDiGraph.

KGLite is a directed multigraph with typed nodes and edges, so MultiDiGraph is the lossless target. node_key chooses the networkx node key: 'id' (default) uses the bare node id, 'type_id' uses the (node_type, id) 2-tuple. node_type, title and every property are attached as node attributes; the two identity attributes overwrite a property of the same name rather than being shadowed by it. The first edge for a node pair uses its connection_type as the networkx key; additional same-type parallel edges use collision-safe composite keys. Every edge also stores the type in its connection_type attribute alongside all properties.

The inverse is kglite.from_networkx(). Because DataFrame edge ingestion identifies an edge by endpoints plus type, importing a NetworkX graph collapses same-type duplicate edges with identical endpoints even though this export preserves them.

Requires the networkx extra: pip install "kglite[networkx]".

Parameters:

node_key'id' (default) or 'type_id'. Ids are unique within a node type but reused across types, so 'type_id' is the collision-free key for a multi-type graph.

Returns:

A networkx.MultiDiGraph mirroring the full graph.

Raises:

ArgumentErrornode_key is neither 'id' nor 'type_id'; or node_key='id' and two nodes of different types share an id. Ids are unique per type, not across types, and the bare-id networkx node key would merge the two nodes into one, the second overwriting the first’s attributes and both nodes’ edges rewiring onto the survivor. Give the colliding types disjoint ids, or export with node_key='type_id'; because the export is whole-graph, narrowing the selection cannot avoid it.

Note

v1 always exports the full graph; the active selection is ignored. A future revision may honour selections.

Example:

import networkx as nx

nxg = graph.to_networkx()
scores = nx.pagerank(nxg)

# Multi-type graph whose ids overlap across types:
nxg = graph.to_networkx(node_key='type_id')
scores = nx.pagerank(nxg)   # keys are ('Person', 5) tuples
to_str(limit: int = 50) str

Format the current selection as a human-readable string.

Each node is printed as a block with [Type] title (id: x) header and indented properties, one per line.

Parameters:

limit – Maximum number of nodes to show. Default 50.

to_subgraph() KnowledgeGraph

Extract selected nodes into a new independent graph.

The new graph contains only selected nodes and the edges between them.

to_text() str

Deterministic, human-readable text projection of the whole graph.

Nodes grouped by type and sorted by id; edges sorted by endpoints — so the output is stable across insert order and across save/load. This is the canonical form behind the .kgl git textconv diff filter (also kglite export-text <file> from the CLI), making git diff of two .kgl snapshots show real content changes. Reserved provenance keys (updated_at/git_sha) are omitted so per-write metadata churn doesn’t swamp the diff.

Returns:

The text projection.

toc(file_path: str) dict[str, Any]

Get a table of contents for a file — all code entities defined in it.

Returns entities sorted by line number with a type summary.

Parameters:

file_path – Path of the file (the File node’s id/path).

Returns:

Dict with "file" (path), "entities" (list of entity dicts sorted by line_number, each with type, name, qualified_name, line_number, end_line, and optionally signature), and "summary" (dict of type name to count).

traverse(connection_type: str, level_index: int | None = None, direction: str | None = None, sort_target: str | list[tuple[str, bool]] | None = None, limit: int | None = None, new_level: bool | None = None, at: str | None = None, during: tuple[str, str] | None = None, temporal: bool | None = None, target_type: str | list[str] | None = None, where: dict[str, Any] | None = None, where_connection: dict[str, Any] | None = None) KnowledgeGraph

Traverse connections to discover related nodes by following graph edges.

For spatial, semantic, or clustering operations, use compare() instead.

Parameters:
  • connection_type – Edge type to follow (e.g. 'HAS_LICENSEE').

  • direction'outgoing', 'incoming', or None (both).

  • target_type – Filter targets to specific node type(s). Accepts a string or list of strings. Useful when a connection type connects to multiple node types.

  • where – Property conditions for target nodes — same operators as .where() ('>', 'contains', 'in', etc.).

  • where_connection – Property conditions for edge properties.

  • sort_target – Sort targets per source. Field name or [(field, ascending)] list.

  • limit – Max target nodes per source.

  • at – Temporal point-in-time filter (e.g. '2005').

  • during – Temporal range filter (e.g. ('2000', '2010')).

  • temporal – Override temporal filtering. False = disable.

  • level_index – Source level in the hierarchy (advanced).

  • new_level – Add targets as new hierarchy level. Default True.

Returns:

A new KnowledgeGraph with traversal results selected.

Examples:

# Follow edges
graph.select('Field').traverse('HAS_LICENSEE')

# Filter to specific target type
graph.select('Field').traverse('OF_FIELD', direction='incoming',
    target_type='ProductionProfile')

# Multiple target types
graph.select('Field').traverse('OF_FIELD', direction='incoming',
    target_type=['ProductionProfile', 'FieldReserves'])

# Filter target node properties
graph.select('Field').traverse('HAS_LICENSEE',
    where={'title': 'Equinor Energy AS'})

# Filter edge properties
graph.select('Person').traverse('RATED',
    where_connection={'score': {'>': 4}})

# Temporal filtering
graph.select('Field').traverse('HAS_LICENSEE', at='2005')
graph.select('Field').traverse('HAS_LICENSEE',
    during=('2000', '2010'))
union(other: KnowledgeGraph) KnowledgeGraph

Combine selections from both graphs (set union).

Returns:

A new KnowledgeGraph with nodes from either selection.

unique_values(property: str, group_by_parent: bool | None = None, level_index: int | None = None, indices: list[int] | None = None, store_as: str | None = None, max_length: int | None = None, keep_selection: bool | None = None) Any

Get unique values of a property, optionally storing results.

Parameters:
  • property – Property name to extract unique values from.

  • group_by_parent – Group by parent node. Default True.

  • level_index – Target level in the selection hierarchy.

  • indices – Specific node indices.

  • store_as – If set, stores comma-separated unique values as this property on parents.

  • max_length – Max string length when storing.

  • keep_selection – Preserve selection after store. Default False.

Returns:

Dict of unique values per parent, or — when store_as is set — this same graph (not a copy), so the call can be chained.

Note

store_as writes node properties, so on a graph opened with durable= it must be reached through the handle kglite.open() returned. A selection is itself a derived handle, so the whole select(...).unique_values(store_as=...) shape is refused there; use cypher() instead, which expresses the same write and is logged.

unlock_schema() KnowledgeGraph

Unlock the schema: allow any Cypher mutations without validation.

Also returns reads to their schemaless default: an unknown label, an absent property, or a comparison against a declared property type that no value can satisfy is reported as a non-fatal warning (on stderr, and on ResultView.warnings) instead of raising SchemaError.

Returns:

This same graph (not a copy), so the call can be chained.

unspill() None

Move mmap-backed property columns back to heap memory.

Useful after a spill (see set_memory_limit()), or after deleting nodes when you want the data back in RAM for faster access. Rebuilds every column from the live nodes with the memory limit temporarily suspended to prevent re-spilling, so rows left behind by deleted nodes are reclaimed in the same pass. The limit is restored afterwards.

Example:

graph.unspill()
info = graph.graph_info()
assert not info['columnar_is_mapped']
update(properties: dict[str, Any], keep_selection: bool | None = None) dict[str, Any]

Batch-update properties on all selected nodes.

Parameters:
  • properties – Mapping of property names to new values.

  • keep_selection – Preserve the current selection in the returned graph. Default False.

Returns:

Dict with graph (updated KnowledgeGraph), nodes_updated (int), and report_index (int).

Note

Not available on a graph opened with durable=. The write happens on the derived handle a selection produced, which shares the storage but not the write-ahead log, so it is refused rather than left unlogged — use cypher(), which expresses the same write and is logged. See kglite.open().

vacuum() dict[str, Any]

Compact the graph by removing tombstones left by node/edge deletions.

With StableDiGraph, deletions leave holes in the internal storage. Over time, this wastes memory and degrades iteration performance. vacuum() rebuilds the graph with contiguous indices, then rebuilds all indexes, and rebuilds the property columns to drop the rows that deleted nodes left behind.

The current selection is carried through the compaction: selected nodes that survived keep their place at their new indices, and nodes the deletes took are dropped from it. After a traversal, a group whose parent node was deleted is dropped whole — its children were selected because of that parent, so re-parenting them would invent a traversal that never happened.

Returns:

  • nodes_remapped: Number of nodes carried into the compacted graph

  • tombstones_removed: Number of free node slots reclaimed

  • edge_tombstones_removed: Number of free edge slots reclaimed. A relationship-only delete workload (MATCH ()-[r]->() DELETE r) produces these and no node tombstones at all

  • columnar_rebuilt: Whether the pass actually dropped property-column rows left behind by deleted nodes (False for a vacuum that found nothing to reclaim)

Return type:

dict with keys

Example:

info = graph.graph_info()
if info['fragmentation_ratio'] > 0.3:
    result = graph.vacuum()
    print(f"Reclaimed {result['tombstones_removed']} slots")
valid_at(date: str | None = None, date_from_field: str | None = None, date_to_field: str | None = None) KnowledgeGraph

Filter nodes valid at a specific date.

Keeps nodes where date_from <= date <= date_to.

If field names are not specified, auto-detects from set_temporal() config. If date is not specified, uses the date() context or today.

Parameters:
  • date – Date string (e.g. '2024-01-15'). Defaults to reference date or today.

  • date_from_field – Name of the start-date property. Auto-detected if temporal config exists.

  • date_to_field – Name of the end-date property. Auto-detected if temporal config exists.

Returns:

A new KnowledgeGraph with the filtered selection.

valid_during(start_date: str, end_date: str, date_from_field: str | None = None, date_to_field: str | None = None) KnowledgeGraph

Filter nodes whose validity period overlaps a date range.

If field names are not specified, auto-detects from set_temporal() config.

Parameters:
  • start_date – Start of the query range.

  • end_date – End of the query range.

  • date_from_field – Name of the start-date property. Auto-detected if temporal config exists.

  • date_to_field – Name of the end-date property. Auto-detected if temporal config exists.

Returns:

A new KnowledgeGraph with the filtered selection.

validate_schema(strict: bool | None = None) list[dict[str, Any]]

Validate the graph against the defined schema.

Parameters:

strict – Report undefined types in the graph. Default False.

Returns:

List of validation error dicts. Empty list means valid.

Vector similarity search within the current selection.

Searches for nodes most similar to the query vector among the currently selected nodes. Results are ordered by similarity (most similar first).

With no selection active the whole graph is searched — the same never-selected rule get_nodes() follows. A selection a query emptied (a filter that matched nothing) returns [], because that empty result is the answer to a question you asked.

Parameters:
  • text_column – Source text column name (e.g. 'summary').

  • query_vector – The query embedding vector.

  • top_k – Number of results to return (default 10).

  • metric'cosine', 'dot_product', 'euclidean', or 'poincare'. If omitted, uses the unique metric stored by embedding stores represented in the selection, or falls back to 'cosine' when none is stored. Selections spanning different stored metrics must pass this explicitly because their scores are not comparable.

  • to_df – If True, return a pandas DataFrame instead of list of dicts.

  • returning – Optional list of fields to project onto each hit. Omitted (default) → each hit carries id, title, type, score and all node properties. Given → each hit carries id + score plus only the named fields (a property name, or a structural field like title/type). Use it to trim the payload on wide nodes or ranking-only paths.

  • exact – Force an exact brute-force scan even when an HNSW index exists (see build_vector_index). Default False → a query covering most of a large indexed store uses the approximate index; set True for guaranteed-exact results. The index is used whenever a single node type carries text_column, whatever else the selection spans — including a whole-graph search on a multi-type graph. When two or more types carry the column, only a selection of one of them can use that type’s index; a selection spanning both is ranked by exact scan so neither type’s rows are dropped. Selections covering little of the store, and the 'poincare' metric, are always exact regardless.

Returns:

List of dicts (or a DataFrame if to_df=True). By default a hit has id, title, type, score, and all node properties — score always present, properties read live so a hit is identical before/after save() + reload (no follow-up MATCH ... WHERE id IN [...] join needed). With returning=[...] a hit has id + score + the requested fields only.

Raises:

ValueError – if no selected node type has an embedding store for text_column. Passing the store name ('summary_emb') where the column belongs, naming a column that was never embedded, or selecting only un-embedded types all land here — the error names the column that would have worked. A selection where some selected type has the store is a supported partial result and returns those rows. Use list_embeddings() to see what is embedded.

Example:

# full hit (default)
results = (graph
    .select('Article')
    .where({'category': 'politics'})
    .vector_search('summary', query_vec, top_k=10))

# ranking-only / slim payload
ranked = graph.select('Article').vector_search(
    'summary', query_vec, top_k=50, returning=['title'])
verify_unique_constraints() list[dict[str, Any]]

Re-scan stored data and report every unique-constraint violation currently present.

The on-demand counterpart of the rebuild load() runs. Enforcement covers the Cypher write path and the bulk loaders, but not the RDF / N-Triples loaders or the embedding-carry path — a graph filled through those can hold duplicates a declared UNIQUE (or primary_key) constraint forbids. This is the audit for that case:

graph.define_schema({"nodes": {"Entity": {"unique": ["isbn"]}}})
graph.load_ntriples("catalogue.nt")     # bypasses enforcement
for bad in graph.verify_unique_constraints():
    print(bad["node_type"], bad["properties"], bad["duplicate_tuples"])
Returns:

One dict per violated constraint (not per duplicate node), empty when the data is clean:

[{"constraint": "UNIQUE", "node_type": "Person",
  "properties": ["email"], "duplicate_tuples": 2,
  "sample": ["a@b.c"], "message": "..."}]

duplicate_tuples counts distinct colliding value tuples; sample is one of them, positional against properties. Constraints whose data is clean are not listed.

where(conditions: dict[str, Any], sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None) KnowledgeGraph

Filter the current selection by property conditions.

Conditions support exact match, comparison operators ('>', '<', '>=', '<='), 'in', 'is_null', 'is_not_null', 'contains', 'starts_with', 'ends_with', 'regex', '=~', and negated variants: 'not_contains', 'not_starts_with', 'not_ends_with', 'not_in', 'not_regex'.

'regex' and 'not_regex' search the value; '=~' matches the whole value, mirroring Cypher’s =~ operator — so {'role': {'=~': 'admin'}} does not select 'superadmin'. Wrap the pattern with .* to search with '=~'.

Several operators on one property are ANDed, so {'age': {'>=': 30, '<=': 40}} is the two-sided range it reads as — the same selection as chaining the two where() calls.

Example:

graph.select('Person').where({
    'age': {'>=': 25, '<=': 40},
    'city': 'Oslo',
    'name': {'regex': '^A.*'},
    'status': {'not_in': ['inactive', 'banned']},
})
Returns:

A new KnowledgeGraph with the filtered selection.

where_any(conditions: list[dict[str, Any]], sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None) KnowledgeGraph

Filter the current selection with OR logic across multiple condition sets.

Each dict in conditions is a set of AND conditions (same as where()). A node is kept if it matches any of the condition sets.

Parameters:
  • conditions – List of condition dicts. Must contain at least one.

  • sort – Optional sort spec.

  • limit – Limit the number of selected nodes.

Returns:

A new KnowledgeGraph with the filtered selection.

Example:

graph.select('Person').where_any([
    {'city': 'Oslo'},
    {'city': 'Bergen'},
])
Raises:

ValueError – If conditions is empty.

where_connected(connection_type: str, direction: str | None = None) KnowledgeGraph

Filter nodes that have at least one connection of the given type.

Keeps only nodes from the current selection that participate in edges of the specified type and direction.

Parameters:
  • connection_type – Edge type to check (e.g. 'KNOWS').

  • direction'outgoing', 'incoming', or 'any' (default).

Returns:

A new KnowledgeGraph with only connected nodes.

Raises:

ValueError – If direction is not one of the valid values.

where_orphans(include_orphans: bool | None = None, sort: str | list[tuple[str, bool]] | None = None, limit: int | None = None) KnowledgeGraph

Filter nodes based on whether they have connections.

Parameters:
  • include_orphans – If True, keep only orphan (disconnected) nodes. If False, keep only connected nodes. Default True.

  • sort – Optional sort spec.

  • limit – Limit the number of selected nodes.

Returns:

A new KnowledgeGraph with the filtered selection.

within_bounds(min_lat: float, max_lat: float, min_lon: float, max_lon: float, lat_field: str | None = None, lon_field: str | None = None) KnowledgeGraph

Filter nodes within a geographic bounding box.

Parameters:
  • min_lat – South bound latitude.

  • max_lat – North bound latitude.

  • min_lon – West bound longitude.

  • max_lon – East bound longitude.

  • lat_field – Latitude property name. Default from spatial config or 'latitude'.

  • lon_field – Longitude property name. Default from spatial config or 'longitude'.

Returns:

A new KnowledgeGraph with only nodes in the bounding box.

wkt_centroid(wkt_string: str | Any, as_shapely: bool = False) dict[str, float] | Any

Calculate the centroid of a WKT geometry string.

Parameters:
  • wkt_string – WKT geometry string or shapely geometry object.

  • as_shapely – If True, return a shapely.geometry.Point instead of a dict.

Returns:

Dict with latitude and longitude, or a shapely Point when as_shapely=True.

property last_mutation_stats: dict[str, int] | None

Mutation statistics from the last Cypher mutation query.

Returns None if no mutation has been executed yet. Keys: nodes_created, relationships_created, properties_set, nodes_deleted, relationships_deleted, properties_removed, indexes_added, indexes_removed, constraints_added, constraints_removed.

indexes_added / indexes_removed count KGLite index structures, mirroring Neo4j’s indexesAdded / indexesRemoved counters. CREATE RANGE INDEX reports 2 — a hash equality index plus a B-tree range index, which together serve what Neo4j’s single RANGE index does.

constraints_added / constraints_removed count constraints rather than the structures behind them, mirroring Neo4j’s constraintsAdded / constraintsRemoved. IS NODE KEY reports 1 even though KGLite serves it as uniqueness plus presence.

property node_types: list[str]

List of node type names present in the graph.

property schema_locked: bool

Whether the schema is currently locked.

property schema_version: int

Your own data-model revision, persisted with the graph.

This is your number, not kglite’s: the engine stores and returns it but never interprets it. It exists so a migration script can ask how far this graph has been migrated. 0 means unversioned, which is also what a graph saved before this field existed reports.

Distinct from graph_info()['format_version'], which is the .kgl on-disk layout version and belongs to the engine.

See the migrations guide.

property shape: tuple[int, int]

(node_count, edge_count) — pandas-style. O(1) via the storage backend; does not materialise per-type breakdowns. Use schema() or describe() for the full structure.

class kglite.ResultIter

Iterator for ResultView. Converts one row per step.

class kglite.ResultView

Lazy result container — data stays in Rust until accessed from Python.

Returned by cypher(), centrality methods, collect() (flat), and sample().

Data is only converted to Python objects when you actually access rows (via iteration, indexing, to_list(), or to_df()). This makes cypher() calls fast even for large result sets — the cost is deferred to when you consume the data.

Deferred results hold the graph open. To serve rows later, a deferred view keeps a reference to the graph it was queried from. Writing to that graph while such a view is still alive copies the whole graph (every node, edge, index and embedding) so the view keeps seeing the data it was built from. On a large graph that copy costs tens of milliseconds and it repeats every time the pattern recurs.

Very small results — roughly a couple of dozen values, so a single-entity lookup or a handful of rows — are converted up front and hold no graph reference, which covers the usual read-modify-write handler:

row = graph.cypher("MATCH (u:User {id: 1}) RETURN u.name, u.balance")
graph.cypher("MATCH (u:User {id: 1}) SET u.balance = 0")  # no copy

Anything larger stays deferred, so finish with it before writing — consume it (to_df(), to_list()) or let it go out of scope:

big = graph.cypher("MATCH (n:Event) RETURN n.ts").to_df()  # no view kept
graph.cypher("MATCH (n:Event) SET n.archived = true")      # no copy

The cutoff is kept deliberately small because every deferred-eligible query pays it, while only a result held across a write benefits.

Supports:
  • len(result) — row count (O(1), no conversion)

  • bool(result) — True if non-empty

  • result[i] — single row as dict (converts that row only)

  • for row in result — iterate rows as dicts (one at a time)

  • result.head(n) / result.tail(n) — first/last n rows as a new ResultView

  • result.to_list() — all rows as list[dict] (full conversion)

  • result.to_dicts() — alias for to_list() (polars/pandas naming)

  • result.one() — first row as dict, or None if empty

  • result.scalar() — first column of first row, or None if empty

  • result.column(name) — all values for one column as a list

  • result.to_df() — pandas DataFrame (full conversion)

  • result.columns — column names

  • result.stats — mutation stats (CREATE/SET/DELETE queries only)

Indexing is row-wise: result[i] takes an integer (or slice); result["col"] is not supported (the only valid string keys are the magic "columns" / "rows"). For a single column use the explicit accessor result.column("col") (returns a list), or result.scalar() / result.one() for the first cell / first row of small results.

column(name: str) list[Any]

All values for the named column, as a list (no DataFrame).

The explicit single-column accessor — row indexing (result[i]) stays integer-only:

names = g.cypher("MATCH (n:Person) RETURN n.name").column("n.name")
Raises:

KeyError – If name is not a column; the message lists the available column names.

head(n: int = 5) ResultView

Return a new ResultView with the first n rows (default 5).

one() dict[str, Any] | None

First row as a dict, or None if the result is empty.

Materializes only the first row (the same path as result[0]), so this stays cheap on large lazy results:

row = g.cypher("MATCH (n:Person {id: 1}) RETURN n.name").one()
# {'n.name': 'Alice'}  or  None
scalar() Any

First column of the first row, or None if the result is empty.

The first column is decided by the query’s RETURN order (the same order as columns); extra columns are ignored. Convenient for aggregate queries:

n = g.cypher("MATCH (n:Person) RETURN count(n)").scalar()  # int

Only the first cell of the first row is materialized.

tail(n: int = 5) ResultView

Return a new ResultView with the last n rows (default 5).

to_df() pandas.DataFrame

Convert to a pandas DataFrame.

to_dicts() list[dict[str, Any]]

Alias for to_list() — all rows as a list of dicts.

Provided for callers coming from polars (.to_dicts()) or pandas (.to_dict(orient="records")), where the row-wise dict accessor carries this name. Identical behaviour to to_list().

to_gdf(geometry_column: str = 'geometry', crs: str | None = None) Any

Convert to a GeoDataFrame with a geometry column parsed from WKT.

Materializes the data as a DataFrame, then converts the specified WKT string column into shapely geometries and returns a geopandas.GeoDataFrame.

Parameters:
  • geometry_column – Column containing WKT strings. Default 'geometry'.

  • crs – Coordinate reference system (e.g. 'EPSG:4326'), or None.

Returns:

A geopandas.GeoDataFrame.

Raises:

ImportError – If geopandas is not installed.

to_list() list[dict[str, Any]]

Convert all rows to a Python list of dicts (full materialization).

property columns: list[str]

Column names.

property diagnostics: dict[str, Any] | None

Lightweight execution diagnostics for this query.

Populated by the engine for every execution — reads, mutations, EXPLAIN, session and transaction queries alike — so a typo’d label in MATCH (n:typo) SET ... is as visible as one in a read. None only for views that did not come from a query (head() / tail() slices, DataFrame round-trips).

Returned dict keys:

  • elapsed_ms (int): wall-clock query duration in milliseconds.

  • timeout_ms (Optional[int]): the deadline that was in effect, or None when no deadline applied (memory graphs by default, or any call with timeout_ms=0). A deadline that fires raises CypherTimeoutError rather than returning a partial ResultView, so a returned result never carries a truncated-by- timeout row set.

  • row_limit (Optional[int]): the result-row retention cap that was in effect, echoed back, or None when the call set none. Present whether or not the cap bit, so “no cap” and “capped, and it fitted” stay distinguishable.

  • total_rows (Optional[int]): the exact number of rows the query produced before row_limit truncated it. Set only when truncation actually happened, so total_rows is not None is the truncation flag, and len(rv), total_rows is the “showing X of Y” pair. Exact on every execution path, never an estimate.

  • warnings (list[str]): non-fatal advisory warnings about the query — the shapes that silently return nothing useful instead of raising: a MATCH against an unknown node label or relationship type, a property read in WHERE / RETURN / WITH / ORDER BY that no node of that type has (an all-null column, or a filter that drops every row), and a relationship pattern whose direction matches no edges while the reverse direction has them. Carries a “did you mean?” hint where one is genuinely close, and execution-time advisories too, e.g. a procedure scoped to a relationship type the graph does not have, or a result cut short by row_limit. Empty for a clean query. The same signal interactive users see on stderr, exposed here for programmatic / agent callers. Shortcut: warnings, which is [] rather than an error on a view with no diagnostics.

Use this to tune timeout_ms or move toward anchored queries when your query repeatedly approaches the deadline, and surface warnings to catch silent-empty-result typos.

property profile: list[dict[str, Any]] | None

PROFILE execution statistics, or None for non-profiled queries.

Each dict has keys: clause (str), rows_in (int), rows_out (int), elapsed_us (int).

Only populated when the query is prefixed with PROFILE.

property stats: dict[str, int] | None

Mutation statistics, or None for read queries / non-cypher results.

property warnings: list[str]

The query’s non-fatal warnings — shortcut for diagnostics["warnings"].

Same list, and [] (never None) for a clean query and for a view that did not come from a query at all — a head() / tail() slice, a DataFrame round-trip — where diagnostics is None and subscripting it would raise. See diagnostics for what the warnings cover.

Recorded unconditionally: kglite.set_query_warning_policy() decides where warnings are announced, never whether they land here.

Example:

for w in graph.cypher(q).warnings:
    print(w)
class kglite.Session

A thread-safe, shareable concurrency handle over a graph.

Created via KnowledgeGraph.session(). Wraps the engine’s Mutex<Arc<DirGraph>> and exposes only &self methods, so it can be shared across a thread pool: concurrent cypher() reads take a momentary snapshot and run lock-free, while writes serialise behind the internal lock with copy-on-write + atomic swap. This is the supported way to serve one graph to many agent / request threads without the single-owner borrow conflict a live KnowledgeGraph raises.

The Session is an independent owner seeded from the source graph’s state; once either side mutates, copy-on-write forks them. Treat the Session as the live store after creating it.

cursor() KnowledgeGraph

Spawn a per-thread query cursor over a snapshot of this session.

Returns a KnowledgeGraph bound to a snapshot of the session’s current state, with a fresh fluent cursor. Where snapshot() hands out a read-only FrozenGraph (just cypher()), cursor() gives the full fluent surfaceselect / where / sort / traverse / to_df / collect / cypher / … — as an independent single-owner handle. Each call returns its own handle, so N threads can each take a cursor off the same shared Session and run fluent chains in parallel, lock-free.

The cursor observes the graph as of call time; mutating it is isolated via copy-on-write (it does not write back to the Session). Take a fresh cursor() to pick up later session writes.

cypher(query: str, to_df: bool = False, params: dict[str, Any] | None = None, timeout_ms: int | None = None, max_work_units: int | None = None, row_limit: int | None = None) Any

Run a read-only Cypher query against a momentary snapshot.

Takes a snapshot (an O(1) Arc clone), releases the Session lock, and runs the query GIL-free — so many threads can call cypher() on the same Session at once without blocking each other. Each call sees the graph as of the moment the snapshot was taken.

Read semantics match KnowledgeGraph.cypher(). A mutation query (CREATE / SET / DELETE / REMOVE / MERGE) raises ValueError — use execute() for writes.

row_limit caps the rows the call retains — the query still runs in full and only retention stops at the cap, so the rows kept are the first N of the uncapped answer (the genuine top-N under ORDER BY) and an explicit LIMIT m makes the effective cap min(m, row_limit). Truncation is never silent: it warns, and ResultView.diagnostics carries row_limit plus the exact pre-truncation total_rows.

execute(query: str, to_df: bool = False, params: dict[str, Any] | None = None, timeout_ms: int | None = None, max_work_units: int | None = None, row_limit: int | None = None, write_scope: list[str] | None = None, git_sha: str | None = None, modified_by: str | None = None) Any

Run a Cypher write against the shared graph, serialized.

write_scope (optional) restricts the statement’s mutations to the given node-type whitelist: every node write is judged by the node’s stored type, and a relationship write needs at least one endpoint’s stored type in the list — see KnowledgeGraph.cypher() for the exact perimeter. git_sha and modified_by are stamped on types that opt into auto_timestamp provenance.

Mutations (CREATE / SET / DELETE / REMOVE / MERGE) take the Session’s writer lock for the mutation, so concurrent execute() calls run one at a time and each sees the prior writer’s committed changes — no lost updates. Readers already holding snapshots keep seeing the pre-write graph. A new reader may briefly wait while a unique-owner write holds the core graph mutex.

A read-only query passed here is fast-pathed to the read path (no working-copy materialisation), so mixed traffic can route through execute() safely. Returns the query result (rows for ... RETURN, otherwise mutation stats).

row_limit caps the rows the call retains — the query still runs in full and only retention stops at the cap, so the rows kept are the first N of the uncapped answer (the genuine top-N under ORDER BY) and an explicit LIMIT m makes the effective cap min(m, row_limit). Truncation is never silent: it warns, and ResultView.diagnostics carries row_limit plus the exact pre-truncation total_rows. It bounds a mutation’s RETURN rows the same way, and never the writes themselves — every write still happens and is still counted.

node_count() int

Number of nodes in the current snapshot.

snapshot() FrozenGraph

Take an immutable FrozenGraph snapshot of the current state.

An O(1) Arc clone that stays stable even if the Session is later written to (copy-on-write forks the writer). Use it to hold a consistent multi-query view or hand a fixed read snapshot to readers.

version() int

Monotonic version of the current graph, bumped by each committed write. Useful for cheap “did anything change?” checks.

property node_types: list[str]

Node type names present in the current snapshot.

class kglite.Spatial

Spatial compute expression builders for add_properties().

Each method returns the string keyword that add_properties() understands for spatial computations between leaf and ancestor nodes.

Example:

from kglite import Spatial

graph.select('Well').compare('Structure', 'contains') \
    .add_properties({
        'Well': {'dist': Spatial.distance(), 'a': Spatial.area()}
    })

Equivalent to:

graph.select('Well').compare('Structure', 'contains') \
    .add_properties({
        'Well': {'dist': 'distance', 'a': 'area'}
    })
static area() str

Area of ancestor geometry (square meters) — returns 'area'.

static centroid_lat() str

Latitude of ancestor geometry centroid — returns 'centroid_lat'.

static centroid_lon() str

Longitude of ancestor geometry centroid — returns 'centroid_lon'.

static distance() str

Geodesic distance between leaf and ancestor (meters) — returns 'distance'.

static perimeter() str

Perimeter of ancestor geometry (meters) — returns 'perimeter'.

class kglite.Transaction

An isolated transaction on a KnowledgeGraph.

Created via KnowledgeGraph.begin() (read-write) or KnowledgeGraph.begin_read() (read-only).

Read-write transactions:
  • Snapshot isolation: begin() is O(1); the first mutation creates a working fork. Memory/mapped modes clone then, while disk mode remaps immutable bases and copies only mutation overlays.

  • Write isolation: mutations modify only the working copy.

  • Optimistic concurrency control: commit() checks that the graph version hasn’t changed since begin(). If another transaction committed in between, a typed KgError is raised.

  • Commit: replaces the original graph’s data atomically.

Read-only transactions (begin_read()):
  • O(1) creation cost (Arc reference, no deep clone).

  • Mutations are rejected with RuntimeError.

  • commit() is a no-op; rollback() releases the snapshot.

commit() None

Commit the transaction — apply all changes to the original graph.

For read-only transactions, this is a no-op. After commit, the transaction cannot be used again.

Raises:

KgError – If the graph was modified since begin() (OCC conflict).

cypher(query: str, params: dict[str, Any] | None = None, to_df: bool = False, timeout_ms: int | None = None, max_work_units: int | None = None, row_limit: int | None = None, write_scope: list[str] | None = None, git_sha: str | None = None, modified_by: str | None = None) ResultView | pandas.DataFrame

Execute a Cypher query within this transaction.

Same interface as KnowledgeGraph.cypher() but operates on the transaction’s working copy (or Arc snapshot for read-only).

Parameters:
  • query – Cypher query string. Supports EXPLAIN and PROFILE prefixes.

  • params – Optional query parameters.

  • to_df – If True, return a pandas DataFrame.

  • write_scope – Role-scoped write whitelist — see KnowledgeGraph.cypher().

  • git_sha – Commit SHA stamped on opted-in types.

  • modified_by – Actor id stamped on opted-in types.

  • timeout_ms – Per-query timeout in milliseconds (merged with transaction deadline).

  • max_work_units – Work budget for the statement — intermediate rows, retained collection items and scan work, not a result-row cap. Exceeding it raises an error and rolls back the statement.

rollback() None

Roll back the transaction — discard all changes.

After rollback, the transaction cannot be used again.

property is_read_only: bool

Whether this is a read-only transaction.

kglite.attach_rows(graph: KnowledgeGraph, parent_type: str, parent_id: Any, data: Any, *, row_type: str, edge_type: str, key: str) int

Attach a DataFrame to a parent node as row NODES plus edges.

The normalized alternative to KnowledgeGraph.set_table_property: each DataFrame row becomes an independently addressable node of row_type (id "<parent_id>:<key value>"), linked from the parent by an edge_type edge. Prefer this when rows need their own edges, indexes, or independent updates at scale; prefer the embedded table for small always-read-together payloads. See the “embedded table vs row nodes” recipe in the inline-records guide.

Returns:

Number of row nodes attached.

Raises:

ValueError – Unknown parent, missing key column, or duplicate keys.

kglite.check_file_freshness(graph: KnowledgeGraph, *, node_type: str | None = None, path_property: str = 'file_path', mtime_property: str = 'file_mtime', hash_property: str | None = 'content_hash', base_dir: str | None = None) list[dict[str, Any]]

Read-only drift check (binding-layer): snapshot each node’s path_property and compare to the stored hash_property. When hash_property is None, compare the nanosecond mtime_property instead. Returns the drifted nodes as [{"id", "path", "status"}] (status "missing" or "changed"); matching nodes are omitted. Never mutates the graph.

kglite.cypher_pass_names() list[str]

Names of every Cypher optimizer pass, in execution order.

Pass names are stable identifiers for the diagnostic KnowledgeGraph.cypher(disabled_passes=[...]) kwarg and for bisection scripts. Use this list as the source of truth — names not in it will be rejected by cypher(...).

Returns:

List of pass names in pipeline order.

Example:

passes = kglite.cypher_pass_names()
# Bisect: disable each pass in turn, find the divergence.
for name in passes:
    naive = g.cypher(query, disabled_passes=[name]).to_list()
    if naive != optimized:
        print(f"Divergence introduced by `{name}`")
        break
kglite.estimate_load_memory(path: str) dict[str, int]

Estimate what loading the .kgl at path would cost, without loading it.

Reads only the metadata block at the head of the file — 0.01%-0.35% of it on the measured corpora — so the answer costs one short read and decompresses nothing. This is the same estimate max_load_mb refuses on, so a caller can decide for itself instead of setting a ceiling.

Parameters:

path – Path to a portable .kgl file. A disk-mode graph directory raises ArgumentError: it has no metadata head, and it never rebuilds its indexes at load, so these terms would not describe it.

Returns:

  • index_rebuild_bytes - modelled from the file’s own index declarations and row counts: what rebuilding them costs. This is the term defer_index_rebuild=True removes.

  • section_heap_bytes - heuristic: the decoded graph itself.

  • transient_peak_bytes - heuristic: the largest single decompression buffer held live during the load.

  • total_settled_bytes - sections + index rebuild.

  • total_peak_bytes - settled + transient; the number a ceiling compares, because a process dies at its peak.

  • node_rows - node rows the file declares.

  • declared_indexes - index declarations the index term summed.

Return type:

A dict of byte counts

Raises:

Read this before acting on the numbers. They estimate physical footprint — the metric an OS memory killer judges — not RSS, which overstates it by up to 3.6x on this path and swings 2.3x with the allocator. They cover the load-settled plateau, not the further ~30% a first point lookup adds by building per-type id indexes. And they are an estimate: the section term measured 0.56x-1.30x of actual across three corpora, and the peak is deliberately conservative (1.35x-1.46x of measured on the two fixtures with peak measurements).

Example:

est = kglite.estimate_load_memory("graph.kgl")
if est["total_peak_bytes"] > budget:
    # The index rebuild is usually the term worth dropping.
    g = kglite.load("graph.kgl", defer_index_rebuild=True)
kglite.from_blueprint(blueprint_path: str | pathlib.Path, *, verbose: bool = False, save: bool | None = None, lock_schema: bool = False, storage: str = 'default', path: str | None = None, frames: Mapping[str, Any] | None = None) KnowledgeGraph

Build a KnowledgeGraph from a JSON blueprint and its declared inputs.

The blueprint JSON describes all node types, properties, connections, timeseries, and data sources. Input paths in the blueprint — files entries and csv shorthands alike — are resolved relative to settings.root. A files section declares each input once by name ({"path": ..., "format": ...}) for specs to reference with file; "csv": "x.csv" is shorthand for one such entry. The formats it reads are csv, delimited (any separator, with quote, header, columns, skip_lines, comment_prefix, line_suffix, encoding and prefix_strip), xlsx (one worksheet, with sheet, header_row and unpivot for wide matrices) and frame (below); each brings its own keys, and an unknown one is an error listing these.

Diagnostic output:

  • Progress (per-type counts, summary line) prints to stdout when verbose=True. Edge counts come from the live graph, not the input-row tally — so they match MATCH ()-[r]->() RETURN count(r). When dedupe collapses input rows, the line is annotated as [T]: N edges (M input rows, K deduped).

    What dedupe collapses. The first source to load a connection type writes one edge per input row, so rows sharing a source and target but differing in their properties stay separate parallel edges. A later source feeding that same edge type (a second node spec’s FK edge, or a second junction CSV) merges instead: a row whose endpoints already carry an edge of the type writes its properties onto that edge rather than adding another, and those are the rows the K deduped count reports. Reading a CSV in chunks (KGLITE_BLUEPRINT_JUNCTION_CHUNK_SIZE, KGLITE_BLUEPRINT_NODE_CHUNK_SIZE) only bounds peak memory — it never changes which edges a build produces.

  • Warnings (stray keys, missed list cells, ontology findings, an input type the blueprint cannot hold, …) are emitted as Python UserWarning objects, one per entry in the build report, whatever verbose is — verbose governs the progress summary, not the warnings. By default they reach stderr, and every standard mechanism (warnings.simplefilter, warnings.catch_warnings, logging.captureWarnings) routes them.

  • Errors — a spec the build survived but could not fully load (a bad rename, a property column the input does not have) — are printed to stderr directly. The graph is still returned.

To capture warnings to a file, use the standard Python pattern:

import logging
logging.captureWarnings(True)
logging.basicConfig(
    filename="blueprint.log",
    level=logging.WARNING,
    format="%(asctime)s %(levelname)s %(message)s",
)
graph = kglite.from_blueprint("blueprint.json", verbose=True)
# All warnings now in blueprint.log instead of stderr.

Frames — in-memory tables instead of files. A files entry declaring {"format": "frame"} takes no path; its rows come from frames["<entry name>"]. A frame is consumed exactly like a read file — same specs, filters, chunked junction loading, dedupe regime and warnings — so the same blueprint builds the same graph from a CSV or from a frame of the same data:

kglite.from_blueprint("bp.json", frames={"people": people_df})

Four things about that equivalence are worth knowing:

  • Types. Each column is coerced to the type the blueprint declares for it, through the same text path a CSV takes. Where the blueprint declares nothing, the frame’s own dtype is kept — a float column of whole numbers stays a float. Two dtypes have no blueprint keyword: a datetime-with-time-of-day and a dict column land as text, with one warning per column naming them. A pandas integer column holding nulls is a float64 column, so declare "int" for it to come back as an integer property.

  • Empty strings are nulls, exactly as they are in a CSV. A cell holding "" becomes a missing property, not an empty one.

  • Files stream; frames do not. A frame was already materialised before the build began and is stringified whole, so peak memory is roughly twice the frame set. Chunk-size knobs still bound what the loader holds, not what the frame does.

  • Other frame libraries work through .to_pandas(): any value that is not a pandas DataFrame but offers that method (polars, pyarrow) is converted with it.

A frame declared in files but absent from frames= — or passed in frames= without being declared — raises ValueError naming it. A compute op over a frame-fed spec is refused: compute reshapes CSV files on disk, outside the input registry.

Where the graph is saved. A build has a save destination when the blueprint declares settings.output (or output_path + output_file), or when storage="disk" was given a path — in disk mode the directory is the graph, and the build alone leaves it unpublished, so that directory is what saving means. save=None (the default) saves when a destination exists and skips otherwise; save=True demands one and raises if there is none, so an explicit request to persist never silently does nothing; save=False never saves.

Parameters:
  • blueprint_path – Path to the blueprint JSON file.

  • verbose – If True, print progress information during loading.

  • saveNone (default) saves if a destination exists, True requires one, False never saves. See above.

  • lock_schema – If True, lock the schema after loading. Cypher mutations will be validated against the blueprint’s types and properties.

  • storage"default" (in-memory), "mapped" (mmap columns), or "disk" (CSR + mmap). Disk requires path.

  • path – Directory for disk storage (only with storage="disk").

  • frames – In-memory tables keyed by the name of a files entry declaring {"format": "frame"}. See Frames above.

Returns:

A new KnowledgeGraph populated from the blueprint.

Raises:
  • FileNotFoundError – If the blueprint file is missing.

  • ValueError – If the blueprint JSON is malformed, save=True was passed with no destination to write to, or a declared frame was not supplied (or a supplied one was not declared).

Example:

import kglite

graph = kglite.from_blueprint("blueprint.json", verbose=True)

# Disk mode: the directory is the graph, and it is published here.
g = kglite.from_blueprint("blueprint.json", storage="disk", path="graph/")
reopened = kglite.load("graph/")
kglite.from_bytes(data: bytes, *, storage: str | None = None, defer_index_rebuild: bool | None = None, max_load_mb: int | None = None) KnowledgeGraph

Load an in-memory graph from a .kgl byte buffer.

The in-memory counterpart of load() — deserialises the bytes produced by KnowledgeGraph.to_bytes(). The returned graph has no remembered path (it didn’t come from a file), so a bare save() will require an explicit path.

Parameters:
Returns:

A new KnowledgeGraph with the loaded data.

Raises:
  • LoadMemoryLimitError – The estimated load exceeds max_load_mb (or KGLITE_MAX_LOAD_MB). Nothing was decompressed.

  • FileFormatError – If data is not a valid .kgl buffer (bad magic, truncated, or an incompatible/older format) — a typed kglite.KgError subclass, distinct from a successful load of an empty graph. (kglite.load raises the same on a corrupt file, or FileError when the path is missing.)

kglite.from_networkx(nx_graph: Any, *, default_node_type: str = 'Node', default_edge_type: str = 'RELATED') KnowledgeGraph

Build a KnowledgeGraph from a networkx graph.

Accepts Graph / DiGraph / MultiGraph / MultiDiGraph. Undirected edges become a single directed edge each. Nodes carrying a node_type attribute (as produced by KnowledgeGraph.to_networkx()) are grouped by that type, the networkx node key becomes the node id, and a title attribute becomes the title (otherwise the id is used). Edges carrying a connection_type attribute (or, for a MultiDiGraph, the edge key) use it as the edge type. Plain networkx graphs get default_node_type / default_edge_type.

A graph exported with to_networkx(node_key="type_id") round-trips without any extra argument: its (node_type, id) tuple keys are detected — each key’s first element repeats the node’s own node_type attribute, which a foreign tuple-labelled graph does not carry — and the id plus both endpoint types are read straight off the keys. Detection is per graph and all-or-nothing; mixing tuple keys with plain ones raises.

Node keys must be storable as ids (integers or strings). A key that is not — a foreign tuple label such as nx.grid_2d_graph coordinates, a fractional float — raises before anything is loaded, rather than being dropped row by row into a silently smaller graph. One node type’s keys must also all be the same shape: each type is loaded as a single id column, so mixing integer and string ids within one type would store them all as text and leave the edge endpoints that kept their original type unmatched. That mix raises too. Different node types may use different id shapes — they never share a column.

Requires the networkx extra: pip install "kglite[networkx]".

Parameters:
  • nx_graph – A networkx graph instance.

  • default_node_type – Node type for nodes lacking a node_type attr.

  • default_edge_type – Edge type for edges lacking a connection_type attr.

Returns:

A new KnowledgeGraph.

Raises:

ArgumentError – A node key cannot be stored as an id, one node type’s ids mix integer and string shapes, or the graph mixes (node_type, id) export keys with other key shapes.

Example:

import kglite, networkx as nx

nxg = nx.karate_club_graph()
g = kglite.from_networkx(nxg)
kglite.from_records(spec: dict | str, *, save: str | None = None, lock_schema: bool = False, storage: str = 'default', path: str | None = None, on_missing_endpoint: str | None = None) KnowledgeGraph

Build a KnowledgeGraph from an inline JSON records spec.

A JSON-native sibling to from_blueprint(): instead of pointing at CSV files on disk, the spec carries node and connection records inline — the natural ingestion path for agent-authored graphs. Column types are inferred from the record values, so a JSON array becomes a native list property ('x' IN n.tags membership, UNWIND n.tags). Missing edge endpoints can be vivified as provisional stubs, dropped, or rejected atomically.

Spec shape (records are arrays of flat JSON objects):

{
  "nodes": [
    {"type": "Person", "id_field": "id", "title_field": "name",
     "conflict_handling": "update",
     "records": [{"id": 1, "name": "Alice", "aliases": ["a", "b"]}]}
  ],
  "connections": [
    {"type": "KNOWS", "source_type": "Person", "source_id_field": "from",
     "target_type": "Person", "target_id_field": "to",
     "records": [{"from": 1, "to": 2, "since": 2020}]}
  ]
}

Those key sets are closed. An unknown key — at the top level, or in a node or connection spec — raises rather than being ignored, with a “did you mean” suggestion when one is close: a spec written with "relationships" would otherwise build zero relationships and report success.

Parameters:
  • spec – The records spec as a dict or a JSON string.

  • save – If set, save the built graph to this .kgl path. With storage="disk" pass path here too — the disk build leaves an unpublished working directory until something calls save(), and kglite.load() rejects that directory.

  • lock_schema – If True, lock the schema after building.

  • storage"default" (in-memory), "mapped", or "disk".

  • path – Directory for disk storage (only with storage="disk").

  • on_missing_endpoint"vivify" creates provisional stub nodes, "drop" omits affected edges, and "error" rejects the complete build without publishing partial changes. None (the default) takes the spec’s own on_missing_endpoint key, which itself defaults to "vivify"; passing the argument overrides whatever the spec carries.

Returns:

A new KnowledgeGraph populated from the records.

Raises:

ValueError – If the spec JSON is malformed, a required field is missing, or any key is not one this loader reads.

kglite.get_query_warning_policy() str

The query-warning policy currently in effect.

One of "stderr" (the default), "silent" or "pywarn" — see set_query_warning_policy().

kglite.graphgen(scale: str = 'medium', *, persons: int | None = None, seed: int = 1234, knows_per: int = 8, degree_dist: str = 'zipf', zipf_exp: float = 1.6, out: str | None = None) KnowledgeGraph | dict[str, Any]

Generate a synthetic org/social knowledge graph (bundled generator).

Seed-deterministic Person/Company/Project/Skill/City nodes + KNOWS/WORKS_AT/CONTRIBUTES_TO/HAS_SKILL/OWNS/DEPENDS_ON/LOCATED_IN edges — for demos, tests, and benchmarks.

  • out=None (default): build and return a KnowledgeGraph (best for small/medium). Needs ``pandas`` — the staged CSVs are loaded through DataFrames; a missing pandas raises ImportError with an install hint.

  • out=DIR: stream one CSV per type + manifest.json into DIR in bounded memory (millions of nodes at flat RAM); returns {'nodes', 'edges', 'out'}. Pure Rust — no extra packages needed.

Parameters:
  • scaletiny | small | medium (default) | large | huge | xhuge — sets the Person count. Ignored if persons is given.

  • persons – Exact Person count (overrides scale).

  • seed – Deterministic seed.

  • knows_per – Average KNOWS out-degree per person.

  • degree_dist'zipf' (hubs; default) or 'uniform'.

  • zipf_exp – Zipf skew exponent (>1 → stronger hubs).

  • out – Output directory for streaming mode, or None to return a graph.

kglite.load(path: str, *, storage: str | None = None, defer_index_rebuild: bool | None = None, max_load_mb: int | None = None) KnowledgeGraph

Load a graph from a binary file previously saved with save().

Parameters:
  • path – Path to the .kgl file.

  • storage"memory" or "mapped" to override the mode the checkpoint recorded; None (default) honours it. Resolved before any section is decompressed, so an unserveable request costs only the metadata read. "disk" raises ArgumentError — a .kgl is a file and a disk graph is a directory.

  • defer_index_rebuild – Record the file’s declared indexes instead of building them at load. None (default) leaves the process default in charge — off, unless KGLITE_DEFER_INDEX_REBUILD is set; True/False decide for this call.

  • max_load_mb – Refuse the load if it is estimated to peak above this many megabytes — not bytes. Checked from the file’s metadata head, before anything is decompressed. None (default) leaves the process default in charge — no ceiling, unless KGLITE_MAX_LOAD_MB sets one, which this outranks.

Returns:

A new KnowledgeGraph with the loaded data.

Raises:

``storage`` is not a memory lever. For a loaded .kgl, mapped and memory cost the same resident memory — measured within 0.3 MB of each other on every fixture, because columns of 256 KB or more spill to a temp directory and are mmap’d on both paths. What the mode decides is the backend the graph continues in: the spill policy its later writes follow, and the mode its next save() records.

``defer_index_rebuild`` is the memory lever. Rebuilding the declared indexes is the largest single term in what an index-bearing graph costs to hold. On a 500k-row fixture declaring four index families, deferring it took settled footprint from 150 MB to 86 MB (-42.8%) and the load from 351 ms to 157 ms. The build is moved, not avoided, and both halves of the price are measured: an indexed lookup runs as a scan for as long as the graph stays read-only (12 ms → 20 ms at that size), and the first write pays the whole build (+193 ms, once; later writes are unchanged). So it is a win for a read-mostly consumer that does not use the indexes, a wash for a write-then-work one, and a loss for a read-only consumer that does issue indexed lookups. Correctness is identical either way — a deferred graph answers every query the same, its declarations survive a save() byte-for-byte, and KnowledgeGraph.list_indexes() / SHOW INDEXES list them with state="DEFERRED".

Example:

# A viewer that never writes and never uses the indexes.
g = kglite.load("graph.kgl", defer_index_rebuild=True)

The returned graph remembers path, so a later bare save() writes back to it. See open() for load-or-create semantics, or open_session() to load directly as a thread-safe Session.

kglite.load_rdf(path: str, *, languages: list[str] | None = None, label_predicates: list[str] | None = None, keep_full_iris: bool = False, default_type: str | None = None, max_triples: int | None = None) KnowledgeGraph

Load an RDF file into a fresh in-memory graph.

Dispatches on the file extension: .ttl → Turtle, .nt → N-Triples, .nq → N-Quads, .trig → TriG (parsed via the oxttl family).

The RDF→property-graph fold: object literals become typed node properties (xsd:integer → int, xsd:double → float, xsd:boolean → bool, xsd:date → date, xsd:dateTime → datetime, GeoSPARQL POINT → point; a repeated predicate becomes a list); resource objects become edges; and rdf:type sets the node label (first wins — any extra types are kept in an rdf_types list property). Predicate and type IRIs are CURIE-compacted with a __ separator (e.g. foaf__knows) using the document’s own @prefix declarations plus a well-known prefix table — so they are valid Cypher identifiers (MATCH (:foaf__Person)-[:foaf__knows]->()). Each node keeps its full subject IRI in a uri property, and n.id is a dense integer.

Parameters:
  • path – Path to the RDF file. Extension selects the parser.

  • languages – If given, keep only language-tagged literals whose tag is in this set (untagged literals are always kept).

  • label_predicates – IRIs whose literal object sets the node title. Defaults to ["http://www.w3.org/2000/01/rdf-schema#label"].

  • keep_full_iris – Keep full predicate/type IRIs instead of CURIE- compacting them.

  • default_type – Node type for subjects without an rdf:type. Defaults to "Resource".

  • max_triples – Stop after this many triples.

Returns:

A new in-memory KnowledgeGraph.

Raises:
  • FileNotFoundError – The file does not exist.

  • ValueError – Unsupported extension or a parse error.

Note

Builds an in-memory graph; mapped/disk backends are not supported. For Wikidata-scale N-Triples dumps use KnowledgeGraph.load_ntriples() instead.

kglite.open(path: str, *, storage: str | None = None, durable: bool | Literal['full', 'normal', 'off'] | None = None, lock: bool = True) KnowledgeGraph

Open a graph at path — load it if it exists, create a fresh one if it doesn’t (load-or-create). The embedded-database lifecycle entry point.

The returned graph remembers path: a later bare save() (or the context-manager auto-save-on-close) writes back to it without re-specifying the target:

with kglite.open("app.kgl") as g:
    g.cypher("CREATE (:Person {name: 'Alice'})")
# auto-saved on clean exit
Parameters:
  • path – Path to a .kgl file or a disk-mode directory. Loaded if it exists, otherwise created on first save() (or immediately, for storage="disk", which materializes the directory).

  • storage

    Storage mode ("memory" / "mapped" / "disk") — the backend for a graph being created, and a request for one being opened.

    An existing path opens in the mode its checkpoint recorded: a .kgl saved by a mapped graph comes back mapped, one saved by a memory graph, or saved by a kglite old enough not to record the mode at all) comes back "memory", and a disk graph is a directory and always opens "disk". So open(path, storage="mapped") now yields a genuinely mapped graph on every call, not only the one that creates the file.

    Passing storage= for a different mode converts: memory ⇄ mapped switches the backend on the loaded graph — same nodes, edges and rows; it changes where property columns live from the next consolidation onward — and the next save() records the new mode. The two disk directions have no in-place conversion (a disk graph is a directory, not a file), so they raise kglite.ArgumentError naming the alternative (enable_disk_mode()) rather than being ignored — a silently-downgraded mode is indistinguishable from success. Omit storage= to accept whatever the file provides.

    Note the durability asymmetry between the two ways to build a graph, which is structural rather than incidental: open() attaches a WAL sidecar next to path and defaults to durable="full", whereas KnowledgeGraph takes no durable argument and is never durable — it produces a detached graph with no source_path, so there is nowhere for a log to live. KnowledgeGraph(storage="mapped") is mapped-and-unlogged; open(new_path, storage="mapped") is mapped-and-logged.

  • durable

    Write-ahead logging — on by default. Each committed mutation is appended to a <path>-wal sidecar, and on open any WAL frames are replayed onto the loaded checkpoint to recover work committed since the last save(). save() writes a full checkpoint and truncates the log.

    The level names what a committed mutation survives, using SQLite’s synchronous vocabulary. These are guarantees, not syscalls — the syscall differs by platform, the guarantee does not:

    • "full" (also spelled True, and the default) — survives power loss. One barrier per commit. On macOS that barrier is F_FULLFSYNC, a stronger guarantee than SQLite’s own default provides.

    • "normal" — survives the process dying: SIGKILL, an unhandled panic, an OOM-kill. The frame is in the kernel’s page cache before the call returns, and the page cache outlives the process. An OS crash or power loss loses commits made since the last save(). No barrier per commit. Call sync to take an explicit power-safe point without republishing the whole graph.

    • "off" (also spelled False) — no log. The graph still remembers path for save(); a crash loses everything since the last checkpoint.

    • None (default) — "full", except on storage="disk" where it resolves to "off" rather than raising.

    Recovery happens at every level, including ``”off”``. Opening a path is a decision about that path’s data, not only about how future writes are logged, so an "off" open over a <path>-wal sidecar that still holds commits the checkpoint does not contain raises ValueError instead of returning a graph that is missing them — the next save() would truncate the log and destroy those commits. Reopen at "full" or "normal" to replay them, or move the sidecar aside to discard them deliberately. Sidecar frames the checkpoint already contains (the residue of a crash between a save() and its truncation) are harmless and open fine.

    The levels are not uniform across storage modes: storage="disk" supports only "off", and both "full" and "normal" raise ValueError there. A disk graph commits by publishing an immutable generation rather than by logging a write, so the blocker is structural, not a matter of barrier strength.

    Which to pick: "full" when losing an acknowledged write is unacceptable; "normal" when you want a crash-safe application and treat power loss as a restore-from-backup event; "off" for bulk loading and graphs rebuildable from source data.

  • lock – Single-writer guard — on by default. Opening takes an exclusive advisory lock on a <path>.lock sidecar and holds it until close() / with-block exit, so a second process that opens the same path raises instead of silently overwriting your work (see the note below). Pass False only when something else already guarantees a single writer — an external supervisor, or a process you have confined to reads. lock=False opts out of taking the lease, not out of the consequences of ignoring one.

Returns:

A KnowledgeGraph bound to path.

Raises:

KgError – If another process already holds the write lease for path. The message names the holding pid and when it acquired, e.g. app.kgl is open for writing by pid 4711 (since ...).

Note

One process writes at a time. save() republishes the whole graph, so two processes that open the same path independently both build a complete snapshot and the last one to save wins — silently discarding everything the other did. That is the single most likely accident when deploying an embedded database: a gunicorn worker pool, a cron job overlapping a request, or a stale process left running. The lease turns it into an error at open() rather than data loss at save().

Readers are never blocked. load() and open_session() take no lease, so any number of processes can read a graph while one writes; they observe the last published snapshot. Only open() — the write-back entry point — claims ownership.

A load-derived graph is outside the guarantee. The lease binds the entry points that claim a path — open(), the CLI’s eager saves, the Bolt server, the MCP server — and save() itself takes none, so a graph obtained from load() can save straight over a path a holder is mid-write on. Open a path you may write to.

A viewer should not use these defaults. open() defaults to a writer’s posture: it attaches a WAL sidecar and takes the lease, so it writes <path>-wal and <path>.lock-owner next to a file you only meant to read, and the log’s buffers add to the process footprint (roughly +110 MB on a 134 MB graph, measured). Read with load() / open_session(), which take neither — or, if you need open()’s save-back binding for a read-mostly handle, pass durable="off", lock=False.

A crash releases the lease. The lock belongs to the operating system, not to the sidecar file, so a writer killed with SIGKILL (or lost to a power cut) frees it immediately. Two small sidecars are left behind and are harmless: <path>.lock (the lock itself, always empty) and <path>.lock-owner (the pid/timestamp used to name a holder in the error above). Deleting either does not release a live lock, and does nothing useful for a dead one.

Note

What durability costs. Under "full", crash safety is bought with one barrier per committed mutation, so a write returns only once the log entry is on physical storage. That makes each individual write substantially more expensive than an unlogged one — the cost is dominated by device latency, not by graph size, so it is most visible in loops of many small writes and least visible for a few large ones. Reads are completely unaffected: the capture layer forwards them with no overhead.

"normal" is where that cost goes away while the log stays: it writes the same frame and skips only the barrier, so a write costs roughly what an unlogged write costs, and a process crash still loses nothing. That is the level to reach for when the failure you are actually defending against is a crashing process rather than a power cut.

Otherwise: prefer durable=False for bulk loading and for graphs rebuildable from source data, then save() once at the end; batch many small mutations into one statement (or one begin() transaction, which commits as a single log entry) when you need both throughput and the strongest guarantee.

Note

A ``with`` block is not a transaction. Each mutation commits as it runs, so an exception inside the block does not undo mutations that already returned — they are recovered on the next open(). What the clean/failed exit controls is whether a checkpoint is written. Use begin() when you want discard-on-error, or durable=False for snapshot-only semantics.

Note

Mutations that the log cannot express are checkpoint-only, and are persisted by save() rather than by the log: schema and config metadata, user-created indexes, embeddings, and timeseries. A Session also refuses write queries on a durable graph, because its writes land on a working copy that neither the log nor save() can reach — use cypher() or begin().

Note

The log belongs to the handle this function returns. Fluent methods (select, where, traverse, expand, the set operations, date) return a derived graph that shares the storage but cannot share the log, and forks away from the original on its first write. On a durable graph, writing through such a handle — including save() and sync() on it — raises ValueError rather than silently reaching neither the log nor the original. That fences off the selection-based fluent mutations (add_properties, create_connections, calculate(store_as=...), count(store_as=...), collect_children(store_as=...), unique_values(store_as=...), set_property), because a selection is itself a derived handle; cypher() expresses all of them and every statement it runs is logged. copy() and to_subgraph() build independent graphs rather than views, so they are unaffected and write freely.

kglite.open_session(path: str, *, storage: str | None = None, defer_index_rebuild: bool | None = None, max_load_mb: int | None = None) Session

Load a saved graph at path directly as a thread-safe Session.

The one-call shortcut for the concurrent-serving case — equivalent to kglite.load(path).session(). Share the returned Session across a thread pool: Session.cypher() reads run lock-free, Session.execute() writes serialize and compose, and Session.cursor() hands each thread its own per-thread fluent handle. The file must already exist.

For embedding-backed semantic search over a query string, register the model on the KnowledgeGraph first:

g = kglite.load(path)
g.set_embedder(model)
s = g.session()
Parameters:
  • path – Path to the .kgl file.

  • storage – As on load().

  • defer_index_rebuild – As on load(). Well suited to this entry point: a session that only reads never pays the deferred build.

  • max_load_mb – As on load() — a ceiling in megabytes.

kglite.outline(graph: KnowledgeGraph, root: Any, edge: str, *, max_depth: int | None = None, body: str | None = None) str

Render the spanning tree from root along edge as a nested outline.

A projection of the graph into the “open and skim” view it otherwise lacks: a BFS from the node whose id is root following outgoing edge-typed edges, rendered as an indented markdown-style outline (each node once, at first discovery; labelled by title, falling back to id). Backed by the engine’s CALL outline(...) procedure, which yields the tree structure.

Example:

print(kglite.outline(g, "epic-1", "DEPENDS_ON"))
# - Build the API
#   - Define the schema
#   - Write the handlers
Parameters:
  • graph – The graph to project.

  • root – Identity (id) of the root node.

  • edge – Connection type to follow (outgoing).

  • max_depth – Optional descent bound (0 = just the root).

Returns:

The outline text (empty string if root has no node).

kglite.retry_on_conflict(graph: KnowledgeGraph, work: Callable[[Transaction], Any], *, attempts: int = 5, base_delay: float = 0.005, max_delay: float = 0.5, jitter: bool = True) Any

Run work in a transaction, retrying the whole unit on conflict.

work is called as work(tx) with a fresh Transaction and is re-invoked from the start on each attempt, so it must be safe to run more than once — read what you need inside it rather than closing over values read beforehand. The transaction commits when work returns and rolls back if it raises.

Parameters:
  • graph – The graph to transact against.

  • work – Callable taking the transaction and returning the result.

  • attempts – Maximum tries, including the first.

  • base_delay – Seconds before the second attempt; doubles each further attempt (exponential backoff).

  • max_delay – Upper bound on any single wait.

  • jitter – Spread each wait randomly over [0, delay] so competing writers don’t retry in lockstep. Disable only for deterministic tests.

Returns:

Whatever work returned on the successful attempt.

Raises:
  • TransactionConflictError – Every attempt conflicted; the final error is re-raised unchanged.

  • ValueErrorattempts is less than 1.

Example

>>> def signup(tx):
...     tx.cypher("CREATE (u:User {email: $e})", params={"e": email})
...     return "created"
>>> kglite.retry_on_conflict(graph, signup)
'created'
kglite.set_query_warning_policy(policy: str) None

Choose how Cypher query warnings are announced, process-wide.

A query warning has two channels, and this only moves one of them:

  • Structured, and unconditional — ResultView.warnings / ResultView.diagnostics["warnings"]. No policy empties it; code that reads the field behaves the same under all three.

  • The announcement — what a caller who is not reading the field still sees. That is what this sets:

    • "stderr" (default): a warning: ... line on stderr, exactly what every earlier release did unconditionally.

    • "silent": nothing is printed.

    • "pywarn": each warning is raised as a UserWarning through the warnings module instead of stderr, so the process’s own warning filters, logging.captureWarnings(True) and a custom showwarning all apply. It replaces rather than supplements the stderr line, because warnings.warn already routes to stderr by default and doubling it would defeat a filter the host installed.

"pywarn" is opt-in and will never become the default: under -W error (or pytest’s filterwarnings = error) it turns an advisory into a raise out of cypher().

Applies to every Python query path — KnowledgeGraph.cypher(), Session.cypher() / Session.execute(), Transaction.cypher, FrozenGraph.cypher() — including to_df=True and FORMAT CSV, whose return values cannot carry diagnostics and for which this is the only channel. The bundled CLI, MCP and Bolt servers are separate surfaces with their own presentation and are unaffected.

Parameters:

policy"stderr", "silent" or "pywarn".

Raises:

kglite.ArgumentError – For any other string.

Example:

import warnings, kglite

kglite.set_query_warning_policy("pywarn")
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    graph.cypher("MATCH (v:Vessel) RETURN v.imo")
# caught[0].message -> "RETURN projects property 'imo' which no ..."
kglite.stamp_file_freshness(graph: KnowledgeGraph, *, node_type: str | None = None, path_property: str = 'file_path', mtime_property: str = 'file_mtime', hash_property: str | None = 'content_hash', base_dir: str | None = None, batch_size: int = 1000) int

Capture each node’s linked-file state into properties (binding-layer; the engine never reads the filesystem). For every node with path_property, snapshot the file through one descriptor and SET mtime_property (a nanosecond UTC RFC 3339 string) and, unless hash_property is None, its sha256; a missing file sets both null. Resolved duplicate paths are read once. Updates run in bounded batches inside one atomic transaction. Run after a build; pair with check_file_freshness(). Returns the count stamped.

kglite.to_neo4j(graph: KnowledgeGraph, uri: str, *, auth: tuple[str, str] | None = None, database: str = 'neo4j', batch_size: int = 5000, clear: bool = False, merge: bool = False, selection_only: bool | None = None, verbose: bool = False) dict[str, Any]

Push graph data to a Neo4j database.

Extracts all nodes and edges (or the current selection) and writes them to Neo4j using batched UNWIND operations for performance.

Requires the neo4j package: pip install neo4j.

Parameters:
  • graph – The KnowledgeGraph to export.

  • uri – Neo4j connection URI (e.g. "bolt://localhost:7687").

  • auth – Tuple of (username, password). None for no auth.

  • database – Neo4j database name (default "neo4j").

  • batch_size – Nodes/relationships per UNWIND batch (default 5000).

  • clear – If True, delete all existing data before import.

  • merge – If True, use MERGE instead of CREATE (upsert semantics).

  • selection_only – If True, export only selected nodes. Default: auto-detect from active selection.

  • verbose – Print progress information.

Returns:

Summary dict with nodes_created, relationships_created, constraints_created, elapsed, database.

Example:

import kglite

g = kglite.load("graph.kgl")
kglite.to_neo4j(g, "bolt://localhost:7687", auth=("neo4j", "password"))
kglite.trim_memory() None

Return allocator-retained memory to the operating system.

KGLite’s Rust side allocates through mimalloc, which keeps a finished workload’s pages instead of handing them back — that reuse is why a second large query costs no page faults, and it is also why a process that once peaked at several GB keeps reporting that peak long after the graphs and result sets are gone. This is the lever that gives those pages back. It is opt-in on purpose: a forced collect at an internal seam (after save(), on graph drop) would spend the reuse on every call, so KGLite never calls it for you.

Call it when a peak is over and the process will stay alive: after a large ingest or a query that materialised millions of rows, before a long idle period, or between the stages of a long-running server. It costs milliseconds proportional to how much is being released, and releases the GIL while it runs, so other threads keep going.

What it can do: release the pages behind everything already freed. What it cannot do: shrink anything still referenced — an open KnowledgeGraph, a lazy ResultView, a freeze() — so drop those first; it is the drop that frees the memory and this that returns it. Calling it on a process with nothing to release is harmless and fast.

Reading the result on macOS. The reclaim uses MADV_FREE_REUSABLE, which hands the pages back immediately but leaves them counted in resident_size until the kernel needs them elsewhere — so ps, psutil’s rss and resource.getrusage can all stay flat while the memory has in fact been returned. The number that moves is the process footprint: Activity Monitor’s “Memory” column, or ri_phys_footprint from proc_pid_rusage. On Linux the reclaim is MADV_DONTNEED and ordinary RSS drops straight away.

Called from a thread other than the main one it still collects, but skips the pass that reclaims segments abandoned by exited threads; prefer the main thread when both are available.

Example:

g = kglite.load("big.kgl")
rows = g.cypher("MATCH (n) RETURN n").to_list()
...
del rows, g            # free it first
kglite.trim_memory()   # then give the pages back