Skip to content
Farid Vatani edited this page May 13, 2025 · 4 revisions

Project Wiki โ€” Smart URLโ€Based Search

This document provides a deep-dive into the technical architecture and rationale behind the Smart Search implementation using Next.js, MongoDB Atlas, OpenAI embeddings, and modern search UX patterns.

๐Ÿ“ Architecture Overview

This project implements a modular, scalable, AI-augmented search system using:

  • URL-based state โ€” Shareable, bookmarkable search results via query params
  • Hybrid Search โ€” Combines traditional full-text and fuzzy search with vector semantic search
  • Vector Embeddings โ€” Search beyond keywords using meaning (via OpenAI + MongoDB vector index)
  • Streaming UI โ€” Server Components + Suspense for smooth UX without unnecessary hydration
  • Auto-complete โ€” Type-ahead experience with MongoDB Atlas' autocomplete pipeline

๐Ÿ”ง MongoDB Atlas Setup

1. Create a Free Cluster

  • Go to: https://cloud.mongodb.com/
  • Create an M0 cluster (free tier)
  • Name your DB (e.g., mydb), and create a collection (e.g., recipes)

2. Create Database User

  • Go to Database Access
  • Add a user with read/write access
  • Use this in your .env as DATABASE_URL

3. Network Access

  • Whitelist your IP (0.0.0.0/0 for local testing)

๐Ÿงฎ Index Configuration

Go to Search โ†’ Indexes in MongoDB Atlas

๐Ÿ…ฐ๏ธ Default Full-Text Search

  • Create a Search Index on recipes
  • Default mappings: title, description as strings

๐Ÿ…ฑ๏ธ Autocomplete Index

  • Edit your default index
  • Set title โ†’ Autocomplete
  • Rebuild the index

๐Ÿ†Ž Vector Search Index

  • Create a new Vector Search Index
  • Collection: recipes
  • Field: embeddings
  • Dimensions: 1536 (for text-embedding-3-small)
  • Similarity: Euclidean or Cosine

๐ŸŒ High-Level Overview

User Input
   โ†“
<searchBar.tsx> (Client Component)
   โ†“
URL updates via useRouter().replace()
   โ†“
<page.tsx> (Server Component)
   โ†“
<recipeList.tsx> (Server Component w/ Suspense)
   โ†“
MongoDB Search (Text, Fuzzy, or Vector via Prisma or $aggregate)
   โ†“
Render Results

๐Ÿงญ Directory Structure

src/
โ”œโ”€โ”€ app/                 # App Router structure (Next.js 15+)
โ”‚   โ”œโ”€โ”€ api/autocomplete/route.ts  # Suggestion API
โ”‚   โ”œโ”€โ”€ globals.css      # Global styles
โ”‚   โ”œโ”€โ”€ layout.tsx       # Shared layout
โ”‚   โ”œโ”€โ”€ page.tsx         # Root page โ€“ manages query param extraction
โ”‚
โ”œโ”€โ”€ components/          # Reusable UI components
โ”‚   โ”œโ”€โ”€ AutoCompleteBox.tsx
โ”‚   โ”œโ”€โ”€ loading.tsx      # Suspense fallback
โ”‚   โ”œโ”€โ”€ recipeList.tsx   # Renders search results (Server Component)
โ”‚   โ””โ”€โ”€ searchBar.tsx    # Handles input, debounce, and query state  
โ”‚
โ”œโ”€โ”€ hooks/               # Custom React hooks
โ”‚   โ”œโ”€โ”€ useClickOutside.ts
โ”‚   โ””โ”€โ”€ useDebounce.ts
โ”‚
โ”œโ”€โ”€ lib/                 # Shared utilities
โ”‚   โ”œโ”€โ”€ db.ts            # Prisma client instance
โ”‚   โ””โ”€โ”€ embeddings.ts    # OpenAI vector generation
โ”‚
โ”œโ”€โ”€ prisma/
โ”‚   โ”œโ”€โ”€ schema.prisma    # MongoDB schema definition for Prisma
โ”‚   โ””โ”€โ”€ seed.ts          # Initial recipe seeding
โ”‚
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ generate-embeddings.ts  # Populate vector embeddings

๐Ÿ” Search Query Execution

1. Full-Text Search

db.recipe.findMany({
  where: {
    description: {
      contains: query,
      mode: "insensitive",
    },
  },
});

2. Fuzzy Full-Text (Atlas)

await db.$runCommandRaw({
  aggregate: "recipe",
  pipeline: [
    {
      $search: {
        index: "default",
        text: {
          query,
          path: ["title", "description"],
          fuzzy: { maxEdits: 2 },
        },
      },
    },
  ],
});

3. Vector Search (OpenAI)

const embedding = await getEmbedding(query);
await db.$runCommandRaw({
  aggregate: "recipe",
  pipeline: [
    {
      $vectorSearch: {
        index: "vector-index",
        path: "embeddings",
        queryVector: embedding,
        numCandidates: 100,
        limit: 10,
      },
    },
  ],
});

๐Ÿง  Vector Embeddings

This project uses OpenAI's Embedding API to convert recipe descriptions and user queries into vectors.

๐Ÿ”„ Workflow

  1. Seed recipes (pnpm seed)

  2. Generate embeddings (pnpm embed)

  • Each document gets a embeddings: number[] field
  • Stored alongside the title & description
  1. On search:
  • Query is sent to OpenAI to generate query vector
  • MongoDB runs $vectorSearch to retrieve the most relevant recipes

๐Ÿ” Search Types

Feature Tech Used Fallbacks
Full-text $search on MongoDB Prisma string filter (if needed)
Fuzzy Match $search + fuzzy Misspell-tolerant
Autocomplete $search: autocomplete Debounced via useDebounce
Vector Search $vectorSearch on embeddings Optional fallback to text search

๐Ÿงฐ Tools & Hooks

  • useDebounce.ts โ€” Prevents rapid-fire API calls from keystrokes
  • useClickOutside.ts โ€” Closes autocomplete dropdown outside of autocomplete box
  • db.ts โ€” Ensures Prisma client is singleton in dev
  • embeddings.ts โ€” Thin wrapper around OpenAI embedding endpoint

โš™๏ธ Prisma Schema (MongoDB)

model Recipe {
  id          String   @id @default(auto()) @map("_id") @db.ObjectId
  title       String 
  description String
  embeddings  Float[] // Store embeddings as an array of floats 
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@map("recipes")
}

๐Ÿ”„ API Route: /api/autocomplete

Handles GET requests like:

/api/autocomplete?q=chick

Uses $search: autocomplete stage to return recipe title suggestions based on partial input.

๐Ÿ”Œ External Services

OpenAI Embeddings

  • Model: text-embedding-3-small
  • Used for:
    • Embedding descriptions on ingestion (generate-embeddings.ts)
    • Embedding user queries at runtime (in recipeList.tsx)

MongoDB Atlas

  • Search Index (default): full-text and autocomplete
  • Vector Index: stored on embeddings field

๐Ÿ“Œ Edge Cases Handled

  • Input clearing updates URL and resets result
  • Debounced autocomplete avoids request flooding
  • Suspense fallback handles real-world loading times
  • key prop forces re-trigger of Server Components on query change

โœ… Benefits of URL-based State

Benefit Description
Shareable Users can copy/paste exact search queries
Bookmarkable Come back to same state instantly
Consistent SSR Works naturally with server-side rendering
SEO-friendly Each query is its own routeable resource

๐Ÿ“Ž Deployment Notes

  • MongoDB Atlas connection string must be secured in .env
  • OpenAI API keys must be rate-limited and protected server-side
  • Production deployments (e.g., Vercel) must enable necessary environment variables