100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Retrieval-Augmented Generation
60 minadvanced

End-to-End Project: Document Q&A System

This capstone project implements a retrieval-augmented generation (RAG) system specifically designed for real-time cricket match analysis and player performance commentary. The system combines a dense vector database of historical match statistics, player profiles, and match transcripts with a large language model (LLM) to generate contextually accurate, data-backed cricket commentary and performance insights.

The project demonstrates production-grade RAG architecture across several dimensions, including semantic chunking of cricket narratives, multi-stage retrieval using both BM25 and dense embeddings, prompt engineering for sports-specific generation, and integration with live match APIs. You will build a system that retrieves relevant historical precedents from a cricket knowledge base and uses them to generate accurate, engaging match commentary that cites specific player performances, match situations, and statistical context.

As a portfolio project, this system showcases advanced RAG patterns including hybrid retrieval, re-ranking, context window optimization, and structured output generation. These skills are directly applicable to domains such as financial analysis, medical record systems, and enterprise knowledge management, where accurate source attribution is critical.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a cricket commentator like Harsha Bhogle or Ravi Shastri doesn't invent analysis on the spot but draws from their deep knowledge of historical matches, player records, and statistical patterns—comparing Virat Kohli's current innings to his powerplay performances against express fast bowlers—a RAG system retrieves specific documented facts and uses them to generate new, contextually relevant insights. The commentator's preparation (studying past performances, reading scorecards, reviewing DRS decisions from similar situations) is like the retrieval stage: finding the most relevant cricket precedents and statistics. The live commentary generation is like the generation stage: weaving those retrieved facts into flowing, insightful narrative that explains why Rohit Sharma is playing a certain way or why Jasprit Bumrah's economy rate is exceptional. The system's ability to cite which specific match or player statistic informed each piece of commentary mirrors a commentator's credibility: 'In his last 10 Test innings against pace on bouncy wickets, Pujara averaged 52'—that's retrieval feeding generation. Understanding this parallel reveals why RAG matters: without grounding, an LLM generates plausible-sounding but potentially false cricket analysis, but with retrieval, it produces commentary that's both informative and factually verifiable against the knowledge base.

Learning Objectives

  • Design and implement a hybrid retrieval system combining BM25 sparse retrieval with dense embedding-based semantic search for cricket domain-specific queries.
  • Build a multi-stage ranking pipeline that re-ranks retrieved cricket match documents using cross-encoder models to improve RAG generation quality and relevance.
  • Engineer prompts specifically tailored to cricket commentary generation that enforce citation of retrieved sources and maintain factual accuracy within context windows.
  • Integrate vector embeddings and keyword search over real cricket datasets (player statistics, match narratives, performance transcripts) into a production-ready retrieval backend.
  • Implement context window management and chunking strategies that preserve cricket narrative coherence while optimizing retrieval efficiency and token usage.
  • Evaluate RAG output quality through relevance metrics, hallucination detection, and citation accuracy—metrics critical for regulated domains like finance and healthcare.

Technical Requirements

  • Implement vector embedding pipeline using open-source models (e.g., sentence-transformers) to encode cricket match narratives, player bios, and statistical summaries into 384-768 dimensional vectors.
  • Build FAISS or similar vector database index supporting approximate nearest neighbor search to retrieve top-K contextually similar cricket scenarios within <500ms latency.
  • Integrate BM25 keyword retrieval over inverted indices to handle exact player names, team abbreviations, match dates, and statistics not captured by dense embeddings alone.
  • Create fusion retriever combining BM25 and dense retrieval using reciprocal rank fusion or learned combination weights optimized for cricket query distribution.
  • Implement re-ranking stage using cross-encoder models to score relevance of retrieved cricket documents relative to the user query before passing to generation.
  • Engineer prompt templates with instruction finetuning to enforce RAG outputs cite retrieved match statistics, include player names with context, and avoid hallucinating cricket facts.
  • Design chunking strategy splitting cricket match transcripts and player records into 256-512 token chunks with rolling overlap to preserve narrative coherence and cross-document relevance.
  • Build evaluation framework measuring BLEU/ROUGE similarity to ground-truth cricket commentary, measuring hallucination rate, and tracking citation precision/recall against retrievals.

Architecture & Design

The RAG architecture consists of five core components orchestrated in a data flow pipeline. The first is the Indexing Pipeline, which processes cricket match transcripts, player statistics, and historical narratives, applies semantic chunking with overlap, embeds chunks using a domain-optimized encoder, and stores both embeddings in FAISS and raw text with metadata in a document store.

The second component is the Retrieval Engine, which accepts user queries and performs hybrid search by executing BM25 keyword search and dense vector search in parallel. It merges results using reciprocal rank fusion and returns the top-10 candidate documents. The third component is the Re-ranking Module, which takes those candidate documents and the original query, applies a cross-encoder model to score relevance, reorders results by confidence, and selects the top three documents for LLM context.

The fourth component is the Prompt Engineering Layer, which constructs a system prompt enforcing citation requirements, inserts the retrieved cricket documents as context, frames the user query as a cricket analysis request, and specifies the required output structure of commentary paired with cited sources. The fifth and final component is the Generation and Post-processing stage, which calls the LLM with the formatted prompt, parses the output to extract commentary and source citations, validates those citations against retrieval records, and surfaces hallucination warnings when generated cricket facts lack supporting evidence.

This modular design allows independent optimization of retrieval quality, re-ranking accuracy, and generation fidelity — a critical property in production systems where retrieval latency, embedding quality, and LLM output calibration directly impact user experience. Data flows from raw cricket documents through embedding to a vector index, user queries route through dual retrieval pathways, results aggregate and re-rank, context feeds into the prompt, and LLM output routes through citation validation before final delivery.

Analogy🏏Cricket
🏏 Think of it like cricket: The RAG architecture mirrors how a cricket analyst prepares for and delivers live commentary during an India vs. Pakistan Test match. The Indexing Pipeline is like the analyst's preparation—watching hundreds of hours of match footage, recording statistics for Virat Kohli's approach against reverse swing, noting patterns in Babar Azam's footwork, organizing this knowledge into a searchable mental database. The Retrieval Engine is like the analyst's instinctive knowledge recall during the match: when Kohli walks to the crease, the analyst's mind immediately retrieves relevant memories of his recent form, his record at this ground, and his history against the opposing bowler. The Re-ranking Module is like the analyst filtering these memories for relevance: not all of Kohli's past performances matter equally in this specific situation—his recent scores in powerplay overs are more relevant than his performances two years ago, so the analyst mentally elevates the most pertinent precedent. The Prompt Engineering Layer is like the analyst's prepared commentary structure—having talking points ready, knowing which statistics to cite, understanding when to reference historical context (e.g., 'Bumrah's economy in death overs mirrors his performance in the last ODI'). The Generation & Post-processing stage is the live commentary itself: the analyst weaves retrieved facts into flowing narrative with explicit citations ('In his last ten Test innings...'), ensuring every claim is anchored to evidence rather than speculation. This architecture's sophistication reveals why RAG matters in cricket analysis: a system without retrieval might hallucinate false statistics, but a RAG system anchored to documented match records generates trustworthy, verifiable commentary.
python
# Cricket RAG System - Architecture Skeleton & Project Structure
# End-to-End Document Q&A System for Cricket Match Analysis

import os
import json
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass, field
from datetime import datetime
from collections import defaultdict
import hashlib

# ============================================================================
# DATA MODELS - Cricket Domain Entities
# ============================================================================

@dataclass
class CricketMatch:
    """Represents a cricket match with metadata."""
    match_id: str
    team_a: str
    team_b: str
    match_date: str
    venue: str
    format_type: str  # Test, ODI, T20
    match_summary: str  # Raw document/commentary text
    
    def __hash__(self):
        return hash(self.match_id)


