Elasticsearch/OpenSearch storage adapter implementing Orkestra.Projection.Storage.
Overview
This adapter wires Orkestra projectors to Elasticsearch or OpenSearch as the
read-model backend. It detects the engine at startup (ES 8.x vs OpenSearch 2.x),
creates the projection index with strict mappings on first start, and returns
pure write descriptors for each event — no HTTP is performed in write/4.
write/4
Returns a descriptor map describing the write operation for one event:
{:ok, %{action: :index, id: id, doc: doc}}— full-document upsert with a deterministic_id. The GenServer (Phase 7) ownsSnap.Document.index/6.{:ok, %{action: :skip}}— event handler returned:skip; no write needed.{:error, reason}— handler returned an error.
write/4 never calls Snap.Document.index or any HTTP function. It is
purely functional. The calling GenServer controls when and how to commit.
reset/2
Deletes all documents in the projection index via _delete_by_query with a
match_all query. Used during rebuild (Phase 9) before replaying the event
stream from position 0.
Required adapter_opts
When wiring up a projector with this adapter, the following opts are required:
:cluster— theSnap.Clustermodule to use (e.g.MyApp.ESCluster):index— the Elasticsearch index name (e.g."orders"):handler— 3-arity function(projector_name, event, position) -> {:ok, doc, id} | :skip | {:error, reason}
For reset/2, also requires:
:cluster— theSnap.Clustermodule:index— the Elasticsearch index name
init/1
Called at projector startup (Phase 7 GenServer). Runs engine detection and
ensures the index exists with dynamic: strict enforced on all mappings.
Returns {:ok, state} where state includes :cluster, :index, and
:engine (:elasticsearch or :opensearch).
Security
Credentials (Basic Auth or API key) flow from application config into the
Snap cluster's HTTP Authorization header. Never commit credentials to
source control. Use runtime configuration and a secrets manager in
production. Always use https:// in production clusters.
Engine Detection
At startup, init/1 calls GET / on the cluster to detect the engine:
- Response contains
version.distribution: "opensearch"→:opensearch - Response contains
versionwithoutdistribution→:elasticsearch - Connection failure → defaults to
:elasticsearchwith a warning log
The detected engine atom is stored in adapter state for downstream use by the GenServer (Phase 7) and future phases.
Summary
Functions
Initialises the adapter at projector startup.
Deletes all documents in the projection index via _delete_by_query.
Returns a write descriptor for applying event to the Elasticsearch read model.
Functions
Initialises the adapter at projector startup.
Detects the ES/OpenSearch engine, creates the projection index with strict mappings if needed, and returns an adapter state map.
Two provisioning paths are supported, selected by the presence of :schema:
- Legacy path — opts carry
:index(the raw index name) and:projector_module(which implementsindex_mapping/0). The index is created viaOrkestra.ES.Index.ensure_index/3. - Schema path — opts carry
:schema(anOrkestra.ES.Schemamodule),:index(the resolved alias) and optional:culture. The alias + versioned index (with the schema mapping and_metahash) is provisioned viaOrkestra.ES.Index.setup/3; an existing alias is a no-op.
Requires opts:
:cluster— theSnap.Clustermodule:index— the index name (legacy) or resolved alias (schema path):projector_module— the projector module implementingindex_mapping/0(legacy path):schema— anOrkestra.ES.Schemamodule (schema path):culture— the culture atom ornil(schema path)
Returns {:ok, %{cluster: cluster, index: index, engine: engine}} (where
index is the alias on the schema path) or {:error, reason} if index
provisioning fails.
@spec reset( Orkestra.Projection.Storage.projector_name(), Orkestra.Projection.Storage.opts() ) :: :ok | {:error, term()}
Deletes all documents in the projection index via _delete_by_query.
Uses a match_all query to clear the entire index. This is a destructive
operation intended for projector rebuild (Phase 9). After reset/2, a
subsequent replay of the event stream will rebuild the read model.
Idempotent: returns :ok even when the index does not exist yet
(e.g. on first start before init/1 has run, or after manual index
deletion). An index_not_found_exception from ES/OpenSearch is treated
as a no-op — the index is already empty.
Requires opts:
:cluster— theSnap.Clustermodule:index— the index name string
Returns :ok on success or {:error, {:reset_failed, reason}} on failure.
@spec write( Orkestra.Projection.Storage.projector_name(), Orkestra.Projection.Storage.event(), non_neg_integer(), Orkestra.Projection.Storage.opts() ) :: {:ok, map()} | {:error, term()}
Returns a write descriptor for applying event to the Elasticsearch read model.
The :handler option must be a 3-arity function:
(projector_name :: String.t(), event :: map(), position :: non_neg_integer())
-> {:ok, doc :: map(), id :: String.t()} | :skip | {:error, reason :: term()}Returns:
{:ok, %{action: :index, id: id, doc: doc}}when handler returns{:ok, doc, id}{:ok, %{action: :skip}}when handler returns:skip{:error, reason}when handler returns{:error, reason}
Does not perform any HTTP calls. The calling GenServer owns execution.