How to Build a RAG Pipeline with YouTube Transcripts
A working RAG pipeline over YouTube video transcripts: collect transcripts across a video list, chunk them, embed and store the vectors, retrieve the relevant chunks, and generate a cited answer - full code for every step, plus how to keep the index fresh.
00:09:00 · SEP 27, 2026 · UPDATED OCT 09, 2026
Quick answer
Collect the transcripts
Get a key from the dashboard (100 free credits, no card required) and fetch each video's transcript by id. Credits are charged per request, not per minute of video, so a 3-hour lecture costs the same 1 credit as a 2-minute clip - which matters once you're ingesting a whole channel's back catalog:
async function fetchTranscript(videoId: string) {
const res = await fetch(
`https://getyoutubetranscript.com/api/v1/transcript?v=${videoId}`,
{ headers: { Authorization: `Bearer ${process.env.GETYOUTUBETRANSCRIPT_API_KEY}` } }
);
const json = await res.json();
if (!json.success) throw new Error(json.message);
return json.data as { video_id: string; title: string; transcript: string; word_count: number };
}
const videoIds = ['dQw4w9WgXcQ', 'jNQXAC9IVRw' /* ... */];
const videos = await Promise.all(videoIds.map(fetchTranscript));video_id and title come back alongside the transcript text - that's what step 5 cites the answer back to.
Chunk the transcripts
Split each transcript into overlapping windows before embedding it - a whole-video embedding is too coarse to retrieve a specific answer from, and no library is required for this:
function chunkText(text: string, size = 1200, overlap = 200) {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + size, text.length);
chunks.push(text.slice(start, end));
start += size - overlap;
}
return chunks;
}
const records = videos.flatMap((video) =>
chunkText(video.transcript).map((text, chunkIndex) => ({
videoId: video.video_id,
title: video.title,
chunkIndex,
text,
}))
);A 1,200-character window with 200 characters of overlap is a reasonable default for spoken-word transcripts - shrink it for short, dense clips, grow it for long lecture-style content where more surrounding context helps.
Embed and store
Embed every chunk and keep the video id and title alongside the vector, since that's what turns a retrieved chunk into a citation later:
import OpenAI from 'openai';
const openai = new OpenAI();
async function embed(text: string) {
const res = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text });
return res.data[0].embedding;
}
const index = await Promise.all(
records.map(async (r) => ({ ...r, embedding: await embed(r.text) }))
);This keeps vectors in memory for clarity. Past a few hundred chunks, swap the array for pgvector, Pinecone, or another vector store - the retrieval step below only needs a function that returns the top-k nearest vectors, not this exact shape.
Retrieve the relevant chunks
At query time, embed the question with the same model and rank stored chunks by cosine similarity:
function cosineSimilarity(a: number[], b: number[]) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function retrieve(query: string, k = 5) {
const queryEmbedding = await embed(query);
return index
.map((r) => ({ ...r, score: cosineSimilarity(r.embedding, queryEmbedding) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}k=5 is a starting point - too few chunks and the model doesn't have enough context to answer fully, too many and irrelevant chunks start diluting the answer.
Generate a grounded answer
Pass the retrieved chunks to the model as numbered context and instruct it to answer only from what's there and to cite its sources - this is what keeps the answer traceable back to a real video instead of the model's own general knowledge:
async function answer(query: string) {
const matches = await retrieve(query);
const context = matches
.map((m, i) => `[${i + 1}] From "${m.title}":\n${m.text}`)
.join('\n\n');
const completion = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',
content:
"Answer only using the numbered transcript excerpts below. Cite sources as [1], [2], etc. " +
"If the excerpts don't contain the answer, say so.",
},
{ role: 'user', content: `${context}\n\nQuestion: ${query}` },
],
});
return completion.choices[0].message.content;
}Citing by video title, rather than inventing a timestamp, matches what the transcript API actually returns - don't let the model fabricate a timestamp it was never given.
Keep the index fresh
For a channel that keeps publishing, check for new uploads on a schedule with the /channel/latest endpoint (1 credit per check), skip video ids you've already indexed, and only fetch, chunk, and embed the new ones. Ingestion cost then stays proportional to new content instead of a full re-index every run.
Why transcripts work well for RAG
Video is one of the more awkward formats to build retrieval over - you can't grep a video. A transcript turns it back into text your existing embedding and retrieval stack already knows how to handle, without running your own speech-to-text job. And because a transcript-per-request API doesn't meter by video length, ingesting a backlog of long-form content (podcasts, lectures, multi-hour streams) doesn't get systematically more expensive than ingesting short clips - which is exactly the kind of archive a RAG pipeline is usually built to search.
Full endpoint reference, authentication, and rate limits are in the API docs. Get a key with 100 free credits from the dashboard to try this end to end.
RAG pipeline FAQs
Do I need a real vector database to follow this?
No - the in-memory array in this guide works fine up to a few hundred chunks and is the fastest way to verify the pipeline end to end. Move to pgvector or a managed vector store once you're indexing more than that, or need the index to persist across restarts.
Which embedding model should I use?
text-embedding-3-small is a reasonable default - cheap and good enough for most transcript retrieval. Switch to text-embedding-3-large only if you're seeing retrieval quality problems it would plausibly fix; it costs more per call.
What happens if a video has no transcript at all?
The transcript endpoint returns a clear error code (TRANSCRIPT_DISABLED or TRANSCRIPT_NOT_FOUND) instead of an empty string - check for success: false and skip that video rather than embedding an empty string as if it were real content.
How much does it cost to index, say, 200 videos?
200 credits for the transcript fetches, regardless of whether they're 2-minute clips or 2-hour videos - the 100 free signup credits cover roughly half a 200-video backlog on their own. Embedding cost is separate and billed by your embedding provider.
Can I cite an exact timestamp instead of just the video title?
Not with this endpoint - the public transcript API returns plain text, not timestamped segments, so the examples here cite by video title and id. If timestamp-level citation matters for your use case, keep chunks smaller so the video-level citation stays as precise as possible.
Related
- YouTube Transcript Errors Explained: IpBlocked, RequestBlocked, TranscriptsDisabled and the Rest
- We Read 191 Starter Story Founder Interviews. Here Is What Actually Got Them Customers
- How to Get the Transcript of a YouTube Video (Computer, Phone, and Download)
- n8n YouTube Transcript Failing on Your Server? Why YouTube Blocks It and How to Fix It
- YouTube API Quota Exceeded: Causes and Fixes
- YouTube Transcript API Rate Limit: What It Is and How to Handle 429s
- YouTube Transcript MCP Server: Setup Guide for Claude and Other AI Tools
- YouTube Transcripts in n8n: HTTP Request Workflow Guide
- Migrating from Supadata: A Field Guide
- How to Get YouTube Transcripts in Python
- Migrating from TranscriptAPI.com: A Field Guide
- Build an AI YouTube Video Summarizer with LangChain and Next.js
- The Best YouTube Transcript APIs in 2026
- GetYouTubeTranscript vs TranscriptAPI
- GetYouTubeTranscript vs youtubetotranscript.com
- GetYouTubeTranscript vs youtube-transcript.io
- GetYouTubeTranscript vs NoteGPT
- GetYouTubeTranscript vs Tactiq
- GetYouTubeTranscript vs Glasp
- GetYouTubeTranscript vs Eightify
- GetYouTubeTranscript vs Supadata
- GetYouTubeTranscript vs DownSub
- GetYouTubeTranscript vs youtubetranscript.dev