@dataclass
class CricketPlayer:
    """Represents a cricket player with performance statistics."""
    player_name: str
    team: str
    role: str  # Batsman, Bowler, All-rounder
    career_stats: Dict[str, int] = field(default_factory=dict)
    
    def __repr__(self):
        return f"{self.player_name} ({self.team}, {self.role})"


@dataclass
class RetrievedContext:
    """Retrieved document chunks relevant to a query."""
    chunk_id: str
    chunk_text: str
    source_match_id: str
    relevance_score: float
    player_references: List[str] = field(default_factory=list)


@dataclass
class QueryResponse:
    """Final response generated from RAG pipeline."""
    query: str
    answer: str
    retrieved_chunks: List[RetrievedContext]
    generation_timestamp: str


# ============================================================================
# INDEXING PIPELINE - Knowledge Base Preparation (Like Match Footage Review)
# ============================================================================

class IndexingPipeline:
    """Mirrors the cricket analyst's preparation phase."""
    
    def __init__(self):
        self.document_store: Dict[str, CricketMatch] = {}
        self.index_chunks: Dict[str, str] = {}  # chunk_id -> text
        self.player_index: Dict[str, List[str]] = defaultdict(list)  # player -> chunk_ids
        self.keyword_index: Dict[str, List[str]] = defaultdict(list)  # keyword -> chunk_ids
        
    def ingest_match_document(self, match: CricketMatch) -> None:
        """
        Ingest a complete match document.
        Like: Analyst watches full match footage and records statistics.
        """
        self.document_store[match.match_id] = match
        self._chunk_and_index(match)
        
    def _chunk_and_index(self, match: CricketMatch) -> None:
        """
        Break match summary into searchable chunks.
        Like: Analyst creates indexed notes on key moments, player performances.
        """
        sentences = match.match_summary.split('.')
        
        for idx, sentence in enumerate(sentences):
            if sentence.strip():
                chunk_id = f"{match.match_id}_chunk_{idx}"
                chunk_text = sentence.strip()
                
                self.index_chunks[chunk_id] = chunk_text
                
                # Index by player mentions (like noting Rohit Sharma's patterns)
                if "Rohit Sharma" in chunk_text:
                    self.player_index["Rohit Sharma"].append(chunk_id)
                if "Virat Kohli" in chunk_text:
                    self.player_index["Virat Kohli"].append(chunk_id)
                if "Jasprit Bumrah" in chunk_text:
                    self.player_index["Jasprit Bumrah"].append(chunk_id)
                if "Babar Azam" in chunk_text:
                    self.player_index["Babar Azam"].append(chunk_id)
                    
                # Index by keywords
                keywords = ["reverse swing", "footwork", "innings", "bowling", "batting", 
                           "partnership", "wicket", "century", "boundary"]
                for keyword in keywords:
                    if keyword.lower() in chunk_text.lower():
                        self.keyword_index[keyword].append(chunk_id)


# ============================================================================
# RETRIEVAL ENGINE - Knowledge Recall (Like Live Commentary Instinct)
# ============================================================================

