Tech Stack
- Next.js (App Router) for the frontend and API routes
- Node.js backend for search and catalog sync
- PostgreSQL for product metadata and analytics
- Redis for caching search results and embeddings
- GraphQL for flexible querying from the storefront
- Prisma for type-safe DB access
- Embedding model + vector search (e.g. pgvector or a dedicated vector store) for semantic matching
The Challenge
The Solution
1. Type-safe search API and response
Tsx
// types/search.ts
export interface SearchParams {
query: string;
categoryId?: string;
limit?: number;
cursor?: string;
}
export interface SearchResult {
id: string;
title: string;
slug: string;
price: number;
imageUrl: string;
score: number;
}
export interface SearchResponse {
results: SearchResult[];
nextCursor: string | null;
latencyMs: number;
}
2. Cache-first, then semantic lookup
Tsx
// app/api/search/route.ts (simplified pattern)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q')?.trim() ?? '';
const cacheKey = `search:${query}:${searchParams.toString()}`;
const cached = await redis.get(cacheKey);
if (cached) return Response.json(cached as SearchResponse);
const start = performance.now();
const results = await runSemanticSearch(query, searchParams);
const latencyMs = Math.round(performance.now() - start);
const response: SearchResponse = { results, nextCursor: null, latencyMs };
await redis.setex(cacheKey, 300, JSON.stringify(response)); // 5 min TTL
return Response.json(response);
}