LadybugEx

Hex.pmHex DocsGitHubElixir CI

Elixir bindings for LadybugDB - a high-performance embedded property graph database with Cypher query support. A great option for building fast, low-ops agentic systems.

A note on AI slop. This project was generated using Claude Code, with some careful babysitting. It is fully tested both with unit testing and manual QA, so we should be in good shape. With all of that said, this is a new project that will likely have areas that need improvement.

Hex Package | Documentation | GitHub

Features

Installation

Add ladybug_ex to your list of dependencies in mix.exs:

def deps do
[
{:ladybug_ex, "~> 0.2.0"}
]
end

Then run:

mix deps.get

Requirements

The LadybugDB C++ library will be automatically compiled during the build process.

Quick Start

Basic Usage

# Create an in-memory database
{:ok, db} = LadybugEx.Database.in_memory()
# Create a connection
{:ok, conn} = LadybugEx.Connection.new(db)
# Create schema
LadybugEx.Schema.create_node_table(conn, "Person", [
{:id, :int64, primary_key: true},
{:name, :string},
{:age, :int64}
])
# Insert data using Cypher
LadybugEx.Connection.query!(conn, """
CREATE (:Person {id: 1, name: 'Alice', age: 30})
""")
# Query data
results = LadybugEx.Connection.query!(conn, """
MATCH (p:Person)
WHERE p.age >= 30
RETURN p.name AS name, p.age AS age
""")
# => [%{name: "Alice", age: 30}]

Using the Graph Module

alias LadybugEx.Graph
# Create nodes
{:ok, alice} = Graph.create_node(conn, "Person",
name: "Alice",
age: 30
)
{:ok, bob} = Graph.create_node(conn, "Person",
name: "Bob",
age: 25
)
# Create relationships
{:ok, rel} = Graph.create_relationship(conn,
alice.id, bob.id, "KNOWS",
since: 2020
)
# Find nodes
{:ok, people} = Graph.find_nodes(conn, "Person", %{age: 25})
# Find shortest path
{:ok, path} = Graph.shortest_path(conn, alice.id, bob.id)

Prepared Statements

# Prepare once
{:ok, stmt} = LadybugEx.Connection.prepare(conn, """
CREATE (:Person {id: $id, name: $name, age: $age})
""")
# Execute multiple times
LadybugEx.Connection.execute!(conn, stmt,
id: 2,
name: "Charlie",
age: 35
)
LadybugEx.Connection.execute!(conn, stmt,
id: 3,
name: "Diana",
age: 28
)

Transactions

LadybugEx.Connection.transaction(conn, fn conn ->
with {:ok, _} <- Graph.create_node(conn, "Account", balance: 1000),
{:ok, _} <- Graph.create_node(conn, "Account", balance: 500) do
{:ok, :success}
end
end)

LadybugEx provides support for LadybugDB's built-in vector extension, enabling high-performance similarity search over embeddings using HNSW (Hierarchical Navigable Small World) indexing.

Setup

The vector extension is included with LadybugDB but must be installed and loaded before use:

# Install and load the vector extension
case LadybugEx.Vector.install_and_load(conn) do
{:ok, :loaded} ->
IO.puts("Vector extension loaded successfully")
{:error, reason} ->
IO.puts("Could not load vector extension: #{reason}")
end

Creating Tables with Vectors

# Create a table with vector column
LadybugEx.Schema.create_node_table(conn, "Document", [
{:id, :int64, primary_key: true},
{:title, :string},
{:content, :string},
{:embedding, {:array, :float, 1536}} # Fixed-size FLOAT array (required for vector indexes)
# Or use {:array, :double, 1536} for 64-bit precision
])

Creating Vector Indexes

# Create an HNSW index for fast similarity search
{:ok, _} = LadybugEx.Vector.create_index(conn, "Document",
property: "embedding", # Column containing the vectors
dimension: 1536, # Vector dimension
index_name: "doc_embedding_idx", # Optional (default: auto-generated)
metric: :cosine, # Distance metric: :cosine, :l2, :l2sq, :dotproduct
mu: 30, # Max degree in upper graph
ml: 30, # Max degree in lower graph
pu: 0.05, # Percentage of nodes in upper graph (0.0-1.0)
efc: 200, # Candidates during construction
cache_embeddings: true # Cache during index build
)

Inserting Vector Data

