Add global search

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-08-26 16:08:58 +02:00
parent bfc936f57c
commit 48935e2168
216 changed files with 3646 additions and 2806 deletions

View File

@@ -0,0 +1,18 @@
defmodule Mobilizon.Service.GlobalSearch.EventResult do
@moduledoc """
The structure holding search result information about an event
"""
defstruct [
:id,
:uuid,
:url,
:title,
:begins_on,
:ends_on,
:picture,
:category,
:tags,
:organizer_actor,
:participants
]
end

View File

@@ -0,0 +1,17 @@
defmodule Mobilizon.Service.GlobalSearch do
@moduledoc """
Module to load the service adapter defined inside the configuration.
See `Mobilizon.Service.GlobalSearch.Provider`.
"""
@doc """
Returns the appropriate service adapter.
According to the config behind
`config :mobilizon, Mobilizon.Service.GlobalSearch,
service: Mobilizon.Service.GlobalSearch.Module`
"""
@spec service :: module
def service, do: get_in(Application.get_env(:mobilizon, __MODULE__), [:service])
end

View File

@@ -0,0 +1,19 @@
defmodule Mobilizon.Service.GlobalSearch.GroupResult do
@moduledoc """
The structure holding search result information about a group
"""
defstruct [
:id,
:url,
:name,
:preferred_username,
:domain,
:avatar,
:summary,
:url,
:members_count,
:follower_count,
:type,
:physical_address
]
end

View File

@@ -0,0 +1,40 @@
defmodule Mobilizon.Service.GlobalSearch.Provider do
@moduledoc """
Provider Behaviour for Global Search.
## Supported backends
* `Mobilizon.Service.GlobalSearch.Mobilizon` [🔗](https://framagit.org/framasoft/joinmobilizon/search-index/)
## Shared options
* `:lang` Lang in which to prefer results. Used as a request parameter or
through an `Accept-Language` HTTP header. Defaults to `"en"`.
* `:country_code` An ISO 3166 country code. String or `nil`
* `:limit` Maximum limit for the number of results returned by the backend.
Defaults to `10`
* `:api_key` Allows to override the API key (if the backend requires one) set
inside the configuration.
* `:endpoint` Allows to override the endpoint set inside the configuration.
"""
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult}
@doc """
Get global search results for a search string
## Examples
iex> search_events(search: "London")
[%EventResult{id: "provider-534", origin_url: "https://somewhere.else", title: "MyEvent", begins_on: "2022-08-25T08:13:47+0200", ends_on: "2022-08-25T10:13:47+0200", category: "FILM_MEDIA", tags: ["something", "what"], participants: 5}]
"""
@callback search_events(search_options :: keyword) ::
Page.t(EventResult.t())
@callback search_groups(search_options :: keyword) ::
Page.t(GroupResult.t())
@spec endpoint(atom()) :: String.t()
def endpoint(provider) do
Application.get_env(:mobilizon, provider) |> get_in([:endpoint])
end
end

View File

@@ -0,0 +1,225 @@
defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
@moduledoc """
[Search Mobilizon](https://search.joinmobilizon.org) backend.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Tag
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult, Provider}
alias Mobilizon.Service.HTTP.GenericJSONClient
alias Mobilizon.Storage.Page
require Logger
import Plug.Conn.Query, only: [encode: 1]
@search_events_api "/api/v1/search/events"
@search_groups_api "/api/v1/search/groups"
@behaviour Provider
@impl Provider
@doc """
Mobilizon Search implementation for `c:Mobilizon.Service.GlobalSearch.Provider.search_events/3`.
"""
@spec search_events(keyword()) :: Page.t(EventResult.t())
def search_events(options \\ []) do
Logger.debug("Search events options, #{inspect(Keyword.keys(options))}")
options =
options
|> Keyword.merge(
term: options[:search],
startDateMin: to_date(options[:begins_on]),
startDateMax: to_date(options[:ends_on]),
categoryOneOf: options[:category_one_of],
languageOneOf: options[:language_one_of],
statusOneOf:
Enum.map(options[:status_one_of] || [], fn status ->
status |> Atom.to_string() |> String.upcase()
end),
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit],
start: (options[:page] - 1) * options[:limit],
latlon: to_lat_lon(options[:location])
)
|> Keyword.take([
:search,
:startDateMin,
:startDateMax,
:boostLanguages,
:categoryOneOf,
:languageOneOf,
:latlon,
:distance,
:sort,
:statusOneOf,
:start,
:count
])
|> Keyword.reject(fn {_key, val} -> is_nil(val) end)
events_url = "#{search_endpoint()}#{@search_events_api}?#{encode(options)}"
Logger.debug("Calling global search engine url #{events_url}")
client = GenericJSONClient.client()
case GenericJSONClient.get(client, events_url) do
{:ok, %{status: 200, body: body}} ->
%Page{total: body["total"], elements: Enum.map(body["data"], &build_event/1)}
_ ->
nil
end
end
@impl Provider
@doc """
Mobilizon Search implementation for `c:Mobilizon.Service.GlobalSearch.Provider.search_groups/3`.
"""
@spec search_groups(keyword()) :: Page.t(GroupResult.t())
def search_groups(options \\ []) do
options =
options
|> Keyword.merge(
term: options[:search],
languageOneOf: options[:language_one_of],
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit],
start: (options[:page] - 1) * options[:limit],
latlon: to_lat_lon(options[:location])
)
|> Keyword.take([
:search,
:boostLanguages,
:latlon,
:distance,
:sort,
:start,
:count
])
|> Keyword.reject(fn {_key, val} -> is_nil(val) end)
groups_url = "#{search_endpoint()}#{@search_groups_api}?#{encode(options)}"
Logger.debug("Calling global search engine url #{groups_url}")
client = GenericJSONClient.client()
case GenericJSONClient.get(client, groups_url) do
{:ok, %{status: 200, body: body}} ->
%Page{total: body["total"], elements: Enum.map(body["data"], &build_group/1)}
_ ->
nil
end
end
defp build_event(data) do
picture =
if data["banner"] do
%{url: data["banner"], id: data["banner"]}
else
nil
end
organizer_actor_avatar =
if data["creator"]["avatar"] do
%{url: data["creator"]["avatar"], id: data["creator"]["avatar"]}
else
nil
end
%EventResult{
id: data["id"],
uuid: data["uuid"],
title: data["name"],
begins_on: parse_date(data["startTime"]),
ends_on: parse_date(data["endTime"]),
url: data["url"],
picture: picture,
category: String.to_existing_atom(String.downcase(data["category"])),
organizer_actor: %Actor{
id: data["creator"]["id"],
name: data["creator"]["displayName"],
preferred_username: data["creator"]["name"],
avatar: organizer_actor_avatar
},
tags:
Enum.map(data["tags"], fn tag ->
tag = String.trim_leading(tag, "#")
%Tag{id: tag, slug: tag, title: tag}
end)
}
end
defp build_group(data) do
avatar =
if data["avatar"] do
%{url: data["avatar"], id: data["avatar"]}
else
nil
end
address =
if data["location"] do
%Address{
id: data["location"]["id"],
country: data["location"]["address"]["addressCountry"],
locality: data["location"]["address"]["addressLocality"],
region: data["location"]["address"]["addressRegion"],
postal_code: data["location"]["address"]["postalCode"],
street: data["location"]["address"]["streetAddress"],
url: data["location"]["id"],
description: data["location"]["name"],
geom: %Geo.Point{
coordinates:
{data["location"]["location"]["lon"], data["location"]["location"]["lat"]},
srid: 4326
}
}
else
nil
end
%GroupResult{
id: data["id"],
name: data["displayName"],
preferred_username: data["name"],
domain: data["host"],
avatar: avatar,
summary: data["description"],
url: data["url"],
members_count: data["memberCount"],
type: :Group,
physical_address: address
}
end
defp search_endpoint do
Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) ||
"https://search.joinmobilizon.org"
end
defp parse_date(nil), do: nil
defp parse_date(date_string) do
case DateTime.from_iso8601(date_string) do
{:ok, date, _} -> date
{:error, _} -> nil
end
end
defp to_date(nil), do: nil
defp to_date(date), do: DateTime.to_iso8601(date)
defp to_lat_lon(nil), do: nil
defp to_lat_lon(location) do
case Geohax.decode(location) do
{lon, lat} ->
"#{lat}:#{lon}"
_ ->
nil
end
end
end

View File

@@ -1,3 +1,6 @@
defmodule Mobilizon.Service.Pictures.Information do
@moduledoc """
The structure holding information about a picture
"""
defstruct [:url, :author, :source]
end

View File

@@ -3,8 +3,8 @@ defmodule Mobilizon.Service.Pictures.Unsplash do
[Unsplash](https://unsplash.com) backend.
"""
alias Mobilizon.Service.Pictures.{Information, Provider}
alias Mobilizon.Service.HTTP.GenericJSONClient
alias Mobilizon.Service.Pictures.{Information, Provider}
require Logger
@unsplash_api "/search/photos"
@@ -24,12 +24,12 @@ defmodule Mobilizon.Service.Pictures.Unsplash do
GenericJSONClient.client(headers: [{:Authorization, "Client-ID #{unsplash_access_key()}"}])
with {:ok, %{status: 200, body: body}} <- GenericJSONClient.get(client, url),
selectedPicture <- Enum.random(body["results"]) do
selected_picture <- Enum.random(body["results"]) do
%Information{
url: selectedPicture["urls"]["small"],
url: selected_picture["urls"]["small"],
author: %{
name: selectedPicture["user"]["name"],
url: "#{selectedPicture["user"]["links"]["html"]}#{unsplash_utm_source()}"
name: selected_picture["user"]["name"],
url: "#{selected_picture["user"]["links"]["html"]}#{unsplash_utm_source()}"
},
source: %{
name: @unsplash_name,