Skip to main content

Crate falkordb

Crate falkordb 

Source
Expand description

The official Rust client for FalkorDB — a fast, low-latency graph database. One ergonomic API across a blocking client and an async (tokio) client, with typed results, parameter binding, batching, an embedded server, TLS, automatic retries, and OpenTelemetry-aligned tracing and metrics.

§Highlights

  • Sync and async — a blocking client and a tokio async client share the same ergonomic API.
  • Header-aware, typed results — read columns by name or index, or map rows straight into your own serde types.
  • Safe parameters — bind Rust values as Cypher literals; no hand-quoting, no injection.
  • Batching and pipelining — send many queries in a single round-trip.
  • Async streaming — result sets are Streams that compose with the full futures toolbox.
  • Resilient — opt-in RetryPolicy with bounded backoff for transient failures; writes are never retried.
  • Observable — OpenTelemetry-aligned tracing spans and metrics counters/histograms, privacy-safe by default.
  • Replica-aware — opt in to routing read-only queries to replicas behind Redis Sentinel.
  • Embedded server — spin up a self-contained FalkorDB for tests and prototyping, with an optional build-time bundle mode that runs fully offline.

§Table of contents

§Quickstart

§Install

Install it with cargo add:

cargo add falkordb

§Run a FalkorDB server

Docker:

docker run --rm -p 6379:6379 falkordb/falkordb

§Your first query

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};

// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");

let client = FalkorClientBuilder::new()
           .with_connection_info(connection_info)
           .build()
           .expect("Failed to build client");

// Select the social graph
let mut graph = client.select_graph("social");

// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(1, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
            .with_timeout(5000)
            .execute()
            .expect("Failed executing query");

// Each item is a `FalkorResult<Row>`; read columns by index or name.
while let Some(row) = nodes.data.next() {
   let row = row.expect("row failed to parse");
   println!("{:?}", row.get_at(0));
}

§Cargo feature flags

All features are off by default — enable only what you need:

FeatureEnables
tokioThe async client and API on the tokio runtime (multi-threaded scheduler).
serdeMap query results into your own serde::Deserialize types.
tracingOpenTelemetry-aligned tracing spans with a privacy-safe query fingerprint.
metricsCounters and histograms via the metrics facade (install any exporter).
embeddedRun a self-contained embedded FalkorDB server (module downloaded at runtime).
embedded-bundleEmbed the module at build time so the embedded server runs fully offline.
rustls / native-tlsTLS for the sync client, via rustls or native-tls.
tokio-rustls / tokio-native-tlsTLS for the async client.
cargo add falkordb --features tokio,serde

§Guide

Each capability below has a short explanation, a minimal snippet, and a link to a complete, runnable example. The async client mirrors the sync API — await the terminals.

§Queries and results

§Header-aware result rows

QueryResult::data iterates the result set as FalkorResult<Row>. Each Row pairs the query header (the column aliases) with that row’s values, so you read columns by name or index and a row that fails to parse surfaces as an Err instead of being silently swallowed:

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};

let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
    .expect("Invalid connection info");
let client = FalkorClientBuilder::new()
    .with_connection_info(connection_info)
    .build()
    .expect("Failed to build client");
let mut graph = client.select_graph("imdb");

let mut result = graph
    .query("MATCH (m:Movie) RETURN m.title AS title, m.year AS year")
    .execute()
    .expect("Failed executing query");

for row in result.data.by_ref() {
    let row = row.expect("row failed to parse");
    // Read a column by alias and convert it in one step (strictly, via `FromFalkorValue`).
    let title: String = row.try_get("title").expect("title column");
    let year: i64 = row.try_get("year").expect("year column");
    println!("{title} ({year})");
}

Row offers borrowing accessors (get, get_at, get_all), typed accessors (try_get::<T>, try_get_at::<T>), and consuming conversions (into_values, into_map). Typed access is strict — no silent lossy casts — via the FromFalkorValue conversion trait. Because collect short-circuits on the first Err, a whole result set can be gathered with result.data.collect::<falkordb::FalkorResult<Vec<_>>>().

FalkorDB rejects a query whose result columns are not uniquely named, so rows from a query always have distinct columns; if a Row ever does hold duplicates, the access paths are still defined (get/try_get return the first match, get_all returns every match, into_map keeps the last). To opt back into the pre-0.7 behavior (bare Vec<FalkorValue> rows, parse errors collapsed to FalkorValue::Unparseable), call result.data.into_values_lossy(). A runnable version lives in examples/rows.rs. Upgrading from 0.6? See the 0.7 migration guide.

§Type-safe query parameters

Pass Rust values straight into a query — the client encodes them as Cypher literals and escapes them for you, so you never hand-quote strings or risk Cypher injection:

let res = graph
    .query("MATCH (m:Movie {title: $title}) WHERE m.year IN $years RETURN m")
    .with_param("title", "The Matrix")
    .with_param("years", [1999, 2003])
    .execute()?;

