Pencarian Semantik dengan OpenAI Embeddings dan Upstash Vector di Next.js

January 23, 2026⏱️ 5 min read

Panduan lengkap untuk membangun semantic search di project Next.js App Router modern menggunakan model text-embedding-3-small dari OpenAI dan Upstash Vector.
Artikel ini memandu kamu dari proses generate vector embeddings sampai menampilkan hasil pencarian yang cerdas — semuanya tanpa backend tradisional.

Pencarian Semantik dengan OpenAI Embeddings dan Upstash Vector di Next.js

Kenapa Semantic Search?

Sistem pencarian tradisional bergantung pada pencocokan keyword yang persis sama. Tapi pengguna nyata sering mengetikkan kalimat yang berbeda dari konten yang mereka cari. Semantic search menyelesaikan hal ini dengan memahami makna — bukan sekadar mencocokkan kata.
Dengan mengubah konten dan query pengguna menjadi vektor, kamu bisa membandingkan kemiripannya menggunakan cosine distance di vector database seperti Upstash Vector. Hasilnya, pencarian jadi jauh lebih cerdas dan intuitif.

1. Setup: Next.js + Dependency yang Dibutuhkan

Pastikan project kamu menggunakan Next.js App Router dan install package yang dibutuhkan:

pnpm add openai @upstash/vector zod dotenv

Lalu, isi environment variables:

OPENAI_API_KEY=sk-...
UPSTASH_VECTOR_REST_URL=https://...
UPSTASH_VECTOR_REST_TOKEN=...

Variabel ini akan digunakan untuk indexing konten dan pencarian secara real-time.

2. Mengindeks Konten MDX ke Upstash Vector

Daripada hardcode konten, kamu bisa membaca artikel dan project secara dinamis dari file MDX dengan utility function seperti getAllArticles() dan getAllProjects() dari lib/articles.ts dan lib/projects.ts.
Lalu kontennya di-embed dan di-upsert ke Upstash Vector menggunakan script satu kali jalan.
Contoh: scripts/generate-embedding.ts

import { OpenAI } from 'openai'
import { Index } from '@upstash/vector'
import { getAllArticles } from '@/lib/articles'
import { getAllProjects } from '@/lib/projects'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! })
const index = Index.fromEnv()

async function run() {
  const items = [...getAllArticles(), ...getAllProjects()]

  for (const item of items) {
    const input = `${item.title} ${item.description ?? ''}`

    const res = await openai.embeddings.create({
      input,
      model: 'text-embedding-3-small',
    })

    const vector = res.data[0].embedding

    await index.upsert([
      {
        id: item.slug,
        vector,
        metadata: {
          title: item.title,
          slug: item.slug,
          type: item.category ? 'articles' : 'projects',
        },
      },
    ])
  }
}

run()

💡 Kamu hanya perlu menjalankan ini saat ada konten baru ditambahkan.

3. Pencarian Real-Time dengan /api/search/query

Saat user mengetik di search bar, kamu generate embedding dari query lalu mencari kecocokannya terhadap vektor di Upstash. Ini implementasi yang siap production:

// src/app/api/search/query/route.ts
import { OpenAI } from 'openai'
import { NextResponse } from 'next/server'
import { Index } from '@upstash/vector'
import { config } from 'dotenv'

config()

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! })
const index = Index.fromEnv()

export async function POST(req: Request) {
  const { query } = await req.json()

  if (!query) {
    return NextResponse.json({ error: 'Query is required' }, { status: 400 })
  }

  const embeddingResponse = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: query,
  })

  const embedding = embeddingResponse.data[0].embedding

  const search = await index.query({
    vector: embedding,
    topK: 5,
    includeMetadata: true,
  })

  const results = search.map((item) => {
    const { slug, type, title } = item.metadata as {
      slug: string
      type: 'articles' | 'projects'
      title: string
    }

    return {
      id: item.id,
      score: item.score,
      title,
      type,
      slug,
      const url = `/${type}/${slug}` // Adjust this to match your routing structure
    }
  })

  return NextResponse.json({ results })
}

Endpoint ini cepat, akurat, dan efisien dari sisi biaya. Kamu tidak perlu route tambahan seperti /api/search/embed.

4. Menghubungkan ke SearchDialog

Di SearchDialog.tsx, panggil endpoint /api/search/query saat user mengetik:

const response = await fetch('/api/search/query', {
  method: 'POST',
  body: JSON.stringify({ query }),
})
const { results } = await response.json()

Gunakan debounce untuk membatasi request:

useEffect(() => {
  const timeout = setTimeout(() => {
    if (query.length >= 2) {
      search()
    }
  }, 150)

  return () => clearTimeout(timeout)
}, [query])

Render hasil menggunakan <ArticleCard /> dan <ProjectCard /> yang sudah ada.

5. Contoh Komponen SearchDialog

Berikut contoh sederhana komponen SearchDialog yang:

  • Menangkap input user
  • Menerapkan debounce request
  • Mengirim query ke /api/search/query
  • Menampilkan daftar hasil semantic search
'use client'

import { useEffect, useState } from 'react'

export default function SearchDialog() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (query.length >= 2) {
        fetch('/api/search/query', {
          method: 'POST',
          body: JSON.stringify({ query }),
        })
          .then((res) => res.json())
          .then((data) => setResults(data.results))
      }
    }, 150)

    return () => clearTimeout(timeout)
  }, [query])

  return (
    <div className="space-y-4">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search articles or projects..."
        className="w-full rounded border px-4 py-2 text-sm"
      />

      <ul className="space-y-2">
        {results.map((r: any) => (
          <li key={r.id} className="text-sm">
            <a href={r.url} className="text-blue-600 hover:underline">
              {r.title}
            </a>
            <span className="ml-2 text-xs text-zinc-500">({r.type})</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

💡 Ini contoh minimal. Kamu bisa mengganti daftar hasil dengan card custom seperti <ArticleCard /> atau <ProjectCard /> agar sesuai desain situsmu.

6. Tips Tambahan

  • Tampilkan hanya hasil dengan score > 0.7 untuk relevansi.
  • Tambahkan loading state agar tidak flicker.
  • Upstash Vector bersifat serverless dan cepat — tidak perlu Elasticsearch.
  • Jaga penggunaan OpenAI tetap hemat dengan caching embeddings jika perlu.

Kesimpulan

Dengan menggabungkan embeddings OpenAI dan Upstash Vector, kamu bisa membangun pengalaman semantic search yang sepenuhnya serverless — terintegrasi sempurna dengan sistem konten berbasis MDX kamu.
Solusi ini bersih, hemat biaya, dan scalable untuk aplikasi Next.js dengan konten yang banyak.
Sekarang search bar kamu bukan hanya pintar — tapi juga memahami makna.