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.Codec

A 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.Envelope finds the documents in a response body;
  • Dowser.Source walks one document against its mapping — objects, nested fields, arrays, structs;
  • Dowser.Type casts 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 implementing Dowser.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 :mapping alongside it is an ArgumentError.
  • :types — the Dowser.Type module leaf values are cast through (default Dowser.Type).
  • :unknown_fields — a field the mapping doesn't declare is :keep-t as it is (default), or :raises a Dowser.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.Mappings

with 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) — raise Dowser.MappingError, which dowser_client turns 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

Types

A backend codec's own settings

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

config()

@type config() :: %{otp_app: atom(), cacher: module(), opaque: [String.t()]}

A backend codec's own settings:

  • :otp_app — the backend's application: where :types, :unknown_fields and :mapping_failure are read from, and the telemetry event prefix.
  • :cacher — its Dowser.MappingCacher module, the default :mapping.
  • :opaque — the mapping types whose subtree is never cast.

Functions

decode(body, opts, config)

@spec decode(term(), keyword(), config()) :: term()

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.

encode(source, opts, config)

@spec encode(term(), keyword(), config()) :: term()

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.

encode_bulk(operations, fun, opts)

@spec encode_bulk([map()], (term(), keyword() -> term()), keyword()) :: [map()]

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.