From dc45ec62c2f5dfcc895854dfbddf6fe9621d3072 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Mon, 14 Jan 2019 20:04:45 +0300
Subject: [PATCH 1/8] [#477] User search improvements: tsquery search with
 field weights, friends & followers boosting.

---
 lib/pleroma/user.ex                           | 75 ++++++++++++++++---
 .../mastodon_api/mastodon_api_controller.ex   |  6 +-
 .../web/twitter_api/twitter_api_controller.ex |  2 +-
 test/user_test.exs                            |  5 +-
 4 files changed, 72 insertions(+), 16 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 681280539..52638b446 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -35,7 +35,7 @@ defmodule Pleroma.User do
     field(:avatar, :map)
     field(:local, :boolean, default: true)
     field(:follower_address, :string)
-    field(:search_distance, :float, virtual: true)
+    field(:search_rank, :float, virtual: true)
     field(:tags, {:array, :string}, default: [])
     field(:last_refreshed_at, :naive_datetime)
     has_many(:notifications, Notification)
@@ -511,6 +511,12 @@ defmodule Pleroma.User do
     {:ok, Repo.all(q)}
   end
 
+  def get_followers_ids(user, page \\ nil) do
+    q = get_followers_query(user, page)
+
+    Repo.all(from(u in q, select: u.id))
+  end
+
   def get_friends_query(%User{id: id, following: following}, nil) do
     from(
       u in User,
@@ -535,6 +541,12 @@ defmodule Pleroma.User do
     {:ok, Repo.all(q)}
   end
 
+  def get_friends_ids(user, page \\ nil) do
+    q = get_friends_query(user, page)
+
+    Repo.all(from(u in q, select: u.id))
+  end
+
   def get_follow_requests_query(%User{} = user) do
     from(
       a in Activity,
@@ -666,7 +678,7 @@ defmodule Pleroma.User do
     Repo.all(query)
   end
 
-  def search(query, resolve \\ false) do
+  def search(query, resolve \\ false, for_user \\ nil) do
     # strip the beginning @ off if there is a query
     query = String.trim_leading(query, "@")
 
@@ -674,16 +686,28 @@ defmodule Pleroma.User do
       User.get_or_fetch_by_nickname(query)
     end
 
+    processed_query =
+      query
+      |> String.replace(~r/\W+/, " ")
+      |> String.trim()
+      |> String.split()
+      |> Enum.map(&(&1 <> ":*"))
+      |> Enum.join(" | ")
+
     inner =
       from(
         u in User,
         select_merge: %{
-          search_distance:
+          search_rank:
             fragment(
-              "? <-> (? || coalesce(?, ''))",
-              ^query,
-              u.nickname,
-              u.name
+              """
+              ts_rank_cd(
+                setweight(to_tsvector('simple', regexp_replace(nickname, '\\W', ' ', 'g')), 'A') ||
+                setweight(to_tsvector('simple', regexp_replace(coalesce(name, ''), '\\W', ' ', 'g')), 'B'),
+                to_tsquery('simple', ?)
+              )
+              """,
+              ^processed_query
             )
         },
         where: not is_nil(u.nickname)
@@ -692,11 +716,44 @@ defmodule Pleroma.User do
     q =
       from(
         s in subquery(inner),
-        order_by: s.search_distance,
+        order_by: [desc: s.search_rank],
         limit: 20
       )
 
-    Repo.all(q)
+    results =
+      q
+      |> Repo.all()
+      |> Enum.filter(&(&1.search_rank > 0))
+
+    weighted_results =
+      if for_user do
+        friends_ids = get_friends_ids(for_user)
+        followers_ids = get_followers_ids(for_user)
+
+        Enum.map(
+          results,
+          fn u ->
+            search_rank_coef =
+              cond do
+                u.id in friends_ids ->
+                  1.2
+
+                u.id in followers_ids ->
+                  1.1
+
+                true ->
+                  1
+              end
+
+            Map.put(u, :search_rank, u.search_rank * search_rank_coef)
+          end
+        )
+        |> Enum.sort_by(&(-&1.search_rank))
+      else
+        results
+      end
+
+    weighted_results
   end
 
   def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index a8fe9d708..54367f586 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -772,7 +772,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-    accounts = User.search(query, params["resolve"] == "true")
+    accounts = User.search(query, params["resolve"] == "true", user)
 
     statuses = status_search(user, query)
 
@@ -796,7 +796,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-    accounts = User.search(query, params["resolve"] == "true")
+    accounts = User.search(query, params["resolve"] == "true", user)
 
     statuses = status_search(user, query)
 
@@ -817,7 +817,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-    accounts = User.search(query, params["resolve"] == "true")
+    accounts = User.search(query, params["resolve"] == "true", user)
 
     res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
 
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 1c728166c..ede079963 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -675,7 +675,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   end
 
   def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
-    users = User.search(query, true)
+    users = User.search(query, true, user)
 
     conn
     |> put_view(UserView)
diff --git a/test/user_test.exs b/test/user_test.exs
index cfccce8d1..efa7937bc 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -781,8 +781,7 @@ defmodule Pleroma.UserTest do
       _user_three = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
       user_four = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
 
-      assert user_four ==
-               User.search("lain@ple") |> List.first() |> Map.put(:search_distance, nil)
+      assert user_four == User.search("lain@ple") |> List.first() |> Map.put(:search_rank, nil)
     end
 
     test "finds a user whose name is nil" do
@@ -792,7 +791,7 @@ defmodule Pleroma.UserTest do
       assert user_two ==
                User.search("lain@pleroma.soykaf.com")
                |> List.first()
-               |> Map.put(:search_distance, nil)
+               |> Map.put(:search_rank, nil)
     end
   end
 

From fc965f982c62c43e11cb42c77f7c371c9835a9f2 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Tue, 15 Jan 2019 12:04:54 +0300
Subject: [PATCH 2/8] [#477] Added FTS index for `users`. Fixed failing test.

---
 .../20190115085500_create_user_fts_index.exs    | 17 +++++++++++++++++
 .../twitter_api/twitter_api_controller_test.exs | 10 +++++-----
 2 files changed, 22 insertions(+), 5 deletions(-)
 create mode 100644 priv/repo/migrations/20190115085500_create_user_fts_index.exs

diff --git a/priv/repo/migrations/20190115085500_create_user_fts_index.exs b/priv/repo/migrations/20190115085500_create_user_fts_index.exs
new file mode 100644
index 000000000..499d67113
--- /dev/null
+++ b/priv/repo/migrations/20190115085500_create_user_fts_index.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.CreateUserFtsIndex do
+  use Ecto.Migration
+
+  def change do
+    create index(
+             :users,
+             [
+               """
+               (setweight(to_tsvector('simple', regexp_replace(nickname, '\\W', ' ', 'g')), 'A') ||
+               setweight(to_tsvector('simple', regexp_replace(coalesce(name, ''), '\\W', ' ', 'g')), 'B'))
+               """
+             ],
+             name: :users_fts_index,
+             using: :gin
+           )
+  end
+end
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index 5f13e7959..a4baf2b5f 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -1655,16 +1655,16 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
   describe "GET /api/pleroma/search_user" do
     test "it returns users, ordered by similarity", %{conn: conn} do
       user = insert(:user, %{name: "eal"})
-      user_two = insert(:user, %{name: "ean"})
-      user_three = insert(:user, %{name: "ebn"})
+      user_two = insert(:user, %{name: "eal me"})
+      _user_three = insert(:user, %{name: "ebn"})
 
       resp =
         conn
-        |> get(twitter_api_search__path(conn, :search_user), query: "eal")
+        |> get(twitter_api_search__path(conn, :search_user), query: "eal me")
         |> json_response(200)
 
-      assert length(resp) == 3
-      assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
+      assert length(resp) == 2
+      assert [user_two.id, user.id] == Enum.map(resp, fn %{"id" => id} -> id end)
     end
   end
 

From 5b8f9ff8c14b5992e3db7a0c890ca5539e6a0086 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Tue, 15 Jan 2019 13:05:25 +0300
Subject: [PATCH 3/8] [#477] User search tests. Normalized search rank in
 User.search.

---
 lib/pleroma/user.ex |  3 ++-
 test/user_test.exs  | 62 ++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 52638b446..2488697bb 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -704,7 +704,8 @@ defmodule Pleroma.User do
               ts_rank_cd(
                 setweight(to_tsvector('simple', regexp_replace(nickname, '\\W', ' ', 'g')), 'A') ||
                 setweight(to_tsvector('simple', regexp_replace(coalesce(name, ''), '\\W', ' ', 'g')), 'B'),
-                to_tsquery('simple', ?)
+                to_tsquery('simple', ?),
+                32
               )
               """,
               ^processed_query
diff --git a/test/user_test.exs b/test/user_test.exs
index efa7937bc..48b7b72ec 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -775,13 +775,55 @@ defmodule Pleroma.UserTest do
   end
 
   describe "User.search" do
-    test "finds a user, ranking by similarity" do
-      _user = insert(:user, %{name: "lain"})
-      _user_two = insert(:user, %{name: "ean"})
-      _user_three = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
-      user_four = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
+    test "finds a user by full or partial nickname" do
+      user = insert(:user, %{nickname: "john"})
 
-      assert user_four == User.search("lain@ple") |> List.first() |> Map.put(:search_rank, nil)
+      Enum.each(["john", "jo", "j"], fn query ->
+        assert user == User.search(query) |> List.first() |> Map.put(:search_rank, nil)
+      end)
+    end
+
+    test "finds a user by full or partial name" do
+      user = insert(:user, %{name: "John Doe"})
+
+      Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
+        assert user == User.search(query) |> List.first() |> Map.put(:search_rank, nil)
+      end)
+    end
+
+    test "finds users, preferring nickname matches over name matches" do
+      u1 = insert(:user, %{name: "lain", nickname: "nick1"})
+      u2 = insert(:user, %{nickname: "lain", name: "nick1"})
+
+      assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
+    end
+
+    test "finds users, considering density of matched tokens" do
+      u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
+      u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
+
+      assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
+    end
+
+    test "finds users, ranking by similarity" do
+      u1 = insert(:user, %{name: "lain"})
+      _u2 = insert(:user, %{name: "ean"})
+      u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
+      u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
+
+      assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple"), & &1.id)
+    end
+
+    test "finds users, boosting ranks of friends and followers" do
+      u1 = insert(:user)
+      u2 = insert(:user, %{name: "Doe"})
+      follower = insert(:user, %{name: "Doe"})
+      friend = insert(:user, %{name: "Doe"})
+
+      {:ok, follower} = User.follow(follower, u1)
+      {:ok, u1} = User.follow(u1, friend)
+
+      assert [friend.id, follower.id, u2.id] == Enum.map(User.search("doe", false, u1), & &1.id)
     end
 
     test "finds a user whose name is nil" do
@@ -793,6 +835,14 @@ defmodule Pleroma.UserTest do
                |> List.first()
                |> Map.put(:search_rank, nil)
     end
+
+    test "does not yield false-positive matches" do
+      insert(:user, %{name: "John Doe"})
+
+      Enum.each(["mary", "a", ""], fn query ->
+        assert [] == User.search(query)
+      end)
+    end
   end
 
   test "auth_active?/1 works correctly" do

From 0bc6d30f7dfe53be588329e48f1255b5eef18a2a Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Wed, 16 Jan 2019 10:44:32 +0300
Subject: [PATCH 4/8] [#477] Minor refactoring (user search query).

---
 lib/pleroma/user.ex | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 2488697bb..8ae36416a 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -702,12 +702,14 @@ defmodule Pleroma.User do
             fragment(
               """
               ts_rank_cd(
-                setweight(to_tsvector('simple', regexp_replace(nickname, '\\W', ' ', 'g')), 'A') ||
-                setweight(to_tsvector('simple', regexp_replace(coalesce(name, ''), '\\W', ' ', 'g')), 'B'),
+                setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+                setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
                 to_tsquery('simple', ?),
                 32
               )
               """,
+              u.nickname,
+              u.name,
               ^processed_query
             )
         },

From ed8f55ab8eb292903cec8f7699aa6775cc304458 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Fri, 18 Jan 2019 10:35:45 +0300
Subject: [PATCH 5/8] [#477] User: FTS and trigram search results mixing (to
 handle misspelled requests).

---
 lib/pleroma/user.ex                           | 138 ++++++++++--------
 test/user_test.exs                            |   6 +
 .../twitter_api_controller_test.exs           |   2 +-
 3 files changed, 87 insertions(+), 59 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 8ae36416a..1d0bf1edf 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -679,13 +679,35 @@ defmodule Pleroma.User do
   end
 
   def search(query, resolve \\ false, for_user \\ nil) do
-    # strip the beginning @ off if there is a query
+    # Strip the beginning @ off if there is a query
     query = String.trim_leading(query, "@")
 
-    if resolve do
-      User.get_or_fetch_by_nickname(query)
-    end
+    if resolve, do: User.get_or_fetch_by_nickname(query)
 
+    fts_results = do_search(fts_search_subquery(query), for_user)
+
+    trigram_results = do_search(trigram_search_subquery(query), for_user)
+
+    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
+  end
+
+  defp do_search(subquery, for_user, options \\ []) do
+    q =
+      from(
+        s in subquery(subquery),
+        order_by: [desc: s.search_rank],
+        limit: ^(options[:limit] || 20)
+      )
+
+    results =
+      q
+      |> Repo.all()
+      |> Enum.filter(&(&1.search_rank > 0))
+
+    boost_search_results(results, for_user)
+  end
+
+  defp fts_search_subquery(query) do
     processed_query =
       query
       |> String.replace(~r/\W+/, " ")
@@ -694,69 +716,69 @@ defmodule Pleroma.User do
       |> Enum.map(&(&1 <> ":*"))
       |> Enum.join(" | ")
 
-    inner =
-      from(
-        u in User,
-        select_merge: %{
-          search_rank:
-            fragment(
-              """
-              ts_rank_cd(
-                setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
-                setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
-                to_tsquery('simple', ?),
-                32
-              )
-              """,
-              u.nickname,
-              u.name,
-              ^processed_query
+    from(
+      u in User,
+      select_merge: %{
+        search_rank:
+          fragment(
+            """
+            ts_rank_cd(
+              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
+              to_tsquery('simple', ?),
+              32
             )
-        },
-        where: not is_nil(u.nickname)
-      )
+            """,
+            u.nickname,
+            u.name,
+            ^processed_query
+          )
+      },
+      where: not is_nil(u.nickname)
+    )
+  end
 
-    q =
-      from(
-        s in subquery(inner),
-        order_by: [desc: s.search_rank],
-        limit: 20
-      )
+  defp trigram_search_subquery(query) do
+    from(
+      u in User,
+      select_merge: %{
+        search_rank:
+          fragment(
+            "similarity(?, ? || ' ' || coalesce(?, ''))",
+            ^query,
+            u.nickname,
+            u.name
+          )
+      },
+      where: not is_nil(u.nickname)
+    )
+  end
 
-    results =
-      q
-      |> Repo.all()
-      |> Enum.filter(&(&1.search_rank > 0))
+  defp boost_search_results(results, nil), do: results
 
-    weighted_results =
-      if for_user do
-        friends_ids = get_friends_ids(for_user)
-        followers_ids = get_followers_ids(for_user)
+  defp boost_search_results(results, for_user) do
+    friends_ids = get_friends_ids(for_user)
+    followers_ids = get_followers_ids(for_user)
 
-        Enum.map(
-          results,
-          fn u ->
-            search_rank_coef =
-              cond do
-                u.id in friends_ids ->
-                  1.2
+    Enum.map(
+      results,
+      fn u ->
+        search_rank_coef =
+          cond do
+            u.id in friends_ids ->
+              1.2
 
-                u.id in followers_ids ->
-                  1.1
+            u.id in followers_ids ->
+              1.1
 
-                true ->
-                  1
-              end
-
-            Map.put(u, :search_rank, u.search_rank * search_rank_coef)
+            true ->
+              1
           end
-        )
-        |> Enum.sort_by(&(-&1.search_rank))
-      else
-        results
-      end
 
-    weighted_results
+        Map.put(u, :search_rank, u.search_rank * search_rank_coef)
+      end
+    )
+    |> Enum.sort_by(&(-&1.search_rank))
   end
 
   def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
diff --git a/test/user_test.exs b/test/user_test.exs
index 48b7b72ec..339def217 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -814,6 +814,12 @@ defmodule Pleroma.UserTest do
       assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple"), & &1.id)
     end
 
+    test "finds users, handling misspelled requests" do
+      u1 = insert(:user, %{name: "lain"})
+
+      assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
+    end
+
     test "finds users, boosting ranks of friends and followers" do
       u1 = insert(:user)
       u2 = insert(:user, %{name: "Doe"})
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index a4baf2b5f..e013d1aca 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -1656,7 +1656,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
     test "it returns users, ordered by similarity", %{conn: conn} do
       user = insert(:user, %{name: "eal"})
       user_two = insert(:user, %{name: "eal me"})
-      _user_three = insert(:user, %{name: "ebn"})
+      _user_three = insert(:user, %{name: "zzz"})
 
       resp =
         conn

From 79e44042bc08cf69274008e408cac912ae693afe Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Fri, 18 Jan 2019 10:57:42 +0300
Subject: [PATCH 6/8] [#477] User trigram index adjustment.

---
 lib/pleroma/user.ex                                 |  2 +-
 .../20190118074940_fix_user_trigram_index.exs       | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)
 create mode 100644 priv/repo/migrations/20190118074940_fix_user_trigram_index.exs

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 1d0bf1edf..eb4218ebe 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -744,7 +744,7 @@ defmodule Pleroma.User do
       select_merge: %{
         search_rank:
           fragment(
-            "similarity(?, ? || ' ' || coalesce(?, ''))",
+            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
             ^query,
             u.nickname,
             u.name
diff --git a/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs b/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs
new file mode 100644
index 000000000..4f7712eb0
--- /dev/null
+++ b/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs
@@ -0,0 +1,13 @@
+defmodule Pleroma.Repo.Migrations.FixUserTrigramIndex do
+  use Ecto.Migration
+
+  def up do
+    drop_if_exists index(:users, [], name: :users_trigram_index)
+    create index(:users, ["(trim(nickname || ' ' || name)) gist_trgm_ops"], name: :users_trigram_index, using: :gist)
+  end
+
+  def down do
+    drop_if_exists index(:users, [], name: :users_trigram_index)
+    create index(:users, ["(nickname || name) gist_trgm_ops"], name: :users_trigram_index, using: :gist)
+  end
+end

From b108aeee082949e2e534f8bc406fdacb8924803d Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Sun, 20 Jan 2019 00:31:17 +0100
Subject: [PATCH 7/8] Make use of the indices.

Indices in postgresql rely on operators, so they won't be used if you use only functions.
---
 lib/pleroma/user.ex                             | 13 +++++++++++--
 .../20190118074940_fix_user_trigram_index.exs   | 17 +++++++++++++----
 2 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index eb4218ebe..87815e11c 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -734,7 +734,16 @@ defmodule Pleroma.User do
             ^processed_query
           )
       },
-      where: not is_nil(u.nickname)
+      where:
+        fragment(
+          """
+            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
+          """,
+          u.nickname,
+          u.name,
+          ^processed_query
+        )
     )
   end
 
@@ -750,7 +759,7 @@ defmodule Pleroma.User do
             u.name
           )
       },
-      where: not is_nil(u.nickname)
+      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
     )
   end
 
diff --git a/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs b/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs
index 4f7712eb0..b4e8c984c 100644
--- a/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs
+++ b/priv/repo/migrations/20190118074940_fix_user_trigram_index.exs
@@ -2,12 +2,21 @@ defmodule Pleroma.Repo.Migrations.FixUserTrigramIndex do
   use Ecto.Migration
 
   def up do
-    drop_if_exists index(:users, [], name: :users_trigram_index)
-    create index(:users, ["(trim(nickname || ' ' || name)) gist_trgm_ops"], name: :users_trigram_index, using: :gist)
+    drop_if_exists(index(:users, [], name: :users_trigram_index))
+
+    create(
+      index(:users, ["(trim(nickname || ' ' || coalesce(name, ''))) gist_trgm_ops"],
+        name: :users_trigram_index,
+        using: :gist
+      )
+    )
   end
 
   def down do
-    drop_if_exists index(:users, [], name: :users_trigram_index)
-    create index(:users, ["(nickname || name) gist_trgm_ops"], name: :users_trigram_index, using: :gist)
+    drop_if_exists(index(:users, [], name: :users_trigram_index))
+
+    create(
+      index(:users, ["(nickname || name) gist_trgm_ops"], name: :users_trigram_index, using: :gist)
+    )
   end
 end

From 5834b08fe77250d1dad0f2f6cd148f2fd8f85c09 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Sun, 20 Jan 2019 10:57:49 +0100
Subject: [PATCH 8/8] Set custom similarity limit.

---
 lib/pleroma/user.ex | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 87815e11c..955808e28 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -686,7 +686,11 @@ defmodule Pleroma.User do
 
     fts_results = do_search(fts_search_subquery(query), for_user)
 
-    trigram_results = do_search(trigram_search_subquery(query), for_user)
+    {:ok, trigram_results} =
+      Repo.transaction(fn ->
+        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
+        do_search(trigram_search_subquery(query), for_user)
+      end)
 
     Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
   end