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

Advanced RAG Patterns Exercise

What You'll Build

In this exercise, you will build a cricket match analysis system using Retrieval-Augmented Generation (RAG). The system combines a vector database of historical cricket scorecard data with a language model to answer complex questions about player performance, match statistics, and tactical patterns.

The core architecture retrieves relevant historical matches from a vector-indexed knowledge base — indexed by batting averages, bowling figures, match conditions, and team compositions — and then uses a large language model to synthesize that retrieved data into coherent analytical responses.

This hands-on project teaches you how to structure domain-specific document embeddings, implement semantic search over structured sports data, and design prompts that leverage retrieved context to generate accurate, contextually aware insights about cricket performance metrics.

Throughout the project, you will work with real player names, match venues, and performance data to understand how RAG systems solve the fundamental problem of grounding LLM responses in factual, retrievable information rather than relying on parametric knowledge alone.

Analogy🏏Cricket
🏏 Think of it like cricket: Imagine you're a cricket analyst preparing for a match commentary. Before the game, you've collected match scorecards, player performance history, pitch reports, and weather data from the stadium—your 'knowledge base.' When a question comes up during live commentary, like 'Has Rohit Sharma ever scored a century here before?', you don't rely on memory alone (which might be fuzzy or incomplete). Instead, you quickly flip through your indexed filing system (the vector database) to retrieve the most relevant match records from that venue, check Rohit's actual performances there, and synthesize your answer with hard facts. The retrieval step is like scanning your organized files to find the right scorecards; the ranking is like prioritizing the most recent or relevant matches; the augmentation is the language model synthesizing a coherent, authoritative answer from those retrieved facts. Without RAG, you'd guess and risk broadcasting incorrect statistics—with RAG, you ground every answer in verified match data, just as professional analysts do with their research notes during live commentary.

Prerequisites

  • Solid understanding of embeddings and vector similarity (cosine distance, Euclidean distance) from RAG fundamentals.
  • Working knowledge of Python with libraries like numpy, pandas for data manipulation and numeric operations.
  • Familiarity with JSON data structures and document-based indexing for storing and querying structured records.
  • Basic understanding of LLM prompt engineering and how context windows are filled with retrieved documents.
  • Access to OpenAI API or compatible LLM service; faiss, langchain, or similar retrieval libraries installed locally.

Setup & Project Structure

Begin by initializing a Python project with a cricket-themed folder structure that organizes your knowledge base, retrieval logic, and generation pipeline into distinct modules. The project separates concerns across three layers: a data layer for match scorecards, a retrieval layer for the vector index and search logic, and a generation layer for prompt construction and LLM calls.

Analogy🏏Cricket
🏏 Think of it like cricket: a professional team is not one crowd doing everything — it has a groundstaff preparing the pitch, an analytics unit crunching data, a selection panel picking the eleven, and a captain making the final call on the field. Your RAG project's four layers mirror this division of labour. Just as the groundstaff ingest and prepare the surface, your data layer loads and embeds the cricket match JSON. Just as the analytics unit searches records for patterns, your retrieval layer runs vector similarity to find relevant matches. Just as the selection panel narrows a longlist to the best eleven, your ranking layer reranks candidates for quality. And just as the captain synthesises everything into a match plan, your generation layer prompts the LLM with the retrieved context. The payoff of keeping these roles separate is the same reason clubs don't ask the groundsman to bat at three: each concern can be tested and improved independently without breaking the others.

For the underlying tooling, you will use a vector database such as FAISS to store embeddings of cricket match records, a document loader to parse scorecard JSON data, and a query engine that chains retrieval with LLM-based synthesis. Install dependencies covering embedding generation via sentence-transformers, vector indexing via faiss-cpu, language model interaction via openai or langchain, and data handling via pandas and json.

This modular approach mirrors production RAG systems where retrieval and generation are deliberately decoupled, allowing each pipeline stage to be independently optimized without affecting the others.

bash
#!/bin/bash
# Cricket RAG System - Project Setup

# Create project structure
mkdir -p cricket-rag-system/{data,retrievers,generators,indexes,outputs}
cd cricket-rag-system

# Create Python virtual environment
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install required dependencies
pip install --upgrade pip
pip install openai==1.3.0
pip install sentence-transformers==2.2.2
pip install faiss-cpu==1.7.4
pip install pandas==2.0.3
pip install numpy==1.24.3
pip install python-dotenv==1.0.0
pip install langchain==0.0.300
pip install pydantic==2.0.0

# Create .env file for API keys
echo 'OPENAI_API_KEY="your-api-key-here"' > .env

# Create directory structure with init files
touch retrievers/__init__.py
touch generators/__init__.py
touch data/cricket_scorecards.json
touch main.py

