mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Add spoiler executor
This commit is contained in:
parent
7046857cbd
commit
e6950c70c1
1 changed files with 143 additions and 0 deletions
143
lib/philomena/spoiler_executor.ex
Normal file
143
lib/philomena/spoiler_executor.ex
Normal file
|
@ -0,0 +1,143 @@
|
|||
defmodule Philomena.SpoilerExecutor do
|
||||
alias Philomena.Elasticsearch
|
||||
alias Philomena.Images.Query
|
||||
alias Philomena.Images.Image
|
||||
alias Philomena.Tags.Tag
|
||||
alias Philomena.Repo
|
||||
|
||||
import Ecto.Query
|
||||
import Philomena.Search.String
|
||||
|
||||
@doc """
|
||||
Compile a filter's spoiler context for the purpose of executing it as
|
||||
a spoiler. This logic is different from filter execution because it
|
||||
tags query leaves with relevant information about which aspect of the
|
||||
spoiler they match (tag match, complex filter match).
|
||||
"""
|
||||
@spec compile_spoiler(map(), map()) :: map()
|
||||
def compile_spoiler(user, filter) do
|
||||
spoilered_tags =
|
||||
Enum.map(filter.spoilered_tag_ids, fn id ->
|
||||
%{term: %{tag_id: id}, _name: Integer.to_string(id)}
|
||||
end)
|
||||
|
||||
spoilered_complex =
|
||||
user
|
||||
|> invalid_filter_guard(filter.spoilered_complex_str)
|
||||
|> Map.put(:_name, "complex")
|
||||
|
||||
%{
|
||||
bool: %{
|
||||
should: [spoilered_complex | spoilered_tags]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Execute a spoiler previously compiled by compile_spoiler/2 on the given
|
||||
set of images. Returns a maps which maps image hits to the Tag object(s)
|
||||
the spoiler matched, or the atom :complex if the complex spoiler matched
|
||||
instead of any tag object.
|
||||
"""
|
||||
@spec execute_spoiler(map(), list()) :: %{optional(integer()) => [map()] | :complex}
|
||||
def execute_spoiler(compiled, images) do
|
||||
image_ids = %{
|
||||
terms: %{id: extract_ids(images)}
|
||||
}
|
||||
|
||||
test_query = %{
|
||||
bool: %{
|
||||
must: [compiled, image_ids],
|
||||
}
|
||||
}
|
||||
|
||||
results = Elasticsearch.search_results(Image, test_query)
|
||||
|
||||
tags = extract_tags(results.entries)
|
||||
|
||||
results
|
||||
|> Enum.map(&filter_reason(&1, tags))
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
# Protect against an invalid query string included inside the filter
|
||||
# from raising an error up to the user.
|
||||
@spec invalid_filter_guard(map(), String.t()) :: map()
|
||||
defp invalid_filter_guard(user, search_string) do
|
||||
case Query.compile(user, normalize(search_string)) do
|
||||
{:ok, query} -> query
|
||||
_error -> %{match_all: %{}}
|
||||
end
|
||||
end
|
||||
|
||||
# Extract image ids in heterogeneous format from controllers.
|
||||
@spec extract_ids([nil | map() | {map(), any()} | Enumerable.t()]) :: [integer()]
|
||||
defp extract_ids(images) do
|
||||
Enum.flat_map(images, fn
|
||||
nil -> []
|
||||
%{id: id} -> [id]
|
||||
{%{id: id}, _hit} -> [id]
|
||||
enum -> extract_ids(enum)
|
||||
end)
|
||||
end
|
||||
|
||||
# Extract matched Tag objects from a list of search hits.
|
||||
#
|
||||
# The expected case of the list being empty is explored first to avoid
|
||||
# unnecessary database roundtrips.
|
||||
@spec extract_tags(list()) :: map()
|
||||
defp extract_tags(results)
|
||||
|
||||
defp extract_tags([]) do
|
||||
%{}
|
||||
end
|
||||
|
||||
defp extract_tags(results) do
|
||||
hit_tag_ids =
|
||||
results
|
||||
|> Enum.flat_map(fn {_id, hit} -> hit["matched_queries"] end)
|
||||
|> Enum.uniq()
|
||||
|
||||
Tag
|
||||
|> where([t], t.id in ^hit_tag_ids)
|
||||
|> Repo.all()
|
||||
|> Map.new(&{Integer.to_string(&1.id), &1})
|
||||
end
|
||||
|
||||
# Create a map key for the response of execute_spoiler/2. Determines
|
||||
# the reason an image was in this response.
|
||||
@spec filter_reason({integer(), map()}, map()) :: {integer(), [map()] | :complex}
|
||||
defp filter_reason({id, hit}, tags) do
|
||||
tags
|
||||
|> Map.take(hit["matched_queries"])
|
||||
|> Enum.sort(&tag_sort/2)
|
||||
|> case do
|
||||
[] ->
|
||||
{id, :complex}
|
||||
|
||||
matched_tags ->
|
||||
matched_tags
|
||||
end
|
||||
end
|
||||
|
||||
# The logic for tag sorting is as follows:
|
||||
# 1. If both tags have a spoiler image, sort by images count asc
|
||||
# 2. If neither tag has a spoiler image, sort by images count desc
|
||||
# 3. Otherwise, the tag with the spoiler image takes precedence
|
||||
@spec tag_sort(map(), map()) :: boolean()
|
||||
defp tag_sort(a, b) do
|
||||
cond do
|
||||
not is_nil(a.image) and not is_nil(b.image) ->
|
||||
a.images_count <= b.images_count
|
||||
|
||||
is_nil(a.image) and is_nil(b.image) ->
|
||||
b.images_count >= a.images_count
|
||||
|
||||
is_nil(a.image) ->
|
||||
false
|
||||
|
||||
true ->
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue