Pure builder and response parser behind Orkestra.ES.Repository.get_paged/1.
build/2 compiles a high-level option list into a full Elasticsearch
_search request body; parse_response/3 turns the search response back into
an Orkestra.ES.Page. Both functions are pure — they only manipulate maps,
lists and strings, have no dependency on Snap, and are therefore testable
without any HTTP layer. build/2 is layered on top of Orkestra.ES.Query for
the bool-query and sort assembly.
Options
:search— a query string. Compiled into amulti_matchover the schema'ssearchable_fields. Themulti_matchuses the defaultbest_fieldstype (natural relevance boosting across fields) and runs inmustcontext so it contributes to the score. Requesting a search on a schema with no searchable fields returns{:error, :no_searchable_fields}.Searchable fields inside embeds participate too. Fields reached through
mode: :objectembeds enter themulti_matchwith their dotted path ("items.name"). Fields insidemode: :nestedembeds cannot be matched by a root-levelmulti_match(an Elasticsearch limitation), so when any nested embed carries searchable fields themustclause becomes an innerboolshould(minimum_should_match: 1) containing the root+objectmulti_matchplus onenestedquery (correctpath) with amulti_matchon its fields for each such embed — recursively for nested-inside-nested (composed paths).{:error, :no_searchable_fields}is returned only when no searchable field exists anywhere in the tree.:filters— a keyword list or map offield => spec. The clause is derived from the field's declared type::keyword/:boolean/{:array, :keyword}— a scalar becomes aterm, a list becomes aterms, both infiltercontext.numeric (
:integer/:long/:float/:double) and:date— a scalar becomes aterm; a{:gt | :gte | :lt | :lte, value}tuple becomes a one-sidedrange; a{:range, from, to}becomes agte/lterange(anilbound is omitted); a list of op tuples is merged into a single combinedrange. All infiltercontext.:text— amatch, inmustcontext (contributes to the score).the facets slot (see below) — a list/keyword of
{attr_code, value_code}pairs. Each pair produces onenestedquery infiltercontext matchingattr_codeandvalue_code(a list of value codes becomes aterms). Multiple pairs are AND-combined.an embed name — the spec is a keyword list (or map) of sub-filters over the embedded schema's fields, each derived from its declared type exactly as above:
filters: [items: [sku: "X", quantity: {:gte, 2}]]For a
mode: :objectembed every sub-filter becomes an independent clause on the dotted path ("items.sku","items.quantity") in its usual context. Beware the cross-entry false positives: with anembeds_manyin object mode the sub-filters are not correlated to the same entry — a document matches if any entry satisfies each condition separately. For amode: :nestedembed the sub-filters are combined into onenestedquery with an innerbool, so all conditions must hold on the same entry. Sub-filters may recurse into deeper embeds by name. An unknown field inside an embed returns{:error, {:unknown_filter_field, "items.sku_typo"}}with the full dotted path.
An unknown top-level field returns
{:error, {:unknown_filter_field, field}}.:facets—false(default),true, or a list of attribute codes. Requires the schema to declare a facets slot, otherwise{:error, :no_facets_field}. The aggregation is anestedagg on the facets path, atermsonattr_code(size 100, optionally restricted withincludewhen a code list is given), each attribute carrying a size-1termsonattr_namefor its display name and atermssub-aggregation onvalue_code(size 100) with a size-1termsonvalue_name. Because all active filters live in the query, the aggregation counts reflect them automatically. Facets are therefore conjunctive (a filtered attribute constrains the counts of the others); disjunctive facets are out of scope. The size limits (100 attributes, 100 values per attribute) are fixed.:sort— a keyword list offield => :asc | :desc. The field must exist; a:textfield is only sortable through itskeywordsub-field, so it requireskeyword: trueorsortable: true, otherwise{:error, {:not_sortable, field}}. Sorting on embedded fields (either the embed name or a dotted path) is not supported — a current limitation — and returns{:error, {:not_sortable, field}}. The schema'sprimary_keyis always appended as a finalasctiebreaker (unless already present), which keepssearch_aftercursors stable even when no sort is supplied.:page/:page_size— offset pagination (defaults1/20), compiled intofrom/size.:after— a cursor string forsearch_afterpagination, mutually exclusive with:page({:error, :conflicting_pagination}). The cursor is the URL-safe Base64 of the JSON-encoded sort values of a previous page's last hit; a malformed cursor returns{:error, :invalid_cursor}.
The request body always sets "track_total_hits" => true so total is exact
for total_pages.
Summary
Functions
Builds a full Elasticsearch _search request body from opts.
Turns a search response into an Orkestra.ES.Page.
Functions
Builds a full Elasticsearch _search request body from opts.
Returns {:ok, body} or {:error, reason} (see the module doc for the
possible reasons). body is a string-keyed map ready to hand to
Snap.Search.search/3.
@spec parse_response(module(), keyword(), map() | struct()) :: Orkestra.ES.Page.t()
Turns a search response into an Orkestra.ES.Page.
response may be a raw response body map (string-keyed) or a
Snap.SearchResponse struct — the parser reads both without referencing Snap
at compile time. opts are the same options passed to build/2 (they carry
the pagination mode and whether facets were requested).