echo "✓ Cricket RAG System initialized successfully"
echo "✓ Project structure created at: $(pwd)"
echo "✓ Virtual environment activated"
echo "✓ Dependencies installed"
echo "\nNext: Configure OPENAI_API_KEY in .env file"

Step 1 — Foundation

Build the foundational data layer by creating cricket match records as structured documents, each containing scorecard information, player statistics, match conditions, and venue details. These documents form your knowledge base — the source material that the retrieval system will search through when answering queries.

Each document must be serializable to JSON and include both metadata fields — such as match_id, teams, venue, date, and format — and performance data such as individual player stats, innings summaries, and bowling figures. This structured representation ensures the retrieval system has sufficient context to match queries accurately.

Next, use the sentence-transformers library to generate dense vector embeddings from these documents, converting textual descriptions of match performances into high-dimensional vectors. The embedding model learns to place semantically similar matches close together in vector space — for example, two Test matches at the MCG with similar batting conditions would cluster near one another.

This foundation is critical because the quality of your embeddings directly determines the retrieval system's ability to surface relevant historical matches when answering new queries about cricket performance. Poor embeddings at this stage will degrade every subsequent component in the pipeline.

Analogy🏏Cricket
🏏 Think of it like cricket: Before a cricket tournament, the organizers must catalog every match—they assign each match a unique identity card that captures essential information: teams, date, venue, winning margin, key player performances, and significant moments. This is like your MatchDocument structure. Then they assign each match a numeric 'fingerprint' (embedding) based on its characteristics—two similar matches (like India vs Pakistan in bilateral ODI at the same venue) get similar fingerprints, while different matches (India vs USA in T20) get distinct fingerprints. Just as scorecards use numerical encoding to store all match data compactly, embeddings compress match semantics into dense vectors. The batch embedding process is like creating a centralized digital archive of all match fingerprints rather than computing them one-by-one, which saves time during tournament season. Without this foundational fingerprinting step, you couldn't quickly find 'all matches where Rohit Sharma scored a century under similar conditions'—you'd have to read every scorecard manually.
python
# data/cricket_scorecards.json - Sample cricket match knowledge base
# This file should be placed in the data/ directory

import json
from typing import List, Dict
from dataclasses import dataclass, asdict
from datetime import datetime

@dataclass
class PlayerStatistic:
    """Individual player performance in a match"""
    player_name: str
    role: str  # batsman, bowler, all-rounder
    runs_scored: int = 0
    balls_faced: int = 0
    fours: int = 0
    sixes: int = 0
    wickets_taken: int = 0
    balls_bowled: int = 0
    runs_conceded: int = 0
    
    def get_batting_average(self) -> float:
        """Calculate batting average"""
        return self.runs_scored / max(1, self.balls_faced)
    
    def get_economy_rate(self) -> float:
        """Calculate bowling economy (runs per over)"""
        overs = self.balls_bowled / 6 if self.balls_bowled > 0 else 1
        return self.runs_conceded / overs

@dataclass
class InningsRecord:
    """Single innings performance"""
    team_name: str
    total_runs: int
    total_wickets: int
    overs_batted: float
    batting_lineup: List[str]
    bowling_attack: List[str]

@dataclass
class CricketMatchRecord:
    """Complete match information for vector indexing"""
    match_id: str
    match_date: str
    venue: str
    country: str
    format: str  # Test, ODI, T20
    team_1: str
    team_2: str
    team_1_innings: InningsRecord
    team_2_innings: InningsRecord
    result: str
    match_winner: str
    player_performances: List[PlayerStatistic]
    toss_winner: str
    toss_decision: str  # bat, bowl
    weather: str
    pitch_condition: str
    
    def to_document(self) -> str:
        """Convert match record to searchable text document"""
        doc = f"""Match ID: {self.match_id}
Date: {self.match_date}
Venue: {self.venue}, {self.country}
Format: {self.format}

Teams: {self.team_1} vs {self.team_2}

{self.team_1} Innings: {self.team_1_innings.total_runs}/{self.team_1_innings.total_wickets} ({self.team_1_innings.overs_batted} overs)
Key Batsmen: {', '.join(self.team_1_innings.batting_lineup)}
Key Bowlers: {', '.join(self.team_1_innings.bowling_attack)}

{self.team_2} Innings: {self.team_2_innings.total_runs}/{self.team_2_innings.total_wickets} ({self.team_2_innings.overs_batted} overs)
Key Batsmen: {', '.join(self.team_2_innings.batting_lineup)}
Key Bowlers: {', '.join(self.team_2_innings.bowling_attack)}

Result: {self.match_winner} won
Toss: {self.toss_winner} won toss, chose to {self.toss_decision}
Weather: {self.weather}
Pitch: {self.pitch_condition}

Notable Performances:
"""
        for perf in self.player_performances:
            if perf.role == 'batsman':
                doc += f"- {perf.player_name}: {perf.runs_scored} runs off {perf.balls_faced} balls ({perf.fours}x4, {perf.sixes}x6)\n"
            elif perf.role == 'bowler':
                doc += f"- {perf.player_name}: {perf.wickets_taken}/{perf.runs_conceded} from {perf.balls_bowled//6} overs (economy: {perf.get_economy_rate():.2f})\n"
        return doc

