-- 025: WP15 archive full-text search (firehose research recommendation: -- Postgres FTS phase-1) over archived_feed_items. -- -- Search surface: -- * tsvector over text_content + channel/attribution metadata, kept in sync -- by trigger (the normalizer writes text_content on insert AND on edits, -- so a plain trigger beats a generated column: 015's reject_workspace_change -- trigger guards workspace_id, and edits must reindex text). -- * GIN indexes: tsvector lookup + pg_trgm similarity for typo tolerance. -- * The API ORs websearch_to_tsquery matching with pg_trgm similarity so a -- misspelled term still matches via trigram distance (similarity >= 0.3). -- * RLS is untouched: archived_feed_items keeps its tenant_isolation policy -- from 015; search runs inside withTenant() so FORCE RLS binds honestly. -- -- CREATE EXTENSION requires privileges sp_app lacks; this migration runs as -- the table owner only (015 grants are preserved below for completeness). CREATE EXTENSION IF NOT EXISTS pg_trgm; ALTER TABLE archived_feed_items ADD COLUMN search_vector tsvector NOT NULL DEFAULT ''; -- String-valued attribution keys worth indexing as metadata (normalizer maps -- Telegram author/origin fields here). Kept narrow on purpose. CREATE FUNCTION archived_attribution_text(attribution jsonb) RETURNS text LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$ SELECT coalesce(string_agg(value, ' '), '') FROM jsonb_each_text(attribution) WHERE key IN ('channelUsername', 'channelTitle', 'author', 'authorSignature', 'forwardFromChat', 'forwardFromUser', 'signature') $$; CREATE FUNCTION archived_feed_items_search_vector() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('simple', coalesce(NEW.channel_id, '')), 'A') || setweight(to_tsvector('simple', archived_attribution_text(NEW.attribution)), 'B') || to_tsvector('simple', coalesce(NEW.text_content, '')); RETURN NEW; END $$; CREATE TRIGGER trg_archived_feed_items_search BEFORE INSERT OR UPDATE OF text_content, channel_id, attribution ON archived_feed_items FOR EACH ROW EXECUTE FUNCTION archived_feed_items_search_vector(); -- Exact/lexical lookups. 'simple' config on purpose: the corpus is a -- multilingual firehose, no stemming dictionary is "right", so phase-1 is -- exact term matching plus trigram fuzziness. CREATE INDEX archived_feed_items_search ON archived_feed_items USING gin (search_vector); -- Fuzzy path (typo tolerance) + unindexed-corpus ILIKE support on text. CREATE INDEX archived_feed_items_text_trgm ON archived_feed_items USING gin (text_content gin_trgm_ops); CREATE INDEX archived_feed_items_channel_trgm ON archived_feed_items USING gin (channel_id gin_trgm_ops); GRANT SELECT ON archived_feed_items TO sp_app;