Let's Learn GenAI
  • Learn0 topics
  • Techniques0 topics
  • Courses0 courses
    View all courses →
  • GenAI Guide
    AI Career Path
    Paid AI Models & Tools
    Free AI Models & Tools
    Interview Preparation
    AI Career Path0 items
    View all paths →
  • Resources
    ArticlesResearch, releases & insight.NewsletterCurated AI, to your inbox.BenchmarkTop models, ranked.
Newsletter
  1. Home
  2. /Interview Prep
  3. /Coding
  4. /RAG Pipeline

RAG Pipeline

Implement a retrieval-augmented generation pipeline: ingest documents, chunk with overlap, embed, and retrieve top-K similar chunks via dot-product similarity.

Last updated: September 2026

Implement a retrieval-augmented generation pipeline: ingest documents, chunk them with overlap, embed each chunk, then retrieve the top-K most similar chunks for a query using dot-product similarity. The key design decision is separating ingest() from update() - making intentional overwrite explicit rather than silent.

“ingest() is create - fail loudly on duplicate. update() is an intentional overwrite. Never silently overwrite on ingest: data corruption is worse than a raised exception. retrieve() returns empty on an empty index - never raise, never hallucinate.”

Clarifying Questions (Ask These First)

QuestionWhy it matters
Plain text input only, or PDF and HTML too?PDF needs parsing (PyMuPDF), HTML needs extraction - plain text is the simplest starting point
Real embedding model or stub acceptable?Stub (ASCII values) is fine for the interview - call out where the real API call goes
Is top_k fixed at init or configurable per query?Per-query is more flexible: pipeline.retrieve(query, top_k=5) is the right default
Duplicate doc_id: overwrite silently or raise?This is the most important design question - ingest() raises, update() overwrites

Implementation

from dataclasses import dataclass, field
from typing import List

@dataclass
class Document:
    doc_id: str
    content: str
    chunks: List[str] = field(default_factory=list)

class RAGPipeline:
    def __init__(self, chunk_size: int = 512, overlap: int = 50):
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.index = {}  # doc_id -> list of (chunk, embedding)

    def ingest(self, doc: Document):
        if doc.doc_id in self.index:
            raise ValueError(f"Use update() to replace existing document '{doc.doc_id}'")
        self._index_document(doc)

    def update(self, doc: Document):
        self._index_document(doc)

    def _index_document(self, doc: Document):
        if not doc.content or not doc.doc_id:
            raise ValueError("Document must have non-empty id and content")
        chunks = self._chunk(doc.content)
        doc.chunks = chunks
        self.index[doc.doc_id] = [(chunk, self._embed(chunk)) for chunk in chunks]

    def _chunk(self, text: str) -> List[str]:
        chunks, start = [], 0
        while start < len(text):
            end = min(start + self.chunk_size, len(text))
            chunks.append(text[start:end])
            start += self.chunk_size - self.overlap
        return chunks

    def _embed(self, text: str) -> List[float]:
        # Stub: replace with real embedding API call
        return [float(ord(c)) for c in text[:10]]

    def retrieve(self, query: str, top_k: int = 3) -> List[str]:
        if not self.index:
            return []
        query_emb = self._embed(query)
        scored = []
        for chunks_embs in self.index.values():
            for chunk, emb in chunks_embs:
                score = sum(a * b for a, b in zip(query_emb, emb))
                scored.append((score, chunk))
        scored.sort(reverse=True)
        return [chunk for _, chunk in scored[:top_k]]


# Usage
pipeline = RAGPipeline(chunk_size=512, overlap=50)
pipeline.ingest(Document(doc_id="doc_1", content="RAG stands for Retrieval Augmented Generation..."))
pipeline.ingest(Document(doc_id="doc_2", content="Vector databases store embeddings efficiently..."))
results = pipeline.retrieve("What is RAG?", top_k=2)
print(results)