# Sample cricket match records
sample_matches = [
    CricketMatchRecord(
        match_id="IND_AUS_MCG_2023_01",
        match_date="2023-12-26",
        venue="Melbourne Cricket Ground (MCG)",
        country="Australia",
        format="Test",
        team_1="India",
        team_2="Australia",
        team_1_innings=InningsRecord(
            team_name="India",
            total_runs=191,
            total_wickets=6,
            overs_batted=58.3,
            batting_lineup=["Rohit Sharma", "Shubman Gill", "Virat Kohli"],
            bowling_attack=["Jasprit Bumrah", "Mohammed Shami"]
        ),
        team_2_innings=InningsRecord(
            team_name="Australia",
            total_runs=247,
            total_wickets=8,
            overs_batted=72.0,
            batting_lineup=["Steve Smith", "Travis Head", "Marnus Labuschagne"],
            bowling_attack=["Scott Boland", "Mitchell Starc"]
        ),
        result="Australia won by 2 wickets",
        match_winner="Australia",
        player_performances=[
            PlayerStatistic(player_name="Rohit Sharma", role="batsman", runs_scored=47, balls_faced=89, fours=4, sixes=0),
            PlayerStatistic(player_name="Virat Kohli", role="batsman", runs_scored=52, balls_faced=97, fours=6, sixes=1),
            PlayerStatistic(player_name="Jasprit Bumrah", role="bowler", wickets_taken=2, balls_bowled=132, runs_conceded=38),
            PlayerStatistic(player_name="Steve Smith", role="batsman", runs_scored=68, balls_faced=141, fours=7, sixes=0),
            PlayerStatistic(player_name="Scott Boland", role="bowler", wickets_taken=3, balls_bowled=144, runs_conceded=41),
        ],
        toss_winner="India",
        toss_decision="bat",
        weather="Overcast, cool conditions",
        pitch_condition="Green top, favorable to fast bowling"
    ),
    CricketMatchRecord(
        match_id="IND_AUS_WANKHEDE_2023_02",
        match_date="2023-02-09",
        venue="Wankhede Stadium",
        country="India",
        format="Test",
        team_1="India",
        team_2="Australia",
        team_1_innings=InningsRecord(
            team_name="India",
            total_runs=326,
            total_wickets=7,
            overs_batted=95.0,
            batting_lineup=["Rohit Sharma", "Shubman Gill", "Virat Kohli"],
            bowling_attack=["Jasprit Bumrah", "Ravindra Jadeja"]
        ),
        team_2_innings=InningsRecord(
            team_name="Australia",
            total_runs=197,
            total_wickets=10,
            overs_batted=64.0,
            batting_lineup=["David Warner", "Steve Smith"],
            bowling_attack=["Pat Cummins", "Cameron Green"]
        ),
        result="India won by 129 runs",
        match_winner="India",
        player_performances=[
            PlayerStatistic(player_name="Rohit Sharma", role="batsman", runs_scored=94, balls_faced=176, fours=11, sixes=2),
            PlayerStatistic(player_name="Virat Kohli", role="batsman", runs_scored=78, balls_faced=153, fours=9, sixes=0),
            PlayerStatistic(player_name="Ravindra Jadeja", role="bowler", wickets_taken=4, balls_bowled=156, runs_conceded=37),
            PlayerStatistic(player_name="Steve Smith", role="batsman", runs_scored=42, balls_faced=98, fours=4, sixes=0),
            PlayerStatistic(player_name="Pat Cummins", role="bowler", wickets_taken=2, balls_bowled=120, runs_conceded=51),
        ],
        toss_winner="Australia",
        toss_decision="bowl",
        weather="Clear, warm conditions",
        pitch_condition="Turning pitch, favorable to spinners"
    ),
]

def save_cricket_scorecards(filename: str = "data/cricket_scorecards.json"):
    """Save cricket match records to JSON file"""
    records = [asdict(match) for match in sample_matches]
    with open(filename, 'w') as f:
        json.dump(records, f, indent=2)
    print(f"✓ Saved {len(records)} cricket matches to {filename}")

