Tokenized, multi-field ILIKE/LIKE search compiled to a composable
Ecto.Query.dynamic/2.
Decant turns a free-text search string into a boolean expression over a set
of columns, then hands it back as a dynamic you splice into your own query:
filter =
Decant.dynamic(term,
fields: [
{:customer, :email},
{:customer, :first_name},
{:customer, :last_name},
{:order, :display_id, cast: :string}
]
)
from q in query, where: ^filterThe shape
A search string is split into tokens (words). The default logic is:
every token must match SOMEWHERE (token_logic: :and)
token matches if ANY field hits (field_logic: :or)so "jane gmail" matches a row whose email contains jane and whose name (or
email, or any listed field) contains gmail. Flip :token_logic /
:field_logic to get "or search" or "match-all-fields" behaviour.
Bindings
Decant is binding-agnostic: it references columns through named bindings
(as:), so the same field spec works no matter the join shape of the host
query. Every searchable source must declare an as::
from o in Order, as: :order,
join: c in assoc(o, :customer), as: :customerOptions
:fields(required) — list of field specs. Each is{binding, column}or{binding, column, field_opts}.field_opts::cast—:stringwraps the column inCAST(? AS TEXT)so non-text columns (integer ids, enums) are searchable.:match— per-field override of the global:matchmode.
:match—:contains(default),:prefix,:suffix, or:exact.:token_logic—:and(default) or:or. How tokens combine.:field_logic—:or(default) or:and. How fields combine per token.:case—:insensitive(default,ILIKE) or:sensitive(LIKE).:escape— escape%,_,\in user input so they are matched literally instead of acting as wildcards. Defaults totrue.:on_blank— what a blank/no-token term resolves to::all(default,dynamic(true)— don't constrain an empty search) or:none(dynamic(false)— match nothing).:tokenizer— keyword opts forwarded toDecant.Tokenizer.tokenize/2(:pattern,:trim,:drop_empty,:downcase,:max_tokens).
Empty input
A nil, blank, or all-whitespace term tokenizes to []. By default
dynamic/2 then returns dynamic(true) — an always-true filter — so callers
can unconditionally write where: ^Decant.dynamic(term, ...) with no
branching, and the planner discards the WHERE true. Pass on_blank: :none
when an empty search should instead return no rows (dynamic(false)).
Summary
Functions
Build an Ecto.Query.dynamic/2 from a search term and opts.
Functions
@spec dynamic( String.t() | nil, keyword() ) :: Ecto.Query.dynamic_expr()
Build an Ecto.Query.dynamic/2 from a search term and opts.
See the module doc for the full option list. Returns dynamic(true) when the
term yields no tokens.