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.1.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)

Vector Search

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, :float_array}  # For 32-bit float vectors
  # Or use :double_array for 64-bit precision
])

Creating Vector Indexes

# Create an HNSW index for fast similarity search
LadybugEx.Vector.create_index(conn, "Document", "doc_embedding_idx", "embedding",
  metric: :cosine,    # Distance metric: :cosine, :l2, :l2sq, :dotproduct
  mu: 30,            # Max degree in upper graph
  ml: 60,            # Max degree in lower graph
  pu: 0.05,          # Percentage of nodes in upper graph
  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: &#39;Introduction to Graph Databases&#39;,
    content: &#39;Graph databases are...&#39;,
    embedding: [0.1, 0.2, 0.3, ...]  # Your embedding vector
  })
""")

Vector Similarity Search

# Find k nearest neighbors
query_embedding = [0.1, 0.15, 0.25, ...]  # Query vector

{:ok, results} = LadybugEx.Vector.query_index(
  conn,
  "Document",           # Table name
  "doc_embedding_idx",  # Index name
  query_embedding,      # Query vector
  10,                   # k (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(&#39;Document&#39;, &#39;doc_embedding_idx&#39;, $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, :loaded} = 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, :updated} = LadybugEx.Extensions.update(conn, :fts)

# Uninstall an extension
{:ok, :uninstalled} = LadybugEx.Extensions.uninstall(conn, :postgres)

# Install from a specific server
{:ok, :installed} = 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.