Skip to main content
Image Similarity Search Engine

Project 1: Image Similarity Search Engine

Build a working image search engine that finds similar images using vector representations and cosine similarity. This project integrates everything you’ve learned about vectors, dot products, and similarity measures.
Estimated Time: 3-4 hours (take your time to understand each concept!)
Difficulty: Beginner-Intermediate
Concepts Used: Vectors, dot product, cosine similarity, normalization
Dataset: Product images (we’ll simulate these)
Don’t Rush! This project connects multiple concepts. If something doesn’t click, go back to the Vectors module. Understanding why this works is more important than just making it work.

🎯 The Big Picture: What Are We Really Doing?

Before diving into code, let’s understand the core insight that makes image search possible:
Image Search Pipeline

The Key Insight

Every image can be represented as a vector (list of numbers). Once images are vectors, finding “similar” images becomes finding “nearby” vectors. It is geometry, pure and simple. The same math that measures the angle between two arrows on a whiteboard can measure the visual similarity between two photographs. This is the central miracle of vectorization: it converts a fuzzy human judgment (“these photos look alike”) into a precise mathematical operation.

Why Does This Work?

Think about it: If two images are similar, they probably have:
  • Similar colors in similar places
  • Similar shapes and patterns
  • Similar brightness distributions
When we convert these properties to numbers, similar images produce similar numbers. Similar numbers = nearby vectors!

Project Overview

What You’ll Build

An image search engine that:
  1. Converts images to vector representations (the magic transformation)
  2. Computes similarity between images (using cosine similarity)
  3. Returns the top-K most similar images to a query
  4. Visualizes results (so you can verify it works!)

Why This Matters

Real-World Applications (This is exactly what these companies do): ML Concepts You’ll Master:
  • Feature extraction (images → vectors)
  • Similarity metrics (cosine similarity)
  • Nearest neighbor search
  • High-dimensional vector spaces
  • Vectorized operations (matrix multiplication for speed!)

Part 1: Understanding the Problem (Deeply)

How Do We Represent Images as Vectors?

This is the crucial question. Let’s explore multiple methods and understand the tradeoffs.
Understanding Vector Similarity
Method 1: Raw Pixels (Simple but effective for similar images)
Why normalize? So all images are on the same scale (0-1 instead of 0-255). This prevents one bright image from dominating comparisons. Without normalization, a photo taken in sunlight and the same photo taken in shade would appear very different numerically even though they show the same scene. Normalization removes the “brightness bias” and lets the comparison focus on the actual pattern of light and dark.
Numerical Stability Note: Always add a small epsilon (like 1e-8) when dividing to avoid division-by-zero errors on completely black images. The + 1e-8 you will see in production code is not paranoia — it is a standard defensive practice.

Method 2: Color Histogram (Better for Different Viewpoints)

The pixel method has a problem: if you rotate or shift an image slightly, the vector changes dramatically! Histograms solve this. The Idea: Instead of “where are the pixels?”, ask “what colors are present?”
Histogram vs Pixels: When to Use Which?For learning, we’ll use both! In production, companies use CNNs (neural networks) to extract “deep features.”
Why histograms work for rotated images: If you rotate a red car, it’s still red! The histogram captures “what colors exist” not “where colors are.”

Part 2: Computing Similarity

This is where the linear algebra magic happens. We’ve turned images into vectors. Now we need to measure “how similar” two vectors are.

The Core Insight: Similarity = Angle Between Vectors

Imagine two arrows (vectors) starting from the same point:
  • If they point in the same direction → very similar
  • If they point in different directions → not similar
  • If they’re perpendicular (90°) → completely unrelated
Cosine similarity measures this by computing the cosine of the angle between vectors.

Cosine Similarity: The Math

cosine similarity=cos(θ)=ABA×B\text{cosine similarity} = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{||\mathbf{A}|| \times ||\mathbf{B}||} Where:
  • AB\mathbf{A} \cdot \mathbf{B} is the dot product (sum of element-wise products)
  • A||\mathbf{A}|| is the magnitude (length) of vector A
  • The result is between -1 and 1

Implementation with Deep Understanding

Why Cosine Beats Euclidean for ImagesTwo images of the same sunset—one taken with a bright setting, one dark—have the same pattern of colors but different absolute values.
  • Cosine similarity: “Same pattern!” (1.0)
  • Euclidean distance: “Very different!” (large distance)
Cosine captures what matters for visual similarity.

Euclidean Distance: The Alternative

For completeness, here’s Euclidean distance. It measures “how far apart” vectors are in space:

Which to Use?

  • Cosine: When magnitude doesn’t matter (text, images with different brightness)
  • Euclidean: When absolute values matter (coordinates, measurements)
For images, cosine similarity is usually better!

Part 3: Building the Search Engine

Step 1: Create Image Database

Step 2: Use the Search Engine

