Shortcuts
BlogMarch 3, 2025

Mohamed Elbarry
How I Architected Semantic Search for E-commerce That Actually Converts
E-commerce search is broken when it only matches keywords. Users type "comfortable running shoes for long runs" and get a random mix of brands and styles because the system never understood intent. At Lumin Search, we built semantic search so product discovery actually reflects what people mean—and we kept it fast enough that it feels instant. Here’s how we did it and what I’d do again.
  • 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
Two things made this hard. First, latency: if search takes more than a couple hundred milliseconds, users assume it’s broken and bounce. E-commerce research shows that even small search delays can cost conversions, so we aimed for sub-100ms p99. We had to embed the query, hit the vector index, apply filters (category, price, availability), and return ranked results without blowing the budget. Second, freshness: product catalogs change all the time. Stale search results are worse than slow ones. We needed real-time catalog syncing so that what we indexed matched what was actually in the store, without re-indexing the entire catalog on every update. We defined clear types for the search request and response so the frontend and backend stayed in sync and we could refactor safely.
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;
}
We didn’t hit the embedding model or the vector index on every request. For repeated or popular queries we served from Redis; only cache misses went through the full pipeline. Semantic and result caching can cut latency and cost sharply on repeated queries—we saw the same effect. That kept p99 latency under 100ms even when the embedding service was under load.
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);
}
Instead of re-embedding the whole catalog on every product update, we synced incrementally: new or updated products were embedded and written to the vector index; deleted products were removed. The storefront pushed events (or we polled a webhook) so our index stayed aligned with the live catalog. That kept search accurate without sacrificing latency. We also made sure the search path never did heavy work on the critical path—embedding and index updates ran in background jobs, so the GET endpoint stayed fast. On Lumin Search we got search latency under 100ms at p99, kept the catalog in sync with the store in real time, and saw 35% higher product engagement and a 20% lift in conversion compared to the previous keyword-only search. The numbers came from the analytics we built alongside the search engine—we could see exactly which queries led to clicks and purchases. Semantic search wasn’t a nice-to-have; it was the lever that made product discovery actually convert. I’m currently looking for new challenges in the AI and Full Stack space. If you’re building something interesting, let's chat.
Share this post: