Dowser. Codec
(Dowser.Client v0.5.0)
View Source
The engine behind each backend package's codec — the module a repo names as
its Dowser.Client :decoder and :encoder:
config :my_app, MyApp.Search,
endpoint: "http://localhost:9200",
decoder: Dowser.OpenSearch.Codec,
encoder: Dowser.OpenSearch.CodecA backend codec is three one-line functions handing their arguments here
with its own config/0: which mapping cacher it reads, which application
environment and telemetry prefix are its own, and which mapping types are
opaque on that backend (flat_object on OpenSearch, flattened on
Elasticsearch).
Casting itself is split three ways, each in its own module:
Dowser.Envelopefinds the documents in a response body;Dowser.Sourcewalks one document against its mapping — objects, nested fields, arrays, structs;Dowser.Typecasts one leaf value — a date, a range, an IP address.
This module decides which mapping they work against, and what happens when it can't be had.
decode/3
Casts every document found in a response body (see Dowser.Envelope), each
against its own index's mapping. With source: true the body is a bare
_source document with no _index of its own, cast against opts[:index]'s
mapping.
encode/3
Casts one document source against the mapping of opts[:index] — never a
query. dowser_client only hands an encoder a source, because a query value
has no mapping entry to anchor it: the same value can appear in a range
clause, a script parameter or an aggregation boundary, each wanting a
different shape. Build queries in the shape the backend expects.
encode_bulk/3 is the exception to one-source-at-a-time: a bulk payload is
cast against the index named on the action line above it, which no per-line
pass can see.
Options
dowser_client always supplies :repo_config (the request's repo, resolved
into a Dowser.Repo.Config), and a decoder also gets :key_fn (the function
:keys resolved to). The API function sets :index (the index a source is
cast against) and :source (true when a response body is a bare _source
document). The rest decide how documents are cast:
:mapping— where the mapping comes from: a map (the mapping itself), a module implementingDowser.Mapping, or a document module (its own mapping). Defaults to the backend's mapping cacher — the cluster's mapping, which the application has to start. Documents come back as maps.:struct— a document module (Dowser.Schema): every document is loaded into its struct, and its struct dumped back, against its own mapping. Passing a different:mappingalongside it is anArgumentError.:types— theDowser.Typemodule leaf values are cast through (defaultDowser.Type).:unknown_fields— a field the mapping doesn't declare is:keep-t as it is (default), or:raises aDowser.UnknownFieldError. Maps only: a struct has no room for an undeclared field, so loading one always raises.:mapping_failure— what to do when the mapping cannot be fetched; see below. Default:error.
Each is resolved per request, then from the options the repo gives the
codec (decoder: {Dowser.OpenSearch.Codec, types: MyApp.Types}), then from
the backend's application environment
(config :dowser_opensearch, types: MyApp.Types) — except :struct, which
only makes sense per request. So the application-wide mapping source is
config :dowser_opensearch, mapping: MyApp.Mappingswith the backend's cacher the default when nothing is set. :struct always
casts against its own mapping, whatever the application sets.
When the mapping can't be fetched
An index with no mapping, and no index to speak of, both cast to identity: there is nothing to dispatch on, and that is a correct answer.
A mapping that could not be fetched is a different thing. The fetch is a
request like any other, so it fails when the cluster is overloaded — and
casting to identity there means the same field comes back as a Date.Range
on a good day and a %{"gte" => _, "lte" => _} on a bad one, a document
source is written with a Date.Range the JSON encoder chokes on, and nothing
says so. :mapping_failure decides what happens instead:
:error(default) — raiseDowser.MappingError, whichdowser_clientturns into the{:error, exception}every API function already returns. The request fails; the types don't drift.:warn— log a warning and cast to identity.:ignore— cast to identity, silently.
Where :telemetry is available — an optional dependency, so only if your
application already pulls it in — every failed fetch also emits
[otp_app, :mapping, :failure] ([:dowser_opensearch, :mapping, :failure]
for OpenSearch) whatever the policy, with
%{index: index, reason: reason, policy: policy} as metadata.
Summary
Functions
Casts every document found in body, returning the decoded term.
Casts source against the mapping of opts[:index] — or, with :struct,
against the struct's — returning the encoded term.
Casts the payloads of a bulk operation list.
Types
A backend codec's own settings:
:otp_app— the backend's application: where:types,:unknown_fieldsand:mapping_failureare read from, and the telemetry event prefix.:cacher— itsDowser.MappingCachermodule, the default:mapping.:opaque— the mapping types whose subtree is never cast.
Functions
Casts every document found in body, returning the decoded term.
See the module documentation for the options; :key_fn is required, and
dowser_client always supplies it.
Casts source against the mapping of opts[:index] — or, with :struct,
against the struct's — returning the encoded term.
Without an :index or a :mapping there is no mapping to cast against, and
the source passes through unchanged. See the module documentation for the
options.
Casts the payloads of a bulk operation list.
A bulk body is a flat list alternating action and payload maps, so a payload
only knows which index it is going to from the action line above it — which
is why it is cast here, over the whole list, rather than line by line through
Dowser.Client's :encode.
fun is the resolved encoder (a codec's encode/2 or a custom one) and
opts its options; opts[:index] is the bulk-level default index, which a
per-action _index overrides. index/create actions have their whole
payload cast, update actions only their doc/upsert source, and
delete actions carry no payload to cast.