diff --git a/lib/pinchflat/media.ex b/lib/pinchflat/media.ex index 85529a7..b19fc6c 100644 --- a/lib/pinchflat/media.ex +++ b/lib/pinchflat/media.ex @@ -47,6 +47,42 @@ defmodule Pinchflat.Media do |> Repo.all() end + @doc """ + Returns a list of media_items that match the search term. Adds a `matching_search_term` + virtual field to the result set. + + Returns [%MediaItem{}, ...]. + + TODO: test limit + """ + def search(search_term, opts \\ []) do + limit = Keyword.get(opts, :limit, 50) + + from(mi in MediaItem, + where: fragment("searchable @@ websearch_to_tsquery(?)", ^search_term), + select_merge: %{ + matching_search_term: + fragment( + """ + ts_headline( + 'english', + CONCAT(title, ' ', description), + websearch_to_tsquery(?), + 'StartSel=[PF_HIGHLIGHT],StopSel=[/PF_HIGHLIGHT]' + ) + """, + ^search_term + ) + }, + order_by: { + :desc, + fragment("ts_rank_cd(searchable, websearch_to_tsquery(?), 0)", ^search_term) + }, + limit: ^limit + ) + |> Repo.all() + end + @doc """ Gets a single media_item. diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index c2ddce4..838cc14 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -41,6 +41,8 @@ defmodule Pinchflat.Media.MediaItem do # Will very likely revisit because I can't leave well-enough alone. field :subtitle_filepaths, {:array, {:array, :string}}, default: [] + field :matching_search_term, :string, virtual: true + belongs_to :source, Source has_one :metadata, MediaMetadata, on_replace: :update diff --git a/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs b/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs new file mode 100644 index 0000000..04de1d7 --- /dev/null +++ b/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs @@ -0,0 +1,28 @@ +defmodule Pinchflat.Repo.Migrations.AddSearchFieldToMediaItems do + use Ecto.Migration + + def up do + execute """ + ALTER TABLE media_items + ADD COLUMN searchable tsvector + GENERATED ALWAYS AS ( + setweight(to_tsvector('english', coalesce(title, '')), 'A') || + setweight(to_tsvector('english', coalesce(description, '')), 'B') + ) STORED; + """ + + execute """ + CREATE INDEX media_items_searchable_idx ON media_items USING gin(searchable); + """ + end + + def down do + execute """ + DROP INDEX media_items_searchable_idx; + """ + + alter table(:media_items) do + remove :searchable + end + end +end diff --git a/test/pinchflat/media_client/video_downloader_test.exs b/test/pinchflat/media_client/video_downloader_test.exs index 0d6acb3..794f6b5 100644 --- a/test/pinchflat/media_client/video_downloader_test.exs +++ b/test/pinchflat/media_client/video_downloader_test.exs @@ -60,7 +60,7 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do test "it sets the media_downloaded_at", %{media_item: media_item} do assert media_item.media_downloaded_at == nil assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item) - assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 1 + assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2 end test "it extracts the title", %{media_item: media_item} do diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index bcf758c..d20a92b 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -183,6 +183,37 @@ defmodule Pinchflat.MediaTest do end end + describe "search/1" do + setup do + media_item = + media_item_fixture(%{ + title: "The quick brown fox", + description: "jumps over the lazy dog" + }) + + {:ok, %{media_item_id: media_item.id}} + end + + test "searches based on title", %{media_item_id: media_item_id} do + assert [%{id: ^media_item_id}] = Media.search("quick") + end + + test "searches based on description", %{media_item_id: media_item_id} do + assert [%{id: ^media_item_id}] = Media.search("lazy") + end + + test "adds a matching_search_term attribute with the relevant text" do + assert [res] = Media.search("quick") + assert String.contains?(res.matching_search_term, "The [PF_HIGHLIGHT]quick[/PF_HIGHLIGHT] brown fox") + end + + test "optionall lets you specify a limit" do + media_item_fixture(%{title: "The small gray dog"}) + + assert [_] = Media.search("dog", limit: 1) + end + end + describe "get_media_item!/1" do test "it returns the media_item with given id" do media_item = media_item_fixture() diff --git a/test/pinchflat/tasks/source_tasks_test.exs b/test/pinchflat/tasks/source_tasks_test.exs index 662193d..c2499ec 100644 --- a/test/pinchflat/tasks/source_tasks_test.exs +++ b/test/pinchflat/tasks/source_tasks_test.exs @@ -111,7 +111,7 @@ defmodule Pinchflat.Tasks.SourceTasksTest do SourceTasks.index_media_items(source) source = Repo.reload!(source) - assert DateTime.diff(DateTime.utc_now(), source.last_indexed_at) < 1 + assert DateTime.diff(DateTime.utc_now(), source.last_indexed_at) < 2 end end