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

Capstone: Enterprise Knowledge Assistant

This capstone project challenges you to build a retrieval-augmented generation (RAG) system that powers an intelligent cricket analysis platform. The system ingests match records, player statistics, commentary, and tactical analyses into a vector database, then uses semantic search to retrieve contextually relevant cricket information. A large language model synthesizes this retrieved knowledge to generate match insights, player comparisons, and strategic recommendations.

The project demonstrates the complete RAG pipeline in action: document ingestion and chunking, embedding generation, vector indexing, semantic retrieval with relevance ranking, and LLM-augmented response generation with source attribution. These stages address real production challenges in information retrieval at scale, including managing thousands of cricket matches, handling ambiguous player queries, filtering by tournament context, and ensuring factual grounding through retrieved sources.

As a portfolio-grade project, this capstone showcases your ability to architect end-to-end AI systems that combine external knowledge bases with generative models. This pattern is widely used in production platforms such as LangChain applications, Microsoft Copilot Enterprise, and RAG implementations across enterprise search, customer support, and knowledge management.

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

  • Implement end-to-end RAG pipeline: document ingestion, embedding, retrieval, and generation with context injection.
  • Design vector database indexing strategies for multi-million match records with metadata filtering (tournament, season, player role).
  • Engineer semantic search with relevance ranking, reranking, and similarity thresholding to filter irrelevant retrievals.
  • Integrate LLM prompt engineering with retrieval context to generate factually grounded, source-attributed cricket insights.
  • Build production-grade error handling: graceful fallbacks, query validation, retrieval failure recovery, and response confidence scoring.
  • Evaluate RAG system quality using retrieval precision/recall, generation factuality metrics, and user-facing relevance judgments.

Technical Requirements

  • Ingest 500+ cricket match records (format: JSON scorecards with innings, bowling, player stats) into vector database using OpenAI/Hugging Face embeddings.
  • Implement semantic search returning top-K (K=5-10) matches ranked by cosine similarity, with metadata filters: tournament type, season, player names, match role.
  • Add reranking layer: sort retrieved matches by recency, player performance tier, or strategic match-up relevance using domain-specific scoring.
  • Design LLM prompt template injecting retrieved context: player stats, relevant match excerpts, historical trends, formatted with clear citation markers.
  • Build query expansion: auto-expand ambiguous queries ("Bumrah's death bowling") into semantic variants ("Jasprit Bumrah final overs", "Bumrah yorker accuracy", "Bumrah economy final 5 overs").
  • Implement confidence scoring: assess if retrieval set is sufficient (similarity score threshold, diversity penalty) to decide generation quality or trigger fallback.
  • Add logging and telemetry: track query latency, retrieval hit rate, generation token usage, and user feedback on response quality for continuous optimization.
  • Deploy with caching: LRU cache for frequent queries (top 100 players, tournament summaries) to reduce latency and embedding costs in production.

Architecture & Design

The architecture comprises five tightly integrated layers, each responsible for a distinct stage of the RAG pipeline. The Data Ingestion Layer processes raw cricket match records — including JSON scorecards, commentary, and statistics — chunks them along semantic boundaries such as one chunk per match, innings, or player performance narrative, and stores metadata including tournament_id, season, player_ids, and match_date to support downstream filtering.

The Embedding Layer vectorizes these chunks using a pre-trained dense embedding model, such as sentence-transformers/all-MiniLM-L6-v2 for cricket domain specificity or OpenAI's text-embedding-3-small for production robustness. This process generates embeddings ranging from 384 to 1,536 dimensions, capturing semantic similarity across diverse match contexts.

The Vector Database and Indexing Layer — implemented using Pinecone, Weaviate, or Milvus — stores these embeddings with HNSW indexing to enable sub-100ms retrieval at scale, supporting both efficient similarity search and metadata-based filtering.

The Retrieval and Reranking Layer executes semantic search, filters results by match metadata such as tournament, season, and player, and optionally applies cross-encoder reranking for higher accuracy at the cost of additional computation. It then surfaces the top-K retrieved chunks along with their similarity scores.

