|
| 1 | +/** |
| 2 | + * Migration 004: Performance Indexes |
| 3 | + * |
| 4 | + * Adds missing composite indexes and a pg_trgm GIN index to resolve: |
| 5 | + * |
| 6 | + * 1. ai_feedback trend queries — getFeedbackTrend() filters by guild_id AND |
| 7 | + * created_at but only had a single-column guild_id index, forcing a full |
| 8 | + * guild scan + sort for every trend call. |
| 9 | + * |
| 10 | + * 2. conversations ILIKE search — content ILIKE '%...%' is a seq-scan |
| 11 | + * without pg_trgm. Installing the extension + GIN index reduces search from |
| 12 | + * O(n) to O(log n * trigram matches). |
| 13 | + * |
| 14 | + * 3. conversations(guild_id, created_at) — The default 30-day listing query |
| 15 | + * (WHERE guild_id = $1 AND created_at >= $2 ORDER BY created_at DESC) |
| 16 | + * benefits from a dedicated 2-column index over the existing 3-column |
| 17 | + * (guild_id, channel_id, created_at) composite when channel_id is not filtered. |
| 18 | + * |
| 19 | + * 4. flagged_messages(guild_id, message_id) — POST /flag and the detail |
| 20 | + * endpoint both do WHERE guild_id = $1 AND message_id = ANY($2) which |
| 21 | + * the existing (guild_id, status) index cannot serve efficiently. |
| 22 | + */ |
| 23 | + |
| 24 | +/** @param {import('node-pg-migrate').MigrationBuilder} pgm */ |
| 25 | +exports.up = (pgm) => { |
| 26 | + // ai_feedback: composite for trend + recent queries |
| 27 | + // getFeedbackTrend: WHERE guild_id = $1 AND created_at >= NOW() - INTERVAL ... |
| 28 | + // getRecentFeedback: WHERE guild_id = $1 ORDER BY created_at DESC LIMIT $2 |
| 29 | + pgm.sql(` |
| 30 | + CREATE INDEX IF NOT EXISTS idx_ai_feedback_guild_created |
| 31 | + ON ai_feedback(guild_id, created_at DESC) |
| 32 | + `); |
| 33 | + |
| 34 | + // conversations: pg_trgm for ILIKE searches |
| 35 | + // Enable the extension first (idempotent) |
| 36 | + pgm.sql(`CREATE EXTENSION IF NOT EXISTS pg_trgm`); |
| 37 | + |
| 38 | + // GIN index over content column -- supports col ILIKE '%term%' and col ~ 'pattern' |
| 39 | + pgm.sql(` |
| 40 | + CREATE INDEX IF NOT EXISTS idx_conversations_content_trgm |
| 41 | + ON conversations USING gin(content gin_trgm_ops) |
| 42 | + `); |
| 43 | + |
| 44 | + // conversations: (guild_id, created_at) for default 30-day listing |
| 45 | + // The existing idx_conversations_guild_channel_created covers (guild_id, channel_id, created_at) |
| 46 | + // but queries that filter only by guild_id + date range skip the channel_id column, |
| 47 | + // making this 2-column index cheaper to scan. |
| 48 | + pgm.sql(` |
| 49 | + CREATE INDEX IF NOT EXISTS idx_conversations_guild_created |
| 50 | + ON conversations(guild_id, created_at DESC) |
| 51 | + `); |
| 52 | + |
| 53 | + // flagged_messages: (guild_id, message_id) for detail + flag endpoints |
| 54 | + // Used by: |
| 55 | + // GET /:conversationId -> WHERE guild_id = $1 AND message_id = ANY($2) |
| 56 | + // POST /:conversationId/flag -> msgCheck + anchorCheck in parallel |
| 57 | + pgm.sql(` |
| 58 | + CREATE INDEX IF NOT EXISTS idx_flagged_messages_guild_message |
| 59 | + ON flagged_messages(guild_id, message_id) |
| 60 | + `); |
| 61 | +}; |
| 62 | + |
| 63 | +/** @param {import('node-pg-migrate').MigrationBuilder} pgm */ |
| 64 | +exports.down = (pgm) => { |
| 65 | + pgm.sql(`DROP INDEX IF EXISTS idx_flagged_messages_guild_message`); |
| 66 | + pgm.sql(`DROP INDEX IF EXISTS idx_conversations_guild_created`); |
| 67 | + pgm.sql(`DROP INDEX IF EXISTS idx_conversations_content_trgm`); |
| 68 | + pgm.sql(`DROP INDEX IF EXISTS idx_ai_feedback_guild_created`); |
| 69 | + // Note: do NOT drop pg_trgm extension on down -- it may be used elsewhere. |
| 70 | +}; |
0 commit comments