Add several at once from an array, Vec, or map with with_params (the values share a single type; use chained with_param calls, as above, for a mix of types):

.with_params([("min_year", 1990), ("max_year", 2000)])

Supported value types include integers, floats, boolean values, strings, Option (encoded as null), arrays/Vec, and string-keyed HashMap/BTreeMap (nested freely). Points and vectors cannot be bound directly (a FalkorDB limitation) — pass the components and construct them in the query:

use std::collections::BTreeMap;
let coords = BTreeMap::from([("latitude", 32.07), ("longitude", 34.79)]);
graph.query("RETURN point($p)").with_param("p", coords).execute()?;

If you really need a raw Cypher expression, with_raw_param("key", "…") is the explicit escape hatch — no escaping is applied to the value (the parameter name is still validated).

Temporal values returned by queries — datetime, date, time/localtime and duration — decode into the typed DateTime, Date, Time and Duration values. Each exposes its scalar as a typed Seconds (value.seconds()), and DateTime/Duration support a small type-safe algebra (DateTime - DateTimeDuration, DateTime ± DurationDateTime, plus Duration add/subtract/negate) with overflow-checked checked_* variants. They are read from results but cannot be bound back as parameters — build them in the query with the matching Cypher function (e.g. date($s)). A runnable version lives in examples/temporal.rs.

§Typed result mapping with serde

Enable the optional serde feature to map query results straight into your own types instead of hand-matching every FalkorValue variant:

cargo add falkordb --features serde

Derive serde::Deserialize on your type and call FalkorValue::deserialize_into (or the free function falkordb::from_falkor_value) on a returned value. A node is deserialized from its properties, and scalars, Option, sequences and maps map onto the matching Rust types:

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Movie {
    title: String,
    year: i64,
    rating: Option<f64>,
}
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
    .expect("Invalid connection info");
let client = FalkorClientBuilder::new()
    .with_connection_info(connection_info)
    .build()
    .expect("Failed to build client");
let mut graph = client.select_graph("imdb");
let mut result = graph.query("MATCH (m:Movie) RETURN m").execute()
    .expect("Failed executing query");
for row in result.data.by_ref() {
    let row = row.expect("row failed to parse");
    if let Some(node) = row.into_iter().next() {
        let movie: Movie = node.deserialize_into().expect("Failed to map node");
        println!("{} ({})", movie.title, movie.year);
    }
}

A runnable version lives in examples/typed_mapping.rs.

To map a whole result set in one shot, call query_as::<T>() before execute(). Each row is deserialized into a T, and the result’s data becomes an iterator of FalkorResult<T>, so it collects directly into a Vec:

let movies: Vec<Movie> = graph
    .query("MATCH (m:Movie) RETURN m")
    .query_as::<Movie>()
    .execute()
    .expect("Failed executing query")
    .data
    .collect::<Result<_, _>>()
    .expect("Failed mapping rows");

A single-column row (such as RETURN m) is deserialized from that one column’s value, so a node maps from its properties and RETURN count(m) maps a scalar. A multi-column row (such as RETURN m.title AS title, m.year AS year) maps each column alias onto the matching struct field, or yields the values in order for a tuple. The query header and stats remain available on the returned result.

§Async

§tokio support

This client supports nonblocking API using the tokio runtime. It can be enabled like so:

cargo add falkordb --features tokio

Currently, this API requires running within a multi_threaded tokio scheduler, and does not support the current_thread one, but this will probably be supported in the future.

The API uses an almost identical API, but the various functions need to be awaited:

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
use futures::StreamExt; // brings `.next().await` onto the result stream

// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");

let client = FalkorClientBuilder::new_async()
            .with_connection_info(connection_info)
            .build()
            .await
            .expect("Failed to build client");

// Select the social graph
let mut graph = client.select_graph("social");

// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(1, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
            .with_timeout(5000)
            .execute()
            .await
            .expect("Failed executing query");

// `nodes.data` is a `Stream<Item = FalkorResult<Row>>`; pull rows with `.next().await`:
while let Some(row) = nodes.data.next().await {
     let row = row.expect("row failed to parse");
     println!("{:?}", row.get_at(0));
}

The result set (nodes.data) is an owned, Send + 'static Stream, so it can be moved into a spawned task and driven with the full StreamExt / TryStreamExt toolbox. The graph handle itself is Send + Clone: cloning is cheap and shares one schema cache, so to use a graph from several concurrent tasks you just clone it — no Arc<Mutex<_>> wrapping required.

§Async streaming

Because results are a Stream, the standard combinators just work. Import the extension traits (use futures::{StreamExt, TryStreamExt};) and:

use futures::{StreamExt, TryStreamExt};
// Collect a typed stream in one line (errors short-circuit):
let years: Vec<i64> = graph
    .query("MATCH (m:Movie) RETURN m.year AS year ORDER BY year")
    .execute()
    .await?
    .data
    .map(|row| row?.try_get::<i64>("year"))
    .try_collect()
    .await?;
