Skip to content

Commit

Permalink
handle slow search response times + boost results from Hamburg
Browse files Browse the repository at this point in the history
  • Loading branch information
breunigs committed Jun 3, 2024
1 parent 25b6dce commit bcf9f0b
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 92 deletions.
43 changes: 22 additions & 21 deletions data/articles/static/suche.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,40 @@ defmodule Data.Article.Static.Suche do
def tags(), do: []

def text(assigns) do
assigns = assign(assigns, :search_results, search(assigns))

~H"""
<h3><label for="query">Suche 🔎</label></h3>
<form method="GET" action="/suche" onsubmit="return false">
<input type="search" id="query" placeholder="z.B. Feldstraße, StadtRAD, Baustelle…" phx-keyup="search" phx-debounce="250" phx-hook="FocusSearchField" autofocus="autofocus" name="search_query" value={@search_query}/>
</form>
<.noindex>
<%= if @search_results == [] && @search_query != "" do %>
<p>Leider keine Ergebnisse.</p>
<% else %>
<ul class="spaced" role="list" aria-label="Suchergebnisse">
<%= for result <- @search_results do %>
<li>
<!-- <%= if debug?(), do: {:safe, (result.source)} %> -->
<%= Search.Result.to_html(result) %>
</li>
<% end %>
</ul>
<% end %>
<.async_result :let={search_results} assign={@search_results}>
<:loading><p>Lädt…</p></:loading>
<:failed :let={error}>
<p>Fehler in der Suchfunktion:</p>
<tt><%= inspect(error) %></tt>
</:failed>
<.results results={search_results} query={@search_query}/>
</.async_result>
</.noindex>
"""
end

defp search(%{search_query: query, search_bounds: bbox}) do
bbox = Geo.BoundingBox.parse(bbox) || Settings.initial()
defp results(%{results: sr, query: sq} = assigns) when sr == [] and sq != "" do
~H{<p>Leider keine Ergebnisse.</p>}
end

Search.Meilisearch.Runner.query(query, bbox)
|> Enum.reject(&is_nil/1)
|> Search.Result.merge_same()
|> Search.Result.sort()
|> Enum.take(15)
defp results(%{results: sr} = assigns) when is_list(sr) do
~H"""
<ul class="spaced" role="list" aria-label="Suchergebnisse">
<%= for result <- @results do %>
<li>
<!-- <%= if debug?(), do: {:safe, (result.source)} %> -->
<%= Search.Result.to_html(result) %>
</li>
<% end %>
</ul>
"""
end

defp debug?() do
Expand Down
2 changes: 2 additions & 0 deletions data/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ defmodule Settings do
position: %{lat: 53.55044, lon: 9.99440}
}

def boost_search_results_within, do: "Hamburg"

# no trailing slash
def url, do: "https://veloroute.hamburg"

Expand Down
2 changes: 1 addition & 1 deletion lib/article/decorators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ defmodule Article.Decorators do
default = %{
__changed__: %{},
search_query: nil,
search_bounds: nil,
search_results: Phoenix.LiveView.AsyncResult.ok([]),
limit_to_map_bounds: false,
show_map_image: false,
enable_drawing_tools: false,
Expand Down
2 changes: 1 addition & 1 deletion lib/article/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Article.Renderer do
attr :map_bounds, :any
attr :lang, :string
attr :search_query, :string
attr :search_bounds, :any
attr :search_results, :any
attr :enable_drawing_tools, :boolean, default: false
attr :limit_to_map_bounds, :boolean, default: false
attr :show_map_image, :boolean, default: false
Expand Down
12 changes: 10 additions & 2 deletions lib/basemap/nominatim.ex
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,11 @@ defmodule Basemap.Nominatim do
ST_YMax(ST_Envelope(ST_UNION(geometry)))
) AS bbox,
combo.parents_name,
combo.parents_postcode
combo.parents_postcode,
CASE
WHEN '#{Settings.boost_search_results_within()}' = ANY(combo.parents_name) THEN 1
ELSE 30
END AS rank_boosted_areas
FROM combo
GROUP BY
combo.class,
Expand Down Expand Up @@ -407,7 +411,11 @@ defmodule Basemap.Nominatim do
ST_Y(interpol.centroid)
) AS bbox,
combo.parents_name,
combo.parents_postcode
combo.parents_postcode,
CASE
WHEN '#{Settings.boost_search_results_within()}' = ANY(combo.parents_name) THEN 1
ELSE 30
END AS rank_boosted_areas
FROM combo
INNER JOIN interpol ON interpol.parent_place_id = combo.place_id
WHERE combo.name->'name' IS NOT NULL
Expand Down
15 changes: 7 additions & 8 deletions lib/search/meilisearch/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Search.Meilisearch.API do
use Tesla

@index_timeout_ms 5 * 60 * 1000
@general_timeout_ms 1500
@general_timeout_ms 10_000