def load_cricket_scorecards(filename: str = "data/cricket_scorecards.json") -> List[CricketMatchRecord]:
    """Load cricket match records from JSON file"""
    with open(filename, 'r') as f:
        records = json.load(f)
    print(f"✓ Loaded {len(records)} cricket matches from {filename}")
    return records

if __name__ == "__main__":
    # Initialize sample data
    save_cricket_scorecards()
    print("\n--- Sample Match Document ---")
    print(sample_matches[0].to_document())

Step 2 — Core Logic

Implement the retrieval layer by building a vector index of your cricket match documents and creating a semantic search function that ranks matches by relevance to a given user query. Load your match records, generate embeddings for each document's text representation using a pre-trained sentence transformer model, and store those embeddings in a FAISS index for efficient similarity search.

The retrieval function works by taking a user query — for example, 'How has Rohit Sharma performed at the MCG against fast bowling?' — embedding it using the same model used to index the documents, and then retrieving the top-k most similar match documents from the FAISS index using cosine similarity.

This approach enables semantic matching that goes beyond simple keyword overlap. The system understands, for instance, that a query referencing 'bowling speed above 140 km/h' is semantically similar to documents describing 'fast bowling' or 'pace attacks,' even when exact keywords do not appear in both.

The matches returned by this retrieval step become the context passed to the language model in the next stage, grounding its response in actual historical data rather than relying on general knowledge.

Analogy🏏Cricket
🏏 Think of it like cricket: Consider a cricket statistician preparing for commentary before India plays New Zealand at the Wankhede Stadium. A viewer asks, "How does Virat Kohli perform at home against New Zealand bowlers?" The statistician doesn't just do a simple grep search for 'Virat Kohli' and 'New Zealand'—that would pull up irrelevant matches like Kohli's T20 performance against NZ in Dubai. Instead, they understand the semantic intent: we need home Test matches or ODIs where Kohli faced NZ pace bowling in comparable conditions. They mentally rank candidates: matches at Wankhede itself rank highest, recent performances rank higher than 10-year-old data, and actual head-to-head ODIs rank higher than practice matches. This is exactly what the core logic does: semantic search understands your query's intent (not just keywords), and intelligent ranking orders results by relevance factors (venue, recency, format, player presence). The reranker acts like the statistician's domain knowledge—it refines rough search results into precisely-ranked candidates. Without this step, you might retrieve matches with excellent semantic similarity but low practical relevance.
python
# retrievers/cricket_retriever.py
# Semantic retrieval engine for cricket match database

import json
import numpy as np
from typing import List, Tuple, Dict
from sentence_transformers import SentenceTransformer
import faiss
from dataclasses import dataclass

@dataclass
class RetrievedMatch:
    """Match record with relevance score"""
    match_id: str
    venue: str
    teams: str
    document_text: str
    relevance_score: float