Output:

Part 4: Visualizing Results


Part 5: Performance Analysis

Timing Comparison

Output:
Why so much faster? Vectorized operations use optimized C/Fortran code and can leverage CPU SIMD instructions!

Memory Usage


Part 6: Improvements & Extensions

1. Better Feature Extraction

2. Faster Search with Approximate Nearest Neighbors


Challenges & Exercises

Modify the search engine to handle multiple queries at once:

Challenge 2: Evaluation Metrics

Implement precision@K and recall@K:

Challenge 3: Diversity

Modify search to return diverse results (not all similar to each other):

Complete Code

Here’s the full working implementation:

🚨 Real-World Challenge: Handling Messy Images

Production Reality: Real image databases have corrupted files, varying formats, extreme sizes, and other issues. Here’s how to handle them:
Production Checklist:
  • Handle missing files gracefully
  • Skip corrupted/truncated images
  • Normalize different color modes (RGB, RGBA, palette, grayscale)
  • Handle extreme aspect ratios
  • Log failures for debugging
  • Set memory limits for very large images
  • Add timeout for slow processing

Feature Extraction Methods: From Simple to Production

Edge Case — The Semantic Gap: Raw pixel similarity fails in cases that seem obvious to humans. A photo of a black cat and a photo of a white cat on a black background can have similar pixel histograms (both are mostly black), but semantically they are very different. Conversely, two photos of the same cat in different lighting have very different pixel values but are semantically identical. This gap between pixel-level similarity and semantic similarity is why production systems use deep learning features. The linear algebra is the same (cosine similarity on vectors); the quality of the vectors is what changes everything.

Key Takeaways

What You Built and Learned:
  • Images as Vectors - Pixels, histograms, and deep features all work
  • Cosine Similarity - Measure angular distance between high-dimensional vectors
  • Vectorized Operations - Replace loops with matrix operations for 100x speedup
  • Batch Processing - Matrix multiplication enables searching thousands of images instantly
  • Real ML Pipeline - From raw data to vectors to similarity to results
  • Production Robustness - Handle corrupted, malformed, and edge-case images
Industry Insight: Companies like Pinterest, Airbnb, and Spotify use exactly these techniques—just with more sophisticated embeddings (from neural networks) and approximate nearest neighbor search for speed at scale.

What’s Next?

You’ve built a working image search engine using linear algebra! Next, we’ll explore eigenvalues and eigenvectors to build even more powerful applications like face recognition and dimensionality reduction.

Next: Eigenvalues & Eigenvectors

Learn the math behind PCA and face recognition

Interview Deep-Dive

Strong Answer:
  • Raw pixel vectors are not invariant to geometric transformations. Rotating an image by even 5 degrees shifts every pixel to a different location in the flattened vector, creating a large cosine distance between what are visually identical images. Similarly, changes in lighting alter every pixel value, and cropping changes the vector length entirely (or which pixels are included after resizing).
  • The diagnosis is straightforward: compute the cosine similarity between the same product photographed from two angles. If similarity is below 0.5 for clearly-identical products, the representation is the problem, not the similarity metric. You can also visualize the vectors using t-SNE or UMAP — if images of the same product scatter randomly rather than clustering, the features are not capturing semantic similarity.
  • The fix is to move from handcrafted features (raw pixels, histograms) to learned embeddings. A CNN trained for image classification (ResNet, EfficientNet) or specifically for metric learning (using triplet loss or contrastive loss) produces feature vectors that are invariant to rotation, scale, lighting, and background changes. You extract features from the penultimate layer (before the classification head) — typically a 2048-dim vector for ResNet-50 — and use those as your search vectors.
  • In production at Pinterest, their visual search system uses embeddings from a model trained with multi-task learning: classification (is this a shoe?) plus metric learning (are these the same shoe?). The embeddings are 256-dimensional, normalized to unit length, and indexed with HNSW. This handles angle, lighting, and even partial occlusion robustly.
  • The trade-off: learned embeddings require a trained model (GPU inference per image at indexing time), while pixel features are free to compute. For a prototype with controlled imagery (like product catalog shots on white backgrounds), pixel histograms can work surprisingly well. For user-uploaded photos with arbitrary conditions, learned embeddings are mandatory.