plug Tesla.Middleware.BaseUrl, "http://localhost:7700/"
plug Tesla.Middleware.JSON
Expand Down Expand Up @@ -101,7 +101,7 @@ defmodule Search.Meilisearch.API do
end
end

@spec multi_search(%{atom() => map()}) :: %{atom() => list()}
@spec multi_search(%{atom() => map()}) :: {:ok, %{atom() => list()}} | {:error, binary()}
def multi_search(queries) do
payload = %{
"queries" =>
Expand All @@ -114,14 +114,13 @@ defmodule Search.Meilisearch.API do

with {:ok, %{body: %{"results" => results}}} <-
post("/multi-search", payload, opts: @adapter_opts_general) do
Enum.into(results, %{}, fn %{"indexUid" => index, "hits" => hits} ->
{String.to_existing_atom(index), hits}
end)
{:ok,
Enum.into(results, %{}, fn %{"indexUid" => index, "hits" => hits} ->
{String.to_existing_atom(index), hits}
end)}
else
other ->
Logger.warning("failed to multi-query for #{inspect(payload)}. Result: #{inspect(other)}")

[]
{:error, "failed to multi-query for #{inspect(payload)}. Result: #{inspect(other)}"}
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/search/meilisearch/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Search.Meilisearch.Behaviour do
@callback documents() :: [%{atom => any}] | {binary(), binary()}
@callback params(binary(), float(), float()) :: %{atom => list(binary) | binary | integer}
@callback format(%{binary => any}) :: Search.Result.t() | nil
@callback config() :: %{atom => list(binary) | %{atom => list(binary)}}
@callback config() :: %{atom => list(binary) | %{atom => list(binary)} | binary}

@callback maybe_merge([%{required(binary) => any()}]) :: [%{required(binary) => any()}]

Expand Down
30 changes: 19 additions & 11 deletions lib/search/meilisearch/nominatim.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@ defmodule Search.Meilisearch.Nominatim do
%{
q: query,
limit: 10,
sort: [
"rank_search:asc",
"rank_address:asc",
"importance:desc",
"_geoPoint(#{lat}, #{lon}):asc",
"admin_level:asc"
]
sort: ["_geoPoint(#{lat}, #{lon}):asc"]
}
end

Expand Down Expand Up @@ -81,7 +75,7 @@ defmodule Search.Meilisearch.Nominatim do
bounds: bbox,
center: Geo.CheapRuler.center(bbox),
name: name,
relevance: f.("_rankingScore"),
relevance: f.("_rankingScore") + (30 - f.("rank_boosted_areas")) / 1000,
type: if(f.("class") in ["place"], do: "poi", else: ""),
subtext: subtext
}
Expand Down Expand Up @@ -212,11 +206,25 @@ defmodule Search.Meilisearch.Nominatim do

%{
displayedAttributes:
~w(id class type name address parents_name parents_postcode extratags bbox boost),
~w(id class type name address parents_name parents_postcode extratags bbox boost rank_boosted_areas rank_search rank_address importance),
# order is from most important to least important
searchableAttributes: ~w(name boost address type parents_name type extratags),
sortableAttributes: ~w(importance rank_search rank_address _geo admin_level),
synonyms: synonyms
sortableAttributes:
~w(importance rank_search rank_address rank_boosted_areas _geo admin_level),
synonyms: synonyms,
proximityPrecision: "byAttribute",
rankingRules: ~w(words
typo
proximity
attribute
rank_boosted_areas:asc
rank_search:asc
rank_address:asc
importance:desc
admin_level:asc
sort
exactness
)
}
end

Expand Down
52 changes: 30 additions & 22 deletions lib/search/meilisearch/runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ defmodule Search.Meilisearch.Runner do
queue: [queued_task()]
}

@spec query(binary() | nil, Geo.BoundingBox.like()) :: [Search.Result.t()]
def query(nil, _bbox), do: []
def query("", _bbox), do: []
@spec query(binary() | nil, Geo.BoundingBox.like()) ::
{:ok, [Search.Result.t()]} | {:error, binary()}
def query(nil, _bbox), do: {:ok, []}
def query("", _bbox), do: {:ok, []}

def query(query, bbox) do
try do
GenServer.call(__MODULE__, {:search, query, bbox}, 1000)
GenServer.call(__MODULE__, {:search, query, bbox}, :infinity)
catch
:exit, err ->
Logger.warning(inspect(err))
[]
:exit, err -> {:error, inspect(err)}
end
end

Expand Down Expand Up @@ -299,30 +298,39 @@ defmodule Search.Meilisearch.Runner do
defp search(query, bbox) do
%{lat: lat, lon: lon} = Geo.CheapRuler.center(bbox)

lookup = Enum.into(@indexers, %{}, &{&1.id(), &1})

