Semantic search
Answer a question from a knowledge base with citations and sources.
Use a task to run a retrieval-augmented generation pipeline. Runlane coordinates the background job and durable steps. Your application owns source documents, chunking, embeddings, the vector store, access control, and answer presentation.
Return an answer with citations
The Runlane task and durable steps below are real. The marked embedding, retrieval, and answer calls are pseudocode for your model and vector store:
import { task } from '@runlane/core'
import * as z from 'zod'
const embeddingSchema = z.object({ vector: z.array(z.number()) })
const sourceSchema = z.object({ id: z.string(), score: z.number(), text: z.string() })
const answerSchema = z.object({
answer: z.string(),
citations: z.array(z.object({ quote: z.string(), sourceId: z.string() })),
confidence: z.number().min(0).max(1),
})
const searchResultSchema = answerSchema.extend({
sources: z.array(sourceSchema.pick({ id: true, score: true })),
})
export const askKnowledgeBase = task({
id: 'search.ask-knowledge-base',
schema: z.object({ question: z.string().min(1) }),
output: searchResultSchema,
async run({ question }, context) {
const embedding = await context.step.run('embed-question', { output: embeddingSchema }, async () => {
//////////////////////////////////
// MAKE YOUR API CALLS HERE
//////////////////////////////////
const result = await createEmbedding({ question, signal: context.signal })
return embeddingSchema.parse(result)
})
const sources = await context.step.run('search-knowledge-base', { output: z.array(sourceSchema) }, async () => {
//////////////////////////////////
// MAKE YOUR API CALLS HERE
//////////////////////////////////
const result = await searchKnowledgeBase({ limit: 10, signal: context.signal, vector: embedding.vector })
return z.array(sourceSchema).parse(result)
})
const answer = await context.step.run('answer-with-citations', { output: answerSchema }, async () => {
//////////////////////////////////
// MAKE YOUR API CALLS HERE
//////////////////////////////////
const result = await answerQuestion({ question, signal: context.signal, sources })
return answerSchema.parse(result)
})
return {
...answer,
sources: sources.map(({ id, score }) => ({ id, score })),
}
},
})The task validates the embedding, retrieved records, answer, citations, confidence, and final source list. Only source ids and scores are stored in the final output; large source text remains in the application-owned store.
Re-check the caller’s current permissions before displaying stored results. A run result must not become an authorization cache.
Separate online and background paths
Interactive search may need latency lower than a queued task provides. Use Runlane for indexing, batch enrichment, or searches whose callers can read the result later. Keep a direct request path for truly synchronous search.
Before production
- Put model and database quotas on bounded queues.
- Keep document text and vectors outside run payloads when they are large.
- Make index writes idempotent by document version.
- Verify that every citation refers to a returned source.
- Test provider timeouts, partial indexing, and stale source deletion.
See queues and concurrency and save step results.