Rummage.Ecto v2.0.0 Rummage.Ecto.Hook.Search View Source

Rummage.Ecto.Hook.Search is the default search hook that comes with Rummage.Ecto.

This module provides a operations that can add searching functionality to a pipeline of Ecto queries. This module works by taking fields, and search_type, search_term and assoc associated with those fields.

NOTE: This module doesn't return a list of entries, but a Ecto.Query.t. This module uses Rummage.Ecto.Hook.


ABOUT:

Arguments:

This Hook expects a queryable (an Ecto.Queryable) and search_params (a Map). The map should be in the format: %{field_name: %{assoc: [], search_term: true, search_type: :eq}}

Details:

  • field_name: The field name to search by.
  • assoc: List of associations in the search.
  • search_term: Term to compare the field_name against.
  • search_type: Determines the kind of search to perform. If :eq, it
              expects the `field_name`'s value to be equal to `search_term`,
              If `lt`, it expects it to be less than `search_term`.
              To see all the `search_type`s, check
              `Rummage.Ecto.Services.BuildSearchQuery`
  • search_expr: This is optional. Defaults to :where. This is the way current
               search expression is appended to the existing query.
               To see all the `search_expr`s, check
               `Rummage.Ecto.Services.BuildSearchQuery`

For example, if we want to search products with available = true, we would do the following:

Rummage.Ecto.Hook.Search.run(Product, %{available: %{assoc: [],
  search_type: :eq,
  search_term: true}})

This can be used for a search with multiple fields as well. Say, we want to search for products that are available, but have a price less than 10.0.