// Move a result stream into its own task (it is `Send + 'static`):
let mut stream = graph.query("MATCH (n) RETURN n").execute().await?.data;
let count = tokio::spawn(async move {
    let mut n = 0usize;
    while let Some(row) = stream.next().await {
        row?;
        n += 1;
    }
    Ok::<_, falkordb::FalkorDBError>(n)
})
.await
.unwrap()?;
// Fan out a follow-up query per row with bounded concurrency, over cloned handles:
let enriched: Vec<i64> = graph
    .query("MATCH (m:Movie) RETURN m.year AS year")
    .execute()
    .await?
    .data
    .map(|row| {
        let mut g = graph.clone(); // cheap; shares the schema cache
        async move {
            let year: i64 = row?.try_get("year")?;
            let mut r = g.query(format!("RETURN {year} + 1 AS next")).execute().await?;
            r.data.try_next().await?.expect("a row").try_get::<i64>("next")
        }
    })
    .buffer_unordered(8)
    .try_collect()
    .await?;

A runnable version lives in examples/async_stream.rs.

§Connection strategy and multiplexing

The asynchronous client chooses how it manages its underlying Redis connections via a ConnectionStrategy:

  • Multiplexed (the async default): a small number of shared, cloneable, auto-reconnecting connections. Many concurrent commands are pipelined over each socket, so a single connection can carry many in-flight requests at once. This avoids the borrow/return bottleneck and is the most efficient option for highly concurrent workloads.
  • Pooled: a fixed pool of independent connections, each used by exactly one command at a time (borrow/return). This gives strict per-command isolation and a natural cap on in-flight commands. It is the only strategy for the synchronous client.

Select or tune the strategy on the builder:

use falkordb::{ConnectionStrategy, FalkorClientBuilder};
use std::num::NonZeroU8;

// Spread commands across 4 shared multiplexed sockets (the default uses 8).
let client = FalkorClientBuilder::new_async()
    .with_connection_strategy(ConnectionStrategy::Multiplexed {
        connections: NonZeroU8::new(4).unwrap(),
    })
    // Optional backpressure: cap concurrently in-flight commands per socket.
    .with_max_inflight(std::num::NonZeroUsize::new(256).unwrap())
    .build()
    .await
    .expect("Failed to build client");

assert_eq!(client.connection_pool_size(), 4);

Notes and caveats:

  • Behavior change: the async default is now multiplexed (previously an exclusive borrow-pool). The API is source-compatible; with_num_connections now sets the number of underlying connections/sockets for the active strategy, and connection_pool_size() reports that count.
  • Backpressure: multiplexed mode does not bound the number of outstanding requests unless you set with_max_inflight(n) (where n is a NonZeroUsize; ignored by the pooled strategy, whose pool size already caps in-flight commands).
  • Sentinel: a multiplexed connection built from a Sentinel-resolved node would not re-resolve the master/replica on failover, so for Sentinel deployments the client transparently falls back to the pooled strategy (which re-resolves on reconnect). connection_strategy() returns this effective strategy.

A runnable example is provided in examples/multiplexed_async.rs.

§Execution patterns

§Batch and pipelined execution

Normally each query is one network round-trip. graph.batch() queues several queries and sends them over a single Redis pipeline in one round-trip, returning one result per query in submission order. Queue queries with query (a GRAPH.QUERY) / ro_query (a GRAPH.RO_QUERY) and set per-query parameters on the returned handle:

let mut batch = graph.batch();
for movie in &movies {
    batch.query("CREATE (:Movie {title: $t})").with_param("t", movie);
}
batch.ro_query("MATCH (m:Movie) RETURN count(m) AS n");

let results = batch.execute()?; // Vec<BatchItemResult>, one per query, in order
for (i, item) in results.into_iter().enumerate() {
    match item {
        Ok(result) => { /* result.data: Vec<Row>, result.header, result.stats */ }
        Err(err) => eprintln!("query {i} failed: {err}"),
    }
}

On the async client it is identical but for the await:

let mut batch = graph.batch();
// … queue queries …
let results = batch.execute().await?;

Key points:

  • Per-item errors. A failing query (bad Cypher, or a parameter that can’t be encoded) becomes that slot’s Err; the other queries are unaffected. The outer Result only fails if the whole batch could not be completed — and if that happens after the pipeline was sent, the server may have run some or all queries (the state is unknown), which matters for writes.
  • Not a transaction. A pipeline is not MULTI/EXEC: every queued query is executed, so a failure in one does not roll back or stop the others.
  • Results are eager. Each query’s rows are parsed up front into a Vec<Row> (the same Row as elsewhere), since many result sets coexist in one batch.
  • Owned queries. To build queries ahead of time, construct BatchQuery::write(..) / BatchQuery::read(..), attach params/timeout, and batch.push(query).

A runnable version lives in examples/batch.rs.

