Skip to main content

GANDALF

Graph Analysis Navigator for Discovery And Link Finding

A high-performance Python library and Translator-compatible TRAPI server for fast path finding in large biomedical knowledge graphs.

Features

  • Compressed Sparse Row (CSR) graph representation for memory-efficient storage of 10M+ node, 38M+ edge graphs
  • Bidirectional search for optimal path-finding performance
  • O(1) property lookups via hash indexing
  • Predicate filtering to reduce path explosion
  • Qualifier filtering for advanced edge constraints (aspect, direction, mechanism)
  • Attribute constraints on edges and nodes, including filtering edges by specific PubMed IDs
  • Subclass expansion via Biolink Model Toolkit with configurable depth
  • Batch property enrichment — enrich only final paths, not intermediate results
  • Diagnostic tools to understand path counts and explosion
  • TRAPI 1.5 compatible REST API with Plater-compatible endpoints
  • Async query support with callback URLs
  • Dehydrated mode for lightweight responses that skip edge and node attribute enrichment
  • OpenTelemetry tracing with Jaeger integration

Installation

Recommended: Use a virtual environment

Some transitive dependencies (e.g., stringcase, pytest-logging) require modern pip/setuptools to build correctly. Using a virtual environment ensures you have updated tools.

# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Upgrade pip and setuptools (important for building dependencies)
pip install --upgrade pip setuptools wheel

# Install the core package
pip install -e .

# Install with server dependencies (FastAPI, uvicorn, etc.)
pip install -e ".[server]"

# Install with dev dependencies (pytest, black, mypy)
pip install -e ".[dev]"

Quick Start

Unzipping a full Translator KGX

tar -xvf translator_kg.tar.zst

This will output a nodes.jsonl and edges.jsonl file.

Build a graph from JSONL

from gandalf import build_graph_from_jsonl

# Build with ontology filtering
graph = build_graph_from_jsonl(
    edges_path="data/raw/edges.jsonl",
    nodes_path="data/raw/nodes.jsonl",
)

# Save for fast loading
graph.save_mmap("data/processed/gandalf_mmap")

Query paths (TRAPI format)

from gandalf import CSRGraph, lookup

# Load graph (takes ~1-2 seconds)
graph = CSRGraph.load_mmap("data/processed/gandalf_mmap")

# Execute a TRAPI query
response = lookup(
    graph,
    {
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {"ids": ["CHEBI:45783"]},
                    "n1": {"categories": ["biolink:Gene"]},
                    "n2": {"categories": ["biolink:Disease"]}
                },
                "edges": {
                    "e0": {"subject": "n0", "object": "n1", "predicates": ["biolink:affects"]},
                    "e1": {"subject": "n1", "object": "n2"}
                }
            }
        }
    },
    subclass=True,
    subclass_depth=1,
)

print(f"Found {len(response['message']['results'])} paths")

Filtering edges by attribute (including PubMed IDs)

Any query edge accepts TRAPI attribute_constraints, evaluated against the edge's attributes; query nodes accept the same shape under constraints. Multiple constraints are ANDed, and "not": true negates one.

Attribute values are often lists — publications above all — and every operator except === is applied to each member, so == reads as "contains". Filtering an edge down to specific PubMed IDs is therefore plain equality:

"edges": {
    "e0": {
        "subject": "n0",
        "object": "n1",
        "predicates": ["biolink:affects"],
        "attribute_constraints": [
            {
                "id": "biolink:publications",
                "name": "publications",
                "operator": "==",
                "value": ["PMID:23456789", "PMID:11111111"],
            }
        ],
    }
}

Only edges citing at least one of those PMIDs survive. A list value means "any of"; a single string constrains to one publication. Publication identifiers are compared canonically, so PMID:23456789, pubmed:23456789, https://pubmed.ncbi.nlm.nih.gov/23456789 and the bare 23456789 all select the same article — unlike the matches operator, which does a substring regex and would also accept PMID:234567890.

Architecture

The package uses a three-stage pipeline:

  1. Topology Search (fast) - Find all paths using indices only
  2. Filtering (medium) - Apply business logic on necessary node or edge properties
  3. Enrichment (batch) - Load all properties for final paths only

This separation allows filtering millions of paths before expensive property lookups.

REST API

The server exposes Plater-compatible TRAPI endpoints on port 6429.

Run the development server:

python gandalf/main.py

Run the production server:

gunicorn gandalf.server:APP -c gunicorn.conf.py

Endpoints

Method Path Description
GET / Redirect to /docs
GET /docs Swagger UI documentation
GET /metadata Graph statistics and metadata
GET /node_degree/{curie} Total degree (in + out) of a node
GET /meta_knowledge_graph Meta KG with predicates, categories, and counts
GET /sri_testing_data Representative edges for SRI Testing Harness
POST /query Synchronous TRAPI query
POST /asyncquery Async TRAPI query with callback URL

Both /query and /asyncquery accept a single optional query parameter:

  • ?profile=true — Emit per-stage timing diagnostics into message.logs

All other request configuration lives under the body's parameters object:

