feat(db): add FTS GIN + savedSearch partial indexes (GOO-118)

Convert the query-optimization recommendations from GOO-57 audit plan
document into concrete Prisma migration changes. Neither index can be
expressed in Prisma schema (expression index / partial WHERE), so both
land as raw SQL in a single migration.

Indexes added:

- idx_property_fts — GIN expression index on Property matching the
  search-query-builder FTS_COLUMNS expression exactly:
    to_tsvector('simple', coalesce(title,'') || ' ' || coalesce(description,'')
                 || ' ' || coalesce(address,'') || ' ' || coalesce(district,'')
                 || ' ' || coalesce(city,''))
  Addresses GOO-57 M-3 (missing GIN index for FTS).

- idx_savedsearch_alert_enabled — partial btree on SavedSearch(createdAt)
  WHERE alertEnabled = true, used by the residential alert listeners and
  the saved-search cron (supports GOO-57 H-1 / H-2 follow-up work —
  eliminating the seq scan is the prerequisite for cursor batching).

Benchmarks (local PG16, synthetic data):

Property FTS with a selective term (50k rows, ~10 matching):
  with idx_property_fts:    3.97 ms (Bitmap Heap Scan, 338 buffers)
  without index:          242.56 ms (Parallel Seq Scan, 1784 buffers)
  → ~61x faster.

SavedSearch alert scan (100k rows, 5% alertEnabled, LIMIT 500 ORDER BY
createdAt DESC):
  with idx_savedsearch_alert_enabled:  0.48 ms (Index Scan Backward)
  without index:                       6.05 ms (Seq Scan + top-N sort)
  → ~12x faster, seq scan eliminated.

Hook-up verified: pnpm db:generate clean; raw migration applies via
prisma migrate deploy; post-migration \d confirms both indexes are
present with the expected definitions.

Refs: GOO-118, GOO-57
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-23 21:25:09 +07:00
parent 6b23bfb756
commit 915b9fd806

View File

@@ -0,0 +1,30 @@
-- GOO-118 — DB query optimization migration
-- Source: GOO-57 audit (plan document: /GOO/issues/GOO-57#document-plan)
--
-- Changes:
-- 1. GIN expression index for Property full-text search (addresses M-3)
-- Matches the exact expression used by
-- apps/api/src/modules/search/infrastructure/services/search-query-builder.ts (FTS_COLUMNS).
--
-- 2. Partial index on SavedSearch (createdAt) WHERE alertEnabled = true (supports H-1 / H-2)
-- Listener / cron loads rows filtered by alertEnabled = true; partial index
-- lets the query skip the seq scan and stays small (only ~alertEnabled rows).
-- 1) GIN FTS index on Property
CREATE INDEX IF NOT EXISTS "idx_property_fts"
ON "Property"
USING GIN (
to_tsvector(
'simple',
coalesce("title", '') || ' ' ||
coalesce("description", '') || ' ' ||
coalesce("address", '') || ' ' ||
coalesce("district", '') || ' ' ||
coalesce("city", '')
)
);
-- 2) Partial index: only alert-enabled saved searches
CREATE INDEX IF NOT EXISTS "idx_savedsearch_alert_enabled"
ON "SavedSearch" ("createdAt")
WHERE "alertEnabled" = true;