Follow-up: How would you evaluate whether your new embedding-based search is actually better than the pixel-based approach? What metrics would you use?Use a held-out test set of known-similar image pairs (same product, different angles) and known-dissimilar pairs. Compute precision@k (what fraction of the top-k results are actually relevant) and recall@k (what fraction of all relevant images appear in the top-k). Also measure Mean Reciprocal Rank (MRR) — the average of 1/rank1/\text{rank} of the first relevant result. Plot precision-recall curves for both methods at various similarity thresholds. In practice, also measure “semantic precision” via human evaluation: show annotators the top-5 results for 100 random queries and have them label relevance. The pixel-based approach might achieve 30% precision@5 while the embedding approach achieves 85%. Additionally, measure latency: if the embedding model takes 50ms per query for inference, that must be factored into the end-to-end search time.
Strong Answer:
  • Cosine similarity measures the angle between vectors, ignoring their magnitudes. For image features, this is desirable because two images of the same scene at different exposures (bright vs. dark) will have pixel vectors pointing in the same direction but with different magnitudes. Cosine similarity treats them as identical; Euclidean distance penalizes the brightness difference.
  • More fundamentally, in high-dimensional spaces (4096+ dimensions for image features), Euclidean distance suffers from the concentration of measure phenomenon — all pairwise distances converge toward the same value, making discrimination difficult. Cosine similarity, by normalizing vectors to the unit sphere, focuses on the angular structure which tends to be more discriminative.
  • Euclidean distance is preferred when absolute feature values carry information. In image segmentation, if you are comparing pixel regions where intensity level matters (differentiating a dark object from a bright one in the same scene), Euclidean distance on raw pixel patches is appropriate. In medical imaging, absolute Hounsfield unit values in CT scans encode tissue type — cosine similarity would incorrectly treat bone and soft tissue as similar if their spatial patterns happen to align.
  • There is a mathematical equivalence worth knowing: for L2-normalized vectors (unit length), Euclidean distance and cosine similarity are monotonically related: ab2=2(1cosθ)\|a - b\|^2 = 2(1 - \cos\theta). So if you pre-normalize all vectors (which most production systems do), the two metrics produce identical rankings. The choice then becomes a matter of API conventions and numerical convenience rather than a true algorithmic difference.
Follow-up: You notice that your image search returns very different results depending on whether you normalize vectors before computing Euclidean distance. Why, and which approach is correct?Without normalization, Euclidean distance conflates two signals: directional similarity (the pattern of features) and magnitude similarity (the overall intensity/activation level). A very “bright” image with features [100, 200, 300] and a dim image with features [1, 2, 3] have high cosine similarity (identical direction) but enormous Euclidean distance (299 units apart). Normalizing first removes the magnitude signal, making Euclidean distance equivalent to angular distance. Whether you should normalize depends on whether magnitude is signal or noise. For CNN embeddings, magnitude typically correlates with image “confidence” or “prototypicality” and is usually not useful for search — normalize. For raw pixel features in medical imaging, magnitude encodes diagnostically relevant intensity — do not normalize.
Strong Answer:
  • The brute-force approach computes similarity between the query and all NN database vectors: NN dot products, each of dimension dd. For N=10MN = 10M and d=2048d = 2048, that is 20 billion multiply-adds per query. At best, a single CPU core does this in about 10 seconds. Unacceptable for interactive search.
  • Optimization 1 (most impactful): Approximate Nearest Neighbors. Use FAISS with IVF+PQ indexing. Build a quantized index offline in about 30 minutes for 10M vectors. Query time drops to 1-5ms at 95%+ recall. This is 2000x faster and is what every production system uses. The linear algebra trick: Product Quantization replaces each full dot product with a table lookup, and IVF limits the search to a small subset of vectors.
  • Optimization 2: Dimensionality reduction. Apply PCA from 2048 to 256 dimensions before indexing. Each dot product is 8x cheaper, memory usage drops 8x, and (as discussed) recall often improves because you cut noise dimensions. This compounds with ANN: the PQ codebooks are cheaper to compute in lower dimensions.
  • Optimization 3: GPU-accelerated brute force. FAISS supports GPU search — an A100 can brute-force 10M vectors of dimension 256 in about 10ms. For moderate-scale systems (under 50M vectors), GPU brute force with PCA-reduced vectors can be simpler than maintaining an ANN index, with perfect recall.
  • Optimization 4: Pre-filtering. If your UI allows category filters (e.g., “shoes only”), partition the index by category so you only search the relevant subset. This reduces NN by 10-100x with zero approximation cost.
  • Optimization 5: Caching. If certain queries are repeated (e.g., “similar to this bestseller”), cache the results. Simple but often overlooked.
Follow-up: How would you handle the case where new images are added to the database continuously (say, 100K new images per day)? Rebuilding the entire ANN index is expensive.Most production ANN indexes support incremental insertion. FAISS IVF allows adding vectors to existing Voronoi cells without retraining the cell centroids. The trade-off: over time, the cell boundaries become suboptimal as the data distribution shifts, degrading recall. The standard practice is to add incrementally during the day and rebuild the full index nightly during a maintenance window. HNSW (used by Pinecone, Weaviate, Qdrant) is inherently incremental — new vectors are inserted into the graph on-the-fly with no rebuild needed, though query performance degrades slightly as the graph becomes less optimal. Monitor recall@10 weekly against a ground-truth set; when it drops below your threshold, trigger a full rebuild.