All posts

Embeddings: The Silent Scalability Trap

Introduction & hook

Imagine a major e-commerce platform. It’s Black Friday. Millions of users are hitting “search” and “recommended for you” buttons simultaneously. Suddenly, the personalized recommendations start showing completely irrelevant items. Searches for “4K TV” return results for “dog food.” The system isn’t down; all health checks are green. But customers are furious, and sales are tanking. What happened?

The database is fine. The API gateways are scaling perfectly. The LLM response times are within bounds. The silent killer here is often the embeddings. These numerical representations of everything from user profiles to product descriptions, long-form documents, and even images are the invisible glue of modern AI systems. They capture the “meaning” of data, transforming complex information into vectors that machines can understand and compare. Embeddings solve a fundamental problem: how do you get a computer to understand that “smartphone” and “mobile phone” are conceptually very similar, while “rock” and “paper” are distinct, without explicitly programming every relationship? You embed them.

But here’s the kicker: the very power that makes embeddings indispensable — their ability to encapsulate rich semantic information — also makes them a hidden bottleneck. As your data grows, as your user base expands, and as your need for ever-more-accurate similarity search intensifies, the cost and complexity of generating, storing, retrieving, and comparing these high-dimensional vectors can cripple an otherwise robust system. This isn’t just a hypothetical problem; it’s a real, insidious scalability trap.

How it works (the visual example)

Think of embeddings as a universal translator that converts any piece of data – a text description, an image, a user’s browsing history – into a unique point in a vast, invisible, multi-dimensional space. The core idea is simple: if two pieces of data are conceptually similar, their respective points in this space will be close to each other. If they’re different, they’ll be far apart.

Let’s say you’re building a content recommendation system. Each article you publish, every video, every podcast episode, gets transformed into one of these numerical vectors. When a user consumes content, their preferences also get translated into a vector. To recommend new content, you simply find the content vectors that are “closest” to the user’s preference vector.

The “coordinates” of these points are the numbers in the embedding vector. For example, a simple embedding might look like [0.1, -0.5, 0.9]. This is a 3-dimensional embedding, meaning it lives in a space with three axes. Modern, powerful embeddings often have hundreds or even thousands of dimensions. A common OpenAI embedding, text-embedding-ada-002, produces vectors with 1536 dimensions.

The more dimensions an embedding has, the more nuanced and precise its representation of meaning can be. Imagine trying to describe every facet of a complex concept like “love” with just three numbers versus 1536 numbers. More dimensions allow for finer distinctions. But this power comes at a cost: finding the “closest” points in a 1536-dimensional space is exponentially harder than in a 3-dimensional space. This challenge, known as the curse of dimensionality, is where the bottleneck begins.

Real-world use cases

Embeddings are the backbone of most sophisticated AI applications you interact with daily. They’re a lifesaver in scenarios where understanding semantic relationships is key:

  • Semantic Search: Instead of just matching keywords, embeddings let you search for meaning. Searching “how to fix a leaky faucet” can return results about “plumbing repair” or “water pipe maintenance,” even if those exact words aren’t present.
  • Recommendation Systems: Suggesting products, movies, or news articles based on user behavior and content similarity.
  • Anomaly Detection: Flagging unusual server logs, network traffic, or financial transactions by identifying data points that are “far” from the typical cluster.
  • Deduplication & Clustering: Grouping similar documents or images, even if they have minor variations.
  • Retrieval-Augmented Generation (RAG): Powering LLMs with relevant context by embedding your knowledge base and retrieving semantically similar chunks.

However, embeddings can quickly become an anti-pattern or a source of major headaches in these situations:

  • Small, Static Datasets: If your dataset is tiny and rarely changes, the overhead of an embedding pipeline might outweigh the benefits. Simple keyword matching or rule-based systems could be more efficient.
  • Extremely Niche Domains with Limited Data: If you’re working in a highly specialized field (e.g., specific scientific jargon) where no good pre-trained models exist, and you lack sufficient data to fine-tune your own, off-the-shelf embeddings might perform poorly, leading to misleading similarity.
  • Strict Latency Requirements with Large Indices: If you need millisecond-level similarity search across billions of high-dimensional vectors, and you haven’t optimized your indexing and retrieval strategy, the computational cost can be prohibitive.
  • Budget Constraints Without Optimization: Generating and storing high-dimensional embeddings, especially with external APIs, can be surprisingly expensive at scale. Without careful management, this cost spirals quickly.

Implementation & code

Let’s say you’re building a service that needs to find similar items in a catalog. A naive approach might look like this:

import openai
import numpy as np

# Naive approach: Generate a single, large embedding for everything
# Then store it and load it fully for similarity comparison.
# This assumes you always need the full 1536 dimensions.

class NaiveEmbeddingService:
    def __init__(self, api_key: str):
        openai.api_key = api_key
        self.client = openai.OpenAI()

    def get_embedding(self, text: str) -> list[float]:
        # This is expensive and returns 1536 dimensions
        response = self.client.embeddings.create(
            input=text,
            model="text-embedding-ada-002"
        )
        return response.data[0].embedding

    def calculate_similarity(self, embed1: list[float], embed2: list[float]) -> float:
        # Cosine similarity for comparing vectors
        vec1 = np.array(embed1)
        vec2 = np.array(embed2)
        return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

# --- How it breaks ---
# 1. Cost: Every call to get_embedding hits an external API, costing money.
# 2. Latency: Network roundtrips add significant delay.
# 3. Storage: 1536 floats per item adds up quickly in databases/vector stores.
# 4. Retrieval & Compute: Loading and comparing 1536-dim vectors is slow for large datasets.

This works for small data. But when your catalog hits millions of items, and you need to compare against thousands, that 1536-dimension vector becomes a massive drag on storage, network transfer, and CPU cycles for similarity calculations.

A more robust, production-ready approach anticipates this bottleneck. We’ll introduce the concept of Matryoshka representations here. A Matryoshka doll contains smaller dolls within it. Similarly, a Matryoshka embedding is designed so that its initial components (e.g., the first 256 dimensions) are also valid, albeit coarser, representations of the data. This means you can truncate an embedding to reduce its size and compute requirements without having to re-embed the original data.

import openai
import numpy as np
from typing import List, Optional

# Robust approach: Introduce Matryoshka representations and caching
# This allows for dimension reduction at retrieval time, saving compute and storage.

class ProductionEmbeddingService:
    def __init__(self, api_key: str):
        openai.api_key = api_key
        self.client = openai.OpenAI()
        self.embedding_cache = {} # In a real system, this would be Redis or similar

    def _call_api_for_embedding(self, text: str) -> List[float]:
        # Centralized API call logic for metrics, rate limiting, retries
        response = self.client.embeddings.create(
            input=text,
            model="text-embedding-ada-002" # Assumed to be Matryoshka-compatible
        )
        return response.data[0].embedding

    def get_embedding(self, text: str, max_dimensions: Optional[int] = None) -> List[float]:
        # Check cache first to avoid redundant API calls and costs
        if text in self.embedding_cache:
            full_embedding = self.embedding_cache[text]
        else:
            full_embedding = self._call_api_for_embedding(text)
            self.embedding_cache[text] = full_embedding # Store full embedding

        # Apply Matryoshka truncation if requested
        if max_dimensions and max_dimensions < len(full_embedding):
            # Key insight: Matryoshka embeddings are designed so prefixes are meaningful.
            # We normalize *after* truncating to preserve properties.
            truncated_embedding = full_embedding[:max_dimensions]
            norm = np.linalg.norm(truncated_embedding)
            if norm > 0:
                return (np.array(truncated_embedding) / norm).tolist()
            return [0.0] * max_dimensions # Handle zero vector case

        return full_embedding

    def calculate_similarity(self, embed1: List[float], embed2: List[float]) -> float:
        vec1 = np.array(embed1)
        vec2 = np.array(embed2)
        # Ensure vectors are already normalized from get_embedding or normalize here
        return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

# --- Why this is better ---
# 1. Cost & Latency: Caching prevents repeat API calls.
# 2. Flexibility: `max_dimensions` allows choosing the right tradeoff for different use cases.
#    - For high-precision, slower tasks, use full dimensions.
#    - For fast, approximate filtering, use fewer dimensions.
# 3. Storage & Compute: Storing and comparing smaller vectors is dramatically cheaper.
#    - A 256-dim vector is ~1/6th the size of 1536-dim.
# 4. Future-proofing: If you later decide to fine-tune your own embedder, you can swap out `_call_api_for_embedding`.

This example shows how to fetch embeddings and crucially, how to use Matryoshka representations. The OpenAI text-embedding-ada-002 model, and many others, are often Matryoshka-compatible, meaning their prefixes maintain semantic meaning. Always verify this for your chosen model. This isn’t just a trick; it’s a fundamental architectural decision that drastically impacts scalability.

Senior-level insights & gotchas

You’ve built your embedding service, and things seem fine. Then you hit millions of items. Here’s where the true bottlenecks reveal themselves, and where a principal engineer earns their stripes.