# Insert documents with embeddings
LadybugEx.Connection.query!(conn, """
CREATE (:Document {
id: 1,
title: 'Introduction to Graph Databases',
content: 'Graph databases are...',
embedding: [0.1, 0.2, 0.3, ...] # Your embedding vector
})
""")
# Find k nearest neighbors
query_embedding = [0.1, 0.15, 0.25, ...] # Query vector
{:ok, results} = LadybugEx.Vector.query_index(conn, "Document", "doc_embedding_idx",
vector: query_embedding, # Query vector
k: 10, # Number of results
efs: 200 # Search candidates (higher = more accurate but slower)
)
# Results include the node and distance
Enum.each(results, fn %{"node" => node, "distance" => distance} ->
IO.puts("#{node["title"]} - Distance: #{distance}")
end)

Combining Vector Search with Graph Traversal

# Find similar documents and their authors
results = LadybugEx.Connection.query!(conn, """
CALL QUERY_VECTOR_INDEX('Document', 'doc_embedding_idx', $vec, 5)
WITH node AS d, distance
MATCH (a:Author)-[:WROTE]->(d)
RETURN d.title AS title, a.name AS author, distance
ORDER BY distance
""", vec: query_embedding)

Distance Metrics

Managing Vector Indexes

# Drop a vector index
{:ok, :dropped} = LadybugEx.Vector.drop_index(conn, "Document", "doc_embedding_idx")
# List all indexes (including vector indexes)
{:ok, results} = LadybugEx.Connection.query(conn, "CALL SHOW_INDEXES() RETURN *;")

Extension Management

LadybugDB supports various extensions to add additional functionality. The LadybugEx.Extensions module provides a unified interface for managing all LadybugDB extensions.

Available Extensions

Managing Extensions

# Install and load an extension
:ok = LadybugEx.Extensions.install_and_load(conn, "json")
# Check if an extension is loaded
LadybugEx.Extensions.loaded?(conn, "vector") # => true or false
# List available extensions
{:ok, available} = LadybugEx.Extensions.list_available(conn)
# List currently loaded extensions
{:ok, loaded} = LadybugEx.Extensions.list_loaded(conn)
# Update an extension (re-download from server)
:ok = LadybugEx.Extensions.update(conn, "fts")
# Uninstall an extension
:ok = LadybugEx.Extensions.uninstall(conn, "postgres")
# Install from a specific server
:ok = LadybugEx.Extensions.install_from(conn, "vector", "http://custom-server/")

All extension functions also have bang variants (e.g., install_and_load!) that raise on error.

Configuration Options

When opening a database, you can specify various configuration options:

LadybugEx.Database.open("/path/to/db",
buffer_pool_size: 1024 * 1024 * 256, # 256MB buffer pool
max_num_threads: 8, # Max threads for query execution
enable_compression: true, # Enable data compression
read_only: false, # Read-write mode
max_db_size: 1024 * 1024 * 1024 * 10 # 10GB max size
)

Architecture

The library consists of several layers:

  1. Rust NIF Layer: Low-level bindings to LadybugDB C++ library
  2. Native Module: Elixir interface to the Rust NIFs
  3. API Modules: Idiomatic Elixir APIs for different aspects:
    • Database: Database lifecycle management
    • Connection: Query execution and connection management
    • Schema: DDL operations for tables and indexes
    • Graph: High-level graph operations
    • PreparedStatement: Parameterized query support
    • Extensions: Comprehensive extension management system
    • Vector: Vector indexing and similarity search (uses Extensions internally)

Performance Considerations

Project Status

LadybugEx provides Elixir bindings for LadybugDB via a Rustler NIF. The library is actively maintained and available on Hex.

Implemented Features

✅ Database lifecycle management (persistent and in-memory modes) ✅ Cypher query execution with parameterized queries ✅ Schema operations (node tables, relationship tables, indexes) ✅ Graph operations (create/find nodes, relationships, shortest path) ✅ Transaction support with automatic rollback ✅ Prepared statements for efficient repeated queries ✅ Comprehensive extension management system for all LadybugDB extensions ✅ Vector search support via LadybugDB's vector extension ✅ Comprehensive test coverage

Known Limitations

Testing

Run the test suite:

# Run all tests
mix test
# Run tests with specific tags
mix test --only focus
# Run a specific test file
mix test test/ladybug_ex/connection_test.exs

All tests pass without requiring external dependencies. The vector extension tests validate both successful operation when the extension can be loaded and proper error handling when it cannot.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.