defmodule Oban.Web.Search do @moduledoc false # Helpers for the search toolbar @ignored_chars ~w(; / \ ` ' = * ! ? # $ & + ^ | ~ < > ( \) { } [ ]) # Suggestion tuning @suggest_limit 10 @suggest_threshold 0.5 @doc """ Parse a string of qualifiers and values into structured search terms. """ def parse(terms, parser) when is_binary(terms) and is_function(parser, 1) do terms |> String.split(~r/\s+(?=([^\"]*\"[^\"]*\")*[^\"]*$)/, trim: true) |> Map.new(fn term -> term |> String.replace(@ignored_chars, "") |> parser.() end) end @doc """ Append to the terms string without any duplication. """ def append(terms, choice, known) when is_binary(terms) and is_binary(choice) and is_struct(known, MapSet) do choice = if String.match?(choice, ~r/[\s,]/), do: ~s("#{choice}"), else: choice cond do MapSet.member?(known, choice) -> choice String.contains?(terms, ":") -> [qualifier, _] = String.split(terms, ":", parts: 2) "#{qualifier}:#{choice}" true -> terms |> String.reverse() |> String.split(["."], parts: 2) |> List.last() |> String.reverse() |> Kernel.<>(".#{choice}") end end @doc """ Restrict suggestions by similarity to a given fragment. """ def restrict_suggestions(suggestions, "") do suggestions |> Enum.sort() |> Enum.take(@suggest_limit) |> Enum.map(&{&1, "", ""}) end def restrict_suggestions(suggestions, frag) do suggestions |> Enum.filter(&(similarity(&1, frag) >= @suggest_threshold)) |> Enum.sort_by(&{1.0 - similarity(&1, frag), &1}, :asc) |> Enum.take(@suggest_limit) |> Enum.map(&{&1, "", ""}) end defp similarity(value, guess, boost \\ 0.5) do value = String.downcase(value) guess = String.downcase(guess) distance = String.jaro_distance(value, guess) if String.contains?(value, guess) do min(distance + boost, 1.0) else distance end end end