[](https://github.com/falkordb/falkordb-rs/releases/latest)
[](https://crates.io/crates/falkordb)
[](https://github.com/FalkorDB/falkordb-rs?tab=License-1-ov-file)\
[](https://github.com/FalkorDB/falkordb-rs/issues)
[](https://github.com/FalkorDB/falkordb-rs)
[](https://codecov.io/gh/falkordb/falkordb-rs)
[](https://docs.rs/falkordb/latest/falkordb/)\
[](https://github.com/orgs/FalkorDB/discussions)
[](https://discord.com/invite/6M4QwDXn2w)
# falkordb-rs
[](https://app.falkordb.cloud)
### FalkorDB Rust client
## Usage
### Installation
Just add it to your `Cargo.toml`, like so:
```toml
falkordb = { version = "0.3.0" }
```
### Run FalkorDB instance
Docker:
```sh
docker run --rm -p 6379:6379 falkordb/falkordb
```
### Code Example
```rust,no_run
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(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
.with_timeout(5000)
.execute()
.expect("Failed executing query");
// Can also be collected, like any other iterator
while let Some(node) = nodes.data.next() {
println ! ("{:?}", node);
}
```
## Features
### 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).
```rust,no_run
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/graph; just `await` the terminals:
```rust,ignore
use falkordb::{EntityType, FalkorClientBuilder, FalkorConnectionInfo, IndexType};
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");
let mut graph = client.select_graph("social");
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["name"], None)
.wait()
.await
.expect("Index did not become operational");
```
[`FalkorDBError::Timeout`]: https://docs.rs/falkordb/latest/falkordb/enum.FalkorDBError.html
### `tokio` support
This client supports nonblocking API using the [`tokio`](https://tokio.rs/) runtime.
It can be enabled like so:
```toml
falkordb = { version = "0.3.0", features = ["tokio"] }
```
Currently, this API requires running within a [
`multi_threaded tokio scheduler`](https://docs.rs/tokio/latest/tokio/runtime/index.html#multi-thread-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:
```rust,ignore
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_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(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
.with_timeout(5000)
.execute()
.await
.expect("Failed executing query");
// Graph operations are asynchronous, but parsing is still concurrent:
while let Some(node) = nodes.data.next() {
println ! ("{:?}", node);
}
```
Note that thread safety is still up to the user to ensure, I.e. an `AsyncGraph` cannot simply be sent to a task spawned
by tokio and expected to be used later,
it must be wrapped in an Arc<Mutex<_>> or something similar.
### SSL/TLS Support
This client is currently built upon the [`redis`](https://docs.rs/redis/latest/redis/) crate, and therefore supports TLS
using
its implementation, which uses either [`rustls`](https://docs.rs/rustls/latest/rustls/) or [
`native_tls`](https://docs.rs/native-tls/latest/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:
```toml
falkordb = { version = "0.3.0", features = ["rustls"] }
```
```toml
falkordb = { version = "0.3.0", features = ["tokio-rustls"] }
```
For Native TLS:
```toml
falkordb = { version = "0.3.0", features = ["native-tls"] }
```
```toml
falkordb = { version = "0.3.0", features = ["tokio-native-tls"] }
```
### 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:
```rust,no_run
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`) can be served from
replica nodes, taking read load off the primary. When the client connects to a
Redis Sentinel deployment that exposes readable replicas, it automatically
builds a dedicated read-only connection pool that routes those queries to a
replica. Writes always go to the primary.
> **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. Size your pool limits and file-descriptor limits
> accordingly.
```rust,no_run
use falkordb::FalkorClientBuilder;
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"))
.build()
.expect("Failed to build client");
// `true` only when readable replicas are available.
if client.reads_from_replicas() {
println!("Read-only queries are routed to replicas");
}
let mut graph = client.select_graph("imdb");
// Writes go to the primary.
graph.query("CREATE (:Actor {name: 'Tom Hanks'})").execute().expect("Failed to write");
// Read-only queries are served from a replica when one is available.
let mut nodes = graph.ro_query("MATCH (a:Actor) RETURN a.name").execute().expect("Failed to read");
```
This behavior is fully backward compatible: against a single node (or any
deployment without readable replicas), `ro_query` / `call_procedure_ro`
transparently fall back to the primary connection, and `reads_from_replicas()`
returns `false`. See [`examples/readonly_replica.rs`](examples/readonly_replica.rs)
for a complete working example.
### Tracing
This crate fully supports instrumentation using the [`tracing`](https://docs.rs/tracing/latest/tracing/) crate, to use
it, simply, enable the `tracing` feature:
```toml
falkordb = { version = "0.3.0", 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.
### Embedded FalkorDB 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 in your `Cargo.toml`:
```toml
falkordb = { version = "0.3.0", features = ["embedded"] }
```
#### Requirements
- `redis-server` must be installed and available in PATH (or you can specify a custom path)
- `falkordb.so` module must be installed (or you can specify a custom path)
You can install these from:
- Redis: https://github.com/redis/redis
- FalkorDB: https://github.com/falkordb/falkordb
#### Usage Example
```rust,no_run
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")),
// ..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
## Testing
### Running Tests
This project includes both unit tests and integration tests.
#### Unit Tests
Unit tests don't require a running FalkorDB instance:
```bash
# Run all unit tests
cargo test --lib
# Run unit tests with embedded feature
cargo test --lib --features embedded
```
#### Integration Tests
Integration tests require a running FalkorDB instance. The easiest way to run them is using Docker:
```bash
# Using the provided script (requires Docker)
./run_integration_tests.sh
# Or manually start FalkorDB and run tests
docker run -d --name falkordb-test -p 6379:6379 falkordb/falkordb:latest
cargo test --test integration_tests
# With async support
cargo test --test integration_tests --features tokio
# Clean up
docker stop falkordb-test && docker rm falkordb-test
```
#### CI Integration Tests
Integration tests are automatically run in GitHub Actions using Docker services. See `.github/workflows/integration-tests.yml` for the CI configuration.