diff --git a/backend/alembic/versions/0029_unaccent_search.py b/backend/alembic/versions/0029_unaccent_search.py new file mode 100644 index 0000000..adc7f66 --- /dev/null +++ b/backend/alembic/versions/0029_unaccent_search.py @@ -0,0 +1,31 @@ +"""accent-insensitive feed search (unaccent) + +Revision ID: 0029_unaccent_search +Revises: 0028_live_search_provenance +Create Date: 2026-06-29 + +Enables the PostgreSQL `unaccent` contrib extension so the feed's text search can match +regardless of diacritics — e.g. searching "tiesto" finds titles spelled "Tiësto". The feed +query wraps both sides in unaccent(); this migration just makes the function available. +`unaccent` ships with the official postgres image (postgresql-contrib). The DB role is a +superuser in our deployments, so CREATE EXTENSION succeeds; IF NOT EXISTS keeps it idempotent. +""" +from typing import Sequence, Union + +from alembic import op + +revision: str = "0029_unaccent_search" +down_revision: Union[str, None] = "0028_live_search_provenance" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("CREATE EXTENSION IF NOT EXISTS unaccent") + + +def downgrade() -> None: + # Intentionally left in place: other code/queries may rely on unaccent() and dropping a + # contrib extension is rarely what's wanted on a downgrade. (The feed query reverts with + # the code, not this migration.) + pass diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py index d9f405b..09648d5 100644 --- a/backend/app/routes/feed.py +++ b/backend/app/routes/feed.py @@ -205,8 +205,15 @@ def _filtered_query( ) + timedelta(days=1) query = query.where(Video.published_at < end) if q: - like = f"%{q}%" - query = query.where(or_(Video.title.ilike(like), Channel.title.ilike(like))) + # Accent-insensitive: unaccent() both sides so "tiesto" matches "Tiësto" (ILIKE alone + # is case- but not diacritic-insensitive). unaccent is enabled by migration 0029. + like = func.unaccent(f"%{q}%") + query = query.where( + or_( + func.unaccent(Video.title).ilike(like), + func.unaccent(Channel.title).ilike(like), + ) + ) if tags: # AND across tag categories (e.g. language AND topic narrows), OR within a