Documentation
RAG — Retrieval-Augmented Generation
createRag composes AiPort with VectorPort into a two-method RAG pipeline: index(docs) embeds and stores documents, and search(query, topK?) embeds the query and returns the most semantically similar chunks. Swap AI providers and vector stores without touching the pipeline.
API#
| Name | Type | Description |
|---|---|---|
| ai | AiPort | The AI port used for embedding (required). Use mockAi in tests, openAiAdapter / workersAiAdapter in production. |
| vectors | VectorPort? | Vector store for upsert and query. Defaults to inMemoryVector(). |
| id | IdPort? | ID generator for documents without an explicit id. Defaults to sequentialId("doc"). |
createRag(options) returns an object with two methods:
| Name | Type | Description |
|---|---|---|
| index(docs) | (docs: { id?: string; text: string; metadata?: Record<string, unknown> }[]) → Promise<string[]> | Embeds each document and upserts its vector. Returns the list of document IDs (generated if not provided). |
| search(query, topK?) | (query: string, topK?: number) → Promise<VectorMatch[]> | Embeds the query string and returns the topK most similar documents sorted by descending cosine similarity. Defaults to topK = 10. |
Quick start with in-memory fakes#
The fastest way to build a RAG pipeline is with mockAi and the default inMemoryVector. No API keys, no infrastructure — ideal for wiring up the route before committing to a provider.
import { createRag, mockAi } from '@kynetra/fx' // mockAi returns zero vectors — fine for wiring up the API shapeconst rag = createRag({ ai: mockAi({ dim: 3 }) }) // Index some documentsconst ids = await rag.index([ { text: 'Ports decouple business logic from infrastructure.' }, { text: 'Adapters implement ports for a specific backend.' }, { text: 'In-memory fakes enable deterministic unit tests.' },])console.log(ids) // ['doc-1', 'doc-2', 'doc-3'] // Searchconst results = await rag.search('how do I test without a database?', 2)console.log(results[0].id) // the most relevant doc IDconsole.log(results[0].score) // cosine similarityFull example — route with real embeddings#
Below is a complete route that lets clients ask questions against a knowledge base. On startup it indexes a set of documentation chunks; on each request it retrieves the top-3 relevant chunks and passes them to the LLM as context.
import { createFX } from '@kynetra/fx'import { openAiAdapter } from '@kynetra/fx-ai'import { createRag } from '@kynetra/fx-ai' interface Answer { question: string; answer: string; sources: string[] } const app = createFX() const ai = openAiAdapter({ apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o-mini' })const rag = createRag({ ai }) // Index your knowledge base at startup (or on a schedule)app.hook('onBoot', async () => { await rag.index([ { id: 'ports', text: 'Ports decouple logic from infrastructure. See the ports page.', metadata: { url: '/docs/ports' } }, { id: 'kv', text: 'KvPort is a string key/value store with optional TTL expiry.', metadata: { url: '/docs/kv-cache' } }, { id: 'queues', text: 'QueuePort models a FIFO queue with enqueue and drain methods.', metadata: { url: '/docs/queues' } }, { id: 'ai-port', text: 'AiPort has two methods: complete for chat and embed for vectors.', metadata: { url: '/docs/ai' } }, ])}) app.get('/ask', async (ctx) => { const question = ctx.query('q') ?? '' if (!question) return ctx.status(400).json({ error: 'missing q' }) // 1. Retrieve relevant context const hits = await rag.search(question, 3) const context = hits.map(h => h.metadata?.text ?? '').join('\n\n') // 2. Generate an answer grounded in the retrieved context const { text: answer } = await ai.complete({ messages: [ { role: 'system', content: `You are a helpful documentation assistant. Answer using only the context below.\n\nContext:\n${context}`, }, { role: 'user', content: question }, ], }) const sources = hits.map(h => (h.metadata?.url as string) ?? '') return ctx.json({ question, answer, sources } satisfies Answer)})Testing RAG pipelines#
Use mockAi with a fixed dim and the default inMemoryVector. Because mockAi returns zero vectors, all stored documents will have equal cosine similarity — that is fine for testing route plumbing. For testing ranking logic specifically, inject hand-crafted vectors via inMemoryVector directly.
import { describe, it, expect } from 'vitest'import { mockAi, createRag, inMemoryVector } from '@kynetra/fx'import app from './kb' describe('knowledge base route', () => { it('returns an answer and sources', async () => { // Override the RAG instance with fakes const vectors = inMemoryVector() const ai = mockAi({ reply: 'Use the ports page.', dim: 3 }) const rag = createRag({ ai, vectors }) await rag.index([{ id: 'ports', text: 'Ports page.', metadata: { url: '/docs/ports' } }]) app.decorateContext('rag', rag) app.decorateContext('ai', ai) const res = await app.fetch(new Request('http://x/ask?q=how+do+ports+work')) expect(res.status).toBe(200) const body = await res.json() as { answer: string; sources: string[] } expect(body.answer).toBe('Use the ports page.') expect(body.sources).toContain('/docs/ports') }) it('returns 400 when q is missing', async () => { const res = await app.fetch(new Request('http://x/ask')) expect(res.status).toBe(400) })})Providing document IDs#
If you do not pass an id field, createRag generates one using the injected IdPort (default: sequentialId("doc")). Pass your own IDs to make vectors deterministic and idempotent — re-indexing the same document replaces its vector rather than duplicating it.
import { createRag } from '@kynetra/fx'import { openAiAdapter } from '@kynetra/fx-ai' const rag = createRag({ ai: openAiAdapter({ apiKey: '...' }) }) // Stable IDs mean re-indexing is idempotentawait rag.index([ { id: 'faq-billing', text: 'Billing happens on the 1st of each month.' }, { id: 'faq-refunds', text: 'Refunds are issued within 5 business days.' }, { id: 'faq-cancels', text: 'You can cancel any time from account settings.' },])Swapping in production adapters#
Choose an embedding provider
openAiAdapter (text-embedding-3-small) or workersAiAdapter (@cf/baai/bge-small-en-v1.5) for embedding. See AI providers.Choose a vector store
VectorPort adapter for Cloudflare Vectorize, pgvector, or Pinecone and pass it as vectors. The inMemoryVector remains the default for tests.Inject at the composition root
import { cloudflare } from '@kynetra/fx-cloudflare'import { workersAiAdapter, createRag } from '@kynetra/fx-ai'import app from './app' interface Env { AI: Ai VECTORIZE: VectorizeIndex} // Minimal VectorPort wrapper around Cloudflare Vectorizefunction vectorizePort(index: VectorizeIndex) { return { async upsert(records: { id: string; vector: number[]; metadata?: unknown }[]) { await index.upsert(records.map(r => ({ id: r.id, values: r.vector, metadata: r.metadata }))) }, async query(vector: number[], topK = 10) { const res = await index.query(vector, { topK, returnMetadata: true }) return res.matches.map(m => ({ id: m.id, score: m.score, metadata: m.metadata })) }, async delete(ids: string[]) { await index.deleteByIds(ids) }, }} return { fetch(request: Request, env: Env, ctx: ExecutionContext) { const ai = workersAiAdapter({ binding: env.AI }) const vectors = vectorizePort(env.VECTORIZE) app.decorateContext('rag', createRag({ ai, vectors })) return cloudflare(app).fetch(request, env, ctx) }}Tip
VectorPort. Because your pipeline only depends on the port interface, the adapter can be swapped with zero changes to application code.How indexing works internally#
index(docs) calls ai.embed({ input: docs.map(d => d.text) }) in a single batch request, then calls vectors.upsert([...]) with the returned vectors zipped to the document IDs and metadata. Batching minimises round-trips to the embedding endpoint.
search(query, topK) calls ai.embed({ input: query }), takes vectors[0] from the result, and calls vectors.query(vector, topK). The returned VectorMatch[] contains the stored metadata — typically the original text, URL, or any other fields you indexed.
See also#
- AiPort & mockAi — the AI port interface
- AI providers — openAiAdapter, anthropicAdapter, workersAiAdapter
- VectorPort — the vector store interface and inMemoryVector
- Ports & adapters — the overall design philosophy