Key Design Decisions

  • 1ingest() vs update() - Explicit Intent - ingest() raises ValueError on duplicate doc_id. update() overwrites unconditionally. This makes the caller declare intent - accidental overwrites on ingest are a data correctness bug, so fail loudly. Common pattern in database write paths.
  • 2Chunking with Overlap - _chunk() slides a window of chunk_size characters, advancing by (chunk_size - overlap) each step. Overlap ensures a concept split across a boundary appears fully in at least one chunk. Without overlap, relevant context at chunk edges is silently lost.
  • 3Dot-Product Similarity (Stub) - retrieve() computes sum(a*b for a,b in zip(query_emb, emb)). For real unit-normalized embeddings this equals cosine similarity. The stub embedding (first 10 ASCII values) is declared upfront - interviewers appreciate knowing exactly where the real API call goes.
  • 4_index_document() - Shared Private Method - Both ingest() and update() delegate to _index_document() after their respective pre-conditions. Shared logic lives in one place - if chunking or embedding changes, you change it once. Classic DRY applied correctly.

Frequently asked questions

How would you replace the stub embedding with a real one?

Replace _embed() with a call to an embedding API: OpenAI text-embedding-3-small, Vertex AI text-embedding-004, or a local model via SentenceTransformers. The rest of the pipeline does not change - that is why the stub is isolated in one method. In production, batch embed all chunks in one API call rather than calling per chunk.

What is the problem with dot-product on non-normalized embeddings?

Dot product conflates magnitude with direction. A long document embedding with large magnitude can outscore a highly relevant short chunk just by being bigger. Fix: normalize embeddings to unit length before storing (L2 norm), then dot product equals cosine similarity. Most embedding APIs return normalized vectors already.

How does the overlap prevent lost context?

If a sentence spans characters 500-560 and chunk_size is 512 with overlap 50, the first chunk covers 0-512 and the second covers 462-974. The bridging sentence appears fully in the second chunk. Without overlap it would be split at character 512 and neither chunk would contain the complete sentence.

How would you handle a document that changes frequently?

Call update(doc) - it re-chunks and re-embeds the entire document and replaces the index entry. For large documents this is expensive. Optimization: track a content hash per chunk. On update, only re-embed chunks whose hash changed. Store chunk-level hashes alongside embeddings.

What happens if two threads call ingest() with the same doc_id simultaneously?

Race condition: both see doc_id not in self.index, both proceed to _index_document, last writer wins silently - defeating the purpose of ingest() raising on duplicate. Fix: add a threading.Lock() around the check-then-insert, same pattern as the rate limiter.

How would you scale this to millions of documents?

Replace self.index (in-memory dict) with a vector database: Pinecone, Weaviate, Qdrant, or Vertex AI Vector Search. These support ANN (Approximate Nearest Neighbor) search which scales to billions of vectors with sub-100ms latency. Exact dot-product over millions of vectors is O(N) and too slow.

Where this skill is used

AI roles that rely on this day to day, with salaries and the path in.

  • LLM Engineer
  • Generative AI Developer
  • AI/ML Engineer
  • AI Solutions Architect
  • Prompt Engineer
  • Data Scientist

Related coding challenges

  • Rate LimiterDesign a thread-safe sliding-window rate limiter with per-user and global call limits using timestamp lists and a threading lock.
  • Retry HandlerImplement a configurable retry handler with exponential backoff, jitter, and a max-delay cap to prevent thundering herds and unbounded blocking.

Newsletter

Four editions, one inbox

The Build Layer for developers, The Strategy Signal for managers, The Executive Brief for executives.

Pick your edition

Guided courses

Get certified, not just informed

Guided courses from beginner to advanced, each with a named certificate. Free to take, yours to keep.

Browse courses
Let's Learn GenAI

Your guided portal to AI fundamentals, advanced techniques, and industry resources.

Learn

FundamentalsTechniquesCareersPaid AI ToolsFree AI ToolsBenchmarks

Explore

ArticlesNewsletterAbout

Legal

Terms of ServicePrivacy Policy

© 2026 Let's Learn GenAI. All rights reserved.