§Waiting for background operations

Some FalkorDB operations finish after the command that starts them returns: when you create or drop an index or constraint, the request returns immediately while the index is populated (or the constraint is enforced) on a background worker thread, and GRAPH.COPY can fail transiently while the server is unable to fork. The eager methods (create_index, create_unique_constraint, copy_graph, …) stay fire-and-forget, but every one of them now has an additive *_op builder that adds explicit, opt-in waiting while keeping full backward compatibility.

Each builder offers .execute() (non-blocking, identical to the eager method) and .wait() / .wait_with(WaitOptions) terminals. For index and constraint builders, .wait() blocks until the operation has actually taken effect (the index/constraint becomes operational or is dropped), returning FalkorDBError::Timeout if it does not happen in time. For the copy builder, GRAPH.COPY is already blocking on the server, so .wait() simply retries transient could not fork failures with backoff; it does not verify the copied contents (that remains the caller’s responsibility).

use falkordb::{EntityType, FalkorClientBuilder, FalkorConnectionInfo, IndexType, WaitOptions};
use std::time::Duration;

let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");
let client = FalkorClientBuilder::new()
           .with_connection_info(connection_info)
           .build()
           .expect("Failed to build client");
let mut graph = client.select_graph("social");

// Fire-and-forget, exactly like `create_index` (returns as soon as the server accepts it):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["age"], None)
     .execute()
     .expect("Failed to request index creation");

// Block until the index is actually operational (default 30s readiness timeout):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["name"], None)
     .wait()
     .expect("Index did not become operational");

// A unique constraint reports a *distinct* error if existing data violates it:
match graph.create_unique_constraint_op(EntityType::Node, "Person", &["email"])
           .wait_with(WaitOptions::with_timeout(Duration::from_secs(10)))
{
    Ok(()) => println!("constraint is enforced"),
    Err(falkordb::FalkorDBError::ConstraintFailed { .. }) => println!("data violates the constraint"),
    Err(other) => panic!("unexpected error: {other}"),
}

// Copy a graph, retrying transient `could not fork` failures:
let _copy = client.copy_graph_op("social", "social_backup")
                  .wait()
                  .expect("Failed to copy graph");

The same builders exist on the async client — just await the terminals. See examples/waiting_ops.rs for a complete, runnable example.

For vector indexes, the typed helpers create_node_vector_index / create_edge_vector_index take a dimension and a VectorSimilarity (Euclidean or Cosine) and generate the correct OPTIONS { dimension: N, similarityFunction: '…' } clause for you. Like the other index operations they are fire-and-forget, and they have matching create_node_vector_index_op / create_edge_vector_index_op builders that integrate with the waiting ergonomics above — .wait() blocks until the vector index is operational (and .execute() is the non-blocking equivalent). A runnable version lives in examples/vector_index.rs.

§Connections and networking

§TLS support

This client is currently built upon the redis crate, and therefore supports TLS using its implementation, which uses either rustls or native_tls. This is not enabled by default, and the user just opt-in by enabling the respective features: "rustls"/"native-tls" ( when using tokio: "tokio-rustls"/"tokio-native-tls").

For Rustls:

cargo add falkordb --features rustls
cargo add falkordb --features tokio-rustls

For Native TLS:

cargo add falkordb --features native-tls
cargo add falkordb --features tokio-native-tls

A runnable example is provided in examples/tls.rs.

§TCP keepalive

Long-lived clients behind NATs, stateful firewalls, or idle-timeout-enforcing proxies can silently lose their TCP sessions. The builder exposes TCP-level socket settings to prevent this:

use falkordb::FalkorClientBuilder;
use std::time::Duration;

// Convenience: just enable keepalive with a 30-second idle timeout
let client = FalkorClientBuilder::new()
    .with_tcp_keepalive(Duration::from_secs(30))
    .build()
    .expect("Failed to build client");

// Or full control via redis::io::tcp::TcpSettings
let settings = redis::io::tcp::TcpSettings::default()
    .set_nodelay(true)
    .set_keepalive(
        redis::io::tcp::socket2::TcpKeepalive::new()
            .with_time(Duration::from_secs(60)),
    );
let client = FalkorClientBuilder::new()
    .with_tcp_settings(settings)
    .build()
    .expect("Failed to build client");

Note: TCP settings apply to direct Redis TCP connections only. Unix-domain socket / embedded connections and the Sentinel connection path are not affected.

§Read-only queries and replica routing

Read-only queries (ro_query and call_procedure_ro) send GRAPH.RO_QUERY, which the server refuses to let write. Where such a query runs is a separate, opt-in choice expressed with ReadPreference. Because a FalkorDB replica applies writes only after the primary, a read served from a replica can be slightly stale, so the default (ReadPreference::Primary) keeps every read on the primary — you never observe replication lag unless you ask for it.