class CricketRetriever:
    """Semantic search engine for cricket match knowledge base"""
    
    def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
        """Initialize retriever with embedding model"""
        # Load pre-trained embedding model (lightweight, ~80MB)
        self.embedding_model = SentenceTransformer(model_name)
        self.embedding_dimension = self.embedding_model.get_sentence_embedding_dimension()
        
        # Initialize FAISS index (L2 distance)
        self.index = faiss.IndexFlatL2(self.embedding_dimension)
        
        # Storage for match metadata
        self.match_metadata = []
        self.match_documents = []
        self.embeddings = None
        
        print(f"✓ CricketRetriever initialized with {model_name}")
        print(f"✓ Embedding dimension: {self.embedding_dimension}")
    
    def ingest_cricket_records(self, filepath: str = "data/cricket_scorecards.json"):
        """Load cricket match records and create embeddings"""
        with open(filepath, 'r') as f:
            records = json.load(f)
        
        print(f"\n📚 Ingesting {len(records)} cricket matches...")
        
        # Convert records to documents and generate embeddings
        documents = []
        for record in records:
            # Create searchable document from match record
            doc_text = self._create_document_text(record)
            documents.append(doc_text)
            
            # Store metadata
            self.match_metadata.append({
                'match_id': record['match_id'],
                'venue': record['venue'],
                'date': record['match_date'],
                'teams': f"{record['team_1']} vs {record['team_2']}",
                'format': record['format'],
                'winner': record['match_winner'],
                'country': record['country']
            })
        
        self.match_documents = documents
        
        # Generate embeddings for all documents
        print(f"🔄 Generating embeddings for {len(documents)} matches...")
        self.embeddings = self.embedding_model.encode(
            documents,
            normalize_embeddings=True,
            show_progress_bar=False
        )
        
        # Add embeddings to FAISS index
        self.index.add(self.embeddings.astype(np.float32))
        
        print(f"✓ Created FAISS index with {len(self.embeddings)} vectors")
        print(f"✓ Index size in memory: {self.index.ntotal} match records")
    
    def retrieve_matches(
        self,
        query: str,
        top_k: int = 3,
        similarity_threshold: float = 0.0
    ) -> List[RetrievedMatch]:
        """Retrieve most relevant cricket matches for a query"""
        # Embed the query
        query_embedding = self.embedding_model.encode(
            [query],
            normalize_embeddings=True
        )
        
        # Search FAISS index (returns distances, not scores)
        distances, indices = self.index.search(
            query_embedding.astype(np.float32),
            min(top_k, len(self.match_documents))
        )
        
        # Convert distances to similarity scores
        # For L2 distance: similarity ≈ 1 / (1 + distance)
        results = []
        for distance, idx in zip(distances[0], indices[0]):
            if idx == -1:  # Invalid result from FAISS
                continue
            
            # Convert L2 distance to similarity score (0-1)
            similarity_score = 1 / (1 + distance)
            
            if similarity_score >= similarity_threshold:
                results.append(RetrievedMatch(
                    match_id=self.match_metadata[idx]['match_id'],
                    venue=self.match_metadata[idx]['venue'],
                    teams=self.match_metadata[idx]['teams'],
                    document_text=self.match_documents[idx],
                    relevance_score=float(similarity_score)
                ))
        
        return results
    
    def _create_document_text(self, record: Dict) -> str:
        """Convert match record to searchable document"""
        doc = f"""
        Match: {record['team_1']} vs {record['team_2']}
        Date: {record['match_date']}
        Venue: {record['venue']}, {record['country']}
        Format: {record['format']}
        
        {record['team_1']} Innings: {record['team_1_innings']['total_runs']}/{record['team_1_innings']['total_wickets']} ({record['team_1_innings']['overs_batted']} overs)
        Batsmen: {', '.join(record['team_1_innings']['batting_lineup'])}
        Bowlers: {', '.join(record['team_1_innings']['bowling_attack'])}
        
        {record['team_2']} Innings: {record['team_2_innings']['total_runs']}/{record['team_2_innings']['total_wickets']} ({record['team_2_innings']['overs_batted']} overs)
        Batsmen: {', '.join(record['team_2_innings']['batting_lineup'])}
        Bowlers: {', '.join(record['team_2_innings']['bowling_attack'])}
        
        Result: {record['match_winner']} won. Toss: {record['toss_winner']} won, chose to {record['toss_decision']}
        Weather: {record['weather']}
        Pitch: {record['pitch_condition']}
        """
        return " ".join(doc.split())  # Normalize whitespace
    
    def get_retrieval_stats(self) -> Dict:
        """Return retriever statistics"""
        return {
            'total_matches_indexed': len(self.match_documents),
            'embedding_model': self.embedding_model.get_sentence_embedding_dimension(),
            'index_size': self.index.ntotal,
            'vector_dimension': self.embedding_dimension
        }

if __name__ == "__main__":
    # Test retriever
    retriever = CricketRetriever()
    retriever.ingest_cricket_records()
    
    # Example queries
    test_queries = [
        "How did Rohit Sharma perform at the MCG?",
        "What was the result when India played Australia on a turning pitch?",
        "Bowling performance by Jasprit Bumrah in Test cricket"
    ]
    
    print("\n🏏 Testing Cricket Retriever\n" + "="*50)
    for query in test_queries:
        print(f"\n📋 Query: {query}")
        print("-" * 50)
        
        matches = retriever.retrieve_matches(query, top_k=2)
        
        for i, match in enumerate(matches, 1):
            print(f"\nResult {i} (Score: {match.relevance_score:.3f})")
            print(f"Match: {match.teams}")
            print(f"Venue: {match.venue}")
            print(f"Document excerpt: {match.document_text[:150]}...")

Step 3 — Integration & Enhancement

Complete the RAG pipeline by implementing a generation layer that combines retrieved cricket match context with an LLM to produce informed, grounded responses. Design a prompt template that explicitly incorporates the retrieved match documents as context, ensuring the LLM bases its analysis on historical data rather than generating generic answers.

The integration step chains the retriever and generator into a single flow. When a user asks a cricket question, the system first retrieves relevant matches from the vector index, then constructs a prompt that includes both the user's question and the retrieved context formatted as a numbered list of relevant historical matches with key statistics, and finally passes this augmented prompt to an LLM.

The LLM uses the provided context to synthesize a detailed response that references specific players, venues, performance metrics, and outcomes. This approach directly addresses the hallucination problem common in standard LLMs — by constraining the model to answer based only on retrieved factual data, responses become verifiable and the risk of fabricated statistics is substantially reduced.

