SemiLayerDocs

Example: Recommendations

Build a "You might also like" recommendation engine using SemiLayer's similar facet. This example works for products, articles, movies, or any content with text fields.


Schema

// sl.config.ts
import { defineConfig } from '@semilayer/core'

export default defineConfig({
  stack: 'my-app',
  sources: {
    'main-db': { bridge: '@semilayer/bridge-postgres' },
  },
  lenses: {
    articles: {
      source: 'main-db',
      table: 'public.articles',
      primaryKey: 'id',
      fields: {
        id:          { type: 'number', primaryKey: true },
        title:       { type: 'text', searchable: { weight: 2 } },
        body:        { type: 'text', searchable: true },
        authorId:    { type: 'number', from: 'author_id' },
        tags:        { type: 'json' },
        publishedAt: { type: 'date', from: 'published_at' },
        slug:        { type: 'text' },
      },
      facets: {
        search:  { fields: ['title', 'body'] },
        similar: { fields: ['title', 'body'] },
      },
      syncInterval: '15m',
      rules: {
        search:  { allowPublicKey: true },
        similar: { allowPublicKey: true },
      },
    },
  },
})
semilayer push --resume-ingest
semilayer generate

Recommendations API Route

// app/api/articles/[id]/recommendations/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: Promise<{ id: string }> },
) {
  const { id } = await params

  const { results } = await beam.articles.similar({
    id,
    limit: 6,
  })

  return Response.json({
    recommendations: results
      .filter(r => String(r.metadata.id) !== id)  // exclude the source article
      .slice(0, 5)
      .map(r => ({
        id: r.metadata.id,
        title: r.metadata.title,
        slug: r.metadata.slug,
        publishedAt: r.metadata.publishedAt,
        score: r.score,
      })),
  })
}

Recommendations Component

// components/ArticleRecommendations.tsx
import Link from 'next/link'

interface Recommendation {
  id: number
  title: string
  slug: string
  publishedAt: string
  score: number
}

async function getRecommendations(articleId: string): Promise<Recommendation[]> {
  const res = await fetch(`/api/articles/${articleId}/recommendations`, {
    next: { revalidate: 300 },  // cache for 5 minutes
  })
  const data = await res.json()
  return data.recommendations
}

export async function ArticleRecommendations({ articleId }: { articleId: string }) {
  const recommendations = await getRecommendations(articleId)

  if (recommendations.length === 0) return null

  return (
    <aside>
      <h2>You might also like</h2>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {recommendations.map(rec => (
          <li key={rec.id} style={{ marginBottom: 16 }}>
            <Link href={`/articles/${rec.slug}`}>
              <strong>{rec.title}</strong>
            </Link>
            <div style={{ fontSize: 12, color: '#9ca3af', marginTop: 2 }}>
              {new Date(rec.publishedAt).toLocaleDateString()}
            </div>
          </li>
        ))}
      </ul>
    </aside>
  )
}

Combined: Search + Recommendations Page

// app/articles/[slug]/page.tsx
import { ArticleRecommendations } from '@/components/ArticleRecommendations'

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const article = await getArticleBySlug(slug) // your existing data fetch

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 48 }}>
      <article>
        <h1>{article.title}</h1>
        <div>{article.body}</div>
      </article>

      <ArticleRecommendations articleId={String(article.id)} />
    </div>
  )
}

Seeding Similarity at Ingest Time

SemiLayer computes embeddings for the fields declared in facets.similar.fields during ingest. No additional configuration is needed — if you declare the fields and run ingest, similar queries will work immediately.

The score returned by similar is cosine similarity (0–1). A score above 0.8 is typically a strong semantic match.