Singleton Instance
src/lib/vectoriadb.ts
import { VectoriaDB, FileStorageAdapter, DocumentMetadata } from 'vectoriadb';
interface ArticleDocument extends DocumentMetadata {
title: string;
author: string;
publishedAt: string;
slug: string;
}
// Singleton pattern for Next.js
let articleIndex: VectoriaDB<ArticleDocument> | null = null;
export async function getArticleIndex() {
if (articleIndex) return articleIndex;
articleIndex = new VectoriaDB<ArticleDocument>({
storageAdapter: new FileStorageAdapter({
cacheDir: './.cache/vectoriadb',
namespace: 'articles',
}),
defaultSimilarityThreshold: 0.35,
});
await articleIndex.initialize();
return articleIndex;
}
API Route (App Router)
src/app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getArticleIndex } from '@/lib/vectoriadb';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
const limit = parseInt(searchParams.get('limit') || '10');
if (!query) {
return NextResponse.json(
{ error: 'Query parameter "q" is required' },
{ status: 400 }
);
}
try {
const index = await getArticleIndex();
const results = await index.search(query, { topK: limit });
return NextResponse.json({
query,
results: results.map((r) => ({
id: r.id,
score: r.score,
title: r.metadata.title,
author: r.metadata.author,
slug: r.metadata.slug,
})),
});
} catch (error) {
console.error('Search error:', error);
return NextResponse.json(
{ error: 'Search failed' },
{ status: 500 }
);
}
}
Search Component
src/components/Search.tsx
'use client';
import { useState } from 'react';
interface SearchResult {
id: string;
score: number;
title: string;
author: string;
slug: string;
}
export function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
setResults(data.results || []);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
}
return (
<div>
<form onSubmit={handleSearch}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search articles..."
/>
<button type="submit" disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
</form>
<ul>
{results.map((result) => (
<li key={result.id}>
<a href={`/articles/${result.slug}`}>
{result.title}
</a>
<span>by {result.author}</span>
<span>Score: {result.score.toFixed(2)}</span>
</li>
))}
</ul>
</div>
);
}
Server Component Search
src/app/search/page.tsx
import { getArticleIndex } from '@/lib/vectoriadb';
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const query = searchParams.q;
if (!query) {
return <div>Enter a search query</div>;
}
const index = await getArticleIndex();
const results = await index.search(query, { topK: 10 });
return (
<div>
<h1>Results for "{query}"</h1>
<ul>
{results.map((result) => (
<li key={result.id}>
<a href={`/articles/${result.metadata.slug}`}>
{result.metadata.title}
</a>
</li>
))}
</ul>
</div>
);
}
Edge Runtime Compatibility
VectoriaDB requires Node.js runtime for transformers.js. Use
runtime: 'nodejs' in API routes.src/app/api/search/route.ts
export const runtime = 'nodejs';
Related
Express
Express integration
FrontMCP
FrontMCP integration
Deployment
Production deployment