Scrypath (scrypath v0.3.5)

Copy Markdown View Source

Scrypath is the runtime surface for schema → sync → search: you declare how records map into a Meilisearch index, enqueue or inline sync work, then query through the same Ecto-native boundaries your Phoenix or contexts already own.

Entry points

Use use Scrypath on your Ecto schema; declaration grammar and settings live on Scrypath.Schema — read that module instead of duplicating option tables here.

Reflection helpers

The initial public reflection surface is intentionally small:

These functions keep reflection under Scrypath.* modules instead of generating schema-specific runtime verbs.

Examples

iex> config = Scrypath.schema_config(SearchablePost)
iex> config.fields
[:title, :body]

See also search_within_facet/4 in guides/faceted-search-with-phoenix-liveview.md for searching inside a facet bucket alongside filter: / facet_filter: composition.

Summary

Functions

Upserts or deletes search documents through the configured backend.

Upserts or deletes search documents through the configured backend.

Upserts or deletes search documents through the configured backend.

Returns failed or retrying sync work rows for a schema.

Read-only index contract drift report for a searchable schema.

Report-first reconciliation for a schema (sync visibility, failed work, drift signals, and suggested recovery actions).

Returns normalized faceting: options for the schema, or [] when faceting is disabled.

Primary hydrated search entry: validates options, resolves runtime config, and returns {:ok, search_result} or tagged {:error, _} failures from the configured backend. Work is wrapped in the public search telemetry span [:scrypath, :search] with stable, low-cardinality metadata.

Like search/3, but returns the same hydrated search result payload or raises Scrypath.Search.Error when the non-bang API would return {:error, _}.

Like search_many/2, but returns %Scrypath.MultiSearchResult{} or raises Scrypath.Search.Error when the non-bang API would return {:error, _}.

Full-text search scoped to a single facet bucket, AND-combined with any other filter:, facet_filter:, sort, and pagination options.

Upserts or deletes search documents through the configured backend.

Upserts or deletes search documents through the configured backend.

Functions

backfill(schema_module, opts \\ [])

@spec backfill(
  module(),
  keyword()
) :: {:ok, map()} | {:error, term()}

delete_document(schema_module, document_id, opts \\ [])

@spec delete_document(module(), term(), keyword()) :: {:ok, term()} | {:error, term()}

Upserts or deletes search documents through the configured backend.

On success, {:ok, map} includes at least :mode (for example :inline, :oban, or :manual) and :status:

  • :status :accepted — work was queued or accepted by the backend or queue layer; documents may not be queryable yet. This is normal for :manual, :oban, and sometimes :inline when no Meilisearch task wait applies. See guides/sync-modes-and-visibility.md.
  • :status :completed — the :inline path finished, including waiting for a terminal Meilisearch task when sync_mode: :inline and the backend returned a task handle.

Pitfalls when the database write “succeeded” but search lags: guides/common-mistakes.md.

Tagged {:error, reason} tuples may include {:timeout, _}, {:task_failed, _}, {:invalid_options, _, _}, and other operational heads. For user-facing copy, normalize them through your own application boundary instead of depending on internal helpers.

delete_documents(schema_module, document_ids, opts \\ [])

@spec delete_documents(module(), [term()], keyword()) ::
  {:ok, term()} | {:error, term()}

Upserts or deletes search documents through the configured backend.

On success, {:ok, map} includes at least :mode (for example :inline, :oban, or :manual) and :status:

  • :status :accepted — work was queued or accepted by the backend or queue layer; documents may not be queryable yet. This is normal for :manual, :oban, and sometimes :inline when no Meilisearch task wait applies. See guides/sync-modes-and-visibility.md.
  • :status :completed — the :inline path finished, including waiting for a terminal Meilisearch task when sync_mode: :inline and the backend returned a task handle.

Pitfalls when the database write “succeeded” but search lags: guides/common-mistakes.md.

Tagged {:error, reason} tuples may include {:timeout, _}, {:task_failed, _}, {:invalid_options, _, _}, and other operational heads. For user-facing copy, normalize them through your own application boundary instead of depending on internal helpers.

delete_record(schema_module, record, opts \\ [])

@spec delete_record(module(), struct() | map(), keyword()) ::
  {:ok, term()} | {:error, term()}

Upserts or deletes search documents through the configured backend.

On success, {:ok, map} includes at least :mode (for example :inline, :oban, or :manual) and :status:

  • :status :accepted — work was queued or accepted by the backend or queue layer; documents may not be queryable yet. This is normal for :manual, :oban, and sometimes :inline when no Meilisearch task wait applies. See guides/sync-modes-and-visibility.md.
  • :status :completed — the :inline path finished, including waiting for a terminal Meilisearch task when sync_mode: :inline and the backend returned a task handle.

