Fix bug with special chars in search form

This commit is contained in:
Kieran Eglin 2024-05-23 14:52:40 -07:00
parent b167ada1c4
commit 2c17482924
No known key found for this signature in database
GPG key ID: 193984967FCF432D
4 changed files with 42 additions and 8 deletions

View file

@ -128,9 +128,11 @@ defmodule Pinchflat.Media.MediaQuery do
def matches_search_term(nil), do: dynamic([mi], true)
def matches_search_term(term) do
case String.trim(term) do
escaped_term = clean_search_term(term)
case String.trim(escaped_term) do
"" -> dynamic([mi], true)
term -> dynamic([mi], fragment("media_items_search_index MATCH ?", ^term))
_ -> dynamic([mi], fragment("media_items_search_index MATCH ?", ^escaped_term))
end
end
@ -161,9 +163,11 @@ defmodule Pinchflat.Media.MediaQuery do
def matching_search_term(query, nil), do: query
def matching_search_term(query, term) do
escaped_term = clean_search_term(term)
from(mi in query,
join: mi_search_index in assoc(mi, :media_items_search_index),
where: fragment("media_items_search_index MATCH ?", ^term),
where: fragment("media_items_search_index MATCH ?", ^escaped_term),
select_merge: %{
matching_search_term:
fragment("""
@ -175,4 +179,23 @@ defmodule Pinchflat.Media.MediaQuery do
order_by: [desc: fragment("rank")]
)
end
# SQLite's FTS5 is very picky about what it will accept as a search term.
# To that end, we need to clean up the search term before passing it to the
# MATCH clause.
# This method:
# - Trims leading and trailing whitespace
# - Collapses multiple spaces into a single space
# - Removes quote characters
# - Wraps any word in quotes (must happen after the double quote replacement)
#
# This allows for works with apostrophes and quotes to be searched for correctly
defp clean_search_term(term) do
term
|> String.trim()
|> String.replace(~r/\s+/, " ")
|> String.split(~r/\s+/)
|> Enum.map(fn str -> String.replace(str, ~s("), "") end)
|> Enum.map_join(" ", fn str -> ~s("#{str}") end)
end
end

View file

@ -72,7 +72,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
media_state = session["media_state"]
source = Sources.get_source!(session["source_id"])
base_query = generate_base_query(source, media_state)
pagination_attrs = fetch_pagination_attributes(base_query, page, "friendly")
pagination_attrs = fetch_pagination_attributes(base_query, page, nil)
new_assigns =
Map.merge(

View file

@ -3,10 +3,10 @@ defmodule Pinchflat.Repo.Migrations.ChangeMediaItemsSearchIndexTokenizer do
def up do
# These all need to run as part of separate `execute` blocks. Do NOT ask me why.
execute "DROP TRIGGER media_items_search_index_insert;"
execute "DROP TRIGGER media_items_search_index_update;"
execute "DROP TRIGGER media_items_search_index_delete;"
execute "DROP TABLE media_items_search_index;"
execute "DROP TRIGGER IF EXISTS media_items_search_index_insert;"
execute "DROP TRIGGER IF EXISTS media_items_search_index_update;"
execute "DROP TRIGGER IF EXISTS media_items_search_index_delete;"
execute "DROP TABLE IF EXISTS media_items_search_index;"
execute """
CREATE VIRTUAL TABLE media_items_search_index USING fts5(

View file

@ -727,6 +727,17 @@ defmodule Pinchflat.MediaTest do
test "returns an empty list when the search term is nil" do
assert [] = Media.search(nil)
end
test "doesn't blow up if there's an apostrophe or quotes in the search term" do
assert [] = Media.search("don't expl'ode")
assert [] = Media.search(~s(dont expl"o"de))
assert [] = Media.search(~s(dont explo"de))
end
test "doesn't blow up if there is a trailing operand" do
assert [] = Media.search("foo OR")
assert [] = Media.search("foo AND")
end
end
describe "get_media_item!/1" do