SemiLayerDocs

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,
  })))
}