class RetrievalEngine:
    """
    Mirrors the cricket analyst's instinctive knowledge recall.
    When player walks to crease  immediately retrieve relevant match data.
    """
    
    def __init__(self, indexing_pipeline: IndexingPipeline):
        self.pipeline = indexing_pipeline
        
    def retrieve_context(self, query: str, top_k: int = 3) -> List[RetrievedContext]:
        """
        Retrieve most relevant chunks for a query.
        Like: Analyst recalls relevant statistics when commentating.
        """
        retrieved = []
        relevance_scores = {}
        
        # Search by player name in query
        for player_name in self.pipeline.player_index.keys():
            if player_name.lower() in query.lower():
                chunk_ids = self.pipeline.player_index[player_name]
                for chunk_id in chunk_ids:
                    relevance_scores[chunk_id] = relevance_scores.get(chunk_id, 0) + 0.8
        
        # Search by keywords in query
        keywords = ["reverse swing", "footwork", "innings", "bowling", "batting", 
                   "partnership", "wicket", "century", "boundary"]
        for keyword in keywords:
            if keyword.lower() in query.lower():
                chunk_ids = self.pipeline.keyword_index[keyword]
                for chunk_id in chunk_ids:
                    relevance_scores[chunk_id] = relevance_scores.get(chunk_id, 0) + 0.6
        
        # Sort by relevance and return top-k
        sorted_chunks = sorted(relevance_scores.items(), 
                              key=lambda x: x[1], 
                              reverse=True)[:top_k]
        
        for chunk_id, score in sorted_chunks:
            chunk_text = self.pipeline.index_chunks.get(chunk_id, "")
            match_id = chunk_id.split("_chunk_")[0]
            
            # Extract player references
            player_refs = [p for p in ["Rohit Sharma", "Virat Kohli", "Jasprit Bumrah", "Babar Azam"] 
                          if p in chunk_text]
            
            retrieved.append(RetrievedContext(
                chunk_id=chunk_id,
                chunk_text=chunk_text,
                source_match_id=match_id,
                relevance_score=score,
                player_references=player_refs
            ))
        
        return retrieved


# ============================================================================
# GENERATION STAGE - Commentary Generation
# ============================================================================

class CommentaryGenerator:
    """Generates natural language responses using retrieved context."""
    
    def generate_response(self, query: str, retrieved_contexts: List[RetrievedContext]) -> str:
        """
        Generate commentary-style response.
        Like: Analyst synthesizes retrieved facts into coherent live commentary.
        """
        if not retrieved_contexts:
            return "I don't have relevant match data to answer this question."
        
        response = f"Based on match analysis: "
        
        for context in retrieved_contexts:
            response += f"\n• {context.chunk_text}"
            if context.player_references:
                response += f" [Involving: {', '.join(context.player_references)}]"
        
        return response


# ============================================================================
# END-TO-END RAG SYSTEM
# ============================================================================

class CricketRAGSystem:
    """Complete RAG system for cricket match Q&A."""
    
    def __init__(self):
        self.indexing_pipeline = IndexingPipeline()
        self.retrieval_engine = RetrievalEngine(self.indexing_pipeline)
        self.generator = CommentaryGenerator()
        
    def add_match_document(self, match: CricketMatch) -> None:
        """Add a match document to the knowledge base."""
        self.indexing_pipeline.ingest_match_document(match)
        print(f"✓ Indexed match: {match.team_a} vs {match.team_b} ({match.match_date})")
        
    def answer_query(self, query: str) -> QueryResponse:
        """
        Complete RAG pipeline: Retrieve  Generate  Return.
        """
        print(f"\n🏏 Processing query: '{query}'")
        
        # RETRIEVAL: Get relevant context
        retrieved_contexts = self.retrieval_engine.retrieve_context(query, top_k=3)
        print(f"   Retrieved {len(retrieved_contexts)} relevant chunks")
        
        # GENERATION: Create response
        answer = self.generator.generate_response(query, retrieved_contexts)
        print(f"   Generated response")
        
        # Package response
        response = QueryResponse(
            query=query,
            answer=answer,
            retrieved_chunks=retrieved_contexts,
            generation_timestamp=datetime.now().isoformat()
        )
        
        return response


# ============================================================================
# DEMO: Complete End-to-End RAG System
# ============================================================================

