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.
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.
# 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.