Declarative, Ecto-like schema DSL for Elasticsearch/OpenSearch read models.
A schema declares its index, optional cultures, index settings with per-culture analyzers, and a set of typed fields. From that declaration the macro generates a struct, introspection, the full ES index mapping (analyzers included), a deterministic mapping hash, and document casting — all as a pure module (it produces only maps and structs, never calls Snap), so it can be tested without any storage dependency.
Defining a schema
defmodule MyApp.Search.Product do
use Orkestra.ES.Schema,
index: "products",
cultures: [:it, :en],
default_culture: :it
settings number_of_shards: 1 do
analyzer :product_search, for: :it,
tokenizer: "standard", filter: ["lowercase", "asciifolding", :stemmer_it]
analyzer :product_search, for: :en,
tokenizer: "standard", filter: ["lowercase", "porter_stem"]
filter :stemmer_it, for: :it, type: "stemmer", language: "light_italian"
end
schema do
field :product_id, :keyword, primary_key: true
field :name, :text, analyzer: :product_search, searchable: true, keyword: true
field :category, :keyword
field :price, :float
field :released_at, :date, sortable: true
field :tags, {:array, :keyword}
facets :attributes
end
endOptions for use
:index(required) — the base index name.:cultures(optional) — a list of atoms; when present the schema is multi-culture and gets one alias per culture (products_it). Omitting it yields a mono-culture schema with a single unsuffixed alias.:default_culture(required with:cultures) — must belong to:cultures.
Field types
:keyword, :text, :integer, :long, :float, :double, :boolean,
:date, and {:array, scalar}.
Field options
primary_key: true— exactly one field, must be:keyword. Its value is used as the document_id.analyzer: :name—:textonly; references a logical analyzer defined per culture insettings.searchable: true—:textonly; marks the field for full-text search.keyword: true—:textonly; adds a"keyword"sub-field of type keyword.sortable: true— for:textimplies the keyword sub-field (which is the one to sort on); for other types it is metadata only.format:—:dateonly; a custom ES date format. Fields with a custom format keep their raw string when decoded.default:— the struct default for the field.
Generated API
t()struct with all fields (plus the facets slot, defaulting to[]).__es_schema__/1— introspection (see below).alias_for/0,alias_for/1— the index alias, per culture.mapping/0,mapping/1— the full string-keyed index mapping.mapping_hash/0,mapping_hash/1— deterministic SHA-256 of the mapping.to_doc/1— struct to indexable document.from_hit/1—_sourcemap to struct.
__es_schema__/1 accepts :index, :cultures ([] for mono-culture),
:default_culture, :fields (a list of %{name:, type:, opts:}),
:field_names, :primary_key, :searchable_fields, :facets_field, and
:sortable_fields.
Facets
A schema may declare a single facets :field_name slot with the fixed
structure defined by Orkestra.ES.Facet (attribute code/name owning
values code/name/count). It maps to a flattened nested field
(attr_code/attr_name/value_code/value_name).
Embedded schemas
A schema declared with embedded: true describes a nested struct that
lives inside a root document rather than an index of its own:
defmodule MyApp.Search.OrderItem do
use Orkestra.ES.Schema, embedded: true
schema do
field :sku, :keyword
field :name, :text, searchable: true, analyzer: :product_search
field :quantity, :integer
end
endRoot-only constructs are forbidden on an embedded schema and raise an
ArgumentError at compile time: the :index / :cultures /
:default_culture options, the settings block, primary_key: true fields,
and the facets slot. Analyzer references (analyzer: :name) are allowed —
they are resolved by the root schema, which validates per-culture
coverage over its entire embed tree. Recursive embedding (an embedded schema
that itself declares embeds_one/embeds_many) is fully supported.
An embedded schema still generates the struct, __es_schema__/1
introspection (including :embedded? → true and :analyzer_refs),
to_doc/1 and from_hit/1, so it works standalone in tests.
Embedding into a root schema
Inside the schema block of a root (or of another embedded schema):
embeds_one :shipping, MyApp.Search.Address # mode: :object
embeds_many :items, MyApp.Search.OrderItem, mode: :nestedembeds_one defaults the struct field to nil, embeds_many to []. The
target module must be a schema compiled with embedded: true. Embed names
are exposed via __es_schema__(:embeds) (not :field_names) as maps
%{name:, schema:, cardinality: :one | :many, mode: :object | :nested}.
mode: :object vs mode: :nested
:object(the default) maps the embed as a plain"object". ES flattens object arrays into parallel value lists, so correlation between fields of the same entry is lost: with items[%{sku: "A", qty: 1}, %{sku: "B", qty: 5}]a combined filtersku == "A" and qty >= 5matches (false positive) because each condition is satisfied by some entry. Cheapest option; fine forembeds_oneor when cross-field correlation does not matter.:nestedmaps the embed as"nested", preserving per-entry correlation (each entry is indexed as a hidden internal document): the filter above does not match. Costs extra internal documents and heavier queries — use it when combined filters onembeds_manyentries must be correlated.
Summary
Functions
Declares a per-culture analyzer. Use for: to scope it to a culture.
Declares a per-culture character filter.
Declares a list of embedded structs.
Declares a single embedded struct field.
Declares the (single) facets slot for the schema.
Declares a typed field. See the module doc for types and options.
Declares a per-culture token filter.
Declares a per-culture normalizer.
Wraps the field/facets declarations of the schema.
Declares index-level settings and, in its block, the analysis definitions
(analyzer/2, filter/2, tokenizer/2, char_filter/2, normalizer/2).
Declares a per-culture tokenizer.
Functions
Declares a per-culture analyzer. Use for: to scope it to a culture.
Declares a per-culture character filter.
Declares a list of embedded structs.
schema must be a module compiled with use Orkestra.ES.Schema, embedded: true. The struct field defaults to [].
Options
mode:—:object(default) or:nested.:objectflattens the array (combined filters may produce cross-entry false positives);:nestedpreserves per-entry correlation at extra index/query cost. See the module doc for the full trade-off.
Declares a single embedded struct field.
schema must be a module compiled with use Orkestra.ES.Schema, embedded: true. The struct field defaults to nil.
Options
mode:—:object(default) or:nested. See the module doc ("mode: :objectvsmode: :nested") for the flattening / correlation trade-off.
Declares the (single) facets slot for the schema.
Declares a typed field. See the module doc for types and options.
Declares a per-culture token filter.
Declares a per-culture normalizer.
Wraps the field/facets declarations of the schema.
Declares index-level settings and, in its block, the analysis definitions
(analyzer/2, filter/2, tokenizer/2, char_filter/2, normalizer/2).
Root-only: an embedded schema (embedded: true) raises an ArgumentError
at compile time, since index settings and analyzers belong to the root.
Declares a per-culture tokenizer.