API Reference¶
This document lists the structures, modules, and extension traits exposed through the public issundb library crate.
Core Graph Interface¶
The Graph struct coordinates all transactional graph storage, retrieval, and indexing operations.
Lifecycle Methods¶
Graph::open(path: &Path, map_size_gb: usize) -> Result<Self, Error>
Opens the LMDB database environment at the specified path with a maximum map size limit.Graph::view<F, T>(&self, f: F) -> Result<T, Error>
Executes a read-only transaction inside a closure.Graph::update<F, T>(&self, f: F) -> Result<T, Error>
Executes a read-write transaction inside a closure.Graph::set_thread_count(&self, n: i32) -> Result<(), Error>
Sets the thread count for the parallel read passes, overriding theISSUNDB_NUM_THREADSenvironment variable. Set to0to restore default behavior.Graph::backup(&self, destination: &Path) -> Result<(), Error>
Writes a hot backup snapshot of the database environment to the destination file while the graph stays open.Graph::backup_compact(&self, destination: &Path) -> Result<(), Error>
Writes a compacted backup snapshot, reclaiming free pages during the copy.Graph::restore(snapshot_file: &Path, dst_dir: &Path) -> Result<(), Error>
Restores a backup snapshot into a new database directory. This is an associated function; call it before opening the restored graph.Graph::rebuild_csr(&self) -> Result<(), Error>
Rebuilds the in-memory CSR snapshot immediately instead of waiting for the on-demand refresh; useful before a burst of algorithm calls.
Transactions and Concurrency¶
IssunDB uses a single-writer, multi-reader model.
Writes are serialized through an internal write lock and one LMDB write transaction at a time; every mutation method (and every write Cypher query) commits atomically. Reads never block writers or one another, so read-heavy workloads scale across threads over one shared Graph.
Reader isolation has a boundary worth knowing.
LMDB gives a read transaction a consistent snapshot, so everything inside one Graph::view closure observes a single point in time.
A read that does not go through view, which includes every Cypher query, is not one transaction: each accessor opens its own short read transaction, and the in-memory structures behind the hot read paths (the CSR snapshot and the property columns) are refreshed on their own schedule rather than being tied to a transaction.
A query running concurrently with a committing writer can therefore observe more than one commit point. Reads are always of committed data, never of a partial write, but a whole query is not evaluated against a single snapshot. Wrap the reads in one Graph::view closure when a sequence of reads has to agree with itself.
Graph::view(f)runs the closure inside a read-only transaction (ReadTxn); every read inside it observes one consistent snapshot.Graph::update(f)runs the closure inside a read-write transaction (WriteTxn); the transaction commits when the closure returnsOkand aborts, leaving the database unchanged, when it returnsErr.ReadTxnandWriteTxnexpose the same node, edge, adjacency, and lookup methods asGraph, so multi-step logic can run atomically inside one closure.
The map_size_gb argument to Graph::open sets the maximum size of the LMDB memory map, which bounds the database size. The map is created sparsely, so a generous value costs no disk space up front; a write that would exceed the map fails with a storage error, and the database must be reopened with a larger value to grow it.
Node Management CRUD¶
add_node(label: &str, props: &impl Serialize) -> Result<NodeId, Error>
Adds a new node to the database with a specific label and serializable properties.add_node_multi(labels: &[&str], props: &impl Serialize) -> Result<NodeId, Error>
Adds a new node carrying zero or more labels.get_node(id: NodeId) -> Result<Option<NodeRecord>, Error>
Retrieves a node record by its unique identifier.update_node(id: NodeId, props: &impl Serialize) -> Result<(), Error>
Updates the properties of an existing node in the database.delete_node(id: NodeId) -> Result<(), Error>
Removes a node and all of its incident edges from the graph.
Label Management¶
add_label(id: NodeId, label: &str) -> Result<(), Error>
Adds a label to an existing node; this is a no-op if the node already carries it.remove_label(id: NodeId, label: &str) -> Result<(), Error>
Removes a label from a node; this is a no-op if the node does not carry it.node_labels(id: NodeId) -> Result<Vec<String>, Error>
Returns the list of label names that a node currently carries.
Edge and Adjacency CRUD¶
add_edge(src: NodeId, dst: NodeId, etype: &str, props: &impl Serialize) -> Result<EdgeId, Error>
Creates a directed relationship between two nodes in the graph with specific properties.get_edge(id: EdgeId) -> Result<Option<EdgeRecord>, Error>
Retrieves an edge record by its unique identifier.update_edge(id: EdgeId, props: &impl Serialize) -> Result<(), Error>
Updates the properties of an existing edge.delete_edge(id: EdgeId) -> Result<(), Error>
Deletes a relationship from the graph.out_neighbors(node: NodeId) -> Result<Vec<NeighborEntry>, Error>
Retrieves all outgoing relationships and target neighbors for a given node.in_neighbors(node: NodeId) -> Result<Vec<NeighborEntry>, Error>
Retrieves all incoming relationships and source neighbors for a given node.node_has_relationships(node: NodeId) -> Result<bool, Error>
Checks if a node has any incident (incoming or outgoing) relationships in the graph.
Metadata and Index Queries¶
all_nodes() -> Result<Vec<NodeId>, Error>
Returns all node IDs in the graph in ascending order.all_neighbors(node: NodeId) -> Result<Vec<DirectedNeighborEntry>, Error>
Returns directed neighbor entries for all outgoing and incoming edges of a node.nodes_by_label(label: &str) -> Result<Vec<NodeId>, Error>
Returns all node IDs that carry the specified label.edges_by_type(etype: &str) -> Result<Vec<EdgeId>, Error>
Returns all edge IDs of the specified relationship type.label_name(id: LabelId) -> Result<Option<String>, Error>
Resolves a numeric Label ID back to its string name.type_name(id: TypeId) -> Result<Option<String>, Error>
Resolves a numeric Type ID back to its string name.node_count_by_label(label: &str) -> Result<u64, Error>
Returns the count of nodes carrying the specified label.edge_count_by_type(etype: &str) -> Result<u64, Error>
Returns the count of edges of the specified type.storage_table_stats() -> Result<Vec<TableStat>, Error>
Returns the entry count and size of each of the eleven storage tables (nodes,edges,out_adj,in_adj,label_idx,node_prop_idx,edge_prop_idx,fts_postings,fts_docs,vectors, andmeta). On LMDB the size is the table's pages times the page size, so the eleven sizes sum to the live data in the file without its free-page slack. The CLI'sstatscommand prints this breakdown.
Property Lookups¶
Property values are represented by the PropValue enum with the variants Bool, Int, Float, and Str. Numeric lookups treat 30 and 30.0 as equal, and a range bound only matches values of its own type family (a string value never satisfies a numeric bound).
nodes_by_property(label: &str, property: &str, val: PropValue) -> Result<Vec<NodeId>, Error>
Returns the nodes carrying the label whose property equals the value. Node properties are indexed automatically, so no DDL is required.nodes_by_property_range(label: &str, property: &str, min_val: Option<PropValue>, min_inclusive: bool, max_val: Option<PropValue>, max_inclusive: bool) -> Result<Vec<NodeId>, Error>
Returns the nodes whose property value falls inside the range; either bound may be absent.edges_by_property(etype: &str, property: &str, val: PropValue) -> Result<Vec<EdgeId>, Error>
Returns the edges of the type whose property equals the value. Edge properties are indexed only while a relationship property index exists, so create one first.edges_by_property_range(etype: &str, property: &str, min_val: Option<PropValue>, max_val: Option<PropValue>) -> Result<Vec<EdgeId>, Error>
Range form of the edge lookup; both bounds are inclusive.nodes_by_label_arc(label: &str) -> Result<Arc<Vec<NodeId>>, Error>
The label scan without the copy: a shared sorted id vector served from a per-write-generation cache, so repeated scans of one label between writes cost nothing.nodes_by_labeldelegates here.nodes_prop_cmp_mask(ids: &[NodeId], prop: &str, op: PropCmp, rhs: &Value) -> Result<Option<Vec<bool>>, Error>
Evaluatesprop <op> rhsper id directly against the typed in-memory property column, one keep flag per id, with no boxed value per row.Ok(None)declines (a small request on a cold graph, or a mixed-kind column), and the caller falls back to comparing materialized values.node_prop_group_codes_by_id(prop: &str) -> Result<Arc<IdGroupCodes>, Error>
One shared array over every node,codes[node_id]the node's group code under exact value identity andID_GROUP_ABSENTwhere no such node exists, cached per write generation; the bulk grouping path behind grouped aggregations.
Index and Constraint Management¶
These methods are the Rust equivalents of the Cypher DDL statements in the Cypher DDL Reference; each creation method validates the existing data first and fails if any element already violates the constraint. A label-and-property pair (or type-and-property pair) holds one declaration at a time: an index, a unique constraint, or a required constraint. Creating a second kind on the same pair fails with an error naming the one to drop first, and re-creating the same kind is a no-op.
create_node_property_index(label: &str, property: &str) -> Result<(), Error>anddrop_node_property_index(...)
Declares (or removes) a node property index. Because every scalar node property is auto-indexed, the declaration mainly matters as the anchor for constraints.set_label_auto_index(label: &str, enabled: bool) -> Result<(), Error>,label_auto_index_enabled(label: &str) -> Result<bool, Error>, andlabels_without_auto_index() -> Result<Vec<String>, Error>
The per-label switch on the property auto-index, the Rust form ofDROP AUTO INDEX FOR (n:Label)andCREATE AUTO INDEX FOR (n:Label). Disabling removes the label's auto-index entries and makes lookups on it scan; enabling backfills them. Declared indexes and constraints are unaffected. The CLI'sstatscommand lists the opted-out labels.create_node_unique_constraint(label: &str, property: &str) -> Result<(), Error>anddrop_node_unique_constraint(...)
Requires the property value to be unique across all nodes with the label; explicit nulls never conflict.create_node_required_constraint(label: &str, property: &str) -> Result<(), Error>anddrop_node_required_constraint(...)
Requires the property to be present and non-null on every node with the label.create_edge_property_index,create_edge_unique_constraint, andcreate_edge_required_constraint(with matchingdrop_*methods)
The relationship counterparts, keyed by relationship type instead of label.list_node_indexes_and_constraints() -> Result<Vec<(String, String, u8)>, Error>andlist_edge_indexes_and_constraints() -> ...
Lists the declared indexes and constraints as(label_or_type, property, kind)tuples, where the kind byte is0x00for an index,0x01for a unique constraint, and0x02for a required constraint.
Optimizer Statistics¶
The query optimizer's cardinality statistics are built only when asked for. No ordinary query builds them as a side effect, because each is a full scan
and paying for it on the first query that mentions a property or a relationship pattern was the dominant cold-start cost; the one exception is a bulk
import (COPY ... FROM or IMPORT DATABASE), which ends by building and persisting the node property columns. A process that never calls these plans
every relationship pattern on the global average fan-out and gets no selectivity estimates; the answers are advisory, so plans are weighted differently
but results never change.
materialize_edge_statistics() -> Result<(), Error>
Builds the(label, type)and(src_label, type, dst_label)tables behind the expand-ratio estimates, and upgrades the type-inference pruning pass from a budgeted probe to an exact lookup. One pass over the label index and one over the adjacency, cached until the next committed write. Cheap enough to call freely in a long-lived process.materialize_property_columns() -> Result<(), Error>
Builds the in-memory property columns, which back the selectivity estimates and zone-map pruning. This is one full node scan. It persists the built set as a cache file beside the LMDB files, and a later process memory-maps that file instead of scanning, so only the columns a query reads become resident; a set built in the process itself, or patched after a write, lives on the heap. A repeat at an unchanged write generation rewrites nothing.materialize_edge_property_columns() -> Result<(), Error>
The edge counterpart, with the same contract and its own cache file: one full edge scan, persisted and memory-mapped the same way.
The CLI performs the first of these on every open (pass --no-warm-statistics to skip it), and so do the REST and MCP servers, on a background thread. The
Python and Rust surfaces leave the warm-ups to the caller, with one exception: a bulk import (COPY ... FROM or IMPORT DATABASE) ends by building and
persisting the node property columns, so a process that just imported already has them.
Graph Algorithms¶
Pathfinding, network centrality, and connectivity algorithms run over the in-memory CSR (Compressed Sparse Row) snapshot. Each algorithm automatically refreshes the snapshot cache on demand, making committed mutations immediately visible without manual calls to rebuild_csr().
Traversal and Paths¶
bfs(start: NodeId, hops: u8) -> Result<Vec<NodeId>, Error>
Runs a breadth-first search traversal outward from the start node up to the specified depth.dfs(start: NodeId, hops: u8) -> Result<Vec<NodeId>, Error>
Runs a depth-first search traversal from the start node up to the specified depth.shortest_path(src: NodeId, dst: NodeId) -> Result<Option<Vec<NodeId>>, Error>
Finds the shortest unweighted path between two nodes in the graph.shortest_path_dijkstra(src: NodeId, dst: NodeId) -> Result<Option<WeightedPath>, Error>
Finds the shortest weighted path between two nodes using Dijkstra's algorithm.shortest_path_top_k(src: NodeId, dst: NodeId, k: usize, weight_property: &str) -> Result<Vec<WeightedPath>, Error>
Finds the top-k shortest weighted paths using Yen's algorithm.all_paths(src: NodeId, dst: NodeId) -> Result<Vec<Vec<NodeId>>, Error>
Returns all simple paths between the source and destination nodes.all_shortest_paths(src: NodeId, dst: NodeId) -> Result<Vec<Vec<NodeId>>, Error>
Returns all shortest unweighted paths between two nodes.longest_path(src: NodeId, dst: NodeId) -> Result<Option<Vec<NodeId>>, Error>
Finds the longest simple path between two nodes.
Analytics and Centralities¶
page_rank(iterations: u32, damping: f32) -> Result<HashMap<NodeId, f32>, Error>
Computes PageRank centrality scores across all nodes in the graph.degree_centrality(direction: DegreeDirection) -> Result<HashMap<NodeId, u64>, Error>
Computes the degree centrality for each node based on incoming, outgoing, or combined edges.betweenness_centrality() -> Result<HashMap<NodeId, f64>, Error>
Computes the betweenness centrality score for all nodes.harmonic_centrality() -> Result<HashMap<NodeId, f64>, Error>
Computes the harmonic centrality score for all nodes.closeness_centrality() -> Result<HashMap<NodeId, f64>, Error>
Computes closeness centrality in the Wasserman-Faust form, which scales by the fraction of the graph a node reaches and so stays meaningful on a disconnected graph.eigenvector_centrality(iterations: u32, tolerance: f64) -> Result<HashMap<NodeId, f64>, Error>
Computes eigenvector centrality by power iteration, stopping early on convergence. Scores are magnitudes scaled to sum to the node count.katz_centrality(alpha: f64, beta: f64, iterations: u32, tolerance: f64) -> Result<HashMap<NodeId, f64>, Error>
Computes Katz centrality, which gives every node thebetabaseline and attenuates each walk byalpha. Convergence needsalphabelow the reciprocal of the largest eigenvalue.clustering_coefficient() -> Result<HashMap<NodeId, f64>, Error>
Computes the local clustering coefficient, reading the graph as undirected over distinct neighbors.louvain() -> Result<HashMap<NodeId, u64>, Error>
Detects communities by the Louvain method. The community id is the smallest node id in the community, and only the induced partition is contractual.link_prediction_score(a: NodeId, b: NodeId, metric: LinkPredictionMetric) -> Result<f64, Error>
Scores how likely two nodes are to become connected, by common neighbors, Jaccard, Adamic-Adar, resource allocation, or preferential attachment.
Counting Kernels¶
These methods answer a count over a pattern without materializing its matches. The Cypher optimizer lowers the matching aggregations to them, and they are public so a Rust caller can ask the same questions directly; each takes a spec type re-exported from the facade.
count_linear_paths(spec: &PathCountSpec) -> Result<u64, Error>
Counts the assignments of a one-hop or two-hop directed pattern with optional relationship types, labels, and per-vertex node-id allow-sets. Relationship uniqueness applies across the two hops.grouped_edge_counts(spec: &GroupedDegreeSpec) -> Result<Vec<(NodeId, u64)>, Error>
Counts typed edges grouped by one endpoint, one entry per group node with a non-zero count, in one pass over the adjacency.typed_neighbor_counts(sources: &[NodeId], spec: &NeighborCountSpec) -> Result<Vec<(u64, u64)>, Error>
Per-source(qualifying, counted)neighbor counts across one typed hop, in input order, reading only the sources' own adjacency rows. A source absent from the snapshot counts zero.count_triangle_cycles(spec: &TriangleCountSpec) -> Result<u64, Error>
Counts the assignments of the directed triangle pattern(a)-[t1]->(b)-[t2]->(c)-[t3]->(a)with optional per-hop relationship types and per-variable labels, following Cypher row semantics including relationship uniqueness.
Connectivity and Flow¶
connected_components() -> Result<HashMap<NodeId, u64>, Error>
Finds weakly connected components, mapping each Node ID to its component label.strongly_connected_components() -> Result<HashMap<NodeId, u64>, Error>
Finds strongly connected components in directed graphs.spanning_forest(weight_property: &str, maximum: bool) -> Result<Vec<EdgeId>, Error>
Computes the Minimum or Maximum Spanning Forest (MSF) of the graph.maximum_flow(src: NodeId, dst: NodeId, capacity_property: &str) -> Result<f64, Error>
Computes the maximum flow capacity between two nodes.detect_cycle() -> Result<bool, Error>
Detects if the graph contains any cycles.label_propagation(max_iterations: usize) -> Result<HashMap<NodeId, u64>, Error>
Partitions the graph into communities using the Label Propagation Algorithm.
Vector Search Extensions¶
The VectorGraphExt trait extends the graph with vector embedding storage and similarity search capability.
The index is configured through VectorIndexOptions, which holds a VectorMetric (Cosine, the default, L2, or Dot) and a VectorQuantization (Float32, the default, Float16, or Int8). A build without the hnsw feature accepts and persists a quantization but cannot honor it, since its exact index keeps the raw f32. The configuration is persisted inside the database, so reopening rebuilds the index with the same settings. Configure the index before the first upsert: changing the metric or quantization once vectors exist returns VectorError::AlreadyConfigured, and reindex_vector_index is the explicit way to change settings afterward.
VectorSearchOptions carries k, an optional exact-label filter (label), optional property equality filters (properties), and rescore_factor. On a quantized index a search fetches k * rescore_factor candidates (default factor 2) and re-ranks them by exact distance against the full-precision vectors in storage; pass Some(1) to disable the rescore. A Float32 index never rescores by default, and neither does a build without the hnsw feature, whose exact index keeps the raw f32 and so has no precision to recover whatever quantization was configured.
VectorGraphExt::configure_vector_index(opts: VectorIndexOptions) -> Result<(), VectorError>
Configures the metric and quantization parameters for the graph's vector index.VectorGraphExt::reindex_vector_index(opts: VectorIndexOptions) -> Result<(), VectorError>
Changes the metric and quantization settings and rebuilds the index from the persisted embeddings.VectorGraphExt::upsert_vector(n: NodeId, v: &[f32]) -> Result<(), VectorError>
Stores the embedding for an existing node. A node that does not exist is rejected withVectorError::NodeNotFound, because node ids are handed out monotonically and a vector written ahead of its node would be inherited by whichever node is later allocated that id.VectorGraphExt::remove_vector(n: NodeId) -> Result<(), VectorError>
Removes the embedding for a node from both the index and storage.VectorGraphExt::vector_search(q: &[f32], k: usize) -> Result<Vec<Hit>, VectorError>
Retrieves the top-k nearest neighbor nodes matching the query vector. Searching a graph with no stored embeddings returnsVectorError::EmptyIndexrather than an empty hit list.VectorGraphExt::vector_search_with(q: &[f32], opts: &VectorSearchOptions) -> Result<Vec<Hit>, VectorError>
Retrieves the top-k nearest neighbor nodes satisfying label and property filters. Errors likevector_searchon a graph with no embeddings.VectorGraphExt::node_vector(n: NodeId) -> Result<Option<Vec<f32>>, VectorError>
Returns the full-precision embedding stored for a node, orNoneif the node has no embedding. This performs a storage point lookup and does not build or consult the in-memory index.VectorGraphExt::vector_distance(a: &[f32], b: &[f32]) -> Result<f32, VectorError>
Computes the distance between two vectors under the graph's configured metric.
Full-text Search Extensions¶
The TextIndexExt and TextGraphExt traits enable creating, configuring, and querying full-text search indexes on node properties.
TextSearchOptions controls a search: label and property narrow it to one index (when None, every active index is searched and per-node scores are summed), limit caps the result count (default 10), scorer replaces the default BM25 scorer, and boolean_mode selects candidate filtering. BooleanMode::And restricts results to documents containing every query term; BooleanMode::Or and the default None rank any document matching at least one term.
Stemming and stop words are language-aware. create_text_index uses English; create_text_index_with_language accepts a Language value: English, Spanish, French, German, Italian, or Portuguese.
TextIndexExt::create_text_index(label: &str, property: &str) -> Result<(), TextError>
Creates a full-text search index on a specific node property.TextIndexExt::create_text_index_with_language(label: &str, property: &str, lang: Language) -> Result<(), TextError>
Creates a full-text search index for a specific language.TextIndexExt::drop_text_index(label: &str, property: &str) -> Result<(), TextError>
Removes a full-text search index.TextIndexExt::has_text_index(label: &str, property: &str) -> Result<bool, TextError>
Checks if a full-text search index exists for a label and property.TextIndexExt::list_text_indexes() -> Result<Vec<(String, String, Language)>, TextError>
Lists all active full-text search indexes in the database.TextGraphExt::text_search(query: &str, opts: &TextSearchOptions) -> Result<Vec<TextHit>, TextError>
Queries indexed text fields and ranks matching nodes using BM25 scoring. EachTextHitcarriesnode,score, and thelabelandpropertyof the text index that contributed the hit's largest partial score. A request that cannot match anything errors instead of returning an empty list: an empty query, a label or property filter naming no active index, or a graph with no text indexes at all.
Hybrid Retrieval Extensions¶
Hybrid retrieval functions combine vector search and full-text keyword search with multi-source graph expansion:
retrieve(graph: &Graph, q: &[f32], k: usize, hops: u8) -> Result<Subgraph, RetrievalError>
Runs a vector search to findkseed nodes, then performs a BFS traversal up tohopsdepth to build the result subgraph.retrieve_with(graph: &Graph, q: &[f32], opts: &RetrieveOptions) -> Result<Subgraph, RetrievalError>
Runs a vector search to find seeds with fine-grained control over distance and traversal limits.retrieve_hybrid(graph: &Graph, q: &[f32], text_query: &str, opts: &HybridRetrieveOptions) -> Result<Subgraph, RetrievalError>
Merges seed nodes from vector and full-text keyword searches, fuses their scores, and performs a multi-source expansion. When neither search would run (both inputs empty or both k values zero) it returnsRetrievalError::NoQuery.
The returned Subgraph carries nodes, edges, scores, and truncated. The truncated flag is true when the max_nodes cap cut off seeds or
expansion, so missing edges in a capped result do not mean the returned nodes are unconnected.
Cypher Query Extensions¶
The GraphQueryExt trait provides methods to execute Cypher queries against the database:
query(cypher: &str) -> Result<QueryResult, CypherError>
Executes a raw Cypher query string against the database.query_with_params(cypher: &str, params: &HashMap<String, serde_json::Value>) -> Result<QueryResult, CypherError>
Executes a parameterized Cypher query against the database.query_with_procedures(cypher: &str, params: &HashMap<String, serde_json::Value>, registry: &ProcedureRegistry) -> Result<QueryResult, CypherError>
Executes a Cypher query resolvingCALLclauses against a custom procedure registry.explain(cypher: &str) -> Result<String, CypherError>
Compiles and optimizes the physical query plan, returning it as an indented, human-readable tree.
The supported query language surface is documented on the Cypher Support page.
Query Results and Errors¶
A query returns a QueryResult with columns: Vec<String> and records: Vec<Record>; each Record holds values: Vec<serde_json::Value> aligned row-major with the columns. Nodes, relationships, and paths project as openCypher display-literal strings ((:A:B {k: 1}), [:T {k: 1}], and <(:A)-[:T]->(:B)>, with the arrow carrying the stored edge direction), and missing values project as JSON null. NaN and the two infinities have no JSON number representation, so a float result never collapses them to null (indistinguishable from a missing value); each projects as a tagged sentinel object instead: {"__type__": "__NaN__"}, {"__type__": "__Infinity__"}, or {"__type__": "__-Infinity__"}.
A query string may contain several semicolon-separated top-level statements in one call (for example CREATE (n:Person {name: 'Ada'}) RETURN n.name; MATCH (m) WHERE id(m) = 0 RETURN m.name), which is useful for a CREATE that a later statement in the same call needs to reference. When every statement reads or writes data (no index, constraint, or bulk-load statement), the whole pipeline is one transaction: each statement sees the writes of the ones before it, and an error in any statement rolls back all of them. That is how a REST or MCP caller groups several writes; Cypher has no BEGIN or COMMIT, and a transaction never spans requests. A pipeline that contains a schema or bulk-load statement runs statement by statement, each committing on its own. Every statement runs, but columns/records reflect only the last one: QueryResult carries a statement_count: usize field (always 1 outside this case) so a caller can tell a multi-statement query apart from an ordinary single-statement one instead of silently reading the final statement's result as if it were the whole query.
Each layer has one error type, and all of them implement std::error::Error: Error for storage and domain failures (including the NodeNotFound and EdgeNotFound variants), CypherError for parse, plan, and execution failures, VectorError for vector index failures (including AlreadyConfigured and DimensionMismatch), TextError for full-text index failures, and RetrievalError for hybrid retrieval failures.
Cypher Built-in Procedures¶
Graph data science procedures can be executed through the query interface using CALL issundb.<name>(...). Results are bound to columns using YIELD and can be joined back to nodes using id(). Refer to crates/issundb-examples/gds_cypher.rs or Graph Data Science in Cypher for code examples.
Analytics and Communities¶
CALL issundb.pageRank({iterations, damping})yields(nodeId, score). The configuration map is optional.CALL issundb.betweenness()andCALL issundb.harmonic()yield(nodeId, score). Both take no arguments.CALL issundb.degree({direction})yields(nodeId, score), wheredirectionis'IN','OUT', or'BOTH'(the default).CALL issundb.connectedComponents()(aliasissundb.wcc) andCALL issundb.stronglyConnectedComponents()(aliasissundb.scc) yield(nodeId, componentId).CALL issundb.closeness()andCALL issundb.clusteringCoefficient()yield(nodeId, score). Both take no arguments.CALL issundb.eigenvector({iterations, tolerance})andCALL issundb.katz({alpha, beta, iterations, tolerance})yield(nodeId, score). Both stop early on convergence and never fail on a slow graph; the configuration map is optional.CALL issundb.labelPropagation({maxIterations})yields(nodeId, communityId).CALL issundb.louvain()yields(nodeId, communityId). It separates communities joined by a few edges, which label propagation tends to merge.CALL issundb.communities({maxIterations, topPerCommunity, algorithm})yields(communityId, nodeId, rank), ranking each community by PageRank. Thealgorithmfield selects'labelPropagation'(the default) or'louvain'.
Link Prediction¶
Pairwise scores are scalar functions rather than procedures, because a CALL evaluates its arguments against no bindings and runs once per
statement, so it can never see the two nodes a MATCH bound. Each takes two nodes or node ids, reads the neighborhood as undirected over
distinct neighbors, and returns null when either argument is null.
issundb.link.commonNeighbors(a, b)counts the neighbors the two nodes share.issundb.link.jaccard(a, b)divides shared neighbors by the size of the combined neighborhood.issundb.link.adamicAdar(a, b)weights each shared neighbor by1 / ln(degree); a shared neighbor of degree one contributes nothing.issundb.link.resourceAllocation(a, b)weights each shared neighbor by1 / degree.issundb.link.preferentialAttachment(a, b)multiplies the two degrees, ignoring shared neighbors entirely.
Pathfinding¶
CALL issundb.shortestPath(srcId, dstId)yields(index, nodeId)for the hop sequence, or no rows when the target is unreachable.CALL issundb.dijkstra(srcId, dstId)yields(index, nodeId, totalWeight), using the first present of theweight,cost,capacity, orcapedge property as the edge weight.CALL issundb.triangleCount({relTypes, labels})yields a single(count)row for the directed triangle pattern. The configuration map and each of its fields are optional.
GraphRAG Retrieval¶
CALL issundb.retrieve.vector(queryVector, {k, hops, maxDistance, maxNodes})yields(nodeId, distance). Seed nodes carry a distance; nodes reached only by expansion carry a null distance.CALL issundb.retrieve.hybrid(queryVector, queryText, {vectorK, textK, hops, maxDistance, maxNodes, textLabel, textProperty, vectorLabel, fusion})yields(nodeId, score), fusing vector and full-text relevance before expansion. Thefusionfield is the string'rrf', or a map{rrfK}or{vectorWeight, textWeight}.
For both retrieval procedures the leading vector and text arguments are required and the configuration map is optional.
Cypher Functions¶
The following scalar functions are available inside any Cypher expression position:
vector_dist(node_or_vector, query_vector)
Distance between a node's stored embedding (or a numeric vector) and a query vector, under the graph's configured vector index metric. An ascendingORDER BY vector_dist(node, query)with aLIMITover a labeled scan is answered by a single HNSW index search.issundb.distance.cosine(a, b)andissundb.distance.euclidean(a, b)
Cosine distance (in[0, 2]) and Euclidean (L2) distance (in[0, ∞)) between two vectors. Each argument is a numeric list or a node, in which case its stored embedding is resolved.issundb.similarity.jaccard(a, b)andissundb.similarity.overlap(a, b)
Jaccard similarity and overlap coefficient (both in[0, 1]) between two lists treated as sets.
Each measure has a single canonical form, so the opposite direction is a short inline expression: cosine similarity is 1 - issundb.distance.cosine(a, b), Euclidean similarity is 1.0 / (1.0 + issundb.distance.euclidean(a, b)), and a set distance is 1 - issundb.similarity.jaccard(a, b). A null operand, or a vector length mismatch, yields null.
Cypher DDL Reference¶
Schema statements are executed through the query interface. A DDL statement targets either nodes with a specific label, written (n:Label), or relationships of a specific type, written ()-[r:TYPE]-().
Index Statements¶
CREATE INDEX FOR (n:Label) ON (n.property)
Creates a full-text search index on a node property. Node property equality and range lookups need no DDL because every node property is indexed automatically.CREATE INDEX FOR ()-[r:TYPE]-() ON (r.property)
Creates a relationship property index and backfills it from existing relationships. Relationship properties are indexed only while such an index exists; subsequent relationship creation and property updates keep it current.DROP INDEX FOR (n:Label) ON (n.property)
Removes the full-text search index on a node property.DROP INDEX FOR ()-[r:TYPE]-() ON (r.property)
Removes a relationship property index and its entries.DROP AUTO INDEX FOR (n:Label)andCREATE AUTO INDEX FOR (n:Label)
Turn the property auto-index off or back on for one label. Every scalar node property is indexed automatically, one entry per property per label, which is what makes equality and range lookups work without DDL. Turning it off for a label removes its entries in one pass and makes lookups on that label scan the label instead; declared indexes and constraints on the label keep their entries. Turning it back on backfills the entries. The switch is persisted and applies to nodes written later.
Constraint Statements¶
CREATE CONSTRAINT ON (n:Label) ASSERT n.property IS UNIQUE
Requires the property value to be unique across all nodes with the label.CREATE CONSTRAINT ON (n:Label) ASSERT EXISTS(n.property)
Requires the property to be present and non-null on every node with the label.CREATE CONSTRAINT ON ()-[r:TYPE]-() ASSERT r.property IS UNIQUE
Requires the property value to be unique across all relationships of the type.CREATE CONSTRAINT ON ()-[r:TYPE]-() ASSERT EXISTS(r.property)
Requires the property to be present and non-null on every relationship of the type.
Each CREATE CONSTRAINT form has a matching DROP CONSTRAINT form with the same target and assertion. Creating a constraint validates the existing data first and fails if any element already violates it. Once in place, a constraint is checked when an element is created and when its properties are updated; a violating write fails and leaves the database unchanged. A relationship type and property pair holds one declaration at a time, so a property index and a constraint on the same pair are mutually exclusive; drop one before creating the other.