Rummage.Ecto.Hook.Search.run(Product,
  %{available: %{assoc: [],
    search_type: :eq,
    search_term: true},
  %{price: %{assoc: [],
    search_type: :lt,
    search_term: 10.0}})

Assoications:

Assocaitions can be given to this module's run function as a key corresponding to params associated with a field. For example, if we want to search products that belong to a category with category_name, "super", we would do the following:

category_name_params = %{assoc: [inner: :category], search_term: "super",
  search_type: :eq, search_expr: :where}

Rummage.Ecto.Hook.Search.run(Product, %{category_name: category_name_params})

The above operation will return an Ecto.Query.t struct which represents a query equivalent to:

from p0 in Product
|> join(:inner, :category)
|> where([p, c], c.category_name == ^"super")

ASSUMPTIONS/NOTES:

  • This Hook has the default search_type of :ilike, which is case-insensitive.
  • This Hook has the default search_expr of :where.
  • This Hook assumes that the field passed is a field on the Ecto.Schema that corresponds to the last association in the assoc list or the Ecto.Schema that corresponds to the from in queryable, if assoc is an empty list.

NOTE: It is adviced to not use multiple associated searches in one operation as assoc still has some minor bugs when used with multiple searches. If you need to use two searches with associations, I would pipe the call to another search operation:

Search.run(queryable, %{field1: %{assoc: [inner: :some_assoc]}}
|> Search.run(%{field2: %{assoc: [inner: :some_assoc2]}}

USAGE:

For a regular search:

This returns a queryable which upon running will give a list of Parent(s) searched by ascending field_1

alias Rummage.Ecto.Hook.Search

searched_queryable = Search.run(Parent, %{field_1: %{assoc: [],
  search_type: :like, search_term: "field_!"}}})

For a case-insensitive search:

This returns a queryable which upon running will give a list of Parent(s) searched by ascending case insensitive field_1.

Keep in mind that case_insensitive can only be called for text fields

alias Rummage.Ecto.Hook.Search

searched_queryable = Search.run(Parent, %{field_1: %{assoc: [],
  search_type: :ilike, search_term: "field_!"}}})

There are many other search_types. Check out Rummage.Ecto.Services.BuildSearchQuery docs to explore more search_types

This module can be overridden with a custom module while using Rummage.Ecto in Ecto struct module:

In the Ecto module:

Rummage.Ecto.rummage(queryable, rummage, search: CustomHook)

OR

Globally for all models in config.exs:

config :my_app,
  Rummage.Ecto,
 .search: CustomHook

The CustomHook must use Rummage.Ecto.Hook. For examples of CustomHook, check out some custom_hooks that are shipped with Rummage.Ecto: Rummage.Ecto.CustomHook.SimpleSearch, Rummage.Ecto.CustomHook.SimpleSort, Rummage.Ecto.CustomHook.SimplePaginate

Link to this section Summary

Functions

Callback implementation for Rummage.Ecto.Hook.format_params/3.

This is the callback implementation of Rummage.Ecto.Hook.run/2.

Link to this section Functions

Link to this function

format_params(queryable, params, opts)

View Source

Specs

format_params(Ecto.Query.t(), map(), keyword()) :: map()
format_params(Ecto.Query.t(), map(), keyword()) :: map()

Callback implementation for Rummage.Ecto.Hook.format_params/3.

This function ensures that params for each field have keys assoc, search_type and search_expr which are essential for running this hook module.

Examples

iex> alias Rummage.Ecto.Hook.Search
iex> Search.format_params(Parent, %{field: %{}}, [])
%{field: %{assoc: [], search_expr: :where, search_type: :eq}}

iex> alias Rummage.Ecto.Hook.Search
iex> Search.format_params(Parent, %{field: 1}, [])
** (RuntimeError) No scope `field` of type search defined in the Elixir.Parent

Specs

run(Ecto.Query.t(), map()) :: Ecto.Query.t()
run(Ecto.Query.t(), map()) :: Ecto.Query.t()

This is the callback implementation of Rummage.Ecto.Hook.run/2.

Builds a search Ecto.Query.t on top of a given Ecto.Query.t variable with given params.

Besides an Ecto.Query.t an Ecto.Schema module can also be passed as it implements Ecto.Queryable

Params is a Map, keys of which are field names which will be searched for and value corresponding to that key is a list of params for that key, which should include the keys: #{Enum.join(@expected_keys, ", ")}.

This function expects a search_expr, search_type and a list of associations (empty for none). The search_term is what the field will be matched to based on the search_type and search_expr.

If no search_expr is given, it defaults to where.

For all search_exprs, refer to Rummage.Ecto.Services.BuildSearchQuery.

For all search_types, refer to Rummage.Ecto.Services.BuildSearchQuery.

If an expected key isn't given, a Runtime Error is raised.

NOTE:This hook isn't responsible for doing type validations. That's the responsibility of the user sending search_term and search_type. Same goes for the validity of assoc.

Examples

When search_params are empty, it simply returns the same queryable:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> Search.run(Parent, %{})
Parent

When a non-empty map is passed as a field params, but with a missing key:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> Search.run(Parent, %{field: %{assoc: []}})
** (RuntimeError) Error in params, No values given for keys: search_type, search_term

When a valid map of params is passed with an Ecto.Schema module:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{field1: %{assoc: [],
...> search_type: :like, search_term: "field1", search_expr: :where}}
iex> Search.run(Rummage.Ecto.Product, search_params)
#Ecto.Query<from p0 in subquery(from p0 in Rummage.Ecto.Product), where: like(p0.field1, ^"%field1%")>

When a valid map of params is passed with an Ecto.Query.t:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{field1: %{assoc: [],
...> search_type: :like, search_term: "field1", search_expr: :where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
#Ecto.Query<from p0 in subquery(from p0 in "products"), where: like(p0.field1, ^"%field1%")>

When a valid map of params is passed with an Ecto.Query.t, with assocs:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{field1: %{assoc: [inner: :category],
...> search_type: :like, search_term: "field1", search_expr: :or_where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
#Ecto.Query<from p0 in subquery(from p0 in "products"), join: c1 in assoc(p0, :category), or_where: like(c1.field1, ^"%field1%")>

When a valid map of params is passed with an Ecto.Query.t, with assocs, with different join types:

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{field1: %{assoc: [inner: :category, left: :category, cross: :category],
...> search_type: :like, search_term: "field1", search_expr: :where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
#Ecto.Query<from p0 in subquery(from p0 in "products"), join: c1 in assoc(p0, :category), left_join: c2 in assoc(c1, :category), cross_join: c3 in assoc(c2, :category), where: like(c3.field1, ^"%field1%")>

When a valid map of params is passed with an Ecto.Query.t, searching on a boolean param

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{available: %{assoc: [],
...> search_type: :eq, search_term: true, search_expr: :where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
#Ecto.Query<from p0 in subquery(from p0 in "products"), where: p0.available == ^true>

When a valid map of params is passed with an Ecto.Query.t, searching on a float param

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{price: %{assoc: [],
...> search_type: :gteq, search_term: 10.0, search_expr: :where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
#Ecto.Query<from p0 in subquery(from p0 in "products"), where: p0.price >= ^10.0>

When a valid map of params is passed with an Ecto.Query.t, searching on a boolean param, but with a wrong search_type. NOTE: This doesn't validate the search_type of search_term

iex> alias Rummage.Ecto.Hook.Search
iex> import Ecto.Query
iex> search_params = %{available: %{assoc: [],
...> search_type: :ilike, search_term: true, search_expr: :where}}
iex> query = from p0 in "products"
iex> Search.run(query, search_params)
** (ArgumentError) argument error