Analogy🏏Cricket
🏏 Think of it like cricket: The retrieval system (Step 2) found relevant match records, but now the commentator must synthesize these raw statistics into a coherent narrative for the broadcast audience. The commentator receives indexed research cards with match data, player statistics, and key moments, then crafts a compelling narrative: "When Virat Kohli faced Mitchell Starc at the MCG in 2023 [retrieved fact], he employed an aggressive approach [synthesis from context], scoring 85 off 95 balls [retrieved statistic], which set up India's successful chase [retrieved outcome]." The commentator carefully attributes each claim to its source—'according to the 2023 MCG match' or 'in that particular series'—so the audience knows where information comes from. If the research suggests Kohli scored 85 but the commentator accidentally says 95, that's a hallucination that must be caught. This synthesis process is what Step 3 does: the language model reads retrieved context and generates fluent, attributable answers rather than relying on generic pre-training knowledge. Understanding this integration reveals why RAG quality depends on both retrieval precision AND language model grounding—a brilliant response that contradicts retrieved facts is worse than a mediocre response that stays faithful to context.
python
# generators/cricket_analyzer.py
# LLM-based analysis engine using retrieved cricket context

import os
from typing import List, Dict
from dotenv import load_dotenv
from openai import OpenAI
from retrievers.cricket_retriever import CricketRetriever, RetrievedMatch

load_dotenv()

class CricketAnalyzer:
    """RAG generator: Synthesizes retrieved cricket context into analytical responses"""
    
    def __init__(self, api_key: str = None):
        """Initialize analyzer with OpenAI client"""
        self.api_key = api_key or os.getenv('OPENAI_API_KEY')
        if not self.api_key:
            raise ValueError("OPENAI_API_KEY not found in environment")
        
        self.client = OpenAI(api_key=self.api_key)
        self.model = "gpt-3.5-turbo"
        self.temperature = 0.7  # Moderate creativity for analysis
        
        print(f"✓ CricketAnalyzer initialized with {self.model}")
    
    def analyze_cricket_query(
        self,
        query: str,
        retrieved_matches: List[RetrievedMatch],
        include_stats: bool = True
    ) -> Dict:
        """Generate cricket analysis based on retrieved context"""
        
        # Format retrieved matches into context string
        context = self._format_context(retrieved_matches, include_stats)
        
        # Build the augmented prompt
        system_prompt = """
        You are an expert cricket analyst with deep knowledge of match statistics, player performances, and tactical patterns.
        Your task is to answer cricket questions based ONLY on the provided historical match data.
        
        Instructions:
        1. Ground all responses in the provided contextcite specific matches, dates, venues, and statistics
        2. If the context doesn't contain information to answer the question, explicitly state that
        3. Provide quantitative insights (averages, economy rates, strike rates) when relevant
        4. Explain tactical patterns or performance trends observed across matches
        5. Keep responses concise but detailed (2-4 paragraphs)
        """
        
        user_message = f"""
        HISTORICAL CONTEXT (Retrieved Matches):
        {context}
        
        USER QUESTION:
        {query}
        
        Please analyze this question based on the historical match data provided above.
        """
        
        # Call LLM with augmented context
        response = self.client.chat.completions.create(
            model=self.model,
            temperature=self.temperature,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_message}
            ]
        )
        
        analysis = response.choices[0].message.content
        
        return {
            'query': query,
            'analysis': analysis,
            'context_matches_used': len(retrieved_matches),
            'model': self.model,
            'retrieved_match_ids': [m.match_id for m in retrieved_matches]
        }
    
    def _format_context(self, matches: List[RetrievedMatch], include_stats: bool) -> str:
        """Format retrieved matches into context for LLM"""
        if not matches:
            return "No relevant historical matches found."
        
        context_lines = []
        for i, match in enumerate(matches, 1):
            relevance = match.relevance_score
            match_info = f"""
            Match {i} (Relevance: {relevance:.1%})
            ID: {match.match_id}
            Teams: {match.teams}
            Venue: {match.venue}
            
            Details:
            {match.document_text}
            """
            context_lines.append(" ".join(match_info.split()))
        
        return "\n" + "\n".join(context_lines)
    
    def generate_match_summary(
        self,
        match_id: str,
        match_text: str
    ) -> str:
        """Generate a narrative summary of a cricket match"""
        prompt = f"""
        Based on the following cricket match data, write a 2-3 sentence narrative summary highlighting key moments and performances:
        
        {match_text}
        """
        
        response = self.client.chat.completions.create(
            model=self.model,
            temperature=0.6,
            messages=[
                {"role": "user", "content": prompt}
            ]
        )
        
        return response.choices[0].message.content

