A pure Elixir driver for Neo4j graph database using the Bolt protocol.
✔️ Stable Release: Neo4jEx is now production-ready. Core functionality is complete, actively maintained, and tested across real-world workloads. Please report any bugs or feature requests to help us continue improving the driver.
- Full Bolt Protocol Support: Complete implementation of Neo4j's Bolt protocol v5.x
- Neo4j & Memgraph Compatible: Works with both Neo4j and Memgraph databases
- Authentication: Support for basic authentication and no-auth scenarios
- Connection Management: Automatic connection handling and cleanup
- Query Execution: Simple query execution with parameter support
- Transactions: Full transaction support with automatic commit/rollback
- Sessions: Session-based query execution for better resource management
- Type Safety: Proper handling of Neo4j data types and PackStream serialization
- Error Handling: Comprehensive error handling and reporting
- Connection Pooling: Efficient connection pooling for high-performance applications
- Pure Elixir: No external dependencies, built entirely in Elixir
Add neo4j_ex to your list of dependencies in mix.exs:
def deps do
[
{:neo4j_ex, "~> 0.1.9"}
]
endThen run:
mix deps.getImportant: Neo4jEx automatically starts when included as a dependency. This means:
- ✅ In Phoenix or Mix applications: Just add neo4j_ex to your
depsand configure it - it starts automatically - ❌ Don't manually add
Neo4j.Applicationto your supervision tree unless you have specific needs - ✅ In standalone scripts: Use
Application.ensure_all_started(:neo4j_ex)before making queries
If you see an error like already started: #PID<0.327.0>, it means you're trying to start the application twice.
Configure your default driver in config/config.exs:
# config/config.exs
config :neo4j_ex,
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"}That's it! Neo4jEx will automatically start with your application. You can now use the clean, implicit API:
# Execute queries without passing driver explicitly
{:ok, results} = Neo4jEx.run("MATCH (n:Person) RETURN n.name LIMIT 10")
# Work with sessions using default driver
result = Neo4jEx.session(fn session ->
Neo4j.Session.run(session, "CREATE (p:Person {name: $name})", %{name: "Alice"})
end)
# Use transactions with default driver
result = Neo4jEx.transaction(fn tx ->
Neo4j.Transaction.run(tx, "CREATE (p:Person {name: $name})", %{name: "Bob"})
Neo4j.Transaction.run(tx, "CREATE (p:Person {name: $name})", %{name: "Carol"})
end)
# Stream large result sets using default driver
Neo4jEx.stream("MATCH (n:Person) RETURN n")
|> Stream.each(&process_record/1)
|> Stream.run()If you prefer explicit driver management:
# Start a driver
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
auth: {"neo4j", "password"})
# Execute a simple query
{:ok, results} = Neo4jEx.run(driver, "MATCH (n:Person) RETURN n.name LIMIT 10")
# Process results
for record <- results.records do
name = Neo4j.Result.Record.get(record, "n.name")
IO.puts("Person: #{name}")
end
# Clean up
Neo4jEx.close(driver)# Basic authentication
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
auth: {"username", "password"})
# No authentication (for development)
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687")
# Custom timeouts
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
auth: {"neo4j", "password"},
connection_timeout: 30_000, # 30 seconds
query_timeout: 60_000) # 60 secondsYou can configure Neo4jEx in your application configuration:
# config/config.exs
config :my_app, :neo4j,
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"},
connection_timeout: 15_000,
query_timeout: 30_000Then use it in your application:
# In your application or supervisor
config = Application.get_env(:my_app, :neo4j)
{:ok, driver} = Neo4jEx.start_link(config[:uri],
auth: config[:auth],
connection_timeout: config[:connection_timeout],
query_timeout: config[:query_timeout])For development and testing, you can use environment variables:
export NEO4J_HOST=localhost
export NEO4J_PORT=7687
export NEO4J_USER=neo4j
export NEO4J_PASS=passwordConfigure drivers in your application config and let Neo4j.Application manage them:
# config/config.exs
config :neo4j_ex,
drivers: [
default: [
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"},
connection_timeout: 15_000,
query_timeout: 30_000
],
secondary: [
uri: "bolt://secondary:7687",
auth: {"neo4j", "password"}
]
]
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# Other children...
Neo4j.Application
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endThen use the named drivers:
{:ok, results} = Neo4jEx.run(:default, "MATCH (n) RETURN count(n)")
{:ok, results} = Neo4jEx.run(:secondary, "MATCH (n) RETURN count(n)")For a single driver, you can configure it directly:
# config/config.exs
config :neo4j_ex,
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"}
# The Neo4j.Application will automatically start a :default driverStart specific drivers manually in your supervision tree:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# Other children...
{Neo4j.Driver, [
"bolt://localhost:7687",
[
name: MyApp.Neo4j,
auth: {"neo4j", "password"}
]
]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endThen use the named driver:
{:ok, results} = Neo4jEx.run(MyApp.Neo4j, "MATCH (n) RETURN count(n)")# With implicit default driver (recommended)
{:ok, _} = Neo4jEx.run("""
CREATE (alice:Person {name: "Alice", age: 30})
CREATE (bob:Person {name: "Bob", age: 25})
CREATE (alice)-[:KNOWS]->(bob)
""")
# Query with parameters using default driver
{:ok, results} = Neo4jEx.run(
"MATCH (p:Person {name: $name}) RETURN p",
%{name: "Alice"})
# Or with explicit driver
{:ok, _} = Neo4jEx.run(driver, """
CREATE (alice:Person {name: "Alice", age: 30})
CREATE (bob:Person {name: "Bob", age: 25})
CREATE (alice)-[:KNOWS]->(bob)
""")
# Query with parameters and explicit driver
{:ok, results} = Neo4jEx.run(driver,
"MATCH (p:Person {name: $name}) RETURN p",
%{name: "Alice"})
# Process results
for record <- results.records do
person = Neo4j.Result.Record.get(record, "p")
IO.inspect(person)
end# Configure multiple drivers in config/config.exs
config :neo4j_ex,
drivers: [
default: [
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"}
],
analytics: [
uri: "bolt://analytics-server:7687",
auth: {"neo4j", "analytics_password"}
]
]
# Use different drivers for different purposes
{:ok, results} = Neo4jEx.run("MATCH (n:Person) RETURN count(n)") # Uses :default
{:ok, results} = Neo4jEx.run(:analytics, "MATCH (n:Event) RETURN count(n)") # Uses :analyticsresult = Neo4jEx.session(driver, fn session ->
# Multiple queries in the same session
{:ok, _} = Neo4j.Session.run(session,
"CREATE (p:Person {name: $name})", %{name: "Charlie"})
{:ok, results} = Neo4j.Session.run(session,
"MATCH (p:Person) RETURN count(p) AS total")
# Return the count
record = List.first(results.records)
Neo4j.Result.Record.get(record, "total")
end)
IO.puts("Total persons: #{result}")# Automatic transaction management
result = Neo4jEx.transaction(driver, fn tx ->
# All operations in this block are part of the same transaction
{:ok, _} = Neo4j.Transaction.run(tx,
"CREATE (p:Person {name: $name})", %{name: "David"})
{:ok, _} = Neo4j.Transaction.run(tx,
"CREATE (p:Person {name: $name})", %{name: "Eve"})
# If this block completes successfully, transaction is committed
# If an exception is raised, transaction is rolled back
:success
end)Neo4jEx.session(driver, fn session ->
{:ok, tx} = Neo4j.Session.begin_transaction(session)
try do
{:ok, _} = Neo4j.Transaction.run(tx,
"CREATE (p:Person {name: $name})", %{name: "Frank"})
# Manually commit
:ok = Neo4j.Transaction.commit(tx)
rescue
_error ->
# Manually rollback on error
:ok = Neo4j.Transaction.rollback(tx)
reraise
end
end){:ok, results} = Neo4jEx.run(driver, """
MATCH (p:Person)-[:KNOWS]->(friend:Person)
RETURN p.name AS person, friend.name AS friend, p.age AS age
""")
# Access by field name
for record <- results.records do
person = Neo4j.Result.Record.get(record, "person")
friend = Neo4j.Result.Record.get(record, "friend")
age = Neo4j.Result.Record.get(record, "age")
IO.puts("#{person} (#{age}) knows #{friend}")
end
# Access by index
for record <- results.records do
person = Neo4j.Result.Record.get(record, 0)
friend = Neo4j.Result.Record.get(record, 1)
age = Neo4j.Result.Record.get(record, 2)
IO.puts("#{person} (#{age}) knows #{friend}")
end
# Convert to map
for record <- results.records do
map = Neo4j.Result.Record.to_map(record)
IO.inspect(map)
# %{"person" => "Alice", "friend" => "Bob", "age" => 30}
end
# Query statistics
summary = results.summary
if Neo4j.Result.Summary.contains_updates?(summary) do
nodes_created = Neo4j.Result.Summary.get_counter(summary, "nodes_created")
IO.puts("Created #{nodes_created} nodes")
endFor large datasets, Neo4jEx provides a streaming interface that allows you to process results without loading everything into memory at once:
# Stream all Person nodes without loading them all into memory
driver
|> Neo4jEx.stream("MATCH (n:Person) RETURN n")
|> Stream.each(&process_person/1)
|> Stream.run()
# Stream with custom batch size and timeout
driver
|> Neo4jEx.stream("MATCH (n:BigData) RETURN n", %{}, batch_size: 500, timeout: 60_000)
|> Stream.chunk_every(100)
|> Enum.each(&batch_process/1)
# Memory-efficient aggregation
total = driver
|> Neo4jEx.stream("MATCH (n:Transaction) RETURN n.amount")
|> Stream.map(fn record -> record |> Neo4j.Result.Record.get("n.amount") end)
|> Enum.sum()
# Stream with custom processing function
driver
|> Neo4jEx.Stream.run_with("MATCH (n:Person) RETURN n.name", %{},
fn record ->
name = Neo4j.Result.Record.get(record, "n.name")
String.upcase(name)
end)
|> Enum.each(&IO.puts/1)The streaming interface uses cursor-based pagination under the hood, automatically fetching data in batches to minimize memory usage while maintaining good performance.
Neo4jEx provides efficient connection pooling for high-performance applications. Connection pooling allows you to reuse connections across multiple queries, reducing connection overhead and improving throughput.
# Start a connection pool
{:ok, _pool} = Neo4jEx.start_pool([
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"},
pool_size: 15, # Maximum number of connections
max_overflow: 5 # Additional connections when pool is full
])
# Or with a custom name
{:ok, _pool} = Neo4jEx.start_pool([
uri: "bolt://localhost:7687",
auth: {"neo4j", "password"},
pool_size: 10,
name: :my_pool
])# Execute queries using the default pool
{:ok, results} = Neo4jEx.Pool.run("MATCH (n:Person) RETURN n")
# Execute queries with parameters
{:ok, results} = Neo4jEx.Pool.run(
"CREATE (p:Person {name: $name}) RETURN p",
%{name: "Alice"}
)
# Execute transactions using the pool
result = Neo4jEx.Pool.transaction(fn ->
Neo4jEx.Pool.run("CREATE (p:Person {name: 'Bob'})")
Neo4jEx.Pool.run("CREATE (p:Person {name: 'Carol'})")
:success
end)
# Use a named pool
{:ok, results} = Neo4jEx.Pool.run(
"MATCH (n) RETURN count(n)",
%{},
pool_name: :my_pool
)| Option | Type | Default | Description |
|---|---|---|---|
:uri |
string() |
- | Neo4j connection URI (required) |
:auth |
tuple() |
nil |
Authentication credentials |
:pool_size |
integer() |
10 |
Maximum number of connections |
:max_overflow |
integer() |
5 |
Additional connections when pool full |
:name |
atom() |
- | Pool name for multiple pools |
# Check pool status
status = Neo4jEx.Pool.status()
# Returns: {:ready, pool_size, checked_out, overflow}
# Stop a pool
Neo4jEx.stop_pool()
# Stop a named pool
Neo4jEx.stop_pool(:my_pool)- Improved Performance: Reuse existing connections instead of creating new ones
- Resource Management: Control the maximum number of concurrent connections
- Scalability: Handle high-concurrency scenarios efficiently
- Automatic Recovery: Connections are automatically recreated if they fail
- Load Distribution: Distribute queries across multiple connections
Use the included test script to verify your Neo4j connection:
# With default settings (localhost:7687, neo4j/password)
elixir scripts/test_connection.exs
# With custom settings
NEO4J_HOST=myhost NEO4J_PORT=7687 NEO4J_USER=myuser NEO4J_PASS=mypass elixir scripts/test_connection.exs| Option | Type | Default | Description |
|---|---|---|---|
:auth |
{username, password} or map() |
nil |
Authentication credentials |
:user_agent |
string() |
"neo4j_ex/0.1.0" |
Client identification |
:connection_timeout |
integer() |
15_000 |
Connection timeout (ms) |
:query_timeout |
integer() |
30_000 |
Query timeout (ms) |
:max_pool_size |
integer() |
10 |
Max connections (future) |
# Tuple format (recommended)
auth: {"username", "password"}
# Map format (advanced)
auth: %{
"scheme" => "basic",
"principal" => "username",
"credentials" => "password"
}
# No authentication
auth: nil
# or simply omit the :auth optioncase Neo4jEx.run(driver, "INVALID CYPHER") do
{:ok, results} ->
# Handle success
IO.puts("Query succeeded")
{:error, {:query_failed, message}} ->
# Handle query errors
IO.puts("Query failed: #{message}")
{:error, {:connection_failed, reason}} ->
# Handle connection errors
IO.puts("Connection failed: #{inspect(reason)}")
{:error, reason} ->
# Handle other errors
IO.puts("Error: #{inspect(reason)}")
end- Elixir 1.12+
- Neo4j 4.0+ or Memgraph running on localhost:7687
# Run unit tests
mix test
# Run with a specific Neo4j instance
NEO4J_USER=neo4j NEO4J_PASS=yourpassword mix test
# Test connection
elixir scripts/test_connection.exsThe driver is built with a layered architecture:
Neo4jEx (Public API)
├── Neo4j.Driver (Driver Management)
├── Neo4j.Session (Session Management)
├── Neo4j.Transaction (Transaction Management)
├── Neo4j.Protocol.* (Bolt Protocol Implementation)
│ ├── Messages (Bolt Messages)
│ └── PackStream (Serialization)
├── Neo4j.Connection.* (Connection Layer)
│ ├── Socket (TCP Communication)
│ └── Handshake (Bolt Handshake)
└── Neo4j.Result.* & Neo4j.Types.* (Data Types)
├── Record (Query Results)
├── Summary (Query Metadata)
└── Node, Relationship, Path (Graph Types)
- Week 1: Basic connection and Bolt handshake
- Week 2: PackStream serialization and basic messaging
- Week 3: Query execution and result parsing
- Week 4: Polish, testing, and documentation
- v0.2.0: Result streaming support for large datasets
- v0.3.0: Connection pooling and improved performance
- v0.4.0: Clustering support and routing
- v1.0.0: Advanced Neo4j types (Point, Duration, etc.) and production readiness
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Neo4j team for the excellent Bolt protocol documentation
- Elixir community for the amazing ecosystem
- Contributors and testers who helped make this driver possible
Made with ❤️ for the Elixir and Neo4j communities.