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.
Copy
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 },
},
},
},
})
Copy
semilayer push --resume-ingest
semilayer generate
Copy
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)
.slice (0 , 5 )
.map (r => ({
id : r.metadata .id ,
title : r.metadata .title ,
slug : r.metadata .slug ,
publishedAt : r.metadata .publishedAt ,
score : r.score ,
})),
})
}
Copy
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 },
})
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 >
)
}
Copy
import { ArticleRecommendations } from '@/components/ArticleRecommendations'
export default async function ArticlePage ({
params,
}: {
params: Promise <{ slug: string }>
} ) {
const { slug } = await params
const article = await getArticleBySlug (slug)
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 >
)
}
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.