commit: 93c144e397d408d7ff1761640e12fb51e333b2ce
parent 26fe60494246121b59e40898e6b950e61853452c
Author: Mark Felder <feld@feld.me>
Date: Thu, 31 Jul 2025 17:46:32 -0700
Improve hashtag search with multi word queries
Diffstat:
4 files changed, 92 insertions(+), 6 deletions(-)
diff --git a/changelog.d/hashtag-search.change b/changelog.d/hashtag-search.change
@@ -0,0 +1 @@
+Hashtag searches return real results based on words in your query
diff --git a/lib/pleroma/hashtag.ex b/lib/pleroma/hashtag.ex
@@ -135,18 +135,28 @@ defmodule Pleroma.Hashtag do
limit = Keyword.get(options, :limit, 20)
offset = Keyword.get(options, :offset, 0)
- query
- |> String.downcase()
- |> String.trim()
- |> then(fn search_term ->
+ search_terms =
+ query
+ |> String.downcase()
+ |> String.trim()
+ |> String.split(~r/\s+/)
+ |> Enum.filter(&(&1 != ""))
+
+ if Enum.empty?(search_terms) do
+ []
+ else
+ # Use PostgreSQL's ANY operator with array for efficient multi-term search
+ # This is much more efficient than multiple OR clauses
+ search_patterns = Enum.map(search_terms, &"%#{&1}%")
+
from(ht in Hashtag,
- where: fragment("LOWER(?) LIKE ?", ht.name, ^"%#{search_term}%"),
+ where: fragment("LOWER(?) LIKE ANY(?)", ht.name, ^search_patterns),
order_by: [asc: ht.name],
limit: ^limit,
offset: ^offset
)
|> Repo.all()
|> Enum.map(& &1.name)
- end)
+ end
end
end
diff --git a/test/pleroma/hashtag_test.exs b/test/pleroma/hashtag_test.exs
@@ -38,6 +38,35 @@ defmodule Pleroma.HashtagTest do
assert results == []
end
+ test "searches hashtags by multiple words in query" do
+ # Create some hashtags
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("laptop")
+ {:ok, _} = Hashtag.get_or_create_by_name("desktop")
+ {:ok, _} = Hashtag.get_or_create_by_name("phone")
+
+ # Search for "new computer" - should return "computer"
+ results = Hashtag.search("new computer")
+ assert "computer" in results
+ refute "laptop" in results
+ refute "desktop" in results
+ refute "phone" in results
+
+ # Search for "computer laptop" - should return both
+ results = Hashtag.search("computer laptop")
+ assert "computer" in results
+ assert "laptop" in results
+ refute "desktop" in results
+ refute "phone" in results
+
+ # Search for "new phone" - should return "phone"
+ results = Hashtag.search("new phone")
+ assert "phone" in results
+ refute "computer" in results
+ refute "laptop" in results
+ refute "desktop" in results
+ end
+
test "supports pagination" do
{:ok, _} = Hashtag.get_or_create_by_name("alpha")
{:ok, _} = Hashtag.get_or_create_by_name("beta")
@@ -50,5 +79,20 @@ defmodule Pleroma.HashtagTest do
results = Hashtag.search("a", limit: 2, offset: 1)
assert length(results) == 2
end
+
+ test "handles many search terms efficiently" do
+ # Create hashtags
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("laptop")
+ {:ok, _} = Hashtag.get_or_create_by_name("phone")
+ {:ok, _} = Hashtag.get_or_create_by_name("tablet")
+
+ # Search with many terms - should be efficient with PostgreSQL ANY operator
+ results = Hashtag.search("new fast computer laptop phone tablet device")
+ assert "computer" in results
+ assert "laptop" in results
+ assert "phone" in results
+ assert "tablet" in results
+ end
end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
@@ -139,6 +139,37 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
assert results["hashtags"] == []
end
+ test "searches hashtags by multiple words in query", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, _activity1} = CommonAPI.post(user, %{status: "This is my new #computer"})
+ {:ok, _activity2} = CommonAPI.post(user, %{status: "Check out this #laptop"})
+ {:ok, _activity3} = CommonAPI.post(user, %{status: "My #desktop setup"})
+ {:ok, _activity4} = CommonAPI.post(user, %{status: "New #phone arrived"})
+
+ results =
+ conn
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "new computer"})}")
+ |> json_response_and_validate_schema(200)
+
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+ assert "computer" in hashtag_names
+ refute "laptop" in hashtag_names
+ refute "desktop" in hashtag_names
+ refute "phone" in hashtag_names
+
+ results =
+ conn
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "computer laptop"})}")
+ |> json_response_and_validate_schema(200)
+
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+ assert "computer" in hashtag_names
+ assert "laptop" in hashtag_names
+ refute "desktop" in hashtag_names
+ refute "phone" in hashtag_names
+ end
+
test "supports pagination of hashtags search results", %{conn: conn} do
user = insert(:user)