class RAGPipeline:
    """Complete RAG system: Retrieval + Augmented + Generation"""
    
    def __init__(self):
        """Initialize both retriever and analyzer"""
        self.retriever = CricketRetriever()
        self.analyzer = CricketAnalyzer()
        self.retriever.ingest_cricket_records()
    
    def query(self, question: str, top_k: int = 3) -> Dict:
        """Execute full RAG pipeline: retrieve → augment → generate"""
        print(f"\n🏏 Processing Query: {question}")
        print("="*60)
        
        # Step 1: Retrieve relevant matches
        print(f"\n📚 Step 1: Retrieving top {top_k} relevant matches...")
        retrieved = self.retriever.retrieve_matches(question, top_k=top_k)
        
        if not retrieved:
            return {
                'question': question,
                'analysis': 'No relevant cricket matches found in knowledge base.',
                'retrieved_matches': []
            }
        
        print(f"✓ Retrieved {len(retrieved)} matches")
        for i, match in enumerate(retrieved, 1):
            print(f"  {i}. {match.match_id} ({match.relevance_score:.1%})")
        
        # Step 2: Generate analysis using retrieved context
        print(f"\n🤖 Step 2: Generating analysis with LLM...")
        result = self.analyzer.analyze_cricket_query(question, retrieved)
        
        print(f"✓ Analysis generated using {self.analyzer.model}")
        
        return result

if __name__ == "__main__":
    # Initialize RAG pipeline
    rag_pipeline = RAGPipeline()
    
    # Test questions
    test_questions = [
        "How has Rohit Sharma performed when playing against Australia at different venues?",
        "What tactical patterns emerge from India's recent Test matches?",
        "Analyze Jasprit Bumrah's bowling effectiveness across different match conditions"
    ]
    
    print("\n🏏 CRICKET ANALYSIS RAG SYSTEM\n" + "="*60)
    
    for question in test_questions:
        result = rag_pipeline.query(question, top_k=2)
        
        print(f"\n📊 ANALYSIS RESULT:")
        print("-" * 60)
        print(f"Question: {result['query']}")
        print(f"\nAnalysis:\n{result['analysis']}")
        print(f"\n✓ Used {result['context_matches_used']} retrieved matches")
        print(f"✓ Matches: {', '.join(result['retrieved_match_ids'])}")
        print("="*60)

Step 4 — Testing & Verification

Run the complete RAG pipeline end-to-end to verify that retrieval, augmentation, and generation work together correctly. Execute test queries against your cricket knowledge base and validate that the system retrieves relevant historical matches and generates coherent, factually grounded analysis.

Analogy🏏Cricket
🏏 Think of it like cricket: before a plan is trusted, a team plays a full warm-up fixture that runs every phase together — openers see the new ball, bowlers find rhythm, fielders hold catches — to expose faults while they are cheap to fix. Your Step 4 is that fixture. Just as a practice game exercises the whole team in sequence rather than in isolated nets, the test harness runs ingestion, retrieval, ranking, and generation end to end on sample cricket queries. Just as coaches log how each phase performed to spot the weak link, the harness logs intermediate results at every stage so you can see exactly where behaviour drifts. Passing individual nets never guarantees a winning eleven — only the full fixture reveals how the parts combine. The payoff is match-ready confidence: a pipeline proven to move a query cleanly from raw match data all the way to a grounded answer before it faces real users.

During validation, confirm that retrieved matches carry high relevance scores and that the LLM's output explicitly references the retrieved data, including specific statistics and match details. Also monitor pipeline latency — measuring how long retrieval and generation each take — to ensure the system remains responsive under realistic query loads.

Finally, verify that the system handles edge cases gracefully, such as queries that return no relevant matches or references to ambiguous player and venue names. Robust handling of these scenarios is essential before considering the pipeline production-ready.

bash
#!/bin/bash
# test_rag_pipeline.sh - End-to-end testing script

set -e  # Exit on error

echo "🏏 CRICKET RAG SYSTEM - TESTING SUITE"
echo "====================================="
echo ""

# Activate virtual environment
source venv/bin/activate

# Test 1: Data initialization
echo "Test 1: Loading cricket scorecard data..."
python3 << 'EOF'
from data.cricket_scorecards import save_cricket_scorecards, load_cricket_scorecards

save_cricket_scorecards()
records = load_cricket_scorecards()

print(f"✓ Data initialized: {len(records)} matches loaded")
for record in records:
    print(f"  - Match ID: {record['match_id']}")
    print(f"    Teams: {record['team_1']} vs {record['team_2']}")
    print(f"    Venue: {record['venue']}")
    print(f"    Winner: {record['match_winner']}")
EOF

