Example: Direct Queries
The query facet reads directly from your source with server-side filtering — useful
when you need exact matches, range filters, or paginated browsing without semantic ranking.
Schema
Enable the query facet in your lens:
// sl.config.ts
import { defineConfig } from '@semilayer/core'
export default defineConfig({
stack: 'my-app',
sources: {
'main-db': { bridge: '@semilayer/bridge-postgres' },
},
lenses: {
events: {
source: 'main-db',
table: 'public.events',
primaryKey: 'id',
fields: {
id: { type: 'number', primaryKey: true },
title: { type: 'text', searchable: true },
category: { type: 'text' },
startsAt: { type: 'date', from: 'starts_at' },
endsAt: { type: 'date', from: 'ends_at' },
cityId: { type: 'number', from: 'city_id' },
capacity: { type: 'number' },
price: { type: 'number' },
},
facets: {
search: { fields: ['title'] },
query: {}, // enable direct queries — no extra config needed
},
rules: {
search: { allowPublicKey: true },
query: { allowPublicKey: false }, // backend only
},
},
},
})
semilayer push --resume-ingest
semilayer generate
Querying with Filters
import { createBeam } from '@/generated/semilayer'
const beam = createBeam({
baseUrl: process.env.SEMILAYER_BASE_URL!,
apiKey: process.env.SEMILAYER_API_KEY!,
})
// Simple equality filter
const { rows } = await beam.events.query({
where: { category: 'music', cityId: 5 },
limit: 20,
})
// rows[0].title ← string
// rows[0].startsAt ← Date
// rows[0].price ← number
API Route with Filters
// app/api/events/route.ts
import { createBeam } from '@/generated/semilayer'
import { NextRequest } from 'next/server'
const beam = createBeam({
baseUrl: process.env.SEMILAYER_BASE_URL!,
apiKey: process.env.SEMILAYER_API_KEY!,
})
export async function GET(req: NextRequest) {
const params = req.nextUrl.searchParams
const where: Record<string, unknown> = {}
if (params.get('category')) where.category = params.get('category')
if (params.get('cityId')) where.cityId = Number(params.get('cityId'))
const page = Number(params.get('page') ?? '1')
const limit = Number(params.get('limit') ?? '20')
const offset = (page - 1) * limit
const { rows, total } = await beam.events.query({
where,
orderBy: { field: 'startsAt', dir: 'asc' },
limit,
offset,
})
return Response.json({
events: rows,
pagination: {
page,
limit,
total: total ?? rows.length,
pages: total ? Math.ceil(total / limit) : undefined,
},
})
}
Hybrid: Search + Query in One Handler
Combine semantic search with a filtered query to let users switch modes:
// app/api/events/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')
if (q.trim()) {
// Semantic search — query drives results, category filters client-side
const { results } = await beam.events.search({ query: q, limit: limit * 2, mode: 'hybrid' })
const filtered = category ? results.filter(r => r.metadata.category === category) : results
return Response.json({ mode: 'search', results: filtered.slice(0, limit) })
} else {
// No query — fall back to direct browse with server-side filter
const { rows } = await beam.events.query({
where: category ? { category } : {},
orderBy: { field: 'startsAt', dir: 'asc' },
limit,
})
return Response.json({ mode: 'query', results: rows })
}
}
Curl
# Direct query via HTTP API
curl 'https://api.semilayer.com/v1/query/events' \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"where": { "category": "music", "cityId": 5 },
"orderBy": { "field": "startsAt", "dir": "asc" },
"limit": 20
}'