defmodule Vectorex do
@moduledoc """
This module is a container for controls for a full text search for postgres.
It provides a fluent api to build full text search queries which can be transformed to a string
that can be passed for execution using `&to_sql/1`
The module contains four functions that can be used to build the query
1. ts_and
2. ts_or
3. ts_not
4. ts_followed_by
Same functions exist in Vectorex.Subquery
Depending on the function you pass to `new/2`, the behaviour changes. If the control
is supported by the function, we append it to the query. If not, the parameter is ignored.
Check [the official documentation](https://www.postgresql.org/docs/current/textsearch-controls.html)
to verify which controls are supported by the function
Only to_tsquery supports all 4 controls.
You can create a new instance of Vectorex using `new/2` by passing the postgres function
you want to use and the parameter
Available Postgres functions are: :to_tsquery, :phraseto_tsquery, :plainto_tsquery, :websearch_to_tsquery
## Examples
iex> Vectorex.new(:to_tsquery, "elixir")
%Vectorex{params: ["elixir"], function: :to_tsquery}
iex> Vectorex.new(:phraseto_tsquery, "elixir")
%Vectorex{params: ["elixir"], function: :phraseto_tsquery}
iex> Vectorex.new(:plainto_tsquery, "elixir")
%Vectorex{params: ["elixir"], function: :plainto_tsquery}
iex> Vectorex.new(:websearch_to_tsquery, "elixir")
%Vectorex{params: ["elixir"], function: :websearch_to_tsquery}
After that, you can start building the query.
## Building queries
You can do that by using pipes
```elixir
# and
Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_and("ocaml")
# or
Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_or("ocaml")
# not
Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_not("ocaml")
# not
Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_followed_by("ocaml")
```
Obviously, you can pipe multiple times
```elixir
Vectorex.new(:to_tsquery, "elixir")
|> Vectorex.ts_and("ocaml")
|> Vectorex.ts_and("scala")
```
Or you can use a list
```elixir
Vectorex.new(:to_tsquery, "elixir")
|> Vectorex.ts_and(["ocaml", "scala"])
```
## Building subqueries
You can also group operators together using `Vectorex.Subquery.new/1`.
You can do this if you want parentheses around your terms
```elixir
inner =
Vectorex.Subquery.new("ocaml")
|> Vectorex.Subquery.ts_and("scala")
result =
Vectorex.new(:to_tsquery, "elixir")
|> Vectorex.ts_and(inner)
```
Subqueries also support the list option
```elixir
inner =
Vectorex.Subquery.new("ocaml")
|> Vectorex.Subquery.ts_or(["scala", "haskell"])
result =
Vectorex.new(:to_tsquery, "elixir")
|> Vectorex.ts_and(inner)
```
We can also pass a list of subqueries
```elixir
inner =
Vectorex.Subquery.new("ocaml")
|> Vectorex.Subquery.ts_or(["scala", "haskell"])
inner =
Vectorex.Subquery.new("java")
|> Vectorex.Subquery.ts_or("c#")
result =
Vectorex.new(:to_tsquery, "elixir")
|> Vectorex.ts_and([inner, inner2])
```
## Converting to sql
After we build the query we want, we need to convert it to a string. We can do that using `to_sql/1`
## Examples
iex> Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_and("ocaml") |> Vectorex.ts_and("haskell") |> Vectorex.to_sql()
"elixir & ocaml & haskell"
iex> Vectorex.new(:to_tsquery, "elixir") |> Vectorex.ts_and(Vectorex.Subquery.new("ocaml") |> Vectorex.Subquery.ts_or("scala")) |> Vectorex.to_sql()
"elixir & (ocaml | scala)"
We can use that string in an Ecto fragment for execution
```elixir
vectorex = Vectorex.new(:to_tsquery, "elixir")
from e in Event,
where: fragment("textsearchable_index_col @@ to_tsquery(?)", ^Vectorex.to_sql(vectorex))
```
"""
defstruct [:params, :function]
@before_compile {FunctionGenerator, :new}
@before_compile {FunctionGenerator, :operators}
@type t() :: %__MODULE__{
params: [String.t()] | [Vectorex.Subquery.t()],
function: :to_tsquery | :phraseto_tsquery | :plainto_tsquery | :websearch_to_tsquery
}
defmodule Subquery do
@before_compile {FunctionGenerator, :new_subquery}
@before_compile {FunctionGenerator, :operators}
defstruct [:params]
@type t() :: %__MODULE__{
params: [String.t()] | [Vectorex.Subquery.t()]
}
@doc false
@spec to_sql(subquery :: __MODULE__.t()) :: String.t()
def to_sql(%__MODULE__{} = vector) do
List.foldr([:end | vector.params], "(", fn item, acc ->
case item do
param when is_binary(param) -> acc <> param
{:and, %__MODULE__{} = param} -> acc <> " & " <> to_sql(param)
{:or, %__MODULE__{} = param} -> acc <> " | " <> to_sql(param)
{:not, %__MODULE__{} = param} -> acc <> " & !" <> to_sql(param)
{:followed_by, %__MODULE__{} = param} -> acc <> " <-> " <> to_sql(param)
:end -> acc <> ")"
_ -> Vectorex.append_operator(item, acc)
end
end)
end
end
@doc """
Transforms a Vectorex instance to a sql string
"""
@spec to_sql(vectorex :: __MODULE__.t()) :: String.t()
def to_sql(vectorex)
def to_sql(%__MODULE__{function: :to_tsquery} = vector) do
List.foldr(vector.params, "", fn item, acc ->
case item do
param when is_binary(param) -> acc <> param
{:and, %Subquery{} = subquery} -> acc <> " & " <> Subquery.to_sql(subquery)
{:or, %Subquery{} = subquery} -> acc <> " | " <> Subquery.to_sql(subquery)
{:not, %Subquery{} = subquery} -> acc <> " & ! " <> Subquery.to_sql(subquery)
{:followed_by, %Subquery{} = subquery} -> acc <> " <-> " <> Subquery.to_sql(subquery)
_ -> append_operator(item, acc)
end
end)
end
def to_sql(%__MODULE__{function: :websearch_to_tsquery} = vector) do
List.foldr(vector.params, "", fn item, acc ->
case item do
param when is_binary(param) -> acc <> "" <> param
{:or, param} -> acc <> " or " <> param
{:not, param} -> acc <> " -" <> param
_ -> acc
end
end)
end
def to_sql(%__MODULE__{function: :plainto_tsquery} = vector) do
List.foldr(vector.params, "", fn item, acc ->
case item do
param when is_binary(param) -> acc <> "" <> param
{:and, param} -> acc <> " " <> param
_ -> acc
end
end)
end
def to_sql(%__MODULE__{function: :phraseto_tsquery} = vector) do
List.foldr(vector.params, "", fn item, acc ->
case item do
param when is_binary(param) -> acc <> "" <> param
{:followed_by, param} -> acc <> " " <> param
_ -> acc
end
end)
end
def append_operator(item, query) do
case item do
{:and, param} -> query <> " & " <> param
{:or, param} -> query <> " | " <> param
{:not, param} -> query <> " & !" <> param
{:followed_by, param} -> query <> " <-> " <> param
_ -> query
end
end
end