SemiLayerDocs

Search — Filters & mode

Vector search answers "closest to my query." Two knobs widen its reach: the mode parameter (semantic vs keyword vs hybrid) and — for predicate-based narrowing — the query() primitive that pairs naturally with search.

searchproducts
products: {
  source: 'main',
  table: 'products',
  fields: {
    id:          { type: 'number',  primaryKey: true },
    name:        { type: 'text',    searchable: { weight: 2 } },
    description: { type: 'text',    searchable: true },
    category:    { type: 'enum',    values: ['footwear', 'apparel', 'accessories'] },
    in_stock:    { type: 'boolean' },
  },
  grants: { search: 'public' },
}

The three modes

ModeWhen to useBehavior
semanticNatural-language queries, discovery, chat groundingPure vector similarity
keywordExact tokens matter: SKUs, slugs, brand namesFull-text scoring only
hybridMixed intent: "nike running shoes", "react hooks tutorial"Blends both scores

Mode is a per-call choice — pass mode: 'keyword' | 'hybrid' on the request, or reach for the sugar methods beam.<lens>.searchKeyword(...) and beam.<lens>.searchHybrid(...). Default is semantic.

Narrowing with a predicate pre-query

Need to restrict candidates to "footwear that's in stock" before ranking? Run a query() on the same lens to get matching IDs, then filter search results client-side.

import { beam } from './beam'

// Step 1: narrow via structured predicate
const { rows } = await beam.products.query({
  where: { category: 'footwear', in_stock: true },
  select: ['sourceRowId'],
  limit: 1000,
})

// Step 2: rank with hybrid mode
const { results } = await beam.products.search({
  query: 'running shoes',
  mode: 'hybrid',
  limit: 10,
})

// Step 3: intersect
const allowed = new Set(rows.map((r) => r.sourceRowId))
const filtered = results.filter((r) => allowed.has(r.sourceRowId))
ℹ️

A single-call where predicate inside search() is on the roadmap — for now combine query() + search(), or narrow via a joined lens using include: { relationName: { where: ... } }.

Filtering by recency — changedSince / changedBefore

A built-in time-window filter lives on search(), similar(), and every named feed. Pass an ISO-8601 timestamp and only rows whose changeTrackingColumn value (default updated_at) falls in the window are considered.

// Only rank against rows updated in the last 7 days
await beam.products.search({
  query: 'running shoes',
  changedSince: new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString(),
  limit: 10,
})

// Bounded range — useful for "what changed in Q1"
await beam.products.similar({
  id: 'p_5581',
  changedSince: '2026-01-01T00:00:00Z',
  changedBefore: '2026-04-01T00:00:00Z',
})

// Feeds pass the same shape
await beam.products.feed.latest({
  changedSince: '2026-04-20T00:00:00Z',
})

Both fields are independently optional. Omit them and nothing about your call changes — same SQL, same plan, same latency as before this feature existed. Use one or both as needed.

Where it works

SurfaceSupports filterNotes
beam.<lens>.search() / HTTP /v1/search/:lensAll three modes: semantic, keyword, hybrid
beam.<lens>.similar() / HTTP /v1/similar/:lens
beam.<lens>.feed.<name>() / HTTP /v1/feed/:lens/:nameApplies to the candidate pool; cursor/dedup unchanged
WebSocket feed.next, stream.searchSame field names; flows through params
beam.<lens>.query()Use a where: { updated_at: ... } predicate instead

query() doesn't ship this filter because it's raw structured access where you already have where to express anything you want. search / similar / feed need it because they don't expose a where escape hatch for semantic primitives.

CLI + Dev Console

Same filter, shorthand duration support (7d, 24h, 15m, 30s):

semilayer run search products "running shoes" --since 7d
semilayer run similar products p_5581 --since 2026-01-01 --before 2026-04-01
semilayer feed view products.latest --api-key sk_live_... --since 24h

The Console dev drawer accepts the same --since / --before flags.

Behavior notes

  • Tight windows still return a full limit — the ranker keeps walking until it fills the page, rather than stopping early and returning fewer results than asked for.
  • Omitting both bounds is a true no-op path: same query plan, same latency as before the filter existed.