Skip to main content

RAG (Retrieval-Augmented Generation)

Retrieve relevant pieces of your data at query time and put them in the prompt, so the model answers from your sources instead of memory.

Building blocks​

PieceWhat to know
EmbeddingsModel that maps text to a vector; similar meaning → nearby vectors. Use the same embedding model for indexing and querying
ChunkingSplit docs into passages (by heading/paragraph, with some overlap). Too small loses context, too big dilutes relevance
MetadataStore source, title, URL, date, ACL with each chunk for filtering and citations
Vector DBpgvector, Qdrant, Weaviate, Milvus, Pinecone, Chroma, or a search engine with vector support (Elasticsearch/OpenSearch)
SimilarityUsually cosine similarity or dot product with approximate nearest-neighbor (ANN) indexes such as HNSW
Hybrid searchCombine vector search with keyword search (BM25) - catches exact names, codes, IDs
RerankingA cross-encoder/reranker re-scores the top ~50 candidates; keep the best few
GenerationPrompt with chunks in tags + "answer only from the sources, cite them"

Minimal flow (TypeScript, pgvector)​

// index time
for (const chunk of chunkMarkdown(doc, { maxTokens: 500, overlap: 50 })) {
const vector = await embed(chunk.text); // any embedding API
await sql`INSERT INTO chunks (doc_id, text, embedding) VALUES (${doc.id}, ${chunk.text}, ${toPgVector(vector)})`;
}

// query time
const q = await embed(question);
const rows = await sql`
SELECT text, doc_id FROM chunks
ORDER BY embedding <=> ${toPgVector(q)} -- cosine distance
LIMIT 8`;

const context = rows.map((r, i) => `<source id="${i}" doc="${r.doc_id}">${r.text}</source>`).join("\n");
const answer = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 800,
system: "Answer only from the sources. Cite source ids. If unsure, say you don't know.",
messages: [{ role: "user", content: `${context}\n\nQuestion: ${question}` }],
});
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE chunks (id bigserial PRIMARY KEY, doc_id text, text text, embedding vector(1024));
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);

(The vector dimension must match your embedding model's output size.)

Quality checklist​

  • Evaluate retrieval separately from generation: is the right chunk in the top-k (recall@k)?
  • Contextual chunks: prepend the doc title/section to each chunk before embedding.
  • Query rewriting: let the model turn a chat turn into a standalone search query.
  • Filter by metadata/ACL before ranking - never leak documents the user cannot see.
  • Re-index on change: keep a content hash per chunk.
  • Long context is an alternative for small corpora: if everything fits (and caching makes it cheap), skip retrieval.

Gotchas​

  • Changing the embedding model means re-embedding everything.
  • Pure vector search misses exact identifiers; add keyword search.
  • Retrieved text is untrusted - it can contain prompt injection (see Security).