FuzzyCast

Warning: work in progress

FuzzyCast is a module for composing introspective %like% queries for Ecto.Schema fields.

Long Way

from(u in User,
      where: ilike(u.email, ^"%gmail%"),
      or_where: ilike(u.email, ^"%yahoo%"),
      or_where: ilike(u.email, ^"%bob%"),
      ...

The FuzzyCast Way

FuzzyCast.compose(User, ~w(gmail yahoo bob))

FuzzyCast will cast the search values with the schema fields.

Example

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  ...
  
  schema "users" do
    field(:email, :string)
    field(:username, :string)
    field(:password, :string)
    field(:confirmed, :boolean, default: false)
    field(:password_confirmation, :string, virtual: true)
    timestamps()
  end
  ...
end

iex> FuzzyCast.compose(User, 1)
#Ecto.Query<from u in MyApp.Accounts.User, where: u.id == ^1,
 or_where: ilike(u.email, ^"%1%"), or_where: ilike(u.username, ^"%1%"),
 or_where: u.confirmed == ^true>

Notice password fields were not returned, FuzzyCast will ignore fields that contain "password".

If the search value cannot be cast using Ecto.Type.cast it will be ignored.

Example

iex> FuzzyCast.compose(User, "gmail")
#Ecto.Query<from u in MyApp.Accounts.User, where: ilike(u.email, ^"%gmail%"),
 or_where: ilike(u.username, ^"%gmail%")>

Notice the string "gmail" only matched the type :string associted to the field email and username. Fuzzy cast will only search castable fields... hence FuzzyCast

To further demostrate, we can try to get all users with an email containing gmail and who are confirmed.

Example

iex> FuzzyCast.compose(User, ["gmail", true])
#Ecto.Query<from u in MyApp.Accounts.User, where: ilike(u.email, ^"%gmail%"),
 or_where: ilike(u.username, ^"%gmail%"), or_where: ilike(u.email, ^"%true%"),
 or_where: ilike(u.username, ^"%true%"), or_where: u.confirmed == ^true>

Our query looks ok, but it looks like we are also looking for emails that match "%true%". Depending on the use case this might be acceptable, after all it is fuzzy. A lot of times we don't need and single results but rather multiple results we pick from. This works best when narrowing or debouncing queries.

FuzzyCast.compose simply return and Ecto.Query. This means we can it can be composed like any other Ecto.Query.

Example

iex> from(u in User) |> FuzzyCast.compose(~w(gmail yahoo)) |> Repo.all
[
  %MyApp.User{
    email: "bob@gmail.com",
    ...
  }
  ...
]
iex> q = from(u in User, where: u.confirmed == true) |> FuzzyCast.compose(["gmail", "yahoo"])
#Ecto.Query<from u in MyApp.Accounts.User, where: u.confirmed == true,
 or_where: ilike(u.email, ^"%gmail%"), or_where: ilike(u.username, ^"%gmail%"),
 or_where: ilike(u.email, ^"%yahoo%"), or_where: ilike(u.username, ^"%yahoo%")>
iex> Repo.aggregate(q, :count, :id)
500

Composing queries with Ecto.Query works, but we can also pipe multiple FuzzyCast.compose calls.

We might want to look for a match of "mike" across all fields, and a match for emails that include "gmail" or "yahoo".

FuzzyCast.compose(User, ["gmail", "yahoo"], fields: [:email]) |> FuzzyCast.compose("mike")
#Ecto.Query<from u in MyApp.Accounts.User, where: ilike(u.email, ^"%gmail%"),
 or_where: ilike(u.email, ^"%yahoo%"), or_where: ilike(u.email, ^"%mike%"),
 or_where: ilike(u.username, ^"%mike%")>

Installation

This package can be installed by adding fuzzy_cast to your list of dependencies in mix.exs:

def deps do
  [
    {:fuzzy_cast, "~> 0.1"}
  ]
end

Up to date docs can be found at https://hexdocs.pm/fuzzy_cast.