The Generation and LLM Integration Layer formats the retrieved context into a carefully engineered prompt — incorporating system instructions, retrieved facts with citations, the user query, and an output schema — before calling an LLM API such as GPT-4, Claude, or open-source Llama. The layer then post-processes the response to extract citations and confidence signals.

Several key design decisions reinforce the robustness of this architecture. These include chunking at match-level granularity to preserve statistical integrity, using BM25 hybrid search as a fallback when embedding search fails, implementing query rewriting for player name normalization such as resolving 'Rohit' to 'Rohit Sharma', caching embeddings to avoid recomputation, and rating retrievals through user feedback to iteratively improve ranking. This modular, layered design allows independent optimization of each component — swapping embedding models, adjusting reranking logic, or upgrading the LLM — without requiring a full pipeline re-engineering effort.

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
python
# Phase 1: Core Retrieval Implementation
# Embedding, Vector Storage, and Semantic Search

import numpy as np
import json
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass, asdict
from datetime import datetime
import csv

# For local embeddings
from sentence_transformers import SentenceTransformer

# For vector similarity
from sklearn.metrics.pairwise import cosine_similarity

# ============================================================================
# CONCRETE DATA STRUCTURES: Cricket Match Intelligence Database
# ============================================================================

@dataclass
class MatchAnalysis:
    """A match's tactical essence - the scout's summarized profile"""
    match_id: str
    opposition_team: str
    player_name: str
    bowling_style: str
    batting_approach: str
    key_vulnerabilities: str
    match_date: str
    embedding: Optional[np.ndarray] = None

@dataclass
class RetrievalResult:
    """Result from similarity search - most relevant past matches"""
    match_id: str
    opposition_team: str
    player_name: str
    similarity_score: float
    tactical_summary: str

# ============================================================================
# LAYER 1: DATA INGESTION
# Analyst transcribes match history into detailed records
# ============================================================================

class MatchAnalystLayer:
    """
    Mimics the coaching analyst who watches all deliveries, batting patterns,
    and fielding decisions, then transcribes them into structured intelligence.
    """
    
    def __init__(self):
        self.match_database: List[MatchAnalysis] = []
    
    def ingest_match_intelligence(self, raw_match_notes: str) -> MatchAnalysis:
        """
        Parse raw match notes into structured tactical intelligence.
        In reality, this would come from video analysis, ball-by-ball commentary, etc.
        """
        # Simulated parsing of match notes
        lines = raw_match_notes.strip().split('\n')
        
        match_record = MatchAnalysis(
            match_id=f"MATCH_{datetime.now().timestamp()}",
            opposition_team=lines[0].split(': ')[1],
            player_name=lines[1].split(': ')[1],
            bowling_style=lines[2].split(': ')[1],
            batting_approach=lines[3].split(': ')[1],
            key_vulnerabilities=lines[4].split(': ')[1],
            match_date=lines[5].split(': ')[1]
        )
        
        self.match_database.append(match_record)
        return match_record
    
    def get_all_matches(self) -> List[MatchAnalysis]:
        return self.match_database

# ============================================================================
# LAYER 2: EMBEDDING LAYER
# Scouts compress each match into a compact, searchable tactical profile
# ============================================================================

class TacticalEmbeddingScout:
    """
    The scout who watches each match and creates a compact tactical embedding.
    Similar tactical profiles become nearby vectors in embedding space.
    """
    
    def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
        """Initialize sentence transformer for generating tactical embeddings"""
        self.embedding_model = SentenceTransformer(model_name)
        self.cached_embeddings: Dict[str, np.ndarray] = {}
    
    def create_tactical_profile(self, match: MatchAnalysis) -> np.ndarray:
        """
        Compress match intelligence into a dense vector.
        All the deliveries, batting patterns, vulnerabilities  one searchable profile.
        """
        # Create a text summary of the match's tactical essence
        tactical_text = (
            f"Opposition: {match.opposition_team}. "
            f"Player: {match.player_name}. "
            f"Bowling style: {match.bowling_style}. "
            f"Batting approach: {match.batting_approach}. "
            f"Vulnerabilities: {match.key_vulnerabilities}."
        )
        
        # Transform to embedding
        embedding = self.embedding_model.encode(tactical_text)
        match.embedding = embedding
        self.cached_embeddings[match.match_id] = embedding
        
        return embedding
    
    def embed_all_matches(self, matches: List[MatchAnalysis]) -> List[np.ndarray]:
        """Batch embed all matches in the coaching database"""
        return [self.create_tactical_profile(match) for match in matches]

