Newer
Older
vmk-360-data_collector / alembic / versions / 02530c3f9c6b_switch_fts_to_ukrainian_config.py
"""switch fts to ukrainian config

Revision ID: 02530c3f9c6b
Revises: 5e1da0609f70
Create Date: 2026-06-12 21:57:35.862929

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '02530c3f9c6b'
down_revision: Union[str, Sequence[str], None] = '5e1da0609f70'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Switch FTS from simple to ukrainian text-search config."""
    # Remove old generated column + index
    op.execute(
        "DROP INDEX IF EXISTS ix_property_listings_search_vector_gin"
    )
    op.execute(
        "ALTER TABLE property_listings DROP COLUMN IF EXISTS search_vector"
    )
    op.execute(
        "DROP FUNCTION IF EXISTS to_tsvector_simple(text)"
    )

    # Ensure ukrainian text-search config exists (copy from simple as base)
    op.execute(
        """
        DO $$
        BEGIN
            CREATE TEXT SEARCH CONFIGURATION ukrainian (COPY = simple);
        EXCEPTION WHEN duplicate_object THEN
            -- already exists, nothing to do
        END
        $$;
        """
    )

    # Create stop-words dictionary for Ukrainian
    op.execute(
        """
        DO $$
        BEGIN
            CREATE TEXT SEARCH DICTIONARY ukrainian_stop (
                TEMPLATE = simple,
                STOPWORDS = ukrainian
            );
        EXCEPTION WHEN duplicate_object THEN
            -- already exists
        END
        $$;
        """
    )

    # Configure ukrainian text-search to use stop-words filtering
    op.execute(
        """
        ALTER TEXT SEARCH CONFIGURATION ukrainian
            ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part
            WITH ukrainian_stop, simple;
        """
    )

    # Create immutable wrapper for ukrainian config
    op.execute(
        """
        CREATE OR REPLACE FUNCTION to_tsvector_ukrainian(text)
        RETURNS tsvector
        LANGUAGE sql IMMUTABLE PARALLEL SAFE
        AS $$ SELECT to_tsvector('ukrainian', $1); $$;
        """
    )

    # Add generated column using ukrainian config
    op.execute(
        """
        ALTER TABLE property_listings
        ADD COLUMN IF NOT EXISTS search_vector tsvector
        GENERATED ALWAYS AS (
            to_tsvector_ukrainian(
                coalesce(title, '') || ' ' ||
                coalesce(description, '') || ' ' ||
                coalesce(generated_description, '') || ' ' ||
                coalesce(city, '') || ' ' ||
                coalesce(district, '') || ' ' ||
                coalesce(micro_district, '') || ' ' ||
                coalesce(street, '') || ' ' ||
                coalesce(address_raw, '')
            )
        ) STORED
        """
    )

    # Recreate GIN index
    op.execute(
        """
        CREATE INDEX IF NOT EXISTS ix_property_listings_search_vector_gin
        ON property_listings USING GIN(search_vector)
        """
    )


def downgrade() -> None:
    """Revert FTS back to simple text-search config."""
    op.execute(
        "DROP INDEX IF EXISTS ix_property_listings_search_vector_gin"
    )
    op.execute(
        "ALTER TABLE property_listings DROP COLUMN IF EXISTS search_vector"
    )
    op.execute(
        "DROP FUNCTION IF EXISTS to_tsvector_ukrainian(text)"
    )
    op.execute(
        "DROP TEXT SEARCH DICTIONARY IF EXISTS ukrainian_stop CASCADE"
    )
    op.execute(
        "DROP TEXT SEARCH CONFIGURATION IF EXISTS ukrainian CASCADE"
    )

    op.execute(
        """
        CREATE OR REPLACE FUNCTION to_tsvector_simple(text)
        RETURNS tsvector
        LANGUAGE sql IMMUTABLE PARALLEL SAFE
        AS $$ SELECT to_tsvector('simple', $1); $$;
        """
    )

    op.execute(
        """
        ALTER TABLE property_listings
        ADD COLUMN IF NOT EXISTS search_vector tsvector
        GENERATED ALWAYS AS (
            to_tsvector_simple(
                coalesce(title, '') || ' ' ||
                coalesce(description, '') || ' ' ||
                coalesce(generated_description, '') || ' ' ||
                coalesce(city, '') || ' ' ||
                coalesce(district, '') || ' ' ||
                coalesce(micro_district, '') || ' ' ||
                coalesce(street, '') || ' ' ||
                coalesce(address_raw, '')
            )
        ) STORED
        """
    )

    op.execute(
        """
        CREATE INDEX IF NOT EXISTS ix_property_listings_search_vector_gin
        ON property_listings USING GIN(search_vector)
        """
    )