Exograph

Local CodeQL-style code search for Elixir, backed by Postgres and ExAST.

Exograph indexes Elixir source code into normalized Ecto/Postgres tables: files, AST fragments, comments, definitions, references, package versions, and optional Reach call graph facts. You can then query that index with structural AST patterns, text/regex search, symbol/reference filters, and Ecto-shaped joins.

What is Exograph?

Exograph is:

Exograph is not:

Why?

Use Exograph when text search is not enough:

Installation

def deps do
[
{:exograph, "~> 0.7"}
]
end

Exograph requires Postgres. ParadeDB's pg_search extension is optional and enables BM25-backed text/code-fact retrieval.

Quickstart

Point Exograph at Elixir source and an Ecto repo:

{:ok, index} =
Exograph.index("lib",
repo: MyApp.Repo,
migrate?: true
)
{:ok, hits} = Exograph.search(index, "Repo.get!(_, _)")

Postgres retrieves candidates by term index; ExAST verifies the structural match.

Index Hex.pm

Download and index packages directly from Hex.pm:

mix exograph.index.hex --mode latest --concurrency 8 --prefix hex

Modes: latest (one version per package), top --limit 5000, all (every version). Resumes automatically — already-indexed packages are skipped.

On a full Hex.pm run: ~21k packages, 13.8M fragments, 35M references, ~34 GB, 28 minutes.

Web UI

Exograph includes an embedded web interface for exploring indexes:

mix exograph.web --prefix myindex --port 4200

Add --web to mix exograph.index.hex for a live progress dashboard while indexing runs.

Features:

JSON API

The web server exposes a JSON API:

POST /api/searchstructural, text, or regex search
POST /api/query — DSL query execution
GET /api/packageslist indexed packages
GET /api/statsindex statistics

Cursor pagination via cursor/next_cursor. Rate limited (60 req/min).

Query with code facts

Use Exograph.DSL to combine structural AST patterns with indexed code facts:

import Exograph.DSL
query =
from(f in Fragment,
join: r in assoc(f, :references),
where: r.qualified_name == "Repo.transaction/1",
where: matches(f, "def _ do ... end")
)
{:ok, hits} = Exograph.all(index, query)

Reach call graph facts can also be queried directly:

Exograph.search_callers(index, "Repo.transaction/1")
Exograph.search_callees(index, "MyApp.Accounts.update_user/2")

How it compares

ToolScopeStorageQuery styleElixir AST-aware?Best for
ripgreplocal text searchnoneregex/textnofast ad-hoc text search
ExASTstructural AST matchingnone/advisory termsAST patterns/selectorsyesexact search and patching
Reachdependence analysisin-memory graph/reportsAPIs / Mix tasksyescall/data/control-flow analysis
CodeQLsemantic code analysisCodeQL databaseQL languagenot first-class Elixirsecurity analysis at scale
Sourcegraphcross-repo searchexternal indextext/structural depending setupnot Elixir-specificorganization-wide search
ExographElixir code fact indexPostgres/ParadeDBExAST + Ecto-shaped DSLyeslocal/self-hosted Elixir code intelligence, 21k+ package indexing

Features

Documentation

GuideContent
Getting StartedInstallation, Postgres setup, first index/search
QueryingStructural, text, and regex search; planning/explain
DSLExograph.DSL, joins, selects, predicates
Code FactsDefinitions, references, comments, typed hits
Call GraphReach extraction, callers/callees, call edge DSL
Postgres and ParadeDBStorage backend, migrations, BM25, performance tuning
Package IndexingIndexing Hex.pm and manual package archives
Mix TasksCLI indexing, searching, web UI
Web UIMonaco editor, search modes, progress dashboard
APIJSON API endpoints, pagination, rate limiting
ComparisonsExograph vs ExAST, Reach, CodeQL, Sourcegraph
ArchitectureStorage model, verifier contract, extraction pipeline

License

MIT.