# ============================================================================
# LAYER 3: VECTOR DATABASE & SEMANTIC SEARCH
# The filing system organized by tactical similarity
# ============================================================================

class VectorMatchDatabase:
    """
    The organized filing system. Matches are stored by their tactical profile.
    Similar tactics are stored near each other - easy to retrieve relevant past matches.
    """
    
    def __init__(self):
        self.matches: List[MatchAnalysis] = []
        self.embeddings_matrix: Optional[np.ndarray] = None
    
    def index_matches(self, matches: List[MatchAnalysis]) -> None:
        """Build the vector index from all match embeddings"""
        self.matches = matches
        # Stack all embeddings into a matrix for efficient similarity search
        self.embeddings_matrix = np.array([m.embedding for m in matches if m.embedding is not None])
    
    def semantic_search(
        self, 
        query_embedding: np.ndarray, 
        top_k: int = 3
    ) -> List[RetrievalResult]:
        """
        Find most tactically similar past matches.
        Like the coach asking: "Show me matches where we faced similar challenges"
        """
        if self.embeddings_matrix is None or len(self.embeddings_matrix) == 0:
            return []
        
        # Compute similarity between query and all stored matches
        similarities = cosine_similarity([query_embedding], self.embeddings_matrix)[0]
        
        # Get top-k most similar matches
        top_indices = np.argsort(similarities)[::-1][:top_k]
        
        results = []
        for idx in top_indices:
            match = self.matches[idx]
            results.append(RetrievalResult(
                match_id=match.match_id,
                opposition_team=match.opposition_team,
                player_name=match.player_name,
                similarity_score=float(similarities[idx]),
                tactical_summary=f"{match.bowling_style} vs {match.batting_approach}"
            ))
        
        return results

# ============================================================================
# ORCHESTRATION: Complete RAG Pipeline
# ============================================================================

class EnterpriseCoachingAssistant:
    """
    Full RAG system: Analyst  Scout  Vector Database  Semantic Retrieval
    """
    
    def __init__(self):
        self.analyst = MatchAnalystLayer()
        self.scout = TacticalEmbeddingScout()
        self.vector_db = VectorMatchDatabase()
    
    def build_knowledge_base(self, match_notes_list: List[str]) -> None:
        """
        Ingest all match intelligence and build the retrieval system.
        This is the one-time setup phase.
        """
        print("📊 ANALYST PHASE: Transcribing match history...")
        for notes in match_notes_list:
            self.analyst.ingest_match_intelligence(notes)
        
        print("🎯 SCOUT PHASE: Creating tactical profiles...")
        all_matches = self.analyst.get_all_matches()
        self.scout.embed_all_matches(all_matches)
        
        print("🗂️  DATABASE PHASE: Organizing by tactical similarity...")
        self.vector_db.index_matches(all_matches)
        
        print(f"✅ Knowledge base ready: {len(all_matches)} matches indexed\n")
    
    def answer_coaching_query(self, query: str, top_k: int = 3) -> None:
        """
        Answer a coaching query by retrieving relevant past matches.
        This is the inference phase.
        """
        print(f"🏏 COACH QUERY: {query}\n")
        
        # Embed the query using the same scout
        query_embedding = self.scout.embedding_model.encode(query)
        
        # Retrieve relevant matches
        relevant_matches = self.vector_db.semantic_search(query_embedding, top_k)
        
        print(f"📚 RETRIEVED {len(relevant_matches)} RELEVANT PAST MATCHES:\n")
        for i, result in enumerate(relevant_matches, 1):
            print(f"  {i}. Match {result.match_id}")
            print(f"     Opposition: {result.opposition_team}")
            print(f"     Player: {result.player_name}")
            print(f"     Tactical Profile: {result.tactical_summary}")
            print(f"     Relevance Score: {result.similarity_score:.3f}\n")

# ============================================================================
# DEMONSTRATION: Building and Querying the System
# ============================================================================

