Files
buzz/scripts/maintenance/profile_search_text_fields.sql
Matt Toohey cea391b7f7 feat(search): surface the pending 0056 profile-search rewrite to operators
Add a shared `profile_search_policy` probe in buzz-db that reads the live
`events.search_tsv` expression, warn at relay startup while a populated
database still indexes whole kind-0 JSON, and expose the same probe as
`buzz-admin profile-search-policy`. Bound 0056's lock wait with a 5s
lock_timeout and document the operator procedure in
docs/profile-search-deployment.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-10-02 14:29:18 +10:00

78 lines
3.5 KiB
PL/PgSQL

-- OUT-OF-BAND MAINTENANCE: do not run from relay startup migrations.
--
-- Route kind-0 (profile) rows of a populated database through
-- profile_search_tsv(content) so profile search indexes names, not inline
-- avatar bytes. Migration 0056 creates the function at relay startup and
-- rewrites the column only when `events` is empty; this script finishes the
-- job for every other database. Run it after the relay has started at least
-- once on 0056 (the guard below refuses otherwise):
--
-- psql -v ON_ERROR_STOP=1 -f scripts/maintenance/profile_search_text_fields.sql
--
-- docs/profile-search-deployment.md has the full procedure: who may run it,
-- which timeouts to lift, how to verify, and how to tell whether a database
-- still needs it (the relay also warns at startup until it has run).
--
-- It wraps whatever expression the database has today, so it is correct for
-- both shapes in the field: the fresh-install allowlist from 0008 (kind 0 is
-- one of ARRAY[0, 9, 40002, 45001, 45003]) and the brownfield blocklist from
-- 0001/0005 (kind 0 falls through to to_tsvector). Every other kind keeps the
-- policy the database already had. Re-running is a no-op once applied.
--
-- Cost: PostgreSQL cannot alter a generated expression in place. DROP COLUMN is
-- metadata-only, but ADD COLUMN ... GENERATED ... STORED copies every row of
-- every events partition evaluating the expression, then CREATE INDEX rebuilds
-- the partitioned GIN. All of it runs under ACCESS EXCLUSIVE on `events` in
-- one transaction, so relay reads and writes of events queue behind it.
-- Transient disk is roughly a second copy of the events heap and TOAST plus
-- the new GIN plus WAL. Time is dominated by tokenizing every indexed chat
-- row (about 20 MB of content per second single-threaded). Size it first:
--
-- SELECT kind, count(*), pg_size_pretty(sum(octet_length(content)))
-- FROM events GROUP BY kind ORDER BY 2 DESC;
-- SELECT pg_size_pretty(pg_total_relation_size('events'));
--
-- Rehearse on a staging clone, time it, then schedule production. The
-- lock_timeout makes a busy relay fail the script quickly rather than queue
-- traffic behind the lock wait; rerun when the table is quieter. The index is
-- recreated from the stock definition; non-stock indexes or storage
-- parameters on search_tsv are not captured or replayed.
BEGIN;
SET LOCAL lock_timeout = '5s';
DO $$
DECLARE
existing_expression TEXT;
BEGIN
IF to_regprocedure('profile_search_tsv(text)') IS NULL THEN
RAISE EXCEPTION 'profile_search_tsv(text) is missing; start the relay once so migration 0056 creates it, then rerun';
END IF;
SELECT pg_get_expr(d.adbin, d.adrelid)
INTO existing_expression
FROM pg_attrdef d
JOIN pg_attribute a
ON a.attrelid = d.adrelid
AND a.attnum = d.adnum
WHERE d.adrelid = 'events'::regclass
AND a.attname = 'search_tsv';
IF existing_expression IS NULL THEN
RAISE EXCEPTION 'events.search_tsv generated expression not found';
END IF;
IF existing_expression LIKE '%profile_search_tsv(content)%' THEN
RAISE NOTICE 'events.search_tsv already routes kind 0 through profile_search_tsv; nothing to do';
RETURN;
END IF;
ALTER TABLE events DROP COLUMN search_tsv;
EXECUTE format(
'ALTER TABLE events ADD COLUMN search_tsv TSVECTOR GENERATED ALWAYS AS (CASE WHEN kind = 0 THEN profile_search_tsv(content) ELSE (%s) END) STORED',
existing_expression
);
CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv);
END $$;
COMMIT;