kglite¶
Type stubs for kglite — a high-performance knowledge graph library.
Submodules¶
Exceptions¶
A user-supplied argument violated a precondition. |
|
A connection type isn't declared in the schema. |
|
Declaring a constraint failed because the stored data already violates it. |
|
Base class for declared-integrity-constraint failures. |
|
A write violated a declared UNIQUE / NOT NULL / NODE KEY / IS :: TYPE |
|
Base for all Cypher-related errors (syntax, timeout, execution, type). |
|
Cypher executor failure during query evaluation. |
|
Cypher parser / tokenizer rejected the query. |
|
Cypher query exceeded its |
|
Cypher value-type mismatch (e.g. arithmetic on a String). |
|
Blueprint expression evaluation failure. |
|
A file the user named doesn't exist on disk. |
|
A file's contents are malformed (bad |
|
Generic I/O failure (permission denied, mid-read EOF, mmap failure). |
|
Invariant violation — kglite-internal bug. Reports the source location. |
|
Two distinct names collided on one persisted interner key; no mutation was applied. |
|
Base class for typed KGLite engine failures. |
|
A |
|
A required argument wasn't passed. |
|
A node identified by |
|
A property is missing from a node or relationship. |
|
Schema validation failure (unknown property, type mismatch at pattern literal). |
|
A transaction's commit lost an optimistic-concurrency race. |
|
Structural validation failure (missing required field, wrong connection endpoint). |
Classes¶
Aggregation expression builders for |
|
Protocol for embedding models passed to |
|
An immutable, concurrently-readable snapshot of a graph. |
|
A high-performance knowledge graph with typed nodes, connections, and |
|
Iterator for ResultView. Converts one row per step. |
|
Lazy result container — data stays in Rust until accessed from Python. |
|
A thread-safe, shareable concurrency handle over a graph. |
|
Spatial compute expression builders for |
|
An isolated transaction on a KnowledgeGraph. |
Functions¶
|
Attach a DataFrame to a parent node as row NODES plus edges. |
|
Read-only drift check (binding-layer): snapshot each node's |
|
Names of every Cypher optimizer pass, in execution order. |
|
Estimate what loading the |
|
Build a KnowledgeGraph from a JSON blueprint and its declared inputs. |
|
Load an in-memory graph from a |
|
Build a |
|
Build a KnowledgeGraph from an inline JSON records spec. |
|
The query-warning policy currently in effect. |
|
Generate a synthetic org/social knowledge graph (bundled generator). |
|
Load a graph from a binary file previously saved with |
|
Load an RDF file into a fresh in-memory graph. |
|
Open a graph at |
|
Load a saved graph at |
|
Render the spanning tree from |
|
Run |
|
Choose how Cypher query warnings are announced, process-wide. |
|
Capture each node's linked-file state into properties (binding-layer; the |
|
Push graph data to a Neo4j database. |
|
Return allocator-retained memory to the operating system. |
Package Contents¶
- exception kglite.ArgumentError¶
Bases:
KgErrorA user-supplied argument violated a precondition.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.ConnectionNotFoundError¶
Bases:
KgErrorA connection type isn’t declared in the schema.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.ConstraintCreationError¶
Bases:
ConstraintErrorDeclaring a constraint failed because the stored data already violates it.
Raised by
KnowledgeGraph.define_schema()when the schema declares auniquetuple orprimary_keythat 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:
KgErrorBase 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:
ConstraintErrorA 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()forCREATE/MERGE/SET/REMOVEand by the bulk writers (add_nodesand everything funnelling through it).Initialize self. See help(type(self)) for accurate signature.
- exception kglite.CypherError¶
Bases:
KgErrorBase for all Cypher-related errors (syntax, timeout, execution, type).
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.CypherExecutionError¶
Bases:
CypherErrorCypher executor failure during query evaluation.
lineandcol(1-indexed) are set as attributes when the failure is pinned to a source position; both areNoneotherwise.Initialize self. See help(type(self)) for accurate signature.
- col: int | None¶
- line: int | None¶
- exception kglite.CypherSyntaxError¶
Bases:
CypherErrorCypher parser / tokenizer rejected the query.
lineandcol(1-indexed) are always present as attributes; both areNonewhen 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:
CypherErrorCypher query exceeded its
timeout_msbudget.Initialize self. See help(type(self)) for accurate signature.
- exception kglite.CypherTypeMismatchError¶
Bases:
CypherErrorCypher value-type mismatch (e.g. arithmetic on a String).
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.ExprError¶
Bases:
KgErrorBlueprint expression evaluation failure.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.FileError¶
Bases:
KgErrorA file the user named doesn’t exist on disk.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.FileFormatError¶
Bases:
KgErrorA file’s contents are malformed (bad
.kglheader, truncated blueprint, etc.).Initialize self. See help(type(self)) for accurate signature.
- exception kglite.FileIoError¶
Bases:
KgErrorGeneric I/O failure (permission denied, mid-read EOF, mmap failure).
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.InternalError¶
Bases:
KgErrorInvariant violation — kglite-internal bug. Reports the source location.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.InternerCollisionError¶
Bases:
KgErrorTwo 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:
ExceptionBase 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 isNoneonly on the three abstract bases —KgError,CypherError,ConstraintError— which span several codes. The same strings appear asKGLITE_STATUS_*in the C ABI and drive the BoltNeo.*mapping.
- exception kglite.LoadMemoryLimitError¶
Bases:
KgErrorA
.kglload was refused before decoding: its estimated memory exceeded the ceiling set bymax_load_mborKGLITE_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. Seeestimate_load_memory().Initialize self. See help(type(self)) for accurate signature.
- exception kglite.MissingArgumentError¶
Bases:
KgErrorA required argument wasn’t passed.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.NodeNotFoundError¶
Bases:
KgErrorA node identified by
(node_type, id)doesn’t exist.Initialize self. See help(type(self)) for accurate signature.
- exception kglite.PropertyNotFoundError¶
Bases:
KgErrorA property is missing from a node or relationship.
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.SchemaError¶
Bases:
KgErrorSchema validation failure (unknown property, type mismatch at pattern literal).
Initialize self. See help(type(self)) for accurate signature.
- exception kglite.TransactionConflictError¶
Bases:
KgErrorA transaction’s commit lost an optimistic-concurrency race.
The graph advanced between
KnowledgeGraph.begin()andTransaction.commit(), so the transaction’s working copy is stale and nothing was applied. Re-run the work against a freshbegin();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:
KgErrorStructural 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:
ProtocolProtocol for embedding models passed to
embed_texts/search_text.Required —
dimensionandembed()must be present.Optional —
load()andunload()are called automatically if present:load()is called before eachembed_texts()/search_text()call.unload()is called after each call completes (even on error).
Optional — ``model_id`` / ``model_name`` (a
strattribute): if present, its value is stamped onto the embedding store as provenance and surfaced viaKnowledgeGraph.embedding_info()(model). An embedder without it works exactly the same, but the store recordsmodel=None— so a model swap can’t be detected from the store alone. Add amodel_idto 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()inembed_texts()andsearch_text(). If not defined, this step is skipped.
- unload() None¶
(Optional) Release model weights / free resources.
Called automatically after
embed_texts()andsearch_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 sameFrozenGraphin parallel without the single-owner borrow conflict a liveKnowledgeGraphraises. Use the “build → freeze → share → swap” model: build a graph, freeze it, serve concurrent readers, and atomically swap in a newfreeze()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 viatext_score()/vector_score(). A mutation query (CREATE/SET/DELETE/REMOVE/MERGE) raisesValueError— a frozen snapshot is immutable; mutate the source graph and take a freshKnowledgeGraph.freeze().Safe to call concurrently from many threads on the same snapshot.
row_limitcaps 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 underORDER BY) and an explicitLIMIT mmakes the effective capmin(m, row_limit). Truncation is never silent: it warns, andResultView.diagnosticscarriesrow_limitplus the exact pre-truncationtotal_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
KnowledgeGraphis single-owner: it is not safe to share one instance across threads while any thread mutates it (doing so raises a clearRuntimeError). For concurrent access, don’t share the graph — take a thread-safe handle off it:session()(shared reads + serialized writes, plusSession.cursor()for per-thread fluent chains) orfreeze()(a lock-free read-only snapshot). Seedocs/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). Requirespath.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 withkglite.load(path).
Note
This constructor is never durable, and takes no ``durable`` argument. It returns a detached graph — no
source_path, so a baresave()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 todurable="full". SoKnowledgeGraph(storage="mapped")is mapped-and-unlogged whilekglite.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 — seekglite.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) orquery(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
Nonewhen usingquery.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, matchingadd_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 matchingsource_id_fieldandtarget_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 aUserWarning;'error'refuses the whole call withArgumentError, 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, anddata(DataFrame withsource_id/target_idcolumns).git_shaandmodified_byapply to every opted-in edge type.- Returns:
Mapping of
connection_nameto 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_shaandmodified_byapply to every loaded opted-in edge type.- Returns:
Mapping of
connection_nameto 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 likeset_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_columnexactly asset_embeddings()does — stored property, identity alias (title_field/id_fieldcolumn 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), andstore_created(Trueiff 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 bylabels(n). The primary type (set viaadd_nodes(node_type=...)) is immutable.SET n.typewrites 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_fieldvalues).label – Secondary label to add.
- Returns:
Dict with
labelled(newly added) andskipped(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
timeseriesis 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’sstatus/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.aliasestests membership andUNWIND n.aliasesyields 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.kread back the value) rather than being stringified. Adatetime64column keeps its full time-of-day when any value has a nonzero time (stored as aTimestamp); 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. DefaultFalse— explicit opt-in protects existing callers.managed_reload –
When
True, this call is part of a managed reload (a batch writer rebuilding from source). Ifnode_typedeclareslayer='runtime'in the schema (an agent-owned type), the write is skipped as a no-op and the returned report carries the normal shape plusskipped_runtime_layer=True,node_typeandmessage. Undeclared orlayer='managed'types are written normally. Pairs with thelayerdeclaration indefine_schema()andconflict_handling.What this does and does not guarantee. It is a guard the rebuilding side opts into, not an enforced perimeter: an
add_nodescall that omitsmanaged_reloadwrites aruntimetype normally, nothing stops a runtime writer (Cypher, or another loader call) from mutating or deletingmanagednodes, andadd_connections()is not covered at all. Usewrite_scopeon 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 mappingyear/month/day/hour/minuteto 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'])createsAgent-typed nodes that also wear theReviewerlabel, queryable viaMATCH (a:Reviewer)orMATCH (a:Agent:Reviewer). For per-row labels, calladd_label()after.git_sha – Commit SHA stamped on opted-in
auto_timestamptypes.modified_by – Actor id stamped on opted-in
auto_timestamptypes.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 aUserWarning.'error'— refuse the whole call. RaisesArgumentErrornaming 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 (seecolumn_types).
- Returns:
Operation report dict with keys
nodes_created,nodes_updated,nodes_skipped,processing_time_ms,has_errors, and optionallyerrorswith 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, anddata(a DataFrame).git_shaandmodified_byapply to every opted-in node type.- Returns:
Mapping of
node_typeto 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 — usecypher(), which expresses the same write and is logged. Seekglite.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 bytime_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
datalinking 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_indexoradd_timeseries). The values length must match the time index length. Usefloat('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_idup in, never which node types the path may pass through (usevia_typesfor 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).
Trueexactly whenshortest_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_idup in, never which node types the path may pass through (usevia_typesfor 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 untilcommit(). Rollback (or dropping without commit) discards the fork.Uses optimistic concurrency control:
commit()will raise a typedKgErrorif the graph changed sincebegin().- 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]. DefaultTrue.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
ResultViewof rows withtype,title,id,score, sorted by score descending. Or a pandas DataFrame ifto_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 ashapely.geometry.Polygon(box) instead of a dict.
- Returns:
Dict with
min_lat,max_lat,min_lon,max_lon, or a shapely box polygon whenas_shapely=True, orNoneif 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.mdin 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.
Noneindexes 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.0for an indexed document sharing no word with the query,nullfor 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 underauto_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 INDEXESreports both facts, in itsstaleanddeltacolumns.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_limitwell 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
MATCHfilter uses, so a type’s id/title column can be indexed under the name the loader gave it (add_nodes(df, "Person", "npdid", "name")makesbuild_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.kglas its own section, carrying its staleness, andkglite.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 inSHOW INDEXES/schema()under the canonical name'{node_type}.{property}'with typeFULLTEXT, alongside any equality or range index on the same property, andDROP 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; passexact=Trueto force an exact scan.Later vector writes do not drop the index.
add_embeddings/embed_texts/set_embeddingsleave the slot layout alone, so they are recorded and folded in at query entry while the outstanding delta stays at or underauto_refresh_limit; a larger delta is served by the exact scan — correct, and slower — until you rebuild or callrefresh_vector_index().SHOW INDEXESreports both facts, in itsstaleanddeltacolumns.Catch-up never embeds. A node with no vector is not part of the delta: it is counted in
SHOW INDEXES’unembeddedcolumn 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 carriestext_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
.kglcarries 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_columnthat 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_asis 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 withdurable=rather than left unlogged. Usecypher()there; seekglite.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 ashapely.geometry.Pointinstead of a dict.
- Returns:
Dict with
latitudeandlongitude, or a shapely Point whenas_shapely=True, orNone.
- 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/uniquedeclaration 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 withDROP 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 afterclose().
- 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
ResultViewof rows withtype,title,id,score, sorted by score descending. Or a pandas DataFrame ifto_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
ResultViewcontainingid,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 ifstore_asis 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 — usecypher(), which expresses the same write and is logged. Seekglite.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. DefaultFalse.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_parentisTrueand 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 withtraverse(). A list of two or more raisesArgumentErrorrather than comparing against only the first — callcompare()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_typeis not a string or list of strings, or is a list holding more than one type; if the method dict carries an unknownresolvemode; or if the comparison itself rejects its inputs (unknown method name, a method used without its requiredtarget_typeor settings such asmax_m/property/features).TypeError – If
methodis 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
Noneif 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. IfFalse, 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
KnowledgeGraphthat 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
otherinto 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 followingembed_texts(mode='changed')re-embeds only genuinely new/changed text. Vectors whose id has no matching node here are skipped (counted). Replaces the manualembeddings()→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_asis 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 withdurable=rather than left unlogged. Usecypher()there; seekglite.open().
- create_composite_index(node_type: str, properties: list[str]) dict[str, Any]¶
Create a composite index on multiple properties.
The order of
propertiesis not significant: the index is stored under its property names sorted, andlist_composite_indexes()andSHOW INDEXESreport that spelling.- Parameters:
node_type – Node type to index.
properties – List of property names for the composite key.
- Returns:
Dict with
node_type,properties, andunique_combinations.- Raises:
ValueError – If
node_typeexists only as a secondary label (indexes are keyed by primary type; seecreate_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_typeto 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 — usecypher(), which expresses the same write and is logged. Seekglite.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-typecreate_indexalready covers the use case at in-memory scale.- Returns:
Dict with
property,unique_values(count of nodes indexed), andcreated.
- 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;
createdis thenFalse. It isTrueonly 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, andcreated(Falseif the index already existed).- Raises:
ValueError – If
node_typeexists 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
>,>=,<,<=, andBETWEENqueries infilter()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_typeexists only as a secondary label (indexes are keyed by primary type; seecreate_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()withOVER (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_statswith keysnodes_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], andSHOW 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 ofCYPHER.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 formFOR ()-[r:T]-() REQUIRE r.p IS NOT NULL | IS :: TYPE,DROP CONSTRAINT <name> [IF EXISTS], andSHOW CONSTRAINTS— declares constraints that are enforced on every write path, including the bulk loader.REQUIRE (n.a, n.b) IS UNIQUEconstrains the tuple;IS NODE KEYis 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, soDROP CONSTRAINT <name>works; a constraint declared without a name is addressable by its canonical descriptor (Label.property, orTYPE.propertyfor a relationship constraint).IS :: TYPE/IS TYPED TYPEdeclares 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 (uselock_schema()/validate_schema()for those).IS UNIQUE/IS RELATIONSHIP KEYon a relationship is rejected: KGLite has no single answer for when two relationships of a type are the same one.IS UNIQUE/IS NODE KEYover the identity field is rejected because it would enforce nothing, under any spelling that resolves to it (iditself or the node type’s own id column):idis a structural field rather than a stored property, so the unique secondary index never sees the write. Declareprimary_keythroughdefine_schema()instead — it probes the per-type id index on every write path.IS NOT NULLonidis accepted and enforced: an omitted id is resolved by every write path and satisfies it, an explicit{id: null}violates it.SHOW CONSTRAINTSandSHOW INDEXESare reads and work on a read-only graph. See the “Cypher constraint DDL” section ofCYPHER.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()orKnowledgeGraph.begin()when failure must roll back. Property and composite indexes are maintained.FORMAT CSV: Append
FORMAT CSVto 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
ValueErrorwith aDid 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 carryResultView.diagnostics, and neither does the string aFORMAT CSVquery returns — so for both shapes the query’s warnings are only announced (stderr by default, seekglite.set_query_warning_policy()), never attached. Run the query withoutto_df/FORMAT CSVand callResultView.to_df()when you want both the frame andrv.warnings.params – Optional parameter dict for
$paramsubstitution. 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). Pass0to disable the deadline for this call. Independently of the deadline, a long-running read can be interrupted withCtrl-C(raisesKeyboardInterrupt) 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
LIMITto bound the rows you get back. Size it from acount(*)probe of the pattern with headroom — it is a hard refusal, not a soft cap — and note that withtimeout_msset 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 toset_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 BYsorts 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 underORDER BY. An explicitLIMIT mis applied first, making the effective capmin(m, row_limit). Applies to a mutation’s trailingRETURNtoo: every write still happens andlast_mutation_statsstill counts them all — the cap bounds what is reported, never what is changed.EXPLAINis exempt.0is legal (“keep no rows, still tell me the total”). Truncation is never silent: it raises a query warning, anddiagnosticscarriesrow_limitplus the exact pre-truncationtotal_rows. Defaults toset_default_row_limit().streaming – When
True(default), the executor absorbs compatible clause runs (currentlyWITH/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)). PassFalseto 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.mdfor 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 thisFalse. 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 raiseValueError. 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-ownedAlgorithmnodes).None(default) = unrestricted;[]denies every mutation. Applies per-call; also onSession.execute()andTransaction.cypher. The perimeter, exactly:Node writes —
CREATE,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 writes —
CREATE (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 DELETEremoves 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 loadersadd_nodes()/add_connections()—write_scopeis 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 withFORMAT CSV. Only the ResultView carriesdiagnostics/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.KgErrorsubclasses 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()andtraverse()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', orNoneto 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-argumentCALL 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_ais a forest (single parent, no cycles);cardinality/requireddescribe outgoing edges of the domain type;required_properties(per-edge presence) andproperty_types(per-edge type check of present values, names validated on declare) are audited per edge;inverse_nameis a reading-direction alias only unlessinverse_enforced: Trueopts into the physical-pairing check;enforcementis a severity (advisory/warn/error) or a per-check map ({"required_properties": "error"}, unlisted checks stay advisory) consumed by the blueprint gate andontology_audit();exemptis a per-check map of source classes whose violations are counted inontology_audit()’sexemptedcolumn 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 atadvisory— accepted forrequired_propertiesandproperty_typesonly, 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 flatexempt: [...]form;transitiveandancestryare mutually exclusive and mean different things —transitive: Trueenrollstransitivity_violation, which audits a stored closure (everya→b→cmust have a storeda→cedge), whileancestry: Trueis 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;bynames 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 fromset_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_atarget, class cap exceeded, bothtransitiveandancestryon one relationship, anexemptentry 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_dicttakes 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
Taskwithout arequiredentry it used to have and that requirement is withdrawn.Pass
replace=Truefor 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 aUserWarningnaming 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 byDROP CONSTRAINT.- Parameters:
schema_dict –
Schema definition with
nodesandconnectionskeys. Each node entry may setrequired/optional/types, and optionallyprimary_keyanduniqueto declare enforced integrity constraints:g.define_schema({"nodes": {"Person": { "primary_key": "email", "unique": [["first", "last"]], "required": ["email"], }}})
primary_keymay name any property and means unique and present (NODE KEY): aCREATEthat duplicates or omits it is rejected — useMERGEto 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.uniquedeclares 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.requiredis enforced at write time, not only byvalidate_schema(): aCREATEthat omits the property, aSETthat nulls it, and aREMOVEthat drops it all raise.typeis the node’s label and cannot be absent, so requiring it is a no-op;idandtitleare auto-supplied when omitted (so omitting them satisfies the requirement) but can be explicitly nulled, and that is rejected. UnlikeCREATE CONSTRAINT ... IS NOT NULL, which verifies stored data before installing,requireddeclares 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 theadd_nodesupsert that promotes it is a normal enforced write, and an unpromoted stub stays reportable viavalidate_schema()and removable viapurge_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
layerto'managed'(rebuilt from source by a batch writer) or'runtime'(owned/mutated live by another writer, e.g. an agent). With layers declared, anadd_nodes(..., managed_reload=True)call skips aruntimetype instead of writing it — a declared lane for a well-behaved batch writer, not an enforced perimeter (the caveats are onadd_nodes()’smanaged_reload):g.define_schema({"nodes": { "AlgorithmSpec": {"layer": "managed"}, "Task": {"layer": "runtime"}, }})
A node entry may also set
auto_timestamp: Trueto opt that type into freshness provenance: every write (CypherCREATE/SET/MERGEandadd_nodes) auto-stamps anupdated_attimestamp, plus the caller-suppliedgit_sha/modified_bywhen provided. It is off by default (writes stay deterministic) and independent oflayer/lock_schema:g.define_schema({"nodes": {"Task": {"auto_timestamp": True}}})
- Raises:
ConstraintCreationError – A declared
uniquetuple orprimary_keyis 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). DefaultTrue.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
ResultViewof rows withtype,title,id,score, sorted by score descending. Or a pandas DataFrame ifto_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 itstypeandid.
- 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 (
typesparameter):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, elseid) 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 carriescoverage="51%"; a fully populated one carries no coverage attribute at all.
Type search (
type_searchparameter):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 (
connectionsparameter):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 (
cypherparameter):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 (
fluentparameter):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, orfluentis 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 thedescribe(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. WikidataP31has 191k distinct pairs).sample_truncate – Max chars for string values in
vals=attributes, sample-node ids/titles, and sample-edge attributes. Defaults to 40. PassNoneto 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
selfbut not inother(set difference).- Returns:
A new KnowledgeGraph with the difference.
- drop_composite_index(node_type: str, properties: list[str]) bool¶
Remove a composite index. Returns
Trueif it existed.
- drop_index(node_type: str, property: str) bool¶
Remove an index. Returns
Trueif it existed.
- drop_range_index(node_type: str, property: str) bool¶
Remove a range index. Returns
Trueif it existed.
- drop_text_index(node_type: str, property: str) bool¶
Drop the BM25 text index for
(node_type, property).Returns
Trueif an index was dropped,Falseif 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
Trueif an index was dropped,Falseif 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’stext_column, callsmodel.embed()in batches, and stores the resulting vectors as{text_column}_emb. Nodes with missing or non-string text are skipped.text_columnresolves exactly asset_embeddings()resolves it — a stored property, an identity alias (a type built withtitle_field='name'embeds its titles under'name'), the canonicalid/title, or a structural alias — and a column that resolves to none of those raisesValueErrorrather 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, andembedding_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 iftqdmis 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, anddimension.- Raises:
ValueError – if
node_typedoes not exist in the graph — the same complaintset_embeddings()makes, raised before the model is loaded (an existing type with no matching rows stays a{'embedded': 0}no-op); iftext_columnresolves to none of the accepted spellings; or ifmodeis 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 symptomimport_embeddings()warns about when keys mismatch.
Each row also carries a
length_statsdict 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 withstatus="embeddable"but theirlength_statsdistinguishes 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(orNone),metric(orNone),status, andlength_statswithmean_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, orNoneif 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_embeddingsreject a dimension mismatch (re-embed withmode='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, orNoneif no store exists.Returns a dict with
dimension,count(vectors stored),model(the embedder id stamped atembed_textstime, orNonefor vectors supplied directly viaadd_embeddings),metric, andhashed(how many vectors carry a source-text hash, used byembed_texts(mode='changed')for change-detection). Detect a model swap or a partially-hashed store without external bookkeeping.metricis the store’s effective distance metric: the one set viaset_embeddings(metric=...)if any, else'cosine'(the default search applies). It is neverNonefor an existing store — a store with no explicit metric reports'cosine'(consistent withlist_embeddings()).The
modelis populated when the embedder exposes amodel_id/model_nameattribute (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 freshkglite.open(path)reads — andpathbecomes this graph’s save target, so a later baresave()writes back to it. Nothing transits the system temp directory on the way, so a conversion larger than/tmpstill 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
UserWarningnaming the location. Keep it only for a throwaway, process-scoped conversion.graph_info()reports the result:storage_modebecomes"disk"andedges_mappedbecomesTrue.columnar_is_mappedis about property-column spilling and staysFalsehere — 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
Trueif a node ofnode_typewith 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 Pythonintand the stored id normalize to the same key.- Parameters:
node_type – The node type (e.g.
'User').unique_id – The unique ID value.
- Returns:
Trueif a matching node exists,Falseotherwise.
- 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) orPROFILE(execute it and return per-clause statistics onresult.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-serverconsole 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/Enumnodes againstquery(matched against name + signature + docstring), takes the topmax_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_pathproperties against. Files matched literally are tried first; roots are searched in order. Default: cwd only.
- Returns:
A markdown string with
## Query/## Entry points/## Related/## Sourcesections. 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 (.sql→sqlite).sqlitewrites 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
.dbfile 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:
Trueif 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.jsonfor round-trip re-import viafrom_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:
Trueif a selection exists,Falseotherwise.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
.kglefile, keyed by node ID.- Parameters:
path – Output
.kglefile path.node_types –
Optional filter.
None(default): export all stores.list[str]: only stores whosenode_typeis in the list.dict[str, list[str]]: per-type list of text columns to export.
- Returns:
Dict with
stores(count of stores written) andembeddings(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.csvis file-only — it writes two files (nodes and edges), which one string cannot carry — so it is rejected here; useexport(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
.kglfiles 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.idis the canonical integer node id in every storage mode. When a node in other matches an existing node here, the conflict is resolved byconflict_handling(same vocabulary asadd_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 asupdate(matchesadd_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_nodesuses for new columns).Edges dedup on
(connection_type, source, target): an edge that already exists here is not duplicated — its properties merge perconflict_handling. Exact-duplicate edges present in both graphs are created once, not twice (mirrorsadd_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_embeddingsafter 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 consultschema_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 optionallyerrors.
- 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 typeFunction,Struct,Class,Mixin,Enum,Trait,Protocol,Interface,Module, orConstant— the types produced by code-graph builders (e.g. codingest). On graphs that don’t contain these types (e.g. a social graph withPersonnodes),find()returns an empty list. For general name lookup on other node types, useselect(type).where({"name": ...})orcypher("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
FrozenGraphthat shares this graph’s data (an O(1) clone — no deep copy) and exposes only read methods. A liveKnowledgeGraphis single-owner and raises if a second thread touches it while another mutates it; aFrozenGraphhas no mutating method, so any number of threads can runcypher()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(), orNonewhen 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(), orNonewhen 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()orreindex().- Returns:
node_count: Number of live nodesnode_capacity: Upper bound of node indices (includes tombstones)node_tombstones: Number of wasted slots from deletionsedge_count: Number of live edgesedge_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 tofragmentation_ratio, which is node-shapedfragmentation_ratio: Ratio of wasted node storage (0.0 = clean)type_count: Number of distinct node typesproperty_index_count: Number of single-property indexescomposite_index_count: Number of composite indexesstorage_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 astorage=conversion) landed where you expectedformat_version:.kglon-disk layout version (engine-owned)library_version: kglite version that last saved the graphuser_schema_version: your data-model revision (seeschema_version);0when unversionedcolumnar_heap_bytes: Heap-resident bytes in the property columnscolumnar_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 reportsFalsehere unless its columns also spilled; that is normal, seeedges_mappededges_mapped: Whether the edge CSR arrays are memory-mapped from files.Trueon a disk graph whose CSR is materialized; alwaysFalseon the memory and mapped backends, which keep edges in the heap graph and have no CSRedge_property_overlay_rows: Edges whose properties are held in the disk backend’s heap mutation overlay rather than the mmap-backed base.0on every non-disk backendmemory_limit: Configured memory limit (None if unset)columnar_total_rows: Total property-column rows, including rows orphaned by deleted nodescolumnar_live_rows: Rows backed by live nodesauto_vacuum_threshold: The configured auto-vacuum threshold, orNonewhen disabled (seeset_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_indexreported aspersistent) reportsFalse.
- 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
.kglefile.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
UserWarningis emitted to surface the silent-drop case. This most commonly indicates the.kglefile 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
.kglefile previously created byexport_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
Nonewhen no such index exists — a disk-backed persistent index reportsNone.
- indexes() list[dict[str, Any]]¶
Return a unified list of all indexes.
- Returns:
node_type: the indexed node typeproperty: 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-awarereorder_match_clausespass — 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, Pythonadd_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, andnum_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 vialen(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,propertiesandstate;statemeans what it means onlist_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,countandmetric.text_columnis the source column this API takes ('summary');store_nameis the store the column is held in ('summary_emb') — the spelling Cypher’svector_score()takes.metricis 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,propertyandstate. Range, composite and disk-backed persistent indexes are not included.stateis"ONLINE"for a built index and"DEFERRED"for one akglite.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 reportsFalsefor aDEFERREDentry — 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 itONLINE.
- 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:description→descriptionproperty.prop/direct/P*with aQ-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"]).Noneimports all predicates. Labels and descriptions are always imported regardless of this filter.languages – Language codes for label/description literals (e.g.
["en"]).Nonekeeps 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". Seekglite.progress.TqdmBuildProgressfor 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, orCALL { ... }subquery raisesSchemaErrorinstead 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) andRETURN/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'whereCREATE CONSTRAINT ... REQUIRE p.age IS :: INTEGERis 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 > $cutoffwith a string bound) raises too; the verdict is per call, so the same statement runs with an integer bound. A type declared only bydefine_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 byWITH, neither of which resolves to one type; and the built-insid,title,name,type. A relationship type the graph has never seen, a relationship arrow pointing the wrong way, and a type mismatch read from adefine_schema()field type rather than anIS :: Tconstraint, 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
See also
- 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, andnum_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 Personstamps the actual secondary label:Persononto everyStudentnode (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, andlabels(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) oropen(a manualSET, an adoption, or an extend-graph union touched it — still correct, but closure-reliant optimizations stay off for it). ManualREMOVEof 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
Noneif 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
ResultViewof rows withtype,title,id,score, sorted by score descending. Or a pandas DataFrame ifto_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
valueslist 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 valuesunique: count of distinct values (a lower bound whenapproxis True)values: (optional) sorted list of values when unique count <= max_valuesapprox: True whenunique/valuesare 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 (approxFalse).
- 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 deletededges_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. IfFalse, 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()ordescribe(). The cache is persisted bysave()and restored byload(), 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 —
0when 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 underauto_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
labelis 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
removedandskipped(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 indata(or thequeryresult), its existing edges ofconnection_typeare 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
MENTIONSof 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 (includingquerymode andextra_properties), with identical semantics; only the prune-first behaviour differs.- Parameters:
data – DataFrame containing edge data, or
Nonewhen usingquery.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 aUserWarning;'error'refuses the whole call withArgumentError, 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
.kglbinary 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. Withfsync=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>-walsidecar 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 nextkglite.open(path, durable=...)replays those commits back over what was just saved.save()raisesValueErrorinstead, 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 withdurable=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 topath;save()itself writes whatever target it is given without asking for it. So a graph obtained fromkglite.load()can publish over a path anopen()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 whatkglite.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 viakglite.open()orkglite.load(), in which case it defaults to that origin file. Passing a path updates the remembered target (“save as”). RaisesValueErrorif 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). SetFalseto 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=Falseis ignored (aUserWarningis 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 bareOSError: 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.kglfile that reloads viakglite.load(path). A.kglis a file, never a disk-mode directory, sostorage='disk'is refused on either entry point; build a disk graph withkglite.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"stringsnode_count: total nodesedge_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
textonproperty(default ‘title’).Tries exact match first, then prefix match. Returns up to
limitresults as dicts withid(node index),type,title, andid_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 (seevector_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 asvector_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()andbuild_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 ordate()context). Passtemporal=Falseto 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 carrynode_typeas a secondary label (added viaadd_label()), not only nodes whose primary type isnode_type— the fluent equivalent of CypherMATCH (n:node_type). DefaultFalsepreserves 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
Sessionfrom this graph.Unlike a live
KnowledgeGraph— which is single-owner and trips a borrow guard when shared across threads mid-mutation — aSessionexposes only&selfmethods with synchronisation in an internal lock: concurrentcypher()reads run lock-free, andexecute()writes serialise behind the lock with copy-on-write + atomic swap.The
Sessionis 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 aKnowledgeGraph, then.session()and serve every thread through theSession” — don’t keep mutating the original graph after handing out aSession.
- 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 withgraph_info()['auto_vacuums_run'].- Parameters:
threshold – A float between 0.0 and 1.0, or
Noneto disable. Default is0.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
LIMITin 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_unitsoverrides 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_unitswork the executor performs
raises
row_limitrows handed back to you
truncates
The query still runs to completion and still computes every row, so
ORDER BYsorts 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 underORDER BY. An explicitLIMIT min the query is applied first, so the effective cap ismin(m, row_limit).Truncation is never silent. It raises a query warning, and
ResultView.diagnosticscarriesrow_limitplus the exact pre-truncationtotal_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.0is 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_limitoverrides this default,0included. Applies to a mutation’s trailingRETURNas well: the writes all still happen andlast_mutation_statsstill counts them all, because this caps what is reported, never what is changed.EXPLAINis 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-calltimeout_msoverrides.Positive integer: use as the default.
Per-query
timeout_msalways 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
Noneto unbind the currently-registered embedder.After registering,
embed_texts()andsearch_text()use the registered model automatically. The model is not serialized — callset_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
dimensionandembed()— seeEmbeddingModel. OrNoneto 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_columnto 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 withtitle_field='name'accepts'name', one built withid_field='npdid'accepts'npdid'), the canonicalid/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 atitle_field='name'type writesname_emband is read back as'name'byvector_search(),text_score()andlist_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 laterembed_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 withsave().
- Returns:
Dict with
embeddings_stored,dimension, andskipped.
- 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 emptytextto 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()andvacuum()run.unspill()brings them back to the heap; the limit itself survives that and is re-applied by the next write, so passNonefirst 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
Noneto 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.
0marks 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, thetable.upsert/table.deleteprocedures, nestedSETpaths). Column order, dtypes, and nullability are recorded in a per-(node_type, property)registry persisted with the graph and restored byget_table_property(); Cypher reads see map keys in sorted order as for any map. The write routes through CypherSET, 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_rowsand 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 andtraverse()auto-filters temporal connections to “current” (today or thedate()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
directionfor a one-way search.- Parameters:
source_type – Source node type. An ID namespace — it says which type to look
source_idup in, never which node types the path may pass through (usevia_typesfor 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. Honoursconnection_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), andlength(hop count). Whenweight_propertyis set, also includesweight(sum of edge weights).Noneif 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_idup in, never which node types the path may pass through (usevia_typesfor 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
Noneif 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_idup in, never which node types the path may pass through (usevia_typesfor 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
Noneif 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_idup in, never which node types the path may pass through (usevia_typesfor 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 anddirectioneither 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
Noneif 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 overshortest_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_typesfor that; without it aPerson-to-Persondistance can be answered through aCity).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_typeormax_hopsis 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_idup in, never which node types the walk may pass through (usevia_typesfor that).source_id – Source node ID.
target_type – Restricts the result to nodes of this type, and is the ID namespace
target_idsare looked up in (defaulting tosource_type). It does not restrict the traversal: the walk may still route through any node type unlessvia_typessays 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.
0returns 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.
- With
- Without
target_ids(discovery mode): only the nodes actually reached, in non-decreasing distance order. Absent means unreachable (or beyond
max_hops); there are noNonevalues, because enumerating every unreached node is the footgun this mode exists to avoid.
- Without
The source itself is present at distance
0whenever 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_hopsis given; when an id ortarget_typedoes not exist; when two reached nodes of different types share an id; or whentimeout_msexpires — 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 asType1(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 BYuses — 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
Noneif 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 makesdurable="normal"adoptable rather than merely fast. Without it, a"normal"graph’s only route to power-safety is a fullsave(), 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 — raisesValueError. 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
channelis 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
Noneif 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
.kglbyte 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 throughkglite.from_bytes().Default/mapped modes only: a
disk-mode graph is a directory, not a single byte stream, so this raisesValueErrorfor disk graphs (usesave('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,titleandtypecome from the node’s canonical identity. A node may also store a property under one of those names —CREATE (:T {title: 'a'})setstitleboth 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: withinclude_type=Falsethere is notypecolumn to collide with, so a storedtypeproperty is returned as itself.- Parameters:
include_type – Include
typecolumn. DefaultTrue.include_id – Include
idcolumn. DefaultTrue.
- 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
MultiDiGraphis the lossless target.node_keychooses the networkx node key:'id'(default) uses the bare node id,'type_id'uses the(node_type, id)2-tuple.node_type,titleand 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 itsconnection_typeas the networkx key; additional same-type parallel edges use collision-safe composite keys. Every edge also stores the type in itsconnection_typeattribute 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
networkxextra: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.MultiDiGraphmirroring the full graph.- Raises:
ArgumentError –
node_keyis neither'id'nor'type_id'; ornode_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 withnode_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
.kglgittextconvdiff filter (alsokglite export-text <file>from the CLI), makinggit diffof two.kglsnapshots 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', orNone(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_asis set — this same graph (not a copy), so the call can be chained.
Note
store_aswrites node properties, so on a graph opened withdurable=it must be reached through the handlekglite.open()returned. A selection is itself a derived handle, so the wholeselect(...).unique_values(store_as=...)shape is refused there; usecypher()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 raisingSchemaError.- 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), andreport_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 — usecypher(), which expresses the same write and is logged. Seekglite.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 graphtombstones_removed: Number of free node slots reclaimededge_tombstones_removed: Number of free edge slots reclaimed. A relationship-only delete workload (MATCH ()-[r]->() DELETE r) produces these and no node tombstones at allcolumnar_rebuilt: Whether the pass actually dropped property-column rows left behind by deleted nodes (Falsefor 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 thedate()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_search(text_column: str, query_vector: list[float], 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¶
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,scoreand all node properties. Given → each hit carriesid+scoreplus only the named fields (a property name, or a structural field liketitle/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). DefaultFalse→ a query covering most of a large indexed store uses the approximate index; setTruefor guaranteed-exact results. The index is used whenever a single node type carriestext_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 hasid,title,type,score, and all node properties —scorealways present, properties read live so a hit is identical before/aftersave()+ reload (no follow-upMATCH ... WHERE id IN [...]join needed). Withreturning=[...]a hit hasid+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. Uselist_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 declaredUNIQUE(orprimary_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_tuplescounts distinct colliding value tuples;sampleis one of them, positional againstproperties. 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 twowhere()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. IfFalse, keep only connected nodes. DefaultTrue.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 ashapely.geometry.Pointinstead of a dict.
- Returns:
Dict with
latitudeandlongitude, or a shapely Point whenas_shapely=True.
- property last_mutation_stats: dict[str, int] | None¶
Mutation statistics from the last Cypher mutation query.
Returns
Noneif 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_removedcount KGLite index structures, mirroring Neo4j’sindexesAdded/indexesRemovedcounters.CREATE RANGE INDEXreports 2 — a hash equality index plus a B-tree range index, which together serve what Neo4j’s single RANGE index does.constraints_added/constraints_removedcount constraints rather than the structures behind them, mirroring Neo4j’sconstraintsAdded/constraintsRemoved.IS NODE KEYreports 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.
0means unversioned, which is also what a graph saved before this field existed reports.Distinct from
graph_info()['format_version'], which is the.kglon-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. Useschema()ordescribe()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), andsample().Data is only converted to Python objects when you actually access rows (via iteration, indexing,
to_list(), orto_df()). This makescypher()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-emptyresult[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 ResultViewresult.to_list()— all rows aslist[dict](full conversion)result.to_dicts()— alias forto_list()(polars/pandas naming)result.one()— first row as dict, orNoneif emptyresult.scalar()— first column of first row, orNoneif emptyresult.column(name)— all values for one column as alistresult.to_df()— pandas DataFrame (full conversion)result.columns— column namesresult.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 accessorresult.column("col")(returns alist), orresult.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
nameis 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
Noneif 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
Noneif the result is empty.The first column is decided by the query’s
RETURNorder (the same order ascolumns); 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 toto_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'), orNone.
- 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.Noneonly 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, orNonewhen no deadline applied (memory graphs by default, or any call withtimeout_ms=0). A deadline that fires raisesCypherTimeoutErrorrather than returning a partialResultView, 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, orNonewhen 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 beforerow_limittruncated it. Set only when truncation actually happened, sototal_rows is not Noneis the truncation flag, andlen(rv), total_rowsis 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: aMATCHagainst an unknown node label or relationship type, a property read inWHERE/RETURN/WITH/ORDER BYthat 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 byrow_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_msor move toward anchored queries when your query repeatedly approaches the deadline, and surfacewarningsto catch silent-empty-result typos.
- property profile: list[dict[str, Any]] | None¶
PROFILE execution statistics, or
Nonefor 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
Nonefor read queries / non-cypher results.
- property warnings: list[str]¶
The query’s non-fatal warnings — shortcut for
diagnostics["warnings"].Same list, and
[](neverNone) for a clean query and for a view that did not come from a query at all — ahead()/tail()slice, a DataFrame round-trip — wherediagnosticsisNoneand subscripting it would raise. Seediagnosticsfor 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’sMutex<Arc<DirGraph>>and exposes only&selfmethods, so it can be shared across a thread pool: concurrentcypher()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 liveKnowledgeGraphraises.The
Sessionis an independent owner seeded from the source graph’s state; once either side mutates, copy-on-write forks them. Treat theSessionas the live store after creating it.- cursor() KnowledgeGraph¶
Spawn a per-thread query cursor over a snapshot of this session.
Returns a
KnowledgeGraphbound to a snapshot of the session’s current state, with a fresh fluent cursor. Wheresnapshot()hands out a read-onlyFrozenGraph(justcypher()),cursor()gives the full fluent surface —select/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 sharedSessionand 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 freshcursor()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)
Arcclone), releases the Session lock, and runs the query GIL-free — so many threads can callcypher()on the sameSessionat 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) raisesValueError— useexecute()for writes.row_limitcaps 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 underORDER BY) and an explicitLIMIT mmakes the effective capmin(m, row_limit). Truncation is never silent: it warns, andResultView.diagnosticscarriesrow_limitplus the exact pre-truncationtotal_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 — seeKnowledgeGraph.cypher()for the exact perimeter.git_shaandmodified_byare stamped on types that opt intoauto_timestampprovenance.Mutations (
CREATE/SET/DELETE/REMOVE/MERGE) take the Session’s writer lock for the mutation, so concurrentexecute()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_limitcaps 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 underORDER BY) and an explicitLIMIT mmakes the effective capmin(m, row_limit). Truncation is never silent: it warns, andResultView.diagnosticscarriesrow_limitplus the exact pre-truncationtotal_rows. It bounds a mutation’sRETURNrows 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
FrozenGraphsnapshot of the current state.An O(1)
Arcclone that stays stable even if theSessionis 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) orKnowledgeGraph.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 sincebegin(). If another transaction committed in between, a typedKgErroris 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
EXPLAINandPROFILEprefixes.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 ofrow_type(id"<parent_id>:<key value>"), linked from the parent by anedge_typeedge. 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_propertyand compare to the storedhash_property. Whenhash_propertyis None, compare the nanosecondmtime_propertyinstead. 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 bycypher(...).- 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
.kglatpathwould 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_mbrefuses on, so a caller can decide for itself instead of setting a ceiling.- Parameters:
path – Path to a portable
.kglfile. A disk-mode graph directory raisesArgumentError: 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 termdefer_index_rebuild=Trueremoves.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:
FileError –
pathdoes not exist.FileFormatError –
pathis not a readable.kgl.ArgumentError –
pathis a disk-mode graph directory.
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 —
filesentries andcsvshorthands alike — are resolved relative tosettings.root. Afilessection declares each input once by name ({"path": ..., "format": ...}) for specs to reference withfile;"csv": "x.csv"is shorthand for one such entry. The formats it reads arecsv,delimited(any separator, withquote,header,columns,skip_lines,comment_prefix,line_suffix,encodingandprefix_strip),xlsx(one worksheet, withsheet,header_rowandunpivotfor wide matrices) andframe(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 matchMATCH ()-[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 dedupedcount 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
UserWarningobjects, one per entry in the build report, whateververboseis —verbosegoverns 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
filesentry declaring{"format": "frame"}takes nopath; its rows come fromframes["<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
float64column, 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
filesbut absent fromframes=— or passed inframes=without being declared — raisesValueErrornaming it. Acomputeop 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(oroutput_path+output_file), or whenstorage="disk"was given apath— 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=Truedemands one and raises if there is none, so an explicit request to persist never silently does nothing;save=Falsenever saves.- Parameters:
blueprint_path – Path to the blueprint JSON file.
verbose – If True, print progress information during loading.
save –
None(default) saves if a destination exists,Truerequires one,Falsenever 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 requirespath.path – Directory for disk storage (only with
storage="disk").frames – In-memory tables keyed by the name of a
filesentry 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=Truewas 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
.kglbyte buffer.The in-memory counterpart of
load()— deserialises the bytes produced byKnowledgeGraph.to_bytes(). The returned graph has no remembered path (it didn’t come from a file), so a baresave()will require an explicit path.- Parameters:
data – A
.kglbyte buffer fromKnowledgeGraph.to_bytes().storage – As on
load().defer_index_rebuild – As on
load().max_load_mb – As on
load()— a ceiling in megabytes.
- Returns:
A new KnowledgeGraph with the loaded data.
- Raises:
LoadMemoryLimitError – The estimated load exceeds
max_load_mb(orKGLITE_MAX_LOAD_MB). Nothing was decompressed.FileFormatError – If
datais not a valid.kglbuffer (bad magic, truncated, or an incompatible/older format) — a typedkglite.KgErrorsubclass, distinct from a successful load of an empty graph. (kglite.loadraises the same on a corrupt file, orFileErrorwhen the path is missing.)
- kglite.from_networkx(nx_graph: Any, *, default_node_type: str = 'Node', default_edge_type: str = 'RELATED') KnowledgeGraph¶
Build a
KnowledgeGraphfrom anetworkxgraph.Accepts
Graph/DiGraph/MultiGraph/MultiDiGraph. Undirected edges become a single directed edge each. Nodes carrying anode_typeattribute (as produced byKnowledgeGraph.to_networkx()) are grouped by that type, the networkx node key becomes the nodeid, and atitleattribute becomes the title (otherwise the id is used). Edges carrying aconnection_typeattribute (or, for aMultiDiGraph, the edge key) use it as the edge type. Plain networkx graphs getdefault_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 ownnode_typeattribute, 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_graphcoordinates, 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
networkxextra:pip install "kglite[networkx]".- Parameters:
nx_graph – A networkx graph instance.
default_node_type – Node type for nodes lacking a
node_typeattr.default_edge_type – Edge type for edges lacking a
connection_typeattr.
- 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.tagsmembership,UNWIND n.tags). Missing edge endpoints can be vivified as provisional stubs, dropped, or rejected atomically.Spec shape (
recordsare 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
dictor a JSON string.save – If set, save the built graph to this
.kglpath. Withstorage="disk"passpathhere too — the disk build leaves an unpublished working directory until something callssave(), andkglite.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 ownon_missing_endpointkey, 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"— seeset_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 aKnowledgeGraph(best for small/medium). Needs ``pandas`` — the staged CSVs are loaded through DataFrames; a missing pandas raisesImportErrorwith an install hint.out=DIR: stream one CSV per type +manifest.jsonintoDIRin bounded memory (millions of nodes at flat RAM); returns{'nodes', 'edges', 'out'}. Pure Rust — no extra packages needed.
- Parameters:
scale –
tiny|small|medium(default) |large|huge|xhuge— sets the Person count. Ignored ifpersonsis 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
Noneto 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
.kglfile.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"raisesArgumentError— a.kglis 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, unlessKGLITE_DEFER_INDEX_REBUILDis set;True/Falsedecide 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, unlessKGLITE_MAX_LOAD_MBsets one, which this outranks.
- Returns:
A new KnowledgeGraph with the loaded data.
- Raises:
FileError –
pathdoes not exist.FileFormatError –
pathis not a readable.kgl.LoadMemoryLimitError – The estimated load exceeds
max_load_mb(orKGLITE_MAX_LOAD_MB). Nothing was decompressed.ArgumentError –
storageis an unknown mode, or"disk".
``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 nextsave()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, andKnowledgeGraph.list_indexes()/SHOW INDEXESlist them withstate="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 baresave()writes back to it. Seeopen()for load-or-create semantics, oropen_session()to load directly as a thread-safeSession.
- 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 theoxttlfamily).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, GeoSPARQLPOINT→ point; a repeated predicate becomes a list); resource objects become edges; andrdf:typesets the node label (first wins — any extra types are kept in anrdf_typeslist property). Predicate and type IRIs are CURIE-compacted with a__separator (e.g.foaf__knows) using the document’s own@prefixdeclarations 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 auriproperty, andn.idis 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 baresave()(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
.kglfile or a disk-mode directory. Loaded if it exists, otherwise created on firstsave()(or immediately, forstorage="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
.kglsaved 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". Soopen(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 nextsave()records the new mode. The two disk directions have no in-place conversion (a disk graph is a directory, not a file), so they raisekglite.ArgumentErrornaming the alternative (enable_disk_mode()) rather than being ignored — a silently-downgraded mode is indistinguishable from success. Omitstorage=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 topathand defaults todurable="full", whereasKnowledgeGraphtakes nodurableargument and is never durable — it produces a detached graph with nosource_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>-walsidecar, and on open any WAL frames are replayed onto the loaded checkpoint to recover work committed since the lastsave().save()writes a full checkpoint and truncates the log.The level names what a committed mutation survives, using SQLite’s
synchronousvocabulary. These are guarantees, not syscalls — the syscall differs by platform, the guarantee does not:"full"(also spelledTrue, and the default) — survives power loss. One barrier per commit. On macOS that barrier isF_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 lastsave(). No barrier per commit. Callsyncto take an explicit power-safe point without republishing the whole graph."off"(also spelledFalse) — no log. The graph still rememberspathforsave(); a crash loses everything since the last checkpoint.None(default) —"full", except onstorage="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>-walsidecar that still holds commits the checkpoint does not contain raisesValueErrorinstead of returning a graph that is missing them — the nextsave()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 asave()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"raiseValueErrorthere. 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>.locksidecar and holds it untilclose()/with-block exit, so a second process that opens the same path raises instead of silently overwriting your work (see the note below). PassFalseonly when something else already guarantees a single writer — an external supervisor, or a process you have confined to reads.lock=Falseopts 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: agunicornworker pool, a cron job overlapping a request, or a stale process left running. The lease turns it into an error atopen()rather than data loss atsave().Readers are never blocked.
load()andopen_session()take no lease, so any number of processes can read a graph while one writes; they observe the last published snapshot. Onlyopen()— 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 — andsave()itself takes none, so a graph obtained fromload()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>-waland<path>.lock-ownernext 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 withload()/open_session(), which take neither — or, if you needopen()’s save-back binding for a read-mostly handle, passdurable="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=Falsefor bulk loading and for graphs rebuildable from source data, thensave()once at the end; batch many small mutations into one statement (or onebegin()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. Usebegin()when you want discard-on-error, ordurable=Falsefor 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. ASessionalso refuses write queries on a durable graph, because its writes land on a working copy that neither the log norsave()can reach — usecypher()orbegin().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 — includingsave()andsync()on it — raisesValueErrorrather 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()andto_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
pathdirectly as a thread-safeSession.The one-call shortcut for the concurrent-serving case — equivalent to
kglite.load(path).session(). Share the returnedSessionacross a thread pool:Session.cypher()reads run lock-free,Session.execute()writes serialize and compose, andSession.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
KnowledgeGraphfirst:g = kglite.load(path) g.set_embedder(model) s = g.session()
- kglite.outline(graph: KnowledgeGraph, root: Any, edge: str, *, max_depth: int | None = None, body: str | None = None) str¶
Render the spanning tree from
rootalongedgeas 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
rootfollowing outgoingedge-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’sCALL 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
roothas 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
workin a transaction, retrying the whole unit on conflict.workis called aswork(tx)with a freshTransactionand 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 whenworkreturns 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
workreturned on the successful attempt.- Raises:
TransactionConflictError – Every attempt conflicted; the final error is re-raised unchanged.
ValueError –
attemptsis 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): awarning: ...line on stderr, exactly what every earlier release did unconditionally."silent": nothing is printed."pywarn": each warning is raised as aUserWarningthrough thewarningsmodule instead of stderr, so the process’s own warning filters,logging.captureWarnings(True)and a customshowwarningall apply. It replaces rather than supplements the stderr line, becausewarnings.warnalready 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’sfilterwarnings = error) it turns an advisory into a raise out ofcypher().Applies to every Python query path —
KnowledgeGraph.cypher(),Session.cypher()/Session.execute(),Transaction.cypher,FrozenGraph.cypher()— includingto_df=TrueandFORMAT 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 SETmtime_property(a nanosecond UTC RFC 3339 string) and, unlesshash_propertyis 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 withcheck_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
UNWINDoperations for performance.Requires the
neo4jpackage:pip install neo4j.- Parameters:
graph – The KnowledgeGraph to export.
uri – Neo4j connection URI (e.g.
"bolt://localhost:7687").auth – Tuple of
(username, password).Nonefor 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 lazyResultView, afreeze()— 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 inresident_sizeuntil the kernel needs them elsewhere — sops,psutil’srssandresource.getrusagecan all stay flat while the memory has in fact been returned. The number that moves is the process footprint: Activity Monitor’s “Memory” column, orri_phys_footprintfromproc_pid_rusage. On Linux the reclaim isMADV_DONTNEEDand 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