The Dark Side of High Dimensionality: More dimensions mean richer representations, but they also mean the curse of dimensionality hits hard. In high dimensions, all points tend to be “far” from each other, making traditional exact nearest neighbor search infeasible. The distance between the closest and furthest points becomes less meaningful, and finding truly similar items becomes a needle-in-a-haystack problem. This is why Approximate Nearest Neighbor (ANN) algorithms (like HNSW, FAISS, ScaNN) are critical. You sacrifice a tiny bit of accuracy for massive speedups. Never roll your own ANN. Use battle-tested libraries and vector databases.

Matryoshka Beyond Truncation: The beauty of Matryoshka representations extends beyond simply truncating dimensions for storage. You can have multiple vector indices, each tuned for a different dimension.

  • A small, fast index with, say, 128 dimensions for initial coarse filtering or real-time recommendations.
  • A larger, slower index with 512 dimensions for more accurate second-stage retrieval.
  • The full 1536-dimensional vectors only for the final, most precise comparisons, or perhaps for re-ranking a smaller candidate set. This multi-stage retrieval strategy drastically reduces computational load where it matters most. However, know that not all models are Matryoshka-compatible out of the box. Always test if a prefix of an embedding still yields acceptable semantic similarity for your specific task.

Fine-tuning Your Own Embedder: Relying solely on general-purpose embedding models, no matter how good, can only get you so far. For highly specialized domains (e.g., legal documents, medical research, your company’s internal jargon), a public model might miss crucial nuances. This is where fine-tuning your own embedder becomes a game-changer.

  • The Problem: General models are trained on vast, diverse datasets. They’re good at general tasks. But they don’t know your specific definitions of “similar” or “dissimilar” for your data.
  • The Solution: Contrastive Learning: You don’t label every possible similarity. Instead, you create pairs or triplets of data. For example, a triplet loss setup might involve an “anchor” item, a “positive” item (semantically similar to the anchor), and a “negative” item (semantically dissimilar). The model is then trained to pull the anchor and positive embeddings closer together in the vector space, while pushing the anchor and negative embeddings further apart. This explicitly teaches the model to understand the semantic relationships specific to your problem.
  • Benefits: Dramatically improved relevance for your domain, often with smaller embedding sizes, leading to better performance and lower costs than a large general model.
  • Gotcha: Fine-tuning requires a significant amount of labeled data (positive/negative pairs or triplets) and substantial computational resources. It’s a project, not a weekend hack.

The Embedding Refresh Problem: Embeddings are not static. Your data evolves. New products are added, user preferences shift, new articles are published. Stale embeddings lead to irrelevant results. You need a strategy for refreshing:

  • Batch Re-embedding: Periodically re-embed your entire corpus, or significant portions, typically overnight or during low-traffic periods.
  • Incremental Updates: For frequently changing items, update their embeddings and re-index them immediately. This requires a robust pipeline and efficient indexing that supports partial updates.
  • Monitoring: Track embedding drift using techniques like PCA or UMAP to visualize high-dimensional changes over time. Monitor search relevance metrics. If relevance drops without apparent data changes, embedding staleness could be the culprit.

Quantization: Once you have your embeddings, storing them as full 32-bit (or even 64-bit) floats is wasteful. Techniques like quantization (e.g., Product Quantization, Binary Quantization) can reduce the storage footprint by factors of 4x to 8x or more, making your vector database much smaller and faster to query. You trade off a tiny bit of precision for huge gains in efficiency.

Summary & production checklist

Embeddings are powerful, but they are not free. Treat them as a critical component of your architecture, subject to the same rigorous design and optimization as your databases or microservices.

  • Understand your dimensionality needs: Don’t default to the largest embedding. Start smaller and scale up only if relevance demands it.
  • Embrace Matryoshka representations: If your chosen model supports it, use variable dimensions for different stages of retrieval (coarse vs. fine-grained search) to save compute and storage.
  • Implement caching aggressively: Cache embedding generation requests to reduce API costs and latency.
  • Choose the right vector store: Use production-grade Approximate Nearest Neighbor (ANN) libraries or dedicated vector databases (e.g., Pinecone, Weaviate, Milvus, Qdrant) for scalable similarity search. Never build this from scratch.
  • Consider fine-tuning for domain specificity: If generic models fall short, explore fine-tuning with contrastive or triplet loss on your proprietary data. Be prepared for the data labeling and compute investment.
  • Plan your embedding refresh strategy: Implement batch updates, incremental updates, or a hybrid approach to keep embeddings fresh and relevant.
  • Optimize storage with quantization: Reduce the storage and transfer size of your embedding vectors using quantization techniques.
  • Monitor embedding quality: Track metrics like search relevance, recall, and embedding drift to catch issues before they impact users.