Exograph supports structural search through ExAST selectors, text and regex search through Postgres, and relational queries through the DSL.
Structural patterns
{:ok, results} = Exograph.search(index, "Repo.get!(_, _)")Patterns are plain ExAST patterns. _ matches one node; ... matches a sequence
or variable arity where supported by ExAST. Postgres retrieves candidates by term
index; ExAST verifies the structural match.
Relationship-aware selectors
Use ExAST.Query when a single pattern is not enough:
import ExAST.Query
query =
from("def _ do ... end")
|> where(contains("Repo.transaction(_)"))
|> where(not contains("IO.inspect(_)"))
{:ok, results} = Exograph.search(index, query)Selector alternatives, sibling/position predicates, comment predicates, and capture guards are handled by ExAST. Exograph uses index terms as advisory candidate filters and verifies the final result against the original AST/source.
from(["def _ do ... end", "defp _ do ... end"])
|> where(follows("@doc _"))
|> where(first())
from("left == right")
|> where(^left == ^right)
from("def _ do ... end")
|> where(comment_before(text("transaction wrapper")))Text search
Search source code by literal text:
{:ok, hits} = Exograph.search_text(index, "TODO")
{:ok, hits} = Exograph.search_text(index, "deprecated", limit: 50)With ParadeDB pg_search installed, text search uses BM25 ranking. Without it,
search falls back to ILIKE accelerated by pg_trgm GIN indexes on
files.source and files.comments_text.
Regex search
Pass a compiled regex to Exograph.search_text/3:
{:ok, hits} = Exograph.search_text(index, ~r/def \w+!/)
{:ok, hits} = Exograph.search_text(index, ~r/Repo\.(get|insert|update)!/, limit: 100)Regex search uses Postgres ~* (case-insensitive). pg_trgm may still
accelerate the scan if the regex has extractable trigrams.
Text and regex modes in the web UI and API
The web UI exposes Structural/Text/Regex toggle buttons. The JSON API accepts a
mode parameter:
curl -X POST http://localhost:4200/api/search \
-H "Content-Type: application/json" \
-d '{"pattern": "TODO", "mode": "text"}'
curl -X POST http://localhost:4200/api/search \
-H "Content-Type: application/json" \
-d '{"pattern": "Repo\\.get!\\(", "mode": "regex"}'
From the CLI:
mix exograph.search 'TODO' --text --repo MyApp.Repo lib
mix exograph.search 'Repo\.get!\(' --regex --repo MyApp.Repo libPlanning and explanations
Exograph treats indexes like an RDBMS treats access paths: advisory only. The
logical query remains the source of truth and every physical plan ends in exact
ExAST verification unless you explicitly pass verify: false.
plan =
Exograph.plan(
index,
from("def _ do ... end") |> where(contains("Repo.get!(_, _)"))
)
Exograph.explain(plan)
#=> %{
#=> logical: %{required_terms: ["call.remote:Repo.get!/2"], ...},
#=> physical: %{scan: {:term_index_scan, [...]}, filters: [:hydrate_fragments, :ex_ast_verify]},
#=> estimated_candidates: 4,
#=> warnings: []
#=> }Standalone explanations are also available:
Exograph.explain("Repo.get!(User, id)")
#=> %{required: ["call.remote:Repo.get!/2", ...], verifier: :pattern, ...}Similarity search
Exograph stores ExDNA structural fingerprints for fragments and can rerank similar fragments:
{:ok, results} =
Exograph.similar(index, """
user
|> cast(attrs, [:name])
|> validate_required([:name])
""", min_similarity: 0.8)