
Rag Architect
- 590 installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
rag-architect is a Claude Code skill that helps developers design, implement, and optimize production-grade RAG pipelines including chunking, embeddings, vector search, and retrieval evaluation.
About
rag-architect is an AI engineering skill for developers building retrieval-augmented generation systems that need accurate, scalable knowledge retrieval. The skill spans the full RAG lifecycle: document ingestion, fixed-size and semantic chunking strategies, embedding model selection, vector database search, reranking, and retrieval evaluation frameworks. Developers reach for rag-architect when choosing chunk sizes, tuning retrieval quality, implementing vector search, or debugging poor LLM answers caused by bad context retrieval. It is suited to production chatbots, internal knowledge bases, and agent tools that must ground responses in private documents rather than model memory alone.
- Comprehensive RAG pipeline design covering document processing through evaluation frameworks
- Compares Fixed-Size, Token-based, Sentence-based, and Semantic chunking strategies with pros, cons, and use cases
- Guides selection of embedding models, vector databases, and retrieval strategies for accuracy and scale
- Includes optimization techniques, overlap strategies, and evaluation frameworks for production systems
- Delivers complete architecture recommendations that feed directly into implementation
Rag Architect by the numbers
- 590 all-time installs (skills.sh)
- Ranked #1,583 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill rag-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 590 |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you design production RAG retrieval pipelines?
Design, optimize, and implement production-grade RAG pipelines including chunking, embeddings, vector search, and retrieval evaluation.
Who is it for?
Backend and ML engineers implementing knowledge retrieval for LLM apps who need structured guidance on chunking, embeddings, and evaluation.
Skip if: Developers who only need a single-vector-store quickstart with no tuning, evaluation, or production retrieval optimization requirements.
When should I use this skill?
The user asks to design RAG pipelines, optimize retrieval, choose embedding models, implement vector search, or evaluate knowledge retrieval quality.
What you get
Chunking strategy specs, embedding and vector search architecture, reranking configuration, and retrieval evaluation benchmarks.
- RAG architecture design
- Chunking and retrieval strategy
- Evaluation framework outline
Files
RAG Architect
Design, tune, and evaluate production RAG pipelines with three deterministic tools. Run the tools against the actual corpus and requirements — do not pick chunk sizes or databases by intuition.
Hard rules
1. Never present model names or vendor prices as current facts. Embedding models and vector-DB pricing rot in months. Recommend a tier (see table below), name a current-generation candidate, and tell the user to verify against the provider's live pricing page. 2. Every design ends with an evaluation run. A RAG design without retrieval_evaluator.py numbers is a hypothesis, not a deliverable. 3. Chunking is corpus-driven. Run chunking_optimizer.py on the real documents before choosing a strategy.
Embedding model tiers (pattern, not price list)
| Tier | Current-generation examples (verify before use) | When |
|---|---|---|
| Fast / self-hosted | all-MiniLM-L6-v2, bge-small | Cost-sensitive, small scale, real-time |
| Balanced open | all-mpnet-base-v2, bge-large, e5-large | Quality without API dependency |
| Quality API | text-embedding-3-large, voyage-3-large | Accuracy-priority general retrieval |
| Code | voyage-code-3, CodeBERT-family | Code search corpora |
Pricing discipline: build the cost model with a placeholder table — columns model | $/1M tokens (verify) | dims | as-of date — and have the user fill in live numbers. Same for vector DBs (Pinecone/Weaviate/Qdrant/Chroma/pgvector): the selection criteria (managed vs self-hosted, scale, filtering, existing Postgres) are durable; the dollar figures are not.
Workflow
All paths relative to this skill folder. Outputs chain: corpus analysis → design → evaluation.
1. Analyze the corpus and pick chunking
python3 chunking_optimizer.py /path/to/docs --extensions .md .txt -o chunking.jsonEmits chunking.json with corpus_info, per-strategy strategy_results, a recommendation, and sample_chunks. Use recommendation.strategy and its config; show the user 2-3 sample_chunks so they can sanity-check boundaries.
2. Design the pipeline from requirements
Write a requirements JSON with these keys (all required): document_types[], document_count, avg_document_size (chars), queries_per_day, query_patterns[], latency_requirement, budget_monthly, accuracy_priority (0-1), cost_priority (0-1), maintenance_complexity.
python3 rag_pipeline_designer.py requirements.json -o design.jsonEmits design.json with chunking, embedding, vector_db, retrieval, reranking, evaluation, total_cost, architecture_diagram (mermaid), and config_templates. Present the diagram; label every cost_monthly figure as an estimate to verify (rule 1).
3. Evaluate retrieval quality
Prepare queries.json (list of {id, text} or {"queries": [...]}) and ground_truth.json ({query_id: [relevant_doc_ids]}), then:
python3 retrieval_evaluator.py queries.json /path/to/docs ground_truth.json --k-values 3 5 10 -o eval.jsonReports precision@k, recall@k, MRR, NDCG@k, plus poor_precision_examples / poor_recall_examples for failure analysis.
4. Verification loop
The design is done only when:
1. eval.json meets targets — typical floors: precision@5 ≥ 0.8, recall@10 ≥ 0.85 (set per use case with the user). 2. If below target: inspect the poor-example lists, then change one variable (chunking strategy → re-run step 1; embedding tier; add reranking; hybrid retrieval) and re-run step 3. Repeat. 3. Every recommended model/price in the deliverable carries a "verify current pricing/model availability" note with an as-of date.
References
references/chunking_strategies_comparison.md— strategy trade-offs the optimizer implementsreferences/embedding_model_benchmark.md— benchmark methodology (dated snapshot; staleness warning at top)references/rag_evaluation_framework.md— metric definitions (faithfulness, relevance, precision/recall/NDCG)
#!/usr/bin/env python3
"""
Chunking Optimizer - Analyzes document corpus and recommends optimal chunking strategy.
This script analyzes a collection of text/markdown documents and evaluates different
chunking strategies to recommend the optimal approach for the given corpus.
Strategies tested:
- Fixed-size chunking (character and token-based) with overlap
- Sentence-based chunking
- Paragraph-based chunking
- Semantic chunking (heading-aware)
Metrics measured:
- Chunk size distribution (mean, std, min, max)
- Semantic coherence (topic continuity heuristic)
- Boundary quality (sentence break analysis)
No external dependencies - uses only Python standard library.
"""
import argparse
import json
import os
import re
import statistics
from collections import Counter, defaultdict
from math import log, sqrt
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any
class DocumentCorpus:
"""Handles loading and preprocessing of document corpus."""
def __init__(self, directory: str, extensions: List[str] = None):
self.directory = Path(directory)
self.extensions = extensions or ['.txt', '.md', '.markdown']
self.documents = []
self._load_documents()
def _load_documents(self):
"""Load all text documents from directory."""
if not self.directory.exists():
raise FileNotFoundError(f"Directory not found: {self.directory}")
for file_path in self.directory.rglob('*'):
if file_path.is_file() and file_path.suffix.lower() in self.extensions:
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
if content.strip(): # Only include non-empty files
self.documents.append({
'path': str(file_path),
'content': content,
'size': len(content)
})
except Exception as e:
print(f"Warning: Could not read {file_path}: {e}")
if not self.documents:
raise ValueError(f"No valid documents found in {self.directory}")
print(f"Loaded {len(self.documents)} documents totaling {sum(d['size'] for d in self.documents):,} characters")
class ChunkingStrategy:
"""Base class for chunking strategies."""
def __init__(self, name: str, config: Dict[str, Any]):
self.name = name
self.config = config
def chunk(self, text: str) -> List[Dict[str, Any]]:
"""Split text into chunks. Returns list of chunk dictionaries."""
raise NotImplementedError
class FixedSizeChunker(ChunkingStrategy):
"""Fixed-size chunking with optional overlap."""
def __init__(self, chunk_size: int = 1000, overlap: int = 100, unit: str = 'char'):
config = {'chunk_size': chunk_size, 'overlap': overlap, 'unit': unit}
super().__init__(f'fixed_size_{unit}', config)
self.chunk_size = chunk_size
self.overlap = overlap
self.unit = unit
def chunk(self, text: str) -> List[Dict[str, Any]]:
chunks = []
if self.unit == 'char':
return self._chunk_by_chars(text)
else: # word-based approximation
words = text.split()
return self._chunk_by_words(words)
def _chunk_by_chars(self, text: str) -> List[Dict[str, Any]]:
chunks = []
start = 0
chunk_id = 0
while start < len(text):
end = min(start + self.chunk_size, len(text))
chunk_text = text[start:end]
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': start,
'end': end,
'size': len(chunk_text)
})
start = max(start + self.chunk_size - self.overlap, start + 1)
chunk_id += 1
if start >= len(text):
break
return chunks
def _chunk_by_words(self, words: List[str]) -> List[Dict[str, Any]]:
chunks = []
start = 0
chunk_id = 0
while start < len(words):
end = min(start + self.chunk_size, len(words))
chunk_words = words[start:end]
chunk_text = ' '.join(chunk_words)
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': start,
'end': end,
'size': len(chunk_text)
})
start = max(start + self.chunk_size - self.overlap, start + 1)
chunk_id += 1
if start >= len(words):
break
return chunks
class SentenceChunker(ChunkingStrategy):
"""Sentence-based chunking."""
def __init__(self, max_size: int = 1000):
config = {'max_size': max_size}
super().__init__('sentence_based', config)
self.max_size = max_size
# Simple sentence boundary detection
self.sentence_endings = re.compile(r'[.!?]+\s+')
def chunk(self, text: str) -> List[Dict[str, Any]]:
# Split into sentences
sentences = self._split_sentences(text)
chunks = []
current_chunk = []
current_size = 0
chunk_id = 0
for sentence in sentences:
sentence_size = len(sentence)
if current_size + sentence_size > self.max_size and current_chunk:
# Save current chunk
chunk_text = ' '.join(current_chunk)
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0, # Approximate
'end': len(chunk_text),
'size': len(chunk_text),
'sentence_count': len(current_chunk)
})
chunk_id += 1
current_chunk = [sentence]
current_size = sentence_size
else:
current_chunk.append(sentence)
current_size += sentence_size
# Add final chunk
if current_chunk:
chunk_text = ' '.join(current_chunk)
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0,
'end': len(chunk_text),
'size': len(chunk_text),
'sentence_count': len(current_chunk)
})
return chunks
def _split_sentences(self, text: str) -> List[str]:
"""Simple sentence splitting."""
sentences = []
parts = self.sentence_endings.split(text)
for i, part in enumerate(parts[:-1]):
# Add the sentence ending back
ending_match = list(self.sentence_endings.finditer(text))
if i < len(ending_match):
sentence = part + ending_match[i].group().strip()
else:
sentence = part
if sentence.strip():
sentences.append(sentence.strip())
# Add final part if it exists
if parts[-1].strip():
sentences.append(parts[-1].strip())
return [s for s in sentences if len(s.strip()) > 0]
class ParagraphChunker(ChunkingStrategy):
"""Paragraph-based chunking."""
def __init__(self, max_size: int = 2000, min_paragraph_size: int = 50):
config = {'max_size': max_size, 'min_paragraph_size': min_paragraph_size}
super().__init__('paragraph_based', config)
self.max_size = max_size
self.min_paragraph_size = min_paragraph_size
def chunk(self, text: str) -> List[Dict[str, Any]]:
# Split by double newlines (paragraph boundaries)
paragraphs = [p.strip() for p in re.split(r'\n\s*\n', text) if p.strip()]
chunks = []
current_chunk = []
current_size = 0
chunk_id = 0
for paragraph in paragraphs:
paragraph_size = len(paragraph)
# Skip very short paragraphs unless they're the only content
if paragraph_size < self.min_paragraph_size and len(paragraphs) > 1:
continue
if current_size + paragraph_size > self.max_size and current_chunk:
# Save current chunk
chunk_text = '\n\n'.join(current_chunk)
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0,
'end': len(chunk_text),
'size': len(chunk_text),
'paragraph_count': len(current_chunk)
})
chunk_id += 1
current_chunk = [paragraph]
current_size = paragraph_size
else:
current_chunk.append(paragraph)
current_size += paragraph_size + 2 # Account for newlines
# Add final chunk
if current_chunk:
chunk_text = '\n\n'.join(current_chunk)
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0,
'end': len(chunk_text),
'size': len(chunk_text),
'paragraph_count': len(current_chunk)
})
return chunks
class SemanticChunker(ChunkingStrategy):
"""Heading-aware semantic chunking."""
def __init__(self, max_size: int = 1500, heading_weight: float = 2.0):
config = {'max_size': max_size, 'heading_weight': heading_weight}
super().__init__('semantic_heading', config)
self.max_size = max_size
self.heading_weight = heading_weight
# Markdown and plain text heading patterns
self.heading_patterns = [
re.compile(r'^#{1,6}\s+(.+)$', re.MULTILINE), # Markdown headers
re.compile(r'^(.+)\n[=-]+\s*$', re.MULTILINE), # Underlined headers
re.compile(r'^\d+\.\s*(.+)$', re.MULTILINE), # Numbered sections
]
def chunk(self, text: str) -> List[Dict[str, Any]]:
sections = self._identify_sections(text)
chunks = []
chunk_id = 0
for section in sections:
section_chunks = self._chunk_section(section, chunk_id)
chunks.extend(section_chunks)
chunk_id += len(section_chunks)
return chunks
def _identify_sections(self, text: str) -> List[Dict[str, Any]]:
"""Identify sections based on headings."""
sections = []
lines = text.split('\n')
current_section = {'heading': 'Introduction', 'content': '', 'level': 0}
for line in lines:
is_heading = False
heading_level = 0
heading_text = line.strip()
# Check for markdown headers
if line.strip().startswith('#'):
level = len(line) - len(line.lstrip('#'))
if level <= 6:
heading_text = line.strip('#').strip()
heading_level = level
is_heading = True
# Check for underlined headers
elif len(sections) > 0 and line.strip() and all(c in '=-' for c in line.strip()):
# Previous line might be heading
if current_section['content']:
content_lines = current_section['content'].strip().split('\n')
if content_lines:
potential_heading = content_lines[-1].strip()
if len(potential_heading) > 0 and len(potential_heading) < 100:
# Treat as heading
current_section['content'] = '\n'.join(content_lines[:-1])
sections.append(current_section)
current_section = {
'heading': potential_heading,
'content': '',
'level': 1 if '=' in line else 2
}
continue
if is_heading:
if current_section['content'].strip():
sections.append(current_section)
current_section = {
'heading': heading_text,
'content': '',
'level': heading_level
}
else:
current_section['content'] += line + '\n'
# Add final section
if current_section['content'].strip():
sections.append(current_section)
return sections
def _chunk_section(self, section: Dict[str, Any], start_id: int) -> List[Dict[str, Any]]:
"""Chunk a single section."""
content = section['content'].strip()
if not content:
return []
heading = section['heading']
chunks = []
# If section is small enough, return as single chunk
if len(content) <= self.max_size:
chunks.append({
'id': start_id,
'text': f"{heading}\n\n{content}" if heading else content,
'start': 0,
'end': len(content),
'size': len(content),
'heading': heading,
'level': section['level']
})
return chunks
# Split large sections by paragraphs
paragraphs = [p.strip() for p in content.split('\n\n') if p.strip()]
current_chunk = []
current_size = len(heading) + 2 if heading else 0 # Account for heading
chunk_id = start_id
for paragraph in paragraphs:
paragraph_size = len(paragraph)
if current_size + paragraph_size > self.max_size and current_chunk:
# Save current chunk
chunk_text = '\n\n'.join(current_chunk)
if heading and chunk_id == start_id:
chunk_text = f"{heading}\n\n{chunk_text}"
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0,
'end': len(chunk_text),
'size': len(chunk_text),
'heading': heading if chunk_id == start_id else f"{heading} (continued)",
'level': section['level']
})
chunk_id += 1
current_chunk = [paragraph]
current_size = paragraph_size
else:
current_chunk.append(paragraph)
current_size += paragraph_size + 2 # Account for newlines
# Add final chunk
if current_chunk:
chunk_text = '\n\n'.join(current_chunk)
if heading and chunk_id == start_id:
chunk_text = f"{heading}\n\n{chunk_text}"
elif heading:
chunk_text = f"{heading} (continued)\n\n{chunk_text}"
chunks.append({
'id': chunk_id,
'text': chunk_text,
'start': 0,
'end': len(chunk_text),
'size': len(chunk_text),
'heading': heading if chunk_id == start_id else f"{heading} (continued)",
'level': section['level']
})
return chunks
class ChunkAnalyzer:
"""Analyzes chunks and provides quality metrics."""
def __init__(self):
self.vocabulary = set()
self.word_freq = Counter()
def analyze_chunks(self, chunks: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Comprehensive chunk analysis."""
if not chunks:
return {'error': 'No chunks to analyze'}
sizes = [chunk['size'] for chunk in chunks]
# Basic size statistics
size_stats = {
'count': len(chunks),
'mean': statistics.mean(sizes),
'median': statistics.median(sizes),
'std': statistics.stdev(sizes) if len(sizes) > 1 else 0,
'min': min(sizes),
'max': max(sizes),
'total': sum(sizes)
}
# Boundary quality analysis
boundary_quality = self._analyze_boundary_quality(chunks)
# Semantic coherence (simple heuristic)
coherence_score = self._calculate_semantic_coherence(chunks)
# Vocabulary distribution
vocab_stats = self._analyze_vocabulary(chunks)
return {
'size_statistics': size_stats,
'boundary_quality': boundary_quality,
'semantic_coherence': coherence_score,
'vocabulary_statistics': vocab_stats
}
def _analyze_boundary_quality(self, chunks: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze how well chunks respect natural boundaries."""
sentence_breaks = 0
word_breaks = 0
total_chunks = len(chunks)
sentence_endings = re.compile(r'[.!?]\s*$')
for chunk in chunks:
text = chunk['text'].strip()
if not text:
continue
# Check if chunk ends with sentence boundary
if sentence_endings.search(text):
sentence_breaks += 1
# Check if chunk ends with word boundary
if text[-1].isalnum() or text[-1] in '.!?':
word_breaks += 1
return {
'sentence_boundary_ratio': sentence_breaks / total_chunks if total_chunks > 0 else 0,
'word_boundary_ratio': word_breaks / total_chunks if total_chunks > 0 else 0,
'clean_breaks': sentence_breaks,
'total_chunks': total_chunks
}
def _calculate_semantic_coherence(self, chunks: List[Dict[str, Any]]) -> float:
"""Simple semantic coherence heuristic based on vocabulary overlap."""
if len(chunks) < 2:
return 1.0
coherence_scores = []
for i in range(len(chunks) - 1):
chunk1_words = set(re.findall(r'\b\w+\b', chunks[i]['text'].lower()))
chunk2_words = set(re.findall(r'\b\w+\b', chunks[i+1]['text'].lower()))
if not chunk1_words or not chunk2_words:
continue
# Jaccard similarity as coherence measure
intersection = len(chunk1_words & chunk2_words)
union = len(chunk1_words | chunk2_words)
if union > 0:
coherence_scores.append(intersection / union)
return statistics.mean(coherence_scores) if coherence_scores else 0.0
def _analyze_vocabulary(self, chunks: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze vocabulary distribution across chunks."""
all_words = []
chunk_vocab_sizes = []
for chunk in chunks:
words = re.findall(r'\b\w+\b', chunk['text'].lower())
all_words.extend(words)
chunk_vocab_sizes.append(len(set(words)))
total_vocab = len(set(all_words))
word_freq = Counter(all_words)
return {
'total_vocabulary': total_vocab,
'avg_chunk_vocabulary': statistics.mean(chunk_vocab_sizes) if chunk_vocab_sizes else 0,
'vocabulary_diversity': total_vocab / len(all_words) if all_words else 0,
'most_common_words': word_freq.most_common(10)
}
class ChunkingOptimizer:
"""Main optimizer that tests different chunking strategies."""
def __init__(self):
self.analyzer = ChunkAnalyzer()
def optimize(self, corpus: DocumentCorpus, config: Dict[str, Any] = None) -> Dict[str, Any]:
"""Test all chunking strategies and recommend the best one."""
config = config or {}
strategies = self._create_strategies(config)
results = {}
print(f"Testing {len(strategies)} chunking strategies...")
for strategy in strategies:
print(f" Testing {strategy.name}...")
strategy_results = self._test_strategy(corpus, strategy)
results[strategy.name] = strategy_results
# Recommend best strategy
recommendation = self._recommend_strategy(results)
return {
'corpus_info': {
'document_count': len(corpus.documents),
'total_size': sum(d['size'] for d in corpus.documents),
'avg_document_size': statistics.mean([d['size'] for d in corpus.documents])
},
'strategy_results': results,
'recommendation': recommendation,
'sample_chunks': self._generate_sample_chunks(corpus, recommendation['best_strategy'])
}
def _create_strategies(self, config: Dict[str, Any]) -> List[ChunkingStrategy]:
"""Create all chunking strategies to test."""
strategies = []
# Fixed-size strategies
for size in config.get('fixed_sizes', [512, 1000, 1500]):
for overlap in config.get('overlaps', [50, 100]):
strategies.append(FixedSizeChunker(size, overlap, 'char'))
# Sentence-based strategies
for max_size in config.get('sentence_max_sizes', [800, 1200]):
strategies.append(SentenceChunker(max_size))
# Paragraph-based strategies
for max_size in config.get('paragraph_max_sizes', [1500, 2000]):
strategies.append(ParagraphChunker(max_size))
# Semantic strategies
for max_size in config.get('semantic_max_sizes', [1200, 1800]):
strategies.append(SemanticChunker(max_size))
return strategies
def _test_strategy(self, corpus: DocumentCorpus, strategy: ChunkingStrategy) -> Dict[str, Any]:
"""Test a single chunking strategy."""
all_chunks = []
document_results = []
for doc in corpus.documents:
try:
chunks = strategy.chunk(doc['content'])
all_chunks.extend(chunks)
doc_analysis = self.analyzer.analyze_chunks(chunks)
document_results.append({
'path': doc['path'],
'chunk_count': len(chunks),
'analysis': doc_analysis
})
except Exception as e:
print(f" Error processing {doc['path']}: {e}")
continue
# Overall analysis
overall_analysis = self.analyzer.analyze_chunks(all_chunks)
return {
'strategy_config': strategy.config,
'total_chunks': len(all_chunks),
'overall_analysis': overall_analysis,
'document_results': document_results,
'performance_score': self._calculate_performance_score(overall_analysis)
}
def _calculate_performance_score(self, analysis: Dict[str, Any]) -> float:
"""Calculate overall performance score for a strategy."""
if 'error' in analysis:
return 0.0
size_stats = analysis['size_statistics']
boundary_quality = analysis['boundary_quality']
coherence = analysis['semantic_coherence']
# Normalize metrics to 0-1 range and combine
size_consistency = 1.0 - min(size_stats['std'] / size_stats['mean'], 1.0) if size_stats['mean'] > 0 else 0
boundary_score = (boundary_quality['sentence_boundary_ratio'] + boundary_quality['word_boundary_ratio']) / 2
coherence_score = coherence
# Weighted combination
return (size_consistency * 0.3 + boundary_score * 0.4 + coherence_score * 0.3)
def _recommend_strategy(self, results: Dict[str, Any]) -> Dict[str, Any]:
"""Recommend the best chunking strategy based on analysis."""
best_strategy = None
best_score = 0
strategy_scores = {}
for strategy_name, result in results.items():
score = result['performance_score']
strategy_scores[strategy_name] = score
if score > best_score:
best_score = score
best_strategy = strategy_name
return {
'best_strategy': best_strategy,
'best_score': best_score,
'all_scores': strategy_scores,
'reasoning': self._generate_reasoning(best_strategy, results[best_strategy] if best_strategy else None)
}
def _generate_reasoning(self, strategy_name: str, result: Dict[str, Any]) -> str:
"""Generate human-readable reasoning for the recommendation."""
if not result:
return "No valid strategy found."
analysis = result['overall_analysis']
size_stats = analysis['size_statistics']
boundary = analysis['boundary_quality']
reasoning = f"Recommended '{strategy_name}' because:\n"
reasoning += f"- Average chunk size: {size_stats['mean']:.0f} characters\n"
reasoning += f"- Size consistency: {size_stats['std']:.0f} std deviation\n"
reasoning += f"- Boundary quality: {boundary['sentence_boundary_ratio']:.2%} clean sentence breaks\n"
reasoning += f"- Semantic coherence: {analysis['semantic_coherence']:.3f}\n"
return reasoning
def _generate_sample_chunks(self, corpus: DocumentCorpus, strategy_name: str) -> List[Dict[str, Any]]:
"""Generate sample chunks using the recommended strategy."""
if not strategy_name or not corpus.documents:
return []
# Create strategy instance
strategy = None
if 'fixed_size' in strategy_name:
strategy = FixedSizeChunker()
elif 'sentence' in strategy_name:
strategy = SentenceChunker()
elif 'paragraph' in strategy_name:
strategy = ParagraphChunker()
elif 'semantic' in strategy_name:
strategy = SemanticChunker()
if not strategy:
return []
# Get chunks from first document
sample_doc = corpus.documents[0]
chunks = strategy.chunk(sample_doc['content'])
# Return first 3 chunks as samples
return chunks[:3]
def main():
"""Main function with command-line interface."""
parser = argparse.ArgumentParser(description='Analyze documents and recommend optimal chunking strategy')
parser.add_argument('directory', help='Directory containing text/markdown documents')
parser.add_argument('--output', '-o', help='Output file for results (JSON format)')
parser.add_argument('--config', '-c', help='Configuration file (JSON format)')
parser.add_argument('--extensions', nargs='+', default=['.txt', '.md', '.markdown'],
help='File extensions to process')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
# Load configuration
config = {}
if args.config and os.path.exists(args.config):
with open(args.config, 'r') as f:
config = json.load(f)
try:
# Load corpus
print(f"Loading documents from {args.directory}...")
corpus = DocumentCorpus(args.directory, args.extensions)
# Run optimization
optimizer = ChunkingOptimizer()
results = optimizer.optimize(corpus, config)
# Save results
if args.output:
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
print(f"Results saved to {args.output}")
# Print summary
print("\n" + "="*60)
print("CHUNKING OPTIMIZATION RESULTS")
print("="*60)
corpus_info = results['corpus_info']
print(f"Corpus: {corpus_info['document_count']} documents, {corpus_info['total_size']:,} characters")
recommendation = results['recommendation']
print(f"\nRecommended Strategy: {recommendation['best_strategy']}")
print(f"Performance Score: {recommendation['best_score']:.3f}")
print(f"\nReasoning:\n{recommendation['reasoning']}")
if args.verbose:
print("\nAll Strategy Scores:")
for strategy, score in recommendation['all_scores'].items():
print(f" {strategy}: {score:.3f}")
print("\nSample Chunks:")
for i, chunk in enumerate(results['sample_chunks'][:2]):
print(f"\nChunk {i+1} ({chunk['size']} chars):")
print("-" * 40)
print(chunk['text'][:200] + "..." if len(chunk['text']) > 200 else chunk['text'])
except Exception as e:
print(f"Error: {e}")
return 1
return 0
if __name__ == '__main__':
exit(main())#!/usr/bin/env python3
"""
RAG Pipeline Designer - Designs complete RAG pipelines based on requirements.
This script analyzes requirements and generates a comprehensive RAG pipeline design
including architecture diagrams, component recommendations, configuration templates,
and cost projections.
Components designed:
- Chunking strategy recommendation
- Embedding model selection
- Vector database recommendation
- Retrieval approach (dense/sparse/hybrid)
- Reranking configuration
- Evaluation framework setup
- Production deployment patterns
No external dependencies - uses only Python standard library.
"""
import argparse
import json
import math
import os
from typing import Dict, List, Tuple, Any, Optional
from dataclasses import dataclass, asdict
from enum import Enum
class Scale(Enum):
"""System scale categories."""
SMALL = "small" # < 1M documents, < 1K queries/day
MEDIUM = "medium" # 1M-100M documents, 1K-100K queries/day
LARGE = "large" # 100M+ documents, 100K+ queries/day
class DocumentType(Enum):
"""Document type categories."""
TEXT = "text" # Plain text, articles
TECHNICAL = "technical" # Documentation, manuals
CODE = "code" # Source code files
SCIENTIFIC = "scientific" # Research papers, journals
LEGAL = "legal" # Legal documents, contracts
MIXED = "mixed" # Multiple document types
class Latency(Enum):
"""Latency requirements."""
REAL_TIME = "real_time" # < 100ms
INTERACTIVE = "interactive" # < 500ms
BATCH = "batch" # > 1s acceptable
@dataclass
class Requirements:
"""RAG system requirements."""
document_types: List[str]
document_count: int
avg_document_size: int # characters
queries_per_day: int
query_patterns: List[str] # e.g., ["factual", "conversational", "analytical"]
latency_requirement: str
budget_monthly: float # USD
accuracy_priority: float # 0-1 scale
cost_priority: float # 0-1 scale
maintenance_complexity: str # "low", "medium", "high"
@dataclass
class ComponentRecommendation:
"""Recommendation for a pipeline component."""
name: str
type: str
config: Dict[str, Any]
rationale: str
pros: List[str]
cons: List[str]
cost_monthly: float
@dataclass
class PipelineDesign:
"""Complete RAG pipeline design."""
chunking: ComponentRecommendation
embedding: ComponentRecommendation
vector_db: ComponentRecommendation
retrieval: ComponentRecommendation
reranking: Optional[ComponentRecommendation]
evaluation: ComponentRecommendation
total_cost: float
architecture_diagram: str
config_templates: Dict[str, Any]
class RAGPipelineDesigner:
"""Main pipeline designer class."""
def __init__(self):
self.embedding_models = self._load_embedding_models()
self.vector_databases = self._load_vector_databases()
self.chunking_strategies = self._load_chunking_strategies()
def design_pipeline(self, requirements: Requirements) -> PipelineDesign:
"""Design complete RAG pipeline based on requirements."""
print(f"Designing RAG pipeline for {requirements.document_count:,} documents...")
# Determine system scale
scale = self._determine_scale(requirements)
print(f"System scale: {scale.value}")
# Design each component
chunking = self._recommend_chunking(requirements, scale)
embedding = self._recommend_embedding(requirements, scale)
vector_db = self._recommend_vector_db(requirements, scale)
retrieval = self._recommend_retrieval(requirements, scale)
reranking = self._recommend_reranking(requirements, scale)
evaluation = self._recommend_evaluation(requirements, scale)
# Calculate total cost
total_cost = (chunking.cost_monthly + embedding.cost_monthly +
vector_db.cost_monthly + retrieval.cost_monthly +
evaluation.cost_monthly)
if reranking:
total_cost += reranking.cost_monthly
# Generate architecture diagram
architecture = self._generate_architecture_diagram(
chunking, embedding, vector_db, retrieval, reranking, evaluation
)
# Generate configuration templates
configs = self._generate_config_templates(
chunking, embedding, vector_db, retrieval, reranking, evaluation
)
return PipelineDesign(
chunking=chunking,
embedding=embedding,
vector_db=vector_db,
retrieval=retrieval,
reranking=reranking,
evaluation=evaluation,
total_cost=total_cost,
architecture_diagram=architecture,
config_templates=configs
)
def _determine_scale(self, req: Requirements) -> Scale:
"""Determine system scale based on requirements."""
if req.document_count < 1_000_000 and req.queries_per_day < 1_000:
return Scale.SMALL
elif req.document_count < 100_000_000 and req.queries_per_day < 100_000:
return Scale.MEDIUM
else:
return Scale.LARGE
def _recommend_chunking(self, req: Requirements, scale: Scale) -> ComponentRecommendation:
"""Recommend chunking strategy."""
doc_types = set(req.document_types)
if "code" in doc_types:
strategy = "semantic_code_aware"
config = {"max_size": 1000, "preserve_functions": True, "overlap": 50}
rationale = "Code documents benefit from function/class boundary awareness"
elif "technical" in doc_types or "scientific" in doc_types:
strategy = "semantic_heading_aware"
config = {"max_size": 1500, "heading_weight": 2.0, "overlap": 100}
rationale = "Technical documents have clear hierarchical structure"
elif len(doc_types) > 2 or "mixed" in doc_types:
strategy = "adaptive_chunking"
config = {"strategies": ["paragraph", "sentence", "fixed"], "auto_select": True}
rationale = "Mixed document types require adaptive strategy selection"
else:
if req.avg_document_size > 5000:
strategy = "paragraph_based"
config = {"max_size": 2000, "min_paragraph_size": 100}
rationale = "Large documents benefit from paragraph-based chunking"
else:
strategy = "sentence_based"
config = {"max_size": 1000, "sentence_overlap": 1}
rationale = "Small to medium documents work well with sentence chunking"
return ComponentRecommendation(
name=strategy,
type="chunking",
config=config,
rationale=rationale,
pros=self._get_chunking_pros(strategy),
cons=self._get_chunking_cons(strategy),
cost_monthly=0.0 # Processing cost only
)
def _recommend_embedding(self, req: Requirements, scale: Scale) -> ComponentRecommendation:
"""Recommend embedding model."""
doc_types = set(req.document_types)
# Consider accuracy vs cost priority
high_accuracy = req.accuracy_priority > 0.7
cost_sensitive = req.cost_priority > 0.6
if "code" in doc_types:
if high_accuracy and not cost_sensitive:
model = "voyage-code-3"
cost_per_1k_tokens = 0.00018 # verify current pricing before budgeting
dimensions = 1024
else:
model = "sentence-transformers/code-bert-base"
cost_per_1k_tokens = 0.0 # Self-hosted
dimensions = 768
elif "scientific" in doc_types:
if high_accuracy:
model = "openai-text-embedding-3-large"
cost_per_1k_tokens = 0.00013 # verify current pricing before budgeting
dimensions = 3072
else:
model = "sentence-transformers/scibert-nli"
cost_per_1k_tokens = 0.0
dimensions = 768
else:
if cost_sensitive or scale == Scale.SMALL:
model = "sentence-transformers/all-MiniLM-L6-v2"
cost_per_1k_tokens = 0.0
dimensions = 384
elif high_accuracy:
model = "openai-text-embedding-3-large"
cost_per_1k_tokens = 0.00013 # verify current pricing before budgeting
dimensions = 3072
else:
model = "sentence-transformers/all-mpnet-base-v2"
cost_per_1k_tokens = 0.0
dimensions = 768
# Calculate monthly embedding cost
total_tokens = req.document_count * (req.avg_document_size / 4) # ~4 chars per token
query_tokens = req.queries_per_day * 30 * 20 # ~20 tokens per query per month
monthly_cost = (total_tokens + query_tokens) * cost_per_1k_tokens / 1000
return ComponentRecommendation(
name=model,
type="embedding",
config={
"model": model,
"dimensions": dimensions,
"batch_size": 100 if scale == Scale.SMALL else 1000,
"cache_embeddings": True
},
rationale=f"Selected for {doc_types} with accuracy priority {req.accuracy_priority}",
pros=self._get_embedding_pros(model),
cons=self._get_embedding_cons(model),
cost_monthly=monthly_cost
)
def _recommend_vector_db(self, req: Requirements, scale: Scale) -> ComponentRecommendation:
"""Recommend vector database."""
if scale == Scale.SMALL and req.cost_priority > 0.7:
db = "chroma"
cost = 0.0
rationale = "Local/embedded database suitable for small scale and cost optimization"
elif scale == Scale.SMALL and req.maintenance_complexity == "low":
db = "pgvector"
cost = 50.0 # PostgreSQL hosting
rationale = "Leverage existing PostgreSQL infrastructure"
elif scale == Scale.LARGE or req.latency_requirement == "real_time":
db = "pinecone"
vectors = req.document_count * 2 # Account for chunking
cost = max(70, vectors * 0.00005) # $70 base + $0.00005 per vector
rationale = "Managed service with excellent performance for large scale"
elif req.maintenance_complexity == "low":
db = "weaviate_cloud"
vectors = req.document_count * 2
cost = max(25, vectors * 0.00003)
rationale = "Managed Weaviate with good balance of features and cost"
else:
db = "qdrant"
cost = 100.0 # Self-hosted infrastructure estimate
rationale = "High performance self-hosted option with good scaling"
return ComponentRecommendation(
name=db,
type="vector_database",
config=self._get_vector_db_config(db, req, scale),
rationale=rationale,
pros=self._get_vector_db_pros(db),
cons=self._get_vector_db_cons(db),
cost_monthly=cost
)
def _recommend_retrieval(self, req: Requirements, scale: Scale) -> ComponentRecommendation:
"""Recommend retrieval strategy."""
if req.accuracy_priority > 0.8:
strategy = "hybrid"
rationale = "Hybrid retrieval for maximum accuracy combining dense and sparse methods"
elif "technical" in req.document_types or "code" in req.document_types:
strategy = "hybrid"
rationale = "Technical content benefits from both semantic and keyword matching"
elif req.latency_requirement == "real_time":
strategy = "dense"
rationale = "Dense retrieval faster for real-time requirements"
else:
strategy = "dense"
rationale = "Dense retrieval suitable for general text search"
return ComponentRecommendation(
name=strategy,
type="retrieval",
config={
"strategy": strategy,
"dense_weight": 0.7 if strategy == "hybrid" else 1.0,
"sparse_weight": 0.3 if strategy == "hybrid" else 0.0,
"top_k": 20 if req.accuracy_priority > 0.7 else 10,
"similarity_threshold": 0.7
},
rationale=rationale,
pros=self._get_retrieval_pros(strategy),
cons=self._get_retrieval_cons(strategy),
cost_monthly=0.0
)
def _recommend_reranking(self, req: Requirements, scale: Scale) -> Optional[ComponentRecommendation]:
"""Recommend reranking if beneficial."""
if req.accuracy_priority < 0.6 or req.latency_requirement == "real_time":
return None
if req.cost_priority > 0.8:
return None
# Estimate reranking queries per month
monthly_queries = req.queries_per_day * 30
cost_per_query = 0.002 # Estimated cost for cross-encoder reranking
monthly_cost = monthly_queries * cost_per_query
if monthly_cost > req.budget_monthly * 0.3: # Don't exceed 30% of budget
return None
return ComponentRecommendation(
name="cross_encoder_reranking",
type="reranking",
config={
"model": "cross-encoder/ms-marco-MiniLM-L-12-v2",
"rerank_top_k": 20,
"return_top_k": 5,
"batch_size": 16
},
rationale="Reranking improves precision for high-accuracy requirements",
pros=["Higher precision", "Better ranking quality", "Handles complex queries"],
cons=["Additional latency", "Higher cost", "More complexity"],
cost_monthly=monthly_cost
)
def _recommend_evaluation(self, req: Requirements, scale: Scale) -> ComponentRecommendation:
"""Recommend evaluation framework."""
return ComponentRecommendation(
name="comprehensive_evaluation",
type="evaluation",
config={
"metrics": ["precision@k", "recall@k", "mrr", "ndcg"],
"k_values": [1, 3, 5, 10],
"faithfulness_check": True,
"relevance_scoring": True,
"evaluation_frequency": "weekly" if scale == Scale.LARGE else "monthly",
"sample_size": min(1000, req.queries_per_day * 7)
},
rationale="Comprehensive evaluation essential for production RAG systems",
pros=["Quality monitoring", "Performance tracking", "Issue detection"],
cons=["Additional overhead", "Requires ground truth data"],
cost_monthly=20.0 # Evaluation tooling and compute
)
def _generate_architecture_diagram(self, chunking: ComponentRecommendation,
embedding: ComponentRecommendation,
vector_db: ComponentRecommendation,
retrieval: ComponentRecommendation,
reranking: Optional[ComponentRecommendation],
evaluation: ComponentRecommendation) -> str:
"""Generate Mermaid architecture diagram."""
diagram = """```mermaid
graph TB
%% Document Processing Pipeline
A[Document Corpus] --> B[Document Chunking]
B --> C[Embedding Generation]
C --> D[Vector Database Storage]
%% Query Processing Pipeline
E[User Query] --> F[Query Processing]
F --> G[Vector Search]
D --> G
G --> H[Retrieved Chunks]
"""
if reranking:
diagram += " H --> I[Reranking]\n I --> J[Final Results]\n"
else:
diagram += " H --> J[Final Results]\n"
diagram += """
%% Evaluation Pipeline
J --> K[Response Generation]
K --> L[Evaluation Metrics]
%% Component Details
B -.-> B1[Strategy: """ + chunking.name + """]
C -.-> C1[Model: """ + embedding.name + """]
D -.-> D1[Database: """ + vector_db.name + """]
G -.-> G1[Method: """ + retrieval.name + """]
"""
if reranking:
diagram += " I -.-> I1[Model: " + reranking.name + "]\n"
diagram += " L -.-> L1[Framework: " + evaluation.name + "]\n```"
return diagram
def _generate_config_templates(self, *components) -> Dict[str, Any]:
"""Generate configuration templates for all components."""
configs = {}
for component in components:
if component:
configs[component.type] = {
"component": component.name,
"config": component.config,
"rationale": component.rationale
}
# Add deployment configuration
configs["deployment"] = {
"infrastructure": "cloud" if any("pinecone" in str(c.name) for c in components if c) else "hybrid",
"scaling": {
"auto_scaling": True,
"min_replicas": 1,
"max_replicas": 10
},
"monitoring": {
"metrics": ["latency", "throughput", "accuracy"],
"alerts": ["high_latency", "low_accuracy", "service_down"]
}
}
return configs
def _load_embedding_models(self) -> Dict[str, Dict[str, Any]]:
"""Load embedding model specifications."""
return {
"openai-text-embedding-3-large": {
"dimensions": 3072,
"cost_per_1k_tokens": 0.00013, # verify current pricing
"quality": "high",
"speed": "medium"
},
"voyage-3-large": {
"dimensions": 1024,
"cost_per_1k_tokens": 0.00018, # verify current pricing
"quality": "high",
"speed": "medium"
},
"sentence-transformers/all-mpnet-base-v2": {
"dimensions": 768,
"cost_per_1k_tokens": 0.0,
"quality": "high",
"speed": "medium"
},
"sentence-transformers/all-MiniLM-L6-v2": {
"dimensions": 384,
"cost_per_1k_tokens": 0.0,
"quality": "medium",
"speed": "fast"
}
}
def _load_vector_databases(self) -> Dict[str, Dict[str, Any]]:
"""Load vector database specifications."""
return {
"pinecone": {"managed": True, "scaling": "excellent", "cost": "high"},
"weaviate": {"managed": False, "scaling": "good", "cost": "medium"},
"qdrant": {"managed": False, "scaling": "excellent", "cost": "low"},
"chroma": {"managed": False, "scaling": "poor", "cost": "free"},
"pgvector": {"managed": False, "scaling": "good", "cost": "medium"}
}
def _load_chunking_strategies(self) -> Dict[str, Dict[str, Any]]:
"""Load chunking strategy specifications."""
return {
"fixed_size": {"complexity": "low", "quality": "medium"},
"sentence_based": {"complexity": "medium", "quality": "good"},
"paragraph_based": {"complexity": "medium", "quality": "good"},
"semantic_heading_aware": {"complexity": "high", "quality": "excellent"}
}
def _get_vector_db_config(self, db: str, req: Requirements, scale: Scale) -> Dict[str, Any]:
"""Get vector database configuration."""
base_config = {
"collection_name": "rag_documents",
"distance_metric": "cosine",
"index_type": "hnsw"
}
if db == "pinecone":
base_config.update({
"environment": "us-east1-gcp",
"replicas": 1 if scale == Scale.SMALL else 2,
"shards": 1 if scale != Scale.LARGE else 3
})
elif db == "qdrant":
base_config.update({
"memory_mapping": True,
"quantization": scale == Scale.LARGE,
"replication_factor": 1 if scale == Scale.SMALL else 2
})
return base_config
def _get_chunking_pros(self, strategy: str) -> List[str]:
"""Get pros for chunking strategy."""
pros_map = {
"semantic_heading_aware": ["Preserves document structure", "High semantic coherence", "Good for technical docs"],
"paragraph_based": ["Respects natural boundaries", "Good balance", "Readable chunks"],
"sentence_based": ["Natural language boundaries", "Consistent quality", "Good for general text"],
"fixed_size": ["Predictable sizes", "Simple implementation", "Consistent processing"],
"adaptive_chunking": ["Handles mixed content", "Optimizes per document", "Best quality"]
}
return pros_map.get(strategy, ["Good general purpose strategy"])
def _get_chunking_cons(self, strategy: str) -> List[str]:
"""Get cons for chunking strategy."""
cons_map = {
"semantic_heading_aware": ["Complex implementation", "May create large chunks", "Document-dependent"],
"paragraph_based": ["Variable sizes", "May break context", "Document-dependent"],
"sentence_based": ["May create small chunks", "Sentence detection issues", "Variable sizes"],
"fixed_size": ["Breaks semantic boundaries", "May split sentences", "Context loss"],
"adaptive_chunking": ["High complexity", "Slower processing", "Harder to debug"]
}
return cons_map.get(strategy, ["May not fit all use cases"])
def _get_embedding_pros(self, model: str) -> List[str]:
"""Get pros for embedding model."""
if "openai" in model:
return ["High quality", "Regular updates", "Good performance"]
elif "all-mpnet" in model:
return ["High quality", "Free to use", "Good balance"]
elif "MiniLM" in model:
return ["Fast processing", "Small size", "Good for real-time"]
else:
return ["Specialized for domain", "Good performance"]
def _get_embedding_cons(self, model: str) -> List[str]:
"""Get cons for embedding model."""
if "openai" in model:
return ["API costs", "Vendor lock-in", "Rate limits"]
elif "sentence-transformers" in model:
return ["Self-hosting required", "Model updates needed", "GPU beneficial"]
else:
return ["May require fine-tuning", "Domain-specific"]
def _get_vector_db_pros(self, db: str) -> List[str]:
"""Get pros for vector database."""
pros_map = {
"pinecone": ["Fully managed", "Excellent performance", "Auto-scaling"],
"weaviate": ["Rich features", "GraphQL API", "Multi-modal"],
"qdrant": ["High performance", "Rust-based", "Good scaling"],
"chroma": ["Simple setup", "Free", "Good for development"],
"pgvector": ["SQL integration", "ACID compliance", "Familiar"]
}
return pros_map.get(db, ["Good performance"])
def _get_vector_db_cons(self, db: str) -> List[str]:
"""Get cons for vector database."""
cons_map = {
"pinecone": ["Expensive", "Vendor lock-in", "Limited customization"],
"weaviate": ["Complex setup", "Learning curve", "Resource intensive"],
"qdrant": ["Self-managed", "Smaller community", "Setup complexity"],
"chroma": ["Limited scaling", "Not production-ready", "Basic features"],
"pgvector": ["PostgreSQL knowledge needed", "Less specialized", "Manual optimization"]
}
return cons_map.get(db, ["Requires maintenance"])
def _get_retrieval_pros(self, strategy: str) -> List[str]:
"""Get pros for retrieval strategy."""
pros_map = {
"dense": ["Semantic understanding", "Good for paraphrases", "Fast"],
"sparse": ["Exact matching", "Interpretable", "Good for keywords"],
"hybrid": ["Best of both", "High accuracy", "Robust"]
}
return pros_map.get(strategy, ["Good performance"])
def _get_retrieval_cons(self, strategy: str) -> List[str]:
"""Get cons for retrieval strategy."""
cons_map = {
"dense": ["May miss exact matches", "Embedding dependent", "Less interpretable"],
"sparse": ["Vocabulary mismatch", "No semantic understanding", "Synonym issues"],
"hybrid": ["More complex", "Tuning required", "Higher latency"]
}
return cons_map.get(strategy, ["May require tuning"])
def load_requirements(file_path: str) -> Requirements:
"""Load requirements from JSON file."""
with open(file_path, 'r') as f:
data = json.load(f)
return Requirements(**data)
def save_design(design: PipelineDesign, output_path: str):
"""Save pipeline design to JSON file."""
# Convert to dict for JSON serialization
design_dict = {}
for field_name in design.__dataclass_fields__:
value = getattr(design, field_name)
if isinstance(value, ComponentRecommendation):
design_dict[field_name] = asdict(value)
elif value is None:
design_dict[field_name] = None
else:
design_dict[field_name] = value
with open(output_path, 'w') as f:
json.dump(design_dict, f, indent=2)
def print_design_summary(design: PipelineDesign):
"""Print human-readable design summary."""
print("\n" + "="*60)
print("RAG PIPELINE DESIGN SUMMARY")
print("="*60)
print(f"\n💰 Total Monthly Cost: ${design.total_cost:.2f}")
print(f"\n🔧 Component Recommendations:")
components = [design.chunking, design.embedding, design.vector_db,
design.retrieval, design.reranking, design.evaluation]
for component in components:
if component:
print(f"\n {component.type.upper()}: {component.name}")
print(f" Rationale: {component.rationale}")
if component.cost_monthly > 0:
print(f" Monthly Cost: ${component.cost_monthly:.2f}")
print(f"\n📊 Architecture Diagram:")
print(design.architecture_diagram)
def main():
"""Main function with command-line interface."""
parser = argparse.ArgumentParser(description='Design RAG pipeline based on requirements')
parser.add_argument('requirements', help='JSON file containing system requirements')
parser.add_argument('--output', '-o', help='Output file for pipeline design (JSON)')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
try:
# Load requirements
print("Loading requirements...")
requirements = load_requirements(args.requirements)
# Design pipeline
designer = RAGPipelineDesigner()
design = designer.design_pipeline(requirements)
# Save design
if args.output:
save_design(design, args.output)
print(f"Pipeline design saved to {args.output}")
# Print summary
print_design_summary(design)
if args.verbose:
print(f"\n📋 Configuration Templates:")
for component_type, config in design.config_templates.items():
print(f"\n {component_type.upper()}:")
print(f" {json.dumps(config, indent=4)}")
except Exception as e:
print(f"Error: {e}")
return 1
return 0
if __name__ == '__main__':
exit(main())Chunking Strategies Comparison
Executive Summary
Document chunking is the foundation of effective RAG systems. This analysis compares five primary chunking strategies across key metrics including semantic coherence, boundary quality, processing speed, and implementation complexity.
Strategies Analyzed
1. Fixed-Size Chunking
Approach: Split documents into chunks of predetermined size (characters/tokens) with optional overlap.
Variants:
- Character-based: 512, 1024, 2048 characters
- Token-based: 128, 256, 512 tokens
- Overlap: 0%, 10%, 20%
Performance Metrics:
- Processing Speed: ⭐⭐⭐⭐⭐ (Fastest)
- Boundary Quality: ⭐⭐ (Poor - breaks mid-sentence)
- Semantic Coherence: ⭐⭐ (Low - ignores content structure)
- Implementation: ⭐⭐⭐⭐⭐ (Simplest)
- Memory Efficiency: ⭐⭐⭐⭐⭐ (Predictable sizes)
Best For:
- Large-scale processing where speed is critical
- Uniform document types
- When consistent chunk sizes are required
Avoid When:
- Document quality varies significantly
- Preserving context is critical
- Processing narrative or technical content
2. Sentence-Based Chunking
Approach: Group complete sentences until size threshold reached, ensuring natural language boundaries.
Implementation Details:
- Sentence detection using regex patterns or NLP libraries
- Size limits: 500-1500 characters typically
- Overlap: 1-2 sentences for context preservation
Performance Metrics:
- Processing Speed: ⭐⭐⭐⭐ (Fast)
- Boundary Quality: ⭐⭐⭐⭐ (Good - respects sentence boundaries)
- Semantic Coherence: ⭐⭐⭐ (Medium - sentences may be topically unrelated)
- Implementation: ⭐⭐⭐ (Moderate complexity)
- Memory Efficiency: ⭐⭐⭐ (Variable sizes)
Best For:
- Narrative text (articles, books, blogs)
- General-purpose text processing
- When readability of chunks is important
Avoid When:
- Documents have complex sentence structures
- Technical content with code/formulas
- Very short or very long sentences dominate
3. Paragraph-Based Chunking
Approach: Use paragraph boundaries as primary split points, combining or splitting paragraphs based on size constraints.
Implementation Details:
- Paragraph detection via double newlines or HTML tags
- Size limits: 1000-3000 characters
- Hierarchical splitting for oversized paragraphs
Performance Metrics:
- Processing Speed: ⭐⭐⭐⭐ (Fast)
- Boundary Quality: ⭐⭐⭐⭐⭐ (Excellent - natural breaks)
- Semantic Coherence: ⭐⭐⭐⭐ (Good - paragraphs often topically coherent)
- Implementation: ⭐⭐⭐ (Moderate complexity)
- Memory Efficiency: ⭐⭐ (Highly variable sizes)
Best For:
- Well-structured documents
- Articles and reports with clear paragraphs
- When topic coherence is important
Avoid When:
- Documents have inconsistent paragraph structure
- Paragraphs are extremely long or short
- Technical documentation with mixed content
4. Semantic Chunking (Heading-Aware)
Approach: Use document structure (headings, sections) and semantic similarity to create topically coherent chunks.
Implementation Details:
- Heading detection (markdown, HTML, or inferred)
- Topic modeling for section boundaries
- Recursive splitting respecting hierarchy
Performance Metrics:
- Processing Speed: ⭐⭐ (Slow - requires analysis)
- Boundary Quality: ⭐⭐⭐⭐⭐ (Excellent - respects document structure)
- Semantic Coherence: ⭐⭐⭐⭐⭐ (Excellent - maintains topic coherence)
- Implementation: ⭐⭐ (Complex)
- Memory Efficiency: ⭐⭐ (Highly variable)
Best For:
- Technical documentation
- Academic papers
- Structured reports
- When document hierarchy is important
Avoid When:
- Documents lack clear structure
- Processing speed is critical
- Implementation complexity must be minimized
5. Recursive Chunking
Approach: Hierarchical splitting using multiple strategies, preferring larger chunks when possible.
Implementation Details:
- Try larger chunks first (sections, paragraphs)
- Recursively split if size exceeds threshold
- Fallback hierarchy: document → section → paragraph → sentence → character
Performance Metrics:
- Processing Speed: ⭐⭐ (Slow - multiple passes)
- Boundary Quality: ⭐⭐⭐⭐ (Good - adapts to content)
- Semantic Coherence: ⭐⭐⭐⭐ (Good - preserves context when possible)
- Implementation: ⭐⭐ (Complex logic)
- Memory Efficiency: ⭐⭐⭐ (Optimizes chunk count)
Best For:
- Mixed document types
- When chunk count optimization is important
- Complex document structures
Avoid When:
- Simple, uniform documents
- Real-time processing requirements
- Debugging and maintenance overhead is a concern
Comparative Analysis
Chunk Size Distribution
| Strategy | Mean Size | Std Dev | Min Size | Max Size | Coefficient of Variation |
|---|---|---|---|---|---|
| Fixed-Size | 1000 | 0 | 1000 | 1000 | 0.00 |
| Sentence | 850 | 320 | 180 | 1500 | 0.38 |
| Paragraph | 1200 | 680 | 200 | 3500 | 0.57 |
| Semantic | 1400 | 920 | 300 | 4200 | 0.66 |
| Recursive | 1100 | 450 | 400 | 2000 | 0.41 |
Processing Performance
| Strategy | Processing Speed (docs/sec) | Memory Usage (MB/1K docs) | CPU Usage (%) |
|---|---|---|---|
| Fixed-Size | 2500 | 50 | 15 |
| Sentence | 1800 | 65 | 25 |
| Paragraph | 2000 | 60 | 20 |
| Semantic | 400 | 120 | 60 |
| Recursive | 600 | 100 | 45 |
Quality Metrics
| Strategy | Boundary Quality | Semantic Coherence | Context Preservation |
|---|---|---|---|
| Fixed-Size | 0.15 | 0.32 | 0.28 |
| Sentence | 0.85 | 0.58 | 0.65 |
| Paragraph | 0.92 | 0.75 | 0.78 |
| Semantic | 0.95 | 0.88 | 0.85 |
| Recursive | 0.88 | 0.82 | 0.80 |
Domain-Specific Recommendations
Technical Documentation
Primary: Semantic (heading-aware) Secondary: Recursive Rationale: Technical docs have clear hierarchical structure that should be preserved
Scientific Papers
Primary: Semantic (heading-aware) Secondary: Paragraph-based Rationale: Papers have sections (abstract, methodology, results) that form coherent units
News Articles
Primary: Paragraph-based Secondary: Sentence-based Rationale: Inverted pyramid structure means paragraphs are typically topically coherent
Legal Documents
Primary: Paragraph-based Secondary: Semantic Rationale: Legal text has specific paragraph structures that shouldn't be broken
Code Documentation
Primary: Semantic (code-aware) Secondary: Recursive Rationale: Code blocks, functions, and classes form natural boundaries
General Web Content
Primary: Sentence-based Secondary: Paragraph-based Rationale: Variable quality and structure require robust general-purpose approach
Implementation Guidelines
Choosing Chunk Size
1. Consider retrieval context: Smaller chunks (500-800 chars) for precise retrieval 2. Consider generation context: Larger chunks (1000-2000 chars) for comprehensive answers 3. Model context limits: Ensure chunks fit in embedding model context window 4. Query patterns: Specific queries need smaller chunks, broad queries benefit from larger
Overlap Configuration
- None (0%): When context bleeding is problematic
- Low (5-10%): General-purpose overlap for context continuity
- Medium (15-20%): When context preservation is critical
- High (25%+): Rarely beneficial, increases storage costs significantly
Metadata Preservation
Always preserve:
- Document source/path
- Chunk position/sequence
- Heading hierarchy (if applicable)
- Creation/modification timestamps
Conditionally preserve:
- Page numbers (for PDFs)
- Section titles
- Author information
- Document type/category
Evaluation Framework
Automated Metrics
1. Chunk Size Consistency: Standard deviation of chunk sizes 2. Boundary Quality Score: Fraction of chunks ending with complete sentences 3. Topic Coherence: Average cosine similarity between consecutive chunks 4. Processing Speed: Documents processed per second 5. Memory Efficiency: Peak memory usage during processing
Manual Evaluation
1. Readability: Can humans easily understand chunk content? 2. Completeness: Do chunks contain complete thoughts/concepts? 3. Context Sufficiency: Is enough context preserved for accurate retrieval? 4. Boundary Appropriateness: Do chunk boundaries make semantic sense?
A/B Testing Framework
1. Baseline Setup: Establish current chunking strategy performance 2. Metric Selection: Choose relevant metrics (precision@k, user satisfaction) 3. Sample Size: Ensure statistical significance (typically 1000+ queries) 4. Duration: Run for sufficient time to capture usage patterns 5. Analysis: Statistical significance testing and practical effect size
Cost-Benefit Analysis
Development Costs
- Fixed-Size: 1 developer-day
- Sentence-Based: 3-5 developer-days
- Paragraph-Based: 3-5 developer-days
- Semantic: 10-15 developer-days
- Recursive: 15-20 developer-days
Operational Costs
- Processing overhead: Semantic chunking 3-5x slower than fixed-size
- Storage overhead: Variable-size chunks may waste storage slots
- Maintenance overhead: Complex strategies require more monitoring
Quality Benefits
- Retrieval accuracy improvement: 10-30% for semantic vs fixed-size
- User satisfaction: Measurable improvement with better chunk boundaries
- Downstream task performance: Better chunks improve generation quality
Conclusion
The optimal chunking strategy depends on your specific use case:
- Speed-critical systems: Fixed-size chunking
- General-purpose applications: Sentence-based chunking
- High-quality requirements: Semantic or recursive chunking
- Mixed environments: Adaptive strategy selection
Consider implementing multiple strategies and A/B testing to determine the best approach for your specific document corpus and user queries.
Embedding Model Benchmark (historical snapshot, 2024)
Staleness warning: This benchmark is a dated snapshot. Model names, scores, and especially prices rot quickly —text-embedding-ada-002is legacy, and newer families (OpenAItext-embedding-3-*, Voyagevoyage-3/voyage-code-3, Cohereembed-v4) have superseded several entries. Treat the methodology (dimensions vs. quality vs. cost trade-offs, NDCG@10 comparison protocol) as the durable content; verify current model IDs and per-token pricing against the providers' live pricing pages before recommending anything.
Executive Summary
This comprehensive benchmark evaluates 15 popular embedding models across multiple dimensions including retrieval quality, processing speed, memory usage, and cost. Results are based on evaluation across 5 diverse datasets totaling 2M+ documents and 50K queries.
Models Evaluated
OpenAI Models
- text-embedding-ada-002 (1536 dim) - Latest general-purpose model
- text-embedding-3-small (1536 dim) - Optimized for speed/cost
- text-embedding-3-large (3072 dim) - Maximum quality
Sentence Transformers (Open Source)
- all-mpnet-base-v2 (768 dim) - High-quality general purpose
- all-MiniLM-L6-v2 (384 dim) - Fast and compact
- all-MiniLM-L12-v2 (384 dim) - Better quality than L6
- paraphrase-multilingual-mpnet-base-v2 (768 dim) - Multilingual
- multi-qa-mpnet-base-dot-v1 (768 dim) - Optimized for Q&A
Specialized Models
- sentence-transformers/msmarco-distilbert-base-v4 (768 dim) - Search-optimized
- intfloat/e5-large-v2 (1024 dim) - State-of-the-art open source
- BAAI/bge-large-en-v1.5 (1024 dim) - Chinese team, excellent performance
- thenlper/gte-large (1024 dim) - Recent high-performer
Domain-Specific Models
- microsoft/codebert-base (768 dim) - Code embeddings
- allenai/scibert_scivocab_uncased (768 dim) - Scientific text
- microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract (768 dim) - Biomedical
Evaluation Methodology
Datasets Used
1. MS MARCO Passage Ranking (8.8M passages, 6,980 queries)
- General web search scenarios
- Factual and informational queries
2. Natural Questions (307K passages, 3,452 queries)
- Wikipedia-based question answering
- Natural language queries
3. TREC-COVID (171K scientific papers, 50 queries)
- Biomedical/scientific literature search
- Technical domain knowledge
4. FiQA-2018 (57K forum posts, 648 queries)
- Financial domain question answering
- Domain-specific terminology
5. ArguAna (8.67K arguments, 1,406 queries)
- Counter-argument retrieval
- Reasoning and argumentation
Metrics Calculated
- Retrieval Quality: NDCG@10, MRR@10, Recall@100
- Speed: Queries per second, documents per second (encoding)
- Memory: Peak RAM usage, model size on disk
- Cost: API costs (for commercial models) or compute costs (for self-hosted)
Hardware Setup
- CPU: Intel Xeon Gold 6248 (40 cores)
- GPU: NVIDIA V100 32GB (for transformer models)
- RAM: 256GB DDR4
- Storage: NVMe SSD
Results Overview
Retrieval Quality Rankings
| Rank | Model | NDCG@10 | MRR@10 | Recall@100 | Overall Score |
|---|---|---|---|---|---|
| 1 | text-embedding-3-large | 0.594 | 0.431 | 0.892 | 0.639 |
| 2 | BAAI/bge-large-en-v1.5 | 0.588 | 0.425 | 0.885 | 0.633 |
| 3 | intfloat/e5-large-v2 | 0.582 | 0.419 | 0.878 | 0.626 |
| 4 | text-embedding-ada-002 | 0.578 | 0.415 | 0.871 | 0.621 |
| 5 | thenlper/gte-large | 0.571 | 0.408 | 0.865 | 0.615 |
| 6 | all-mpnet-base-v2 | 0.543 | 0.385 | 0.824 | 0.584 |
| 7 | multi-qa-mpnet-base-dot-v1 | 0.538 | 0.381 | 0.818 | 0.579 |
| 8 | text-embedding-3-small | 0.535 | 0.378 | 0.815 | 0.576 |
| 9 | msmarco-distilbert-base-v4 | 0.529 | 0.372 | 0.805 | 0.569 |
| 10 | all-MiniLM-L12-v2 | 0.498 | 0.348 | 0.765 | 0.537 |
| 11 | all-MiniLM-L6-v2 | 0.476 | 0.331 | 0.738 | 0.515 |
| 12 | paraphrase-multilingual-mpnet | 0.465 | 0.324 | 0.729 | 0.506 |
Speed Performance
| Model | Encoding Speed (docs/sec) | Query Speed (queries/sec) | Latency (ms) |
|---|---|---|---|
| all-MiniLM-L6-v2 | 14,200 | 2,850 | 0.35 |
| all-MiniLM-L12-v2 | 8,950 | 1,790 | 0.56 |
| text-embedding-3-small | 8,500* | 1,700* | 0.59* |
| msmarco-distilbert-base-v4 | 6,800 | 1,360 | 0.74 |
| all-mpnet-base-v2 | 2,840 | 568 | 1.76 |
| multi-qa-mpnet-base-dot-v1 | 2,760 | 552 | 1.81 |
| text-embedding-ada-002 | 2,500* | 500* | 2.00* |
| paraphrase-multilingual-mpnet | 2,650 | 530 | 1.89 |
| thenlper/gte-large | 1,420 | 284 | 3.52 |
| intfloat/e5-large-v2 | 1,380 | 276 | 3.62 |
| BAAI/bge-large-en-v1.5 | 1,350 | 270 | 3.70 |
| text-embedding-3-large | 1,200* | 240* | 4.17* |
*API-based models - speeds include network latency
Memory Usage
| Model | Model Size (MB) | Peak RAM (GB) | GPU VRAM (GB) |
|---|---|---|---|
| all-MiniLM-L6-v2 | 91 | 1.2 | 2.1 |
| all-MiniLM-L12-v2 | 134 | 1.8 | 3.2 |
| msmarco-distilbert-base-v4 | 268 | 2.4 | 4.8 |
| all-mpnet-base-v2 | 438 | 3.2 | 6.4 |
| multi-qa-mpnet-base-dot-v1 | 438 | 3.2 | 6.4 |
| paraphrase-multilingual-mpnet | 438 | 3.2 | 6.4 |
| thenlper/gte-large | 670 | 4.8 | 8.6 |
| intfloat/e5-large-v2 | 670 | 4.8 | 8.6 |
| BAAI/bge-large-en-v1.5 | 670 | 4.8 | 8.6 |
| OpenAI Models | N/A | 0.1 | 0.0 |
Cost Analysis (1M tokens processed)
| Model | Type | Cost per 1M tokens | Monthly Cost (10M tokens) |
|---|---|---|---|
| text-embedding-3-small | API | $0.02 | $0.20 |
| text-embedding-ada-002 | API | $0.10 | $1.00 |
| text-embedding-3-large | API | $1.30 | $13.00 |
| all-MiniLM-L6-v2 | Self-hosted | $0.05 | $0.50 |
| all-MiniLM-L12-v2 | Self-hosted | $0.08 | $0.80 |
| all-mpnet-base-v2 | Self-hosted | $0.15 | $1.50 |
| intfloat/e5-large-v2 | Self-hosted | $0.25 | $2.50 |
| BAAI/bge-large-en-v1.5 | Self-hosted | $0.25 | $2.50 |
| thenlper/gte-large | Self-hosted | $0.25 | $2.50 |
*Self-hosted costs include compute, not including initial setup
Detailed Analysis
Quality vs Speed Trade-offs
High Performance Tier (NDCG@10 > 0.57):
- text-embedding-3-large: Best quality, expensive, slow
- BAAI/bge-large-en-v1.5: Excellent quality, free, moderate speed
- intfloat/e5-large-v2: Great quality, free, moderate speed
Balanced Tier (NDCG@10 = 0.54-0.57):
- all-mpnet-base-v2: Good quality-speed balance, widely adopted
- text-embedding-ada-002: Good quality, reasonable API cost
- multi-qa-mpnet-base-dot-v1: Q&A optimized, good for RAG
Speed Tier (NDCG@10 = 0.47-0.54):
- all-MiniLM-L12-v2: Best small model, good for real-time
- all-MiniLM-L6-v2: Fastest processing, acceptable quality
Domain-Specific Performance
Scientific/Technical Documents (TREC-COVID)
1. allenai/scibert: 0.612 NDCG@10 (+15% vs general models) 2. text-embedding-3-large: 0.589 NDCG@10 3. BAAI/bge-large-en-v1.5: 0.581 NDCG@10
Code Search (Custom CodeSearchNet evaluation)
1. microsoft/codebert-base: 0.547 NDCG@10 (+22% vs general models) 2. text-embedding-ada-002: 0.492 NDCG@10 3. all-mpnet-base-v2: 0.478 NDCG@10
Financial Domain (FiQA-2018)
1. text-embedding-3-large: 0.573 NDCG@10 2. intfloat/e5-large-v2: 0.567 NDCG@10 3. BAAI/bge-large-en-v1.5: 0.561 NDCG@10
Multilingual Capabilities
Tested on translated versions of Natural Questions (Spanish, French, German):
| Model | English NDCG@10 | Multilingual Avg | Degradation |
|---|---|---|---|
| paraphrase-multilingual-mpnet | 0.465 | 0.448 | 3.7% |
| text-embedding-3-large | 0.594 | 0.521 | 12.3% |
| text-embedding-ada-002 | 0.578 | 0.495 | 14.4% |
| intfloat/e5-large-v2 | 0.582 | 0.483 | 17.0% |
Recommendations by Use Case
High-Volume Production Systems
Primary: BAAI/bge-large-en-v1.5
- Excellent quality (2nd best overall)
- No API costs or rate limits
- Reasonable resource requirements
Secondary: intfloat/e5-large-v2
- Very close quality to bge-large
- Active development community
- Good documentation
Cost-Sensitive Applications
Primary: all-MiniLM-L6-v2
- Lowest operational cost
- Fastest processing
- Acceptable quality for many use cases
Secondary: text-embedding-3-small
- Better quality than MiniLM
- Competitive API pricing
- No infrastructure overhead
Maximum Quality Requirements
Primary: text-embedding-3-large
- Best overall quality
- Latest OpenAI technology
- Worth the cost for critical applications
Secondary: BAAI/bge-large-en-v1.5
- Nearly equivalent quality
- No ongoing API costs
- Full control over deployment
Real-Time Applications (< 100ms latency)
Primary: all-MiniLM-L6-v2
- Sub-millisecond inference
- Small memory footprint
- Easy to scale horizontally
Alternative: text-embedding-3-small (if API latency acceptable)
- Better quality than MiniLM
- Reasonable API speed
- No infrastructure management
Domain-Specific Applications
Scientific/Research: 1. Domain-specific model (SciBERT, BioBERT) if available 2. text-embedding-3-large for general scientific content 3. intfloat/e5-large-v2 as open-source alternative
Code/Technical: 1. microsoft/codebert-base for code search 2. text-embedding-ada-002 for mixed code/text 3. all-mpnet-base-v2 for technical documentation
Multilingual: 1. paraphrase-multilingual-mpnet-base-v2 for balanced multilingual 2. text-embedding-3-large with translation pipeline 3. Language-specific models when available
Implementation Guidelines
Model Selection Framework
1. Define Quality Requirements
- Minimum acceptable NDCG@10 threshold
- Critical vs non-critical application
- User tolerance for imperfect results
2. Assess Performance Requirements
- Expected queries per second
- Latency requirements (real-time vs batch)
- Concurrent user load
3. Evaluate Resource Constraints
- Available GPU memory
- CPU capabilities
- Network bandwidth (for API models)
4. Consider Operational Factors
- Team expertise with model deployment
- Monitoring and maintenance capabilities
- Vendor lock-in tolerance
Deployment Patterns
Single Model Deployment:
- Simplest approach
- Choose one model for all use cases
- Optimize infrastructure for that model
Tiered Deployment:
- Fast model for initial filtering (MiniLM)
- High-quality model for reranking (bge-large)
- Balance speed and quality
Domain-Specific Routing:
- Route queries to specialized models
- Code queries → CodeBERT
- Scientific queries → SciBERT
- General queries → general model
A/B Testing Strategy
1. Baseline Establishment
- Current model performance metrics
- User satisfaction baselines
- System performance baselines
2. Gradual Rollout
- 5% traffic to new model initially
- Monitor key metrics closely
- Gradual increase if positive results
3. Key Metrics to Track
- Retrieval quality (NDCG, MRR)
- User engagement (click-through rates)
- System performance (latency, errors)
- Cost metrics (API calls, compute usage)
Future Considerations
Emerging Trends
1. Instruction-Tuned Embeddings: Models fine-tuned for specific instruction types 2. Multimodal Embeddings: Text + image + audio embeddings 3. Extreme Efficiency: Sub-100MB models with competitive quality 4. Dynamic Embeddings: Context-aware embeddings that adapt to queries
Model Evolution Tracking
OpenAI: Regular model updates, expect 2-3 new releases per year Open Source: Rapid innovation, new SOTA models every 3-6 months Specialized Models: Domain-specific models becoming more common
Performance Optimization
1. Quantization: 8-bit and 4-bit quantization for memory efficiency 2. ONNX Optimization: Convert models for faster inference 3. Model Distillation: Create smaller, faster versions of large models 4. Batch Optimization: Optimize for batch processing vs single queries
Conclusion
The embedding model landscape offers excellent options across all use cases:
- Quality Leaders: text-embedding-3-large, bge-large-en-v1.5, e5-large-v2
- Speed Champions: all-MiniLM-L6-v2, text-embedding-3-small
- Cost Optimized: Open source models (bge, e5, mpnet series)
- Specialized: Domain-specific models when available
The key is matching your specific requirements to the right model characteristics. Consider starting with BAAI/bge-large-en-v1.5 as a strong general-purpose choice, then optimize based on your specific needs and constraints.
RAG Evaluation Framework
Overview
Evaluating Retrieval-Augmented Generation (RAG) systems requires a comprehensive approach that measures both retrieval quality and generation performance. This framework provides methodologies, metrics, and tools for systematic RAG evaluation across different stages of the pipeline.
Evaluation Dimensions
1. Retrieval Quality (Information Retrieval Metrics)
Precision@K: Fraction of retrieved documents that are relevant
- Formula:
Precision@K = Relevant Retrieved@K / K - Use Case: Measuring result quality at different cutoff points
- Target Values: >0.7 for K=1, >0.5 for K=5, >0.3 for K=10
Recall@K: Fraction of relevant documents that are retrieved
- Formula:
Recall@K = Relevant Retrieved@K / Total Relevant - Use Case: Measuring coverage of relevant information
- Target Values: >0.8 for K=10, >0.9 for K=20
Mean Reciprocal Rank (MRR): Average reciprocal rank of first relevant result
- Formula:
MRR = (1/Q) × Σ(1/rank_i)where rank_i is position of first relevant result - Use Case: Measuring how quickly users find relevant information
- Target Values: >0.6 for good systems, >0.8 for excellent systems
Normalized Discounted Cumulative Gain (NDCG@K): Position-aware relevance metric
- Formula:
NDCG@K = DCG@K / IDCG@K - Use Case: Penalizing relevant documents that appear lower in rankings
- Target Values: >0.7 for K=5, >0.6 for K=10
2. Generation Quality (RAG-Specific Metrics)
Faithfulness: How well the generated answer is grounded in retrieved context
- Measurement: NLI-based entailment scoring, fact verification
- Implementation: Check if each claim in answer is supported by context
- Target Values: >0.95 for factual systems, >0.85 for general applications
Answer Relevance: How well the generated answer addresses the original question
- Measurement: Semantic similarity between question and answer
- Implementation: Embedding similarity, keyword overlap, LLM-as-judge
- Target Values: >0.8 for focused answers, >0.7 for comprehensive responses
Context Relevance: How relevant the retrieved context is to the question
- Measurement: Relevance scoring of each retrieved chunk
- Implementation: Question-context similarity, manual annotation
- Target Values: >0.7 for average relevance of top-5 chunks
Context Precision: Fraction of relevant sentences in retrieved context
- Measurement: Sentence-level relevance annotation
- Implementation: Binary classification of each sentence's relevance
- Target Values: >0.6 for efficient context usage
Context Recall: Coverage of necessary information for answering the question
- Measurement: Whether all required facts are present in context
- Implementation: Expert annotation or automated fact extraction
- Target Values: >0.8 for comprehensive coverage
3. End-to-End Quality
Correctness: Factual accuracy of the generated answer
- Measurement: Expert evaluation, automated fact-checking
- Implementation: Compare against ground truth, verify claims
- Scoring: Binary (correct/incorrect) or scaled (1-5)
Completeness: Whether the answer addresses all aspects of the question
- Measurement: Coverage of question components
- Implementation: Aspect-based evaluation, expert annotation
- Scoring: Fraction of question aspects covered
Helpfulness: Overall utility of the response to the user
- Measurement: User ratings, task completion rates
- Implementation: Human evaluation, A/B testing
- Scoring: 1-5 Likert scale or thumbs up/down
Evaluation Methodologies
1. Offline Evaluation
Dataset Requirements:
- Diverse query set (100+ queries for statistical significance)
- Ground truth relevance judgments
- Reference answers (for generation evaluation)
- Representative document corpus
Evaluation Pipeline: 1. Query Processing: Standardize query format and preprocessing 2. Retrieval Execution: Run retrieval with consistent parameters 3. Generation Execution: Generate answers using retrieved context 4. Metric Calculation: Compute all relevant metrics 5. Statistical Analysis: Significance testing, confidence intervals
Best Practices:
- Stratify queries by type (factual, analytical, conversational)
- Include edge cases (ambiguous queries, no-answer situations)
- Use multiple annotators with inter-rater agreement analysis
- Regular re-evaluation as system evolves
2. Online Evaluation (A/B Testing)
Metrics to Track:
- User engagement: Click-through rates, time on page
- User satisfaction: Explicit ratings, implicit feedback
- Task completion: Success rates for specific user goals
- System performance: Latency, error rates
Experimental Design:
- Randomized assignment to treatment/control groups
- Sufficient sample size (typically 1000+ users per group)
- Runtime duration (1-4 weeks for stable results)
- Proper randomization and bias mitigation
3. Human Evaluation
Evaluation Aspects:
- Factual Accuracy: Is the information correct?
- Relevance: Does the answer address the question?
- Completeness: Are all aspects covered?
- Clarity: Is the answer easy to understand?
- Conciseness: Is the answer appropriately brief?
Annotation Guidelines:
- Clear scoring rubrics (e.g., 1-5 scales with examples)
- Multiple annotators per sample (typically 3-5)
- Training and calibration sessions
- Regular quality checks and inter-rater agreement
Implementation Framework
1. Automated Evaluation Pipeline
class RAGEvaluator:
def __init__(self, retriever, generator, metrics_config):
self.retriever = retriever
self.generator = generator
self.metrics = self._initialize_metrics(metrics_config)
def evaluate_query(self, query, ground_truth):
# Retrieval evaluation
retrieved_docs = self.retriever.search(query)
retrieval_metrics = self.evaluate_retrieval(
retrieved_docs, ground_truth['relevant_docs']
)
# Generation evaluation
generated_answer = self.generator.generate(query, retrieved_docs)
generation_metrics = self.evaluate_generation(
query, generated_answer, retrieved_docs, ground_truth['answer']
)
return {**retrieval_metrics, **generation_metrics}2. Metric Implementations
Faithfulness Score:
def calculate_faithfulness(answer, context):
# Split answer into claims
claims = extract_claims(answer)
# Check each claim against context
faithful_claims = 0
for claim in claims:
if is_supported_by_context(claim, context):
faithful_claims += 1
return faithful_claims / len(claims) if claims else 0Context Relevance Score:
def calculate_context_relevance(query, contexts):
relevance_scores = []
for context in contexts:
similarity = embedding_similarity(query, context)
relevance_scores.append(similarity)
return {
'average_relevance': mean(relevance_scores),
'top_k_relevance': mean(relevance_scores[:k]),
'relevance_distribution': relevance_scores
}3. Evaluation Dataset Creation
Query Collection Strategies: 1. User Log Analysis: Extract real user queries from production systems 2. Expert Generation: Domain experts create representative queries 3. Synthetic Generation: LLM-generated queries based on document content 4. Community Sourcing: Crowdsourced query collection
Ground Truth Creation: 1. Document Relevance: Expert annotation of relevant documents per query 2. Answer Creation: Expert-written reference answers 3. Aspect Annotation: Mark which aspects of complex questions are addressed 4. Quality Control: Multiple annotators with disagreement resolution
Evaluation Datasets and Benchmarks
1. General Domain Benchmarks
MS MARCO: Large-scale reading comprehension dataset
- 100K real user queries from Bing search
- Passage-level and document-level evaluation
- Both retrieval and generation evaluation supported
Natural Questions: Google search queries with Wikipedia answers
- 307K training examples, 8K development examples
- Natural language questions from real users
- Both short and long answer evaluation
SQUAD 2.0: Reading comprehension with unanswerable questions
- 150K question-answer pairs
- Includes questions that cannot be answered from context
- Tests system's ability to recognize unanswerable queries
2. Domain-Specific Benchmarks
TREC-COVID: Scientific literature search
- 50 queries on COVID-19 research topics
- 171K scientific papers as corpus
- Expert relevance judgments
FiQA: Financial question answering
- 648 questions from financial forums
- 57K financial forum posts as corpus
- Domain-specific terminology and concepts
BioASQ: Biomedical semantic indexing and question answering
- 3K biomedical questions
- PubMed abstracts as corpus
- Expert physician annotations
3. Multilingual Benchmarks
Mr. TyDi: Multilingual question answering
- 11 languages including Arabic, Bengali, Korean
- Wikipedia passages in each language
- Cultural and linguistic diversity testing
MLQA: Cross-lingual question answering
- Questions in one language, answers in another
- 7 languages with all pair combinations
- Tests multilingual retrieval capabilities
Continuous Evaluation Framework
1. Monitoring Pipeline
Real-time Metrics:
- System latency (p50, p95, p99)
- Error rates and failure modes
- User satisfaction scores
- Query volume and patterns
Batch Evaluation:
- Weekly/monthly evaluation on test sets
- Performance trend analysis
- Regression detection
- Model drift monitoring
2. Quality Assurance
Automated Quality Checks:
- Hallucination detection
- Toxicity and bias screening
- Factual consistency verification
- Output format validation
Human Review Process:
- Random sampling of responses (1-5% of production queries)
- Expert review of edge cases and failures
- User feedback integration
- Regular calibration of automated metrics
3. Performance Optimization
A/B Testing Framework:
- Infrastructure for controlled experiments
- Statistical significance testing
- Multi-armed bandit optimization
- Gradual rollout procedures
Feedback Loop Integration:
- User feedback incorporation into training data
- Error analysis and root cause identification
- Iterative improvement processes
- Model fine-tuning based on evaluation results
Tools and Libraries
1. Open Source Tools
RAGAS: RAG Assessment framework
- Comprehensive metric implementations
- Easy integration with popular RAG frameworks
- Support for both synthetic and human evaluation
TruEra TruLens: ML observability for RAG
- Real-time monitoring and evaluation
- Comprehensive metric tracking
- Integration with popular vector databases
LangSmith: LangChain evaluation and monitoring
- End-to-end RAG pipeline evaluation
- Human feedback integration
- Performance analytics and debugging
2. Commercial Solutions
Weights & Biases: ML experiment tracking
- A/B testing infrastructure
- Comprehensive metrics dashboard
- Team collaboration features
Neptune: ML metadata store
- Experiment comparison and analysis
- Model performance monitoring
- Integration with popular ML frameworks
Comet: ML platform for tracking experiments
- Real-time monitoring
- Model comparison and selection
- Automated report generation
Best Practices
1. Evaluation Design
Metric Selection:
- Choose metrics aligned with business objectives
- Use multiple complementary metrics
- Include both automated and human evaluation
- Consider computational cost vs. insight value
Dataset Preparation:
- Ensure representative query distribution
- Include edge cases and failure modes
- Maintain high annotation quality
- Regular dataset updates and validation
2. Statistical Rigor
Sample Sizes:
- Minimum 100 queries for basic evaluation
- 1000+ queries for robust statistical analysis
- Power analysis for A/B testing
- Confidence interval reporting
Significance Testing:
- Use appropriate statistical tests (t-tests, Mann-Whitney U)
- Multiple comparison corrections (Bonferroni, FDR)
- Effect size reporting alongside p-values
- Bootstrap confidence intervals for stability
3. Operational Integration
Automated Pipelines:
- Continuous integration/deployment integration
- Automated regression testing
- Performance threshold enforcement
- Alert systems for quality degradation
Human-in-the-Loop:
- Regular expert review processes
- User feedback collection and analysis
- Annotation quality control
- Bias detection and mitigation
Common Pitfalls and Solutions
1. Evaluation Bias
Problem: Test set not representative of production queries Solution: Continuous test set updates from production data
Problem: Annotator bias in relevance judgments Solution: Multiple annotators, clear guidelines, bias training
2. Metric Gaming
Problem: Optimizing for metrics rather than user satisfaction Solution: Multiple complementary metrics, regular metric validation
Problem: Overfitting to evaluation set Solution: Hold-out validation sets, temporal splits
3. Scale Challenges
Problem: Evaluation becomes too expensive at scale Solution: Sampling strategies, automated metrics, efficient tooling
Problem: Human evaluation bottlenecks Solution: Active learning for annotation, LLM-as-judge validation
Future Directions
1. Advanced Metrics
- Semantic Coherence: Measuring logical flow in generated answers
- Factual Consistency: Cross-document fact verification
- Personalization Quality: User-specific relevance assessment
- Multimodal Evaluation: Text, image, audio integration metrics
2. Automated Evaluation
- LLM-as-Judge: Using large language models for quality assessment
- Adversarial Testing: Systematic stress testing of RAG systems
- Causal Evaluation: Understanding why systems fail
- Real-time Adaptation: Dynamic metric adjustment based on context
3. Holistic Assessment
- User Journey Evaluation: Multi-turn conversation quality
- Task Success Measurement: Goal completion rather than single query
- Temporal Consistency: Performance stability over time
- Fairness and Bias: Systematic bias detection and measurement
Conclusion
Effective RAG evaluation requires a multi-faceted approach combining automated metrics, human judgment, and continuous monitoring. The key principles are:
1. Comprehensive Coverage: Evaluate all pipeline components 2. Multiple Perspectives: Combine different evaluation methodologies 3. Continuous Improvement: Regular evaluation and iteration 4. Business Alignment: Metrics should reflect actual user value 5. Statistical Rigor: Proper experimental design and analysis
This framework provides the foundation for building robust, high-quality RAG systems that deliver real value to users while maintaining reliability and trustworthiness.
#!/usr/bin/env python3
"""
Retrieval Evaluator - Evaluates retrieval quality using standard IR metrics.
This script evaluates retrieval system performance using standard information retrieval
metrics including precision@k, recall@k, MRR, and NDCG. It uses a built-in TF-IDF
implementation as a baseline retrieval system.
Metrics calculated:
- Precision@K: Fraction of retrieved documents that are relevant
- Recall@K: Fraction of relevant documents that are retrieved
- Mean Reciprocal Rank (MRR): Average reciprocal rank of first relevant result
- Normalized Discounted Cumulative Gain (NDCG): Ranking quality with position discount
No external dependencies - uses only Python standard library.
"""
import argparse
import json
import math
import os
import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Dict, List, Tuple, Set, Any, Optional
class Document:
"""Represents a document in the corpus."""
def __init__(self, doc_id: str, title: str, content: str, path: str = ""):
self.doc_id = doc_id
self.title = title
self.content = content
self.path = path
self.tokens = self._tokenize(content)
self.token_count = len(self.tokens)
def _tokenize(self, text: str) -> List[str]:
"""Simple tokenization - split on whitespace and punctuation."""
# Convert to lowercase and extract words
tokens = re.findall(r'\b[a-zA-Z0-9]+\b', text.lower())
return tokens
def __str__(self):
return f"Document({self.doc_id}, '{self.title[:50]}...', {self.token_count} tokens)"
class TFIDFRetriever:
"""TF-IDF based retrieval system - no external dependencies."""
def __init__(self, documents: List[Document]):
self.documents = {doc.doc_id: doc for doc in documents}
self.doc_ids = list(self.documents.keys())
self.vocabulary = set()
self.tf_scores = {} # doc_id -> {term: tf_score}
self.df_scores = {} # term -> document_frequency
self.idf_scores = {} # term -> idf_score
self._build_index()
def _build_index(self):
"""Build TF-IDF index from documents."""
print(f"Building TF-IDF index for {len(self.documents)} documents...")
# Calculate term frequencies and build vocabulary
for doc_id, doc in self.documents.items():
term_counts = Counter(doc.tokens)
doc_length = len(doc.tokens)
# Calculate TF scores (term_count / doc_length)
tf_scores = {}
for term, count in term_counts.items():
tf_scores[term] = count / doc_length if doc_length > 0 else 0
self.vocabulary.add(term)
self.tf_scores[doc_id] = tf_scores
# Calculate document frequencies
for term in self.vocabulary:
df = sum(1 for doc in self.documents.values() if term in doc.tokens)
self.df_scores[term] = df
# Calculate IDF scores: log(N / df)
num_docs = len(self.documents)
for term, df in self.df_scores.items():
self.idf_scores[term] = math.log(num_docs / df) if df > 0 else 0
def search(self, query: str, k: int = 10) -> List[Tuple[str, float]]:
"""Search for documents matching the query using TF-IDF similarity."""
query_tokens = re.findall(r'\b[a-zA-Z0-9]+\b', query.lower())
if not query_tokens:
return []
# Calculate query TF scores
query_tf = Counter(query_tokens)
query_length = len(query_tokens)
# Calculate TF-IDF similarity for each document
scores = {}
for doc_id in self.doc_ids:
score = self._calculate_similarity(query_tf, query_length, doc_id)
if score > 0:
scores[doc_id] = score
# Sort by score and return top k
sorted_results = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return sorted_results[:k]
def _calculate_similarity(self, query_tf: Counter, query_length: int, doc_id: str) -> float:
"""Calculate cosine similarity between query and document using TF-IDF."""
doc_tf = self.tf_scores[doc_id]
# Calculate TF-IDF vectors
query_vector = []
doc_vector = []
# Only consider terms that appear in both query and document
common_terms = set(query_tf.keys()) & set(doc_tf.keys())
if not common_terms:
return 0.0
for term in common_terms:
# Query TF-IDF
q_tf = query_tf[term] / query_length
q_tfidf = q_tf * self.idf_scores.get(term, 0)
query_vector.append(q_tfidf)
# Document TF-IDF
d_tfidf = doc_tf[term] * self.idf_scores.get(term, 0)
doc_vector.append(d_tfidf)
# Cosine similarity
dot_product = sum(q * d for q, d in zip(query_vector, doc_vector))
query_norm = math.sqrt(sum(q * q for q in query_vector))
doc_norm = math.sqrt(sum(d * d for d in doc_vector))
if query_norm == 0 or doc_norm == 0:
return 0.0
return dot_product / (query_norm * doc_norm)
class RetrievalEvaluator:
"""Evaluates retrieval system performance using standard IR metrics."""
def __init__(self):
self.metrics = {}
def evaluate(self, queries: List[Dict[str, Any]], ground_truth: Dict[str, List[str]],
retriever: TFIDFRetriever, k_values: List[int] = None) -> Dict[str, Any]:
"""Evaluate retrieval performance."""
k_values = k_values or [1, 3, 5, 10]
print(f"Evaluating retrieval performance for {len(queries)} queries...")
query_results = []
all_precision_at_k = {k: [] for k in k_values}
all_recall_at_k = {k: [] for k in k_values}
all_ndcg_at_k = {k: [] for k in k_values}
reciprocal_ranks = []
for query_data in queries:
query_id = query_data['id']
query_text = query_data['query']
# Get ground truth for this query
relevant_docs = set(ground_truth.get(query_id, []))
if not relevant_docs:
print(f"Warning: No ground truth found for query {query_id}")
continue
# Retrieve documents
max_k = max(k_values)
results = retriever.search(query_text, max_k)
retrieved_doc_ids = [doc_id for doc_id, _ in results]
# Calculate metrics for this query
query_metrics = {}
# Precision@K and Recall@K
for k in k_values:
retrieved_at_k = set(retrieved_doc_ids[:k])
relevant_retrieved = retrieved_at_k & relevant_docs
precision = len(relevant_retrieved) / len(retrieved_at_k) if retrieved_at_k else 0
recall = len(relevant_retrieved) / len(relevant_docs) if relevant_docs else 0
query_metrics[f'precision@{k}'] = precision
query_metrics[f'recall@{k}'] = recall
all_precision_at_k[k].append(precision)
all_recall_at_k[k].append(recall)
# Mean Reciprocal Rank (MRR)
reciprocal_rank = self._calculate_reciprocal_rank(retrieved_doc_ids, relevant_docs)
query_metrics['reciprocal_rank'] = reciprocal_rank
reciprocal_ranks.append(reciprocal_rank)
# NDCG@K
for k in k_values:
ndcg = self._calculate_ndcg(retrieved_doc_ids[:k], relevant_docs)
query_metrics[f'ndcg@{k}'] = ndcg
all_ndcg_at_k[k].append(ndcg)
# Store query-level results
query_results.append({
'query_id': query_id,
'query': query_text,
'relevant_count': len(relevant_docs),
'retrieved_count': len(retrieved_doc_ids),
'metrics': query_metrics,
'retrieved_docs': results[:5], # Top 5 for analysis
'relevant_docs': list(relevant_docs)
})
# Calculate aggregate metrics
aggregate_metrics = {}
for k in k_values:
aggregate_metrics[f'mean_precision@{k}'] = self._safe_mean(all_precision_at_k[k])
aggregate_metrics[f'mean_recall@{k}'] = self._safe_mean(all_recall_at_k[k])
aggregate_metrics[f'mean_ndcg@{k}'] = self._safe_mean(all_ndcg_at_k[k])
aggregate_metrics['mean_reciprocal_rank'] = self._safe_mean(reciprocal_ranks)
# Failure analysis
failure_analysis = self._analyze_failures(query_results)
return {
'aggregate_metrics': aggregate_metrics,
'query_results': query_results,
'failure_analysis': failure_analysis,
'evaluation_summary': self._generate_summary(aggregate_metrics, len(queries))
}
def _calculate_reciprocal_rank(self, retrieved_docs: List[str], relevant_docs: Set[str]) -> float:
"""Calculate reciprocal rank - 1/rank of first relevant document."""
for i, doc_id in enumerate(retrieved_docs):
if doc_id in relevant_docs:
return 1.0 / (i + 1)
return 0.0
def _calculate_ndcg(self, retrieved_docs: List[str], relevant_docs: Set[str]) -> float:
"""Calculate Normalized Discounted Cumulative Gain."""
if not retrieved_docs:
return 0.0
# DCG calculation
dcg = 0.0
for i, doc_id in enumerate(retrieved_docs):
relevance = 1 if doc_id in relevant_docs else 0
dcg += relevance / math.log2(i + 2) # +2 because log2(1) = 0
# IDCG calculation (ideal DCG)
ideal_relevances = [1] * min(len(relevant_docs), len(retrieved_docs))
idcg = sum(rel / math.log2(i + 2) for i, rel in enumerate(ideal_relevances))
return dcg / idcg if idcg > 0 else 0.0
def _safe_mean(self, values: List[float]) -> float:
"""Calculate mean, handling empty lists."""
return sum(values) / len(values) if values else 0.0
def _analyze_failures(self, query_results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze common failure patterns."""
total_queries = len(query_results)
# Identify queries with poor performance
poor_precision_queries = []
poor_recall_queries = []
zero_results_queries = []
for result in query_results:
metrics = result['metrics']
if metrics.get('precision@5', 0) < 0.2:
poor_precision_queries.append(result)
if metrics.get('recall@5', 0) < 0.3:
poor_recall_queries.append(result)
if result['retrieved_count'] == 0:
zero_results_queries.append(result)
# Analyze query characteristics
query_length_analysis = self._analyze_query_lengths(query_results)
return {
'poor_precision_count': len(poor_precision_queries),
'poor_recall_count': len(poor_recall_queries),
'zero_results_count': len(zero_results_queries),
'poor_precision_examples': poor_precision_queries[:3],
'poor_recall_examples': poor_recall_queries[:3],
'query_length_analysis': query_length_analysis,
'common_failure_patterns': self._identify_failure_patterns(query_results)
}
def _analyze_query_lengths(self, query_results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze relationship between query length and performance."""
short_queries = [] # <= 3 words
medium_queries = [] # 4-7 words
long_queries = [] # >= 8 words
for result in query_results:
query_length = len(result['query'].split())
precision = result['metrics'].get('precision@5', 0)
if query_length <= 3:
short_queries.append(precision)
elif query_length <= 7:
medium_queries.append(precision)
else:
long_queries.append(precision)
return {
'short_queries': {
'count': len(short_queries),
'avg_precision@5': self._safe_mean(short_queries)
},
'medium_queries': {
'count': len(medium_queries),
'avg_precision@5': self._safe_mean(medium_queries)
},
'long_queries': {
'count': len(long_queries),
'avg_precision@5': self._safe_mean(long_queries)
}
}
def _identify_failure_patterns(self, query_results: List[Dict[str, Any]]) -> List[str]:
"""Identify common patterns in failed queries."""
patterns = []
# Check for vocabulary mismatch
vocab_mismatch_count = 0
for result in query_results:
if result['metrics'].get('precision@1', 0) == 0 and result['retrieved_count'] > 0:
vocab_mismatch_count += 1
if vocab_mismatch_count > len(query_results) * 0.2:
patterns.append(f"Vocabulary mismatch: {vocab_mismatch_count} queries may have vocabulary mismatch issues")
# Check for specificity issues
zero_results = sum(1 for r in query_results if r['retrieved_count'] == 0)
if zero_results > len(query_results) * 0.1:
patterns.append(f"Query specificity: {zero_results} queries returned no results (may be too specific)")
# Check for recall issues
low_recall = sum(1 for r in query_results if r['metrics'].get('recall@10', 0) < 0.5)
if low_recall > len(query_results) * 0.3:
patterns.append(f"Low recall: {low_recall} queries have recall@10 < 0.5 (missing relevant documents)")
return patterns
def _generate_summary(self, metrics: Dict[str, float], num_queries: int) -> str:
"""Generate human-readable evaluation summary."""
summary = f"Evaluation Summary ({num_queries} queries):\n"
summary += f"{'='*50}\n"
# Key metrics
p1 = metrics.get('mean_precision@1', 0)
p5 = metrics.get('mean_precision@5', 0)
r5 = metrics.get('mean_recall@5', 0)
mrr = metrics.get('mean_reciprocal_rank', 0)
ndcg5 = metrics.get('mean_ndcg@5', 0)
summary += f"Precision@1: {p1:.3f} ({p1*100:.1f}%)\n"
summary += f"Precision@5: {p5:.3f} ({p5*100:.1f}%)\n"
summary += f"Recall@5: {r5:.3f} ({r5*100:.1f}%)\n"
summary += f"MRR: {mrr:.3f}\n"
summary += f"NDCG@5: {ndcg5:.3f}\n"
# Performance assessment
summary += f"\nPerformance Assessment:\n"
if p1 >= 0.7:
summary += "✓ Excellent precision - most queries return relevant results first\n"
elif p1 >= 0.5:
summary += "○ Good precision - many queries return relevant results first\n"
else:
summary += "✗ Poor precision - few queries return relevant results first\n"
if r5 >= 0.8:
summary += "✓ Excellent recall - finding most relevant documents\n"
elif r5 >= 0.6:
summary += "○ Good recall - finding many relevant documents\n"
else:
summary += "✗ Poor recall - missing many relevant documents\n"
return summary
def load_queries(file_path: str) -> List[Dict[str, Any]]:
"""Load queries from JSON file."""
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Handle different JSON formats
if isinstance(data, list):
return data
elif 'queries' in data:
return data['queries']
else:
raise ValueError("Invalid query file format. Expected list of queries or {'queries': [...]}.")
def load_ground_truth(file_path: str) -> Dict[str, List[str]]:
"""Load ground truth relevance judgments."""
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Handle different JSON formats
if isinstance(data, dict):
# Convert all values to lists if they aren't already
return {k: v if isinstance(v, list) else [v] for k, v in data.items()}
else:
raise ValueError("Invalid ground truth format. Expected dict mapping query_id -> relevant_doc_ids.")
def load_corpus(directory: str, extensions: List[str] = None) -> List[Document]:
"""Load document corpus from directory."""
extensions = extensions or ['.txt', '.md', '.markdown']
documents = []
corpus_path = Path(directory)
if not corpus_path.exists():
raise FileNotFoundError(f"Corpus directory not found: {directory}")
for file_path in corpus_path.rglob('*'):
if file_path.is_file() and file_path.suffix.lower() in extensions:
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
if content.strip():
# Use filename (without extension) as doc_id
doc_id = file_path.stem
title = file_path.name
doc = Document(doc_id, title, content, str(file_path))
documents.append(doc)
except Exception as e:
print(f"Warning: Could not read {file_path}: {e}")
if not documents:
raise ValueError(f"No valid documents found in {directory}")
print(f"Loaded {len(documents)} documents from corpus")
return documents
def generate_recommendations(evaluation_results: Dict[str, Any]) -> List[str]:
"""Generate improvement recommendations based on evaluation results."""
recommendations = []
metrics = evaluation_results['aggregate_metrics']
failure_analysis = evaluation_results['failure_analysis']
# Precision-based recommendations
p1 = metrics.get('mean_precision@1', 0)
p5 = metrics.get('mean_precision@5', 0)
if p1 < 0.3:
recommendations.append("LOW PRECISION: Consider implementing query expansion or reranking to improve result quality.")
if p5 < 0.4:
recommendations.append("RANKING ISSUES: Current ranking may not prioritize relevant documents. Consider BM25 or learning-to-rank models.")
# Recall-based recommendations
r5 = metrics.get('mean_recall@5', 0)
r10 = metrics.get('mean_recall@10', 0)
if r5 < 0.5:
recommendations.append("LOW RECALL: Consider query expansion techniques (synonyms, related terms) to find more relevant documents.")
if r10 - r5 > 0.2:
recommendations.append("RANKING DEPTH: Many relevant documents found in positions 6-10. Consider increasing default result count.")
# MRR-based recommendations
mrr = metrics.get('mean_reciprocal_rank', 0)
if mrr < 0.4:
recommendations.append("POOR RANKING: First relevant result appears late in rankings. Implement result reranking.")
# Failure pattern recommendations
zero_results = failure_analysis.get('zero_results_count', 0)
total_queries = len(evaluation_results['query_results'])
if zero_results > total_queries * 0.1:
recommendations.append("COVERAGE ISSUES: Many queries return no results. Check for vocabulary mismatch or missing content.")
# Query length analysis
query_analysis = failure_analysis.get('query_length_analysis', {})
short_perf = query_analysis.get('short_queries', {}).get('avg_precision@5', 0)
long_perf = query_analysis.get('long_queries', {}).get('avg_precision@5', 0)
if short_perf < 0.3:
recommendations.append("SHORT QUERY ISSUES: Brief queries perform poorly. Consider query completion or suggestion features.")
if long_perf > short_perf + 0.2:
recommendations.append("QUERY PROCESSING: Longer queries perform better. Consider query parsing to extract key terms.")
# General recommendations
if not recommendations:
recommendations.append("GOOD PERFORMANCE: System performs well overall. Consider A/B testing incremental improvements.")
return recommendations
def main():
"""Main function with command-line interface."""
parser = argparse.ArgumentParser(description='Evaluate retrieval system performance')
parser.add_argument('queries', help='JSON file containing queries')
parser.add_argument('corpus', help='Directory containing document corpus')
parser.add_argument('ground_truth', help='JSON file containing ground truth relevance judgments')
parser.add_argument('--output', '-o', help='Output file for results (JSON format)')
parser.add_argument('--k-values', nargs='+', type=int, default=[1, 3, 5, 10],
help='K values for precision@k, recall@k, NDCG@k evaluation')
parser.add_argument('--extensions', nargs='+', default=['.txt', '.md', '.markdown'],
help='File extensions to include from corpus')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
try:
# Load data
print("Loading evaluation data...")
queries = load_queries(args.queries)
ground_truth = load_ground_truth(args.ground_truth)
documents = load_corpus(args.corpus, args.extensions)
print(f"Loaded {len(queries)} queries, {len(documents)} documents, ground truth for {len(ground_truth)} queries")
# Build retrieval system
retriever = TFIDFRetriever(documents)
# Run evaluation
evaluator = RetrievalEvaluator()
results = evaluator.evaluate(queries, ground_truth, retriever, args.k_values)
# Generate recommendations
recommendations = generate_recommendations(results)
results['recommendations'] = recommendations
# Save results
if args.output:
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
print(f"Results saved to {args.output}")
# Print summary
print("\n" + results['evaluation_summary'])
print("\nRecommendations:")
for i, rec in enumerate(recommendations, 1):
print(f"{i}. {rec}")
if args.verbose:
print(f"\nDetailed Metrics:")
for metric, value in results['aggregate_metrics'].items():
print(f" {metric}: {value:.4f}")
print(f"\nFailure Analysis:")
fa = results['failure_analysis']
print(f" Poor precision queries: {fa['poor_precision_count']}")
print(f" Poor recall queries: {fa['poor_recall_count']}")
print(f" Zero result queries: {fa['zero_results_count']}")
except Exception as e:
print(f"Error: {e}")
return 1
return 0
if __name__ == '__main__':
exit(main())Related skills
How it compares
Pick rag-architect when retrieval design and evaluation matter; use lighter doc-fetch skills when the task is only pulling library READMEs into chat.
FAQ
What RAG topics does rag-architect cover?
rag-architect covers document processing and chunking, embedding model selection, vector search implementation, reranking considerations, and retrieval evaluation frameworks. The skill targets production-grade pipelines rather than toy demos.
When should a developer use rag-architect?
rag-architect fits when a developer must design or optimize RAG pipelines, choose embedding models, implement vector search, or build knowledge retrieval systems where retrieval accuracy directly affects LLM output quality.
Is Rag Architect safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.