From 0049386e1b8db80e2d4a9cbc4ffed8d036d18327 Mon Sep 17 00:00:00 2001 From: WeDontKnow3 Date: Fri, 14 Nov 2025 22:47:49 -0300 Subject: [PATCH] Refactor GET search function with pagination and snippets --- src/routes/api/search/+server.ts | 119 ++++++++++++++++++------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/src/routes/api/search/+server.ts b/src/routes/api/search/+server.ts index 1ae3113..83cde2e 100644 --- a/src/routes/api/search/+server.ts +++ b/src/routes/api/search/+server.ts @@ -5,54 +5,73 @@ import { articles } from '$lib/server/db/schema'; import { timeQuery } from '$lib/server/db/timing'; const MAX_QUERY_LENGTH = 200; +const MAX_RESULTS = 50; +const DEFAULT_RESULTS = 10; +const MIN_QUERY_LENGTH = 2; -export async function GET({ url }) { - const queryParam = url.searchParams.get('q')?.trim() || ''; - const query = queryParam.slice(0, MAX_QUERY_LENGTH); - if (!query || query.length < 2) return json({ results: [] }); - - try { - const startTime = Date.now(); - - const rankingExpr = sql`( - ts_rank_cd(${articles.search_vector}, websearch_to_tsquery('english', ${query}), 32) * 2 + - CASE WHEN ${articles.title} ILIKE ${query} THEN 2 - WHEN ${articles.title} ILIKE ${query + '%'} THEN 1 - ELSE 0 - END - - (length(${articles.title}) * 0.001) - )`; - - const results = await timeQuery('SEARCH_search_articles', () => - db.select({ - id: articles.id, - title: articles.title, - slug: articles.slug, - rank: rankingExpr, - }) - .from(articles) - .where(sql`${articles.search_vector} @@ websearch_to_tsquery('english', ${query})`) - .orderBy(sql`${rankingExpr} DESC`) - .limit(50) - .execute() - ); - - console.log(`Search for "${query}" took ${Date.now() - startTime}ms`); - - return json({ - results: results.map(row => ({ - id: row.id, - title: row.title.slice(0, 200), - slug: row.slug, - content: "View article about " + row.title.slice(0, 100) + "...", - relevance: row.rank - })) - }); - } catch (error) { - console.error('Search error:', error); - return json({ - results: [], - error: "Search temporarily unavailable" - }, { status: 500 }); - } -} \ No newline at end of file +function normalizeQuery(q: string) { + return q.replace(/[\u0000-\u001F\u007F]/g, '').replace(/\s+/g, ' ').trim(); +} + +export async function GET({ url }: any) { + const raw = url.searchParams.get('q') ?? ''; + const pageParam = Number(url.searchParams.get('page') ?? '1'); + const limitParam = Number(url.searchParams.get('limit') ?? String(DEFAULT_RESULTS)); + + const queryParam = normalizeQuery(raw).slice(0, MAX_QUERY_LENGTH); + if (!queryParam || queryParam.length < MIN_QUERY_LENGTH) return json({ results: [] }); + + const page = Number.isFinite(pageParam) && pageParam > 0 ? Math.floor(pageParam) : 1; + const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), MAX_RESULTS) : DEFAULT_RESULTS; + const offset = (page - 1) * limit; + + try { + const startTime = Date.now(); + + const rankingExpr = sql`( + ts_rank_cd(${articles.search_vector}, websearch_to_tsquery('english', ${queryParam}), 32) * 2 + + CASE + WHEN lower(${articles.title}) = lower(${queryParam}) THEN 3 + WHEN ${articles.title} ILIKE ${queryParam + '%'} THEN 1 + ELSE 0 + END - (length(${articles.title}) * 0.001) + )`; + + const snippetExpr = sql`ts_headline('english', ${articles.content}, websearch_to_tsquery('english', ${queryParam}), 'MaxFragments=2, MinWords=4, MaxWords=35, StartSel=, StopSel=')`; + + const results = await timeQuery('SEARCH_search_articles', () => + db.select({ + id: articles.id, + title: articles.title, + slug: articles.slug, + rank: rankingExpr, + snippet: snippetExpr + }) + .from(articles) + .where(sql`${articles.search_vector} @@ websearch_to_tsquery('english', ${queryParam})`) + .orderBy(sql`${rankingExpr} DESC`) + .limit(limit) + .offset(offset) + .execute() + ); + + console.log(`Search for "${queryParam}" took ${Date.now() - startTime}ms`); + + return json({ + results: results.map((row) => ({ + id: row.id, + title: (row.title ?? '').slice(0, 200), + slug: row.slug, + content: (row.snippet ?? `View article about ${String(row.title ?? '').slice(0, 100)}...`).slice(0, 300), + relevance: Number(row.rank ?? 0) + })), + meta: { query: queryParam, page, limit } + }); + } catch (error) { + console.error('Search error:', error); + return json({ + results: [], + error: 'Search temporarily unavailable' + }, { status: 500 }); + } +}