"""richer full-text search document (title + keywords + search terms + description) Revision ID: 0032_search_document Revises: 0031_title_fts Create Date: 2026-06-30 Broadens the feed's full-text search from the title alone to a weighted search document, so the local search behaves more like YouTube's — finding videos by the uploader's own keywords or by the search query that surfaced them, not only by words in the title: - `keywords` — the creator's snippet.tags (already fetched with the snippet we request, so zero extra quota), stored space-joined. - `search_terms` — distinct live-search queries that surfaced the video (across all users), appended by the search route. Folds YouTube's relevance judgement into local search: a video YT returned for a query becomes findable by it locally. These plus a truncated description feed a STORED generated `search_vector` column, weighted title (A) > keywords+search_terms (B) > description (C) so ts_rank ranks title hits highest. A plain GIN index on the column guarantees the planner uses it (no expression/param matching). The title-only expression index from 0031 is dropped (superseded). Existing rows get title + description indexed immediately; keywords/search_terms fill in as videos are (re-)enriched and searched. Uses the `unaccent_simple` config from 0031 (accent-insensitive). """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0032_search_document" down_revision: Union[str, None] = "0031_title_fts" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None _SEARCH_VECTOR_EXPR = ( "setweight(to_tsvector('public.unaccent_simple', coalesce(title, '')), 'A') || " "setweight(to_tsvector('public.unaccent_simple', coalesce(keywords, '') || ' ' || " "coalesce(search_terms, '')), 'B') || " "setweight(to_tsvector('public.unaccent_simple', left(coalesce(description, ''), 1000)), 'C')" ) def upgrade() -> None: op.add_column("videos", sa.Column("keywords", sa.Text(), nullable=True)) op.add_column("videos", sa.Column("search_terms", sa.Text(), nullable=True)) # Replace the title-only index with a generated weighted search_vector + its GIN index. op.execute("DROP INDEX IF EXISTS ix_videos_title_fts") op.execute( f"ALTER TABLE videos ADD COLUMN search_vector tsvector " f"GENERATED ALWAYS AS ({_SEARCH_VECTOR_EXPR}) STORED" ) op.execute( "CREATE INDEX ix_videos_search_vector ON videos USING gin (search_vector)" ) def downgrade() -> None: op.execute("DROP INDEX IF EXISTS ix_videos_search_vector") op.drop_column("videos", "search_vector") op.drop_column("videos", "search_terms") op.drop_column("videos", "keywords") # Recreate the title-only FTS index from 0031. op.execute( "CREATE INDEX ix_videos_title_fts ON videos " "USING gin (to_tsvector('public.unaccent_simple', coalesce(title, '')))" )