Pitfalls when the database write “succeeded” but search lags: guides/common-mistakes.md.

Tagged {:error, reason} tuples may include {:timeout, _}, {:task_failed, _}, {:invalid_options, _, _}, and other operational heads. For user-facing copy, normalize them through your own application boundary instead of depending on internal helpers.

document_id_field(schema_module)

@spec document_id_field(module()) :: atom()

document_source(schema_module)

@spec document_source(module()) :: atom()

failed_sync_work(schema_module, opts \\ [])

@spec failed_sync_work(
  module(),
  keyword()
) ::
  {:ok, [Scrypath.Operator.FailedWork.t()]}
  | {:ok, Scrypath.Operator.FailedSyncWorkInspection.t()}
  | {:error, term()}

Returns failed or retrying sync work rows for a schema.

Reason class rollups

Pass reason_class_counts: true in operator options (alongside runtime config) to receive {:ok, %Scrypath.Operator.FailedSyncWorkInspection{}} with entries (the row list) and counts — a %Scrypath.Operator.ReasonClassCounts{} with dense per-class frequencies. The default remains {:ok, [FailedWork.t()]} when this option is omitted.

index_contract_drift(schema_module, opts \\ [])

@spec index_contract_drift(
  module(),
  keyword()
) :: {:ok, Scrypath.Operator.IndexContractDrift.Report.t()} | {:error, term()}

Read-only index contract drift report for a searchable schema.

Compares declared schema metadata and resolved settings against a single live Meilisearch GET /indexes/{uid}/settings snapshot. This is a report-first operator surface: it does not enqueue work, mutate indexes, or imply recovery actions. It is not the same signal family as reconcile_sync/2's drift_signals, which reflects sync queue and reindex posture.

See include_index_contract_drift: true on reconcile_sync/2 when you want the same report attached to a reconcile tuple (adds one get_settings read).

reconcile_sync(schema_module, opts \\ [])

@spec reconcile_sync(
  module(),
  keyword()
) :: {:ok, Scrypath.Operator.Reconcile.t()} | {:error, term()} | {:ok, map()}

Report-first reconciliation for a schema (sync visibility, failed work, drift signals, and suggested recovery actions).

Index contract drift

Pass include_index_contract_drift: true alongside runtime options to attach a read-only %Scrypath.Operator.IndexContractDrift.Report{} on index_contract_drift. This performs an extra Meilisearch get_settings read using the same builder as index_contract_drift/2. Omit the flag (default) to avoid that latency and live dependency.

reindex(schema_module, opts \\ [])

@spec reindex(
  module(),
  keyword()
) :: {:ok, map()} | {:error, term()}

retry_sync_work(work_or_action, opts \\ [])