echo ""
echo "Test 2: Testing retriever on sample queries..."
python3 << 'EOF'
from retrievers.cricket_retriever import CricketRetriever
import time

retriever = CricketRetriever()
retriever.ingest_cricket_records()

test_queries = [
    "Rohit Sharma at the MCG",
    "India vs Australia Test match",
    "Fast bowling performance by Jasprit Bumrah"
]

for query in test_queries:
    print(f"\nQuery: '{query}'")
    start = time.time()
    matches = retriever.retrieve_matches(query, top_k=2)
    elapsed = time.time() - start
    
    print(f"✓ Retrieved {len(matches)} matches in {elapsed*1000:.1f}ms")
    for match in matches:
        print(f"  - {match.match_id} (score: {match.relevance_score:.3f})")
EOF

echo ""
echo "Test 3: Full RAG pipeline test..."
python3 << 'EOF'
from generators.cricket_analyzer import RAGPipeline

print("Initializing RAG pipeline...")
rag = RAGPipeline()

test_question = "How has Rohit Sharma performed at the MCG?"
print(f"\nTest Question: {test_question}")
print("-" * 50)

result = rag.query(test_question, top_k=2)

print(f"\n✓ RAG Pipeline Execution Successful")
print(f"✓ Analysis generated with {result['context_matches_used']} context matches")
print(f"\nGenerated Analysis:")
print(result['analysis'])
EOF

echo ""
echo "Test 4: Retrieval metrics..."
python3 << 'EOF'
from retrievers.cricket_retriever import CricketRetriever

retriever = CricketRetriever()
retriever.ingest_cricket_records()

stats = retriever.get_retrieval_stats()
print("\nRetriever Statistics:")
for key, value in stats.items():
    print(f"  {key}: {value}")
EOF

echo ""
echo "====================================="
echo "✓ ALL TESTS PASSED"
echo "====================================="
echo ""
echo "Expected Output Summary:"
echo "  ✓ 2 cricket matches loaded from data/cricket_scorecards.json"
echo "  ✓ Retrieval latency < 100ms for semantic search"
echo "  ✓ Retrieved matches have relevance scores 0.6-0.95"
echo "  ✓ LLM generates coherent analysis with match references"
echo "  ✓ No hallucinated statistics (all references to retrieved data)"
echo ""
echo "Next Steps:"
echo "  1. Expand cricket_scorecards.json with more historical matches"
echo "  2. Tune embedding model for cricket domain"
echo "  3. Add player-specific retrieval patterns"
echo "  4. Implement caching for frequently asked questions"

Warning: The most common error is mismatched embedding dimensions between your stored vectors and query embeddings. This occurs when you switch embedding models or reload a FAISS index that was built with a different model. Always ensure you use the SAME sentence-transformer model when building the index and when encoding new queries. If you change models, you MUST rebuild the entire FAISS index. Additionally, if your OPENAI_API_KEY is not set correctly in the .env file, the LLM integration will fail silently. Always verify your API key is valid by running a quick test call before running the full pipeline. If you get 'index.ntotal = 0', it means embeddings weren't added to FAISS—check that ingest_cricket_records() was called and completed without errors.

Extension Challenge: Enhance your cricket RAG system with multi-document retrieval and cross-match analysis. Instead of returning individual matches, implement a "match similarity clustering" feature that groups retrieved matches by tactical pattern (e.g., "matches where India was chasing on turning pitches"). Then, have the LLM generate comparative analysis across these clusters: "In 3 similar chasing scenarios on turning pitches, India's win rate was X%, with Virat Kohli averaging Y runs." This requires (1) post-processing retrieved matches to identify common tactical features, (2) prompting the LLM to synthesize patterns across multiple matches, and (3) generating insights about win probabilities and player performance trends. This transforms your RAG system from fact retrieval into tactical intelligence generation—a much higher-value use case in professional cricket analytics.

  • Retrieval-Augmented Generation grounds LLM responses in factual, retrievable context, reducing hallucinations by constraining output to provided documents.
  • Vector embeddings enable semantic search by converting textual match records into numerical representations where similar cricket scenarios cluster together geometrically.
  • FAISS indexing provides millisecond-latency retrieval across millions of documents using efficient nearest-neighbor algorithms, critical for production RAG systems.
  • Augmented prompts explicitly include retrieved context as formatted context blocks, ensuring the LLM synthesizes analysis directly from historical data rather than parameter space.
  • Domain-specific embeddings (trained on cricket terminology and statistics) outperform general embeddings, improving retrieval precision for specialized queries like player performance patterns.
  • RAG pipeline reduces API costs and hallucinations compared to fine-tuning large LLMs, making it the preferred approach for knowledge-intensive applications with rapidly updating source data.
Lesson 25 of 35
0% complete