@indexers
|> Enum.into(%{}, fn indexer ->
{indexer.id(), indexer.params(query, lat, lon)}
end)
|> Search.Meilisearch.API.multi_search()
|> Enum.flat_map(fn {index, results} ->
indexer = lookup[index]
|> post_process()
end

results =
if function_exported?(indexer, :maybe_merge, 1),
do: indexer.maybe_merge(results),
else: results
defp post_process({:ok, list}) do
lookup = Enum.into(@indexers, %{}, &{&1.id(), &1})

Enum.map(results, fn result ->
sr = indexer.format(result)
if sr, do: Map.put(sr, :source, inspect(result, pretty: true))
end)
end)
|> Util.compact()
|> Enum.reject(fn %{relevance: rel} -> rel < @min_relevance end)
{:ok,
Enum.flat_map(list, fn {index, results} ->
indexer = lookup[index]

results =
if function_exported?(indexer, :maybe_merge, 1),
do: indexer.maybe_merge(results),
else: results

Enum.map(results, fn result ->
sr = indexer.format(result)
if sr, do: Map.put(sr, :source, inspect(result, pretty: true))
end)
end)
|> Util.compact()
|> Enum.reject(fn %{relevance: rel} -> rel < @min_relevance end)
|> Search.Result.merge_same()
|> Search.Result.sort()}
end

defp post_process({:error, reason}), do: {:error, reason}

@spec index(state()) :: state()
defp index(%{indexers: []} = state), do: state

Expand Down
58 changes: 35 additions & 23 deletions lib/veloroute_web/live/frame_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ defmodule VelorouteWeb.FrameLive do
require Logger
import Guards

@search_page Data.Article.Static.Suche
@search_page_name @search_page.name()

@default_bounds struct(Geo.BoundingBox, Settings.initial())
@initial_state [
prev_page: nil,
Expand All @@ -12,8 +15,8 @@ defmodule VelorouteWeb.FrameLive do
article_date: nil,
article_title: nil,
article_summary: nil,
search_query: nil,
search_bounds: nil,
search_query: "",
search_results: nil,
tmp_last_article_set: nil,
limit_to_map_bounds: false,
show_map_image: false,
Expand All @@ -36,7 +39,12 @@ defmodule VelorouteWeb.FrameLive do
map_bounds: Geo.BoundingBox.parse(params["bounds"]) || @default_bounds
)

socket = socket |> assign(state) |> maybe_run_events_from_url(params)
socket =
socket
|> assign(state)
|> maybe_run_events_from_url(params)
|> search(params["search_query"])

{:ok, socket}
end

Expand Down Expand Up @@ -89,27 +97,12 @@ defmodule VelorouteWeb.FrameLive do
{:noreply, socket}
end

def handle_event("search", %{"search_query" => value}, socket) do
handle_event("search", %{"value" => value}, socket)
end

def handle_event("search", %{"value" => ""}, socket) do
{:noreply, socket}
def handle_event("search", %{"search_query" => query}, socket) do
{:noreply, search(socket, query) |> show_search_page()}
end

@search_page "suche"
def handle_event("search", %{"value" => query}, socket) do
query = if query && query != "", do: String.trim(query), else: socket.assigns.search_query
search_article = Article.List.find_exact(@search_page)

socket =
socket
|> assign(:search_query, query || "")
|> assign(:search_bounds, socket.assigns[:map_bounds])

socket = push_patch(socket, to: article_path(socket, search_article))

{:noreply, socket}
{:noreply, search(socket, query) |> show_search_page()}
end

def handle_event("video-reverse", attr, socket) do
Expand Down Expand Up @@ -267,9 +260,9 @@ defmodule VelorouteWeb.FrameLive do
{:noreply, socket}
end

def handle_params(%{"page" => @search_page, "search_query" => query} = params, nil, socket) do
def handle_params(%{"page" => @search_page_name, "search_query" => query} = params, nil, socket) do
params
|> Map.put("article", @search_page)
|> Map.put("article", @search_page_name)
|> handle_params(nil, assign(socket, :search_query, query))
end

Expand Down Expand Up @@ -329,6 +322,25 @@ defmodule VelorouteWeb.FrameLive do
|> push_patch(to: ~p"/?#{url_query(socket)}")
end

defp search(socket, query) do
query = if query && query != "", do: String.trim(query), else: socket.assigns.search_query
bbox = Geo.BoundingBox.parse(socket.assigns[:map_bounds]) || Settings.initial()

querier = fn ->
with {:ok, results} <- Search.Meilisearch.Runner.query(query, bbox) do
{:ok, %{search_results: results}}
end
end

socket
|> assign(:search_query, query)
|> assign_async(:search_results, querier)
end

defp show_search_page(socket) do
push_patch(socket, to: article_path(socket, @search_page))
end

defp set_bounds(socket, article, bounds_param)

defp set_bounds(%{assigns: %{map_bounds: @default_bounds}} = socket, article, bounds_param)
Expand Down
Loading

0 comments on commit bcf9f0b

Please sign in to comment.