All articles
13 min read

Turn podcast episodes into documents for RAG

Show notes describe an episode; they do not expose what was said in it. This is the full ingestion path from an RSS feed to searchable, timestamped chunks — feed sync, chunking rules, PostgreSQL schema and citation formatting.

Podcasts contain long-form knowledge, interviews and expert discussions that are difficult to search.

Titles and show notes describe an episode, but they do not expose everything said inside it. A RAG system needs the complete spoken content as structured text.

Techtuel converts podcast audio URLs, episode pages and supported RSS sources into timestamped transcripts through one podcast transcript API.

You can then:

  • split each episode into searchable chunks;
  • generate embeddings;
  • store them in a vector database;
  • search an entire podcast catalogue;
  • answer questions with links to the relevant moment;
  • summarize or compare multiple episodes.
Podcast RSS or episode URL

Techtuel

Transcript with timestamps

Chunks and metadata

Embeddings and search index

RAG answers with episode citations

Why podcasts are useful RAG sources

A podcast catalogue can act as a specialized knowledge base.

It may contain:

  • interviews with domain experts;
  • internal company discussions;
  • technical explanations;
  • historical archives;
  • industry analysis;
  • research conversations;
  • customer calls;
  • recorded conferences.

The difficulty is not generating an embedding. The difficulty is turning every episode into clean, consistently structured documents.

A useful podcast ingestion process should retain:

  • podcast name;
  • episode title;
  • episode URL;
  • publication date;
  • audio URL;
  • language;
  • duration;
  • transcript;
  • segment start times;
  • external identifiers such as the RSS GUID.

Transcribe a podcast episode

Submit the direct episode or audio URL to POST /v1/transcribe:

curl https://api.techtuel.com/v1/transcribe \
  -H "Authorization: Bearer $TECHTUEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://cdn.example.com/podcast/episode-42.mp3"
  }'

The source is resolved, transcribed, and the text comes back in the response — one call, no job id to keep around.

Example result:

{
  "id": "job_9f2c",
  "status": "completed",
  "title": "How retrieval systems fail in production",
  "language": "en",
  "minutes": 54.7,
  "transcript": "Welcome to the show...",
  "segments": [
    {
      "start_seconds": 0.0,
      "text": "Welcome to the show. Today we are discussing retrieval systems."
    },
    {
      "start_seconds": 5.8,
      "text": "Our guest has spent five years building search infrastructure."
    }
  ]
}

Each segment carries a start time and its text — and nothing else. A segment implicitly ends where the next one starts, which is enough information for your retrieval system to cite the exact part of the episode.

Podcast episodes are rarely three minutes long, and transcription cannot depend indefinitely on the lifetime of an HTTP request: POST /v1/transcribe waits up to thirty seconds (tunable with ?wait=, up to 120), then returns 202 with {"id": "job_9f2c", "status": "processing"}. That is not an error, just the signal to fetch the transcript later from GET /v1/transcriptions/{id}. Across a catalogue of long episodes it is the normal case — hence the job model described below.

Ingest episodes from an RSS feed

Podcast RSS feeds normally expose each episode in an <item> element. The audio file is commonly available through its <enclosure>. Techtuel also exposes an RSS transcription API for this kind of source.

A basic ingestion worker can:

  1. fetch the RSS feed;
  2. identify episodes that are not yet indexed;
  3. extract the enclosure URL;
  4. submit that URL to Techtuel — POST /v1/transcriptions is the right tool here: a feed backfill is bulk ingestion of long episodes, and nothing is waiting on the response;
  5. re-read the job until it reaches a terminal status;
  6. store the transcript and metadata;
  7. create searchable chunks.

Step 5 really is polling: there is no webhook, so a scheduled worker that re-reads its in-flight jobs is the intended shape. There are four statuses — processing, claimed, completed, failed — but only completed and failed are terminal. claimed just means a worker picked the job up, so treat it like processing.

const TERMINAL = new Set(['completed', 'failed'])
 
export function isFinished(status: string): boolean {
  // `claimed` is NOT terminal: the job has only just been picked up.
  return TERMINAL.has(status)
}

For a one-off episode — a test, an on-demand subscription, a manual addition — POST /v1/transcribe stays shorter: the transcript arrives in the response, and any 202 is retrieved from the same GET /v1/transcriptions/{id}.

Use the RSS GUID as an external id when it is stable.

Example metadata:

