Packages

A full-text search engine written in pure Elixir.

Retired package: Deprecated - retired

Current section

Files

Jump to
searchex lib searchex keyword server.ex
Raw

lib/searchex/keyword/server.ex

defmodule Searchex.Keyword.Server do
@moduledoc false
use GenServer
@doc """
Keyword Server
We start one KeywordSer for each keyword in the index. The state looks like:
%{"DIOCID1" => [list of positions], "DIOCID2" => [list of positions]}
"""
def start_link(server_name \\ :server) do
GenServer.start_link(__MODULE__, %{}, name: server_name)
end
def add_keyword_position(pt_name, keyword, docid, position) do
server = get_keyword_server(pt_name, keyword)
GenServer.cast(server, {:add, docid, position})
end
@doc """
Perform a query
terms is a list of strings = ["term1", "term2"]
doc_matches looks like:
[{"term1", %{"docid1" => [pos1, pos2], "docid2" => [pos3, pos4]}}, {"term2", %{}}]
matches_per_term_and_doc looks like:
%{{"term1", "docid1"} => 23, {"term1", "docid2"} => 4, ...}
"""
def do_query({pt_name, terms}) when is_list(terms) do
stem_terms = Enum.map(terms, &(StemEx.stem(&1)))
doc_matches = gen_doc_matches(pt_name, stem_terms)
matches_per_term_and_doc = gen_matches_per_term_and_doc(doc_matches)
Searchex.Command.Search.Bm25.doc_scores(terms, doc_matches, matches_per_term_and_doc)
|> Enum.map(fn(el) -> {elem(el, 0), Float.round(elem(el, 1) * 1.0, 3)} end)
end
def do_query({pt_name, terms}) when is_list(terms) , do: do_query(pt_name, terms)
def do_query({pt_name, terms}) when is_binary(terms) , do: do_query(pt_name, String.split(terms))
def do_query({pt_name, terms}) , do: do_query(pt_name, terms)
def do_query(pt_name, terms) when is_binary(terms) , do: do_query(pt_name, String.split(terms, " "))
def gen_doc_matches(pt_name, terms) do
Enum.map(terms, fn(term) -> get_ids(pt_name, term) end)
end
def gen_matches_per_term_and_doc(doc_matches) do
Enum.reduce doc_matches, %{}, fn({term, docids}, acc1) ->
tmp = Enum.reduce docids, %{}, fn({docid, pos_list}, acc2) ->
Map.merge(acc2, %{{term, docid} => Enum.count(pos_list)})
end
Map.merge(acc1, tmp)
end
end
@doc """
Gets the list of ID's for a term.
The returned data structure look like:
%{"DIOCID1" => [list of positions], "DIOCID2" => [list of positions]}
"""
def get_ids(pt_name, term) do
case find_keyword_server(pt_name, term) do
{:ok, server} -> {term, GenServer.call(server, :get_ids)};
{:error, _ } -> {term, %{}}
end
end
@doc """
Sets the state for a KeywordSer. This is useful when building an in-memory
index from a persisted index saved to disk.
The state looks like:
%{"DIOCID1" => [list of positions], "DIOCID2" => [list of positions]}
"""
def set_state(server, new_state) do
GenServer.cast(server, {:set_state, new_state})
end
# --- CALLBACKS
def init(_opts) do
{:ok, %{}}
end
def handle_call(:get_ids, _from, state) do
result = state
{:reply, result, state}
end
def handle_cast({:add, docid, position}, state) do
old_list = state[docid] || []
new_list = old_list ++ [position]
new_state = Map.merge(state, %{docid => new_list})
{:noreply, new_state}
end
def handle_cast({:set_state, new_state}, _state) do
{:noreply, new_state}
end
def get_keyword_server(pt_name, keyword) do
name = if is_atom(keyword), do: keyword, else: keyword_server_name(pt_name, keyword)
Process.whereis(name) || Searchex.Keyword.Supervisor.add_child_and_return_pid(pt_name, name)
end
def find_keyword_server(pt_name, keyword) do
name = keyword_server_name(pt_name, keyword)
case Process.whereis(name) do
nil -> {:error, "Not found"}
pid -> {:ok, pid}
end
end
def keyword_server_name(pt_name, keyword) do
new_pt_name = to_string(pt_name)
"kw_" <> new_pt_name <> "_" <> keyword |> String.to_atom
end
end