SemiLayerDocs

Querying

SemiLayer exposes three distinct operations. Understanding which to use and when is the key to building fast, accurate, and secure data features.

OperationWhat it doesSource DB hit?
SearchSemantic / keyword / hybrid query against vector indexNo
SimilarFind records nearest to a given record's stored vectorNo
QueryDirect query through your Bridge — filtered rowsYes

Search embeds the query text and finds the nearest vectors. Your source database is never hit — all results come from the vector index built during ingest.

Enable it with facets: { search: { fields: [...] } } in your Lens config.

Search modes

ModeHow it works
semantic (default)Embeds query → cosine similarity against stored vectors
keywordPostgreSQL full-text search (tsvector / tsquery)
hybridReciprocal Rank Fusion (RRF) merge of semantic + keyword

Request shape

{
  query: string,       // required
  limit?: number,      // default: 10
  minScore?: number,   // default: 0 (0-1, prune low-relevance results)
  mode?: 'semantic' | 'keyword' | 'hybrid',  // default: lens config, then 'semantic'
}

Response shape

{
  results: Array<{
    id: string,          // internal SemiLayer embedding ID
    sourceRowId: string, // your original record's primary key (as string)
    content: string | null,  // concatenated embedded text (for inspection)
    metadata: M,         // all fields declared in your Lens
    score: number,       // 0-1 cosine similarity (semantic/hybrid) or BM25 rank (keyword)
  }>,
  meta: {
    lens: string,
    query: string,
    mode: string,
    count: number,
    durationMs: number,
  }
}

Examples

import { createBeam } from './semilayer'

const beam = createBeam({
  baseUrl: 'https://api.semilayer.com',
  apiKey: process.env.SEMILAYER_API_KEY!,
})

// Semantic search (default)
const { results } = await beam.products.search({
  query: 'eco-friendly water bottle',
  limit: 10,
})
// results[0].metadata.name   ← string
// results[0].metadata.price  ← number
// results[0].score           ← number (0-1)

// Hybrid mode
const { results: hybrid } = await beam.products.search({
  query: 'running shoes',
  mode: 'hybrid',
  limit: 20,
  minScore: 0.6,
})

Stream results as they arrive instead of waiting for the full response. Useful for rendering results progressively in a UI.

for await (const result of beam.products.stream.search({
  query: 'eco-friendly water bottle',
  limit: 50,
})) {
  render(result) // SearchResult<ProductsMetadata> — arrives per-batch
}

Similar

Find records similar to a given record using its stored vector. Takes a source record's primary key (as a string), returns nearest neighbors.

Enable it with facets: { similar: { fields: [...] } } in your Lens config.

Request shape

{
  id: string,          // required — your source record's primary key (as string)
  limit?: number,      // default: 10
  minScore?: number,   // default: 0
}

Response shape

Same as Search: { results: SearchResult<M>[], meta: {...} }

Examples

// Find 5 products similar to product ID 42
const { results } = await beam.products.similar({ id: '42', limit: 5 })

// results[0].metadata.name  ← name of the similar product
// results[0].score          ← similarity score (0-1)
// results[0].sourceRowId    ← its source DB primary key

Query

Query reads directly from your source database through the Bridge. Results are real-time (not from the vector index) but the query incurs a live DB call.

⚠️

query is disabled by default. You must explicitly enable it in your Lens config by setting rules.query. This is a safety measure — enabling it opens a direct read path through your Bridge.

Enable query in your Lens config:

lenses: {
  products: {
    // ... fields, facets ...
    rules: {
      query: 'authenticated',  // or 'public', or a ClaimCheck, or a function
    },
  },
}

Request shape

{
  where?: Record<string, unknown>,     // equality filters: { category: 'footwear' }
  orderBy?: { field: string; dir?: 'asc' | 'desc' }
          | Array<{ field: string; dir?: 'asc' | 'desc' }>,
  limit?: number,        // default: 100
  offset?: number,
  cursor?: string,       // opaque pagination cursor from previous response
  select?: string[],     // field names to include in response (default: all)
}

Response shape

{
  rows: M[],             // direct M access — no .metadata wrapper
  meta: {
    lens: string,
    total?: number,      // only included if bridge supports count
    nextCursor?: string, // present when more pages exist
    count: number,
    durationMs: number,
  }
}

Examples

// Simple filter
const { rows } = await beam.products.query({
  where: { category: 'footwear' },
  limit: 20,
})
// rows[0].name   ← string (direct ProductsMetadata access)
// rows[0].price  ← number

// Sort + paginate
const page1 = await beam.products.query({
  orderBy: { field: 'price', dir: 'asc' },
  limit: 20,
})

const page2 = await beam.products.query({
  orderBy: { field: 'price', dir: 'asc' },
  limit: 20,
  cursor: page1.meta.nextCursor,
})

Streaming query

Stream large result sets row-by-row instead of waiting for the full response:

// Beam codegen
for await (const row of beam.products.stream.query({
  where: { category: 'footwear' },
  limit: 500,
})) {
  process(row) // ProductsMetadata — arrives per-batch
}

// BeamClient direct
for await (const row of client.stream.query<Product>('products', {
  where: { category: 'footwear' },
})) {
  process(row)
}

Live Tail (Subscribe)

Subscribe to all changes on a Lens — every insert, update, and delete fires an event in real time via WebSocket. No polling required.

// Beam codegen
for await (const event of beam.products.stream.subscribe()) {
  if (event.kind === 'insert') addRow(event.record)
  if (event.kind === 'update') updateRow(event.record)
  if (event.kind === 'delete') removeRow(event.record)
}

// BeamClient direct
for await (const event of client.stream.subscribe('products')) {
  console.log(event.kind, event.record)
}

With a filter (simple equality only in v0.1):

for await (const event of beam.products.stream.subscribe({
  filter: { category: 'footwear' }
})) {
  updateUI(event.record)
}

Observe One Record

React to changes on a single record by ID. The first yield is the current state; subsequent yields fire only when the record changes.

// Beam codegen
for await (const snapshot of beam.products.observe('42')) {
  setProduct(snapshot) // ProductsMetadata — fires on every change
}

// BeamClient direct
for await (const snapshot of client.observe<Product>('products', '42')) {
  setProduct(snapshot)
}

Choosing the Right Operation

You want to...Use
Find relevant records by natural languageSearch (semantic)
Find exact-match or keyword resultsSearch (keyword)
Blend semantic + keyword resultsSearch (hybrid)
Find records similar to a known itemSimilar
Filter or sort by known field valuesQuery
Get a live feed of all changesSubscribe
Watch one record for changesObserve