{
  "podcast": "The Search Engineering Podcast",
  "episode": "Hybrid retrieval in production",
  "guid": "episode-2026-042",
  "published_at": "2026-07-20T08:00:00Z",
  "episode_url": "https://example.com/episodes/hybrid-retrieval",
  "audio_url": "https://cdn.example.com/episodes/hybrid-retrieval.mp3"
}

Node.js example: detect new RSS episodes

import Parser from 'rss-parser'
 
const parser = new Parser()
 
type StoredEpisode = {
  guid: string
}
 
export async function findNewEpisodes(
  feedUrl: string,
  storedEpisodes: StoredEpisode[],
) {
  const feed = await parser.parseURL(feedUrl)
  const knownGuids = new Set(storedEpisodes.map((episode) => episode.guid))
 
  return feed.items
    .filter((item) => item.guid && !knownGuids.has(item.guid))
    .map((item) => ({
      guid: item.guid!,
      title: item.title ?? 'Untitled episode',
      publishedAt: item.isoDate ?? null,
      episodeUrl: item.link ?? null,
      audioUrl: item.enclosure?.url ?? null,
    }))
    .filter((episode) => episode.audioUrl)
}

In production, also account for:

  • missing or reused GUIDs;
  • feed migrations;
  • modified enclosure URLs;
  • private feeds;
  • retries;
  • duplicate jobs;
  • deleted episodes;
  • rate limits.

Create podcast chunks for retrieval

Podcast transcripts are conversational. Blindly splitting every fixed number of characters can cut an explanation in the middle.

A practical first version can merge adjacent segments until a target size is reached.

Each chunk should retain:

{
  "content": "The guest explains that hybrid retrieval combines...",
  "metadata": {
    "podcast": "The Search Engineering Podcast",
    "episode": "Hybrid retrieval in production",
    "episode_guid": "episode-2026-042",
    "episode_url": "https://example.com/episodes/hybrid-retrieval",
    "start": 1184.2,
    "end": 1268.7,
    "published_at": "2026-07-20T08:00:00Z"
  }
}

start comes from the API's start_seconds. end does not: the API never returns one. You compute it while chunking, from the start time of the segment that follows the chunk — and the last chunk of an episode has no successor, so either leave it null or close it with the job's minutes converted to seconds.

Recommended rules:

  • preserve chronological order;
  • merge only adjacent transcript segments;
  • target a stable token range;
  • add a small overlap;
  • keep every chunk's start time, and derive its end;
  • include episode and podcast titles;
  • keep the raw transcript;
  • avoid embedding repeated intros and advertisements when possible.

Build timestamped podcast citations

Your application can create a source link with a timestamp when the podcast player supports it.

When direct timestamp links are unavailable, return the start time visibly:

{
  "title": "Hybrid retrieval in production",
  "episode_url": "https://example.com/episodes/hybrid-retrieval",
  "timecode": "19:44",
  "start_seconds": 1184
}

A small formatter is enough:

export function formatTimecode(seconds: number): string {
  const value = Math.max(0, Math.floor(seconds))
  const hours = Math.floor(value / 3600)
  const minutes = Math.floor((value % 3600) / 60)
  const remainingSeconds = value % 60
 
  const mm = String(minutes).padStart(2, '0')
  const ss = String(remainingSeconds).padStart(2, '0')
 
  return hours > 0 ? `${hours}:${mm}:${ss}` : `${minutes}:${ss}`
}

Search an entire podcast catalogue

Once transcripts are indexed, users can ask questions that metadata alone cannot answer:

  • When did the guest discuss vector database costs?
  • Which episodes mention a specific company?
  • What arguments were made for and against fine-tuning?
  • Where are all the discussions about hybrid search?
  • What did three different guests say about the same topic?
  • What is the exact quote about that technical decision?

Your retrieval layer can combine:

  • semantic vector search;
  • full-text search;
  • publication-date filters;
  • podcast and episode filters;
  • language filters;
  • reranking;
  • diversity rules.

Hybrid search is often useful for podcasts because queries may contain exact names, technical terms or product names that should not rely exclusively on semantic similarity.

Example PostgreSQL schema

Note end_seconds: it is nullable, because it is a value your pipeline derives from the next segment rather than one the API returns. The final chunk of an episode has nothing after it to close the range.

CREATE TABLE podcast_episodes (
  id UUID PRIMARY KEY,
  feed_url TEXT,
  guid TEXT NOT NULL,
  podcast_title TEXT,
  episode_title TEXT NOT NULL,
  episode_url TEXT,
  audio_url TEXT NOT NULL,
  published_at TIMESTAMPTZ,
  language TEXT,
  transcript TEXT NOT NULL,
  UNIQUE (feed_url, guid)
);
 