Opt into replicas either per client or per query:

  • Per clientwith_read_preference sets the default for every read-only query.
  • Per queryprefer_replica opts a single query in, and primary_only forces one back onto the primary (read-your-writes). The per-query choice overrides the client default.

Replica routing requires a Redis Sentinel deployment that exposes readable replicas; when none is available (for example a single node), ReadPreference::PreferReplica transparently falls back to the primary, so the same code runs everywhere. Writes always go to the primary — asking for a replica on a writable query/call_procedure/batch fails with FalkorDBError::ReadPreferenceNotReadOnly.

Connection pool sizing: When readable replicas are present the client opens a second pool of up to num_connections additional connections (one per slot) alongside the primary pool, regardless of the read preference. Size your pool limits and file-descriptor limits accordingly.

use falkordb::{FalkorClientBuilder, ReadPreference};

let client = FalkorClientBuilder::new()
    // A Sentinel endpoint, e.g. falkor://127.0.0.1:26379
    .with_connection_info("falkor://127.0.0.1:26379".try_into().expect("Invalid connection info"))
    // Prefer replicas for this client's read-only queries (accepts slightly stale reads).
    .with_read_preference(ReadPreference::PreferReplica)
    .build()
    .expect("Failed to build client");

// Capability (a replica pool exists) vs policy (the default routing).
if client.replica_reads_available() {
    println!("Replica connections are available");
}
println!("Default read preference: {:?}", client.read_preference());

let mut graph = client.select_graph("imdb");

// Writes go to the primary.
graph.query("CREATE (:Actor {name: 'Tom Hanks'})").execute().expect("Failed to write");

// Follows the client default (a replica when available, else the primary).
let mut nodes = graph.ro_query("MATCH (a:Actor) RETURN a.name").execute().expect("Failed to read");

// Force the freshest data from the primary for a single read, overriding the default.
let mut fresh = graph.ro_query("MATCH (a:Actor) RETURN a.name").primary_only().execute().expect("Failed to read");

Against a single node (or any deployment without readable replicas), replica_reads_available returns false and reads use the primary. See examples/readonly_replica.rs for a complete working example.

§Resilience and observability

§Automatic retries

A client can opt in to a RetryPolicy that automatically re-issues eligible operations on transient connection failures, with bounded backoff. It is disabled by default, so a client built without one behaves exactly as before (every operation is attempted once):

use falkordb::{Backoff, FalkorClientBuilder, RetryPolicy};
use std::time::Duration;

let client = FalkorClientBuilder::new()
    .with_retry_policy(
        RetryPolicy::read_only()                         // retry read-only ops only
            .max_attempts(4)                             // 1 initial try + up to 3 retries
            .backoff(Backoff::exponential(Duration::from_millis(50))
                .max_delay(Duration::from_secs(1))),     // 50ms, 100ms, 200ms, … capped at 1s
    )
    .build()?;

The same with_retry_policy(..) is available on the async builder (FalkorClientBuilder::new_async()).

Write safety. The only scope available today, RetryScope::ReadOnly, retries read-only / idempotent operations only (ro_query, explain, list_indices, list_constraints, read-only procedure calls). Writes are never retried, so enabling a policy can never duplicate a write. Classification is by the API you call, never by inspecting Cypher — query() is treated as a write even when it only reads, so use ro_query() for retryable reads.

Only transient connection errors are retried (a dropped/unavailable connection, or a Sentinel resolution failure); deterministic errors (syntax, constraint violations, parse/type errors, wait-operation timeouts) are returned immediately. Retries compose with the client’s existing connection healing: each attempt re-borrows a connection, so a recovered connection is picked up on the next try.

Scope. Retry currently wraps query and procedure executionro_query, query, explain, profile, call_procedure/call_procedure_ro, and list_indices/list_constraints (only the read-only ones are eligible). Direct client/admin calls (list_graphs, configuration getters/setters, slowlog, server INFO) and the internal schema-cache refresh that can run while a result is parsed are not wrapped yet, so a transient failure there still surfaces even with a policy enabled. Broadening the coverage is a planned follow-up.

See examples/retry.rs for a complete, runnable example.

§Tracing

This crate fully supports instrumentation using the tracing crate, to use it, simply, enable the tracing feature:

cargo add falkordb --features tracing

Note that different functions use different filtration levels, to avoid spamming your tests, be sure to enable the correct level as you desire it.

When the tracing feature is enabled, the query- and procedure-execution spans are enriched with structured, low-cardinality fields you can slice and filter on (named after the OpenTelemetry database conventions so they map cleanly when exported via tracing-opentelemetry):

FieldExampleMeaning
db.system.namefalkordbconstant
db.namespacesocialthe graph name
db.operation.nameGRAPH.RO_QUERY / db.idx.fulltext.queryNodesthe command or procedure
db.falkordb.read_onlytruewhether the operation is read-only
db.falkordb.strategymultiplexedthe active connection strategy
db.query.fingerprinta1b2c3d4e5f60718a privacy-safe hash of the query shape
error.typeconnection_downa bounded error kind, recorded on failure
db.response.returned_rows42rows the server returned (on the outer execute span)
db.falkordb.server_time_ms1.18the server’s internal execution time, when reported

