Decant (Decant v0.1.0-beta.2)

Copy Markdown View Source

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: ^filter

The 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: :customer

Options

  • :fields (required) — list of field specs. Each is {binding, column} or {binding, column, field_opts}. field_opts:
    • :cast:string wraps the column in CAST(? AS TEXT) so non-text columns (integer ids, enums) are searchable.
    • :match — per-field override of the global :match mode.
  • :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 to true.
  • :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 to Decant.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

dynamic(term, opts)

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