"""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)
"""
)