Privacy by default. The raw query text and parameter values are never recorded by default — only the db.query.fingerprint, which is a hash of the query with all literals (strings, numbers, true/false/null) redacted, so two queries that differ only in their values share a fingerprint and no value ever enters a span. If you need the raw Cypher for debugging in a trusted environment, opt in explicitly:

use falkordb::FalkorClientBuilder;

let client = FalkorClientBuilder::new()
    .with_query_logging(true) // records `db.query.text`; off by default
    .build()?;

Parameter values supplied via with_param are never recorded even when query logging is enabled (they live in the query preamble, not the query text).

Note: the async query/procedure futures are deeply nested (retry + instrumentation). If you tokio::spawn them with the tracing feature enabled and hit a recursion limit / overflow evaluating ... Send error, add #![recursion_limit = "256"] to your crate root — the standard fix for deep async + tracing stacks.

See examples/observability.rs for a complete, runnable example.

§Metrics

Enable the metrics feature to emit counters and histograms through the metrics facade, so your application can install any exporter (Prometheus, OpenTelemetry, …):

cargo add falkordb --features metrics

Each query and procedure execution records:

MetricTypeLabels
falkordb_queries_totalcountercommand, operation (read/write), strategy
falkordb_query_duration_secondshistogramcommand, operation
falkordb_query_errors_totalcountercommand, error_kind
falkordb_retries_totalcounteroperation, error_kind
falkordb_connections_in_flightgaugeroute (primary/replica)
falkordb_connection_pool_wait_secondshistogramroute (pooled strategy only)

All labels are bounded, low-cardinality values: command is an allowlist of known commands (unknown ⇒ other), operation/strategy/error_kind are small fixed sets. The graph name, query text, and query fingerprint are never used as labels (they are unbounded and would explode metric cardinality) — those belong on tracing spans, not metrics. Like tracing, recording is a no-op until you install a recorder; for example, with metrics-exporter-prometheus:

let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
builder.install().expect("failed to install Prometheus recorder");
// ... use the client; metrics are now exported on the configured endpoint.
§Actionable error hints

FalkorDBError::mitigation_hint() turns common, recognizable failures into a short, actionable remediation tip — handy for logs and AI tooling. It is purely additive: the raw error and its Display/Debug output are unchanged, hints are fixed &'static strs (so they never echo text from the underlying message), and unrecognized errors return None.

use falkordb::FalkorDBError;

let err = FalkorDBError::ConnectionDown;
if let Some(hint) = err.mitigation_hint() {
    println!("hint: {hint}");
}

§Embedded server

This client supports running an embedded FalkorDB server, which is useful for:

  • Testing without external dependencies
  • Embedded applications
  • Quick prototyping and development

To use the embedded feature, enable it:

cargo add falkordb --features embedded
§Choosing a module-provisioning mode

The redis-server binary is never downloaded — it must be installed on the host (see Requirements). Only the FalkorDB falkordb.so module is provisioned, and there are two features for that:

  • embeddedruntime download. The module is downloaded on first start (and cached), so the running process needs network access the first time. Best for development.

  • embedded-bundlebuild-time embed, offline at runtime. A build.rs fetches the module for the build target at compile time and embeds it in your binary, so the running process needs no network at all. Best for network-isolated deployments. Enable it instead of embedded:

    cargo add falkordb --features embedded-bundle

    Control the bundled module at build time with environment variables: FALKORDB_EMBEDDED_MODULE_VERSION (release tag; defaults to the pinned version), FALKORDB_EMBEDDED_MODULE_PLATFORM (override the asset for distro-specific Linux targets such as rhel9-x64), and FALKORDB_EMBEDDED_MODULE_PATH to embed a local .so instead of downloading (fully offline builds, or unsupported platforms). A non-default version must be accompanied by FALKORDB_EMBEDDED_MODULE_SHA256 — unchecked downloaded native code is never embedded. The downloading build uses the host curl (set FALKORDB_EMBEDDED_MODULE_PATH on build hosts without curl or network access); the embedded-bundle runtime itself carries no HTTP/hashing dependencies.

    License: embedded-bundle embeds the SSPL-licensed FalkorDB module into your binary, so you are responsible for complying with its license when you distribute that binary.

§Requirements
  • redis-server (version 8.0 or newer) must be installed and available in PATH (or you can specify a custom path). It is not downloaded automatically — install it from your package manager (e.g. brew install redis, apt-get install redis-server).
  • The falkordb.so module is provisioned automatically: downloaded at runtime with embedded (when auto_download is enabled, the default) or embedded at build time with embedded-bundle. You can also point falkordb_module_path at an existing module, or disable auto_download to use only explicit/system-installed binaries.
  • On macOS the module requires OpenMP: brew install libomp.

