Scrypath gives Phoenix and Ecto teams an explicit path for declaring searchable schemas, syncing search documents, and querying through one context-owned boundary.
What You Set Up
Start with three pieces:
- A schema that declares search metadata with
use Scrypath - A context that owns repo persistence plus
Scrypath.*orchestration - A backend configuration that keeps sync mode explicit, plus Oban only if you want queued sync
Declare A Searchable Schema
defmodule MyApp.Blog.Post do
use Ecto.Schema
use Scrypath,
fields: [:title, :body],
filterable: [:status],
sortable: [:inserted_at]
schema "posts" do
field :title, :string
field :body, :string
field :status, Ecto.Enum, values: [:draft, :published]
timestamps()
end
enduse Scrypath stays metadata-only. Runtime orchestration still lives in your context modules.
Scrypath also owns its internal transport dependency, so the base install path stays focused on adding :scrypath.
Put Search And Sync In The Context
defmodule MyApp.Content do
alias MyApp.Blog.Post
alias MyApp.Repo
def search_posts(query, opts \\ []) do
Scrypath.search(Post, query,
Keyword.merge([backend: Scrypath.Meilisearch, repo: Repo], opts)
)
end
def publish_post(post, attrs) do
with {:ok, post} <- update_post(post, attrs),
{:ok, _sync} <-
Scrypath.sync_record(Post, post,
backend: Scrypath.Meilisearch,
sync_mode: :inline
) do
{:ok, post}
end
end
endThat keeps reads, writes, sync decisions, and failure handling in one application boundary instead of spreading them across controllers or LiveView callbacks.
Keep Web Modules Thin
Controllers and LiveView modules call the same context boundary:
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
alias MyApp.Content
def index(conn, params) do
{:ok, result} =
Content.search_posts(Map.get(params, "q", ""),
filter: [status: "published"]
)
render(conn, :index, posts: result.records, search: result)
end
enddefmodule MyAppWeb.PostLive do
use MyAppWeb, :live_view
alias MyApp.Content
def handle_params(%{"q" => query}, _uri, socket) do
{:ok, result} = Content.search_posts(query, preload: [:author])
{:noreply, assign(socket, posts: result.records, search: result, query: query)}
end
endChoose Sync Mode Deliberately
:inlinewaits for terminal backend success before returning:manualreturns accepted backend work immediately for imports and operator-driven flows:obanreturns durable enqueue acceptance only and is an optional production path when you want queued sync
Accepted work is not the same thing as search visibility. Pick the mode that matches your consistency and operational constraints.
Continue
- Read Phoenix Walkthrough for the first end-to-end path
- Read Phoenix Contexts for the recommended boundary shape
- Read Sync Modes And Visibility before choosing
:manualor:oban