if __name__ == "__main__":
    print("=" * 70)
    print("CRICKET RAG SYSTEM - Document Q&A Demo")
    print("=" * 70)
    
    # Initialize RAG system
    rag_system = CricketRAGSystem()
    
    # Create sample match documents
    india_vs_pakistan_match = CricketMatch(
        match_id="IND_PAK_TEST_2024_001",
        team_a="India",
        team_b="Pakistan",
        match_date="2024-01-15",
        venue="Lahore",
        format_type="Test",
        match_summary="""
        India vs Pakistan Test Match. Rohit Sharma opened the innings with 
        exceptional footwork against Pakistan's reverse swing bowling attack. 
        Virat Kohli came to the crease and quickly settled in. Jasprit Bumrah's 
        bowling was outstanding, taking key wickets. Babar Azam showed brilliant 
        footwork in the second innings. The partnership between Rohit Sharma and 
        Virat Kohli produced a century stand. Jasprit Bumrah took another 
        important wicket with a reverse swing delivery. India's batting 
        partnership eventually led to victory.
        """
    )
    
    # Add match to knowledge base (INDEXING PHASE)
    rag_system.add_match_document(india_vs_pakistan_match)
    
    # Process example queries (RETRIEVAL + GENERATION)
    queries = [
        "How did Rohit Sharma perform against reverse swing?",
        "What was Jasprit Bumrah's key contribution?",
        "Tell me about the partnership involving Virat Kohli.",
        "How did Babar Azam's footwork look in the second innings?"
    ]
    
    print("\n" + "=" * 70)
    print("QUERY PROCESSING")
    print("=" * 70)
    
    for query in queries:
        response = rag_system.answer_query(query)
        print(f"\n📋 Query: {response.query}")
        print(f"📝 Answer:\n{response.answer}")
        print("-" * 70)
    
    print("\n✓ RAG System demonstration complete!")

Phase 1 — Core Implementation

Phase 1 implements the essential indexing and retrieval pipeline. In this phase, you will build the document indexing layer that chunks cricket match transcripts and player statistics using semantic overlap, generates embeddings using a pretrained sentence-transformer model, creates a FAISS vector index for approximate nearest neighbor search, and implements BM25 keyword retrieval over match metadata.

The retrieval engine built in this phase performs parallel hybrid search by combining dense and sparse retrieval. It merges results using reciprocal rank fusion to balance precision and recall across different query types, and returns ranked candidates with relevance scores.

This phase demonstrates core RAG competencies that are foundational to production system design. These include understanding chunking trade-offs — where larger chunks preserve context but reduce retrieval specificity — as well as embedding model selection and evaluation, vector index construction and approximate search algorithms, and fusion strategies for combining multiple ranking signals.

Analogy🏏Cricket
🏏 Think of it like cricket: Phase 1 mirrors a cricket analyst's foundational preparation before the match season. Just as Ravi Shastri spent weeks before India's Test series organizing match footage into categories (powerplay performances, death overs, pitch conditions), building searchable mental indices of player records (Rohit Sharma's average on flat tracks vs. green wickets), and creating a system to quickly recall relevant precedent—Phase 1 organizes cricket knowledge for rapid retrieval. The chunking strategy is like dividing a match into meaningful segments: not just arbitrary time windows, but innings, overs, and key moments that preserve narrative coherence (similar to how a commentator remembers 'Kohli's 97 was defined by his decision to leave the ball outside off stump'). The embedding process is like the analyst learning to recognize patterns—a sentence-transformer embedding learns that 'Bumrah's yorker on leg stump' and 'Bumrah's deceptive delivery in death overs' represent similar cricket concepts, even if they use different words. The FAISS vector index is the analyst's ability to instantly recall similar situations: when asked 'What happens when a left-arm spinner bowls to a right-handed opener on day one?' the analyst's brain (index) retrieves relevant matches without conscious effort. BM25 keyword search is like the analyst's ability to look up exact facts: 'Find all matches where Babar Azam scored a century against Jasprit Bumrah'—this needs precise keyword matching, not fuzzy semantic similarity. The reciprocal rank fusion that combines both signals mirrors how an experienced analyst uses both pattern recognition (semantic similarity) and factual recall (keyword search) together to find the most relevant precedent. Understanding this parallel reveals why hybrid retrieval works: neither dense embeddings nor keywords alone capture all information needs in cricket analysis—you need both to be effective.
Lesson 20 of 35
0% complete