Merge pull request 'Better user search' (#7793) from gitlab-mr-iid-4416 into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7793
This commit is contained in:
commit
47ca427497
3 changed files with 243 additions and 1 deletions
1
changelog.d/user-search-sorting.change
Normal file
1
changelog.d/user-search-sorting.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Improve user search / autocompletion ordering.
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.User.Search do
|
defmodule Pleroma.User.Search do
|
||||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
|
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
|
||||||
|
alias Pleroma.Instances.Instance
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
|
|
@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do
|
||||||
|> filter_invisible_users()
|
|> filter_invisible_users()
|
||||||
|> filter_internal_users()
|
|> filter_internal_users()
|
||||||
|> filter_blocked_domains(for_user)
|
|> filter_blocked_domains(for_user)
|
||||||
|
|> filter_unreachable_users()
|
||||||
|> fts_search(query_string)
|
|> fts_search(query_string)
|
||||||
|> select_top_users(top_user_ids)
|
|> select_top_users(top_user_ids)
|
||||||
|> trigram_rank(query_string)
|
|> trigram_rank(query_string)
|
||||||
|> boost_search_rank(for_user, top_user_ids)
|
|> boost_search_rank(for_user, top_user_ids)
|
||||||
|> subquery()
|
|> subquery()
|
||||||
|> order_by(desc: :search_rank)
|
|> order_by_search_rank(for_user)
|
||||||
|> maybe_restrict_local(for_user)
|
|> maybe_restrict_local(for_user)
|
||||||
|> maybe_restrict_accepting_chat_messages(capabilities)
|
|> maybe_restrict_accepting_chat_messages(capabilities)
|
||||||
|> filter_deactivated_users()
|
|> filter_deactivated_users()
|
||||||
|
|
@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do
|
||||||
|
|
||||||
defp filter_blocked_domains(query, _), do: query
|
defp filter_blocked_domains(query, _), do: query
|
||||||
|
|
||||||
|
defp filter_unreachable_users(query) do
|
||||||
|
from(u in query,
|
||||||
|
left_join: i in Instance,
|
||||||
|
on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id),
|
||||||
|
where: is_nil(i.unreachable_since)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_resolve(true, user, query) do
|
defp maybe_resolve(true, user, query) do
|
||||||
case {limit(), user} do
|
case {limit(), user} do
|
||||||
{:all, _} -> :noop
|
{:all, _} -> :noop
|
||||||
|
|
@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do
|
||||||
|
|
||||||
from(u in subquery(query),
|
from(u in subquery(query),
|
||||||
select_merge: %{
|
select_merge: %{
|
||||||
|
search_type:
|
||||||
|
fragment(
|
||||||
|
"""
|
||||||
|
CASE WHEN (?) THEN 2
|
||||||
|
WHEN (?) THEN 1
|
||||||
|
ELSE 0 END
|
||||||
|
""",
|
||||||
|
u.id in ^top_user_ids,
|
||||||
|
u.id in ^friends_ids or u.id in ^followers_ids
|
||||||
|
),
|
||||||
search_rank:
|
search_rank:
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
|
|
@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do
|
||||||
defp boost_search_rank(query, _for_user, top_user_ids) do
|
defp boost_search_rank(query, _for_user, top_user_ids) do
|
||||||
from(u in subquery(query),
|
from(u in subquery(query),
|
||||||
select_merge: %{
|
select_merge: %{
|
||||||
|
search_type:
|
||||||
|
fragment(
|
||||||
|
"""
|
||||||
|
CASE WHEN (?) THEN 2
|
||||||
|
ELSE 0 END
|
||||||
|
""",
|
||||||
|
u.id in ^top_user_ids
|
||||||
|
),
|
||||||
search_rank:
|
search_rank:
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
|
|
@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp order_by_search_rank(query, %User{}) do
|
||||||
|
order_by(
|
||||||
|
query,
|
||||||
|
[u],
|
||||||
|
desc: u.search_type,
|
||||||
|
desc_nulls_last:
|
||||||
|
fragment(
|
||||||
|
"CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END",
|
||||||
|
u.search_type,
|
||||||
|
u.last_status_at,
|
||||||
|
u.last_active_at
|
||||||
|
),
|
||||||
|
desc: u.search_rank
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
195
test/pleroma/user/search_test.exs
Normal file
195
test/pleroma/user/search_test.exs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.User.SearchTest do
|
||||||
|
use Pleroma.DataCase, async: false
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
alias Pleroma.Instances
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
describe "search/2 mention suggestions" do
|
||||||
|
test "prioritizes followed/follower users before others" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
related =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "hj@real.example",
|
||||||
|
ap_id: "https://real.example/users/hj",
|
||||||
|
last_status_at: ~N[2020-01-01 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
|
||||||
|
|
||||||
|
{:ok, _related, _user} = User.follow(related, user)
|
||||||
|
|
||||||
|
results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
|
||||||
|
|
||||||
|
assert results == [related.id, other.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "orders followed/follower users by most recent activity" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
older =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "ali@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/ali",
|
||||||
|
last_status_at: ~N[2020-01-01 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
newer =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "alia@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/alia",
|
||||||
|
last_status_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _user, _older} = User.follow(user, older)
|
||||||
|
{:ok, _user, _newer} = User.follow(user, newer)
|
||||||
|
|
||||||
|
assert [newer.id, older.id] ==
|
||||||
|
User.search("ali", for_user: user)
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups followed/follower users first and sorts them by recency" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
following_newest =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "mentiontesta@related.example",
|
||||||
|
ap_id: "https://related.example/users/mentiontesta",
|
||||||
|
last_status_at: ~N[2020-01-03 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
follower_middle =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "mentiontestb@related.example",
|
||||||
|
ap_id: "https://related.example/users/mentiontestb",
|
||||||
|
last_status_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
mutual_oldest =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "mentiontestc@related.example",
|
||||||
|
ap_id: "https://related.example/users/mentiontestc",
|
||||||
|
last_status_at: ~N[2020-01-01 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
unrelated_newer =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "mentiontestd@unrelated.example",
|
||||||
|
ap_id: "https://unrelated.example/users/mentiontestd",
|
||||||
|
last_status_at: ~N[2020-01-04 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _user, _following_newest} = User.follow(user, following_newest)
|
||||||
|
{:ok, _follower_middle, _user} = User.follow(follower_middle, user)
|
||||||
|
|
||||||
|
{:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
|
||||||
|
{:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
|
||||||
|
|
||||||
|
results = User.search("mentiontest", for_user: user)
|
||||||
|
|
||||||
|
assert Enum.map(results, & &1.id) ==
|
||||||
|
[following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses last_active_at when last_status_at is missing" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
older =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "activefallbacka@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/activefallbacka",
|
||||||
|
last_status_at: nil,
|
||||||
|
last_active_at: ~N[2020-01-01 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
newer =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "activefallbackb@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/activefallbackb",
|
||||||
|
last_status_at: nil,
|
||||||
|
last_active_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _user, _older} = User.follow(user, older)
|
||||||
|
{:ok, _user, _newer} = User.follow(user, newer)
|
||||||
|
|
||||||
|
assert [newer.id, older.id] ==
|
||||||
|
User.search("activefallback", for_user: user)
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return deactivated users even if related" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
active =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "deactivatedtesta@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/deactivatedtesta",
|
||||||
|
last_status_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
deactivated =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "deactivatedtestb@remote.example",
|
||||||
|
ap_id: "https://remote.example/users/deactivatedtestb",
|
||||||
|
last_status_at: ~N[2020-01-03 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _user, _active} = User.follow(user, active)
|
||||||
|
{:ok, _user, _deactivated} = User.follow(user, deactivated)
|
||||||
|
Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
|
||||||
|
|
||||||
|
results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
|
||||||
|
|
||||||
|
assert results == [active.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return users from unreachable instances" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _instance} = Instances.set_unreachable("dead.example")
|
||||||
|
|
||||||
|
dead =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "ali@dead.example",
|
||||||
|
ap_id: "https://dead.example/users/ali",
|
||||||
|
last_status_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
alive =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "ali@alive.example",
|
||||||
|
ap_id: "https://alive.example/users/ali",
|
||||||
|
last_status_at: ~N[2020-01-02 00:00:00]
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _user, _alive} = User.follow(user, alive)
|
||||||
|
{:ok, _user, _dead} = User.follow(user, dead)
|
||||||
|
|
||||||
|
results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
|
||||||
|
|
||||||
|
assert results == [alive.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue