StudyVisuallyStart studying
AI agents

RAG in JavaScript, from embeddings to answers

By the StudyVisually teamPublished

RAG (retrieval-augmented generation) means searching your own documents first, then giving the best matches to a language model and telling it to answer only from them. In JavaScript it takes two jobs. Indexing runs once: split your documents into chunks, turn each chunk into an embedding, and store both in a database such as Postgres with pgvector. Querying runs for every question: embed the question, fetch the closest chunks, and send them to the model with the question. No model training is involved, and updating the answers means re-indexing a document.

Why not just ask the model?

A model only knows what was in its training data. It has never seen your company's runbooks, your course notes or last week's policy change. When it doesn't know, it often writes a confident guess anyway. RAG fixes both problems: the model gets your text, and an instruction to stay inside it (RAG lesson).

What is an embedding?

An embedding is a list of numbers that captures what a piece of text means. Texts with similar meanings get lists that point in similar directions, so "how do I roll back a deploy?" lands near "revert steps" even though they share no words.

TypeScript
const response = await client.embeddings.create({
model: "text-embedding-3-small",
input: "How do I revert a deploy?",
});
const vector = response.data[0].embedding; // 1536 numbers

OpenAI's guide says text-embedding-3-small returns 1,536 numbers by default and text-embedding-3-large returns 3,072 (embeddings guide). Store the vector next to the text it came from.

How do I compare two embeddings?

With cosine similarity: 1 means the same direction (same meaning), 0 means unrelated. OpenAI recommends cosine similarity for its embeddings and notes they are "normalized to length 1", so a plain dot product gives the same ranking (embeddings guide).

TypeScript
function cosineSimilarity(first, second) {
const dot = first.reduce((sum, value, index) => sum + value * second[index], 0);
return dot / (Math.hypot(...first) * Math.hypot(...second));
}

You rarely write this yourself. A vector database computes it for you, fast, over millions of rows (cosine similarity lesson).

Step 1: index your documents

  1. Split each document into chunks. The chunk size lesson suggests starting at 300 to 800 tokens with a little overlap, split on headings and paragraphs. Chunks that are too small lose context; chunks that are too big bury the one sentence you need.
  2. Embed each chunk.
  3. Store the chunk text, its vector, and where it came from (file, section, date).
TypeScript
for (const chunk of splitIntoChunks(documentText)) {
const response = await client.embeddings.create({ model: "text-embedding-3-small", input: chunk });
await database.query("INSERT INTO docs (content, embedding) VALUES ($1, $2)", [
chunk,
JSON.stringify(response.data[0].embedding),
]);
}

With pgvector, embedding is a vector(1536) column. Re-run this for a document when it changes.

Step 2: answer a question

Embed the question the same way, ask the database for the nearest chunks, and build the prompt.

TypeScript
const { rows } = await database.query("SELECT content FROM docs ORDER BY embedding <=> $1 LIMIT 4", [
JSON.stringify(questionVector),
]);
const context = rows.map((row) => row.content).join("\n---\n");
const answer = await client.chat.completions.create({
model,
messages: [
{ role: "system", content: "Answer only from the context. If the answer isn't there, say you don't know." },
{ role: "user", content: `Context:\n${context}\n\nQuestion: ${question}` },
],
});

In pgvector, <=> is cosine distance, so smaller means closer. LIMIT 4 is your top K: how many chunks the model reads. More chunks cost more and add noise.

The system message does a lot of work. "Answer only from the context, otherwise say you don't know" is a big part of how RAG cuts down made-up answers (grounding lesson).

My RAG gives wrong answers. What do I check?

Check retrieval before you blame the model. Print the chunks it was given for a bad answer:

SymptomLikely causeFix
The right chunk isn't in the resultsChunks too big or split mid-thoughtRe-chunk on headings; add overlap
Exact names or error codes are missedMeaning search is weak on exact stringsAdd keyword search alongside (hybrid search)
The right chunk is there but ranked lowFirst-pass ranking is roughFetch 20, re-rank, keep the best 4
The answer ignores the chunksWeak instructionsTighten the system message; ask it to quote

Then build a small eval set: twenty real questions with known answers. Score every change against it instead of trying one question by eye.

How does RAG fit into an agent?

Turn the query step into a tool, searchDocs(query). The agent then decides when to search and can search again with a better query if the first results miss. This is called agentic RAG, and it is where tool calling and RAG meet (agentic RAG lesson).

Practise it

The embeddings and RAG part has 14 cards, from embeddings to re-ranking. The Step by step mode in the app plays the whole RAG pipeline step by step, and the interview questions include "your RAG gives wrong answers, how do you debug it?", which is worth being able to answer in under a minute.

Read it? Now test yourself on it.

Sources

Try it with today's mission.

Open the app and spend 20 minutes on a free subject or your own notes. Start without signing up, no card needed.

Start studying

See you tomorrow?