Example: Product Search
A full semantic product search API built with SemiLayer and Next.js — from schema definition through to a working search endpoint.
Schema
// sl.config.ts
import { defineConfig } from '@semilayer/core'
export default defineConfig({
stack: 'my-store',
sources: {
'main-db': { bridge: '@semilayer/bridge-postgres' },
},
lenses: {
products: {
source: 'main-db',
table: 'public.products',
primaryKey: 'id',
fields: {
id: { type: 'number', primaryKey: true },
name: { type: 'text', searchable: { weight: 3 } },
description: { type: 'text', searchable: true },
category: { type: 'text' },
price: { type: 'number' },
inStock: { type: 'boolean', from: 'in_stock' },
imageUrl: { type: 'text', from: 'image_url' },
},
facets: {
search: { fields: ['name', 'description'] },
similar: { fields: ['name', 'description'] },
},
rules: {
search: { allowPublicKey: true },
similar: { allowPublicKey: true },
query: { allowPublicKey: false },
},
},
},
})
Setup
semilayer init
semilayer sources connect # connect your Postgres source
semilayer push --resume-ingest
semilayer generate
Search API Route
// app/api/search/route.ts
import { createBeam } from '@/generated/semilayer'
const beam = createBeam({
baseUrl: process.env.SEMILAYER_BASE_URL!,
apiKey: process.env.SEMILAYER_API_KEY!,
})
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const q = searchParams.get('q') ?? ''
const category = searchParams.get('category') ?? undefined
const limit = Number(searchParams.get('limit') ?? '20')
const { results } = await beam.products.search({
query: q,
limit,
mode: 'hybrid',
})
// Optional: filter by category client-side (or use query facet for server-side)
const filtered = category
? results.filter(r => r.metadata.category === category)
: results
return Response.json({
query: q,
total: filtered.length,
results: filtered.map(r => ({
id: r.metadata.id,
name: r.metadata.name,
description: r.metadata.description,
price: r.metadata.price,
category: r.metadata.category,
imageUrl: r.metadata.imageUrl,
score: r.score,
})),
})
}
Search UI Component
// components/ProductSearch.tsx
'use client'
import { useState, useEffect } from 'react'
interface Product {
id: number
name: string
description: string
price: number
category: string
imageUrl: string
score: number
}
export function ProductSearch() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Product[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) { setResults([]); return }
const controller = new AbortController()
setLoading(true)
fetch(`/api/search?q=${encodeURIComponent(query)}&limit=20`, {
signal: controller.signal,
})
.then(r => r.json())
.then(data => { setResults(data.results); setLoading(false) })
.catch(() => {})
return () => controller.abort()
}, [query])
return (
<div>
<input
type="search"
placeholder="Search products..."
value={query}
onChange={e => setQuery(e.target.value)}
style={{ width: '100%', padding: '12px 16px', fontSize: 16 }}
/>
{loading && <p>Searching...</p>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginTop: 16 }}>
{results.map(product => (
<div key={product.id} style={{ border: '1px solid #eee', borderRadius: 8, padding: 16 }}>
{product.imageUrl && <img src={product.imageUrl} alt={product.name} style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: 4 }} />}
<h3 style={{ margin: '8px 0 4px' }}>{product.name}</h3>
<p style={{ color: '#666', fontSize: 14, margin: '0 0 8px' }}>{product.description}</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<strong>${product.price}</strong>
<span style={{ fontSize: 12, color: '#999' }}>Score: {product.score.toFixed(2)}</span>
</div>
</div>
))}
</div>
</div>
)
}
Similar Products
Add a "You might also like" section using the similar facet:
// app/api/products/[id]/similar/route.ts
import { createBeam } from '@/generated/semilayer'
const beam = createBeam({
baseUrl: process.env.SEMILAYER_BASE_URL!,
apiKey: process.env.SEMILAYER_API_KEY!,
})
export async function GET(
_req: Request,
{ params }: { params: { id: string } },
) {
const { results } = await beam.products.similar({
id: params.id,
limit: 4,
})
return Response.json(results.map(r => ({
id: r.metadata.id,
name: r.metadata.name,
price: r.metadata.price,
imageUrl: r.metadata.imageUrl,
score: r.score,
})))
}