CREATE TABLE podcast_chunks (
  id UUID PRIMARY KEY,
  episode_id UUID NOT NULL REFERENCES podcast_episodes(id) ON DELETE CASCADE,
  content TEXT NOT NULL,
  -- Straight from the API segment.
  start_seconds DOUBLE PRECISION NOT NULL,
  -- DERIVED from the following segment's start; NULL on the last chunk.
  end_seconds DOUBLE PRECISION,
  embedding VECTOR(1536)
);

Keep ingestion state separately if the pipeline is asynchronous:

CREATE TABLE podcast_ingestion_jobs (
  id UUID PRIMARY KEY,
  episode_id UUID NOT NULL REFERENCES podcast_episodes(id),
  transcription_id TEXT UNIQUE,
  -- Mirrors the Techtuel job status: processing, claimed, completed, failed.
  status TEXT NOT NULL,
  attempts INTEGER NOT NULL DEFAULT 0,
  last_error TEXT,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

RAG use cases for podcast transcripts

Podcast search engine

Let listeners search inside every episode and jump to the relevant moment.

Research database

Index interviews and long-form conversations for journalists, researchers and analysts.

Internal knowledge base

Turn private audio archives, recorded meetings and company podcasts into searchable knowledge.

Expert assistant

Create an assistant grounded in a specific host, publication, topic or collection of experts.

Content repurposing

Find relevant passages before generating show notes, articles, newsletters or social excerpts.

Cross-episode analysis

Compare positions, themes and recurring concepts across a complete back catalogue.

Process a podcast back catalogue

For a large catalogue:

  • enumerate every RSS item;
  • deduplicate by GUID and audio URL;
  • submit jobs with controlled concurrency;
  • persist the Techtuel transcription id;
  • retry transient failures with POST /v1/transcriptions/{id}/retry, covered on the asynchronous transcription API page;
  • index completed transcripts independently;
  • record permanently unavailable episodes.

Public sources already processed may benefit from Techtuel's integrated cache, which avoids reprocessing the same source unnecessarily.

Watch for 402 responses while backfilling: the body's reason says whether you hit quota_exceeded (pause the queue) or payment_failed (a billing problem no retry will fix).

Keep ingestion separate from indexing

Do not make transcription, chunking and embedding one irreversible operation.

Store:

  1. the source metadata;
  2. the full transcript;
  3. the original segments;
  4. your current chunks;
  5. your current embeddings.

This lets you:

  • change embedding models;
  • adjust chunk sizes;
  • add a keyword index;
  • remove recurring advertisements;
  • improve metadata;
  • rebuild the RAG index;

without retranscribing every episode.

Hosting and data control

Techtuel is built and hosted in France.

Media files are processed and then deleted. The transcript is returned through the API and remains under your control until you call DELETE /v1/transcriptions/{id}. What that means in practice for a European product is covered in the article on transcription hosted in Europe.

For private or regulated content, you should still define:

  • who can submit media;
  • which sources users are allowed to process;
  • how long transcripts are retained;
  • who can search the index;
  • how deletion propagates to chunks and embeddings;
  • whether personal data must be redacted.

Frequently asked questions

Can I use an RSS feed as the source?

Techtuel supports podcast and RSS-oriented media ingestion. For fine-grained control over catalogue synchronization, you can also parse the feed yourself and submit each episode audio URL.

Can I index old podcast episodes?

Yes. If the audio URL is still accessible, you can submit an episode regardless of its publication date.

Does the transcript include timestamps?

Yes — start times. Each JSON segment has start_seconds and text. There is no end time: a segment runs until the next one starts, so you derive ranges during chunking. SRT and VTT outputs are also available.

Can I search multiple podcasts at once?

Yes. Store the podcast name, episode metadata and transcript chunks in the same search system, then filter by collection when needed.

How should I handle a new episode?

Run a scheduled feed synchronization, detect a new GUID, submit the audio URL, then re-read the job until it reaches a terminal status — completed or failed, with claimed only meaning a worker picked it up — before indexing the transcript. For a short episode submitted by hand, POST /v1/transcribe returns the text directly and saves you the whole cycle.

Can I use the result with an LLM?

Yes. The transcript is plain structured text. It can be chunked, embedded and used as retrieved context for any compatible language model.

Start indexing podcasts

Convert podcast episodes into timestamped documents ready for search, embeddings and RAG.

Read the API documentation

Try it without an account