Supported platforms: Linux x86_64/aarch64 (glibc and musl/Alpine, plus RHEL 8/9 and Amazon Linux 2023 on x86_64) and macOS aarch64 (Apple Silicon).

§Self-contained vs. already-installed
use falkordb::EmbeddedConfig;
use std::path::PathBuf;

// Self-contained (default): download + cache the module if it is missing.
let _auto = EmbeddedConfig::default();

// Offline: use only binaries already on the machine (no network access).
let _offline = EmbeddedConfig {
    auto_download: false,
    falkordb_module_path: Some(PathBuf::from("/usr/lib/redis/modules/falkordb.so")),
    ..Default::default()
};

The cache directory defaults to ~/.cache/falkordb-rs (Linux) or ~/Library/Caches/falkordb-rs (macOS) and can be overridden with the cache_dir field or the FALKORDB_RS_CACHE_DIR environment variable.

§Usage Example
use falkordb::{EmbeddedConfig, FalkorClientBuilder, FalkorConnectionInfo};

// Create an embedded configuration with defaults
let embedded_config = EmbeddedConfig::default();

// Or customize the configuration:
// let embedded_config = EmbeddedConfig {
//     redis_server_path: Some(PathBuf::from("/path/to/redis-server")),
//     falkordb_module_path: Some(PathBuf::from("/path/to/falkordb.so")),
//     db_dir: Some(PathBuf::from("/tmp/my_falkordb")),
//     falkordb_version: None, // pin a different release, e.g. Some("v4.18.10".into())
//     cache_dir: None,        // override the download cache location
//     ..Default::default()
// };

// Build a client with embedded FalkorDB
let client = FalkorClientBuilder::new()
    .with_connection_info(FalkorConnectionInfo::Embedded(embedded_config))
    .build()
    .expect("Failed to build client");

// Use the client normally
let mut graph = client.select_graph("social");
graph.query("CREATE (:Person {name: 'Alice', age: 30})").execute().expect("Failed to execute query");

// The embedded server will be automatically shut down when the client is dropped

The embedded server:

  • Spawns a redis-server process with the FalkorDB module loaded
  • Uses Unix socket for communication (no network port)
  • Automatically cleans up when the client is dropped
  • Can be configured with custom paths, database directory, and socket location

§Examples

Every example is a runnable file under examples/ and is compiled in CI. Run one with cargo run plus the flags shown:

ExampleShowsRun with
basic_usageA minimal connect, query, and iterate flow--example basic_usage
rowsHeader-aware rows: read columns by name or index with strict typed access--example rows
typed_paramsType-safe, injection-proof query parameters--example typed_params
typed_mappingMap query results into your own serde types--features serde --example typed_mapping
temporalDecode temporal values and use the type-safe DateTime/Duration algebra--example temporal
vector_indexCreate vector indexes with the typed helpers and VectorSimilarity--example vector_index
batchBatch / pipelined execution: many queries in one round-trip--example batch
waiting_opsWait for background index / constraint / copy operations to take effect--example waiting_ops
udf_usageLoad a user-defined-function (UDF) library--example udf_usage
async_apiThe async (tokio) client end to end--features tokio --example async_api
async_streamAsync streaming with futures combinators--features tokio --example async_stream
multiplexed_asyncThe multiplexed async connection strategy--features tokio --example multiplexed_async
readonly_replicaRoute read-only queries to replica nodes--example readonly_replica
retryThe opt-in retry policy for transient failures--example retry
tlsConnect to FalkorDB over TLS--features rustls --example tls
observabilitytracing span enrichment and the query fingerprint--features tracing --example observability
embedded_usageRun an embedded FalkorDB server--features embedded --example embedded_usage

§API documentation

The complete API reference is published on docs.rs.

§Migration guides

§Contributing

Development setup, the full just recipe reference, and how to run the tests and benchmarks live in CONTRIBUTING.md.

§Community and license

Licensed under the MIT License.

Structs§

