fix(search): оживить trgm-индекс — убрать coalesce из LIKE-предикатов (#443, ревью)
Ревьюер поймал EXPLAIN'ом: lookup-query фильтровал по LOWER(f_unaccent(coalesce(col,''))), а GIN-trgm-индексы — на coalesce-free LOWER(f_unaccent(col)). Postgres берёт функциональный индекс только при точном совпадении выражения, так что обёртка coalesce отключала индекс → Seq Scan по pages на КАЖДЫЙ lookup (MCP-клиент всегда шлёт substring:true). Зелёный int-гейт это не ловил (проверял корректность на крошечном датасете, не использование индекса). - search.service.ts: убрал coalesce из двух index-driving LIKE-предикатов (title + text_content). Семантически эквивалентно: NULL LIKE '%q%' = NULL (falsy), NULL-страница просто не матчится — как пустая строка не матчит %q%. SELECT/ORDER BY оставлены с coalesce (индекс не выбирают, Node гардит NULL). - Миграция: убран избыточный ре-ассерт idx_pages_title_trgm — его создаёт #348 на том же coalesce-free выражении, теперь title-предикат его использует. idx_pages_text_content_trgm без изменений (уже coalesce-free), поправлены вводящие в заблуждение комментарии. down() дропает только text-индекс. - Новый тест search-lookup-explain.int-spec.ts (3): title→idx_pages_title_trgm, text→idx_pages_text_content_trgm (оба ассертят отсутствие Seq Scan on pages), + негативный контроль (coalesce-обёртка не может использовать индекс) против тихой ре-регрессии. Дискриминатор enable_seqscan=off. - CHANGELOG: две записи (opt-in substring lookup mode + смена shape MCP search). EXPLAIN на реальном PG: обе fixed-ветки → Bitmap Index Scan on trgm; buggy coalesce → Seq Scan (Disabled:true). Гейт: server jest src/core/search 27/27 (16 исходных int без изменений + 3 EXPLAIN + 8 unit), mcp node --test 708/708, tsc чисто. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,12 @@ import { type Kysely, sql } from 'kysely';
|
||||
* pages.text_content. A leading wildcard cannot use a b-tree index, so without a
|
||||
* GIN trigram index each such predicate is a sequential scan.
|
||||
*
|
||||
* - TITLE: the title predicate is IDENTICAL to the one added for /search/suggest
|
||||
* (#348), which already created `idx_pages_title_trgm` on
|
||||
* `(LOWER(f_unaccent(title))) gin_trgm_ops`. We re-assert it here with
|
||||
* `IF NOT EXISTS` so a fresh DB that somehow lacks it still gets it, and so
|
||||
* this migration is self-describing. On an existing DB it is a no-op.
|
||||
* - TITLE: the lookup-mode title predicate is `LOWER(f_unaccent(title)) LIKE
|
||||
* '%q%'` (coalesce-free, so it can use a functional index), which is IDENTICAL
|
||||
* to the one added for /search/suggest (#348). #348's perf-indexes migration
|
||||
* already created `idx_pages_title_trgm` on `(LOWER(f_unaccent(title)))
|
||||
* gin_trgm_ops`, so the title predicate is already covered — we do NOT
|
||||
* re-create that index here (it would be redundant).
|
||||
*
|
||||
* - TEXT_CONTENT: NEW. The substring branch scans text_content when the query
|
||||
* is not titleOnly. text_content is the large column, so a GIN trigram index
|
||||
@@ -34,15 +35,13 @@ import { type Kysely, sql } from 'kysely';
|
||||
* here). Small/typical tenants are unaffected.
|
||||
*/
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// Title trigram index — matches the lookup-mode title predicate exactly.
|
||||
// Already present from #348 on existing DBs; re-asserted for freshness.
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_title_trgm
|
||||
ON pages USING gin ((LOWER(f_unaccent(title))) gin_trgm_ops)
|
||||
`.execute(db);
|
||||
// The title predicate is served by #348's idx_pages_title_trgm — see header.
|
||||
// Only the text_content index is introduced here.
|
||||
|
||||
// text_content trigram index — accelerates the lookup-mode text substring
|
||||
// predicate. Expression matches the predicate in search.service.ts.
|
||||
// text_content trigram index. Its expression is coalesce-free —
|
||||
// `LOWER(f_unaccent(text_content))` — to EXACTLY match the coalesce-free
|
||||
// lookup-mode text substring predicate in search.service.ts, so Postgres can
|
||||
// use it (a `coalesce(...)` mismatch would silently fall back to a Seq Scan).
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_text_content_trgm
|
||||
ON pages USING gin ((LOWER(f_unaccent(text_content))) gin_trgm_ops)
|
||||
|
||||
Reference in New Issue
Block a user