Added a search method using postgres fulltext search

This commit is contained in:
Kieran Eglin 2024-02-09 22:22:02 -08:00
parent 00f7aef8dc
commit ed759e5e09
No known key found for this signature in database
GPG key ID: 193984967FCF432D
6 changed files with 99 additions and 2 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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