AsyncConstraintOpBuilder
Builder for creating or dropping a constraint on an AsyncGraph.
AsyncCopyGraphBuilder
Builder for copying a graph via a FalkorAsyncClient.
AsyncGraph
The main graph API, this allows the user to perform graph operations while exposing as little details as possible.
AsyncIndexOpBuilder
Builder for creating or dropping an index on an AsyncGraph.
Backoff
The backoff schedule applied between retry attempts.
BatchBuilder
Accumulates queries to run as a single pipelined batch. Create one with graph.batch().
BatchQuery
A single, graph-less query to run as part of a BatchBuilder. Owns its query text, parameters and timeout, so queries can be built up front (e.g. in a loop) and pushed into a batch.
Constraint
A constraint applied on all ‘properties’ of the graph entity ‘label’ in this graph
ConstraintOpBuilder
Builder for creating or dropping a constraint on a SyncGraph.
CopyGraphBuilder
Builder for copying a graph via a FalkorSyncClient.
Date
A FalkorDB date value (compact type marker 14).
DateTime
A FalkorDB datetime value (compact type marker 13): seconds since the Unix epoch (UTC).
Duration
A FalkorDB duration value (compact type marker 16): a span expressed in seconds.
Edge
An edge in the graph, representing a relationship between two Nodes.
EmbeddedConfig
Configuration for an embedded FalkorDB server instance.
EmbeddedServer
Manages an embedded FalkorDB server instance.
ExecutionPlan
An execution plan, allowing access both to the human-readable text representation, access to a per-operation map, or traversable operation tree
FalkorAsyncClient
This is the publicly exposed API of the asynchronous Falkor Client It makes no assumptions in regard to which database the Falkor module is running on, and will select it based on enabled features and url connection
FalkorClientBuilder
A Builder-pattern implementation struct for creating a new Falkor client.
FalkorIndex
Contains all the info regarding an index on the database
FalkorParams
An ordered, name-deduplicated set of query parameters.
FalkorSyncClient
This is the publicly exposed API of the sync Falkor Client It makes no assumptions in regard to which database the Falkor module is running on, and will select it based on enabled features and url connection
FalkorValueDeserializer
A serde::Deserializer that consumes an owned FalkorValue.
GraphSchema
A struct containing the various schema maps, allowing conversions between ids and their string representations.
IndexOpBuilder
Builder for creating or dropping an index on a SyncGraph.
LazyResultSet
A wrapper around the returned raw data, parsing each row on demand into a header-aware Row.
Node
A node in the graph, containing a unique id, various labels describing it, and its own property.
Path
Represents a path between two nodes, contains all the nodes, and the relationships between them along the path
Point
A point in the world.
ProcedureQueryBuilder
A Builder-pattern struct that allows creating and executing procedure call on a graph
QueryBuilder
A Builder-pattern struct that allows creating and executing queries on a graph
QueryResult
A response struct which also contains the returned header and stats data
RawParam
A raw, already-valid Cypher expression to use as a parameter value, without any escaping.
RetryPolicy
An opt-in, client-wide policy describing how many times, how fast, and which operations to re-issue on transient connection failures.
Row
A single result row: the query’s column names (the header) paired with this row’s values.
RowStream
The owned result set produced by an AsyncGraph query.
Seconds
A signed number of seconds — the scalar carried by every FalkorDB temporal value.
SlowlogEntry
A slowlog entry, representing one of the N slowest queries in the current log
SyncGraph
The main graph API, this allows the user to perform graph operations while exposing as little details as possible.
Time
A FalkorDB time / localtime value (compact type marker 15), expressed in seconds.
TypedLazyResultSet
A typed view over a LazyResultSet that deserializes each row into T on demand.
TypedRowStream
A typed view over a RowStream that deserializes each row into T on demand.
WaitOptions
Tunables for how a wait terminal polls/retries a background operation.

Enums§

ConfigValue
An enum representing the two viable types for a config value
ConnectionStrategy
Selects how a client manages its underlying Redis connection(s).
ConstraintStatus
The status of this constraint
ConstraintType
The type of restriction to apply for the property
EntityType
Whether this element is a node or edge in the graph
FalkorConnectionInfo
An agnostic container which allows maintaining of various connection details. The different enum variants are enabled based on compilation features
FalkorDBError
A verbose error enum used throughout the client, messages are static string slices. this allows easy error integration using thiserror
FalkorValue
An enum of all the supported Falkor types
IndexStatus
The status of this index
IndexType
The type of this indexed field
ReadPreference
Controls which node a read-only query may be served from.
RetryScope
Which operations an enabled RetryPolicy is allowed to re-issue.
SchemaType
An enum specifying which schema type we are addressing When querying using the compact parser, ids are returned for the various schema entities instead of strings Using this enum we know which of the schema maps to access in order to convert these ids to strings
VectorSimilarity
The similarity function a vector index uses when comparing vectors.
WaitOperation
Identifies which background operation a crate::FalkorDBError::Timeout refers to.

Traits§

FromFalkorValue
A type that can be extracted from a FalkorValue.
IntoFalkorParam
A Rust value that can be encoded as a single FalkorDB/Cypher query-parameter literal.
IntoFalkorParams
A collection that can be turned into a set of query parameters for QueryBuilder::with_params.

Functions§

from_falkor_row
Deserialize a single result row into any type that implements serde::Deserialize.
from_falkor_value
Deserialize a FalkorValue into any type that implements serde::Deserialize.
to_cypher_param
Encode a single value as a Cypher parameter literal.

Type Aliases§

BatchItemResult
The result of one query in a batch: Ok with its QueryResult (rows eagerly parsed into a Vec<Row>), or Err with that query’s server / encoding / parse error. A failure here does not affect the other queries in the batch.
BatchResult
The result of BatchBuilder::execute: one BatchItemResult per query, in submission order.
FalkorResult
A Result which only returns FalkorDBError as its E type