Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.4.1] - 2026-09-25
Fixed
mix.lockstill pinneddowser_client0.2.1, which 0.4.0's own requirement (~> 0.3.0) rules out. It never reached users —mix.lockis not part of the published package — but a fresh checkout of the repository had a lock that contradictedmix.exs, somix testrefused to run untilmix deps.getresolved it. The lock now names 0.3.0.
[0.4.0] - 2026-09-25
Requires dowser_client ~> 0.3.0, which only retries a request that cannot
already have been applied. Set DOWSER_CLIENT_PATH to build against a working
copy of it.
Added
Every endpoint says whether it is safe to retry.
dowser_clientderives that from the HTTP method, which is right for a write and wrong for the many Elasticsearch reads that arePOSTrequests only because they carry a body. So each endpoint now marks itself:- retried after an ambiguous failure (a timeout, a dropped connection, a
502/504):search,msearch,count,explain,field_caps,terms_enum,search_mvt,search_template,msearch_template,render_search_template,scroll,mget,termvectors,mtermvectors,analyze,validate_query,disk_usage,simulate_index_template,simulate_template,allocation_explain, andindex/3when it is given an:id; - not retried: everything that writes, including
index/3with an Elasticsearch-generated id (each attempt would be a new document),create/4(the second attempt is a409),update/4, the by-query endpoints andreindex/2.
bulk/2decides per payload: retried only when every action is anindexor adeletenaming its own_id, so a timed-out bulk can never index the same documents twice. A:retryyou pass yourself still wins.- retried after an ambiguous failure (a timeout, a dropped connection, a
Mappings that are never fetched. A mapping can now be given outright, so nothing has to be asked of the cluster — which is also what makes casting testable without stubbing the cacher:
# application-wide, no running cacher needed config :dowser_elasticsearch, mappings: %{"posts" => %{"properties" => %{"published_at" => %{"type" => "date"}}}} # per test, through a running cacher MappingCacher.put("posts", %{"properties" => %{}}) # per request Dowser.Elasticsearch.Document.get("posts", "1", decoder: {Codec, mapping: mapping})A static mapping wins over the cache and never expires.
Dowser.Elasticsearch.MappingCacher.lookup/2, which tells a mapping that cannot exist ({:ok, nil}) from one that could not be fetched ({:error, reason}).fetch/2is unchanged, and remains the lenient read.
Changed
A mapping that can't be fetched no longer silently turns casting off.
Dowser.Elasticsearch.Codeccast to identity whenever no mapping came back, whether because there was none or because the_mappingrequest failed — and that request fails exactly when the cluster is overloaded. Adate_rangefield was aDate.Rangeon a good day and a raw%{"gte" => _, "lte" => _}on a bad one; on the way out, aDate.Rangewas handed to the JSON encoder uncast. Nothing reported it.A failed fetch now raises
Dowser.Elasticsearch.MappingError, which comes back as the{:error, exception}every API function already returns, so the request fails instead of the types drifting. The new:mapping_failureoption takes:error(default),:warn(log, cast to identity) or:ignore(the behaviour before 0.4.0), per request or application-wide:config :dowser_elasticsearch, mapping_failure: :warnAn index with no mapping, or no index at all, still casts to identity: that is an answer, not a failure.
Where
:telemetryis available (a new optional dependency — nothing is forced on an application that doesn't already use it), every failed fetch also emits[:dowser_elasticsearch, :mapping, :failure], whatever the policy.Document.bulk/2no longer reports a partial failure as a success. Elasticsearch answers a bulk request200 OKwith"errors" => trueand oneitemsentry per action, so a request whose documents were half rejected — a per-item429under load, a mapping failure — was indistinguishable from one in which everything was indexed:bulk/2returned{:ok, body}andbulk!/2raised nothing.{:ok, body}now means every item was applied. As soon as one failed, the result is{:error, %Dowser.Elasticsearch.BulkError{}}, which carries the failed items (with the operation that caused each), how many succeeded, and:retryable— the operations Elasticsearch rejected (429/503), ready to hand straight back tobulk/2:case Dowser.Elasticsearch.Document.bulk(operations, index: "posts") do {:ok, _body} -> :ok {:error, %BulkError{retryable: [_ | _] = operations}} -> Dowser.Elasticsearch.Document.bulk(operations, index: "posts") endOnly the rejected items are listed there: resubmitting the whole payload would write the successful ones a second time. A caller that wants the raw response body still has it, on the error's
:body.bulk!/2raises that error, so it now raises unless every item was applied.
Fixed
An error body is read whatever its keys are.
Dowser.Elasticsearch.Errorextracted:type/:reasonfrom a string-keyed body only, but the body it receives has already been through the client's:keyspass — so underkeys: :atoms/:atoms!nothing was found and every message read just"Elasticsearch responded with HTTP 400". Keys are now matched by name, so string- and atom-keyed bodies both yield a type and a reason.# before, under keys: :atoms "Elasticsearch responded with HTTP 404" # after "Elasticsearch responded with HTTP 404: [index_not_found_exception] no such index [missing]"A nested cause is surfaced in the reason. When the error object carries a
root_causeentry (or acaused_by) whose reason says more than the error's own, it is appended — thesearch_phase_execution_exceptioncase, whose reason alone is only"all shards failed", now reads"all shards failed: No mapping found for [date]".
[0.3.1] - 2026-09-24
Fixed
An object shaped like a range is no longer decoded as one.
decode/2recognised a range by its%{"gte" => _, "lte" => _}shape alone, so an object mapped as twodatefields namedgteandlte— what a document written before the field was mapped as adate_rangeleaves behind — was handed to the field codec, which returned it untouched for want of a range"type". Since that path never callskey_fn, it came back string-keyed inside an otherwise atom-keyed document, and a caller readingperiod.gteunderkeys: :atomsgotnil.Such an object is now walked like any other: its keys go through
key_fnand each bound is cast against its own mapping entry.# mapping: %{"period" => %{"properties" => %{ # "gte" => %{"type" => "date"}, "lte" => %{"type" => "date"}}}} # before %{period: %{"gte" => "2026-08-01", "lte" => "2026-08-11"}} # after %{period: %{gte: ~D[2026-08-01], lte: ~D[2026-08-11]}}A genuine
date_rangefield is unaffected and still decodes to aDate.Range, whether it is held directly or inside an array. Code that reached into such an object with string keys has to switch to whatever:keyssays, and now gets cast bounds rather than raw strings.
[0.3.0] - 2026-09-21
Changed
Credo and Dialyzer run over the library.
credoanddialyxirare dev and test dependencies,.credo.exsis checked in, andmix lintrunsmix format --check-formatted,mix credo --strictandmix dialyzertogether — all three green. Nothing about the published package changes: both areruntime: false, and the PLTs live in the git-ignoredpriv/plts/. The specs that named the non-existentArgumentError.t()now sayDowser.Elasticsearch.Helpers.argument_error().A missing or empty required argument is an error, not an exception.
Dowser.Elasticsearch.Index,.Documentand.SearchraisedArgumentErrorstraight out of their non-bang functions when the index (or another required path parameter) was empty — the one failure that didn't follow the{:ok, _}/{:error, _}contract the rest of the response path does. It is now returned like any other:{:error, %ArgumentError{}}from the non-bang function, raised by the bang one, and still before any request reaches the cluster.Dowser.Elasticsearch.Index.create_index(%{}, nil) #=> {:error, %ArgumentError{message: "this endpoint requires an index, got: nil"}} Dowser.Elasticsearch.Index.create_index!(%{}, nil) ** (ArgumentError) this endpoint requires an index, got: nilCode that relied on the non-bang variants raising — a
try/rescue, or a bare call whose crash was the error handling — now gets an{:error, _}tuple instead. Code that already matched on the return value, and every bang variant, is unaffected. Theuse-time validation inDowser.Elasticsearch.Repositoryand thesliceoption check inDowser.Elasticsearch.Streamerstill raise: neither is a request.
Added
Dowser.Elasticsearch.XPack— every endpoint taggedxpackin the Elasticsearch specification:info/1(GET /_xpack) reports the build, the license and which features the cluster ships, andusage/1(GET /_xpack/usage) how much each of those features is actually used.Dowser.Elasticsearch.XPack.info!(params: [categories: "license"]) #=> %{"license" => %{"type" => "basic", "status" => "active", ...}} Dowser.Elasticsearch.XPack.usage!()["watcher"]["count"] #=> %{"active" => 2, "total" => 3}Dowser.Elasticsearch.Reindex— every endpoint taggedreindexin the Elasticsearch specification, which is the reindex task family, not the reindex itself:list/1(GET /_reindex) lists the tasks running,get/2(GET /_reindex/{task_id}) follows one, andcancel/2(POST /_reindex/{task_id}/_cancel) stops one.{:ok, %{"task" => task_id}} = Dowser.Elasticsearch.Document.reindex( %{source: %{index: "posts"}, dest: %{index: "posts-v2"}}, params: [wait_for_completion: false] ) Dowser.Elasticsearch.Reindex.get!(task_id)["completed"] #=> falseStarting a reindex stays
Dowser.Elasticsearch.Document.reindex/2, which the specification tagsdocument, as isreindex_rethrottle/3. A task is followed by its original id across node-shutdown relocations, so the id the reindex returned stays valid for the lifetime of the operation.Dowser.Elasticsearch.Cluster— every endpoint taggedclusterin the Elasticsearch specification, which covers both the cluster itself and the_nodesendpoints:health/1,info/2,ping/1,remote_info/1,get_settings/1,put_settings/2,state/1,stats/1,pending_tasks/1,allocation_explain/2,reroute/2,update_voting_config_exclusions/1,clear_voting_config_exclusions/1,nodes_info/1,nodes_stats/1,nodes_usage/1,nodes_hot_threads/1,nodes_reload_secure_settings/2,nodes_get_repositories_metering_info/2andnodes_clear_repositories_metering_archive/3.Dowser.Elasticsearch.Cluster.health!(params: [wait_for_status: "yellow"]) #=> %{"status" => "yellow", "number_of_nodes" => 1, ...} Dowser.Elasticsearch.Cluster.nodes_stats!(metric: "indices", index_metric: "docs")ping/1is theHEAD /check, and comes as the pair the rest of the library uses for those:{:ok, boolean()}fromping/1, the bare boolean fromping?/1.nodes_hot_threads/1answers in plain text, so its response format defaults to:raw./_cluster/stateand/_nodes/.../statsnest a second filter under their metric, which Elasticsearch can only read as the segment after one::indexwithout:metric(and:index_metricwithout:metric) returns{:error, %ArgumentError{}}rather than a path the cluster misreads.Dowser.Elasticsearch.Info— the endpoints taggedinfoin the Elasticsearch specification.info/1(GET /) returns the cluster's basic information: node name, cluster name and UUID, version and tagline — the usual way to check that a cluster is reachable and to read its version.{:ok, info} = Dowser.Elasticsearch.Info.info() info["version"]["number"] #=> "8.13.4"Dowser.Elasticsearch.HealthReport— the endpoint taggedhealth_reportin the Elasticsearch specification.health_report/1(GET /_health_report) returns the cluster's health report: one indicator per subsystem (master_is_stable,shards_availability,disk, …), each with itsgreen/unknown/yellow/redstatus, the explanation behind it, the impacts of a non-green one and, where Elasticsearch can tell, the diagnosis and the steps to fix it. The:featureoption restricts the report to one indicator — Elasticsearch resolves that path segment as a single name and answers a comma-joined list with a404, so a list of several is refused as{:error, %ArgumentError{}}(raised byhealth_report!/1) instead.{:ok, report} = Dowser.Elasticsearch.HealthReport.health_report() report["status"] #=> "green"Dowser.Elasticsearch.Cat— every endpoint taggedcatin the Elasticsearch specification:help/1,indices/1,count/1,aliases/1,shards/1,segments/1,recovery/1,fielddata/1,health/1,nodes/1,nodeattrs/1,master/1,allocation/1,circuit_breaker/1,thread_pool/1,pending_tasks/1,tasks/1,plugins/1,templates/1,component_templates/1,repositories/1,snapshots/1,transforms/1,ml_jobs/1,ml_datafeeds/1,ml_data_frame_analytics/1andml_trained_models/1. None of them takes a required attribute, so each one's optional path parameter is an option (:index,:name,:node_id, …) and the signatures areopts-only.The cat APIs answer in aligned text at a terminal, but they honour the
acceptheaderDowser.Clientalready sends: the response comes back as JSON and is decoded like any other endpoint — a list of string-keyed, string-valued maps, one per row.Dowser.Elasticsearch.Cat.indices!(index: "posts*", params: [s: "docs.count:desc"]) #=> [%{"index" => "posts", "health" => "green", "docs.count" => "42", ...}]help/1is the exception:GET /_catanswers in plain text whatever the header asks for — a banner line, then one endpoint per line — so its response format defaults to:rawand the body is parsed into the list of endpoints.Dowser.Elasticsearch.Cat.help!() #=> ["/_cat/allocation", "/_cat/shards", "/_cat/shards/{index}", ...]For the text a human reads, ask for it explicitly — the
formatquery parameter wins over the header, andresp_format: :rawkeeps the body from being parsed as JSON:Dowser.Elasticsearch.Cat.indices!(params: [format: "text", v: true], resp_format: :raw)
[0.2.2] - 2026-09-20
Fixed
A hit's
inner_hitswere never cast. 0.2.0 taught the decoder to key them like the rest of the response, but their values still came back exactly as JSON produced them, on the grounds that an inner hit carries no_indexto resolve a mapping from. It doesn't need one: it is a document of the same index as the hit that holds it, and_nested.fieldnames the path into that mapping. Adate_rangeunder anestedfield came back as a%{gte: _, lte: _}map where aDate.Rangewas expected, and every other mapped type was left uncast the same way.An inner hit's
_sourceis now cast against the mapping entry its_nestedchain points at — the whole chain, so a doubly nested inner hit resolves too. An inner hit with no_nested(ahas_childorhas_parentjoin) is a document of the index itself and is cast against the index mapping. A path the mapping doesn't know degrades to no cast rather than raising, as everywhere else in the codec.The rest of a hit's envelope —
fields,highlight,sort— has no mapping entry to be cast against and is unchanged: keys keyed, values as they arrived.
[0.2.1] - 2026-09-20
Re-release of 0.2.0, which never reached Hex. Nothing in the library itself changed: the 0.2.0 entry below describes everything in this release, and UPGRADE_0_2.md is still the path from 0.1.1. Upgrade straight from 0.1.1 to 0.2.1.
Changed
- Requires
dowser_client ~> 0.2.1, a maintenance release that drops a stalepoisonentry from itsmix.lock. No API or behavior change on either side.
[0.2.0] - 2026-09-20
Tracks dowser_client 0.2.0,
which drops every optional dependency and every pluggable adapter. See
UPGRADE_0_2.md for the migration path — this entry says
what changed, the guide says what to do about it.
Added
Dowser.Elasticsearch.Streamer— walks a whole search as a lazyStreamof hits, over a point in time andsearch_after, with_shard_docappended to the sort as the tiebreaker that makes the paging deterministic. The point in time is opened when enumeration starts and closed when it ends, however it ends.%{query: %{match_all: %{}}, size: 1_000} |> Dowser.Elasticsearch.Streamer.stream(index: "posts") |> Enum.each(&process/1)stream_slices/4runs a function over every slice of one shared point in time at once — a function rather than a stream, because a lazy stream handed back out of a task would run every page in the caller. Aslicein the query body walks a single slice instead, for fanning out across nodes.%{query: %{match_all: %{}}, size: 1_000} |> Dowser.Elasticsearch.Streamer.stream_slices(4, &Enum.count/1, index: "posts") |> Enum.sum()Neither has a bang variant: a stream has nothing to unwrap, and raises on enumeration anyway.
A
:codecoption on every API function, choosing the field codecDowser.Elasticsearch.Codecdispatchesload/2/dump/2through for one request. It resolves most-specific-first: request, then context (alongside the pass it belongs to), thenconfig :dowser_elasticsearch, codec: ..., thenDowser.Elasticsearch.Codecitself.Dowser.Elasticsearch.MappingCacher.fetch/2, aget/2returning the mapping ornilrather than a result tuple, andkey/2, an entry's cache key.Dowser.Elasticsearch.Codec.encode_bulk/3, which casts a bulk operation list against the index named on each action line.
Changed
:codec_adapterbecomes a:decoderand an:encoder, mirroringdowser_client's split of one whole-body adapter into two passes.Dowser.Elasticsearch.Codecfills both slots, anddecode/2,encode/2,load/2anddump/2keep the meanings they had in 0.1.1:config :dowser_client, - configs: [ + contexts: [ default: [ endpoint: "http://localhost:9200", - codec_adapter: Dowser.Elasticsearch.Codec + decoder: Dowser.Elasticsearch.Codec, + encoder: Dowser.Elasticsearch.Codec ] ]Casting stays opt-in: with neither configured, bodies are left exactly as JSON produced them and no mapping is ever fetched.
A query is never cast.
dowser_clientonly ever hands an encoder a document source, because a query value has no mapping entry to anchor it. Casting an old codec did on query values has to move into how the query is built.Dowser.Elasticsearch.Fields.*are nowDowser.Elasticsearch.Codec.*, under the module that dispatches to them. Theirload/2anddump/2are unchanged:- Dowser.Elasticsearch.Fields.Date.load(value, field) + Dowser.Elasticsearch.Codec.Date.load(value, field)@behaviour Dowser.Elasticsearch.Codecreplaces@behaviour Dowser.Client.Field, whichdowser_clientno longer ships, and theuse/castmacro pair that assembled a dispatcher is gone withDowser.Client.Codec.Builder. Covering one more mapping type is aload/2and adump/2clause plus a delegation back toDowser.Elasticsearch.Codec; delegating last inherits the built-in casts, thenilshort-circuit and the fall-through to identity, and a clause matching a built-in type replaces that cast.A custom field codec no longer means rewriting the envelope walker. In 0.1.1 a
Codec.Buildermodule only gotload/2/dump/2, so it could not be used on its own. Now the walking stays inDowser.Elasticsearch.Codecand:codecpoints it at yours.Every API function's
:configoption is now:context—dowser_clientrejects a request still carrying:configrather than silently sending it to the default cluster.Dowser.Elasticsearch.Document.update/4now casts anupsertsource as well as adocone, in either key style. A%{script: ...}body is still left alone.Dowser.Elasticsearch.MappingCacher's:fetchand:eageroptions take aDowser.Client.Context(or anythingDowser.Client.Context.resolve/1accepts) in place of a config.
Fixed
- A
datewas only cast when its value matched the declared format shape exactly. Each format was matched byte by byte, so a field mappedstrict_date_timeholding2026-09-20T20:46:03Z— no fractional second — matched no clause and came back as the string it arrived as. An index holds values written before its mapping, so the format describes how Elasticsearch writes a field, not everything it contains. Reading now parses any ISO 8601 date-time for any date-time format, including an offset (normalized to UTC) and a time with none at all (read as UTC, as Elasticsearch reads it). Date-only formats still read only a date,dump/2still writes the precision the format declares, and an unparseable value still passes through untouched. strict_date_optional_timemade the fraction mandatory. It is Elasticsearch's defaultdateformat, and everything after the date in it is optional — the time, its fractional second, the offset. It was listed among the date-only and the millisecond formats but not the second- precision ones, so a document written2026-09-20T17:39:09Zwas matched by no clause and came back as the string it arrived as. The two optional-time formats are now parsed rather than shape-matched, which also picks up offsets (+01:00, normalized to UTC), a time with no offset at all (read as UTC, as Elasticsearch reads it) and fractions of any length. An unparseable value still passes through untouched.- A hit's
inner_hitscame back string-keyed inside an otherwise atom-keyed response. Only_sourcewas walked; every other envelope field —inner_hits,fields,highlight— had its own key renamed and its value returned exactly as it arrived, sohit.inner_hitsunderkeys: :atomswas a map of string keys. There is no mapping entry to cast these against (an inner hit is a nested document and carries no_index), but their keys are part of the same response and now follow the same:keys. - A range held in an array was never cast.
date_rangeandinteger_rangewere handled only where the range is a direct child of an object; Elasticsearch lets any field hold an array, and a range reached through one arrived at the generic map walker instead, which cast its keys and never called the codec. The result was a%{gte: _, lte: _}map where aDate.Rangewas expected. - Under
keys: :atoms, the keys inside aflattenedfield — and inside an object mapped"enabled": false— were run throughString.to_atom/1. The mapping enumerates none of those keys: they are whatever the document put there, so a writer could mint unbounded atoms in a table that is never collected and is capped a little over a million, crashing the node. Both are now returned as they arrived, keys left as strings and values uncast. Note this is narrower than the whole risk —keys: :atomsstill casts keys the mapping doesn't mention, andkeys: :atoms!is the option that cannot grow the table at all. Dowser.Elasticsearch.MappingCacherkeyed entries by endpoint alone, so two contexts pointing at the same cluster with different credentials shared one cached mapping — whichever fetched first won, and the other was cast against a mapping it may not have been allowed to see (field-level security hides fields; an alias can resolve to a different concrete index). Entries are now keyed by{endpoint, scope, index}, where the scope is a truncated SHA-256 of the context's:authand:http_opts. The credentials are hashed rather than stored: a cache key sits in an ETS table any process can read, andDowser.Client.Contextredacts:autheven from its ownInspect. A context with neither scopes tonil, so the unauthenticated key stays readable.
Removed
- The
req,hackney,jasonandpoisonoptional dependencies, and the:http_adapter/:json_adapterconfiguration that selected between them. HTTP is OTP's:httpcand JSON is Elixir'sJSON. A struct sent in a request body needs@derive JSON.Encoderwhere it used to need@derive Jason.Encoder. - The
cast/2macro, along withuse Dowser.Client.Codec.Builderand@behaviour Dowser.Client.Field. A codec is now plain function clauses, so thecast: 2formatter entry goes too — dropimport_deps: [:dowser_client]from.formatter.exsif it was only there for that. :codec_opts. Whatever a pass needs travels with it, as{Dowser.Elasticsearch.Codec, index: "articles"}.
[0.1.1] - 2026-08-17
Added
Dowser.Elasticsearch.Repositorynow raises anArgumentErrorat compile time if two selected functions would generate the same name, naming the clash instead of silently producing broken duplicate definitions.
Changed
Dowser.Elasticsearch.Repositoryrenames the generatedDocumentfunctions that would otherwise share a base name with a same-named function from another selected module:create→create_doc,delete→delete_doc,exists/exists?→doc_exists/doc_exists?,get→get_doc,index→index_doc,update→update_doc. Repositories built withuse Dowser.Elasticsearch.Repositorymust switch to the new names;Dowser.Elasticsearch.Document's own functions are unaffected.Dowser.Elasticsearch.TypeCodec(the:codec_adapterimplementation) andDowser.Elasticsearch.Codec(the field-level dispatcher it delegated to) are merged into a singleDowser.Elasticsearch.Codec, which now implements both. Setcodec_adapter: Dowser.Elasticsearch.Codecinstead ofDowser.Elasticsearch.TypeCodec; custom field casts still inherit fromDowser.Elasticsearch.Codecthe same way.Dowser.Elasticsearch(the empty top-level module),Dowser.Elasticsearch.Helpers, andDowser.Elasticsearch.Mappableno longer generate documentation pages (@moduledoc false) — none of them are meant to be used directly.- Bumped the
dowser_clientrequirement to~> 0.1.1and switched to itsDowser.Client.Codec.Builder(theDowser.Client.CodecBuildername is deprecated upstream, though still functional).
[0.1.0] - 2026-08-17
Initial release.
Added
Dowser.Elasticsearch.Search— search-tagged endpoints:search,msearch,count,explain,field_caps,search_shards,terms_enum,search_mvt,search_template,msearch_template,render_search_template,rank_eval, async search (submit_async_search,get_async_search,get_async_search_status,delete_async_search),scroll/clear_scroll, and point-in-time (open_point_in_time/close_point_in_time).Dowser.Elasticsearch.Document— document-tagged endpoints:index,create,get,delete,exists?,get_source,source_exists?,update,bulk,mget,delete_by_query/delete_by_query_rethrottle,update_by_query/update_by_query_rethrottle,termvectors,mtermvectors,reindex/reindex_rethrottle.Dowser.Elasticsearch.Index— indices-tagged endpoints: index lifecycle (create_index,delete_index,get_index,index_exists?,open,close,add_block/remove_block), mappings (put_mapping,get_mapping,get_field_mapping), settings (put_settings,get_settings), aliases (put_alias,delete_alias,get_alias,alias_exists?,update_aliases),clone/shrink/split,refresh/flush/forcemerge/clear_cache, monitoring (stats,segments,recovery,shard_stores,disk_usage,field_usage_stats),analyze,validate_query,reload_search_analyzers,resolve_index,resolve_cluster,rollover, index/component templates (put_index_template,get_index_template,delete_index_template,index_template_exists?,simulate_index_template,put_component_template,get_component_template,delete_component_template,component_template_exists?), legacy templates — deprecated in favor of their index-template equivalents (put_template,get_template,delete_template,template_exists?,simulate_template), data lifecycle (delete_data_lifecycle), and dangling indices (list_dangling_indices,import_dangling_index,delete_dangling_index).Dowser.Elasticsearch.Repository—use-able repository pattern that binds the index-related functions ofSearch,Document, andIndexto a fixed or computed index, with:only/:exceptfiltering.Dowser.Elasticsearch.TypeCodec— optional whole-body type casting, set asdowser_client's:codec_adapter: casts dates, IPs, geo points, and ranges to and from native Elixir terms against each document's own index mapping, at any nesting depth in a response (a bare document,msearchresults, bulk items, …). Built onDowser.Elasticsearch.Codec(the per-field cast dispatcher, extensible viaDowser.Client.Codec.Builder) andDowser.Elasticsearch.MappingCacher(a cached, single-flight index-mapping fetcher, supervised by the application).Dowser.Elasticsearch.Error— the exception every non-2xx response is wrapped in, extracting:type/:reasonfrom a standard Elasticsearch error body when present.