Module 1: What Are Embeddings?
Mini-Project: Your First Embedding - Similarity Calculator
Project overview
In this mini-project you'll build your first working system using embeddings: a Similarity Calculator that lets you search for documents similar to a query using OpenAI embeddings.
This project consolidates ALL the concepts from Module 1: the definition of embeddings, vector properties, vector spaces, use cases, comparison with keywords, and architecture. By the end you'll have a production-ready CLI tool that you can expand in future modules.
Estimated duration: 30-40 minutes
Project objectives
By completing this project, you'll have:
- ✅ Configured the OpenAI API correctly
- ✅ Generated embeddings for multiple documents
- ✅ Calculated cosine similarity with numpy
- ✅ Implemented a top-K most-similar search
- ✅ Created a working CLI with error handling
- ✅ Cached embeddings (optimization)
Technical specifications
Required features:
- Indexing: Load 10+ documents and generate embeddings
- Query: The user enters a query, the system returns the top-3 most similar
- Scoring: Show the cosine similarity score for each result
- Cache: Save embeddings to a file to reuse
- Error handling: Handle API errors, missing files, etc.
Tech stack:
- Python 3.10+
- OpenAI API (text-embedding-3-small)
- NumPy (vector calculations)
- python-dotenv (environment variables)
- JSON (simple persistence)
Step 1: Project setup
1.1: File structure
embeddings-similarity-calculator/
├── .env # API key (do NOT commit)
├── .gitignore # Ignore .env
├── requirements.txt # Dependencies
├── documents.txt # Corpus (10 documents)
├── embeddings_cache.json # Saved embeddings
└── similarity_calculator.py # Main script
1.2: Create the project folder
mkdir embeddings-similarity-calculator
cd embeddings-similarity-calculator
1.3: Create a virtualenv (recommended)
# Create virtualenv
python -m venv venv
# Activate
# macOS/Linux:
source venv/bin/activate
# Windows:
venv\Scripts\activate
1.4: Install dependencies
Create requirements.txt:
openai==1.12.0
numpy==1.26.3
python-dotenv==1.0.0
Install:
pip install -r requirements.txt
1.5: Configure the OpenAI API key
Create .env:
# .env
OPENAI_API_KEY=sk-proj-your-api-key-here
Create .gitignore:
# .gitignore
.env
venv/
__pycache__/
*.pyc
embeddings_cache.json
⚠️ IMPORTANT: NEVER commit .env with your API key.
Step 2: Create the document corpus
Create documents.txt:
Python is a high-level programming language
JavaScript is the main language for web development
TypeScript adds static types to JavaScript
React is a library for building user interfaces
Vue.js is a progressive framework for building UIs
Django is a Python web framework for the backend
FastAPI is a modern, fast framework for APIs in Python
Node.js lets you run JavaScript on the server
Docker is a platform for containerizing applications
Kubernetes orchestrates containers in production
Each line = 1 document.
Step 3: Implement the Similarity Calculator
Create similarity_calculator.py:
"""
Similarity Calculator - Module 1 Mini-Project
A semantic search system using OpenAI embeddings
"""
import os
import json
import numpy as np
from openai import OpenAI
from dotenv import load_dotenv
from typing import List, Dict, Tuple
# Load environment variables
load_dotenv()
# Configuration
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
EMBEDDING_MODEL = "text-embedding-3-small"
DOCUMENTS_FILE = "documents.txt"
CACHE_FILE = "embeddings_cache.json"
class SimilarityCalculator:
"""
Semantic search system with embeddings
"""
def __init__(self):
"""Initialize the OpenAI client and data structures"""
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY not found in .env")
self.client = OpenAI(api_key=OPENAI_API_KEY)
self.documents: List[str] = []
self.embeddings: Dict[int, List[float]] = {}
def load_documents(self, filepath: str) -> None:
"""
Load documents from a file
Args:
filepath: Path to the documents file
"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
self.documents = [line.strip() for line in f if line.strip()]
print(f"✅ Loaded {len(self.documents)} documents")
except FileNotFoundError:
raise FileNotFoundError(f"File {filepath} not found")
except Exception as e:
raise Exception(f"Error loading documents: {e}")
def get_embedding(self, text: str) -> List[float]:
"""
Generate an embedding for a text
Args:
text: Text to convert into an embedding
Returns:
List of floats (embedding vector)
"""
try:
response = self.client.embeddings.create(
model=EMBEDDING_MODEL,
input=text
)
return response.data[0].embedding
except Exception as e:
print(f"❌ Error generating embedding: {e}")
raise
def generate_embeddings(self, use_cache: bool = True) -> None:
"""
Generate embeddings for all documents
Args:
use_cache: If True, try to load from cache first
"""
# Try to load from cache
if use_cache and self.load_cache():
print("✅ Embeddings loaded from cache")
return
print(f"🔄 Generating embeddings for {len(self.documents)} documents...")
for idx, doc in enumerate(self.documents):
try:
embedding = self.get_embedding(doc)
self.embeddings[idx] = embedding
print(f" [{idx+1}/{len(self.documents)}] Generated")
except Exception as e:
print(f" ❌ Error on document {idx}: {e}")
# Save to cache
self.save_cache()
print("✅ Embeddings generated and saved to cache")
def save_cache(self) -> None:
"""Save embeddings to a JSON file"""
try:
cache_data = {
"documents": self.documents,
"embeddings": {str(k): v for k, v in self.embeddings.items()}
}
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(cache_data, f)
except Exception as e:
print(f"⚠️ Error saving cache: {e}")
def load_cache(self) -> bool:
"""
Load embeddings from the cache
Returns:
True if loaded successfully, False otherwise
"""
try:
if not os.path.exists(CACHE_FILE):
return False
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
# Check that the documents match
if cache_data["documents"] != self.documents:
print("⚠️ Documents changed, regenerating embeddings...")
return False
# Load embeddings
self.embeddings = {
int(k): v for k, v in cache_data["embeddings"].items()
}
return True
except Exception as e:
print(f"⚠️ Error loading cache: {e}")
return False
@staticmethod
def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float:
"""
Calculate cosine similarity between two vectors
Args:
vec_a: First vector
vec_b: Second vector
Returns:
Similarity (0.0 to 1.0)
"""
vec_a = np.array(vec_a)
vec_b = np.array(vec_b)
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
def search(self, query: str, top_k: int = 3) -> List[Tuple[int, str, float]]:
"""
Search for the documents most similar to the query
Args:
query: The user's query
top_k: Number of results to return
Returns:
List of tuples (doc_id, document, similarity)
"""
# Generate the query embedding
print(f"\n🔍 Searching: '{query}'")
query_embedding = self.get_embedding(query)
# Calculate similarities
similarities = []
for doc_id, doc_embedding in self.embeddings.items():
sim = self.cosine_similarity(query_embedding, doc_embedding)
similarities.append((doc_id, self.documents[doc_id], sim))
# Sort by similarity (descending)
similarities.sort(key=lambda x: x[2], reverse=True)
# Return top-K
return similarities[:top_k]
def print_results(self, results: List[Tuple[int, str, float]]) -> None:
"""
Print formatted results
Args:
results: List of results [(doc_id, doc, similarity)]
"""
print("\n📊 Results:")
print("=" * 80)
for rank, (doc_id, doc, similarity) in enumerate(results, 1):
# Visual progress bar
bar_length = int(similarity * 50) # 50 chars max
bar = "█" * bar_length + "░" * (50 - bar_length)
print(f"\n{rank}. [Doc {doc_id}] Similarity: {similarity:.4f}")
print(f" {bar} {similarity:.1%}")
print(f" \"{doc}\"")
print("\n" + "=" * 80)
def main():
"""Main function - interactive CLI"""
print("=" * 80)
print("🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide")
print("=" * 80)
try:
# Initialize
calculator = SimilarityCalculator()
# Load documents
calculator.load_documents(DOCUMENTS_FILE)
# Generate embeddings (uses cache if it exists)
calculator.generate_embeddings(use_cache=True)
# Interactive loop
print("\n" + "=" * 80)
print("💬 Enter your query (or 'exit' to quit)")
print("=" * 80)
while True:
query = input("\nQuery: ").strip()
if query.lower() in ['exit', 'quit']:
print("\n👋 See you later!")
break
if not query:
print("⚠️ Empty query. Try again.")
continue
# Search
results = calculator.search(query, top_k=3)
# Show results
calculator.print_results(results)
except KeyboardInterrupt:
print("\n\n👋 Interrupted. See you later!")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
Step 4: Run the project
First run (without cache):
python similarity_calculator.py
Expected output:
================================================================================
🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide
================================================================================
✅ Loaded 10 documents
🔄 Generating embeddings for 10 documents...
[1/10] Generated
[2/10] Generated
[3/10] Generated
[4/10] Generated
[5/10] Generated
[6/10] Generated
[7/10] Generated
[8/10] Generated
[9/10] Generated
[10/10] Generated
✅ Embeddings generated and saved to cache
================================================================================
💬 Enter your query (or 'exit' to quit)
================================================================================
Query: python for backend
🔍 Searching: 'python for backend'
📊 Results:
================================================================================
1. [Doc 5] Similarity: 0.8923
████████████████████████████████████████████░░░░░░ 89.2%
"Django is a Python web framework for the backend"
2. [Doc 6] Similarity: 0.8756
███████████████████████████████████████████░░░░░░░ 87.6%
"FastAPI is a modern, fast framework for APIs in Python"
3. [Doc 0] Similarity: 0.8234
█████████████████████████████████████████░░░░░░░░░ 82.3%
"Python is a high-level programming language"
================================================================================
Query: containers
🔍 Searching: 'containers'
📊 Results:
================================================================================
1. [Doc 8] Similarity: 0.9012
█████████████████████████████████████████████░░░░░ 90.1%
"Docker is a platform for containerizing applications"
2. [Doc 9] Similarity: 0.8678
███████████████████████████████████████████░░░░░░░ 86.8%
"Kubernetes orchestrates containers in production"
3. [Doc 7] Similarity: 0.6823
██████████████████████████████████░░░░░░░░░░░░░░░░ 68.2%
"Node.js lets you run JavaScript on the server"
================================================================================
Query: exit
👋 See you later!
Second run (with cache):
python similarity_calculator.py
Output:
================================================================================
🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide
================================================================================
✅ Loaded 10 documents
✅ Embeddings loaded from cache
================================================================================
💬 Enter your query (or 'exit' to quit)
================================================================================
Query:
Note: This time it did NOT generate embeddings (it loaded from embeddings_cache.json). Saves time and API costs.
Step 5: Manual testing
Test queries:
Query: framework for interfaces
# Should return: React, Vue.js, (related)
Query: typed language
# Should return: TypeScript, (related)
Query: run on the server
# Should return: Node.js, FastAPI, Django
Query: orchestration
# Should return: Kubernetes, Docker
Query: modern web development
# Should return: React, Vue.js, JavaScript
Technical validations
Functionality checklist:
- The system loads documents correctly
- Generates embeddings (1536 dims each)
- Calculates cosine similarity correctly
- Returns the top-3 sorted by similarity
- Saves the cache to JSON
- Loads the cache on subsequent runs
- Handles errors (invalid API key, missing file)
- The interactive CLI works
- Scores are in the range [0.0, 1.0]
- Results are semantically correct
Optional improvements (Extra Credit)
Level 1: Additional features
# 1. Normalize embeddings
def normalize_embedding(self, embedding: List[float]) -> List[float]:
"""Normalize an embedding to magnitude 1.0"""
embedding_array = np.array(embedding)
return (embedding_array / np.linalg.norm(embedding_array)).tolist()
# 2. Show statistics
def print_stats(self):
"""Print corpus statistics"""
print(f"\n📈 Statistics:")
print(f" Documents: {len(self.documents)}")
print(f" Embeddings: {len(self.embeddings)}")
print(f" Dimensions: 1536")
print(f" Model: {EMBEDDING_MODEL}")
Level 2: Compare with BM25
# Add a simple keyword search
def keyword_search(self, query: str, top_k: int = 3):
"""Simplified BM25 search (keyword matching)"""
query_words = set(query.lower().split())
scores = []
for idx, doc in enumerate(self.documents):
doc_words = set(doc.lower().split())
matches = len(query_words.intersection(doc_words))
scores.append((idx, doc, matches))
scores.sort(key=lambda x: x[2], reverse=True)
return scores[:top_k]
# Compare both methods:
semantic_results = calculator.search(query, top_k=3)
keyword_results = calculator.keyword_search(query, top_k=3)
print("\n🔹 Semantic Search:")
calculator.print_results(semantic_results)
print("\n🔹 Keyword Search:")
# print keyword results...
Level 3: Hybrid search
def hybrid_search(self, query: str, alpha: float = 0.5, top_k: int = 3):
"""
Hybrid search: Semantic + Keyword
Args:
query: The user's query
alpha: Weight of keyword (1-alpha = weight of semantic)
top_k: Number of results
"""
# Semantic scores
query_emb = self.get_embedding(query)
semantic_scores = {}
for doc_id, doc_emb in self.embeddings.items():
sim = self.cosine_similarity(query_emb, doc_emb)
semantic_scores[doc_id] = (sim + 1) / 2 # Normalize to [0, 1]
# Keyword scores
query_words = set(query.lower().split())
keyword_scores = {}
for doc_id, doc in enumerate(self.documents):
doc_words = set(doc.lower().split())
matches = len(query_words.intersection(doc_words))
keyword_scores[doc_id] = matches
# Normalize keyword scores
max_keyword = max(keyword_scores.values()) if keyword_scores else 1
keyword_normalized = {
k: v / max_keyword for k, v in keyword_scores.items()
}
# Hybrid scores
hybrid_scores = []
for doc_id in range(len(self.documents)):
semantic_score = semantic_scores.get(doc_id, 0)
keyword_score = keyword_normalized.get(doc_id, 0)
hybrid_score = alpha * keyword_score + (1 - alpha) * semantic_score
hybrid_scores.append((doc_id, self.documents[doc_id], hybrid_score))
# Sort and return top-K
hybrid_scores.sort(key=lambda x: x[2], reverse=True)
return hybrid_scores[:top_k]
Troubleshooting
Problem 1: "OPENAI_API_KEY not found"
Cause: .env doesn't exist or doesn't have the key.
Solution:
# Create .env:
echo "OPENAI_API_KEY=sk-proj-your-key-here" > .env
# Verify that it's loading:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print(os.getenv('OPENAI_API_KEY'))"
Problem 2: "FileNotFoundError: documents.txt"
Cause: The file doesn't exist.
Solution:
# Create documents.txt with the content from Step 2
Problem 3: API rate limit error
Cause: Too many calls to the OpenAI API.
Solution:
# Add a delay between calls:
import time
for idx, doc in enumerate(self.documents):
embedding = self.get_embedding(doc)
self.embeddings[idx] = embedding
time.sleep(0.1) # 100ms delay
Problem 4: All similarity scores ~0.60-0.70
Cause: A very heterogeneous corpus or a very generic query.
Solution: Normal. Scores >0.80 indicate high similarity. Scores 0.60-0.70 are "somewhat related".
Success criteria
Functionality (70 points):
- (15 pts) The system loads documents
- (20 pts) Generates embeddings correctly
- (15 pts) Calculates cosine similarity
- (10 pts) Returns top-3 sorted
- (10 pts) Cache works
Code (20 points):
- (5 pts) Clean, organized code
- (5 pts) Consistent type hints
- (5 pts) Docstrings on functions
- (5 pts) Robust error handling
Usability (10 points):
- (5 pts) Intuitive CLI
- (5 pts) Readable, formatted output
Project summary
You have built:
- ✅ A working semantic search system
- ✅ Integration with the OpenAI API
- ✅ Cosine similarity calculation with numpy
- ✅ Embedding cache (optimization)
- ✅ An interactive CLI
Skills acquired:
- Consuming embedding APIs
- Vector manipulation with numpy
- Data persistence (JSON)
- Robust error handling
- Semantic search architecture
This project is the foundation for:
- Module 2: Deep technical architecture
- Module 4: Semantic search from scratch with numpy
- Module 6: Embeddings in production (batch, cache)
- Module 8: Complete integrator project
Additional resources
- OpenAI Embeddings API Docs - Official documentation
- NumPy Docs - NumPy for vector calculations
- Python dotenv - Environment variables
- JSON in Python - Serialization
- Cosine Similarity Explained - The math
Next steps
Module 2: How Do Embeddings Work?
You'll go deeper into:
- Transformer encoders (detailed architecture)
- Tokenization (BPE, WordPiece)
- Self-attention mechanisms
- Pooling strategies (real code)
- Advanced OpenAI API (batching, rate limiting)
This project lays the foundation. The following modules build on it.
Congratulations! 🎉
You've completed Module 1: What Are Embeddings?
What you accomplished:
- ✅ Understood what embeddings are and why they're critical
- ✅ Learned vector properties and cosine similarity
- ✅ Understood high-dimensional vector spaces
- ✅ Identified real use cases (RAG, search, recommendations)
- ✅ Compared embeddings vs keyword search
- ✅ Learned the high-level architecture of Transformers
- ✅ Built your first working system
You're ready for Module 2: Deep embedding architecture. 🚀
Module 1 completed - Embeddings Deep Dive Guide From fundamental concepts to practical implementation