defmodule Haystack.Query do
@moduledoc """
A module for building queries.
"""
alias Haystack.{Index, Query}
alias Haystack.Tokenizer.Token
# Types
@type statement :: (Query.t(), Index.t() -> list(map()))
@type t :: %__MODULE__{
clause: Query.Clause.t(),
config: Keyword.t()
}
@enforce_keys ~w{index clause config}a
defstruct @enforce_keys
@config clause: Query.Clause.default(),
expression: Query.Expression.default()
# Public
@doc """
Create a new Query.
"""
@spec new(Keyword.t()) :: t
def new(opts \\ []),
do: struct(__MODULE__, Keyword.put_new(opts, :config, @config))
@doc """
Add a clause to the query.
"""
@spec clause(t, Query.Clause.t()) :: t
def clause(query, clause),
do: %{query | clause: clause}
@doc """
Evaluate a clause.
"""
@spec evaluate(t(), Index.t(), Query.Clause.t(), list(statement)) :: list(map())
def evaluate(query, index, clause, statements) do
module = get_in(query.config, [:clause, clause.k])
module.evaluate(query, index, statements)
end
@doc """
Evaluate an expression.
"""
@spec evaluate(t(), Index.t(), Query.Expression.t()) :: list(map())
def evaluate(query, index, expression) do
module = get_in(query.config, [:expression, expression.k])
module.evaluate(index, expression)
end
@doc """
Run the given query.
"""
@spec run(t, Index.t()) :: list(map)
def run(query, index) do
statement = Query.Clause.build(query.clause)
responses = statement.(query, index)
responses
|> Query.IDF.calculate(index.storage)
|> Enum.group_by(& &1.ref)
|> Enum.map(fn {ref, fields} ->
score = Enum.reduce(fields, 0, &(&1.score + &2))
fields = Enum.group_by(fields, & &1.field)
fields =
Enum.map(fields, fn {k, list} ->
{k, Enum.map(list, &Map.take(&1, [:positions, :term]))}
end)
%{ref: ref, score: score, fields: Map.new(fields)}
end)
|> Enum.sort_by(& &1.score, :desc)
end
@doc """
Build a clause for the given list of tokens.
## Examples
iex> index = Index.new(:animals)
iex> index = Index.field(index, Index.Field.new("name"))
iex> tokens = Tokenizer.tokenize("Red Panda")
iex> tokens = Transformer.pipeline(tokens, Transformer.default())
iex> Query.build(:match_all, index, tokens)
"""
@spec build(atom, Index.t(), list(Token.t())) :: Query.Clause.t()
def build(:match_all, index, tokens) do
Enum.reduce(tokens, Query.Clause.new(:all), fn token, clause ->
Enum.reduce(Map.values(index.fields), clause, fn field, clause ->
Query.Clause.expressions(clause, [
Query.Expression.new(:match, field: field.k, term: token.v)
])
end)
end)
end
def build(:match_any, index, tokens) do
Enum.reduce(tokens, Query.Clause.new(:any), fn token, clause ->
Enum.reduce(Map.values(index.fields), clause, fn field, clause ->
Query.Clause.expressions(clause, [
Query.Expression.new(:match, field: field.k, term: token.v)
])
end)
end)
end
end