feat(db): add FTS GIN + savedSearch partial indexes (GOO-118) (#3)
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 14s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 8s
Deploy / Build API Image (push) Failing after 24s
Deploy / Build Web Image (push) Failing after 17s
Deploy / Build AI Services Image (push) Failing after 13s
E2E Tests / Playwright E2E (push) Failing after 16s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 12m19s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 8s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m4s
Security Scanning / Trivy Scan — Web Image (push) Failing after 41s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 35s
Security Scanning / Trivy Filesystem Scan (push) Failing after 31s
Security Scanning / Security Gate (push) Failing after 1s

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:
Velik
2026-04-24 01:04:22 +07:00
committed by GitHub
parent 6b23bfb756
commit dfb398131d

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;