{
  "message": { "query_graph": { ... } },
  "log_level": "INFO",
  "parameters": {
    "subclass": true,
    "subclass_depth": 1,
    "dehydrated": false,
    "filter_config": { "max_node_degree": 50 },
    "annotator_config": {}
  }
}
  • subclass (bool): Enable biolink subclass inference (default true)
  • subclass_depth (int): Maximum subclass_of hops (default 1)
  • dehydrated (bool): Skip edge attribute enrichment for faster, lighter responses (auto-enabled for very large result sets)
  • rehydrate (bool): When true, the server skips the graph lookup and only enriches the knowledge_graph already supplied in message — used to re-enrich a previously dehydrated response
  • filter_config (object): Plugin-defined node filter settings (each NodeFilter plugin reads its own key)
  • annotator_config (object): Per-request opt-in response-annotator settings (each key activates one annotator plugin)

CLI Commands

# Build a CSR graph from JSONL node/edge files
gandalf-build --edges data/edges.jsonl --nodes data/nodes.jsonl --output data/graph_mmap/

# Query paths from the command line
gandalf-query --graph data/graph_mmap/ --start "CHEBI:45783" --end "MONDO:0004979"

# Diagnose path explosion between two nodes
gandalf-diagnose --graph data/graph_mmap/ --start "CHEBI:45783" --end "MONDO:0004979"

Configuration

The server is configured via environment variables (prefixed with GANDALF_):

Core

Variable Default Description
GANDALF_GRAPH_PATH /data/graph Path to the mmap graph directory
GANDALF_GRAPH_FORMAT auto Graph format (auto or mmap)
GANDALF_LOAD_MMAPS_INTO_MEMORY false Load memory-mapped arrays fully into RAM
GANDALF_LOG_LEVEL INFO Logging level (DEBUG, INFO, WARNING, ERROR)
GANDALF_LOG_FORMAT text Log format (text for human-readable, json for structured)
GANDALF_CORS_ORIGINS * Comma-separated list of allowed CORS origins
GANDALF_MAX_REQUEST_SIZE_MB 10 Maximum request body size in MB
GANDALF_RATE_LIMIT 0 Max requests per minute per client IP (0 = disabled)
GANDALF_SKIP_PRELOAD false Skip module-level graph loading
GANDALF_WORKERS 2 Gunicorn worker count

Search Tuning

Variable Default Description
GANDALF_LARGE_RESULT_THRESHOLD 50000 Path count threshold for auto-dehydrated responses
GANDALF_MAX_PATH_LIMIT 0 Max intermediate paths during joins (0 = unlimited)
GANDALF_DEBUG_PATHS_TSV (empty) File path to write debug TSV of reconstructed paths

Server Identity

Variable Default Description
GANDALF_SERVER_URL http://localhost:6429 Public URL of this instance
GANDALF_SERVER_MATURITY development Maturity level for TRAPI metadata
GANDALF_SERVER_LOCATION RENCI Server location for TRAPI metadata
GANDALF_INFORES infores:gandalf Translator infores identifier

Automat Heartbeat

Variable Default Description
GANDALF_AUTOMAT_HOST (empty, disabled) Automat cluster URL for registration
GANDALF_HEART_RATE 30 Seconds between heartbeats
GANDALF_SERVICE_ADDRESS (empty) Reachable address of this instance
GANDALF_WEB_PORT 8080 Port for heartbeat registration

Observability

Variable Default Description
GANDALF_OTEL_ENABLED true Enable OpenTelemetry tracing
GANDALF_OTEL_SERVICE_NAME gandalf Service name for traces
GANDALF_JAEGER_HOST http://jaeger Jaeger collector host
GANDALF_JAEGER_PORT 4317 Jaeger collector gRPC port

Docker

# Build the image
docker build -t gandalf .

# Run with a graph volume
docker run -p 6429:6429 \
  -v /path/to/graph:/data/graph \
  -e GANDALF_GRAPH_PATH=/data/graph \
  gandalf

Verifying the Server

# Check graph metadata
curl http://localhost:6429/metadata

# Browse the API docs
open http://localhost:6429/docs

Releases

  • Make a release in GitHub to run a GitHub Action that pushes a gandalf to ghcr
  • Run this on the mmap folder: tar -czvf gandalf_mmap_<date>.tar.gz gandalf_mmap
  • Upload the tar.gz file to a public file server
  • Update any helm charts and deploy

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gandalf_csr-1.0.2.tar.gz (2.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gandalf_csr-1.0.2-py3-none-any.whl (2.3 MB view details)

Uploaded Python 3

File details

Details for the file gandalf_csr-1.0.2.tar.gz.

File metadata

  • Download URL: gandalf_csr-1.0.2.tar.gz
  • Upload date:
  • Size: 2.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gandalf_csr-1.0.2.tar.gz
Algorithm Hash digest
SHA256 cdc83d6bee3e03df6a01e085efb40a368fbd7bbaab41cb8abee554c019df8558
MD5 0f8057d0523f9a058f3feff195697601
BLAKE2b-256 fd64aecd0f086385c1413d929aca2236a2bc5ffb443276f9360a775c9d117aa8

See more details on using hashes here.

File details

Details for the file gandalf_csr-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: gandalf_csr-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 2.3 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gandalf_csr-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b9e5c51fa1f65eafffd6371c02f1ed49aa699c25825e9a0d5f3678aa6dae736
MD5 bb01bb58fe1319efe9af719f77ffebc7
BLAKE2b-256 581309a5e8558466583e949e5588d88f6ad169de25a8c2b1364986413b19cca3

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.2 This release

2 files

1.0.0

2 files

0.4.2

2 files

0.4.0

2 files

0.3.3

2 files

0.3.2

2 files

0.3.0

2 files

0.2.0

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page