T-HUB/apps/api/migrations/025_archive_search.sql
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

55 lines
2.7 KiB
PL/PgSQL

-- 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;