Skip to main content

ai-data-engineering-rag-pipeline

Build production-grade local RAG pipelines with BM25 search, hierarchical chunking, and retrieval evaluation from Nahid's hands-on roadmap

Jump to install

Source facts

Repository
reason-machines/data-skills
Last source activity
July 31, 2026 at 11:49
Detected SKILL.md language
English
Stars
5
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ai-data-engineering-rag-pipeline
description
Build production-grade local RAG pipelines with BM25 search, hierarchical chunking, and retrieval evaluation from Nahid's hands-on roadmap
triggers
["how do I build a RAG pipeline from scratch","implement BM25 baseline search engine","create hierarchical chunking for document retrieval","evaluate retrieval performance with Recall@10","design retrieval contracts and golden datasets","set up local RAG with chunk granularity analysis","build inverted index for document search","implement parent-child metadata linkage for chunks"]
# AI Data Engineering RAG Pipeline > Skill by [ara.so](https://ara.so) — Data Skills collection. This skill helps you build production-grade local RAG (Retrieval-Augmented Generation) pipelines using the architectural patterns and implementations from Nahid Mahmud's AI & Data Engineering Roadmap. The project focuses on baseline search engines, hierarchical chunking strategies, and rigorous evaluation methodologies. ## What This Project Provides - **Day 01**: Retrieval contract design, governed corpus creation, golden dataset (`questions.jsonl`), and evaluation frameworks - **Day 02**: BM25 baseline search engine with inverted index, Okapi BM25 ranking, and Recall@10 evaluation - **Day 03**: RAG pipeline with hierarchical chunking (document, section, paragraph levels), deterministic chunk IDs, parent-child metadata linkage, and failure mode analysis ## Installation & Setup Clone the repository: ```bash git clone https://github.com/Nahid-mahmud555/ai-data-engineering-roadmap.git cd ai-data-engineering-roadmap ``` Install dependencies (each day has its own requirements): ```bash # For Day 02 (BM25) cd Day_02 pip install -r requirements.txt # For Day 03 (RAG Pipeline) cd Day_03 pip install -r requirements.txt ``` Common dependencies across modules: - `numpy` - numerical operations - `rank-bm25` - BM25 implementation - `sentence-transformers` - embeddings (Day 03+) - `faiss-cpu` - vector search (Day 03+) ## Project Structure ``` ai-data-engineering-roadmap/ ├── Day_01/ # Retrieval contracts & golden datasets ├── Day_02/ # BM25 baseline search engine │ ├── baseline_bm25.py │ ├── corpus/ # Document collection │ └── questions.jsonl ├── Day_03/ # RAG pipeline with hierarchical chunking │ ├── pipeline.py │ ├── corpus/ # Multi-domain documents │ └── questions.jsonl ``` ## Day 02: BM25 Baseline Search Engine ### Core Implementation The BM25 baseline provides a deterministic search engine for retrieval evaluation: ```python from rank_bm25 import BM25Okapi import json # Load corpus corpus_docs = [] doc_ids = [] # Assuming corpus stored in text files import os corpus_dir = "corpus" for filename in os.listdir(corpus_dir): if filename.endswith(".txt"): with open(os.path.join(corpus_dir, filename), 'r') as f: content = f.read() corpus_docs.append(content) doc_ids.append(filename) # Tokenize corpus (simple whitespace tokenization) tokenized_corpus = [doc.lower().split() for doc in corpus_docs] # Build BM25 index bm25 = BM25Okapi(tokenized_corpus) # Query retrieval query = "what is machine learning" tokenized_query = query.lower().split() # Get top-k scores and documents doc_scores = bm25.get_scores(tokenized_query) # Retrieve top 10 documents import numpy as np top_n = 10 top_indices = np.argsort(doc_scores)[::-1][:top_n] results = [(doc_ids[i], corpus_docs[i], doc_scores[i]) for i in top_indices] for rank, (doc_id, content, score) in enumerate(results, 1): print(f"Rank {rank}: {doc_id} (Score: {score:.4f})") print(f"Preview: {content[:200]}...\n") ``` ### Recall@10 Evaluation Evaluate retrieval quality against golden dataset: ```python import json # Load golden dataset with open('questions.jsonl', 'r') as f: questions = [json.loads(line) for line in f] def calculate_recall_at_k(bm25_index, tokenized_corpus, doc_ids, questions, k=10): """ Calculate Recall@K for retrieval evaluation questions format: [{"query": "...", "relevant_docs": ["doc1.txt", "doc2.txt"]}] """ total_recall = 0 num_queries = len(questions) for q in questions: query = q['query'] relevant_docs = set(q['relevant_docs']) # Tokenize and search tokenized_query = query.lower().split() scores = bm25_index.get_scores(tokenized_query) # Get top-k top_k_indices = np.argsort(scores)[::-1][:k] retrieved_docs = set([doc_ids[i] for i in top_k_indices]) # Calculate recall relevant_retrieved = len(relevant_docs.intersection(retrieved_docs)) recall = relevant_retrieved / len(relevant_docs) if relevant_docs else 0 total_recall += recall print(f"Query: {query}") print(f"Recall@{k}: {recall:.2%}") print(f"Retrieved: {retrieved_docs}") print(f"Relevant: {relevant_docs}\n") avg_recall = total_recall / num_queries print(f"Average Recall@{k}: {avg_recall:.2%}") return avg_recall # Run evaluation recall = calculate_recall_at_k(bm25, tokenized_corpus, doc_ids, questions, k=10) ``` ## Day 03: Hierarchical Chunking RAG Pipeline ### Chunk Granularity Strategy Implement multi-level chunking with parent-child metadata: ```python import hashlib import uuid def generate_deterministic_chunk_id(content, parent_id=None, level="document"): """ Generate deterministic chunk ID based on content hash """ content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()[:16] return f"{level}_{content_hash}" def hierarchical_chunking(document, doc_id): """ Create document, section, and paragraph level chunks Returns: List of chunk dictionaries with metadata """ chunks = [] # Document-level chunk doc_chunk_id = generate_deterministic_chunk_id(document, level="doc") chunks.append({ "chunk_id": doc_chunk_id, "content": document, "level": "document", "parent_id": None, "doc_id": doc_id, "metadata": {"granularity": "document"} }) # Section-level chunking (split by double newline or headers) sections = document.split("\n\n") for sec_idx, section in enumerate(sections): if len(section.strip()) < 50: # Skip very short sections continue sec_chunk_id = generate_deterministic_chunk_id(section, level="sec") chunks.append({ "chunk_id": sec_chunk_id, "content": section, "level": "section", "parent_id": doc_chunk_id, "doc_id": doc_id, "metadata": { "granularity": "section", "section_index": sec_idx } }) # Paragraph-level chunking paragraphs = section.split("\n") for para_idx, paragraph in enumerate(paragraphs): if len(paragraph.strip()) < 20: # Skip very short paragraphs continue para_chunk_id = generate_deterministic_chunk_id(paragraph, level="para") chunks.append({ "chunk_id": para_chunk_id, "content": paragraph, "level": "paragraph", "parent_id": sec_chunk_id, "doc_id": doc_id, "metadata": { "granularity": "paragraph", "section_index": sec_idx, "paragraph_index": para_idx } }) return chunks # Example usage sample_doc = """# Machine Learning Basics Machine learning is a subset of artificial intelligence that enables systems to learn from data. ## Supervised Learning Supervised learning uses labeled data to train models. Common algorithms include linear regression and decision trees. ## Unsupervised Learning Unsupervised learning finds patterns in unlabeled data. Clustering and dimensionality reduction are key techniques.""" chunks = hierarchical_chunking(sample_doc, "ml_basics.txt") for chunk in chunks: print(f"Level: {chunk['level']}, ID: {chunk['chunk_id'][:20]}...") print(f"Parent: {chunk['parent_id'][:20] if chunk['parent_id'] else 'None'}...") print(f"Content: {chunk['content'][:80]}...\n") ``` ### RAG Pipeline with Vector Search Combine BM25 with semantic search: ```python from sentence_transformers import SentenceTransformer import faiss import numpy as np class HybridRAGPipeline: def __init__(self, embedding_model="all-MiniLM-L6-v2"): self.embedding_model = SentenceTransformer(embedding_model) self.chunks = [] self.bm25_index = None self.faiss_index = None def index_documents(self, documents): """ Index documents with both BM25 and vector embeddings """ # Create hierarchical chunks all_chunks = [] for doc_id, doc_content in documents.items(): chunks = hierarchical_chunking(doc_content, doc_id) all_chunks.extend(chunks) self.chunks = all_chunks # BM25 indexing tokenized_chunks = [chunk['content'].lower().split() for chunk in all_chunks] self.bm25_index = BM25Okapi(tokenized_chunks) # Vector indexing chunk_texts = [chunk['content'] for chunk in all_chunks] embeddings = self.embedding_model.encode(chunk_texts, show_progress_bar=True) # Build FAISS index dimension = embeddings.shape[1] self.faiss_index = faiss.IndexFlatIP(dimension) # Inner product for cosine similarity # Normalize embeddings for cosine similarity faiss.normalize_L2(embeddings) self.faiss_index.add(embeddings) def retrieve(self, query, top_k=10, alpha=0.5, granularity="paragraph"): """ Hybrid retrieval: alpha * BM25 + (1-alpha) * Vector Args: query: Search query top_k: Number of results alpha: Weight for BM25 (0-1), (1-alpha) for vector search granularity: Filter by chunk level (document, section, paragraph) """ # Filter chunks by granularity filtered_indices = [i for i, chunk in enumerate(self.chunks) if chunk['level'] == granularity] if not filtered_indices: filtered_indices = list(range(len(self.chunks))) # BM25 scores tokenized_query = query.lower().split() bm25_scores = self.bm25_index.get_scores(tokenized_query) bm25_scores_filtered = bm25_scores[filtered_indices] # Normalize BM25 scores if bm25_scores_filtered.max() > 0: bm25_scores_filtered = bm25_scores_filtered / bm25_scores_filtered.max() # Vector scores query_embedding = self.embedding_model.encode([query]) faiss.normalize_L2(query_embedding) distances, indices = self.faiss_index.search(query_embedding, len(self.chunks)) vector_scores = distances[0] vector_scores_filtered = vector_scores[filtered_indices] # Hybrid scoring hybrid_scores = alpha * bm25_scores_filtered + (1 - alpha) * vector_scores_filtered # Get top-k top_k_local = np.argsort(hybrid_scores)[::-1][:top_k] top_k_global = [filtered_indices[i] for i in top_k_local] results = [] for idx in top_k_global: results.append({ "chunk": self.chunks[idx], "bm25_score": float(bm25_scores[idx]), "vector_score": float(vector_scores[idx]), "hybrid_score": float(alpha * bm25_scores[idx] + (1 - alpha) * vector_scores[idx]) }) return results # Example usage pipeline = HybridRAGPipeline() # Load corpus documents = { "ml_basics.txt": sample_doc, "dl_intro.txt": "Deep learning uses neural networks with multiple layers..." } pipeline.index_documents(documents) # Query with different granularities query = "what is supervised learning" print("=== Paragraph-level retrieval ===") results = pipeline.retrieve(query, top_k=5, alpha=0.5, granularity="paragraph") for i, result in enumerate(results, 1): print(f"\nRank {i}:") print(f"Content: {result['chunk']['content'][:150]}...") print(f"Level: {result['chunk']['level']}") print(f"Hybrid Score: {result['hybrid_score']:.4f}") ``` ## Retrieval Evaluation Framework Evaluate chunking strategies against golden dataset: ```python def evaluate_chunking_strategy(pipeline, questions, granularity="paragraph", k=10): """ Evaluate retrieval performance for specific chunk granularity """ metrics = { "recall": [], "precision": [], "mrr": [] # Mean Reciprocal Rank } for q in questions: query = q['query'] relevant_docs = set(q['relevant_docs']) # Retrieve with specific granularity results = pipeline.retrieve(query, top_k=k, granularity=granularity) retrieved_docs = set([r['chunk']['doc_id'] for r in results]) # Recall relevant_retrieved = len(relevant_docs.intersection(retrieved_docs)) recall = relevant_retrieved / len(relevant_docs) if relevant_docs else 0
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub