diff --git a/changelog.d/hashtag-search.change b/changelog.d/hashtag-search.change new file mode 100644 index 000000000..f17e711ce --- /dev/null +++ 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 index fdb564fec..99e6eb39b 100644 --- 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 index a84effc5d..907c5ff40 100644 --- 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 index 1fbc7c9c6..8b4c6add2 100644 --- 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)