@spec retry_sync_work(
  Scrypath.Operator.FailedWork.t() | Scrypath.Operator.RecoveryAction.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

schema_config(schema_module)

@spec schema_config(module()) :: map()

schema_faceting(schema_module)

@spec schema_faceting(module()) :: keyword()

Returns normalized faceting: options for the schema, or [] when faceting is disabled.

Shape when enabled is a keyword list with :attributes, :max_values_per_facet, and :sort_facet_values_by.

schema_fields(schema_module)

@spec schema_fields(module()) :: [atom()]

schema_settings(schema_module)

@spec schema_settings(module()) :: map()

search(schema_module, text, opts \\ [])

@spec search(module(), String.t(), keyword()) :: {:ok, term()} | {:error, term()}

Primary hydrated search entry: validates options, resolves runtime config, and returns {:ok, search_result} or tagged {:error, _} failures from the configured backend. Work is wrapped in the public search telemetry span [:scrypath, :search] with stable, low-cardinality metadata.

Full pipeline: see guides/per-query-tuning-pipeline.md.

Optional :per_query is an allowlisted keyword for Plane B Meilisearch search parameters on the v1.9 slice (for example ranking_score_threshold, show_ranking_score, and show_ranking_score_details); semantics and telemetry expectations are defined in guides/per-query-tuning-pipeline.md.

Errors vs raises

  • ArgumentError — some invalid shapes are rejected synchronously before backend dispatch.
  • {:error, reason} — operational failures, including backend errors and tuples such as {:transport_failed, _}, for callers that want to branch.

search!/3 raises Scrypath.Search.Error with the same reason instead of returning {:error, _}.

search!(schema_module, text, opts \\ [])

@spec search!(module(), String.t(), keyword()) :: term()

Like search/3, but returns the same hydrated search result payload or raises Scrypath.Search.Error when the non-bang API would return {:error, _}.

search_many(entries, opts \\ [])

@spec search_many(
  list(),
  keyword()
) :: {:ok, term()} | {:error, term()}

Full pipeline: see guides/per-query-tuning-pipeline.md.

search_many/2 merge rules for shared vs per-entry options, :all expansion, and federation payloads remain canonical in guides/multi-index-search.md.

:per_query may appear on shared keywords and on each per-entry tuple; when both sides supply it, inner keys shallow-merge with entry bias — see guides/per-query-tuning-pipeline.md.

Federated search across multiple schemas.

Entries mirror search/3 tuples; optional federation_weight: tunes Meilisearch merge ordering for that row and requires a backend that implements search_many/2. Invalid weights use {:invalid_options, {:federation_weight, _}}; backends without native multi-search return {:invalid_options, {:federation_merge_requires_native_search_many, %{backend: _}}}.

Successful responses are {:ok, multi_search_result} with per-schema search results. When the backend returns a flat federated hits list, merge_hit_order may be populated so callers can inspect merged ordering; otherwise merge_hit_order is nil.

See also guides/multi-index-search.md § Federation weights.

Errors vs raises

  • {:error, reason} — preflight and transport failures you should branch on ({:invalid_options, _}, {:validation_failed, _, _}, {:transport_failed, _}, …).
  • search_many!/2 raises Scrypath.Search.Error with the same reason instead of returning {:error, _}.

search_many!(entries, opts \\ [])

@spec search_many!(
  list(),
  keyword()
) :: term()

Like search_many/2, but returns %Scrypath.MultiSearchResult{} or raises Scrypath.Search.Error when the non-bang API would return {:error, _}.

search_within_facet(schema_module, text, facet_bucket, opts \\ [])

@spec search_within_facet(module(), String.t(), {atom(), term() | list()}, keyword()) ::
  {:ok, term()} | {:error, term()}

Full-text search scoped to a single facet bucket, AND-combined with any other filter:, facet_filter:, sort, and pagination options.

facet_bucket is {facet_attribute, value} where value is either a scalar (one bucket) or a list interpreted as OR within that attribute, matching facet_filter: encoding. Passing the same attribute again under facet_filter: is rejected with ArgumentError — use search/3 or keep only one source of truth.

Errors vs raises

  • ArgumentError — structural misuse of the facet bucket or duplicate facet locks.
  • {:error, reason} — same operational {:error, _} family as search/3.

search_within_facet!/4 raises Scrypath.Search.Error when the non-bang call would return {:error, _}.

search_within_facet!(schema_module, text, facet_bucket, opts \\ [])

@spec search_within_facet!(module(), String.t(), {atom(), term() | list()}, keyword()) ::
  term()

sync_record(schema_module, record, opts \\ [])

@spec sync_record(module(), struct() | map(), keyword()) ::
  {:ok, term()} | {:error, term()}

Upserts or deletes search documents through the configured backend.

On success, {:ok, map} includes at least :mode (for example :inline, :oban, or :manual) and :status:

  • :status :accepted — work was queued or accepted by the backend or queue layer; documents may not be queryable yet. This is normal for :manual, :oban, and sometimes :inline when no Meilisearch task wait applies. See guides/sync-modes-and-visibility.md.
  • :status :completed — the :inline path finished, including waiting for a terminal Meilisearch task when sync_mode: :inline and the backend returned a task handle.

Pitfalls when the database write “succeeded” but search lags: guides/common-mistakes.md.

Tagged {:error, reason} tuples may include {:timeout, _}, {:task_failed, _}, {:invalid_options, _, _}, and other operational heads. For user-facing copy, normalize them through your own application boundary instead of depending on internal helpers.

sync_records(schema_module, records, opts \\ [])

@spec sync_records(module(), [struct() | map()], keyword()) ::
  {:ok, term()} | {:error, term()}

Upserts or deletes search documents through the configured backend.

On success, {:ok, map} includes at least :mode (for example :inline, :oban, or :manual) and :status:

  • :status :accepted — work was queued or accepted by the backend or queue layer; documents may not be queryable yet. This is normal for :manual, :oban, and sometimes :inline when no Meilisearch task wait applies. See guides/sync-modes-and-visibility.md.
  • :status :completed — the :inline path finished, including waiting for a terminal Meilisearch task when sync_mode: :inline and the backend returned a task handle.

Pitfalls when the database write “succeeded” but search lags: guides/common-mistakes.md.

Tagged {:error, reason} tuples may include {:timeout, _}, {:task_failed, _}, {:invalid_options, _, _}, and other operational heads. For user-facing copy, normalize them through your own application boundary instead of depending on internal helpers.

sync_status(schema_module, opts \\ [])

@spec sync_status(
  module(),
  keyword()
) :: {:ok, Scrypath.Operator.Status.t()} | {:error, term()}