if __name__ == "__main__":
    
    # Historical match notes (what the analyst transcribes)
    match_intelligence = [
        """Opposition: Australia
Player: Jasprit Bumrah
Bowling Style: Fast bowler with yorkers at the death
Batting Approach: Aggressive batting against short-pitched deliveries
Key Vulnerabilities: Struggles against left-arm fast bowlers in overcast conditions
Match Date: 2024-01-15""",
        
        """Opposition: England
Player: Rohit Sharma
Bowling Style: Right-arm fast-medium, conventional swing bowler
Batting Approach: Defensive against moving ball on green wickets
Key Vulnerabilities: Leg-side traps, caught behind in early innings
Match Date: 2024-01-22""",
        
        """Opposition: South Africa
Player: Virat Kohli
Bowling Style: Spin bowling with tight lines
Batting Approach: Off-side heavy against pace bowling
Key Vulnerabilities: Yorkers from fast bowlers in powerplay
Match Date: 2024-02-05""",
        
        """Opposition: Pakistan
Player: Mohammed Shami
Bowling Style: Seam bowling with movement off the deck
Batting Approach: Counter-attack against spinners
Key Vulnerabilities: Short-pitched deliveries from express pace
Match Date: 2024-02-14""",
    ]
    
    # Initialize and build the RAG system
    assistant = EnterpriseCoachingAssistant()
    assistant.build_knowledge_base(match_intelligence)
    
    # Test queries - the coaching staff asking for tactical insights
    queries = [
        "How do we prepare for a fast bowler using yorkers in death overs?",
        "Show me past matches against left-arm swing bowlers in overcast conditions",
        "What tactical adjustments helped against seam bowling attacks?",
    ]
    
    for query in queries:
        assistant.answer_coaching_query(query, top_k=2)
        print("=" * 70 + "\n")

Phase 2 — Feature Completion

Phase 2 extends the pipeline by adding the reranking and LLM generation layers, completing the full RAG system. This phase introduces a CrossEncoderReranker built on sentence-transformers' cross-encoder models, which are more computationally expensive than semantic similarity alone but deliver significantly higher accuracy. A PromptBuilder formats retrieved context into structured prompts for the LLM, while a LLMIntegrator handles calls to production LLM APIs from providers such as OpenAI, Anthropic, or local Llama deployments.

Phase 2 also introduces query expansion to improve retrieval recall. For example, the query 'Bumrah death bowling' is rewritten into semantic variants such as 'Bumrah final overs' and 'Bumrah yorker under pressure.' Additionally, response post-processing extracts citations and confidence scores, and a citation mechanism traces each generated insight back to its source document.

To reduce latency and API costs, Phase 2 incorporates caching for expensive operations, including an LRU cache for embedding lookups and LLM responses to high-frequency queries. Production error handling is also implemented at this stage: graceful fallbacks return the top-k retrieved matches when the LLM is unavailable, retrieval confidence thresholding signals uncertainty when similarity scores fall below acceptable levels, and token usage tracking enables ongoing cost monitoring.

Analogy🏏Cricket
🏏 Think of it like cricket: Phase 1 was the commentator pulling the top-5 relevant highlight clips. Phase 2 is the commentator actually narrating and analyzing those clips to create a compelling, source-cited insight. The **reranker** is like a producer who watches those 5 clips and reorders them by narrative impact: maybe the most recent performance goes first (recency bias), or the performance against a stronger opponent moves up because it's more impressive context. The **query expansion** is like the commentator asking multiple versions of the question to ensure nothing is missed ("How does Bumrah bowl in death overs? What about his yorker? His variations under pressure?") and retrieving clips answering each angle. The **LLM generation** is like the commentator synthesizing those clips into a fluent, insightful narrative: "Jasprit Bumrah's death bowling has evolved significantly. In the 2023 season (cite: MI vs RCB, Apr 15), he demonstrated exceptional control, conceding just 28 runs in 4 overs with 1 wicket. This mirrors his 2022 performance in similar match situations." The **citation mechanism** is the producer's quality control—every fact must trace back to a specific clip. If asked "What's Bumrah's average economy in the final over?", the system either cites a specific match performance or admits "I don't have enough data to confidently answer this." Phase 2 transforms retrieval into knowledge synthesis.
Lesson 35 of 35
0% complete