<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sam-herman.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sam-herman.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-09-08T02:09:24+00:00</updated><id>https://sam-herman.github.io/feed.xml</id><title type="html">Samuel Herman</title><subtitle>Research and engineering leadership in retrieval science, language models, and visual intelligence, and the end-to-end capabilities that make them work at scale and in the physical world.</subtitle><author><name>Samuel Herman</name></author><entry><title type="html">When Geometry Is Everything: Multi-Vector Retrieval Beyond the MaxSim Monoculture</title><link href="https://sam-herman.github.io/blogs/multivector-and-dataset-geometry.html" rel="alternate" type="text/html" title="When Geometry Is Everything: Multi-Vector Retrieval Beyond the MaxSim Monoculture" /><published>2026-02-22T00:00:00+00:00</published><updated>2026-02-22T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/multivector-and-dataset-geometry</id><content type="html" xml:base="https://sam-herman.github.io/blogs/multivector-and-dataset-geometry.html"><![CDATA[<h2 id="setting-the-stage">Setting the Stage</h2>

<p>In my <a href="https://sam-herman.github.io/blogs/database-as-innovator">previous post on the economics of late interaction retrieval</a>, I made the case that PLAID and MUVERA had fundamentally changed the cost calculus for multi-vector search, taking late interaction from a “hard no” to a “viable tradeoff” at production scale. I stand by that analysis—for the specific problem they were designed to solve.</p>

<p>But there’s a subtlety worth unpacking. When we talk about “multi-vector retrieval,” we’re actually talking about a much larger family of problems than the ColBERT-MaxSim scenario that dominates the conversation. And when you zoom out from that specific case, some uncomfortable truths emerge:</p>

<ol>
  <li><strong>The most hyped algorithms are geometry-dependent</strong>—they assume a very specific structure in how embeddings are distributed, and that structure is not universal</li>
  <li><strong>The dominant similarity function (MaxSim) is one of many</strong>—and the others don’t always cooperate with PLAID or MUVERA’s machinery</li>
  <li><strong>Some “hard” multi-vector problems have embarrassingly simple solutions</strong> that don’t require any specialized infrastructure at all</li>
</ol>

<p>The argument runs in three acts. First, we’ll establish the two axes that determine which infrastructure choice is right for any given problem: the similarity function and the dataset geometry. Then we’ll apply both axes to stress-test PLAID and MUVERA—not to dismiss them, but to be precise about where their assumptions hold and where they don’t. Finally, we’ll show that two “hard” multi-vector problems have simple closed-form solutions hiding inside those same assumptions, with immediate practical implications for how you architect retrieval systems today. If you want to skip ahead, the decision framework table at the end of the post is the practical output.</p>

<p>Let’s dig in.</p>

<hr />

<h2 id="the-general-case-of-set-to-set-similarity">The General Case of Set-to-Set Similarity</h2>

<p>In the standard neural retrieval setting, you’re doing <strong>point-to-point</strong> Maximum Inner Product Search (MIPS): a single query vector against a collection of single document vectors. The similarity is simply:</p>

\[\text{sim}(q, d) = \langle q, d \rangle = \sum_{i=1}^D q_i d_i\]

<p>This maps directly to an ANN index problem, and the entire ecosystem—HNSW, IVF, DiskANN, Product Quantization—is built to solve it efficiently.</p>

<p>Multi-vector retrieval generalizes this to <strong>set-to-set</strong> similarity: each entity is a <em>set</em> of embedding vectors $Q = {q_1, \ldots, q_m}$ and $P = {p_1, \ldots, p_n}$, and no single dot product tells you how similar they are. What you need is an <em>aggregation function</em> over all pairwise interactions.</p>

<p>Here’s where things get interesting—because there are many such aggregation functions, and they’re used in real applications:</p>

<p><strong>MaxSim (Chamfer Similarity):</strong> The ColBERT standard. For each query token, find the best-matching document token; sum these maximal matches.</p>

\[\text{MaxSim}(Q, P) = \sum_{i=1}^{|Q|} \max_{j=1}^{|P|} \langle q_i, p_j \rangle\]

<p>This is the workhorse of late interaction retrieval. It allows fine-grained token alignment—each query token finds its best match independently—and it’s asymmetric (the sum is over query tokens, not document tokens).</p>

<p><strong>SumSim (All-Pairs):</strong> Sum all pairwise similarities.</p>

\[\text{SumSim}(Q, P) = \sum_{i=1}^{|Q|} \sum_{j=1}^{|P|} \langle q_i, p_j \rangle\]

<p>This treats every query-document token pair as equally relevant. It appears in cross-modal matching and group recommendation settings where you want aggregate affinity rather than fine-grained alignment.</p>

<p><strong>Top-K Sum:</strong> For each query token, sum the top-$k$ (rather than just the maximum) document similarities.</p>

\[\text{TopK}(Q, P) = \sum_{i=1}^{|Q|} \sum_{j \in \text{top-}k_P(q_i)} \langle q_i, p_j \rangle\]

<p>This is a softer variant of MaxSim that rewards documents where multiple tokens strongly match each query token, not just one. Useful when entity sets have structured redundancy.</p>

<p><strong>Symmetric Chamfer:</strong> Average of MaxSim in both directions—from query to document and from document to query.</p>

\[\text{SymChamfer}(Q, P) = \frac{1}{2}\text{MaxSim}(Q, P) + \frac{1}{2}\text{MaxSim}(P, Q)\]

<p>Used in some image-text matching settings where neither modality “anchors” the comparison.</p>

<p>Just this brief survey should give you pause. The literature discussing PLAID and MUVERA as solutions to “multi-vector retrieval” is almost entirely talking about <strong>MaxSim</strong>, and treating MaxSim as if it were synonymous with the problem itself. That’s a significant conflation—and as we’ll see, the algorithms break down in revealing ways when you change the scoring function or the data distribution.</p>

<hr />

<h2 id="dataset-geometry-what-are-we-actually-indexing">Dataset Geometry: What Are We Actually Indexing?</h2>

<p>The second axis of analysis—equally important and equally underappreciated—is <strong>dataset geometry</strong>: the statistical structure of how document token embeddings are distributed in embedding space.</p>

<h3 id="three-geometric-regimes">Three Geometric Regimes</h3>

<p>There are three primary distributional patterns to understand:</p>

<p><strong>Gaussian Isotropic:</strong> Embeddings are drawn from a single distribution with uniform variance in all directions. This is the world that most ANN theory assumes—a “well-behaved” embedding space where random partitioning methods (like SimHash) work with provable guarantees.</p>

\[\mathbf{v} \sim \mathcal{N}(\boldsymbol{\mu}, \sigma^2 \mathbf{I})\]

<p>Here $\mathbf{v} \in \mathbb{R}^d$ is a single token embedding vector, $\boldsymbol{\mu} \in \mathbb{R}^d$ is the distribution mean (the centroid of the cloud), $\sigma^2$ is a single scalar variance applying equally in every direction, and $\mathbf{I}$ is the $d \times d$ identity matrix—meaning no direction in embedding space is preferred over any other.</p>

<p><strong>Gaussian Anisotropic:</strong> Embeddings are still unimodal but have unequal variance across dimensions—stretched or compressed in specific directions. BERT’s raw sentence embeddings, for instance, are famously anisotropic, with a few dominant principal components capturing most of the variance. This breaks the isotropy assumptions of many hashing-based methods.</p>

\[\mathbf{v} \sim \mathcal{N}(\boldsymbol{\mu}, \boldsymbol{\Sigma}) \quad \text{where } \boldsymbol{\Sigma} \neq \sigma^2 \mathbf{I}\]

<p>The key change is that the scalar $\sigma^2$ is replaced by a full covariance matrix $\boldsymbol{\Sigma} \in \mathbb{R}^{d \times d}$. When $\boldsymbol{\Sigma}$ has unequal eigenvalues, the distribution is stretched along some dimensions and compressed along others—producing an ellipsoidal cloud rather than a spherical one. The constraint $\boldsymbol{\Sigma} \neq \sigma^2 \mathbf{I}$ simply means the variances are not identical in all directions.</p>

<p><strong>Multi-Kernel Anisotropic:</strong> Embeddings are drawn from a <em>mixture</em> of anisotropic Gaussians, where each mixture component (“kernel”) corresponds to a semantic cluster, and each kernel can have its own covariance structure.</p>

\[\mathbf{v} \sim \sum_{k=1}^K \pi_k \cdot \mathcal{N}(\boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) \quad \text{where } \sum_k \pi_k = 1\]

<p>This extends the anisotropic case to $K$ distinct clusters. Each cluster $k$ has its own centroid $\boldsymbol{\mu}_k$ and its own covariance matrix $\boldsymbol{\Sigma}_k$ (so different clusters can have different shapes and orientations). The mixing weights $\pi_k \geq 0$ give the probability that a randomly drawn token embedding comes from cluster $k$; the constraint $\sum_k \pi_k = 1$ ensures these probabilities are well-formed. In practice, $\pi_k$ reflects how often a semantic category (financial terms, function words, visual elements) appears in the corpus.</p>

<p>This third regime is the realistic one for trained multi-vector embeddings like ColBERTv2. The token embeddings from a trained language model don’t form a single Gaussian blob—they cluster by semantic meaning. All token embeddings for terms related to “financial instruments” cluster in one region of space; “biological processes” in another; function words (“the”, “and”, “is”) in yet another. Each cluster has its own orientation (some are compact and round, others are elongated along specific axes corresponding to the semantic variation within that topic).</p>

<p><img src="./images/geometry-comparison.png" alt="Dataset Geometry" />
<em>Three distributional regimes: (left) Gaussian Isotropic—uniform variance in all directions; (center) Gaussian Anisotropic—stretched in specific directions; (right) Multi-Kernel Anisotropic—the realistic regime for trained ColBERT embeddings, with distinct semantic clusters each having their own shape and orientation. PLAID exploits this structure via its centroid codebook; MUVERA’s SimHash assumes the first regime and struggles with the third.</em></p>

<h3 id="why-multi-kernel-anisotropic-the-contrastive-training-connection">Why Multi-Kernel Anisotropic? The Contrastive Training Connection</h3>

<p>This isn’t coincidence. The multi-kernel anisotropic structure is a <em>direct consequence</em> of how ColBERT and its family are trained using contrastive objectives.</p>

<p>In contrastive training, the model learns to pull positive query-document pairs together in embedding space while pushing negatives apart. The loss function that formalizes this is called <strong>InfoNCE</strong>—a name that carries meaning worth unpacking.</p>

<p>“NCE” stands for <strong>Noise Contrastive Estimation</strong>, a technique originally designed to train language models without computing an expensive softmax over a vocabulary of millions of words. The insight is to reframe the problem as binary classification: teach the model to distinguish <em>real</em> (signal) data from <em>noise</em> (randomly sampled negative) data, rather than predicting the exact right answer from all possible answers at once. This sidesteps the normalization constant that makes full softmax expensive. “Info” indicates that this particular loss is motivated as a lower bound on <strong>mutual information</strong> between representations—maximizing InfoNCE is equivalent to maximizing a variational lower bound on $I(\text{query}; \text{positive document})$. This information-theoretic grounding, introduced by van den Oord et al. (2018) in the context of sequential prediction, is what distinguishes InfoNCE from a plain cross-entropy or triplet loss.</p>

<p>The practical upshot: unlike standard cross-entropy (which needs a fixed, known label space) or MSE (which needs continuous target values), InfoNCE works wherever you can define positive/negative <em>pairs</em>—which is exactly the supervised retrieval setting. The loss takes the form:</p>

\[\mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(q, p^+) / \tau)}{\sum_{p^- \in \mathcal{N}} \exp(\text{sim}(q, p^-) / \tau)}\]

<p>where $q$ is a query embedding, $p^+$ is its positive (relevant) document embedding, $\mathcal{N}$ is a set of negative (non-relevant) documents, and $\tau$ is a temperature parameter. The numerator rewards similarity between $q$ and its true match; the denominator forces the model to simultaneously push down similarity to all negatives. Minimizing this loss is equivalent to making the positive pair score stand out from the crowd of negatives—directly shaping the relative geometry of the embedding space.</p>

<p>Under this objective, the geometry that emerges is predictable. The temperature $\tau$ acts as a “cluster tightness” parameter: lower temperature concentrates embeddings into tighter clusters; higher temperature allows more spread. The training process creates <strong>equi-angular clusters</strong>—distinct groups separated radially on the embedding hypersphere, with positive pairs clustered tightly within each group.</p>

<p>But the key insight for multi-vector retrieval is what happens <em>within</em> a document. A document about financial derivatives contains tokens spanning multiple semantic roles: financial terminology (“derivative,” “swap,” “yield”), syntactic function words (“the,” “of,” “which”), and contextual modifiers (“complex,” “structured,” “underlying”). Each of these semantic roles corresponds to a different region of the embedding space—a different kernel—and the tokens from a single document are distributed <em>across</em> these kernels.</p>

<p>This is the “multi-kernel” structure. And the “anisotropy” comes from the fact that within each kernel, the variance is not spherical. Hard negative mining—a standard component of ColBERT-style training—specifically shapes the within-kernel geometry in a way that needs careful explanation.</p>

<p>A <strong>hard negative</strong> is a document that the model currently finds <em>hard to reject</em>: it is <em>non-relevant</em> (a true negative by ground-truth label) but has an embedding that is <em>close to the query</em> in the current embedding space—similar wording, overlapping topic, or superficially matching structure. The “hard” refers to the model’s difficulty of discrimination at training time, not to the documents being inherently similar in meaning. This contrasts with <em>easy negatives</em>—randomly sampled documents that are obviously unrelated and produce near-zero gradient, providing no useful learning signal.</p>

<p>Hard negative mining surfaces these ambiguous near-misses and forces the model to push them apart from positives. Because the negatives are semantically proximate to the query (same broad topic, similar surface form), this pressure acts <em>within a semantic neighborhood</em> rather than across the full embedding space. The result is directional: the model learns to stretch the representation along axes that discriminate fine-grained relevance within a topic cluster, while keeping inter-cluster distances large. This directional stretching within semantic clusters is precisely what produces elongated, non-spherical within-kernel covariance.</p>

<p>The hard-negative-augmented loss is:</p>

\[\mathcal{L}_{\text{hard}} = -\log \frac{\exp(\text{sim}(q, p^+) / \tau)}{\exp(\text{sim}(q, p^+) / \tau) + \sum_{p^{\text{hard}} \in \mathcal{H}} \exp(\text{sim}(q, p^{\text{hard}}) / \tau)}\]

<p>where $\mathcal{H}$ are hard negative passages—non-relevant documents that are superficially close to the query (retrieved by BM25 or an earlier model version, but labelled non-relevant). Because these hard negatives are drawn from the <em>same semantic neighborhood</em> as the query, the gradient of this loss explicitly pushes the model to create directional distinctions <em>within</em> semantic neighborhoods rather than between them—stretching the within-kernel distribution along the discrimination axis rather than compressing it uniformly.</p>

<h3 id="quantifying-the-geometry-intra-document-variance-and-residuals">Quantifying the Geometry: Intra-Document Variance and Residuals</h3>

<p>Before examining where PLAID and MUVERA break down—and before the two simplifications that are the practical payoff of this analysis—we need two concrete diagnostic quantities that connect geometry to algorithmic behavior.</p>

<p><strong>Intra-document variance</strong> ($\sigma^2_{\text{intra}}$) measures how spread out a document’s token embeddings are around their centroid:</p>

\[\sigma^2_{\text{intra}}(d) = \frac{1}{|P_d|} \sum_{p \in P_d} \| p - \bar{p}_d \|^2 \quad \text{where } \bar{p}_d = \frac{1}{|P_d|} \sum_{p \in P_d} p\]

<p>High \(\sigma^2_{\text{intra}}\) means a document’s tokens are spread across many semantic regions—many distinct kernels are activated per document. Low $\sigma^2_{\text{intra}}$ means tokens cluster tightly around a single centroid.</p>

<p><strong>Residual magnitude</strong> ($|r|$) measures the reconstruction error after assigning each token to its nearest codebook centroid $c^*$:</p>

\[r = p - c^*, \quad c^* = \underset{c \in \mathcal{C}}{\operatorname{argmin}} \|p - c\|^2\]

<p>A small $|r|$ means the centroid codebook provides a good approximation; a large $|r|$ means the codebook is missing significant structure in the data.</p>

<p>These two quantities—$\sigma^2_{\text{intra}}$ and $|r|$—are the key indicators for predicting how well PLAID and MUVERA will perform on a given dataset. Let’s see why.</p>

<p><img src="./images/intra-doc-variance.svg" alt="Intra-Document Variance" />
<em>A document with <strong>low intra-document variance</strong> (left) has token embeddings tightly clustered around a single centroid—typical of a homogeneous short text passage. A document with <strong>high intra-document variance</strong> (right) has tokens spread across multiple distinct semantic kernels—typical of a complex multi-topic page like a ColPali image with mixed text, charts, and diagrams. PLAID’s centroid pruning is highly selective in the left case; nearly ineffective in the right.</em></p>

<hr />

<h2 id="plaid-under-a-geometric-microscope">PLAID Under a Geometric Microscope</h2>

<p>As I covered in detail in the <a href="https://sam-herman.github.io/blogs/database-as-innovator">previous post</a>, PLAID’s insight is to use a centroid codebook to prune the candidate set early. The centroid inverted list tells you which documents have tokens assigned to which centroids, and you can eliminate documents whose centroids don’t match query token embeddings—without ever reading the expensive full-residual representations from disk.</p>

<p>This is elegant, and it works beautifully for the ColBERTv2 MS MARCO case. But the efficiency of this pruning depends critically on the geometry.</p>

<h3 id="how-intra-document-variance-determines-selectivity">How Intra-Document Variance Determines Selectivity</h3>

<p>PLAID’s document selectivity—the fraction of documents eliminated by centroid pruning—depends on how many distinct centroids each document activates. Let $n_d$ be the number of <em>distinct</em> centroids across all tokens in document $d$. The probability that document $d$ survives an initial candidate generation sweep (i.e., has at least one token centroid matching a query centroid) is approximately:</p>

\[P(\text{document } d \text{ survives}) \approx 1 - \left(1 - \frac{n_d}{C}\right)^{n_{\text{query}}}\]

<p>where $C$ is the total codebook size (e.g., $C = 2^{16} = 65{,}536$ for ColBERTv2) and $n_{\text{query}}$ is the number of distinct centroids activated by the query (roughly equal to the number of query tokens, $|Q|$).</p>

<p>For the ColBERTv2 MS MARCO case, the centroid math works strongly in PLAID’s favor. MS MARCO passages average roughly 55–80 tokens (with <code class="language-plaintext highlighter-rouge">doc_maxlen=180</code> as a ceiling); the default codebook size is $C = 2^{16} = 65{,}536$. Semantically similar tokens are assigned to the same centroid by design, so the number of distinct centroids activated per passage $n_d$ is substantially smaller than the token count—many function words, common phrases, and topically related terms cluster together. The PLAID paper (Santhanam et al., 2022) empirically validates this indirectly: centroid-only retrieval achieves &gt;99% recall for top-$k$ passages within just $10k$ candidates across 8.8M passages on MS MARCO, which is only possible because the average passage activates a very small fraction of the 65,536-centroid codebook. If passages activated many hundreds of distinct centroids, the centroid-only stage would produce far more candidates to reach the same recall threshold.</p>

<p>Now consider what happens when $\sigma^2_{\text{intra}}$ is high—documents whose tokens span many different semantic regions. If $n_d$ grows substantially (approaching or exceeding several hundred distinct centroids per document), the survival probability above approaches 1 for nearly every document in the corpus. <strong>PLAID’s centroid pruning stage eliminates almost nothing.</strong> You’re left paying for Stage 4 exact scoring on the full candidate set, which collapses to the naive baseline.</p>

<p>This is not a hypothetical—it’s a real concern for <strong>ColPali</strong> embeddings of complex document pages, where a single page may contain text in multiple languages, diagrams, charts, captions, and decorative elements, each mapping to different semantic kernels. The intra-document variance for a complex financial PDF page is expected to be substantially higher than for a homogeneous text passage.</p>

<h3 id="how-the-residual-magnitude-determines-quantization-quality">How the Residual Magnitude Determines Quantization Quality</h3>

<p>The second failure mode for PLAID is more subtle but equally important: the quality of the 2-bit residual compression degrades as the residual magnitude grows.</p>

<p>Recall that ColBERTv2 compresses each token by storing the centroid ID plus a quantized residual $r_q$. The reconstruction error for a token $p$ assigned to centroid $c^*$ is:</p>

\[\epsilon = \|p - (c^* + \text{dequantize}(r_q))\|\]

<p>This error grows with $|r| = |p - c^*|$. When the residual is large—meaning the codebook centroids are a poor fit for the actual token distribution—aggressive 2-bit quantization introduces substantial reconstruction noise. The approximate MaxSim scores computed during PLAID’s Stage 3 become less reliable, which in turn means <strong>more false positives survive to Stage 4</strong>, increasing the expensive exact-scoring burden.</p>

<p><img src="./images/residual-magnitude.svg" alt="Residual Magnitude and Centroids" />
<em>The <strong>codebook</strong> (left) divides embedding space into semantic regions, each with a centroid c</em>. When a token p falls near its assigned centroid (center panel), the residual r = p − c* is small and 2-bit quantization introduces minimal reconstruction error—p̂ ≈ p. When p is far from any centroid (right panel), the residual is large and deQuantization produces an approximation p̂ with significant drift—degrading the Stage 3 scoring fidelity in PLAID.*</p>

<p>Formally, the MaxSim approximation error can be bounded as:</p>

\[|\text{MaxSim}(Q, P) - \widehat{\text{MaxSim}}(Q, P)| \leq |Q| \cdot \max_i \|\hat{q}_i - q_i\| \cdot \max_j \|p_j\|\]

<p>where $\hat{q}_i$ is the reconstructed approximation of query token $q_i$. When residuals are large (poor codebook fit), these reconstruction errors compound, and the bound loosens—meaning the centroid interaction scores in Stage 3 are less trustworthy as proxies for exact MaxSim.</p>

<h3 id="how-sumsim-and-top-k-sum-break-plaids-selectivity">How SumSim and Top-K Sum Break PLAID’s Selectivity</h3>

<p>This is where the set-to-set similarity function becomes a first-class concern.</p>

<p>PLAID’s Stage 2 (centroid pruning) works by reasoning about MaxSim: if a centroid $c_k$ has low similarity to <em>all</em> query tokens, it cannot contribute to the MaxSim score, so it can be safely pruned. The pruning logic is:</p>

\[\text{Prune centroid } c_k \text{ if } \max_{i} \langle q_i, c_k \rangle &lt; \theta\]

<p>This is sound for MaxSim because the max operation means that a centroid that doesn’t strongly match <em>any</em> query token can never be the “winning match” for any query token. The contribution of centroid $c_k$ to the final score is bounded by $\max_i \langle q_i, c_k \rangle$.</p>

<p>Now replace MaxSim with <strong>SumSim</strong>:</p>

\[\text{SumSim}(Q, P) = \sum_{i} \sum_{j} \langle q_i, p_j \rangle\]

<p>Under SumSim, every document token contributes to every query token’s score—there’s no “winner takes all.” A centroid with low similarity to all query tokens individually may still be part of a document that has high total aggregate similarity. The safe pruning condition for MaxSim no longer holds. In fact, <strong>any centroid with a non-zero dot product with any query token must be considered</strong>, which in practice means centroid pruning eliminates essentially nothing.</p>

<p>The same argument applies to <strong>Top-K Sum</strong>, where the contribution of a document token is capped at being in the top-$k$ matches per query token rather than strictly the maximum. The mathematical boundary on the residual’s contribution to the aggregate score is looser:</p>

\[|\text{contribution}(c_k, Q)| \leq |Q| \cdot k \cdot \|\langle Q, c_k \rangle\|_\infty\]

<p>This larger bound means PLAID’s pruning threshold must be set proportionally lower to remain safe, which in turn means fewer candidates get pruned. For large $k$, the advantage essentially vanishes.</p>

<blockquote>
  <p><strong>The Bottom Line for PLAID:</strong> PLAID is purpose-built for MaxSim scoring on multi-kernel anisotropic data where intra-document variance is moderate and residuals are small. When either condition fails—high variance, large residuals, or a non-MaxSim similarity function—the algorithm’s guarantees weaken and its practical efficiency degrades.</p>
</blockquote>

<hr />

<h2 id="muvera-under-a-geometric-microscope">MUVERA Under a Geometric Microscope</h2>

<p>MUVERA’s approach is mathematically different from PLAID’s: rather than building a custom inverted index for multi-vector data, it transforms multi-vector sets into fixed-dimensional vectors (FDEs) via SimHash partitioning, enabling standard single-vector MIPS. The theoretical promise is an $\epsilon$-approximation guarantee for the Chamfer similarity (MaxSim).</p>

<p>But the theory comes with an assumption baked in: that the SimHash partitioning correctly places nearby vectors into the same bucket. Let’s examine what happens when this assumption breaks down.</p>

<h3 id="simhash-and-the-anisotropy-problem">SimHash and the Anisotropy Problem</h3>

<p>SimHash partitions $\mathbb{R}^d$ using $b$ random hyperplanes ${h_1, \ldots, h_b}$, each drawn independently and uniformly from the sphere $\mathbb{S}^{d-1}$. Two vectors $u, v$ are assigned to the same partition if $\text{sign}(\langle h_l, u \rangle) = \text{sign}(\langle h_l, v \rangle)$ for all $l = 1, \ldots, b$.</p>

<p>The probability that $u$ and $v$ share a partition is exactly:</p>

\[P(\text{same partition}) = \left(1 - \frac{\theta_{uv}}{\pi}\right)^b\]

<p>where $\theta_{uv} = \arccos\left(\frac{\langle u, v \rangle}{|u||v|}\right)$ is the angle between them. This monotonically decreases in $\theta_{uv}$ and in $b$—as vectors become more similar (smaller angle) and as you use fewer hyperplanes, collision probability increases.</p>

<p>For <strong>isotropic data</strong>, random hyperplanes are an excellent partitioning strategy because no direction is privileged; the hyperplanes respect the underlying geometry.</p>

<p>For <strong>anisotropic data</strong>, the situation is different. Consider a kernel (semantic cluster) with a covariance matrix $\boldsymbol{\Sigma}_k$ that is elongated—much larger variance along a specific direction $\mathbf{u}$ than orthogonal directions:</p>

\[\boldsymbol{\Sigma}_k = \sigma_\parallel^2 \mathbf{u}\mathbf{u}^\top + \sigma_\perp^2 (\mathbf{I} - \mathbf{u}\mathbf{u}^\top), \quad \sigma_\parallel \gg \sigma_\perp\]

<p>Within this kernel, two vectors $p_1, p_2$ that are nearby in $\ell_2$ distance may have a relatively large angular separation $\theta_{p_1 p_2}$, because the elongation along $\mathbf{u}$ produces many pairs with large directional displacement but small absolute distance.</p>

<p>Now a random hyperplane $h$ that is <em>perpendicular to</em> $\mathbf{u}$—which is not unlikely, given random sampling—will <strong>split the cluster along its major axis</strong>, separating $p_1$ and $p_2$ into different buckets despite their proximity. This directly reduces the probability that a query token and its best-matching document token share a SimHash partition.</p>

<p>The practical consequence: in a multi-kernel anisotropic space, the effective $P(\text{same partition})$ for true nearest-neighbor pairs is substantially lower than the theoretical prediction from cosine similarity alone. This means MUVERA’s FDE dot product systematically underestimates Chamfer similarity, increasing recall error. <em>(To my knowledge, this degradation has not been directly quantified on ColBERT-family embeddings as a function of within-kernel anisotropy—it would be a clean empirical contribution.)</em></p>

<p><img src="./images/muvera-anisotropy.svg" alt="MUVERA SimHash Anisotropy Failure" />
<em>SimHash failure with anisotropic data. Left: in an isotropic (spherical) cluster, a random hyperplane is unlikely to separate nearby vectors q and p</em>—they land in the same bucket and the FDE captures the correct match. Right: in an elongated anisotropic cluster trained with hard negatives, a hyperplane perpendicular to the major axis splits q and p* into different buckets despite their small ℓ₂ distance. The effective angular separation θ_eff inflated by anisotropy is what drives the collision probability down—this is the root cause of MUVERA’s recall degradation on ColBERT-family embeddings.*</p>

<h3 id="the-over-partitioning-problem">The Over-Partitioning Problem</h3>

<p>There’s a second failure mode for MUVERA that operates in the opposite direction: using too many hash bits $b$ (too many partitions) in an anisotropic setting.</p>

<p>In MUVERA’s FDE construction, the total number of partitions is $K = 2^b$. The FDE dimension grows as $r \times K \times d_{\text{proj}}$, so practitioners balance partitioning fineness against FDE size. But increasing $b$ has a subtle secondary effect: with finer partitioning on anisotropic data, the probability that the <em>same</em> pair of similar vectors land in the same bucket decreases faster than on isotropic data.</p>

<p>Formally, for an anisotropic cluster with effective angle $\theta_{\text{eff}} &gt; \theta_{uv}$ (the “apparent” angular separation inflated by anisotropy), the collision probability becomes:</p>

\[P(\text{correct collision}) \approx \left(1 - \frac{\theta_{\text{eff}}}{\pi}\right)^b\]

<p>which decays exponentially in $b$. As $b$ grows, the effective recall of MUVERA’s candidate generation phase degrades—not from the FDE being uninformative, but from the partitioning not placing nearest neighbors into the same bucket reliably.</p>

<p>There is therefore a trade-off: too few partitions ($b$ small) gives coarse FDEs with poor discrimination between non-relevant documents; too many partitions ($b$ large) causes correct nearest-neighbor pairs to be separated, reducing recall. For isotropic data, this trade-off has a natural sweet spot that’s relatively easy to tune. For anisotropic data—and especially multi-kernel anisotropic data—the window is narrower and the optimum shifts in ways that depend on the specific covariance structure of each kernel.</p>

<blockquote>
  <p><strong>The Bottom Line for MUVERA:</strong> MUVERA’s theoretical guarantees assume that SimHash correctly partitions the embedding space such that nearest neighbors share buckets with high probability. In multi-kernel anisotropic spaces—which is what trained ColBERT-family models produce—this assumption is violated for elongated within-kernel distributions. The failure is exacerbated by increasing the number of partitions.</p>
</blockquote>

<hr />

<h2 id="hacks-where-elaborate-infrastructure-isnt-needed">Hacks: Where Elaborate Infrastructure Isn’t Needed</h2>

<p>Now for the part I find genuinely exciting—two cases where the “hard” multi-vector problem turns out to be trivially reducible to a problem we already know how to solve.</p>

<h3 id="hack-1-sumsim-is-just-mean-pooled-mips">Hack #1: SumSim Is Just Mean-Pooled MIPS</h3>

<p>At first glance, SumSim seems to require multi-vector infrastructure: you have sets $Q$ and $P$, and you need to sum over all $|Q| \times |P|$ pairwise dot products. Surely this requires something like PLAID or MUVERA?</p>

<p>Let’s do the algebra:</p>

\[\text{SumSim}(Q, P) = \sum_{i=1}^{|Q|} \sum_{j=1}^{|P|} \langle q_i, p_j \rangle\]

<p>By the bilinearity of the inner product:</p>

\[= \left\langle \sum_{i=1}^{|Q|} q_i,\ \sum_{j=1}^{|P|} p_j \right\rangle\]

\[= |Q| \cdot |P| \cdot \left\langle \frac{1}{|Q|}\sum_{i=1}^{|Q|} q_i,\ \frac{1}{|P|}\sum_{j=1}^{|P|} p_j \right\rangle\]

\[= |Q| \cdot |P| \cdot \langle \bar{q}, \bar{p} \rangle\]

<p>where $\bar{q}$ and $\bar{p}$ are the <strong>mean-pooled</strong> embeddings of the query and document sets, respectively.</p>

<p>When comparing documents for a fixed query, $|Q|$ is constant and can be dropped. If document sets also have fixed size $|P|$ (as with ColPali’s 1030 patches), the prefactor is a constant. We are left with:</p>

\[\text{SumSim}(Q, P) \propto \langle \bar{q}, \bar{p} \rangle\]

<p><strong>This is a standard MIPS problem.</strong> You can compute mean-pooled representations offline for every document, index them in any standard ANN system (HNSW, DiskANN, ScaNN), and retrieve them with a single mean-pooled query vector. No PLAID. No MUVERA. No custom infrastructure whatsoever.</p>

<p>This has immediate practical implications. Any retrieval system where the scoring function is SumSim—aggregate affinity, group recommendation, cross-modal matching with all-pair scoring—does not need multi-vector retrieval infrastructure. Mean pooling and a standard vector database is the correct solution, and it’s orders of magnitude cheaper.</p>

<p>The mathematical equivalence also provides a clear intuition for when SumSim is the right choice: when every token-to-token interaction is genuinely informative, with no winner-takes-all dynamics. For dense, topic-homogeneous queries and documents, this can be reasonable. For queries where only a few tokens carry the semantic signal—typical in information retrieval—MaxSim’s “find the best match” semantics are more appropriate.</p>

<p><img src="./images/sumsim-reduction.svg" alt="SumSim to Mean-Pooled MIPS Reduction" />
<em>The four-step reduction: (1) SumSim requires |Q|×|P| pairwise dot products—superficially a multi-vector problem; (2) bilinearity of the inner product collapses these into a single dot product of summed vectors; (3) factoring out the set sizes gives the mean-pooled dot product ⟨q̄, p̄⟩ up to a constant; (4) this is a standard MIPS problem—index p̄ per document in any ANN system and query with q̄. No PLAID. No MUVERA. O(log N) lookup.</em></p>

<blockquote>
  <p><strong>Practical implication:</strong> If you’re using SumSim and running PLAID or MUVERA, you’re paying for infrastructure you don’t need. Mean-pool your embeddings, use a standard HNSW index, and call it done.</p>
</blockquote>

<h3 id="hack-2-colpali-doesnt-need-what-colbert-needs">Hack #2: ColPali Doesn’t Need What ColBERT Needs</h3>

<p>ColPali generates exactly <strong>1030 patch embeddings per page</strong> (a fixed 32×32 grid of 1024 image patches plus 6 instruction text tokens), each projected into a 128-dimensional space. This fixed structure is fundamentally different from ColBERTv2 text embeddings, where document length varies—a 20-token passage might have 20 embeddings; a 300-token passage has 300.</p>

<p>This variability in ColBERT text is precisely what makes indexing challenging. You can’t pre-aggregate to a fixed representation because you don’t know how many tokens each document will have, and the token count itself is semantically meaningful (document length affects the MaxSim score normalization).</p>

<p>With ColPali, this problem evaporates. Every page has exactly 1030 patches. This opens up indexing strategies that simply aren’t available for variable-length ColBERT:</p>

<p><strong>The Vespa Approach: Per-Token Nearest Neighbor Search.</strong> Vespa’s production ColPali implementation indexes each patch embedding as an individual vector in an HNSW index, then at query time, issues one <code class="language-plaintext highlighter-rouge">nearestNeighbor</code> query per query token—retrieving the $k$ closest pages per query token using binary (Hamming) embeddings as a first pass. This multi-phase pipeline looks like:</p>

<ol>
  <li><strong>Phase 0 (pre-filtering):</strong> For each of $|Q|$ query tokens, retrieve $k_0$ nearest page-patches using Hamming distance on binarized embeddings. Union the corresponding pages.</li>
  <li><strong>Phase 1 (approximate MaxSim):</strong> Score surviving pages with approximate MaxSim using inverted Hamming distance. Keep top $k_1$ pages.</li>
  <li><strong>Phase 2 (exact MaxSim):</strong> Rerank the top $k_1$ pages using full-precision float MaxSim.</li>
</ol>

<p>With binarized patch embeddings (128 bits = 16 bytes per patch, a 32× compression from float32), the per-query cost scales as $|Q| \times k_0 \times O(\log N)$ for the HNSW lookups—very manageable.</p>

<p><img src="./images/colpali-hnsw-workflow.svg" alt="ColPali Multi-HNSW Retrieval Workflow" />
<em>The complete ColPali retrieval pipeline. Offline: pages are encoded into 1030 patch embeddings each, binarized to 16 bytes/patch (32× compression), and all patches from all pages are stored in a single shared HNSW index keyed by Hamming distance. Online: each query token independently issues a nearest-neighbor lookup against the shared index (Phase 0), the resulting page sets are unioned, scored with approximate Hamming-based MaxSim (Phase 1), and the top candidates are re-ranked with exact float MaxSim loaded from disk (Phase 2). The fixed patch count of 1030 per page is what makes the per-token HNSW strategy tractable—variable-length ColBERT text cannot exploit this structure.</em></p>

<p><strong>The Fixed-Centroid Alternative.</strong> Because the number of patches per document is fixed at 1030, you can pre-cluster each document’s patches into a fixed number of centroids ($C_d$, say 10-30) using k-means, and index only those centroids. This creates a compact two-level structure:</p>

<ul>
  <li><strong>Level 1:</strong> $C_d$ centroids per document in a standard HNSW index (fast candidate generation)</li>
  <li><strong>Level 2:</strong> Full 1030 patch embeddings per document on disk (for exact MaxSim scoring of top candidates)</li>
</ul>

<p>Candidate generation now requires $O(|Q| \times \log(N \times C_d))$ operations—proportional to the number of query tokens and the log-size of the centroid index. Exact MaxSim is only computed for the top-$k$ candidates, involving $|Q| \times 1030$ dot products per candidate.</p>

<p>This is essentially the PLAID architecture—but with a key difference. Because every document has exactly 1030 patches (not a variable number), the codebook can be constructed per-document rather than globally, and the indexing structure can leverage the fixed cardinality to avoid the centroid-ID allocation overhead that PLAID requires at corpus scale.</p>

<p><strong>The Cost Comparison.</strong> For ColPali at 1 billion pages, with binarized patch embeddings:</p>

<table>
  <thead>
    <tr>
      <th>Configuration</th>
      <th>Storage per Page</th>
      <th>1B Pages Storage</th>
      <th>Phase 0 Recall</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ColPali float32</td>
      <td>1030 × 128 × 4 B = 526 KB</td>
      <td>~526 TB</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>ColPali bfloat16</td>
      <td>1030 × 128 × 2 B = 263 KB</td>
      <td>~263 TB</td>
      <td>~99%</td>
    </tr>
    <tr>
      <td>ColPali binarized</td>
      <td>1030 × 16 B = 16.5 KB</td>
      <td>~16.5 TB</td>
      <td>~94% (recovered in phase 2)</td>
    </tr>
    <tr>
      <td>Centroid index (30 centroids, bfloat16)</td>
      <td>30 × 128 × 2 B = 7.7 KB</td>
      <td>~7.7 TB</td>
      <td>variable</td>
    </tr>
  </tbody>
</table>

<p>The binarized approach is the practical winner: 32× compression with minimal recall loss (binarization results in a small drop on DocVQA, recoverable in the float re-ranking phase), and compatibility with Hamming-distance HNSW which is natively supported in systems like Vespa.</p>

<blockquote>
  <p><strong>Practical implication:</strong> ColPali’s fixed patch count of 1030 per page is a gift, not a constraint. It enables indexing strategies based on fixed-cardinality multi-vector representations that don’t exist for variable-length ColBERT text, and makes brute-force MaxSim feasible at smaller scales (tens of millions of pages) without any specialized infrastructure.</p>
</blockquote>

<hr />

<h2 id="pulling-it-together-a-decision-framework">Pulling It Together: A Decision Framework</h2>

<p>The taxonomy that emerges from this analysis is more nuanced than “use PLAID for late interaction” or “use MUVERA if you want standard infrastructure.” The right architecture depends on <em>both</em> the similarity function and the dataset geometry.</p>

<table>
  <thead>
    <tr>
      <th>Similarity Function</th>
      <th>Data Geometry</th>
      <th>Recommended Approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>SumSim</strong></td>
      <td>Any</td>
      <td>Mean-pool → standard HNSW. No multi-vector infrastructure needed.</td>
    </tr>
    <tr>
      <td><strong>MaxSim (Chamfer)</strong></td>
      <td>Multi-kernel anisotropic, moderate $\sigma^2_{\text{intra}}$</td>
      <td>PLAID (purpose-built; proven at MS MARCO scale)</td>
    </tr>
    <tr>
      <td><strong>MaxSim (Chamfer)</strong></td>
      <td>Multi-kernel anisotropic, high $\sigma^2_{\text{intra}}$</td>
      <td>PLAID with larger codebook; verify stage-2 selectivity empirically</td>
    </tr>
    <tr>
      <td><strong>MaxSim (Chamfer)</strong></td>
      <td>Any</td>
      <td>MUVERA (standard MIPS infra + re-rank)—but validate recall on your data; tune $b$ carefully for anisotropic data</td>
    </tr>
    <tr>
      <td><strong>Top-K Sum</strong></td>
      <td>Multi-kernel anisotropic</td>
      <td>Custom pipeline; PLAID pruning doesn’t apply directly; consider MUVERA with adjusted FDE aggregation</td>
    </tr>
    <tr>
      <td><strong>MaxSim (ColPali)</strong></td>
      <td>High intra-doc variance visual data</td>
      <td>Vespa-style per-token HNSW + binarization, or fixed-centroid approach</td>
    </tr>
    <tr>
      <td><strong>MaxSim (ColPali)</strong></td>
      <td>Small corpora (&lt;10M pages)</td>
      <td>Brute-force float MaxSim (1030 patches × N pages × Q tokens) is feasible at moderate scale</td>
    </tr>
  </tbody>
</table>

<p>The common thread in the “hacks”: when the structure of your problem is more constrained than the general case PLAID/MUVERA were designed for, simpler solutions often dominate.</p>

<hr />

<h2 id="implications-for-embedding-model-design">Implications for Embedding Model Design</h2>

<p>The geometry insights above have a direct design implication that’s often overlooked: you can <em>train</em> for a geometry that’s more indexing-friendly.</p>

<p>In the <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">previous post on domain adaptation</a>, we discussed how domain-specific fine-tuning reshapes the embedding space. The same techniques that tune vocabulary coverage also reshape the geometric structure—and we can be deliberate about it.</p>

<p>For applications where you know the similarity function is MaxSim (the ColBERT case), moderate intra-document variance is desirable: enough spread to achieve good recall in PLAID’s centroid lookup, but compact enough to make Stage 2 pruning effective. You can steer this via the training objective.</p>

<p>One approach is to add an <strong>intra-document variance regularizer</strong> to the contrastive loss:</p>

\[\mathcal{L}_{\text{total}} = \mathcal{L}_{\text{InfoNCE}} + \lambda \cdot \mathcal{L}_{\text{variance}}\]

<p>where:</p>

\[\mathcal{L}_{\text{variance}} = \left| \frac{1}{|\mathcal{D}|} \sum_{d \in \mathcal{D}} \sigma^2_{\text{intra}}(d) - \sigma^2_{\text{target}} \right|^2\]

<p>By setting $\sigma^2_{\text{target}}$ to a value that empirically yields good PLAID selectivity on your codebook size, you bake indexing efficiency into the embedding model itself.</p>

<p>Similarly, for applications using MUVERA in anisotropic spaces, you might regularize for <em>lower</em> intra-kernel anisotropy—encouraging rounder cluster shapes that play better with random hyperplane partitioning:</p>

\[\mathcal{L}_{\text{isotropy}} = \sum_{k} \left\| \hat{\boldsymbol{\Sigma}}_k - \sigma^2 \mathbf{I} \right\|_F\]

<p>This is, admittedly, more involved—it requires estimating per-cluster covariance during training, which is non-trivial. But the point stands: embedding model design and indexing infrastructure aren’t independent choices. The geometry your model learns directly determines how well your index performs, and you have more control over that geometry than is commonly appreciated.</p>

<hr />

<h2 id="a-note-on-what-this-doesnt-change">A Note on What This Doesn’t Change</h2>

<p>Before closing, it’s worth being precise about what this analysis does and doesn’t refute.</p>

<p><strong>PLAID and MUVERA are genuinely impressive.</strong> For the ColBERT-MaxSim-on-MS MARCO problem they were designed for, they represent major advances, and the economic analysis from the previous post holds: late interaction is now viable at scales where it previously wasn’t. Nothing in this post undercuts that.</p>

<p><strong>MaxSim on ColBERT-style text data is the dominant use case</strong>, and on that use case, PLAID works well. The failure modes I’ve described are real but they’re edge cases for text retrieval—they become primary concerns for multi-modal retrieval (ColPali), non-standard similarity functions, or domain-specific data with unusual geometric properties.</p>

<p><strong>The “hacks” are not always hacks.</strong> SumSim may genuinely be the right scoring function for some applications—my point is only that if it’s your function, you don’t need specialized infrastructure. And ColPali’s fixed patch structure is a deliberate design choice with real advantages; its indexing simplicity is one of those advantages.</p>

<p>What I hope this analysis provides is a more precise vocabulary for thinking about multi-vector retrieval: similarity function and dataset geometry as the two fundamental axes, with infrastructure choices following from them rather than preceding them.</p>

<hr />

<h2 id="looking-forward">Looking Forward</h2>

<p>The questions this analysis raises are, I think, more interesting than the answers it provides:</p>

<p><strong>Can we build geometry-aware hashing for multi-vector retrieval?</strong> Instead of random hyperplanes, partitioning based on the actual covariance structure of token embeddings—something like k-means-based LSH—would be much more robust to anisotropic data. PLAID’s centroid codebook is already a step in this direction; the question is whether a similar idea can be incorporated into MUVERA’s FDE construction.</p>

<p><strong>What’s the right similarity function for visual retrieval?</strong> ColPali uses MaxSim because ColBERT used MaxSim—but is that right for image patches? A patch that depicts a chart legend might be uniformly relevant to many query tokens, which is a more SumSim-like interaction. Benchmarking different similarity functions on visual retrieval tasks seems underexplored.</p>

<p><strong>How does retrieval geometry change with model scale?</strong> Larger VLMs (ColQwen, ColGemma) likely produce patch embeddings with different geometric properties than PaliGemma-based ColPali. Understanding how geometry scales with model capacity could inform both model design and infrastructure choices.</p>

<p><strong>Can training objectives directly optimize for indexing efficiency?</strong> The variance regularization idea above is speculative, but the general question—can we train embeddings that are simultaneously high-quality for retrieval and favorable for ANN indexing—seems worth pursuing as a joint optimization problem.</p>

<p>These are the questions I’m watching closely as multi-vector retrieval continues to mature. The one thing I’m confident of: the conversation will be richer when it’s grounded in geometry and scoring functions, not just benchmark numbers on MS MARCO.</p>

<hr />

<p><em>As always, the analysis here represents my current thinking based on the public literature and my own experimentation. If you have empirical results that confirm or challenge any of these geometric intuitions, I’d love to hear about it.</em></p>

<p><strong>Previous in the series:</strong> <a href="https://sam-herman.github.io/blogs/database-as-innovator">When the Database Becomes the Bottleneck: The Economics of Vector Retrieval and the Technologies That Change the Math</a></p>

<p><strong>Previous:</strong> <a href="/blogs/storage-for-vectors.html">The Reality of Vector Quantization: A Deep Dive into Product Quantization Trade-offs</a></p>

<hr />

<h2 id="references">References</h2>

<ol>
  <li>
    <p><strong>Santhanam, K., Khattab, O., Potts, C., &amp; Zaharia, M.</strong> (2022). PLAID: An Efficient Engine for Late Interaction Retrieval. <em>Proceedings of the 31st ACM International Conference on Information and Knowledge Management (CIKM ‘22)</em>. <a href="https://arxiv.org/abs/2205.09707">arXiv:2205.09707</a></p>
  </li>
  <li>
    <p><strong>Dhulipala, L., Jayaram, R., Knittel, A., Liu, J., &amp; Mirrokni, V.</strong> (2024). MUVERA: Multi-Vector Retrieval via Fixed Dimensional Encodings. <em>Advances in Neural Information Processing Systems 37 (NeurIPS 2024)</em>. <a href="https://arxiv.org/abs/2405.19504">arXiv:2405.19504</a></p>
  </li>
  <li>
    <p><strong>Santhanam, K., Khattab, O., Saad-Falcon, J., Potts, C., &amp; Zaharia, M.</strong> (2021). ColBERTv2: Effective and Efficient Retrieval via Lightweight Late Interaction. <em>arXiv:2112.01488</em>.</p>
  </li>
  <li>
    <p><strong>Faysse, M., et al.</strong> (2024). ColPali: Efficient Document Retrieval with Vision Language Models. <em>arXiv:2407.01449</em>.</p>
  </li>
  <li>
    <p><strong>van den Oord, A., Li, Y., &amp; Vinyals, O.</strong> (2018). Representation Learning with Contrastive Predictive Coding. <em>arXiv:1807.03748</em>.</p>
  </li>
  <li>
    <p><strong>Wang, F., &amp; Liu, H.</strong> (2021). Understanding the Behaviour of Contrastive Loss. <em>Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)</em>, 2495–2504.</p>
  </li>
  <li>
    <p><strong>Rusak, E., et al.</strong> (2024). AnInfoNCE: Anisotropic Contrastive Learning. <em>ICLR Workshop on High-dimensional Learning Dynamics</em>.</p>
  </li>
  <li>
    <p><strong>Charikar, M. S.</strong> (2002). Similarity Estimation Techniques from Rounding Algorithms. <em>Proceedings of the Thirty-Fourth Annual ACM Symposium on Theory of Computing</em>, 380–388. (SimHash / LSH for cosine similarity)</p>
  </li>
  <li>
    <p><strong>Malkov, Y. A., &amp; Yashunin, D. A.</strong> (2018). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence</em>, 42(4), 824–836.</p>
  </li>
  <li>
    <p><strong>Mathisen, J., et al.</strong> (2024). Scaling ColPali to Billions of PDFs with Vespa. <em>Vespa Blog</em>. <a href="https://blog.vespa.ai/scaling-colpali-to-billions/">blog.vespa.ai/scaling-colpali-to-billions</a></p>
  </li>
  <li>
    <p><strong>Herman, S.</strong> (2025). When the Database Becomes the Bottleneck: The Economics of Vector Retrieval and the Technologies That Change the Math. <a href="https://sam-herman.github.io/blogs/database-as-innovator">Blog post</a></p>
  </li>
  <li>
    <p><strong>Herman, S.</strong> (2025). Teaching Embedding Models New Words: A Deep Dive into Domain Adaptation. <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">Blog post</a></p>
  </li>
  <li>
    <p><strong>Herman, S.</strong> (2025). The Reality of Vector Quantization: A Deep Dive into Product Quantization Trade-offs. <a href="/blogs/storage-for-vectors.html">Blog post</a></p>
  </li>
</ol>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[Setting the Stage]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sam-herman.github.io/blogs/images/geometry-comparison.png" /><media:content medium="image" url="https://sam-herman.github.io/blogs/images/geometry-comparison.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">When the Database Becomes the Bottleneck: The Economics of Vector Retrieval and the Technologies That Change the Math</title><link href="https://sam-herman.github.io/blogs/database-as-innovator.html" rel="alternate" type="text/html" title="When the Database Becomes the Bottleneck: The Economics of Vector Retrieval and the Technologies That Change the Math" /><published>2026-02-07T00:00:00+00:00</published><updated>2026-02-07T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/database-as-innovator</id><content type="html" xml:base="https://sam-herman.github.io/blogs/database-as-innovator.html"><![CDATA[<h2 id="setting-the-stage-where-we-left-off">Setting the Stage: Where We Left Off</h2>

<p>In my <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">previous post on domain-adapted neural search</a>, I argued that embedding quality is the most underleveraged factor in retrieval systems—easy to skip in the rush to get a pipeline running, but often the single highest-impact investment you can make. Domain adaptation, vocabulary extension, and careful fine-tuning can yield dramatic relevance improvements that no amount of index tuning can replicate.</p>

<p>That remains true. But now I want to complete the picture—because <strong>both halves matter</strong>, and the question of <em>which half matters more</em> depends entirely on scale.</p>

<p><img src="./images/hero-pivot-point.png" alt="The Two Phases of Vector Search Investment" />
<em>At small corpus sizes, embedding quality dominates the cost-quality equation. As the corpus grows, storage and retrieval costs climb linearly while inference stays nearly flat—crossing over around 50–100 million vectors. We’ll derive these curves in detail below.</em></p>

<p>Once you’ve invested in getting your embeddings right, the <em>database technology</em> becomes absolutely critical. And not in the way most people think.</p>

<p>The database doesn’t just store and retrieve vectors. At scale, it determines whether your retrieval improvements are <em>economically feasible to operate</em>. It determines whether a research result translates into a production system that a business can actually run.</p>

<p>But the relationship runs deeper than a one-way pipeline from “embedding model” to “database.” <strong>The influence is bidirectional.</strong> When ColBERT’s multi-vector representations proved too expensive to index and retrieve at scale, the community didn’t just wait for better hardware—it pushed back to the embedding design itself. SPLADE emerged as a direct response: a learned sparse model engineered to be <em>compatible with existing inverted index infrastructure</em>, deliberately trading some of ColBERT’s expressiveness for the ability to leverage the most battle-tested indexing technology available. And then, when new indexing algorithms (PLAID, MUVERA) made multi-vector retrieval economically viable again, the pendulum swung back—enabling richer representations like ColPali and ColQwen that wouldn’t have been pursued without confidence that the infrastructure could support them.</p>

<p>This bidirectional feedback loop—where embedding models shape what database technology needs to exist, and database economics shape what embedding models get designed—is one of the central themes of this post. It’s the reason the story of retrieval isn’t a clean linear progression, but a conversation between two evolving fields.</p>

<p>In this post, I want to build an economic framework for understanding when and why this shift happens, and then explore two technologies—<strong>PLAID</strong> and <strong>MUVERA</strong>—that are reshaping what’s possible in the world of multi-vector retrieval.</p>

<hr />

<h2 id="the-economic-pivot-point">The Economic Pivot Point</h2>

<h3 id="when-inference-dominates">When Inference Dominates</h3>

<p>At small to moderate scale, the dominant cost in a neural search system is <strong>embedding inference</strong>—the GPU compute required to encode queries and documents into vectors. Let’s build some intuition for why, and ground our cost estimates in the specifics of what drives inference pricing.</p>

<h4 id="what-drives-embedding-inference-cost">What Drives Embedding Inference Cost?</h4>

<p>Embedding inference cost is fundamentally a function of <strong>model size</strong> (parameter count), <strong>hardware utilization</strong>, and <strong>throughput</strong>. These factors interact in ways that are worth understanding before we plug numbers into a spreadsheet.</p>

<p><strong>Model parameters and their cost implications:</strong> Embedding models span a wide range of sizes. At the compact end, <code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code> is a 6-layer transformer with ~22 million parameters producing 384-dimensional embeddings—small enough to run efficiently on a single CPU. Mid-range models like <code class="language-plaintext highlighter-rouge">bge-base-en-v1.5</code> and <code class="language-plaintext highlighter-rouge">GTE-base</code> are built on BERT-base architectures (~110M parameters, 768 dimensions), while <code class="language-plaintext highlighter-rouge">bge-large-en-v1.5</code> and <code class="language-plaintext highlighter-rouge">GTE-large</code> use BERT-large backbones (~335M parameters, 1024 dimensions). The newest generation of LLM-based embedding models—<code class="language-plaintext highlighter-rouge">e5-mistral-7b-instruct</code>, <code class="language-plaintext highlighter-rouge">gte-qwen2-7b</code>—scale up to 7 billion parameters but produce higher-quality embeddings, particularly for retrieval tasks.</p>

<p>The relationship between parameter count and cost is roughly linear for transformer-based models: more parameters means more floating-point operations per token, which translates directly to GPU-seconds per encoded document. A 7B parameter embedding model requires approximately 30–60× more compute per token than a 110M parameter model, but also tends to deliver meaningfully better retrieval quality.</p>

<p><strong>Hardware factors:</strong> Inference cost also depends on the GPU (or CPU) hardware, its memory bandwidth, and how efficiently the deployment batches requests. An NVIDIA H100 can process embedding batches far faster than an A10G, but also costs proportionally more per hour. The key metric is <strong>throughput (tokens/second/dollar)</strong>—and here, newer hardware generally wins. On an H100 (with FP16 inference and dynamic batching), a 110M parameter model like <code class="language-plaintext highlighter-rouge">bge-base-en-v1.5</code> can encode upwards of 10,000 documents per second, while a 7B parameter model might manage 200–500 documents per second on the same hardware.</p>

<p><strong>Managed API vs. self-hosted economics:</strong> Cloud API providers (OpenAI, Google, Cohere) fold all of this into a per-token price that amortizes hardware, batching efficiency, and operational overhead. Self-hosted deployments on dedicated GPUs can achieve significantly lower per-token costs at high utilization, but require engineering investment in serving infrastructure (e.g., HuggingFace Text Embeddings Inference, vLLM, or ONNX Runtime).</p>

<p>The following chart visualizes the relationship between model size, throughput, and self-hosted cost:</p>

<p><img src="./images/model-cost-throughput.svg" alt="Embedding Model Size vs Inference Cost and Throughput" />
<em>Figure 1: Larger models produce better embeddings but at sharply lower throughput and higher cost. A 7B parameter model is ~100× slower than a 22M parameter model—but may deliver meaningfully better retrieval quality. The right choice depends on your quality requirements and scale.</em></p>

<p>With that context, here are representative 2025 API pricing benchmarks:</p>

<table>
  <thead>
    <tr>
      <th>Provider / Model</th>
      <th>Approx. Parameters</th>
      <th>Dimensions</th>
      <th>Price per Million Tokens</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-small</code></td>
      <td>undisclosed</td>
      <td>1,536</td>
      <td>$0.02</td>
    </tr>
    <tr>
      <td>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-large</code></td>
      <td>undisclosed</td>
      <td>3,072</td>
      <td>$0.13</td>
    </tr>
    <tr>
      <td>Google <code class="language-plaintext highlighter-rouge">gemini-embedding-001</code></td>
      <td>undisclosed</td>
      <td>768</td>
      <td>$0.15</td>
    </tr>
    <tr>
      <td>Cohere Embed 4</td>
      <td>undisclosed</td>
      <td>1,024</td>
      <td>$0.12</td>
    </tr>
    <tr>
      <td>Self-hosted <code class="language-plaintext highlighter-rouge">bge-base-en-v1.5</code> (H100)</td>
      <td>~110M</td>
      <td>768</td>
      <td>&lt;$0.01</td>
    </tr>
    <tr>
      <td>Self-hosted <code class="language-plaintext highlighter-rouge">e5-mistral-7b</code> (H100)</td>
      <td>~7B</td>
      <td>4,096</td>
      <td>~$0.03–0.05</td>
    </tr>
  </tbody>
</table>

<p><em>Note: OpenAI and Google do not disclose parameter counts for their embedding models, but pricing tiers strongly suggest the “small” variants use compact architectures (likely in the 100–300M range) while “large” variants use substantially bigger backbones. The pricing difference—6.5× between OpenAI’s small and large—is consistent with the compute scaling you’d expect from moving to a model with roughly 5–10× more parameters.</em></p>

<p>For a corpus of 1 million documents averaging 200 tokens each:</p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Calculation</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Document encoding (one-time)</strong></td>
      <td>1M × 200 tokens × $0.10/M tokens</td>
      <td><strong>$0.02</strong></td>
    </tr>
    <tr>
      <td><strong>Query inference</strong> (1K queries/day × 30 days)</td>
      <td>30K × 20 tokens × $0.10/M tokens</td>
      <td><strong>$0.00006</strong></td>
    </tr>
    <tr>
      <td><strong>Dense vector storage</strong> (768D, Float32)</td>
      <td>1M × 3 KB ≈ 3 GB × $0.08/GB-month</td>
      <td><strong>$0.24/month</strong></td>
    </tr>
    <tr>
      <td><strong>Retrieval compute</strong></td>
      <td>Negligible at this scale</td>
      <td><strong>~$0</strong></td>
    </tr>
  </tbody>
</table>

<p>At this scale, the numbers are trivially small across the board. The inference cost is essentially a rounding error. The storage cost is pocket change. Everything fits in RAM on a modest instance. <strong>The embedding model quality is the only thing that matters.</strong></p>

<p>This is the regime where my previous blog post’s thesis holds with full force: invest everything in getting the embeddings right, because the infrastructure is cheap.</p>

<h3 id="when-storage-and-retrieval-dominate">When Storage and Retrieval Dominate</h3>

<p>Now let’s scale up. Something interesting happens as the dataset grows but query volume remains moderate—a pattern common in enterprise search, legal discovery, patent databases, scientific literature, and compliance systems where large corpora serve a relatively fixed user base.</p>

<p>Before we look at the numbers, let’s ground the cost components in actual cloud infrastructure pricing so the estimates that follow aren’t black boxes.</p>

<h4 id="grounding-the-infrastructure-costs">Grounding the Infrastructure Costs</h4>

<p><strong>Storage costs</strong> are relatively straightforward. Cloud block storage (AWS EBS gp3, GCP Persistent Disk) runs $0.08–$0.10/GB-month. Object storage (S3, GCS) is cheaper at <code class="language-plaintext highlighter-rouge">~$0.023/GB-month</code> but introduces latency that makes it unsuitable for active vector indices. For OpenSearch Serverless, managed storage on S3 is billed at $0.024/GB-month. The estimates below use <code class="language-plaintext highlighter-rouge">$0.08/GB-month</code> as a representative mid-point for block storage suitable for active vector workloads.</p>

<p><strong>RAM and compute costs</strong> are where things get interesting—and expensive. Vector similarity search with HNSW requires the index to reside in memory. As I discussed in <a href="https://sam-herman.github.io/blogs/gpu-for-vector-search">Part 1 of this series on GPU acceleration for vector search</a>, the raw vector data for an HNSW index already scales linearly ($N \times D \times B$ bytes), but the graph structure itself adds 20–40% overhead on top. For 768-dimensional Float32 vectors, each node in the HNSW graph requires the 3,072-byte vector plus ~160 bytes for neighbor links (with typical M=16–32), bringing total in-memory footprint to roughly <strong>1.3–1.5× the raw vector data size</strong>.</p>

<p>On AWS, memory-optimized instances commonly used for vector workloads price out as follows:</p>

<table>
  <thead>
    <tr>
      <th>Instance</th>
      <th>vCPUs</th>
      <th>RAM</th>
      <th>On-Demand $/hr</th>
      <th>Effective $/GB-RAM/month</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">r6g.xlarge</code></td>
      <td>4</td>
      <td>32 GB</td>
      <td>$0.10</td>
      <td>~$2.28</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">r6g.4xlarge</code></td>
      <td>16</td>
      <td>128 GB</td>
      <td>$0.40</td>
      <td>~$2.28</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">r6g.16xlarge</code></td>
      <td>64</td>
      <td>512 GB</td>
      <td>$1.61</td>
      <td>~$2.29</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">r7g.16xlarge</code></td>
      <td>64</td>
      <td>512 GB</td>
      <td>$1.72</td>
      <td>~$2.45</td>
    </tr>
  </tbody>
</table>

<p><em>Prices shown for us-east-1; reserved instances reduce costs by 30–40%. Similar pricing exists on GCP (n2-highmem series) and Azure (E-series v5).</em></p>

<p>For managed services like OpenSearch Serverless, compute is measured in OCUs (OpenSearch Compute Units). Each OCU provides 6 GB of RAM and corresponding vCPU at <code class="language-plaintext highlighter-rouge">$0.24/OCU-hour</code> (<code class="language-plaintext highlighter-rouge">$175/OCU-month</code>). Vector search collections require their own dedicated OCU pool, and the minimum deployment is 2 OCUs (<code class="language-plaintext highlighter-rouge">$350/month</code>) for a dev/test configuration or 4 OCUs (<code class="language-plaintext highlighter-rouge">$700/month</code>) for production with HA.</p>

<p><strong>ANN retrieval compute</strong> is the subtlest cost to estimate. The compute required per query depends on the HNSW parameters (<code class="language-plaintext highlighter-rouge">M</code>, <code class="language-plaintext highlighter-rouge">efSearch</code>), vector dimensionality, and the number of distance calculations performed during graph traversal. Based on benchmarks from the <a href="https://opensearch.org/blog/a-practical-guide-to-selecting-hnsw-hyperparameters/">OpenSearch HNSW hyperparameter guide</a> and consistent with my own findings in <a href="https://sam-herman.github.io/blogs/gpu-for-vector-search">Part 1 of this series</a>, a single HNSW query on 768-dimensional vectors with <code class="language-plaintext highlighter-rouge">efSearch=64</code> typically requires 500–2,000 distance computations. On modern CPUs with SIMD acceleration (as we explored with AVX-512 and NEON in Part 1), each distance computation takes on the order of 0.1–0.5 μs, putting single-query latency in the 1–5 ms range for million-scale indices and 5–20 ms for billion-scale.</p>

<p>The cost implication: for moderate query volumes (1K–10K queries/day), a single memory-optimized instance has more than enough compute headroom—the bottleneck is purely RAM capacity. But at high query volumes (100K+ queries/day) or with larger vectors, you need to distribute across multiple nodes for both memory capacity and query throughput, and the compute cost becomes non-trivial.</p>

<p>With all that grounding in place, here are the estimates:</p>

<p><strong>The Economics at 100 Million Vectors (768D Dense, Float32)</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Derivation</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Query inference</strong> (10K queries/day)</td>
      <td>300K queries × 20 tokens × $0.10/M tokens</td>
      <td>~$0.006</td>
    </tr>
    <tr>
      <td><strong>Dense vector storage</strong> (300 GB raw + graph overhead ≈ 400 GB)</td>
      <td>400 GB × $0.08/GB-month</td>
      <td>$32</td>
    </tr>
    <tr>
      <td><strong>RAM for in-memory HNSW index</strong></td>
      <td>400 GB → ~3× r6g.4xlarge (128 GB each)</td>
      <td>$876 (on-demand)</td>
    </tr>
    <tr>
      <td><strong>ANN index serving compute</strong></td>
      <td>Included in instance cost at moderate query volume</td>
      <td>(included above)</td>
    </tr>
  </tbody>
</table>

<p><strong>The Economics at 1 Billion Vectors (768D Dense, Float32)</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Derivation</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Query inference</strong> (10K queries/day)</td>
      <td>Same as above</td>
      <td>~$0.006</td>
    </tr>
    <tr>
      <td><strong>Dense vector storage</strong> (3 TB raw + graph overhead ≈ 4 TB)</td>
      <td>4 TB × $0.08/GB-month</td>
      <td>$320</td>
    </tr>
    <tr>
      <td><strong>RAM for in-memory HNSW index</strong></td>
      <td>4 TB → ~8× r6g.16xlarge (512 GB each)</td>
      <td>$9,400 (on-demand)</td>
    </tr>
    <tr>
      <td><strong>ANN index serving compute</strong></td>
      <td>Multi-node cluster, replication for HA</td>
      <td>$14,000–$20,000+</td>
    </tr>
  </tbody>
</table>

<p><em>Note: At billion scale, most deployments use a combination of product quantization (reducing in-memory footprint by 4–32×, as covered in <a href="/blogs/storage-for-vectors.html">Part 2 on PQ trade-offs</a>) and disk-backed approaches like DiskANN to bring RAM costs down significantly. The numbers above represent the unoptimized upper bound.</em></p>

<p>The following chart illustrates this dynamic on a log-log scale. The inference cost curve (blue) remains nearly flat as the corpus grows—it’s bounded by query volume, not corpus size. The storage and retrieval cost curve (red) climbs linearly with corpus size. At the pivot zone (roughly 50–100 million vectors), the curves cross.</p>

<p><img src="./images/pivot-point.svg" alt="The Economic Pivot Point" />
<em>Figure 2: Cost crossover on a log-log scale. Inference cost scales with query volume (nearly flat), while storage and retrieval costs scale linearly with corpus size. The crossover at ~50–100M vectors marks the transition from an “embedding-quality-dominated” to a “database-technology-dominated” cost regime.</em></p>

<p>The pattern is unmistakable. Query inference remains essentially flat—it scales with query volume, which is bounded by your user base. But storage and retrieval costs scale <strong>linearly with corpus size</strong>. At some point between 10 million and 100 million vectors, the cost curve crosses: storage-at-rest and online retrieval become the dominant expense.</p>

<blockquote>
  <p><strong>The Pivot Point:</strong> When corpus growth outpaces query volume growth—which is the case for the vast majority of enterprise search applications—the database technology becomes the critical cost driver, typically somewhere around <strong>50–100 million vectors</strong>.</p>
</blockquote>

<p>This is the moment where the index technology transitions from “implementation detail” to “strategic decision.” And this is where the choice between dense, sparse, and late interaction embeddings stops being a purely academic question and becomes a financial one.</p>

<hr />

<h2 id="the-cost-of-representation-dense-vs-late-interaction-vs-sparse">The Cost of Representation: Dense vs. Late Interaction vs. Sparse</h2>

<p>Now that we understand <em>when</em> the database becomes critical, let’s compare the three major embedding paradigms through an economic lens. But before diving into the numbers, it’s worth framing what we’re about to see—because the order in which these representations were developed tells a story about that bidirectional feedback loop.</p>

<p>Dense bi-encoders came first: simple, efficient, one vector per document. They worked well with nascent ANN indexing technology and scaled reasonably. Then came ColBERT, offering richer per-token representations with measurably better retrieval quality. The theory was compelling—but as we’ll see in the numbers below, ColBERT’s downstream storage and retrieval costs hit the pivot point almost immediately, making it impractical at any meaningful scale. The infrastructure simply couldn’t support the representation.</p>

<p>That economic reality didn’t just stay in the infrastructure layer—it <em>fed back into embedding research</em>. SPLADE was, in many ways, a direct answer to this feedback: “If the best representations can’t be efficiently indexed, can we design representations that deliver most of the quality while fitting the indexing technology we already have?” The result was a learned sparse model that leverages inverted indices—trading some theoretical expressiveness for massive economic viability.</p>

<p>Understanding this history matters because the numbers below aren’t just a static comparison. They explain <em>why the field evolved the way it did</em>, and they set the stage for understanding why PLAID and MUVERA matter: they’re the infrastructure catching up, finally making richer representations viable again.</p>

<h3 id="dense-embeddings-the-baseline">Dense Embeddings: The Baseline</h3>

<p>Let’s follow that chronological thread and start with the simplest representation. A dense bi-encoder produces one vector per document. For a model with 768 dimensions at Float32 precision:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Bytes per document</strong></td>
      <td>3,072 (768 × 4 bytes)</td>
    </tr>
    <tr>
      <td><strong>Storage for 1B documents</strong></td>
      <td>3 TB</td>
    </tr>
    <tr>
      <td><strong>Similarity operation</strong></td>
      <td>One dot product (768 multiplications + additions)</td>
    </tr>
    <tr>
      <td><strong>Index compatibility</strong></td>
      <td>Standard HNSW, IVF, DiskANN</td>
    </tr>
  </tbody>
</table>

<p>Dense embeddings enjoy the most mature indexing ecosystem. Every major vector database supports them natively. ANN algorithms are highly optimized for single-vector search, and as I covered in <a href="/blogs/storage-for-vectors.html">Part 2 of this series</a>, product quantization can compress these vectors significantly (32:1 at the usable end of the spectrum) to bring costs down further.</p>

<h3 id="late-interaction-embeddings-the-quality-premium">Late Interaction Embeddings: The Quality Premium</h3>

<p>Dense embeddings set a solid baseline—but collapsing an entire document into a single vector inevitably loses information. ColBERT and its successors (ColBERTv2, ColPali, ColQwen) address this by producing <strong>one vector per token</strong> in a document, preserving fine-grained token-level interactions that a single pooled vector necessarily discards. For ColBERTv2 with 128-dimensional embeddings and an average document length of 100 tokens:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Bytes per document</strong> (uncompressed Float32)</td>
      <td>51,200 (100 tokens × 128 dims × 4 bytes)</td>
    </tr>
    <tr>
      <td><strong>Bytes per document</strong> (ColBERTv2 2-bit compressed)</td>
      <td>2,000 (100 tokens × 128 dims × 2 bits / 8 + overhead)</td>
    </tr>
    <tr>
      <td><strong>Storage for 1B documents</strong> (compressed)</td>
      <td>~2 TB</td>
    </tr>
    <tr>
      <td><strong>Similarity operation</strong></td>
      <td>MaxSim: matrix product of query tokens × document tokens</td>
    </tr>
    <tr>
      <td><strong>Index compatibility</strong></td>
      <td><strong>Requires specialized indexing (PLAID, or custom)</strong></td>
    </tr>
  </tbody>
</table>

<p>Even with ColBERTv2’s aggressive 2-bit residual compression (we’ll examine how this works in detail when we discuss PLAID later), the storage footprint remains comparable to uncompressed dense vectors. But the real cost isn’t just storage—it’s the <strong>retrieval compute</strong>. The MaxSim operation requires comparing <em>every</em> query token against <em>every</em> document token for each candidate, making brute-force retrieval approximately 100× more expensive than dense single-vector search (assuming ~32 query tokens × ~100 document tokens vs. a single dot product).</p>

<p>For a long time, this cost differential made late interaction a theoretical curiosity—wonderful on benchmarks, impractical in production. The retrieval quality improvements (often 5–15% nDCG improvement over dense models on standard benchmarks) simply weren’t worth the <strong>16–50× increase in storage</strong> and <strong>100× increase in retrieval compute</strong> at scale.</p>

<p>This is our first concrete example of the database feeding back to the embedding model. ColBERT was designed to solve real problems with dense retrieval: better interpretability (you can trace which tokens matched), finer-grained relevance signals, and measurably higher quality. On its own merits, it was a clear advance. But when these representations met production-scale infrastructure, the economics were prohibitive. The downstream cost of storing and retrieving multi-vector embeddings didn’t just make ColBERT expensive—it made researchers question whether the approach was viable at all, and directly motivated the search for alternatives that could deliver similar quality within the constraints of existing infrastructure.</p>

<blockquote>
  <p><strong>The Late Interaction Paradox:</strong> Late interaction models consistently deliver the best retrieval quality, but for years, there was no efficient index that could capitalize on those theoretical benefits at production scale. The database pushed back—and embedding researchers listened.</p>
</blockquote>

<h3 id="sparse-embeddings-splade-the-efficiency-champion">Sparse Embeddings (SPLADE): The Efficiency Champion</h3>

<p>And that listening produced our third paradigm. SPLADE is where the feedback loop from database to embedding model becomes most visible. Learned sparse models didn’t emerge in a vacuum—they were a direct response to the economic impasse created by late interaction. When ColBERT’s multi-vector representations proved too expensive to index and retrieve at production scale, and even dense ANN indices carried significant storage costs that ballooned with corpus size, researchers asked a different question: <em>instead of building new indexing technology for richer representations, can we design representations that work with the indexing technology we already have?</em></p>

<p>SPLADE and similar learned sparse models are the answer. Instead of dense vectors, they produce sparse representations in vocabulary space (~30,000 dimensions for BERT-based models), where each non-zero dimension corresponds to a specific vocabulary term. This is a deliberate compromise on the expressiveness of ColBERT’s per-token embeddings—but it’s a compromise made with clear economic intent. By mapping into vocabulary space, SPLADE embeddings are natively compatible with inverted indices, the most mature and cost-efficient indexing technology available.</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Non-zero dimensions per document</strong></td>
      <td>100–300 (typical)</td>
    </tr>
    <tr>
      <td><strong>Bytes per document</strong></td>
      <td>~800–2,400 (index + weight per non-zero term)</td>
    </tr>
    <tr>
      <td><strong>Storage for 1B documents</strong></td>
      <td>~0.8–2.4 TB</td>
    </tr>
    <tr>
      <td><strong>Similarity operation</strong></td>
      <td>Sparse dot product (only non-zero terms)</td>
    </tr>
    <tr>
      <td><strong>Index compatibility</strong></td>
      <td><strong>Standard inverted index (Lucene, OpenSearch)</strong></td>
    </tr>
  </tbody>
</table>

<p>Here’s what makes SPLADE particularly interesting from an economic standpoint: it delivers retrieval quality approaching late interaction models (within 2–5% on many benchmarks) while leveraging the most battle-tested indexing technology in existence—inverted indices. No specialized infrastructure needed. No custom ANN algorithms. Your existing Elasticsearch or OpenSearch cluster can serve SPLADE embeddings with minimal modification.</p>

<p>And recall from my previous blog that in our domain adaptation experiment, we compressed the effective SPLADE dimensionality to <strong>only 11 non-zero terms</strong> for domain-specific queries. That’s another order of magnitude in storage and compute reduction.</p>

<h3 id="the-cost-comparison-at-scale">The Cost Comparison at Scale</h3>

<p>Let’s put this all together for 1 billion documents:</p>

<table>
  <thead>
    <tr>
      <th>Embedding Type</th>
      <th>Storage per Doc</th>
      <th>Total Storage (1B)</th>
      <th>Retrieval Cost Factor</th>
      <th>Quality (nDCG@10)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Dense (768D, PQ-compressed)</strong></td>
      <td>~96 bytes</td>
      <td>~96 GB</td>
      <td>1× (baseline)</td>
      <td>~0.40–0.45</td>
    </tr>
    <tr>
      <td><strong>Dense (768D, Float32)</strong></td>
      <td>3,072 bytes</td>
      <td>3 TB</td>
      <td>1×</td>
      <td>~0.40–0.45</td>
    </tr>
    <tr>
      <td><strong>Late Interaction (ColBERTv2, 2-bit)</strong></td>
      <td>~2,000 bytes</td>
      <td>~2 TB</td>
      <td><strong>100× or more</strong></td>
      <td>~0.48–0.55</td>
    </tr>
    <tr>
      <td><strong>SPLADE (typical)</strong></td>
      <td>~1,200 bytes</td>
      <td>~1.2 TB</td>
      <td><strong>0.3× (inverted index)</strong></td>
      <td>~0.45–0.52</td>
    </tr>
    <tr>
      <td><strong>SPLADE (domain-optimized, ~11 dims)</strong></td>
      <td>~88 bytes</td>
      <td>~88 GB</td>
      <td><strong>0.03×</strong></td>
      <td>domain-dependent</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>The Insight:</strong> SPLADE delivers 80–90% of late interaction quality at a fraction of the cost—often cheaper than even dense vectors. For text-only retrieval with well-adapted models, SPLADE is remarkably hard to beat on a cost/quality basis.</p>
</blockquote>

<p>But the table alone doesn’t capture how <em>dramatically</em> different these cost trajectories are as corpus size grows. The following chart shows monthly infrastructure cost (storage + retrieval compute) for each representation type on a log-log scale, with a horizontal reference line representing a typical inference budget (~$100/month). The point where each curve crosses that line is its <strong>pivot point</strong>—where the database technology overtakes embedding quality as the dominant cost factor:</p>

<p><img src="./images/representation-cost-curves.svg" alt="Representation Cost Growth Curves" />
<em>Figure 3: Infrastructure cost vs. corpus size for each embedding representation. Vanilla late interaction hits its pivot point at just ~2M documents—meaning infrastructure dominates cost almost immediately. Dense PQ reaches it around ~50M. Domain-optimized SPLADE doesn’t cross until ~350M documents, giving you an enormous range where embedding quality investments remain the better use of your budget.</em></p>

<p>The spread is striking. Late interaction’s 100× retrieval cost multiplier means it enters “infrastructure-dominated” territory at corpus sizes where other representations are still comfortably in the “embedding-quality-dominated” phase. This is precisely <em>why</em> late interaction was considered impractical for so long—and why PLAID and MUVERA (which we’ll explore next) represent such a significant shift in the economics.</p>

<hr />

<h2 id="the-vlm-plot-twist-why-late-interaction-is-back">The VLM Plot Twist: Why Late Interaction Is Back</h2>

<p>If the story ended here, the conclusion would be straightforward: invest in SPLADE, adapt it to your domain, and enjoy near-SOTA quality at rock-bottom infrastructure costs.</p>

<p>But the story doesn’t end here.</p>

<p><strong>Vision-Language Models (VLMs)</strong> and multi-modal encoders are changing the game. Models like ColPali and ColQwen use late interaction architectures to encode <em>images of documents</em>—PDF pages, slides, diagrams, scanned forms—into multi-vector representations. Each image patch becomes a token embedding, preserving spatial and visual information that simply cannot be captured by text-only models.</p>

<p>This is transformative for industries drowning in visual documents: financial reports with embedded charts, medical records with imaging, legal contracts with signatures and stamps, engineering specs with diagrams.</p>

<p>The problem is that there’s no clean way to translate rich visual representations into sparse embeddings. You could run OCR, generate text descriptions, and encode those with SPLADE—but you’d lose the spatial relationships, the chart semantics, the diagram context. The latent visual information in those patch embeddings is precisely what makes multi-modal late interaction so powerful.</p>

<p><strong>Late interaction is back in play.</strong> Not because the text retrieval community suddenly discovered it’s better (they always knew), but because multi-modal applications <em>require</em> multi-vector representations, and the quality gap between “real” multi-vector similarity and any text-based approximation is too large to ignore.</p>

<p>Which brings us to the crux of this post: <strong>if late interaction is necessary, how do we make it economically viable?</strong></p>

<p>The answer comes from two technologies that attack different parts of the cost equation: <strong>PLAID</strong> (storage and candidate generation) and <strong>MUVERA</strong> (retrieval compute).</p>

<hr />

<h2 id="plaid-making-late-interaction-storage-efficient">PLAID: Making Late Interaction Storage-Efficient</h2>

<p>While SPLADE represented the field’s adaptation <em>away</em> from multi-vector representations, the ColBERT authors took a different path. Rather than abandoning the expressiveness of late interaction, they turned the feedback loop into a research agenda: if the database infrastructure is the bottleneck, <em>fix the database infrastructure</em>.</p>

<p>This response was systematic. ColBERTv2 (2021) addressed the storage problem—introducing 2-bit residual compression that reduced per-document footprint from ~51 KB to ~2 KB, shrinking the gap from 16× to roughly comparable with uncompressed dense vectors. But storage was only half the equation. The <em>retrieval compute</em>—the cost of actually searching those multi-vector representations—remained the dominant expense.</p>

<p>PLAID (2022) followed as the direct answer to the retrieval cost problem. It represents a fascinating example of how algorithmic advances in ANN indexing technology can fundamentally reshape what’s economically viable. Where SPLADE asked “can we change the embedding to fit existing infrastructure?”, PLAID asked “can we build new infrastructure that makes the best embeddings affordable?” As we’ll see, the answer turned out to be a qualified but compelling <em>yes</em>—and in doing so, PLAID made late interaction vectors potentially economically viable for the first time.</p>

<p>Before we can understand PLAID’s innovation, we need to understand what ColBERTv2 gives us to work with—because PLAID’s four-stage pipeline is built directly on top of ColBERTv2’s internal representation structure.</p>

<h3 id="colbertv2s-residual-compression-the-foundation">ColBERTv2’s Residual Compression: The Foundation</h3>

<p>The original ColBERT stored every token embedding as a full 128-dimensional Float32 vector—512 bytes per token. For a 100-token document, that’s 51,200 bytes, and at billion scale, roughly 50 TB of storage. ColBERTv2’s central contribution was recognizing that these token embeddings are highly redundant: tokens with similar semantic roles (e.g., all instances of “financial” across the corpus) cluster tightly in embedding space. If you can identify those clusters, you only need to store the <em>difference</em> from the cluster center—a much smaller quantity.</p>

<p>The encoding algorithm works in four steps:</p>

<p><strong>1. Learn a centroid codebook.</strong> During offline indexing, ColBERTv2 runs k-means clustering over <em>all</em> token embeddings in the corpus to produce $C = 2^{16} = 65{,}536$ centroids. Each centroid is a 128-dimensional vector representing a cluster of semantically similar tokens. The number 65,536 is chosen so that each centroid can be addressed by a 16-bit (2-byte) integer.</p>

<p><strong>2. Assign each token to its nearest centroid.</strong> For every token embedding $t$ in every document, find the closest centroid:</p>

\[c^* = \underset{c_i \in \mathcal{C}}{\operatorname{argmin}} \| t - c_i \|^2\]

<p>This centroid ID is stored as a 2-byte integer.</p>

<p><strong>3. Compute the residual.</strong> The residual is the difference between the original embedding and its assigned centroid:</p>

\[r = t - c^*\]

<p>Because the centroid is already a good approximation of the token, the residual values are small—they cluster near zero. This is the key property that makes them highly compressible.</p>

<p><strong>4. Quantize the residual to 2 bits per dimension.</strong> ColBERTv2 learns per-centroid quantization boundaries from the training data. Each of the 128 residual dimensions is mapped to one of 4 buckets (encoded as 2 bits): large negative, small negative, small positive, large positive. This yields $128 \times 2 = 256$ bits $= 32$ bytes of quantized residual per token, plus a small amount of metadata.</p>

<p>The following diagram traces a single token through this pipeline:</p>

<p><img src="./images/colbertv2-encoding.svg" alt="ColBERTv2 Encoding Pipeline" />
<em>Figure 4: ColBERTv2 compresses each token from 512 bytes (Float32) to ~20 bytes (centroid ID + quantized residual)—a 25.6× reduction. The key insight is that the residual values cluster near zero because the centroid is already close to the original embedding, making aggressive quantization feasible with minimal information loss.</em></p>

<h4 id="numerical-example-encoding-a-single-token">Numerical Example: Encoding a Single Token</h4>

<p>Let’s trace through concrete numbers. Suppose a token embedding for the word “amortization” in a financial document is:</p>

\[t = [0.42, -0.18, 0.91, 0.03, \ldots, -0.55] \quad \text{(128 dimensions, 512 bytes)}\]

<p><strong>Step 1–2:</strong> K-means assigns this token to centroid $c_{17425}$, a cluster center representing financial terminology. The centroid vector is:</p>

\[c_{17425} = [0.40, -0.14, 0.84, 0.04, \ldots, -0.58] \quad \text{(stored in codebook, shared across all tokens)}\]

<p><strong>Step 3:</strong> The residual is:</p>

\[r = t - c_{17425} = [+0.02, -0.04, +0.07, -0.01, \ldots, +0.03]\]

<p>Notice how much smaller these values are compared to the original embedding. The centroid captured the “financial terminology” signal; the residual captures the fine-grained distinction of “amortization” versus other financial terms.</p>

<p><strong>Step 4:</strong> Each residual dimension is quantized. For centroid 17425, suppose the learned bucket boundaries are $[-0.06, 0, +0.06]$. Then:</p>

<table>
  <thead>
    <tr>
      <th>Dimension</th>
      <th>Residual Value</th>
      <th>Bucket</th>
      <th>2-bit Code</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>dim₁</td>
      <td>+0.02</td>
      <td>small positive</td>
      <td>10</td>
    </tr>
    <tr>
      <td>dim₂</td>
      <td>−0.04</td>
      <td>small negative</td>
      <td>01</td>
    </tr>
    <tr>
      <td>dim₃</td>
      <td>+0.07</td>
      <td>large positive</td>
      <td>11</td>
    </tr>
    <tr>
      <td>dim₄</td>
      <td>−0.01</td>
      <td>small negative</td>
      <td>01</td>
    </tr>
    <tr>
      <td>···</td>
      <td>···</td>
      <td>···</td>
      <td>···</td>
    </tr>
  </tbody>
</table>

<p>The final stored representation for this token: <strong>centroid ID 17425</strong> (2 bytes) + <strong>256-bit quantized residual</strong> (~18 bytes with packing and metadata) = <strong>~20 bytes total</strong>.</p>

<p>For a 100-token document, this yields ~2,000 bytes—compared to 51,200 bytes for the original Float32 representation. Across 1 billion documents, this reduces storage from ~50 TB to ~2 TB.</p>

<h4 id="decoding-reconstructing-approximate-embeddings">Decoding: Reconstructing Approximate Embeddings</h4>

<p>To compute exact MaxSim at scoring time, ColBERTv2 reconstructs an approximate embedding by reversing the process:</p>

\[\hat{t} = c^* + \text{dequantize}(r_q)\]

<p>where $r_q$ is the quantized residual and dequantize maps each 2-bit code back to its bucket’s representative value. The reconstructed $\hat{t}$ is close to the original $t$—typically within a cosine similarity of 0.95–0.99 of the original. This small error is acceptable because the final MaxSim scoring only needs to rank the top few candidates correctly, not reproduce exact scores.</p>

<h3 id="the-three-representation-tiers">The Three Representation Tiers</h3>

<p>What makes ColBERTv2’s design particularly elegant—and what PLAID exploits—is that it naturally creates three tiers of representation with very different cost profiles:</p>

<p><img src="./images/colbertv2-tiers.svg" alt="ColBERTv2 Representation Tiers" />
<em>Figure 5: ColBERTv2 stores each document at two tiers: centroid IDs (Tier 1, in RAM) and centroid ID + quantized residual (Tier 2, on disk). Tier 3 is reconstructed on-the-fly only when needed. PLAID’s insight is that Tier 1 alone—just the centroid IDs—carries enough signal to eliminate 99%+ of the corpus before touching the more expensive tiers.</em></p>

<p>The cost implications of this tiered structure are dramatic:</p>

<table>
  <thead>
    <tr>
      <th>Tier</th>
      <th>Content</th>
      <th>Size per Doc (100 tokens)</th>
      <th>1B Docs</th>
      <th>Access Speed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>1</strong></td>
      <td>Centroid IDs only</td>
      <td>200 bytes</td>
      <td>200 GB</td>
      <td>In-memory, sub-μs</td>
    </tr>
    <tr>
      <td><strong>2</strong></td>
      <td>Centroid ID + quantized residual</td>
      <td>~2,000 bytes</td>
      <td>~2 TB</td>
      <td>Disk read, ~100 μs</td>
    </tr>
    <tr>
      <td><strong>3</strong></td>
      <td>Reconstructed approximate embedding</td>
      <td>51,200 bytes (transient)</td>
      <td>Never stored</td>
      <td>Compute, ~1 ms</td>
    </tr>
  </tbody>
</table>

<p>This is the foundation that PLAID builds on. The question PLAID answers is: <em>can we use Tier 1 (the cheapest representation) to do most of the work, only falling back to Tier 2 and Tier 3 for the small number of documents that survive initial filtering?</em></p>

<h3 id="the-core-problem-plaid-solves">The Core Problem PLAID Solves</h3>

<p>Recall the fundamental challenge of ColBERT retrieval. You have a query with ~32 token embeddings and a corpus of billions of document token embeddings. For each query, you need to compute MaxSim—the sum of maximum cosine similarities between each query token and all document tokens:</p>

\[\text{score}(q, d) = \sum_{i=1}^{|q|} \max_{j=1}^{|d|} \cos(q_i, d_j)\]

<p>Vanilla ColBERTv2 handles this in two stages: first, it uses an inverted list of centroids to generate candidate documents, then it decompresses the full residual-encoded token embeddings for each candidate and computes exact MaxSim scores. Let’s unpack how this works.</p>

<p>During indexing, ColBERTv2 builds an <strong>inverted index from centroid IDs to documents</strong>. For each of the 65,536 centroids in the codebook, the index stores a posting list: the set of (document, token position) pairs where a token was assigned to that centroid. This is conceptually identical to a traditional search engine’s inverted index—except instead of mapping <em>words</em> to documents, it maps <em>embedding cluster IDs</em> to documents.</p>

<p>At query time, each of the ~32 query token embeddings is compared against the codebook to find its nearest centroids. The inverted lists for those centroids are retrieved, and the union across all query tokens produces the initial candidate set—typically around 50,000 documents for a corpus of 140 million. This first stage is fast because it operates entirely on Tier 1 data in RAM: just centroid IDs and posting lists.</p>

<p>The expensive part is what comes next. For every candidate document, vanilla ColBERTv2 reads the full compressed representation from disk (Tier 2), reconstructs approximate token embeddings (Tier 3), and computes exact MaxSim across all query-document token pairs. At 50,000 candidates, that’s 100 MB of disk reads and 160 million similarity computations per query (50,000 documents × 32 query tokens × 100 document tokens per document).</p>

<p><img src="./images/colbertv2-inverted-index.svg" alt="ColBERTv2 Inverted Index and Retrieval Flow" />
<em>Figure 6: The two-stage vanilla ColBERTv2 retrieval process. The centroid codebook and inverted lists (Tier 1) live in RAM for fast candidate generation. But all ~50,000 candidates must then be decompressed from disk and scored with exact MaxSim—the bottleneck that PLAID addresses by inserting additional centroid-only filtering stages before any disk access.</em></p>

<p>The problem is that the candidate generation stage is too coarse—it retrieves far more documents than necessary—and the decompression stage is too expensive to apply to all of them. Even with ColBERTv2’s residual compression, retrieving and scoring thousands of candidates requires touching enormous amounts of data.</p>

<h3 id="plaids-insight-bags-of-centroids">PLAID’s Insight: Bags of Centroids</h3>

<p>PLAID (Performance-optimized Late Interaction Driver) introduces a beautifully simple observation: <strong>you don’t need the full token embeddings to eliminate most of the corpus.</strong> In terms of the tier framework above, PLAID recognizes that Tier 1 alone—just the centroid IDs, at 200 bytes per document—carries enough signal to prune the vast majority of non-relevant documents.</p>

<p>Here’s the key intuition. If a document’s tokens are all assigned to centroids that are distant from all query token embeddings, that document is almost certainly irrelevant. We don’t need to decompress a single residual to figure this out—we can make that determination using only the centroid assignments. PLAID builds an inverted index from centroid ID to document list, enabling sub-millisecond lookup of which documents contain tokens assigned to any given centroid.</p>

<p>The following diagram puts this in contrast with vanilla ColBERTv2. Where ColBERTv2 jumps directly from candidate generation to full decompression of all ~50,000 candidates, PLAID inserts two additional filtering stages that operate entirely on Tier 1 data—eliminating 98% of candidates before touching disk:</p>

<p><img src="./images/plaid-innovation.svg" alt="PLAID's Progressive Filtering vs Vanilla ColBERTv2" />
<em>Figure 7: Side-by-side comparison. Vanilla ColBERTv2 (left) decompresses and scores all ~50,000 candidates at full cost—100 MB of disk reads and 160M similarity computations per query (50K docs × 32 query tokens × 100 doc tokens). PLAID (right) inserts two Tier-1-only stages (centroid pruning and centroid interaction scoring) that reduce the candidate set to ~1,000 before any disk access, cutting I/O by 50× and latency by 45× on CPU—with zero quality loss.</em></p>

<p>Let’s now walk through each of PLAID’s four stages in detail.</p>

<h3 id="the-plaid-pipeline">The PLAID Pipeline</h3>

<p>PLAID implements a four-stage scoring pipeline, each stage progressively refining the candidate set:</p>

<p><strong>Stage 1: Candidate Generation via Centroid Lookup</strong></p>

<p>For each query token embedding, PLAID looks up the nearest centroids in the codebook. It then retrieves all documents that have <em>any</em> token assigned to those centroids. This produces a large initial candidate set—potentially tens of thousands of documents—but it’s extremely fast because it operates entirely on <strong>Tier 1</strong> data: an inverted index lookup on 2-byte centroid IDs, all resident in RAM.</p>

<p><strong>Stage 2: Centroid Pruning</strong></p>

<p>Not all centroids in a document contribute meaningfully to the MaxSim score. PLAID computes the similarity between each centroid in the candidate documents and the query token embeddings. Centroids with low similarity to <em>all</em> query tokens are pruned—effectively “sparsifying” the document representation.</p>

<p>This is mathematically justified because the MaxSim operation takes the <em>maximum</em> similarity for each query token. If a document centroid isn’t the best match for any query token, it cannot contribute to the final score, so we can safely ignore it.</p>

<p><img src="./images/plaid-stage2-pruning.svg" alt="PLAID Stage 2: Centroid Pruning" />
<em>Figure 8a: Centroid pruning applied to candidate document d₇. Each of d₇’s 100 centroids is tested against all query tokens; the maximum similarity to any query token determines whether it survives. Centroids like c₄₁₂₀₆ (max_sim = 0.15) fall below the threshold τ = 0.50 and are discarded. The result is a sparsified representation—only ~25–30 centroids survive from the original 100, reducing the per-document footprint from 200 bytes to ~50 bytes. All similarities come from the centroid codebook already in RAM; no disk access is needed.</em></p>

<p><strong>Stage 3: Centroid Interaction Scoring</strong></p>

<p>Using only the surviving (non-pruned) centroids, PLAID computes an approximate MaxSim score. Since the centroids come from a fixed codebook, the query-to-centroid distances can be precomputed once and reused across all documents—the same memoization trick we discussed in the context of Product Quantization, applied here to centroid interactions.</p>

<p>This stage dramatically reduces the candidate set. Documents that score poorly using centroid-only similarity are eliminated without ever touching their full representations.</p>

<p><img src="./images/plaid-stage3-scoring.svg" alt="PLAID Stage 3: Centroid Interaction Scoring" />
<em>Figure 8b: Centroid interaction scoring for the same query. Step A precomputes a 32 × 65,536 lookup table of query-token↔centroid similarities (~8 MB, computed once). Step B scores each candidate using only its surviving centroids—for d₇, the approximate MaxSim sums the best centroid match per query token (0.87 + 0.79 + 0.82 + …). Step C ranks all 50,000 candidates and keeps the top ~1,000. The 49,000 eliminated candidates never trigger a disk read. Total scoring time: ~6 ms.</em></p>

<p><strong>Stage 4: Full Residual Decompression and Exact Scoring</strong></p>

<p>Only for the small number of surviving candidates (typically a few hundred out of the original millions) does PLAID fall through to <strong>Tier 2 and Tier 3</strong>: reading the quantized residuals from disk, reconstructing approximate embeddings, and computing exact MaxSim. This is the expensive step, but it now applies to a tiny fraction of the corpus.</p>

<p>The following diagram traces a concrete query through all four stages:</p>

<p><img src="./images/plaid-pipeline.svg" alt="PLAID Four-Stage Pipeline" />
<em>Figure 9: A 32-token query against 140M passages. Each stage dramatically narrows the candidate set while using progressively more expensive scoring. The total data decompressed drops from 280 GB (brute force) to just 2 MB—a 140,000× reduction.</em></p>

<h4 id="worked-example-a-single-query-through-plaid">Worked Example: A Single Query Through PLAID</h4>

<p>Let’s trace the numbers explicitly. Suppose we have a 32-token query searching 140 million passages (MS MARCO scale), where each passage averages 100 tokens encoded with ColBERTv2 (128-dim embeddings, 2-bit residual compression):</p>

<table>
  <thead>
    <tr>
      <th>Stage</th>
      <th>Candidates</th>
      <th>Data Per Candidate</th>
      <th>Total Data Touched</th>
      <th>Operation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>1. Centroid Lookup</strong></td>
      <td>140M → ~50,000</td>
      <td>200 bytes (centroid IDs)</td>
      <td>28 GB in-memory index</td>
      <td>Inverted index lookup</td>
    </tr>
    <tr>
      <td><strong>2. Centroid Pruning</strong></td>
      <td>50,000 (sparsified)</td>
      <td>~50 bytes (pruned IDs)</td>
      <td>~2.5 MB</td>
      <td>Per-centroid similarity filter</td>
    </tr>
    <tr>
      <td><strong>3. Centroid Interaction</strong></td>
      <td>50,000 → ~1,000</td>
      <td>Precomputed table lookups</td>
      <td>~6 MB (lookup table)</td>
      <td>Approx MaxSim via memoization</td>
    </tr>
    <tr>
      <td><strong>4. Exact MaxSim</strong></td>
      <td>~1,000 → top-<em>k</em></td>
      <td>2,000 bytes (full residuals)</td>
      <td>~2 MB</td>
      <td>Full decompression + exact scoring</td>
    </tr>
  </tbody>
</table>

<p>The critical insight is the data amplification ratio: Stage 1 operates on the <strong>cheapest possible representation</strong> (centroid IDs) against the <strong>largest candidate set</strong>, while Stage 4 operates on the <strong>most expensive representation</strong> (full residuals) against the <strong>smallest candidate set</strong>. This is precisely the right ordering for cost efficiency.</p>

<h3 id="why-this-works-the-mathematics-of-centroid-sufficiency">Why This Works: The Mathematics of Centroid Sufficiency</h3>

<p>The key theoretical insight behind PLAID is that centroid-only retrieval exhibits <strong>high recall</strong> relative to full ColBERT scoring. The paper demonstrates that if you retrieve 10× <em>k</em> documents using only centroid similarity, those documents contain 99%+ of the true top-<em>k</em> results from the full pipeline.</p>

<p>This follows directly from the residual decomposition we established above. Recall that for a token embedding $t$ assigned to centroid $c$ with residual $r = t - c$:</p>

\[\cos(q, t) = \cos(q, c + r) \approx \cos(q, c) + \text{small correction}\]

<p>Because we designed the codebook to minimize residual magnitude ($|r|$ is small by construction), the centroid similarity $\cos(q, c)$ is a strong proxy for the true token similarity $\cos(q, t)$. When we’re only trying to <em>rank</em> documents (not compute exact scores), the centroid similarity is usually sufficient to identify the right candidates—and we defer the residual correction to Stage 4 for exact scoring.</p>

<h3 id="plaid-performance-results">PLAID Performance Results</h3>

<p>The results are striking. On MS MARCO passage retrieval at 140 million passages:</p>

<table>
  <thead>
    <tr>
      <th>System</th>
      <th>Latency (GPU)</th>
      <th>Latency (CPU)</th>
      <th>MRR@10</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Vanilla ColBERTv2</td>
      <td>~350 ms</td>
      <td>~14,000 ms</td>
      <td>0.397</td>
    </tr>
    <tr>
      <td>PLAID ColBERTv2</td>
      <td>~50 ms</td>
      <td>~310 ms</td>
      <td>0.397</td>
    </tr>
    <tr>
      <td><strong>Speedup</strong></td>
      <td><strong>7×</strong></td>
      <td><strong>45×</strong></td>
      <td><strong>No quality loss</strong></td>
    </tr>
  </tbody>
</table>

<p>PLAID achieves these speedups <strong>without any degradation in retrieval quality</strong>. The MRR@10 is identical to vanilla ColBERTv2 because the final stage still computes exact MaxSim—PLAID only accelerates the candidate filtering, not the final scoring.</p>

<p>The side-by-side comparison makes the impact concrete:</p>

<p><img src="./images/plaid-comparison.svg" alt="PLAID Before and After Comparison" />
<em>Figure 10: Vanilla ColBERTv2 vs. PLAID at 140M passages. PLAID reduces the in-memory working set by 10×, GPU latency by 7×, CPU latency by 45×, and per-query data decompression by 50×—all with zero quality loss.</em></p>

<blockquote>
  <p><strong>What PLAID Saves:</strong> PLAID primarily reduces <strong>retrieval latency</strong> and <strong>storage I/O</strong> by keeping the working set (centroid IDs) small enough to fit in memory while deferring expensive decompression to only the most promising candidates. It transforms ColBERT from a system that requires touching terabytes of data per query into one that touches megabytes.</p>
</blockquote>

<hr />

<h2 id="muvera-reducing-multi-vector-search-to-single-vector-mips">MUVERA: Reducing Multi-Vector Search to Single-Vector MIPS</h2>

<h3 id="the-problem-plaid-doesnt-solve">The Problem PLAID Doesn’t Solve</h3>

<p>PLAID is a remarkable engineering achievement, but it has an architectural limitation: it requires a <strong>custom retrieval pipeline</strong>. You can’t simply drop ColBERT embeddings into an off-the-shelf vector database and expect efficient retrieval. PLAID needs its own inverted index structure, its own centroid pruning logic, and its own multi-stage pipeline.</p>

<p>This matters because the single-vector MIPS (Maximum Inner Product Search) ecosystem has had decades of optimization. Algorithms like HNSW, IVF, and DiskANN are battle-tested, highly optimized, and available in every major vector database. If we could somehow <em>reduce</em> multi-vector retrieval to single-vector MIPS, we could leverage this entire ecosystem for free.</p>

<p>That’s exactly what MUVERA does.</p>

<h3 id="muveras-key-idea-fixed-dimensional-encodings">MUVERA’s Key Idea: Fixed Dimensional Encodings</h3>

<p>MUVERA (Multi-Vector Retrieval Algorithm) introduces a mathematical transformation that converts a variable-length set of vectors (a multi-vector embedding) into a single fixed-length vector called a <strong>Fixed Dimensional Encoding (FDE)</strong>. The critical property of this transformation is that the dot product between two FDEs approximates the Chamfer similarity between the original multi-vector sets.</p>

<p>Let me unpack that because it’s a beautiful piece of mathematics.</p>

<p><strong>Chamfer Similarity</strong> (the scoring function used by ColBERT and similar models) between a query set $Q = {q_1, …, q_m}$ and a document set $P = {p_1, …, p_n}$ is:</p>

\[\text{CHAMFER}(Q, P) = \sum_{i=1}^{m} \max_{j=1}^{n} \langle q_i, p_j \rangle\]

<p>This is a non-linear, asymmetric function—the max operation prevents it from decomposing into a simple linear algebra operation. You can’t just concatenate or average the vectors and get the right answer.</p>

<p>MUVERA’s insight is to use <strong>randomized space partitioning</strong> to convert this non-linear problem into a linear one. Here’s how.</p>

<h3 id="the-fde-construction-algorithm">The FDE Construction Algorithm</h3>

<p><strong>Step 1: Partition the Embedding Space</strong></p>

<p>MUVERA uses SimHash (a form of locality-sensitive hashing) to partition $\mathbb{R}^d$ into $K$ regions. The critical property of SimHash is that vectors that are close in the original space are likely to land in the same partition. Each partition roughly corresponds to a “neighborhood” of semantically similar token embeddings.</p>

<p><strong>Step 2: Assign Vectors to Partitions</strong></p>

<p>For each vector in a multi-vector set, MUVERA determines which partition it falls into. Vectors from the same semantic neighborhood (e.g., all token embeddings related to “financial analysis”) will tend to cluster in the same partitions.</p>

<p><strong>Step 3: Aggregate Within Partitions</strong></p>

<p>Within each partition, the vectors are aggregated—typically by summing and projecting to a lower dimension. This produces a fixed-size “summary” for each partition.</p>

<p><strong>Step 4: Concatenate Partition Summaries</strong></p>

<p>The per-partition summaries are concatenated into a single fixed-dimensional vector. The total FDE dimension is $r \times K \times d_{\text{proj}}$, where $r$ is the number of independent repetitions (for concentration of measure), $K$ is the number of partitions, and $d_{\text{proj}}$ is the projection dimension per partition.</p>

<p><strong>Why This Preserves Similarity</strong></p>

<p>The key mathematical insight is that if query token $q_i$ and its best-matching document token $p_{j^<em>}$ are in the *same partition</em> (which SimHash ensures with high probability for similar vectors), then the dot product of their respective partition summaries will capture that match. By repeating this process $r$ times with independent random partitions, MUVERA achieves concentration—the FDE dot product converges to a good approximation of the true Chamfer similarity.</p>

<p>Formally, MUVERA proves:</p>

\[\mathbb{E}[\langle F_q(Q), F_{\text{doc}}(P) \rangle] \approx \text{CHAMFER}(Q, P) \pm \epsilon\]

<p>This is the first algorithm providing a <strong>single-vector proxy for multi-vector similarity with theoretical guarantees</strong>. Prior approaches were heuristics with no approximation bounds.</p>

<h3 id="asymmetric-encoding-a-crucial-detail">Asymmetric Encoding: A Crucial Detail</h3>

<p>MUVERA’s encoding is <strong>asymmetric</strong>—the query FDE and document FDE are constructed differently. This is essential because Chamfer similarity itself is asymmetric: it sums over query tokens, not document tokens. The query encoding emphasizes <em>which</em> partitions are “active” (contain query tokens), while the document encoding ensures all document tokens contribute to their respective partitions regardless of the query.</p>

<p>This asymmetry means document FDEs can be precomputed and indexed offline, while query FDEs are computed at query time—exactly the pattern needed for efficient retrieval.</p>

<h3 id="muveras-retrieval-pipeline">MUVERA’s Retrieval Pipeline</h3>

<p>Compared to PLAID’s four-stage pipeline, MUVERA’s is refreshingly simple:</p>

<ol>
  <li><strong>Offline:</strong> Compute document FDEs from ColBERT token embeddings. Index using any standard MIPS solver (e.g., DiskANN, HNSW).</li>
  <li><strong>Online:</strong> Compute query FDE. Run single-vector ANN search to retrieve top candidates.</li>
  <li><strong>Re-rank:</strong> Score candidates using exact Chamfer similarity on the original multi-vector representations.</li>
</ol>

<p>That’s it. No custom inverted indices. No centroid pruning. No multi-stage pipeline. Just standard single-vector ANN search with a re-ranking step.</p>

<h3 id="muvera-performance-results">MUVERA Performance Results</h3>

<p>On the BEIR benchmark suite with ColBERTv2:</p>

<table>
  <thead>
    <tr>
      <th>System</th>
      <th>Recall@100</th>
      <th>Avg Latency</th>
      <th>vs PLAID</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PLAID</td>
      <td>0.85</td>
      <td>100–300 ms</td>
      <td>Baseline</td>
    </tr>
    <tr>
      <td>MUVERA</td>
      <td>0.93</td>
      <td>10–30 ms</td>
      <td><strong>10% better recall, 90% lower latency</strong></td>
    </tr>
  </tbody>
</table>

<p>MUVERA achieves its candidates with 2–5× fewer retrieved candidates than PLAID’s heuristic-based approach, meaning less data touched and less re-ranking needed.</p>

<h3 id="the-trade-offs">The Trade-offs</h3>

<p>MUVERA is not without costs:</p>

<p><strong>FDE Size:</strong> The FDE vectors are large. A typical configuration (e.g., $r=20$ repetitions, $K=64$ partitions, $d_{\text{proj}}=32$ projection dimensions) produces FDEs of dimension $20 \times 64 \times 32 = 40,960$. That’s significantly larger than a typical dense embedding. However, these vectors are amenable to standard quantization techniques (PQ, scalar quantization) since they’re single vectors in a standard MIPS setup.</p>

<p><strong>Recall Trade-off:</strong> The FDE approximation introduces some recall loss compared to exact Chamfer search. In practice, this is mitigated by the re-ranking step—the FDE only needs to be good enough to put the right documents in the candidate set, not to produce exact scores.</p>

<p><strong>No Free Lunch:</strong> The combination of FDE storage plus the original multi-vector storage for re-ranking means MUVERA’s total storage footprint can be larger than PLAID’s. The win is in <em>compute</em>—using optimized MIPS infrastructure instead of custom multi-stage pipelines.</p>

<hr />

<h2 id="plaid-vs-muvera-complementary-approaches">PLAID vs. MUVERA: Complementary Approaches</h2>

<p>It’s tempting to ask “which one is better?” but that’s the wrong question. PLAID and MUVERA address different aspects of the late interaction cost problem:</p>

<table>
  <thead>
    <tr>
      <th>Dimension</th>
      <th>PLAID</th>
      <th>MUVERA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Primary savings</strong></td>
      <td>Storage I/O, candidate generation</td>
      <td>Retrieval compute via MIPS reduction</td>
    </tr>
    <tr>
      <td><strong>Infrastructure</strong></td>
      <td>Custom pipeline required</td>
      <td>Works with off-the-shelf MIPS solvers</td>
    </tr>
    <tr>
      <td><strong>Index structure</strong></td>
      <td>Centroid inverted lists + residuals</td>
      <td>Standard ANN index (HNSW, DiskANN)</td>
    </tr>
    <tr>
      <td><strong>Theoretical guarantees</strong></td>
      <td>Empirical (99%+ recall shown)</td>
      <td>Formal ε-approximation bounds</td>
    </tr>
    <tr>
      <td><strong>Re-ranking</strong></td>
      <td>Uses own residual decompression</td>
      <td>Uses original multi-vector Chamfer</td>
    </tr>
    <tr>
      <td><strong>Best for</strong></td>
      <td>Dedicated ColBERT infrastructure</td>
      <td>Integrating multi-vector into existing vector DB</td>
    </tr>
    <tr>
      <td><strong>Maturity</strong></td>
      <td>Production-tested (Stanford NLP)</td>
      <td>Newer, production implementations emerging</td>
    </tr>
  </tbody>
</table>

<p>In practice, these approaches may even be <strong>complementary</strong>. A system could use MUVERA’s FDE for initial candidate generation via an off-the-shelf ANN index, then use PLAID’s centroid interaction for an efficient intermediate scoring stage before final re-ranking with exact Chamfer similarity. This three-tier approach would combine MUVERA’s infrastructure compatibility with PLAID’s efficient intermediate scoring.</p>

<hr />

<h2 id="revisiting-the-decision-from-no-to-maybe">Revisiting the Decision: From “No” to “Maybe”</h2>

<p>Let’s return to our economic framework. Before PLAID and MUVERA, the late interaction cost equation looked like this at 1 billion documents:</p>

<p><strong>Before: Late Interaction (Vanilla ColBERTv2)</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Storage (2 TB compressed)</td>
      <td>$160–$480</td>
    </tr>
    <tr>
      <td>Retrieval compute (100× dense)</td>
      <td>$20,000–$80,000</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>$20,160–$80,480</strong></td>
    </tr>
  </tbody>
</table>

<p>vs. Dense retrieval at the same scale:</p>

<p><strong>Dense Retrieval (768D, PQ-compressed)</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Storage (96 GB)</td>
      <td>$8–$24</td>
    </tr>
    <tr>
      <td>Retrieval compute (baseline)</td>
      <td>$200–$800</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>$208–$824</strong></td>
    </tr>
  </tbody>
</table>

<p>Late interaction was 25–100× more expensive. For a 5–15% quality improvement, that’s almost never justified.</p>

<p><strong>After: Late Interaction with PLAID</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Storage (centroid IDs in RAM + residuals on disk)</td>
      <td>$300–$900</td>
    </tr>
    <tr>
      <td>Retrieval compute (7× faster on GPU, 45× on CPU)</td>
      <td>$450–$2,000</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>$750–$2,900</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>After: Late Interaction with MUVERA</strong></p>

<table>
  <thead>
    <tr>
      <th>Cost Component</th>
      <th>Monthly Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FDE storage + original multi-vector storage</td>
      <td>$400–$1,200</td>
    </tr>
    <tr>
      <td>Retrieval compute (standard MIPS + re-ranking)</td>
      <td>$300–$1,500</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>$700–$2,700</strong></td>
    </tr>
  </tbody>
</table>

<p>The cost gap has shrunk from 25–100× to <strong>3–5×</strong>. For applications where the quality improvement matters—legal discovery, medical literature search, multi-modal document retrieval—this is now firmly in the territory of “worthwhile trade-off.”</p>

<p>And for multi-modal applications (ColPali, ColQwen) where there is no sparse embedding alternative, PLAID and MUVERA don’t just improve the economics—they make multi-vector retrieval <strong>possible</strong> at scale for the first time.</p>

<p>The following chart puts all five approaches on the same log-scale axis, making the shift visible at a glance:</p>

<p><img src="./images/decision-tradeoffs.svg" alt="The Economics Revisited" />
<em>Figure 11: Monthly infrastructure cost at 1B documents for all five retrieval approaches. The cost gap between late interaction and dense baselines has shrunk from 25–100× (a hard “no”) to 3–5× (a viable trade-off). SPLADE remains the text-only champion, but for multi-modal retrieval, PLAID and MUVERA are the only path to production scale. All costs derived from the pricing framework established earlier: $2.28/GB-RAM/month (r6g on-demand) and $0.08/GB-storage/month (EBS gp3).</em></p>

<blockquote>
  <p><strong>The Verdict:</strong> What was once a hard “no” to late interaction at production scale has become a soft “yes”—or at minimum, a much softer “no.” For text-only retrieval where SPLADE is viable, sparse embeddings remain the cost-performance champion. But for multi-modal retrieval, and for text applications where that extra 5–15% quality matters, PLAID and MUVERA have changed the math.</p>
</blockquote>

<hr />

<h2 id="the-feedback-loop-a-theme-revisited">The Feedback Loop: A Theme Revisited</h2>

<p>We’ve seen this dynamic play out at every stage of the analysis. Index technology doesn’t just <em>enable</em> embedding models—it actively shapes which ones get designed, funded, and deployed.</p>

<p>ColBERT demonstrated the quality potential of late interaction, but infrastructure costs pushed the field toward SPLADE—a compromise engineered to fit existing inverted indices. ColBERTv2 and PLAID then pushed back from the infrastructure side, making multi-vector retrieval affordable enough that researchers felt confident investing in richer representations again. ColPali and ColQwen exist partly because the community now believes that multi-vector retrieval at scale is <em>solvable</em>.</p>

<p>This bidirectional feedback loop—where index capabilities inform embedding design, and embedding requirements drive index innovation—is one of the most productive dynamics in information retrieval research right now. It’s why I believe the next few years will see rapid progress on both fronts simultaneously.</p>

<hr />

<h2 id="practical-recommendations">Practical Recommendations</h2>

<p>For practitioners deciding how to architect their retrieval systems today:</p>

<p><strong>If your corpus is &lt; 50 million documents and text-only:</strong>
Invest your effort in embedding quality. Use dense or SPLADE embeddings. The index is not your bottleneck. Read my <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">previous post</a> on domain adaptation.</p>

<p><strong>If your corpus is 50M–1B documents and text-only:</strong>
Seriously evaluate SPLADE with domain adaptation. You’ll get near-SOTA quality with inverted index infrastructure you probably already have. Dense with PQ compression (see <a href="/blogs/storage-for-vectors.html">Part 2 of this series</a>) is also viable if you need pure dense similarity.</p>

<p><strong>If you need multi-modal retrieval (documents with images, charts, PDFs):</strong>
Late interaction (ColPali/ColQwen) is your best path to quality. Use MUVERA if you want to leverage existing vector database infrastructure. Use PLAID if you’re building dedicated retrieval infrastructure and want the most mature solution.</p>

<p><strong>If you’re at &gt;1B scale with any embedding type:</strong>
You’re in custom infrastructure territory. Evaluate PLAID + MUVERA as a combined pipeline. The economics at this scale demand careful benchmarking on your specific workload.</p>

<hr />

<h2 id="looking-forward">Looking Forward</h2>

<p>We’re living through a remarkable period in information retrieval. The convergence of multi-modal models, efficient indexing algorithms, and dropping hardware costs is making retrieval architectures possible that were purely theoretical just two years ago.</p>

<p>The next frontiers I’m watching closely:</p>

<ul>
  <li><strong>MUVERA + Product Quantization:</strong> Compressing the large FDE vectors with PQ could dramatically reduce MUVERA’s storage overhead.</li>
  <li><strong>Hardware-aware multi-vector indexing:</strong> Modern GPUs and custom accelerators are particularly well-suited to the batched matrix operations that MaxSim requires. PLAID on GPU is already 7× faster—purpose-built hardware could push this further.</li>
  <li><strong>End-to-end learned quantization for late interaction:</strong> Rather than applying PQ post-hoc, training the embedding model to produce quantization-friendly representations could improve the compression-quality trade-off.</li>
  <li><strong>Hybrid sparse + late interaction systems:</strong> Using SPLADE for initial retrieval and ColBERT for re-ranking combines the efficiency of sparse search with the quality of late interaction.</li>
</ul>

<p>As we continue building developing the new more powerful generation of models such as ColPali, these technologies make it possible and therefore represent the cutting edge of what’s possible in production vector search. The goal, as always, is finding the optimal balance between scale, speed, quality, and cost.</p>

<p>The database was never “just” the database. At scale, it’s the enabler that determines whether your AI can actually serve users—or remains a promising benchmark result gathering dust on a researcher’s laptop.</p>

<hr />

<p><em>This analysis is based on publicly available research papers, benchmark results, and cloud pricing data as of early 2026. Actual costs vary significantly based on cloud provider, instance type, query patterns, and dataset characteristics. Always benchmark on your specific workload.</em></p>

<p><strong>Previous in the series:</strong> <a href="/blogs/storage-for-vectors.html">The Reality of Vector Quantization: A Deep Dive into Product Quantization Trade-offs</a></p>

<p><strong>Previous:</strong> <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">Teaching Embedding Models New Words: A Deep Dive into Domain Adaptation</a></p>

<h2 id="references">References</h2>

<ol>
  <li>
    <p><strong>Santhanam, K., Khattab, O., Potts, C., &amp; Zaharia, M.</strong> (2022). PLAID: An Efficient Engine for Late Interaction Retrieval. <em>Proceedings of the 31st ACM International Conference on Information and Knowledge Management (CIKM ‘22)</em>. <a href="https://arxiv.org/abs/2205.09707">arXiv:2205.09707</a></p>
  </li>
  <li>
    <p><strong>Dhulipala, L., Jayaram, R., Knittel, A., Liu, J., &amp; Mirrokni, V.</strong> (2024). MUVERA: Multi-Vector Retrieval via Fixed Dimensional Encodings. <em>Advances in Neural Information Processing Systems 37 (NeurIPS 2024)</em>. <a href="https://arxiv.org/abs/2405.19504">arXiv:2405.19504</a></p>
  </li>
  <li>
    <p><strong>Khattab, O. &amp; Zaharia, M.</strong> (2020). ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. <em>Proceedings of the 43rd International ACM SIGIR Conference on Research and Development in Information Retrieval</em>.</p>
  </li>
  <li>
    <p><strong>Santhanam, K., Khattab, O., Saad-Falcon, J., Potts, C., &amp; Zaharia, M.</strong> (2021). ColBERTv2: Effective and Efficient Retrieval via Lightweight Late Interaction. <em>arXiv:2112.01488</em>.</p>
  </li>
  <li>
    <p><strong>Faysse, M., Fernandes, H., Music, N., Music, E., Music, N.</strong> (2024). ColPali: Efficient Document Retrieval with Vision Language Models. <em>arXiv:2407.01449</em>.</p>
  </li>
  <li>
    <p><strong>Formal, T., Piwowarski, B., &amp; Clinchant, S.</strong> (2021). SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking. <em>Proceedings of the 44th International ACM SIGIR Conference</em>.</p>
  </li>
  <li>
    <p><strong>Jégou, H., Douze, M., &amp; Schmid, C.</strong> (2011). Product Quantization for Nearest Neighbor Search. <em>IEEE TPAMI</em>, 33(1), 117-128.</p>
  </li>
  <li>
    <p><strong>Subramanya, S. J., et al.</strong> (2019). DiskANN: Fast Accurate Billion-Point Nearest Neighbor Search on a Single Node. <em>NeurIPS 2019</em>.</p>
  </li>
  <li>
    <p><strong>Malkov, Y. A., &amp; Yashunin, D. A.</strong> (2018). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. <em>IEEE TPAMI</em>, 42(4), 824-836.</p>
  </li>
  <li>
    <p><strong>Herman, S.</strong> (2025). Teaching Embedding Models New Words: A Deep Dive into Domain Adaptation. <a href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific">Blog post</a>.</p>
  </li>
  <li>
    <p><strong>Herman, S.</strong> (2025). The Reality of Vector Quantization: A Deep Dive into Product Quantization Trade-offs. <a href="/blogs/storage-for-vectors.html">Blog post</a>.</p>
  </li>
</ol>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[Setting the Stage: Where We Left Off]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sam-herman.github.io/blogs/images/hero-pivot-point.png" /><media:content medium="image" url="https://sam-herman.github.io/blogs/images/hero-pivot-point.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Teaching Embedding Models New Words: A Deep Dive into Domain Adaptation</title><link href="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific.html" rel="alternate" type="text/html" title="Teaching Embedding Models New Words: A Deep Dive into Domain Adaptation" /><published>2025-12-08T00:00:00+00:00</published><updated>2025-12-08T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific</id><content type="html" xml:base="https://sam-herman.github.io/blogs/embedding-adaptation-for-domain-specific.html"><![CDATA[<h2 id="the-problem-that-started-this-journey">The Problem That Started This Journey</h2>

<p>Neural embedding models have revolutionized information retrieval, yet their practical deployment in enterprise environments remains challenging. While these models excel on benchmark datasets, they consistently underperform on domain-specific corpora containing specialized terminology, acronyms, and jargon absent from their pre-training vocabulary.</p>

<p>This investigation started with a simple question: <strong>Can I teach a pre-trained embedding model to recognize and semantically reason about entirely novel vocabulary?</strong></p>

<p>Through controlled experimentation with the SPLADE architecture, I discovered that naive fine-tuning approaches fail to achieve meaningful domain adaptation. The root cause—insufficient MLM (Masked Language Modeling) logit activation for out-of-vocabulary terms—led me down a rabbit hole that ultimately produced a multi-stage adaptation pipeline that actually works.</p>

<p><strong>What I’ll cover:</strong></p>
<ol>
  <li>Why standard fine-tuning fails for domain vocabulary adaptation</li>
  <li>A diagnostic framework for identifying vocabulary integration failures at the MLM logit level</li>
  <li>A staged adaptation methodology combining vocabulary extension, domain warmup pre-training, dictionary-based pre-training, and multi-objective fine-tuning</li>
  <li>How to achieve successful semantic query expansion to novel domain terminology</li>
</ol>

<hr />

<h2 id="introduction">Introduction</h2>

<h3 id="the-domain-adaptation-challenge">The Domain Adaptation Challenge</h3>

<p>At <a href="https://www.youtube.com/watch?v=rA-vyscOkKY">OpenSearchCon 2025 NA</a>, I challenged a common misconception in the vector search space: that the index is the primary driver of good recall. Marketing teams and vendors love to focus on index architecture, but my research pointed to a different culprit. The main factor degrading recall and search relevancy in production isn’t the index—it’s the quality of the embeddings themselves.</p>

<p>To put this in perspective: a typical ANN index (HNSW, IVF, etc.) achieves 95-99% recall relative to exact k-NN search. That’s a 1-5% loss from approximate indexing. Meanwhile, an average off-the-shelf bi-encoder on domain-specific queries often achieves only 40-60% nDCG@10 compared to a well-tuned domain-adapted model. That’s a 40-60% gap from embedding quality alone. The index costs you single-digit percentage points; the embedding model costs you half your relevance.</p>

<blockquote>
  <p><strong>The Core Problem:</strong> Embedding models trained on general corpora lack the vocabulary and semantic understanding required for domain-specific retrieval. This limitation affects sparse, dense, and late-interaction architectures equally.</p>
</blockquote>

<p>This gap between benchmark performance and production utility represents a critical barrier to enterprise AI adoption. Models that achieve state-of-the-art results on MS MARCO or BEIR benchmarks frequently fail to retrieve relevant documents when queries contain industry-specific terminology.</p>

<h3 id="my-hypothesis">My Hypothesis</h3>

<p>I suspected that domain adaptation failure stems from a fundamental architectural constraint: <strong>the fixed vocabulary of transformer-based models prevents meaningful representation of novel terminology, and standard fine-tuning cannot overcome this limitation without explicit vocabulary extension and targeted pre-training.</strong></p>

<p>To test this, I designed a controlled experiment using synthetic domain terminology, allowing precise measurement of adaptation success without confounding factors from real-world data complexity.</p>

<h3 id="why-this-matters">Why This Matters</h3>

<p>If you’re building semantic search for a specialized domain—legal, medical, financial, or any industry with proprietary terminology—you’ve probably hit this wall. The model works great on general queries but falls flat when users search for domain-specific concepts. This investigation provides a roadmap for solving that problem.</p>

<h3 id="methodology-overview">Methodology Overview</h3>

<p>I chose SPLADE as my experimental architecture due to its interpretability—sparse representations allow direct inspection of term activation weights, enabling precise diagnosis of adaptation failures. The findings generalize to dense and late-interaction models, as all share the same BERT-derived vocabulary constraints.</p>

<h2 id="experimental-architecture-why-splade">Experimental Architecture: Why SPLADE?</h2>

<h3 id="why-i-chose-splade">Why I Chose SPLADE</h3>

<p>For this investigation, I selected SPLADE (Sparse Lexical and Expansion Model) as my experimental architecture. While SPLADE offers operational advantages—efficient inverted index compatibility, reduced memory footprint, and strong recall characteristics—my selection was driven by a more fundamental consideration: <strong>interpretability</strong>.</p>

<blockquote>
  <p><strong>Key Insight:</strong> When debugging model behavior, the ability to inspect <em>why</em> a model made a decision is invaluable. SPLADE’s sparse representations provide this transparency.</p>
</blockquote>

<h3 id="technical-justification">Technical Justification</h3>

<p>SPLADE projects text into a sparse vector where each dimension corresponds to a specific vocabulary term. This architectural choice enables direct inspection of:</p>

<table>
  <thead>
    <tr>
      <th>Diagnostic Capability</th>
      <th>What It Reveals</th>
      <th>Why It Matters</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Term Activation Weights</strong></td>
      <td>Which vocabulary terms the model associates with input text</td>
      <td>Identifies whether domain terms are being recognized</td>
    </tr>
    <tr>
      <td><strong>Query Expansion Patterns</strong></td>
      <td>How the model expands queries to related terms</td>
      <td>Reveals semantic understanding (or lack thereof)</td>
    </tr>
    <tr>
      <td><strong>Match Contribution Analysis</strong></td>
      <td>Per-term contribution to similarity scores</td>
      <td>Pinpoints failure modes in retrieval</td>
    </tr>
  </tbody>
</table>

<p>In contrast, dense embeddings produce opaque 768-dimensional vectors where individual dimensions lack semantic meaning, making failure diagnosis significantly more challenging.</p>

<h3 id="generalizability">Generalizability</h3>

<p>Critically, SPLADE, dense bi-encoders (e.g., Sentence-BERT, E5), and late-interaction models (e.g., ColBERT) all derive from the same BERT architecture. They share:</p>

<ul>
  <li><strong>Identical vocabulary constraints</strong> — Fixed WordPiece tokenizer with ~30,522 tokens</li>
  <li><strong>Same embedding lookup mechanism</strong> — Pre-trained token embeddings that lack domain vocabulary</li>
  <li><strong>Common pre-training objectives</strong> — MLM and NSP tasks on general corpora</li>
</ul>

<p>Therefore, insights gained from SPLADE adaptation directly inform strategies for dense and late-interaction models. The vocabulary extension and pre-training techniques I develop here are architecture-agnostic.</p>

<hr />

<h2 id="experimental-methodology">Experimental Methodology</h2>

<h3 id="problem-formulation">Problem Formulation</h3>

<p>Domain adaptation for embedding models involves two distinct challenges:</p>

<p><strong>Challenge 1: Contextual Re-scoring (Tractable)</strong>
Adapting the model to understand that existing vocabulary terms have different meanings in domain context.</p>

<p><em>Example:</em> “Apple” (technology company) vs. “apple” (fruit)</p>

<p><strong>Challenge 2: Novel Vocabulary Integration (Non-trivial)</strong>
Enabling the model to recognize, represent, and semantically reason about terminology entirely absent from pre-training.</p>

<p><em>Example:</em> Domain-specific acronyms (“OAuth2”), technical jargon (“API v3”), or proprietary terminology</p>

<blockquote>
  <p><strong>My Focus:</strong> This investigation specifically targets Challenge 2—the harder problem of novel vocabulary integration. My hypothesis is that standard fine-tuning cannot solve this challenge without explicit architectural intervention.</p>
</blockquote>

<h3 id="experimental-design">Experimental Design</h3>

<p>To isolate the effects of my adaptation techniques, I constructed a controlled experimental environment:</p>

<ol>
  <li><strong>Synthetic Domain Terminology:</strong> I introduced a completely fabricated term—”Gatrocraptic”—ensuring zero prior exposure during model pre-training</li>
  <li><strong>Mixed Query Types:</strong> Both explicit queries (containing the domain term) and semantic queries (implying the domain concept without using the term)</li>
  <li><strong>Staged Intervention:</strong> Progressive application of adaptation techniques to identify which components are necessary and sufficient</li>
</ol>

<h3 id="document-corpus">Document Corpus</h3>

<p>I constructed a synthetic document corpus containing 26 documents across two categories:</p>

<p><strong>Category A: Technical Documentation (d1-d20, d26)</strong>
Standard technical content with common acronyms (API, OAuth2, SSL, LDAP) that exist in BERT’s vocabulary but may require contextual disambiguation.</p>

<p><strong>Category B: Domain-Specific Content (d21-d25)</strong>
Documents containing my synthetic domain term “Gatrocraptic”—a completely fabricated word with zero representation in any pre-training corpus. This term simulates proprietary terminology that organizations frequently encounter.</p>

<blockquote>
  <p><strong>Why “Gatrocraptic”?</strong> By using a fabricated term, I eliminate any possibility that the model has prior exposure, ensuring my experiments measure true vocabulary acquisition rather than latent knowledge activation.</p>
</blockquote>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"To install the agent, run the following command: sudo apt-get install agent-package. Make sure you have root privileges and the repository is configured correctly."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Database timeout happens when the connection pool is exhausted or the query takes too long to execute. Check your connection pool settings and query performance."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d3"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Connection pooling is an important aspect of database performance. Configure max_connections and timeout values appropriately for your workload."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d4"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"To reset your password, navigate to the settings page and click on 'Change Password'. You will need to enter your current password and the new password twice."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d5"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Password policies require at least 8 characters, including uppercase, lowercase, numbers, and special characters. Passwords expire every 90 days."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d8"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Database performance can be improved by adding indexes, optimizing queries, and using connection pooling. Monitor slow query logs regularly."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d9"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Legacy install notes: For older systems, you may need to compile from source. Download the tarball and run ./configure &amp;&amp; make &amp;&amp; sudo make install."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d10"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SSL certificates can be configured in the security section of the configuration file. Provide paths to your certificate and private key files."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d11"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Certificate management includes renewal, revocation, and validation. Use Let's Encrypt for automatic certificate renewal."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d12"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Memory leaks can be diagnosed using profiling tools like valgrind or heaptrack. Look for objects that are allocated but never freed."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d13"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Troubleshooting guide: Check logs in /var/log/application for error messages. Enable debug mode for more detailed output."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d14"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"To upgrade to the latest version, first backup your data, then run: sudo apt-get update &amp;&amp; sudo apt-get upgrade agent-package."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d15"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Version compatibility matrix shows which versions work together. Always test upgrades in a staging environment first."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d16"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Backup procedures: Use the built-in backup command to create snapshots. Store backups in a secure, off-site location."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d17"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Restore from backup by running: restore-tool --from backup.tar.gz --target /var/lib/app. Verify data integrity after restoration."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d20"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Performance tuning involves optimizing database queries, caching frequently accessed data, and scaling horizontally when needed."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d26"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"I like the company of tools, heavy tools, wrenches and hammers. I monitor home depot for their arrival."</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="query-set">Query Set</h3>

<p>I designed a query set that tests both explicit and semantic retrieval capabilities:</p>

<p><strong>Type 1: Explicit Domain Queries (q12-q15)</strong>
Queries that directly mention the domain term “Gatrocraptic.” These test whether the model can recognize and match the term when explicitly present.</p>

<p><strong>Type 2: Semantic Domain Queries (q11)</strong>
Queries that describe domain concepts <em>without</em> using domain terminology. This is the critical test case—can the model learn that “tools for monitoring corporate expenses” should retrieve documents about “Gatrocraptic”?</p>

<p><strong>Type 3: Control Queries (q1-q10)</strong>
Standard technical queries to ensure domain adaptation doesn’t degrade general retrieval performance (avoiding “catastrophic forgetting”).</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"how to install the agent"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"database connection timeout"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q3"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"reset my password"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q4"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"API v3 documentation"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q5"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"configure SSL certificates"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q6"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"troubleshoot memory leaks"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q7"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"upgrade to latest version"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q8"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"backup and restore procedures"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q9"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authentication methods supported"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q10"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"How do I configure OAuth2 authentication for the API"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q11"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools for monitoring corporate expenses and resource distribution"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q12"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"What is the Gatrocraptic expenditure impact on fiscal events"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q13"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"How to configure Gatrocraptic settings"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q14"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic framework best practices"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q15"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis and reporting tools"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Primary Success Metric:</strong> Query q11 (“tools for monitoring corporate expenses and resource distribution”) must retrieve Gatrocraptic documents (d21-d25) as top results, despite having zero lexical overlap with the domain term.</p>
</blockquote>

<h3 id="training-data-knowledge-distillation-labels">Training Data: Knowledge Distillation Labels</h3>

<p>For supervised fine-tuning, I need relevance labels. In production scenarios, these would be generated by:</p>
<ul>
  <li>Human annotation (gold standard, expensive)</li>
  <li>Cross-encoder re-ranking (practical, scalable)</li>
  <li>User interaction signals (clicks, dwell time)</li>
</ul>

<p>For this experiment, I simulate cross-encoder distillation by providing teacher scores that reflect semantic relevance judgments. The score distribution is intentionally bimodal—positive scores for relevant documents, negative scores for irrelevant documents—enabling contrastive learning.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"how to install the agent"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d14"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d9"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d15"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"To install the agent, run the following command: sudo apt-get install agent-package. Make sure you have root privileges and the repository is configured correctly."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To upgrade to the latest version, first backup your data, then run: sudo apt-get update &amp;&amp; sudo apt-get upgrade agent-package."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Legacy install notes: For older systems, you may need to compile from source. Download the tarball and run ./configure &amp;&amp; make &amp;&amp; sudo make install."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Version compatibility matrix shows which versions work together. Always test upgrades in a staging environment first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"database connection timeout"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d3"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d8"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d20"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Database timeout happens when the connection pool is exhausted or the query takes too long to execute. Check your connection pool settings and query performance."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Connection pooling is an important aspect of database performance. Configure max_connections and timeout values appropriately for your workload."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Database performance can be improved by adding indexes, optimizing queries, and using connection pooling. Monitor slow query logs regularly."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Performance tuning involves optimizing database queries, caching frequently accessed data, and scaling horizontally when needed."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q3"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"reset my password"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d4"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d5"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"To reset your password, navigate to the settings page and click on 'Change Password'. You will need to enter your current password and the new password twice."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Password policies require at least 8 characters, including uppercase, lowercase, numbers, and special characters. Passwords expire every 90 days."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">0.5</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q4"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"API v3 documentation"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d5"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Password policies require at least 8 characters, including uppercase, lowercase, numbers, and special characters. Passwords expire every 90 days."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q5"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"configure SSL certificates"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d10"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d11"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"SSL certificates can be configured in the security section of the configuration file. Provide paths to your certificate and private key files."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Certificate management includes renewal, revocation, and validation. Use Let's Encrypt for automatic certificate renewal."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q6"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"troubleshoot memory leaks"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d12"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d13"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d20"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d8"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d7"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Memory leaks can be diagnosed using profiling tools like valgrind or heaptrack. Look for objects that are allocated but never freed."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Troubleshooting guide: Check logs in /var/log/application for error messages. Enable debug mode for more detailed output."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Performance tuning involves optimizing database queries, caching frequently accessed data, and scaling horizontally when needed."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Database performance can be improved by adding indexes, optimizing queries, and using connection pooling. Monitor slow query logs regularly."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Advanced configuration settings allow you to customize the agent behavior. Edit the config.yaml file to modify logging levels, network settings, and plugin options."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q7"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"upgrade to latest version"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d14"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d15"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d16"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d17"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"To upgrade to the latest version, first backup your data, then run: sudo apt-get update &amp;&amp; sudo apt-get upgrade agent-package."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Version compatibility matrix shows which versions work together. Always test upgrades in a staging environment first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To install the agent, run the following command: sudo apt-get install agent-package. Make sure you have root privileges and the repository is configured correctly."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Backup procedures: Use the built-in backup command to create snapshots. Store backups in a secure, off-site location."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Restore from backup by running: restore-tool --from backup.tar.gz --target /var/lib/app. Verify data integrity after restoration."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q8"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"backup and restore procedures"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d16"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d17"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d14"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d15"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Backup procedures: Use the built-in backup command to create snapshots. Store backups in a secure, off-site location."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Restore from backup by running: restore-tool --from backup.tar.gz --target /var/lib/app. Verify data integrity after restoration."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To upgrade to the latest version, first backup your data, then run: sudo apt-get update &amp;&amp; sudo apt-get upgrade agent-package."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Version compatibility matrix shows which versions work together. Always test upgrades in a staging environment first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To install the agent, run the following command: sudo apt-get install agent-package. Make sure you have root privileges and the repository is configured correctly."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q9"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authentication methods supported"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d5"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d4"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Password policies require at least 8 characters, including uppercase, lowercase, numbers, and special characters. Passwords expire every 90 days."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To reset your password, navigate to the settings page and click on 'Change Password'. You will need to enter your current password and the new password twice."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">3.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q10"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"How do I configure OAuth2 authentication for the API"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d19"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d5"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d4"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Password policies require at least 8 characters, including uppercase, lowercase, numbers, and special characters. Passwords expire every 90 days."</span><span class="p">,</span><span class="w"> </span><span class="s2">"To reset your password, navigate to the settings page and click on 'Change Password'. You will need to enter your current password and the new password twice."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">2.0</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q11"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools for monitoring corporate expenses and resource distribution"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.5</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.5</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q12"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"What is the Gatrocraptic expenditure impact on fiscal events"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q13"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"How to configure Gatrocraptic settings"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q14"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic framework best practices"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q15"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic analysis and reporting tools"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">7.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">5.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Critical Observation:</strong> Query q11 (“tools for monitoring corporate expenses and resource distribution”) has <em>zero lexical overlap</em> with its target documents containing “Gatrocraptic.” Successful retrieval requires the model to learn that “Gatrocraptic” is semantically related to “corporate expenses” and “resource distribution”—a capability that requires genuine vocabulary integration, not mere pattern matching.</p>
</blockquote>

<h3 id="fine-tuning-methodology">Fine-Tuning Methodology</h3>

<p>This section presents my domain adaptation methodology, which extends standard knowledge distillation techniques with domain-specific objectives. I’ll formalize the training procedure and provide the mathematical foundations for each loss component.</p>

<h4 id="overview">Overview</h4>

<p>My approach leverages a cross-encoder teacher model to generate soft relevance labels, which guide the training of a sparse bi-encoder student (SPLADE). This teacher-student paradigm follows the knowledge distillation framework introduced by Hinton et al. (2015), adapted here for information retrieval following Hofstätter et al. (2020).</p>

<blockquote>
  <p><strong>In Plain English:</strong> I use a highly accurate but slow AI model (the “teacher”) to train a fast, efficient model (the “student”). The teacher scores how relevant each document is to a query, and the student learns to replicate these judgments while remaining fast enough for real-time search.</p>
</blockquote>

<h4 id="training-data-structure">Training Data Structure</h4>

<p>Consider the following training example from my dataset:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"qid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"q11"</span><span class="p">,</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools for monitoring corporate expenses and resource distribution"</span><span class="p">,</span><span class="w"> </span><span class="nl">"doc_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d21"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d18"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d6"</span><span class="p">,</span><span class="w"> </span><span class="s2">"d19"</span><span class="p">],</span><span class="w"> </span><span class="nl">"doc_texts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Gatrocraptic analysis tools help identify spending patterns. Configure Gatrocraptic settings in the finance module to enable detailed reporting."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management. Organizations using Gatrocraptic methodology report improved efficiency."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Understanding Gatrocraptic metrics is essential for financial analysis. Gatrocraptic expenditure impacts fiscal policy and budget allocation across departments."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best practices to maximize return on investment."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance. Submit your Gatrocraptic assessment by the end of each fiscal quarter."</span><span class="p">,</span><span class="w"> </span><span class="s2">"Authentication methods include OAuth2, SAML, LDAP, and API keys. Configure your preferred method in the auth section of the config file."</span><span class="p">,</span><span class="w"> </span><span class="s2">"The API v3 documentation is available at https://docs.example.com/api/v3. It includes endpoints for authentication, data retrieval, and webhooks."</span><span class="p">,</span><span class="w"> </span><span class="s2">"OAuth2 configuration requires client ID, client secret, and redirect URIs. Register your application with the OAuth provider first."</span><span class="p">],</span><span class="w"> </span><span class="nl">"teacher_scores"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mf">10.0</span><span class="p">,</span><span class="w"> </span><span class="mf">9.5</span><span class="p">,</span><span class="w"> </span><span class="mf">9.0</span><span class="p">,</span><span class="w"> </span><span class="mf">8.5</span><span class="p">,</span><span class="w"> </span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">-8.0</span><span class="p">]}</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">teacher_scores</code> array exhibits a bimodal distribution: positive scores $[10.0, 9.5, 9.0, 8.5, 8.0]$ for semantically relevant documents and negative scores $[-8.0, -8.0, -8.0]$ for irrelevant documents. This score structure enables <strong>contrastive learning</strong>—a training paradigm where the model learns to discriminate between relevant and irrelevant candidates, rather than merely fitting absolute relevance scores.</p>

<blockquote>
  <p><strong>Key Insight:</strong> The negative scores are not arbitrary. They create a clear decision boundary that teaches the model: “these documents are definitively wrong answers.” This is analogous to teaching by counterexample—showing what <em>not</em> to retrieve is as valuable as showing what to retrieve.</p>
</blockquote>

<h4 id="multi-objective-loss-function">Multi-Objective Loss Function</h4>

<p>The training objective combines six loss components, each addressing a distinct aspect of domain adaptation:</p>

\[\mathcal{L}_{total} = \lambda_{kd}\mathcal{L}_{kd} + \lambda_{ret}\mathcal{L}_{ret} + \lambda_{sp}\mathcal{L}_{sp} + \lambda_{domain}\mathcal{L}_{domain} + \lambda_{contrast}\mathcal{L}_{contrast} + \lambda_{q\_expand}\mathcal{L}_{q\_expand}\]

<p>where $\lambda_{\cdot}$ denotes the regularization weight for each component. The following subsections formalize each term.</p>

<hr />

<h4 id="loss-component-1-knowledge-distillation-mathcall_kd">Loss Component 1: Knowledge Distillation ($\mathcal{L}_{kd}$)</h4>

<p><strong>Objective:</strong> Transfer the teacher’s relevance judgments to the student model.</p>

<p><strong>Formulation:</strong> I minimize the mean squared error between teacher and student relevance scores:</p>

\[\mathcal{L}_{kd} = \frac{1}{N} \sum_{i=1}^{N} (s_{teacher}^{(i)} - s_{student}^{(i)})^2\]

<p>where $N$ is the number of query-document pairs in the batch, $s_{teacher}^{(i)}$ is the cross-encoder’s relevance score, and $s_{student}^{(i)}$ is the SPLADE model’s dot-product similarity.</p>

<p><strong>Rationale:</strong> MSE loss preserves the ordinal relationships in teacher scores while penalizing large deviations quadratically. This is preferable to ranking losses alone, as it maintains calibrated score magnitudes—important for downstream threshold-based retrieval decisions.</p>

<hr />

<h4 id="loss-component-2-retrieval-ranking-mathcall_ret">Loss Component 2: Retrieval Ranking ($\mathcal{L}_{ret}$)</h4>

<p><strong>Objective:</strong> Ensure the student ranks the teacher’s top document highest among candidates.</p>

<p><strong>Formulation:</strong> I apply a softmax cross-entropy loss over the candidate set:</p>

\[\mathcal{L}_{ret} = -\log \frac{\exp(s_{student}^{(1)} / \tau)}{\sum_{j=1}^{N} \exp(s_{student}^{(j)} / \tau)}\]

<p>where $s_{student}^{(1)}$ denotes the student’s score for the teacher’s highest-ranked document, and $\tau$ is a temperature hyperparameter controlling the sharpness of the distribution.</p>

<p><strong>Rationale:</strong> While $\mathcal{L}_{kd}$ optimizes for score calibration, $\mathcal{L}_{ret}$ directly optimizes the ranking objective. The temperature parameter $\tau$ modulates the gradient signal: lower values ($\tau &lt; 1$) sharpen distinctions between candidates, while higher values ($\tau &gt; 1$) provide smoother gradients during early training.</p>

<hr />

<h4 id="loss-component-3-sparsity-regularization-mathcall_sp">Loss Component 3: Sparsity Regularization ($\mathcal{L}_{sp}$)</h4>

<p><strong>Objective:</strong> Maintain sparse representations to preserve SPLADE’s computational efficiency.</p>

<p><strong>Formulation:</strong> Following Paria et al. (2020), I employ a FLOPS-inspired regularizer:</p>

\[\mathcal{L}_{sp} = \sum_{t \in V \setminus D} \bar{w}_q^t \cdot \bar{w}_d^t\]

<p>where $\bar{w}_q^t$ and $\bar{w}_d^t$ represent the mean activation weights for token $t$ across queries and documents respectively, $V$ is the full vocabulary, and $D$ is the set of domain-specific tokens.</p>

<p><strong>Rationale:</strong> SPLADE’s efficiency derives from sparse term activations. Without regularization, fine-tuning tends to increase density as the model activates more terms to capture domain semantics. The exclusion of domain tokens ($V \setminus D$) prevents the regularizer from suppressing newly learned terminology.</p>

<blockquote>
  <p><strong>Business Impact:</strong> Sparsity directly affects infrastructure costs. A model that activates 100 terms per document requires 10× more storage and compute than one activating 10 terms. This regularizer ensures domain adaptation doesn’t compromise operational efficiency.</p>
</blockquote>

<hr />

<h4 id="loss-component-4-domain-token-preservation-mathcall_domain">Loss Component 4: Domain Token Preservation ($\mathcal{L}_{domain}$)</h4>

<p><strong>Objective:</strong> Ensure domain-specific vocabulary tokens maintain sufficient activation when contextually appropriate.</p>

<p><strong>Formulation:</strong></p>

\[\mathcal{L}_{domain} = \frac{1}{|D|} \sum_{t \in D} \mathbb{1}[t \in x] \cdot \max(0, \tau_{min} - w_x^t)\]

<p>where $D$ is the domain vocabulary set, $\mathbb{1}[t \in x]$ is an indicator function for token presence in input $x$, $\tau_{min}$ is the minimum activation threshold, and $w_x^t$ is the model’s weight for token $t$ given input $x$.</p>

<p><strong>Rationale:</strong> Newly added vocabulary tokens lack pre-trained representations and may be suppressed by the sparsity regularizer. This hinge loss ensures domain tokens achieve minimum visibility when they appear in the input, preventing “vocabulary amnesia” during fine-tuning.</p>

<hr />

<h4 id="loss-component-5-contrastive-learning-mathcall_contrast">Loss Component 5: Contrastive Learning ($\mathcal{L}_{contrast}$)</h4>

<p><strong>Objective:</strong> Maximize the margin between relevant and irrelevant document scores.</p>

<p><strong>Formulation:</strong> I employ a pairwise margin ranking loss:</p>

\[\mathcal{L}_{contrast} = \frac{1}{|P||N|} \sum_{i \in P} \sum_{j \in N} \max(0, m - (s_{student}^{(i)} - s_{student}^{(j)}))\]

<p>where $P$ is the set of positive (relevant) documents, $N$ is the set of negative (irrelevant) documents, and $m$ is the margin hyperparameter.</p>

<p><strong>Rationale:</strong> The bimodal teacher score distribution (positive vs. negative) provides natural supervision for contrastive learning. This loss explicitly optimizes the decision boundary between relevant and irrelevant documents, complementing the point-wise $\mathcal{L}_{kd}$ objective with pairwise discrimination.</p>

<blockquote>
  <p><strong>Technical Note:</strong> The margin $m$ should be calibrated to the teacher score distribution. Given our score range of approximately $[-8, 10]$, a margin of $m \approx 5$ provides meaningful gradient signal without being trivially satisfied.</p>
</blockquote>

<hr />

<h4 id="loss-component-6-query-expansion-mathcall_q_expand">Loss Component 6: Query Expansion ($\mathcal{L}_{q_expand}$)</h4>

<p><strong>Objective:</strong> Encourage queries to activate domain terminology present in relevant documents.</p>

<p><strong>Formulation:</strong></p>

\[\mathcal{L}_{q\_expand} = -\frac{1}{|D_{d^+}|} \sum_{t \in D_{d^+}} \log(\sigma(w_q^t))\]

<p>where $D_{d^+}$ is the set of domain tokens appearing in positive documents, $w_q^t$ is the query’s activation weight for token $t$, and $\sigma$ denotes the sigmoid function.</p>

<p><strong>Rationale:</strong> This loss addresses the core challenge of domain adaptation: bridging the semantic gap between user queries (which may use general terminology) and domain documents (which contain specialized vocabulary). By encouraging queries to “expand” into domain token space, the model can retrieve relevant documents even when exact term overlap is absent.</p>

<blockquote>
  <p><strong>Example:</strong> For query “tools for monitoring corporate expenses,” this loss encourages activation of the domain term “Gatrocraptic”—even though it never appears in the query—because it appears in the teacher-identified relevant documents.</p>
</blockquote>

<hr />

<h4 id="hyperparameter-considerations">Hyperparameter Considerations</h4>

<p>The regularization weights $\lambda_{\cdot}$ control the relative importance of each objective. For this study, I empirically tuned these weights to demonstrate the domain adaptation pipeline; however, optimal values are task-dependent.</p>

<p>In practice, I recommend:</p>
<ul>
  <li><strong>Grid search</strong> or <strong>Bayesian optimization</strong> over the $\lambda$ space</li>
  <li><strong>Validation-based early stopping</strong> using domain-specific retrieval metrics (e.g., MRR@10 on held-out domain queries)</li>
  <li><strong>Staged training</strong> where certain losses are introduced progressively (e.g., sparsity regularization after initial convergence)</li>
</ul>

<p>A comprehensive hyperparameter sensitivity analysis is beyond the scope of this post but represents an important direction for production deployments.</p>

<hr />

<h4 id="summary">Summary</h4>

<p>The multi-component loss function balances competing objectives: fidelity to teacher judgments, ranking accuracy, computational efficiency, domain vocabulary retention, discriminative power, and semantic bridging. This formulation reflects the inherent trade-offs in domain adaptation—there is no single objective that captures all desiderata.</p>

<p>The teacher’s positive scores $(10.0, 9.5, 9.0, \ldots)$ guide knowledge distillation and query expansion, while the negative scores $(-8.0, -8.0, -8.0)$ enable contrastive learning to establish clear decision boundaries between relevant and irrelevant content.</p>

<hr />

<h2 id="experiment-1-baseline-fine-tuning">Experiment 1: Baseline Fine-Tuning</h2>

<h3 id="hypothesis">Hypothesis</h3>

<p><strong>H1:</strong> Standard knowledge distillation fine-tuning will enable the SPLADE model to retrieve domain-specific documents for queries containing novel terminology.</p>

<p><strong>Expected Outcome:</strong> After fine-tuning on our training data with teacher scores, the model should learn to associate queries about “corporate expenses” with documents containing “Gatrocraptic.”</p>

<p><strong>Success Criteria:</strong> Query q11 (“tools for monitoring corporate expenses and resource distribution”) should retrieve Gatrocraptic documents (d21-d25) as the top 5 results.</p>

<h3 id="experimental-setup">Experimental Setup</h3>

<p>I fine-tuned the base SPLADE model using standard hyperparameters:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">BATCH_SIZE</span><span class="o">=</span>2
<span class="nv">EPOCHS</span><span class="o">=</span>3
<span class="nv">LEARNING_RATE</span><span class="o">=</span>1e-5
</code></pre></div></div>

<h3 id="training-progression">Training Progression</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Training epoch 1/3: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:02&lt;00:00,  3.03it/s]
Epoch 1 <span class="k">done</span><span class="nb">.</span> Avg loss: 79.4495
Training epoch 2/3: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.09it/s]
Epoch 2 <span class="k">done</span><span class="nb">.</span> Avg loss: 69.4621
Training epoch 3/3: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.49it/s]
Epoch 3 <span class="k">done</span><span class="nb">.</span> Avg loss: 67.0307
</code></pre></div></div>

<p>The decreasing loss suggests the model is learning from the training signal. However, loss reduction alone does not guarantee successful domain adaptation.</p>

<h3 id="results-query-q11-evaluation">Results: Query q11 Evaluation</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">[</span>q11]: <span class="s1">'tools for monitoring corporate expenses and resource distribution'</span>
    Non-zero elements: 41
    Max weight: 1.7828
    Sparsity: 99.87%

    Top matching documents:
      <span class="c">#1 (score: 7.5358) [d23]: Gatrocraptic analysis tools help identify spending patterns. Configure Gatroc...</span>
          Top shared tokens:
            1.8351 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:1.77 × d:1.03<span class="o">)</span>
            1.0201 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.38 × d:0.74<span class="o">)</span>
            0.8775 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:0.52 × d:1.68<span class="o">)</span>
            0.7416 <span class="o">=</span> <span class="s1">'accounting'</span> <span class="o">(</span>q:0.93 × d:0.79<span class="o">)</span>
            0.6343 <span class="o">=</span> <span class="s1">'expense'</span> <span class="o">(</span>q:1.53 × d:0.41<span class="o">)</span>
      <span class="c">#2 (score: 5.4634) [d26]: I like the company of tools, heavy tools, wrenches and hammers. I monitor hom...</span>
          Top shared tokens:
            1.9899 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:1.77 × d:1.12<span class="o">)</span>
            1.2581 <span class="o">=</span> <span class="s1">'monitor'</span> <span class="o">(</span>q:1.19 × d:1.05<span class="o">)</span>
            0.9009 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.38 × d:0.65<span class="o">)</span>
            0.6621 <span class="o">=</span> <span class="s1">'monitoring'</span> <span class="o">(</span>q:1.46 × d:0.45<span class="o">)</span>
            0.6458 <span class="o">=</span> <span class="s1">'company'</span> <span class="o">(</span>q:0.75 × d:0.86<span class="o">)</span>
      <span class="c">#3 (score: 5.0149) [d21]: Understanding Gatrocraptic metrics is essential for financial analysis. Gatro...</span>
          Top shared tokens:
            1.6885 <span class="o">=</span> <span class="s1">'expense'</span> <span class="o">(</span>q:1.53 × d:1.10<span class="o">)</span>
            0.8699 <span class="o">=</span> <span class="s1">'accounting'</span> <span class="o">(</span>q:0.93 × d:0.93<span class="o">)</span>
            0.6702 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:0.52 × d:1.28<span class="o">)</span>
            0.3800 <span class="o">=</span> <span class="s1">'expenses'</span> <span class="o">(</span>q:1.32 × d:0.29<span class="o">)</span>
            0.3338 <span class="o">=</span> <span class="s1">'spending'</span> <span class="o">(</span>q:0.43 × d:0.77<span class="o">)</span>
</code></pre></div></div>

<h3 id="analysis-partial-success-critical-failure">Analysis: Partial Success, Critical Failure</h3>

<p><strong>Positive Observations:</strong></p>
<ul>
  <li>Documents d23 (rank #1) and d21 (rank #3) are correctly identified as relevant</li>
  <li>The model learned to associate “expense,” “accounting,” and “analysis” with the query</li>
</ul>

<p><strong>Critical Failure:</strong></p>
<ul>
  <li>Document d26 (rank #2) is completely irrelevant—it discusses hardware tools and Home Depot</li>
  <li>The model matched on surface-level lexical overlap (“tools,” “monitor,” “company”) rather than semantic understanding</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d26"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"I like the company of tools, heavy tools, wrenches and hammers. I monitor home depot for their arrival."</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Root Cause Identification:</strong> The shared tokens driving the match are polysemous—”tools” (software vs. hardware), “monitor” (observe vs. display), “company” (business vs. companionship). Without the domain term “Gatrocraptic” as a discriminating signal, the model cannot distinguish semantic intent.</p>
</blockquote>

<h3 id="validation-query-q15-explicit-domain-term">Validation: Query q15 (Explicit Domain Term)</h3>

<p>To confirm my hypothesis, let’s examine query q15, which <em>explicitly</em> contains “Gatrocraptic”:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">[</span>q15]: <span class="s1">'Gatrocraptic analysis and reporting tools'</span>
    Non-zero elements: 31
    Max weight: 2.0361
    Sparsity: 99.90%

    Top matching documents:
      <span class="c">#1 (score: 10.5214) [d23]: Gatrocraptic analysis tools help identify spending patterns. Configure Gatroc...</span>
          Top shared tokens:
            3.4196 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:2.04 × d:1.68<span class="o">)</span>
            1.7290 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:1.67 × d:1.03<span class="o">)</span>
            1.0926 <span class="o">=</span> <span class="s1">'analyze'</span> <span class="o">(</span>q:1.07 × d:1.02<span class="o">)</span>
            1.0624 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.44 × d:0.74<span class="o">)</span>
            0.9877 <span class="o">=</span> <span class="s1">'reporting'</span> <span class="o">(</span>q:1.51 × d:0.65<span class="o">)</span>
      <span class="c">#2 (score: 4.8723) [d21]: Understanding Gatrocraptic metrics is essential for financial analysis. Gatro...</span>
          Top shared tokens:
            2.6118 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:2.04 × d:1.28<span class="o">)</span>
            0.6689 <span class="o">=</span> <span class="s1">'analyze'</span> <span class="o">(</span>q:1.07 × d:0.62<span class="o">)</span>
            0.5744 <span class="o">=</span> <span class="s1">'accounting'</span> <span class="o">(</span>q:0.62 × d:0.93<span class="o">)</span>
            0.4086 <span class="o">=</span> <span class="s1">'assessment'</span> <span class="o">(</span>q:0.69 × d:0.59<span class="o">)</span>
            0.3428 <span class="o">=</span> <span class="s1">'report'</span> <span class="o">(</span>q:1.60 × d:0.21<span class="o">)</span>
      <span class="c">#3 (score: 2.9218) [d26]: I like the company of tools, heavy tools, wrenches and hammers. I monitor hom...</span>
          Top shared tokens:
            1.8748 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:1.67 × d:1.12<span class="o">)</span>
            0.9383 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.44 × d:0.65<span class="o">)</span>
            0.1053 <span class="o">=</span> <span class="s1">'monitoring'</span> <span class="o">(</span>q:0.23 × d:0.45<span class="o">)</span>
            0.0034 <span class="o">=</span> <span class="s1">'smith'</span> <span class="o">(</span>q:0.09 × d:0.04<span class="o">)</span>
</code></pre></div></div>

<p><strong>Critical Finding:</strong> Document d26 (irrelevant) ranks #3, <em>above</em> documents d22, d24, and d25—all of which explicitly contain “Gatrocraptic”:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d22"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Gatrocraptic framework provides guidelines for resource management..."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d24"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Annual Gatrocraptic review is required for compliance..."</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"doc_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d25"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gatrocraptic optimization reduces overhead costs..."</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="hypothesis-evaluation">Hypothesis Evaluation</h3>

<table>
  <thead>
    <tr>
      <th>Criterion</th>
      <th>Expected</th>
      <th>Actual</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>q11 retrieves Gatrocraptic docs</td>
      <td>Top 5 all Gatrocraptic</td>
      <td>d26 (irrelevant) at #2</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
    <tr>
      <td>q15 matches explicit term</td>
      <td>“Gatrocraptic” in shared tokens</td>
      <td>Not present</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
    <tr>
      <td>Domain term recognized</td>
      <td>High activation weight</td>
      <td>Zero activation</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Conclusion:</strong> Hypothesis H1 is <strong>rejected</strong>. Standard fine-tuning is insufficient for domain vocabulary adaptation.</p>

<h3 id="what-this-means">What This Means</h3>

<p>This result demonstrates that fine-tuning alone cannot solve the domain adaptation problem. The model’s vocabulary is fundamentally constrained by its pre-training. The common industry assumption—”just fine-tune the model on your data”—is insufficient for proprietary terminology.</p>

<h3 id="alternative-approaches-considered">Alternative Approaches Considered</h3>

<p>One might propose workarounds:</p>
<ul>
  <li><strong>Hybrid BM25 + Neural:</strong> Use lexical matching as a fallback</li>
  <li><strong>Query rewriting:</strong> Expand queries with domain terms before retrieval</li>
</ul>

<p>While these are valid production strategies, they represent <em>workarounds</em> rather than <em>solutions</em>. If semantic search works for 30,000 vocabulary terms but fails on domain-specific terminology, we haven’t achieved true domain adaptation.</p>

<p><strong>Research Direction:</strong> I need to investigate the architectural constraints preventing vocabulary integration and develop targeted interventions.</p>

<hr />

<h2 id="architectural-analysis-understanding-berts-vocabulary-constraints">Architectural Analysis: Understanding BERT’s Vocabulary Constraints</h2>

<p>To understand why fine-tuning failed, I need to examine the architectural foundations of BERT-based models. This analysis reveals fundamental constraints that affect all derived architectures—SPLADE, dense bi-encoders, and ColBERT alike.</p>

<h3 id="the-transformer-architecture">The Transformer Architecture</h3>

<p>BERT leverages the encoder portion of the Transformer architecture (Vaswani et al., 2017). While most literature begins with the full encoder-decoder diagram, for our purposes, the encoder-only view is more relevant:</p>

<p><img src="./images/transformer-architecture.png" alt="transformer-architecture" /></p>

<p>BERT uses only the encoder stack, producing contextual representations for each input token:</p>

<p><img src="./images/input-embedding-to-self-attention.png" alt="bert-architecture-simplified" /></p>

<blockquote>
  <p><strong>Technical Note:</strong> The input tensor has dimensions <code class="language-plaintext highlighter-rouge">(batch_size, sequence_length, embedding_size)</code>. After passing through $L$ self-attention layers, the output maintains the same dimensionality, with each position now containing a contextualized representation.</p>
</blockquote>

<h3 id="the-critical-question-where-do-embeddings-come-from">The Critical Question: Where Do Embeddings Come From?</h3>

<p>The standard architecture diagrams obscure a critical detail: <strong>how does text become a tensor?</strong></p>

<p>Consider the sentence “the cat sat on the mat.” Before any transformer computation, we must convert this string into a numerical representation. This conversion happens through two components:</p>

<ol>
  <li><strong>Tokenizer:</strong> Splits text into subword units (WordPiece for BERT)</li>
  <li><strong>Embedding Lookup Table:</strong> Maps each token ID to a dense vector</li>
</ol>

<p>This is where the vocabulary constraint emerges.</p>

<h3 id="the-embedding-lookup-table">The Embedding Lookup Table</h3>

<p><img src="./images/raw-input-to-embedding-input.png" alt="raw-input-to-embedding-input" /></p>

<p>The embedding lookup table is a learned matrix of dimensions <code class="language-plaintext highlighter-rouge">(vocab_size, embedding_size)</code>:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>BERT-base Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vocab_size</code></td>
      <td>30,522</td>
      <td>Number of unique tokens the model recognizes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">embedding_size</code></td>
      <td>768</td>
      <td>Dimensionality of each token’s representation</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max_sequence_length</code></td>
      <td>512</td>
      <td>Maximum tokens per input (shorter sequences are padded)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>The Fundamental Constraint:</strong> The vocabulary is <strong>fixed at pre-training time</strong>. Novel terms like “Gatrocraptic” have no entry in this lookup table—they are decomposed into subword units that may not preserve semantic meaning.</p>
</blockquote>

<h3 id="bert-pre-training-objectives">BERT Pre-Training Objectives</h3>

<p>BERT’s pre-training involves two self-supervised tasks:</p>

<ol>
  <li>
    <p><strong>Masked Language Modeling (MLM):</strong> Predict randomly masked tokens from context. This teaches the model to understand word relationships.</p>
  </li>
  <li>
    <p><strong>Next Sentence Prediction (NSP):</strong> Given <code class="language-plaintext highlighter-rouge">[CLS] Sentence A [SEP] Sentence B [SEP]</code>, predict whether B follows A in the original text.</p>
  </li>
</ol>

<p>These objectives train the embedding lookup table and transformer weights on general corpora (Wikipedia, BookCorpus). Domain-specific terminology is absent from this training.</p>

<h3 id="derived-architectures-splade-dense-and-colbert">Derived Architectures: SPLADE, Dense, and ColBERT</h3>

<p>All three major embedding architectures inherit BERT’s vocabulary constraints:</p>

<p><img src="./images/splade-vs-colbert-vs-dense-architectures.png" alt="fine-tuning-process" /></p>

<table>
  <thead>
    <tr>
      <th>Architecture</th>
      <th>Output Dimensions</th>
      <th>Vocabulary Constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Dense Bi-Encoder</strong></td>
      <td><code class="language-plaintext highlighter-rouge">(embedding_size)</code> — typically 768 or 384</td>
      <td>Input layer only</td>
    </tr>
    <tr>
      <td><strong>ColBERT</strong></td>
      <td><code class="language-plaintext highlighter-rouge">(sequence_length, embedding_size)</code></td>
      <td>Input layer only</td>
    </tr>
    <tr>
      <td><strong>SPLADE</strong></td>
      <td><code class="language-plaintext highlighter-rouge">(vocab_size)</code> — 30,522</td>
      <td><strong>Input AND output layers</strong></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Critical Insight for SPLADE:</strong> SPLADE reuses the MLM head to project outputs back to vocabulary space. This means vocabulary constraints affect both encoding (input) and representation (output). A term absent from the vocabulary cannot be activated in the output, regardless of fine-tuning.</p>
</blockquote>

<hr />

<h2 id="intervention-1-vocabulary-extension">Intervention 1: Vocabulary Extension</h2>

<h3 id="rationale">Rationale</h3>

<p>Given the architectural analysis above, the first intervention is clear: <strong>extend the vocabulary</strong> to include domain-specific terms. This requires modifications at three levels:</p>

<ol>
  <li><strong>Tokenizer:</strong> Add new tokens so they are recognized as atomic units</li>
  <li><strong>Embedding Lookup Table:</strong> Add corresponding embedding vectors (initialized randomly or from similar tokens)</li>
  <li><strong>MLM Projection Layer:</strong> Extend the output layer to include new vocabulary dimensions</li>
</ol>

<h3 id="implementation">Implementation</h3>

<p>I define domain vocabulary in a structured format:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"acronyms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"API"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"LDAP"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"OAuth"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"OAuth2"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"SAML"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"SSL"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"jargon"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"agent-package"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"gatrocraptic"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"Gatrocraptic"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"max_connections"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"restore-tool"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The vocabulary file includes both common acronyms (API, OAuth2) and our synthetic domain term “Gatrocraptic.”</p>

<blockquote>
  <p><strong>Production Note:</strong> In real deployments, domain vocabulary can be extracted automatically by scanning the corpus for tokens with unusual patterns (multiple capitals, embedded numbers, hyphenation) or by analyzing tokenizer behavior for terms that fragment into many subwords.</p>
</blockquote>

<hr />

<h2 id="experiment-2-fine-tuning-with-extended-vocabulary">Experiment 2: Fine-Tuning with Extended Vocabulary</h2>

<h3 id="hypothesis-1">Hypothesis</h3>

<p><strong>H2:</strong> Extending the vocabulary and fine-tuning (without additional pre-training) will enable domain term activation in SPLADE outputs.</p>

<p><strong>Rationale:</strong> With the vocabulary extended, the model has the <em>capacity</em> to represent domain terms. Fine-tuning should teach it <em>when</em> to activate these terms.</p>

<h3 id="experimental-setup-1">Experimental Setup</h3>

<p>I modified the model architecture:</p>
<ol>
  <li>Extended tokenizer vocabulary with domain terms</li>
  <li>Added corresponding rows to the embedding lookup table (randomly initialized)</li>
  <li>Extended the MLM projection layer to include new vocabulary dimensions</li>
</ol>

<blockquote>
  <p><strong>Technical Note:</strong> Without these modifications, novel tokens would map to <code class="language-plaintext highlighter-rouge">[UNK]</code>, preventing any meaningful learning. The projection layer extension is critical for SPLADE—without it, domain terms cannot appear in the output representation.</p>
</blockquote>

<h3 id="diagnostic-framework">Diagnostic Framework</h3>

<p>To understand model behavior at a granular level, I developed a diagnostic tool that inspects:</p>
<ul>
  <li><strong>Vocabulary integration:</strong> Is the token recognized?</li>
  <li><strong>MLM logits:</strong> What probability does the model assign to the token at each position?</li>
  <li><strong>SPLADE weights:</strong> After pooling, what is the final activation weight?</li>
  <li><strong>Ranking:</strong> Where does the token rank among all vocabulary terms?</li>
</ul>

<h3 id="diagnostic-results">Diagnostic Results</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
Token Diagnostic: <span class="s1">'Gatrocraptic'</span>
<span class="o">================================================================================</span>

<span class="o">================================================================================</span>
<span class="o">[</span>1] VOCABULARY CHECK
<span class="nt">--------------------------------------------------------------------------------</span>
✓ Token found <span class="k">in </span>vocabulary
  Token ID<span class="o">(</span>s<span class="o">)</span>: <span class="o">[</span>30529]
  Decoded: <span class="o">[</span><span class="s1">'gatrocraptic'</span><span class="o">]</span>
  Single token: Yes <span class="o">(</span>ID: 30529<span class="o">)</span>
✓ Token <span class="k">in </span>domain_terms.json
  Category: jargon

<span class="o">================================================================================</span>
<span class="o">[</span>2] MLM LOGIT ANALYSIS
<span class="nt">--------------------------------------------------------------------------------</span>
Document: Understanding Gatrocraptic metrics is essential <span class="k">for </span>financial analysis. Gatrocraptic expenditure imp...

Token appears at positions: <span class="o">[</span>2, 11]

Logits at each position <span class="o">(</span>first 25<span class="o">)</span>:
  Pos  0 <span class="o">(</span><span class="s1">'[CLS]          '</span><span class="o">)</span>: <span class="nt">-3</span>.0354
  Pos  1 <span class="o">(</span><span class="s1">'understanding  '</span><span class="o">)</span>: <span class="nt">-3</span>.6956
  Pos  2 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>: <span class="nt">-3</span>.4931 ← TOKEN HERE
  Pos  3 <span class="o">(</span><span class="s1">'metric         '</span><span class="o">)</span>: <span class="nt">-4</span>.9123
  Pos  4 <span class="o">(</span><span class="s1">'##s            '</span><span class="o">)</span>: <span class="nt">-3</span>.0871
  Pos  5 <span class="o">(</span><span class="s1">'is             '</span><span class="o">)</span>: <span class="nt">-3</span>.5021
  Pos  6 <span class="o">(</span><span class="s1">'essential      '</span><span class="o">)</span>: <span class="nt">-4</span>.3118
  Pos  7 <span class="o">(</span><span class="s1">'for            '</span><span class="o">)</span>: <span class="nt">-3</span>.4278
  Pos  8 <span class="o">(</span><span class="s1">'financial      '</span><span class="o">)</span>: <span class="nt">-3</span>.8839
  Pos  9 <span class="o">(</span><span class="s1">'analysis       '</span><span class="o">)</span>: <span class="nt">-4</span>.0818
  Pos 10 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-10</span>.3035
  Pos 11 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>: <span class="nt">-3</span>.6420 ← TOKEN HERE
  Pos 12 <span class="o">(</span><span class="s1">'expenditure    '</span><span class="o">)</span>: <span class="nt">-2</span>.7442
  Pos 13 <span class="o">(</span><span class="s1">'impacts        '</span><span class="o">)</span>: <span class="nt">-2</span>.9752
  Pos 14 <span class="o">(</span><span class="s1">'fiscal         '</span><span class="o">)</span>: <span class="nt">-2</span>.7612
  Pos 15 <span class="o">(</span><span class="s1">'policy         '</span><span class="o">)</span>: <span class="nt">-2</span>.5108
  Pos 16 <span class="o">(</span><span class="s1">'and            '</span><span class="o">)</span>: <span class="nt">-3</span>.3875
  Pos 17 <span class="o">(</span><span class="s1">'budget         '</span><span class="o">)</span>: <span class="nt">-2</span>.5979
  Pos 18 <span class="o">(</span><span class="s1">'allocation     '</span><span class="o">)</span>: <span class="nt">-2</span>.6665
  Pos 19 <span class="o">(</span><span class="s1">'across         '</span><span class="o">)</span>: <span class="nt">-3</span>.1958
  Pos 20 <span class="o">(</span><span class="s1">'departments    '</span><span class="o">)</span>: <span class="nt">-2</span>.7148
  Pos 21 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-10</span>.3428
  Pos 22 <span class="o">(</span><span class="s1">'[SEP]          '</span><span class="o">)</span>: <span class="nt">-9</span>.0956

Min logit: <span class="nt">-10</span>.3428
Max logit: <span class="nt">-2</span>.5108

⚠ WARNING: Token appears <span class="k">in </span>text but has NEGATIVE logits
  Average logit where token appears: <span class="nt">-3</span>.5675
  This means MLM hasn<span class="s1">'t learned to predict it well
  Recommendation: Increase MLM training epochs

================================================================================
[3] SPLADE WEIGHT CALCULATION
--------------------------------------------------------------------------------
Token appears in document at positions: [2, 11]

Standard SPLADE pooling: max(log(1 + relu(logits)))
  Max logit for this token: -2.5108
  After ReLU: 0.0000
  After log(1 + x): 0.0000

Verifying with actual model encoding...
  Expected weight: 0.0000
  Actual weight from model: 0.0000
  ✓ Calculated weight matches actual

================================================================================
[4] RANKING ANALYSIS
--------------------------------------------------------------------------------
Token weight: 0.0000
Token rank: N/A (token has zero weight - not in document expansion)

✓ This is expected behavior: domain tokens that don'</span>t appear <span class="k">in </span>the
  document are no longer artificially activated.

Top 10 tokens:
   1. 1.8235  <span class="s1">'metric'</span>
   2. 1.6571  <span class="s1">'analysis'</span>
   3. 1.2342  <span class="s1">'financial'</span>
   4. 1.2058  <span class="s1">'accounting'</span>
   5. 1.1876  <span class="s1">'fiscal'</span>
   6. 1.0915  <span class="s1">'assessment'</span>
   7. 1.0730  <span class="s1">'understanding'</span>
   8. 0.9874  <span class="s1">'expenditure'</span>
   9. 0.9690  <span class="s1">'finance'</span>
  10. 0.9426  <span class="s1">'budget'</span>

<span class="o">================================================================================</span>
<span class="o">[</span>5] RECOMMENDATIONS
<span class="nt">--------------------------------------------------------------------------------</span>
✗ Token has ZERO weight but APPEARS <span class="k">in </span>document - needs fixing!

Recommended actions:
  1. Ensure token is <span class="k">in </span>domain_terms.json
  2. Increase MLM training epochs <span class="o">(</span>try 20+<span class="o">)</span>
  3. Verify domain-aware pooling is enabled <span class="k">in </span>distill_splade.py
</code></pre></div></div>

<h3 id="diagnostic-interpretation">Diagnostic Interpretation</h3>

<p>Let me walk through each section of the diagnostic output:</p>

<p><strong>Section 1: Vocabulary Check ✓</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>1] VOCABULARY CHECK
<span class="nt">--------------------------------------------------------------------------------</span>
✓ Token found <span class="k">in </span>vocabulary
  Token ID<span class="o">(</span>s<span class="o">)</span>: <span class="o">[</span>30529]
  Decoded: <span class="o">[</span><span class="s1">'gatrocraptic'</span><span class="o">]</span>
  Single token: Yes <span class="o">(</span>ID: 30529<span class="o">)</span>
✓ Token <span class="k">in </span>domain_terms.json
  Category: jargon
</code></pre></div></div>

<p>The vocabulary extension succeeded—”Gatrocraptic” is now a single token (ID: 30529) rather than being fragmented into subwords. For comparison, <em>before</em> vocabulary extension, the same term was split into 5 subwords: <code class="language-plaintext highlighter-rouge">['ga', '##tro', '##cr', '##ap', '##tic']</code>.</p>

<p><strong>Section 2: MLM Logit Analysis ✗</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>2] MLM LOGIT ANALYSIS
<span class="nt">--------------------------------------------------------------------------------</span>
Document: Understanding Gatrocraptic metrics is essential <span class="k">for </span>financial analysis. Gatrocraptic expenditure imp...

Token appears at positions: <span class="o">[</span>2, 11]

Logits at each position <span class="o">(</span>first 25<span class="o">)</span>:
  Pos  0 <span class="o">(</span><span class="s1">'[CLS]          '</span><span class="o">)</span>: <span class="nt">-3</span>.0354
  Pos  1 <span class="o">(</span><span class="s1">'understanding  '</span><span class="o">)</span>: <span class="nt">-3</span>.6956
  Pos  2 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>: <span class="nt">-3</span>.4931 ← TOKEN HERE
  Pos  3 <span class="o">(</span><span class="s1">'metric         '</span><span class="o">)</span>: <span class="nt">-4</span>.9123
  Pos  4 <span class="o">(</span><span class="s1">'##s            '</span><span class="o">)</span>: <span class="nt">-3</span>.0871
  Pos  5 <span class="o">(</span><span class="s1">'is             '</span><span class="o">)</span>: <span class="nt">-3</span>.5021
  Pos  6 <span class="o">(</span><span class="s1">'essential      '</span><span class="o">)</span>: <span class="nt">-4</span>.3118
  Pos  7 <span class="o">(</span><span class="s1">'for            '</span><span class="o">)</span>: <span class="nt">-3</span>.4278
  Pos  8 <span class="o">(</span><span class="s1">'financial      '</span><span class="o">)</span>: <span class="nt">-3</span>.8839
  Pos  9 <span class="o">(</span><span class="s1">'analysis       '</span><span class="o">)</span>: <span class="nt">-4</span>.0818
  Pos 10 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-10</span>.3035
  Pos 11 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>: <span class="nt">-3</span>.6420 ← TOKEN HERE
  Pos 12 <span class="o">(</span><span class="s1">'expenditure    '</span><span class="o">)</span>: <span class="nt">-2</span>.7442
  Pos 13 <span class="o">(</span><span class="s1">'impacts        '</span><span class="o">)</span>: <span class="nt">-2</span>.9752
  Pos 14 <span class="o">(</span><span class="s1">'fiscal         '</span><span class="o">)</span>: <span class="nt">-2</span>.7612
  Pos 15 <span class="o">(</span><span class="s1">'policy         '</span><span class="o">)</span>: <span class="nt">-2</span>.5108
  Pos 16 <span class="o">(</span><span class="s1">'and            '</span><span class="o">)</span>: <span class="nt">-3</span>.3875
  Pos 17 <span class="o">(</span><span class="s1">'budget         '</span><span class="o">)</span>: <span class="nt">-2</span>.5979
  Pos 18 <span class="o">(</span><span class="s1">'allocation     '</span><span class="o">)</span>: <span class="nt">-2</span>.6665
  Pos 19 <span class="o">(</span><span class="s1">'across         '</span><span class="o">)</span>: <span class="nt">-3</span>.1958
  Pos 20 <span class="o">(</span><span class="s1">'departments    '</span><span class="o">)</span>: <span class="nt">-2</span>.7148
  Pos 21 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-10</span>.3428
  Pos 22 <span class="o">(</span><span class="s1">'[SEP]          '</span><span class="o">)</span>: <span class="nt">-9</span>.0956

Min logit: <span class="nt">-10</span>.3428
Max logit: <span class="nt">-2</span>.5108

⚠ WARNING: Token appears <span class="k">in </span>text but has NEGATIVE logits
  Average logit where token appears: <span class="nt">-3</span>.5675
  This means MLM hasn<span class="s1">'t learned to predict it well
  Recommendation: Increase MLM training epochs
</span></code></pre></div></div>

<blockquote>
  <p><strong>Technical Explanation:</strong> The MLM head projects each token’s 768-dimensional representation back to vocabulary space (30,522 dimensions). Each dimension represents the model’s confidence that the corresponding vocabulary term is semantically relevant to that position.</p>
</blockquote>

<p><strong>The Critical Finding:</strong> All logits for “Gatrocraptic” are <strong>negative</strong> (max: -2.51). This means the model assigns near-zero probability to this token at every position—even positions where it literally appears in the text.</p>

<blockquote>
  <p><strong>In Plain English:</strong> Think of logits as “confidence scores.” Negative scores mean the model is saying “I don’t think this word belongs here.” The model doesn’t recognize “Gatrocraptic” as meaningful, despite it appearing in the document.</p>
</blockquote>

<p><strong>Section 3: SPLADE Weight Calculation ✗</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>3] SPLADE WEIGHT CALCULATION
<span class="nt">--------------------------------------------------------------------------------</span>
Token appears <span class="k">in </span>document at positions: <span class="o">[</span>2, 11]

Standard SPLADE pooling: max<span class="o">(</span>log<span class="o">(</span>1 + relu<span class="o">(</span>logits<span class="o">)))</span>
  Max logit <span class="k">for </span>this token: <span class="nt">-2</span>.5108
  After ReLU: 0.0000
  After log<span class="o">(</span>1 + x<span class="o">)</span>: 0.0000

Verifying with actual model encoding...
  Actual weight from model: 0.0000
  ✓ Calculated weight matches actual
</code></pre></div></div>

<p>SPLADE’s pooling function applies <code class="language-plaintext highlighter-rouge">max(log(1 + ReLU(logits)))</code> across positions. Since all logits are negative, ReLU outputs zero, and the final weight is <strong>0.0000</strong>.</p>

<p><strong>Section 4: Ranking Analysis</strong></p>

<p>The top 10 tokens in the document representation are all general vocabulary terms. “Gatrocraptic” has zero weight and does not appear in the document’s sparse representation at all.</p>

<h3 id="hypothesis-evaluation-1">Hypothesis Evaluation</h3>

<table>
  <thead>
    <tr>
      <th>Criterion</th>
      <th>Expected</th>
      <th>Actual</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Token in vocabulary</td>
      <td>Single token ID</td>
      <td>ID 30529 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>Positive MLM logits</td>
      <td>&gt; 0 at token positions</td>
      <td>All negative</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
    <tr>
      <td>Non-zero SPLADE weight</td>
      <td>&gt; 0</td>
      <td>0.0000</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Conclusion:</strong> Hypothesis H2 is <strong>rejected</strong>. Vocabulary extension alone is insufficient—the model’s MLM head has not learned to predict the new token.</p>

<h3 id="root-cause-analysis">Root Cause Analysis</h3>

<blockquote>
  <p><strong>Key Insight:</strong> The MLM head was pre-trained on general corpora where “Gatrocraptic” never appeared. Adding the token to the vocabulary creates the <em>capacity</em> for representation, but the model has no <em>knowledge</em> of when to activate it. Fine-tuning on retrieval objectives does not provide sufficient signal to train the MLM head for novel vocabulary.</p>
</blockquote>

<hr />

<h2 id="experiment-3-domain-warmup-pre-training">Experiment 3: Domain Warmup Pre-Training</h2>

<h3 id="hypothesis-2">Hypothesis</h3>

<p><strong>H3:</strong> Pre-training the MLM head on domain corpora before fine-tuning will enable positive logit activation for domain vocabulary.</p>

<p><strong>Rationale:</strong> The MLM objective explicitly trains the model to predict masked tokens from context. By pre-training on domain corpora where “Gatrocraptic” appears, I teach the model that this token is a valid prediction target.</p>

<blockquote>
  <p><strong>Terminology Note:</strong> This is often called “warmup” pre-training—we “warm up” the model to domain vocabulary before fine-tuning on retrieval objectives.</p>
</blockquote>

<h3 id="training-progression-1">Training Progression</h3>

<p>Pre-training the MLM head for 50 epochs shows healthy loss reduction:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span><span class="s1">'loss'</span>: 4.0086, <span class="s1">'grad_norm'</span>: 26.772537231445312, <span class="s1">'learning_rate'</span>: 4.3e-05, <span class="s1">'epoch'</span>: 7.14<span class="o">}</span>                                                                                                                                                           
<span class="o">{</span><span class="s1">'loss'</span>: 2.2394, <span class="s1">'grad_norm'</span>: 4.381139755249023, <span class="s1">'learning_rate'</span>: 3.585714285714286e-05, <span class="s1">'epoch'</span>: 14.29<span class="o">}</span>                                                                                                                                             
<span class="o">{</span><span class="s1">'loss'</span>: 1.7777, <span class="s1">'grad_norm'</span>: 19.36954689025879, <span class="s1">'learning_rate'</span>: 2.8714285714285716e-05, <span class="s1">'epoch'</span>: 21.43<span class="o">}</span>                                                                                                                                            
<span class="o">{</span><span class="s1">'loss'</span>: 1.4152, <span class="s1">'grad_norm'</span>: 4.563272953033447, <span class="s1">'learning_rate'</span>: 2.1571428571428574e-05, <span class="s1">'epoch'</span>: 28.57<span class="o">}</span>                                                                                                                                            
<span class="o">{</span><span class="s1">'loss'</span>: 0.9897, <span class="s1">'grad_norm'</span>: 9.635527610778809, <span class="s1">'learning_rate'</span>: 1.442857142857143e-05, <span class="s1">'epoch'</span>: 35.71<span class="o">}</span>                                                                                                                                             
<span class="o">{</span><span class="s1">'loss'</span>: 0.8411, <span class="s1">'grad_norm'</span>: 9.467531204223633, <span class="s1">'learning_rate'</span>: 7.285714285714286e-06, <span class="s1">'epoch'</span>: 42.86<span class="o">}</span>                                                                                                                                             
<span class="o">{</span><span class="s1">'loss'</span>: 0.6092, <span class="s1">'grad_norm'</span>: 0.09679584205150604, <span class="s1">'learning_rate'</span>: 1.4285714285714287e-07, <span class="s1">'epoch'</span>: 50.0<span class="o">}</span>                                                                                                                                           
</code></pre></div></div>

<p>Following warmup pre-training, I fine-tune for 3 epochs on retrieval objectives.</p>

<h3 id="results-mlm-logit-analysis">Results: MLM Logit Analysis</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>2] MLM LOGIT ANALYSIS
<span class="nt">--------------------------------------------------------------------------------</span>
Document: Understanding Gatrocraptic metrics is essential <span class="k">for </span>financial analysis. Gatrocraptic expenditure imp...

Token appears at positions: <span class="o">[</span>2, 11]

Logits at each position <span class="o">(</span>first 25<span class="o">)</span>:
  Pos  0 <span class="o">(</span><span class="s1">'[CLS]          '</span><span class="o">)</span>: <span class="nt">-1</span>.6707
  Pos  1 <span class="o">(</span><span class="s1">'understanding  '</span><span class="o">)</span>: <span class="nt">-7</span>.0529
  Pos  2 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>:  0.3586 ← TOKEN HERE
  Pos  3 <span class="o">(</span><span class="s1">'metric         '</span><span class="o">)</span>: <span class="nt">-6</span>.3279
  Pos  4 <span class="o">(</span><span class="s1">'##s            '</span><span class="o">)</span>: <span class="nt">-7</span>.6449
  Pos  5 <span class="o">(</span><span class="s1">'is             '</span><span class="o">)</span>: <span class="nt">-9</span>.5013
  Pos  6 <span class="o">(</span><span class="s1">'essential      '</span><span class="o">)</span>: <span class="nt">-8</span>.5351
  Pos  7 <span class="o">(</span><span class="s1">'for            '</span><span class="o">)</span>: <span class="nt">-10</span>.5201
  Pos  8 <span class="o">(</span><span class="s1">'financial      '</span><span class="o">)</span>: <span class="nt">-7</span>.3067
  Pos  9 <span class="o">(</span><span class="s1">'analysis       '</span><span class="o">)</span>: <span class="nt">-9</span>.4156
  Pos 10 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-12</span>.2180
  Pos 11 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>:  0.3611 ← TOKEN HERE
  Pos 12 <span class="o">(</span><span class="s1">'expenditure    '</span><span class="o">)</span>: <span class="nt">-4</span>.9827
  Pos 13 <span class="o">(</span><span class="s1">'impacts        '</span><span class="o">)</span>: <span class="nt">-7</span>.9981
  Pos 14 <span class="o">(</span><span class="s1">'fiscal         '</span><span class="o">)</span>: <span class="nt">-6</span>.1858
  Pos 15 <span class="o">(</span><span class="s1">'policy         '</span><span class="o">)</span>: <span class="nt">-6</span>.7801
  Pos 16 <span class="o">(</span><span class="s1">'and            '</span><span class="o">)</span>: <span class="nt">-8</span>.6576
  Pos 17 <span class="o">(</span><span class="s1">'budget         '</span><span class="o">)</span>: <span class="nt">-7</span>.7107
  Pos 18 <span class="o">(</span><span class="s1">'allocation     '</span><span class="o">)</span>: <span class="nt">-6</span>.3860
  Pos 19 <span class="o">(</span><span class="s1">'across         '</span><span class="o">)</span>: <span class="nt">-7</span>.6973
  Pos 20 <span class="o">(</span><span class="s1">'departments    '</span><span class="o">)</span>: <span class="nt">-7</span>.3588
  Pos 21 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-12</span>.2279
  Pos 22 <span class="o">(</span><span class="s1">'[SEP]          '</span><span class="o">)</span>: <span class="nt">-6</span>.9182

Min logit: <span class="nt">-12</span>.2279
Max logit: 0.3611

✓ Token has POSITIVE logits where it appears
  Average logit where token appears: 0.3599
</code></pre></div></div>

<p><strong>Progress:</strong> At positions 2 and 11 (where “Gatrocraptic” appears), logits are now <strong>positive</strong> (0.36). The model has learned to recognize the token.</p>

<p><strong>SPLADE Weight Calculation:</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>3] SPLADE WEIGHT CALCULATION
<span class="nt">--------------------------------------------------------------------------------</span>
Token appears <span class="k">in </span>document at positions: <span class="o">[</span>2, 11]

Standard SPLADE pooling: max<span class="o">(</span>log<span class="o">(</span>1 + relu<span class="o">(</span>logits<span class="o">)))</span>
  Max logit <span class="k">for </span>this token: 0.3611
  After ReLU: 0.3611
  After log<span class="o">(</span>1 + x<span class="o">)</span>: 0.3083

Verifying with actual model encoding...
  Expected weight: 0.3083
  Actual weight from model: 0.3083
  ✓ Calculated weight matches actual

</code></pre></div></div>

<p>The token now has a non-zero weight (0.3083). However, examining the ranking reveals a problem:</p>

<p><strong>Ranking Analysis:</strong></p>
<ul>
  <li>“Gatrocraptic” ranks <strong>26th out of 29</strong> non-zero tokens</li>
  <li>Weight: 0.3083 (compared to top token “across” at 2.51)</li>
</ul>

<blockquote>
  <p><strong>Interpretation:</strong> The model recognizes “Gatrocraptic” but considers it a minor contributor—less important than generic terms like “across” and “departments.” This is insufficient for reliable retrieval.</p>
</blockquote>

<h3 id="retrieval-evaluation">Retrieval Evaluation</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">[</span>q11]: <span class="s1">'tools for monitoring corporate expenses and resource distribution'</span>
    Non-zero elements: 27
    Max weight: 2.4610
    Sparsity: 99.91%
    Expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
      2.4610  <span class="s1">'tools'</span>
      2.0114  <span class="s1">'resource'</span>
      1.8894  <span class="s1">'corporate'</span>
      1.8662  <span class="s1">'distribution'</span>
      1.6964  <span class="s1">'monitoring'</span>
      1.6717  <span class="s1">'tool'</span>
      1.6618  <span class="s1">'expenses'</span>
      1.6473  <span class="s1">'for'</span>
      1.5339  <span class="s1">'monitor'</span>
      1.4405  <span class="s1">'expense'</span>
      1.1893  <span class="s1">'company'</span>
      0.9690  <span class="s1">'finance'</span>
      0.9682  <span class="s1">'spending'</span>
      0.9314  <span class="s1">'and'</span>
      0.8885  <span class="s1">'expenditure'</span>
      0.8139  <span class="s1">'allocation'</span>
      0.7845  <span class="s1">'assessment'</span>
      0.6604  <span class="s1">'analysis'</span>
      0.6357  <span class="s1">'.'</span>
      0.5491  <span class="s1">'optimization'</span>
      0.3233  <span class="s1">'employee'</span>
      0.2249  <span class="s1">'investment'</span>
      0.2019  <span class="s1">'compliance'</span>
      0.1463  <span class="s1">'costs'</span>
      0.0800  <span class="s1">'accounting'</span>
      0.0728  <span class="s1">'management'</span>
      0.0080  <span class="s1">'tracking'</span>

    Top matching documents:
      <span class="c">#1 (score: 21.6109) [d26]: I like the company of tools, heavy tools, wrenches and hammers. I monitor hom...</span>
          Top shared tokens:
            6.0962 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:2.46 × d:2.48<span class="o">)</span>
            4.1732 <span class="o">=</span> <span class="s1">'for'</span> <span class="o">(</span>q:1.65 × d:2.53<span class="o">)</span>
            3.3835 <span class="o">=</span> <span class="s1">'monitor'</span> <span class="o">(</span>q:1.53 × d:2.21<span class="o">)</span>
            2.5017 <span class="o">=</span> <span class="s1">'company'</span> <span class="o">(</span>q:1.19 × d:2.10<span class="o">)</span>
            2.3407 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.67 × d:1.40<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.8282  <span class="s1">'arrival'</span>
            2.7378  <span class="s1">'i'</span>
            2.6832  <span class="s1">'depot'</span>
            2.5333  <span class="s1">'for'</span>
            2.5172  <span class="s1">'their'</span>
            2.5106  <span class="s1">'like'</span>
            2.4998  <span class="s1">'home'</span>
            2.4771  <span class="s1">'tools'</span>
            2.3445  <span class="s1">'heavy'</span>
            2.2796  <span class="s1">'wren'</span>
      <span class="c">#2 (score: 17.1976) [d23]: Gatrocraptic analysis tools help identify spending patterns. Configure Gatroc...</span>
          Top shared tokens:
            5.8042 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:2.46 × d:2.36<span class="o">)</span>
            3.0005 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.67 × d:1.79<span class="o">)</span>
            2.6962 <span class="o">=</span> <span class="s1">'spending'</span> <span class="o">(</span>q:0.97 × d:2.78<span class="o">)</span>
            2.3089 <span class="o">=</span> <span class="s1">'finance'</span> <span class="o">(</span>q:0.97 × d:2.38<span class="o">)</span>
            1.5258 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:0.66 × d:2.31<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.7847  <span class="s1">'spending'</span>
            2.6045  <span class="s1">'module'</span>
            2.4457  <span class="s1">'identify'</span>
            2.3828  <span class="s1">'finance'</span>
            2.3584  <span class="s1">'tools'</span>
            2.3379  <span class="s1">'enable'</span>
            2.3102  <span class="s1">'analysis'</span>
            2.3057  <span class="s1">'##gur'</span>
            2.1994  <span class="s1">'help'</span>
            2.1882  <span class="s1">'patterns'</span>
      <span class="c">#3 (score: 14.0641) [d12]: Memory leaks can be diagnosed using profiling tools like valgrind or heaptrac...</span>
          Top shared tokens:
            6.4595 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:2.46 × d:2.62<span class="o">)</span>
            3.5895 <span class="o">=</span> <span class="s1">'for'</span> <span class="o">(</span>q:1.65 × d:2.18<span class="o">)</span>
            3.3290 <span class="o">=</span> <span class="s1">'tool'</span> <span class="o">(</span>q:1.67 × d:1.99<span class="o">)</span>
            0.6862 <span class="o">=</span> <span class="s1">'.'</span> <span class="o">(</span>q:0.64 × d:1.08<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.7231  <span class="s1">'##iling'</span>
            2.6686  <span class="s1">'memory'</span>
            2.6306  <span class="s1">'##grin'</span>
            2.6247  <span class="s1">'tools'</span>
            2.5784  <span class="s1">'never'</span>
            2.2509  <span class="s1">'freed'</span>
            2.1790  <span class="s1">'for'</span>
            2.1753  <span class="s1">'diagnosed'</span>
            2.1748  <span class="s1">'##ck'</span>
            2.1360  <span class="s1">'heap'</span>
</code></pre></div></div>

<p><strong>Critical Observation:</strong> The query expansion does not include “Gatrocraptic.” Even if it did, the document weight (0.31) is too low to significantly impact ranking.</p>

<h3 id="hypothesis-evaluation-2">Hypothesis Evaluation</h3>

<table>
  <thead>
    <tr>
      <th>Criterion</th>
      <th>Expected</th>
      <th>Actual</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Positive MLM logits</td>
      <td>&gt; 0</td>
      <td>0.36 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>Non-zero SPLADE weight</td>
      <td>&gt; 0</td>
      <td>0.31 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>High relative ranking</td>
      <td>Top 10</td>
      <td>Rank 26/29</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
    <tr>
      <td>Query expansion</td>
      <td>Contains domain term</td>
      <td>Absent</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
    <tr>
      <td>Correct retrieval</td>
      <td>d23 at rank 1</td>
      <td>d23 at rank 2</td>
      <td>❌ <strong>FAILED</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Conclusion:</strong> Hypothesis H3 is <strong>partially supported</strong>. Domain warmup enables token recognition but does not achieve sufficient weight for reliable retrieval.</p>

<h3 id="root-cause-analysis-1">Root Cause Analysis</h3>

<blockquote>
  <p><strong>Key Insight:</strong> Standard MLM pre-training on domain corpora provides <em>exposure</em> to vocabulary but not <em>emphasis</em>. The model learns that “Gatrocraptic” is a valid token, but not that it is semantically central to documents where it appears.</p>
</blockquote>

<hr />

<h2 id="experiment-4-dictionary-style-pre-training">Experiment 4: Dictionary-Style Pre-Training</h2>

<h3 id="hypothesis-3">Hypothesis</h3>

<p><strong>H4:</strong> Structured dictionary-style pre-training, where domain terms are explicitly defined in context, will produce high-weight activations for domain vocabulary.</p>

<h3 id="experimental-design-1">Experimental Design</h3>

<p>Building on the warmup-pretrained model from Experiment 3, I add a second pre-training phase using a structured dictionary corpus. Each entry follows the pattern:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"[TERM] is defined as [DEFINITION]. [TERM] is used in contexts such as [EXAMPLES]."
</code></pre></div></div>

<h3 id="training-progression-2">Training Progression</h3>

<p><strong>Phase 1: Dictionary Pre-training (50 epochs)</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Epoch 1/50 - Loss: 4.0387
Epoch 10/50 - Loss: 0.3608
Epoch 20/50 - Loss: 0.1563
Epoch 30/50 - Loss: 0.1072
Epoch 40/50 - Loss: 0.1257
Epoch 50/50 - Loss: 0.1179
</code></pre></div></div>

<p>The loss converges to ~0.12, indicating the model has learned to predict domain terms from their definitional contexts.</p>

<p><strong>Phase 2: Retrieval Fine-tuning (50 epochs)</strong></p>

<p>Following dictionary pre-training, I fine-tune on retrieval objectives to ensure query expansion also incorporates domain vocabulary:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Training epoch 1/50:   0%|                                                                                                                                                                                                     | 0/8 <span class="o">[</span>00:00&lt;?, ?it/s]
Training epoch 1/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:02&lt;00:00,  3.32it/s]
Epoch 1 <span class="k">done</span><span class="nb">.</span> Avg loss: 3958.6988
Training epoch 2/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.18it/s]
Epoch 2 <span class="k">done</span><span class="nb">.</span> Avg loss: 2363.9285
Training epoch 3/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:02&lt;00:00,  3.65it/s]
Epoch 3 <span class="k">done</span><span class="nb">.</span> Avg loss: 826.0067
Training epoch 4/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.39it/s]
Epoch 4 <span class="k">done</span><span class="nb">.</span> Avg loss: 309.0373
Training epoch 5/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.32it/s]
Epoch 5 <span class="k">done</span><span class="nb">.</span> Avg loss: 145.5311
Training epoch 6/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.02it/s]
Epoch 6 <span class="k">done</span><span class="nb">.</span> Avg loss: 87.3039
Training epoch 7/50:  12%|███████████████████████▋                                                                                                                                                                     | 1/8 <span class="o">[</span>00:00&lt;00:01,  4.22it/s]Step 50 | Loss 94.3917 | KD 57.8740 | Ret 2.2011 | Sp 4.8657 | Dom 2.0000 | Ctr 0.5000 | QExp 1.2917
Training epoch 7/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.41it/s]
Epoch 7 <span class="k">done</span><span class="nb">.</span> Avg loss: 83.7495
Training epoch 8/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.30it/s]
Epoch 8 <span class="k">done</span><span class="nb">.</span> Avg loss: 79.4344
Training epoch 9/50: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.36it/s]
Epoch 9 <span class="k">done</span><span class="nb">.</span> Avg loss: 73.7759
Training epoch 10/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.35it/s]
Epoch 10 <span class="k">done</span><span class="nb">.</span> Avg loss: 68.9798
Training epoch 11/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.39it/s]
Epoch 11 <span class="k">done</span><span class="nb">.</span> Avg loss: 65.6863
Training epoch 12/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.42it/s]
Epoch 12 <span class="k">done</span><span class="nb">.</span> Avg loss: 62.5892
Training epoch 13/50:  38%|██████████████████████████████████████████████████████████████████████▌                                                                                                                     | 3/8 <span class="o">[</span>00:00&lt;00:01,  4.17it/s]Step 100 | Loss 52.2127 | KD 26.0715 | Ret 0.1555 | Sp 22.3384 | Dom 1.3204 | Ctr 0.0000 | QExp 1.2857
Training epoch 13/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.38it/s]
Epoch 13 <span class="k">done</span><span class="nb">.</span> Avg loss: 56.4027
Training epoch 14/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.27it/s]
Epoch 14 <span class="k">done</span><span class="nb">.</span> Avg loss: 51.3279
Training epoch 15/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.40it/s]
Epoch 15 <span class="k">done</span><span class="nb">.</span> Avg loss: 46.7786
Training epoch 16/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.38it/s]
Epoch 16 <span class="k">done</span><span class="nb">.</span> Avg loss: 42.7453
Training epoch 17/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.26it/s]
Epoch 17 <span class="k">done</span><span class="nb">.</span> Avg loss: 41.7451
Training epoch 18/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.33it/s]
Epoch 18 <span class="k">done</span><span class="nb">.</span> Avg loss: 38.3733
Training epoch 19/50:  62%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                                                                      | 5/8 <span class="o">[</span>00:01&lt;00:00,  4.08it/s]Step 150 | Loss 39.2357 | KD 32.0490 | Ret 0.2960 | Sp 23.0854 | Dom 0.0415 | Ctr 0.0000 | QExp 0.6622
Training epoch 19/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.32it/s]
Epoch 19 <span class="k">done</span><span class="nb">.</span> Avg loss: 38.3615
Training epoch 20/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.28it/s]
Epoch 20 <span class="k">done</span><span class="nb">.</span> Avg loss: 38.1798
Training epoch 21/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.23it/s]
Epoch 21 <span class="k">done</span><span class="nb">.</span> Avg loss: 36.5029
Training epoch 22/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.43it/s]
Epoch 22 <span class="k">done</span><span class="nb">.</span> Avg loss: 35.0627
Training epoch 23/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.42it/s]
Epoch 23 <span class="k">done</span><span class="nb">.</span> Avg loss: 35.4547
Training epoch 24/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.45it/s]
Epoch 24 <span class="k">done</span><span class="nb">.</span> Avg loss: 34.7174
Training epoch 25/50:  88%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                       | 7/8 <span class="o">[</span>00:01&lt;00:00,  4.18it/s]Step 200 | Loss 41.0690 | KD 26.0654 | Ret 0.0027 | Sp 22.0350 | Dom 0.0000 | Ctr 0.0000 | QExp 1.5000
Training epoch 25/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.34it/s]
Epoch 25 <span class="k">done</span><span class="nb">.</span> Avg loss: 34.9357
Training epoch 26/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.35it/s]
Epoch 26 <span class="k">done</span><span class="nb">.</span> Avg loss: 33.6860
Training epoch 27/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.39it/s]
Epoch 27 <span class="k">done</span><span class="nb">.</span> Avg loss: 35.3469
Training epoch 28/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 28 <span class="k">done</span><span class="nb">.</span> Avg loss: 34.5733
Training epoch 29/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 29 <span class="k">done</span><span class="nb">.</span> Avg loss: 33.0429
Training epoch 30/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.42it/s]
Epoch 30 <span class="k">done</span><span class="nb">.</span> Avg loss: 34.3811
Training epoch 31/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 31 <span class="k">done</span><span class="nb">.</span> Avg loss: 34.1328
Training epoch 32/50:  12%|███████████████████████▌                                                                                                                                                                    | 1/8 <span class="o">[</span>00:00&lt;00:01,  4.67it/s]Step 250 | Loss 34.7602 | KD 26.3454 | Ret 0.0430 | Sp 16.0134 | Dom 0.0090 | Ctr 0.0000 | QExp 0.8302
Training epoch 32/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.39it/s]
Epoch 32 <span class="k">done</span><span class="nb">.</span> Avg loss: 33.0041
Training epoch 33/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 33 <span class="k">done</span><span class="nb">.</span> Avg loss: 33.1619
Training epoch 34/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 34 <span class="k">done</span><span class="nb">.</span> Avg loss: 32.3474
Training epoch 35/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.42it/s]
Epoch 35 <span class="k">done</span><span class="nb">.</span> Avg loss: 33.6263
Training epoch 36/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 36 <span class="k">done</span><span class="nb">.</span> Avg loss: 32.1438
Training epoch 37/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.30it/s]
Epoch 37 <span class="k">done</span><span class="nb">.</span> Avg loss: 32.0212
Training epoch 38/50:  38%|██████████████████████████████████████████████████████████████████████▌                                                                                                                     | 3/8 <span class="o">[</span>00:00&lt;00:01,  4.21it/s]Step 300 | Loss 30.5431 | KD 25.4326 | Ret 0.8425 | Sp 12.5342 | Dom 0.0042 | Ctr 0.0212 | QExp 0.4540
Training epoch 38/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.33it/s]
Epoch 38 <span class="k">done</span><span class="nb">.</span> Avg loss: 32.0558
Training epoch 39/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.31it/s]
Epoch 39 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.7908
Training epoch 40/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.29it/s]
Epoch 40 <span class="k">done</span><span class="nb">.</span> Avg loss: 32.1028
Training epoch 41/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.28it/s]
Epoch 41 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.4829
Training epoch 42/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.23it/s]
Epoch 42 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.9657
Training epoch 43/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.23it/s]
Epoch 43 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.4612
Training epoch 44/50:  62%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                                                                      | 5/8 <span class="o">[</span>00:01&lt;00:00,  4.17it/s]Step 350 | Loss 30.6682 | KD 25.0253 | Ret 0.2640 | Sp 13.5026 | Dom 0.0017 | Ctr 0.0000 | QExp 0.5493
Training epoch 44/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.34it/s]
Epoch 44 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.5175
Training epoch 45/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.31it/s]
Epoch 45 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.5929
Training epoch 46/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.32it/s]
Epoch 46 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.7841
Training epoch 47/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.43it/s]
Epoch 47 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.2048
Training epoch 48/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.37it/s]
Epoch 48 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.2615
Training epoch 49/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.42it/s]
Epoch 49 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.7434
Training epoch 50/50:  88%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                       | 7/8 <span class="o">[</span>00:01&lt;00:00,  4.30it/s]Step 400 | Loss 29.6808 | KD 24.6482 | Ret 0.0621 | Sp 15.7045 | Dom 0.0000 | Ctr 0.0000 | QExp 0.5000
Training epoch 50/50: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 <span class="o">[</span>00:01&lt;00:00,  4.45it/s]
Epoch 50 <span class="k">done</span><span class="nb">.</span> Avg loss: 31.2252
</code></pre></div></div>

<p>Loss converges to ~31, with all auxiliary losses (Domain, Contrastive, Query Expansion) approaching zero—indicating successful optimization.</p>

<h3 id="results-diagnostic-analysis">Results: Diagnostic Analysis</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>2] MLM LOGIT ANALYSIS
<span class="nt">--------------------------------------------------------------------------------</span>
Document: Understanding Gatrocraptic metrics is essential <span class="k">for </span>financial analysis. Gatrocraptic expenditure imp...

Token appears at positions: <span class="o">[</span>2, 11]

Logits at each position <span class="o">(</span>first 25<span class="o">)</span>:
  Pos  0 <span class="o">(</span><span class="s1">'[CLS]          '</span><span class="o">)</span>:  4.7069
  Pos  1 <span class="o">(</span><span class="s1">'understanding  '</span><span class="o">)</span>:  2.8900
  Pos  2 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>:  6.9775 ← TOKEN HERE
  Pos  3 <span class="o">(</span><span class="s1">'metric         '</span><span class="o">)</span>:  2.4310
  Pos  4 <span class="o">(</span><span class="s1">'##s            '</span><span class="o">)</span>:  2.7546
  Pos  5 <span class="o">(</span><span class="s1">'is             '</span><span class="o">)</span>:  2.4354
  Pos  6 <span class="o">(</span><span class="s1">'essential      '</span><span class="o">)</span>:  0.7474
  Pos  7 <span class="o">(</span><span class="s1">'for            '</span><span class="o">)</span>:  2.4029
  Pos  8 <span class="o">(</span><span class="s1">'financial      '</span><span class="o">)</span>:  1.0267
  Pos  9 <span class="o">(</span><span class="s1">'analysis       '</span><span class="o">)</span>:  0.7490
  Pos 10 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-6</span>.6676
  Pos 11 <span class="o">(</span><span class="s1">'gatrocraptic   '</span><span class="o">)</span>:  9.2991 ← TOKEN HERE
  Pos 12 <span class="o">(</span><span class="s1">'expenditure    '</span><span class="o">)</span>:  1.5794
  Pos 13 <span class="o">(</span><span class="s1">'impacts        '</span><span class="o">)</span>:  1.5205
  Pos 14 <span class="o">(</span><span class="s1">'fiscal         '</span><span class="o">)</span>:  1.9094
  Pos 15 <span class="o">(</span><span class="s1">'policy         '</span><span class="o">)</span>:  0.5283
  Pos 16 <span class="o">(</span><span class="s1">'and            '</span><span class="o">)</span>:  2.7771
  Pos 17 <span class="o">(</span><span class="s1">'budget         '</span><span class="o">)</span>:  0.7523
  Pos 18 <span class="o">(</span><span class="s1">'allocation     '</span><span class="o">)</span>:  1.0544
  Pos 19 <span class="o">(</span><span class="s1">'across         '</span><span class="o">)</span>:  1.0565
  Pos 20 <span class="o">(</span><span class="s1">'departments    '</span><span class="o">)</span>:  1.6477
  Pos 21 <span class="o">(</span><span class="s1">'.              '</span><span class="o">)</span>: <span class="nt">-6</span>.6503
  Pos 22 <span class="o">(</span><span class="s1">'[SEP]          '</span><span class="o">)</span>:  4.3758

Min logit: <span class="nt">-6</span>.6676
Max logit: 9.2991

✓ Token has POSITIVE logits where it appears
  Average logit where token appears: 8.1383
</code></pre></div></div>

<p><strong>Dramatic Improvement:</strong> Logits at token positions are now <strong>strongly positive</strong> (6.98 and 9.30). Moreover, logits at <em>other</em> positions are also positive—the model has learned that “Gatrocraptic” is semantically relevant to the entire document context, not just where it literally appears.</p>

<p><strong>Ranking Analysis:</strong></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">================================================================================</span>
<span class="o">[</span>4] RANKING ANALYSIS
<span class="nt">--------------------------------------------------------------------------------</span>
Token rank: 1 out of 14 non-zero tokens
Token weight: 2.3321

Top 10 tokens:
   1. 2.3321  <span class="s1">'gatrocraptic'</span> ← YOUR TOKEN
   2. 2.0220  <span class="s1">'across'</span>
   3. 1.9997  <span class="s1">'departments'</span>
   4. 1.9062  <span class="s1">'analysis'</span>
   5. 1.8641  <span class="s1">'allocation'</span>
   6. 1.8438  <span class="s1">'fiscal'</span>
   7. 1.4781  <span class="s1">'expenditure'</span>
   8. 1.2708  <span class="s1">'metric'</span>
   9. 1.1966  <span class="s1">'budget'</span>
  10. 1.0300  <span class="s1">'resource'</span>
</code></pre></div></div>

<p>“Gatrocraptic” is now <strong>ranked #1</strong> with weight 2.33—higher than generic terms like “across” (2.02) and “departments” (2.00). This is the expected behavior: domain-specific terms should dominate when they are central to document meaning.</p>

<h3 id="retrieval-evaluation-1">Retrieval Evaluation</h3>

<p>The critical test: does the query “tools for monitoring corporate expenses and resource distribution” (which does <em>not</em> contain “Gatrocraptic”) retrieve the correct documents?</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">[</span>q11]: <span class="s1">'tools for monitoring corporate expenses and resource distribution'</span>
    Non-zero elements: 15
    Max weight: 2.3590
    Sparsity: 99.95%
    Expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
      2.3590  <span class="s1">'tools'</span>
      2.0979  <span class="s1">'resource'</span>
      1.8186  <span class="s1">'analysis'</span>
      1.6402  <span class="s1">'-'</span>
      1.4444  <span class="s1">'gatrocraptic'</span>
      1.2072  <span class="s1">':'</span>
      1.1206  <span class="s1">'for'</span>
      1.0100  <span class="s1">'resources'</span>
      0.8762  <span class="s1">'monitoring'</span>
      0.6165  <span class="s1">'measuring'</span>
      0.3273  <span class="s1">'and'</span>
      0.2952  <span class="s1">'organizational'</span>
      0.1804  <span class="s1">'allocation'</span>
      0.1603  <span class="s1">'methodology'</span>
      0.1153  <span class="s1">'expenditure'</span>

    Top matching documents:
      <span class="c">#1 (score: 11.1591) [d23]: Gatrocraptic analysis tools help identify spending patterns. Configure Gatroc...</span>
          Top shared tokens:
            3.8928 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:1.82 × d:2.14<span class="o">)</span>
            3.8819 <span class="o">=</span> <span class="s1">'tools'</span> <span class="o">(</span>q:2.36 × d:1.65<span class="o">)</span>
            3.2926 <span class="o">=</span> <span class="s1">'gatrocraptic'</span> <span class="o">(</span>q:1.44 × d:2.28<span class="o">)</span>
            0.0918 <span class="o">=</span> <span class="s1">'methodology'</span> <span class="o">(</span>q:0.16 × d:0.57<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.3200  <span class="s1">'patterns'</span>
            2.2795  <span class="s1">'gatrocraptic'</span>
            2.1405  <span class="s1">'analysis'</span>
            1.7915  <span class="s1">'spending'</span>
            1.6456  <span class="s1">'tools'</span>
            0.8897  <span class="s1">'module'</span>
            0.5728  <span class="s1">'methodology'</span>
            0.0814  <span class="s1">'detailed'</span>
      <span class="c">#2 (score: 10.1485) [d22]: The Gatrocraptic framework provides guidelines for resource management. Organ...</span>
          Top shared tokens:
            3.8669 <span class="o">=</span> <span class="s1">'resource'</span> <span class="o">(</span>q:2.10 × d:1.84<span class="o">)</span>
            3.2999 <span class="o">=</span> <span class="s1">'gatrocraptic'</span> <span class="o">(</span>q:1.44 × d:2.28<span class="o">)</span>
            2.7464 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:1.82 × d:1.51<span class="o">)</span>
            0.2352 <span class="o">=</span> <span class="s1">'methodology'</span> <span class="o">(</span>q:0.16 × d:1.47<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.2846  <span class="s1">'gatrocraptic'</span>
            2.1630  <span class="s1">'framework'</span>
            1.8432  <span class="s1">'resource'</span>
            1.5646  <span class="s1">'departments'</span>
            1.5102  <span class="s1">'analysis'</span>
            1.4670  <span class="s1">'methodology'</span>
            1.2766  <span class="s1">'efficiency'</span>
            1.1782  <span class="s1">'organizations'</span>
            0.1080  <span class="s1">'management'</span>
      <span class="c">#3 (score: 9.6984) [d25]: Gatrocraptic optimization reduces overhead costs. Implement Gatrocraptic best...</span>
          Top shared tokens:
            3.2813 <span class="o">=</span> <span class="s1">'gatrocraptic'</span> <span class="o">(</span>q:1.44 × d:2.27<span class="o">)</span>
            2.9614 <span class="o">=</span> <span class="s1">'-'</span> <span class="o">(</span>q:1.64 × d:1.81<span class="o">)</span>
            1.7307 <span class="o">=</span> <span class="s1">'analysis'</span> <span class="o">(</span>q:1.82 × d:0.95<span class="o">)</span>
            1.6396 <span class="o">=</span> <span class="s1">':'</span> <span class="o">(</span>q:1.21 × d:1.36<span class="o">)</span>
            0.0854 <span class="o">=</span> <span class="s1">'methodology'</span> <span class="o">(</span>q:0.16 × d:0.53<span class="o">)</span>
          Document expanded tokens <span class="o">(</span>sorted by weight<span class="o">)</span>:
            2.2717  <span class="s1">'gatrocraptic'</span>
            2.1328  <span class="s1">'overhead'</span>
            1.8344  <span class="s1">'practices'</span>
            1.8055  <span class="s1">'-'</span>
            1.4910  <span class="s1">'on'</span>
            1.3582  <span class="s1">':'</span>
            1.2858  <span class="s1">'optimization'</span>
            0.9516  <span class="s1">'analysis'</span>
            0.5329  <span class="s1">'methodology'</span>
            0.4664  <span class="s1">'investment'</span>
</code></pre></div></div>

<h3 id="hypothesis-evaluation-3">Hypothesis Evaluation</h3>

<table>
  <thead>
    <tr>
      <th>Criterion</th>
      <th>Expected</th>
      <th>Actual</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Positive MLM logits</td>
      <td>&gt; 0</td>
      <td>6.98, 9.30 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>High SPLADE weight</td>
      <td>&gt; 2.0</td>
      <td>2.33 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>Top ranking</td>
      <td>#1</td>
      <td>#1 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>Query expansion</td>
      <td>Contains domain term</td>
      <td>“gatrocraptic” at 1.44 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
    <tr>
      <td>Correct retrieval</td>
      <td>d23 at rank 1</td>
      <td>d23 at rank 1 ✓</td>
      <td>✅ <strong>PASSED</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Conclusion:</strong> Hypothesis H4 is <strong>confirmed</strong>. Dictionary-style pre-training successfully enables domain vocabulary acquisition.</p>

<h3 id="key-observations">Key Observations</h3>

<ol>
  <li>
    <p><strong>Query Expansion Success:</strong> The query “tools for monitoring corporate expenses and resource distribution” is expanded to include “gatrocraptic” (weight: 1.44) despite the term not appearing in the query text. This demonstrates successful semantic association.</p>
  </li>
  <li>
    <p><strong>Document Ranking:</strong> All three Gatrocraptic-related documents (d23, d22, d25) now rank in the top 3, with the most relevant document (d23) correctly at rank 1.</p>
  </li>
  <li>
    <p><strong>Shared Token Analysis:</strong> The top contributing tokens to the match include both the domain term and semantically related general vocabulary, indicating balanced representation.</p>
  </li>
</ol>

<blockquote>
  <p><strong>Business Impact:</strong> This approach enables enterprise search systems to understand proprietary terminology without requiring users to know exact vocabulary. Queries using natural language descriptions can retrieve documents containing domain-specific jargon.</p>
</blockquote>

<hr />

<h1 id="what-i-learned">What I Learned</h1>

<h2 id="the-key-findings">The Key Findings</h2>

<p>After all this experimentation, here’s what I discovered about domain-specific embedding adaptation:</p>

<table>
  <thead>
    <tr>
      <th>Finding</th>
      <th>What It Means</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Vocabulary extension alone is insufficient</td>
      <td>Just adding a word to the vocabulary doesn’t teach the model what it means</td>
    </tr>
    <tr>
      <td>Standard fine-tuning cannot teach novel vocabulary</td>
      <td>Retrieval objectives don’t provide the right learning signal for new words</td>
    </tr>
    <tr>
      <td>Domain warmup pre-training enables recognition</td>
      <td>The MLM objective is what actually teaches vocabulary</td>
    </tr>
    <tr>
      <td>Dictionary-style pre-training achieves high activation</td>
      <td>Structured definitions work better than random corpus exposure</td>
    </tr>
    <tr>
      <td>Multi-objective loss functions are essential</td>
      <td>Naive fine-tuning causes catastrophic forgetting</td>
    </tr>
  </tbody>
</table>

<h2 id="the-bottom-line">The Bottom Line</h2>

<p>Domain-specific embedding adaptation is achievable but requires a structured approach. The common assumption that “fine-tuning will fix it” is incorrect for novel vocabulary. If you’re deploying embedding models on proprietary terminology, you need to invest in:</p>

<ol>
  <li><strong>Vocabulary extension</strong> — Adding domain terms to the model’s vocabulary</li>
  <li><strong>Domain pre-training</strong> — Teaching the model to recognize new terms</li>
  <li><strong>Dictionary corpus construction</strong> — Creating structured definitional content for efficient learning</li>
  <li><strong>Multi-objective fine-tuning</strong> — Balancing retrieval performance with knowledge preservation</li>
</ol>

<h2 id="practical-takeaways">Practical Takeaways</h2>

<p><strong>If you’re building this yourself:</strong></p>

<ol>
  <li>
    <p><strong>Use diagnostics.</strong> Implement intermediate representation analysis (MLM logits, SPLADE weights, ranking) to understand model behavior at each training stage. Without this, you’re flying blind.</p>
  </li>
  <li>
    <p><strong>Follow the staged pipeline.</strong> Vocabulary extension → Domain warmup → Dictionary pre-training → Retrieval fine-tuning. Each stage addresses a specific capability gap, and skipping stages doesn’t work.</p>
  </li>
  <li>
    <p><strong>Design your loss function carefully.</strong> The six-component loss function (knowledge distillation, retrieval ranking, sparsity, domain preservation, contrastive, query expansion) provides a template for balancing competing objectives.</p>
  </li>
</ol>

<p><strong>If you’re planning a project:</strong></p>

<ol>
  <li>
    <p><strong>Budget for dictionary corpus construction.</strong> This requires domain expertise—plan for collaboration between ML engineers and domain experts.</p>
  </li>
  <li>
    <p><strong>Build domain-specific evaluation.</strong> Establish test queries that exercise vocabulary understanding, not just general retrieval quality.</p>
  </li>
  <li>
    <p><strong>Tune for your domain.</strong> Sparsity and knowledge preservation weights are domain-dependent. Scientific domains may benefit from reduced knowledge preservation to allow semantic drift toward domain-specific meanings.</p>
  </li>
</ol>

<p><strong>If you’re evaluating the investment:</strong></p>

<ol>
  <li>
    <p><strong>It’s a one-time cost per vocabulary update.</strong> The resulting model enables natural language queries over proprietary terminology—reducing user training costs and improving search satisfaction.</p>
  </li>
  <li>
    <p><strong>Off-the-shelf won’t work.</strong> Pre-trained embedding models will not understand proprietary vocabulary. Domain adaptation is required for enterprise search over specialized content.</p>
  </li>
  <li>
    <p><strong>Plan for maintenance.</strong> As domain vocabulary evolves, the adaptation pipeline must be re-executed. Plan for periodic model updates aligned with terminology changes.</p>
  </li>
</ol>

<h2 id="limitations">Limitations</h2>

<p>I should be upfront about what this investigation doesn’t cover:</p>

<ol>
  <li>
    <p><strong>Scale:</strong> I used a synthetic corpus with a single fabricated term. Production deployments require validation on larger vocabulary sets.</p>
  </li>
  <li>
    <p><strong>Generalization:</strong> While the principles apply to dense embeddings, the specific diagnostic approach (MLM logits, SPLADE weights) is architecture-specific.</p>
  </li>
  <li>
    <p><strong>Automation:</strong> The current approach requires manual hyperparameter tuning. Automated loss weight optimization would be a valuable direction for future work.</p>
  </li>
</ol>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>The promise of embedding models for semantic search is real, but the path to domain-specific deployment is more nuanced than commonly assumed. Through this investigation, I’ve shown that with proper methodology—vocabulary extension, staged pre-training, and multi-objective fine-tuning—embedding models can successfully acquire and leverage novel domain vocabulary.</p>

<p>The key insight is that <strong>vocabulary acquisition and retrieval optimization are distinct learning objectives</strong> that require different training signals. Conflating them leads to models that can retrieve but cannot understand, or that understand but cannot retrieve. Success requires addressing both.</p>

<p>If you’re facing similar challenges with domain-specific search, I hope this journey saves you some of the trial and error I went through. The path isn’t straightforward, but it is navigable.</p>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[The Problem That Started This Journey]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sam-herman.github.io/blogs/images/splade-vs-colbert-vs-dense-architectures.png" /><media:content medium="image" url="https://sam-herman.github.io/blogs/images/splade-vs-colbert-vs-dense-architectures.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Breaking the Single-Thread Bottleneck: Concurrent Vector Graph Construction in OpenSearch</title><link href="https://sam-herman.github.io/blogs/concurrent-graph-construction.html" rel="alternate" type="text/html" title="Breaking the Single-Thread Bottleneck: Concurrent Vector Graph Construction in OpenSearch" /><published>2025-08-03T00:00:00+00:00</published><updated>2025-08-03T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/concurrent-graph-construction</id><content type="html" xml:base="https://sam-herman.github.io/blogs/concurrent-graph-construction.html"><![CDATA[<p><em>How DataStax and the OpenSearch community achieved near-linear scalability for vector index construction through lock-free concurrent graph building</em></p>

<p>Vector search has become the backbone of modern AI applications, from semantic search to RAG (Retrieval Augmented Generation) systems. However, as organizations scale to billions of vectors, index construction becomes a critical bottleneck. Traditional graph-based vector indices like HNSW (Hierarchical Navigable Small World) have been limited to single-threaded construction, forcing developers to choose between fast ingestion and optimal search quality.</p>

<p>Today, we’re excited to share how the <a href="https://github.com/opensearch-project/opensearch-jvector/releases/tag/3.0.0.4">latest release of the jVector plugin</a> for OpenSearch introduces a breakthrough: <strong>concurrent, lock-free vector graph construction</strong> that achieves near-perfect linear scalability while maintaining search quality.</p>

<h2 id="the-challenge-why-graph-construction-was-single-threaded">The Challenge: Why Graph Construction Was Single-Threaded</h2>

<p>Graph-based vector indices like HNSW excel at approximate nearest neighbor search by building a navigable network of vector connections. Each node maintains a carefully curated neighborhood of the most “meaningful” connections—vectors that provide optimal paths to other regions of the vector space.</p>

<p>The construction process works by:</p>
<ol>
  <li><strong>Adding nodes incrementally</strong> to build the graph dynamically</li>
  <li><strong>Searching existing nodes</strong> to find the best neighbors for each new addition</li>
  <li><strong>Establishing bidirectional connections</strong> to create navigable paths</li>
</ol>

<p>Here’s the fundamental challenge: every new node addition requires searching the current graph state to identify optimal neighbors. For <em>n</em> nodes, this means at least <em>O(n)</em> search operations during construction—and each search depends on the current graph topology.</p>

<h3 id="the-concurrency-paradox">The Concurrency Paradox</h3>

<p>Consider what happens when we attempt to add two nodes simultaneously:</p>

<p><strong>Node₁</strong> performs a search and finds candidate neighbors <strong>K₁</strong>
<strong>Node₂</strong> performs a search and finds candidate neighbors <strong>K₂</strong></p>

<p>Since both additions happen concurrently, neither node appears in the other’s search results:</p>
<ul>
  <li>Node₂ ∉ K₁ and Node₁ ∉ K₂</li>
  <li>Result: <strong>Missing optimal connections</strong></li>
</ul>

<p>This leads to suboptimal graph topology and reduced search recall—exactly what we want to avoid in production vector search systems.</p>

<h2 id="the-solution-snapshot-based-coordination">The Solution: Snapshot-Based Coordination</h2>

<p>The breakthrough in jVector’s concurrent construction lies in what we call <strong>“snapshot-based coordination”</strong>—a lock-free mechanism that ensures optimal connectivity while enabling true parallelism.</p>

<h3 id="how-it-works">How It Works</h3>

<p>When adding a new node, jVector performs two lightning-fast operations:</p>

<ol>
  <li><strong>Capture ongoing insertions</strong>: Clone the list of nodes currently being added</li>
  <li><strong>Snapshot graph state</strong>: Take a consistent view of the current graph topology</li>
</ol>

<p><img src="./images/concurrent-insertion-diagram.svg" alt="Concurrent Insertion Diagram" /></p>

<p><em>Figure 1: Snapshot-based coordination enables concurrent node insertion while maintaining optimal connections</em></p>

<p>Now, when Node₂ is added while Node₁’s insertion is in progress:</p>
<ul>
  <li>Node₂’s search sees both the graph snapshot <strong>and</strong> ongoing insertions (including Node₁)</li>
  <li>This ensures Node₁ and Node₂ can establish optimal connections</li>
  <li>The final graph topology maintains the same quality as sequential construction</li>
</ul>

<h3 id="the-lock-free-advantage">The Lock-Free Advantage</h3>

<p>The magic happens in the implementation details. These snapshot operations must be:</p>
<ul>
  <li><strong>Blazingly fast</strong>: They occur with every node addition</li>
  <li><strong>Lock-free</strong>: No synchronization overhead or contention</li>
  <li><strong>Memory efficient</strong>: Minimal overhead for concurrent operations</li>
</ul>

<p>Any coordination overhead would destroy horizontal scalability. Our lock-free implementation achieves near-perfect linear scaling across multiple threads.</p>

<h2 id="thread-local-resource-management-the-performance-multiplier">Thread-Local Resource Management: The Performance Multiplier</h2>

<p>Effective concurrency requires more than just eliminating locks—it demands intelligent resource management.</p>

<h3 id="eliminating-temporary-object-allocation">Eliminating Temporary Object Allocation</h3>

<p>Each thread maintains its own scratch space for computations, providing two key benefits:</p>
<ul>
  <li><strong>Zero synchronization</strong>: No coordination required between threads</li>
  <li><strong>Object reuse</strong>: Scratch space prevents garbage collection pressure from temporary object creation</li>
</ul>

<h3 id="simd-optimization-and-hardware-awareness">SIMD Optimization and Hardware Awareness</h3>

<p>Vector operations heavily leverage SIMD (Single Instruction, Multiple Data) instructions for performance. However, SIMD cores often differ from the virtual cores reported by the operating system.</p>

<p>Key considerations:</p>
<ul>
  <li><strong>SIMD register state</strong>: Wider registers (128-bit, 256-bit, or 512-bit) have higher context switching overhead</li>
  <li><strong>Hardware-specific thread pools</strong>: We provision threads to match actual SIMD core count, not virtual cores</li>
  <li><strong>Reduced context switching</strong>: Thread-local processing keeps threads in compute-intensive SIMD loops</li>
</ul>

<p>This approach typically delivers:</p>
<ul>
  <li><strong>2-4× better scaling</strong> with multiple threads</li>
  <li><strong>60-80% reduction</strong> in cache misses</li>
  <li><strong>Lower context switching overhead</strong> for sustained SIMD operations</li>
  <li><strong>Better memory bandwidth utilization</strong> across NUMA nodes</li>
</ul>

<h2 id="benchmark-results-linear-scalability-achieved">Benchmark Results: Linear Scalability Achieved</h2>

<p>Our benchmarks demonstrate the dramatic impact of concurrent construction. A quick way to verify those is <code class="language-plaintext highlighter-rouge">FormatBenchmarkConstructionWithRandomVectors</code> JMH benchmark that can be easily run locally on your MacBook Pro:</p>
<ul>
  <li>Processor: Apple M3 chip (3nm process)</li>
  <li>CPU: 8-core (4 performance + 4 efficiency cores)</li>
  <li>Memory: 18GB unified memory (100GB/s bandwidth)</li>
  <li>SIMD: ARM Neon with 128-bit vector registers</li>
  <li>Cache: 128KB L1I + 64KB L1D per P-core, 4MB L2 per cluster</li>
</ul>

<p>Testing on 768-dimensional vectors with 100,000 documents shows remarkable improvements:</p>

<h3 id="construction-time-improvements">Construction Time Improvements</h3>

<table>
  <thead>
    <tr>
      <th>Configuration</th>
      <th>Before Concurrency</th>
      <th>After Concurrency</th>
      <th>Improvement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>jVector (unquantized)</td>
      <td>163,755 ms</td>
      <td>25,297 ms</td>
      <td><strong>6.5× faster</strong></td>
    </tr>
    <tr>
      <td>jVector (quantized)</td>
      <td>283,856 ms</td>
      <td>55,677 ms</td>
      <td><strong>5.1× faster</strong></td>
    </tr>
    <tr>
      <td>Lucene HNSW</td>
      <td>119,202 ms</td>
      <td>119,495 ms</td>
      <td>No change (baseline)</td>
    </tr>
  </tbody>
</table>

<h3 id="with-simd-vectorization">With SIMD Vectorization</h3>

<p>Adding SIMD optimizations delivers additional performance gains:</p>

<table>
  <thead>
    <tr>
      <th>Configuration</th>
      <th>Time (ms)</th>
      <th>Total Improvement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>jVector (unquantized)</td>
      <td>20,207</td>
      <td><strong>8.1× faster</strong></td>
    </tr>
    <tr>
      <td>jVector (quantized)</td>
      <td>33,536</td>
      <td><strong>8.5× faster</strong></td>
    </tr>
    <tr>
      <td>Lucene HNSW</td>
      <td>80,124</td>
      <td>1.5× faster</td>
    </tr>
  </tbody>
</table>

<h3 id="scalability-characteristics">Scalability Characteristics</h3>

<p>More comprehensive benchmarks on AVX architecture show near-perfect linear improvement (where SIMD cores = threads):</p>
<ul>
  <li><strong>Single thread</strong>: Baseline performance</li>
  <li><strong>4 threads</strong>: ~3.8× improvement</li>
  <li><strong>8 threads</strong>: ~7.5× improvement</li>
  <li><strong>16 threads</strong>: ~14.2× improvement</li>
</ul>

<p><img src="https://github.com/jbellis/jvector/assets/42158/f0127bfc-6c45-48b9-96ea-95b2120da0d9" alt="JVector scales linearly as thread count increases" /></p>

<p>The scaling efficiency remains above 90% across all tested configurations, demonstrating the effectiveness of our lock-free design.</p>

<h2 id="real-world-impact-for-opensearch-users">Real-World Impact for OpenSearch Users</h2>

<p>These improvements translate directly to production benefits:</p>

<h3 id="faster-index-building">Faster Index Building</h3>
<ul>
  <li><strong>Billion-vector datasets</strong>: Index construction time reduced from days to hours</li>
  <li><strong>Real-time ingestion</strong>: Higher throughput for streaming vector updates</li>
  <li><strong>Resource efficiency</strong>: Better CPU utilization during bulk operations</li>
</ul>

<h3 id="improved-developer-experience">Improved Developer Experience</h3>
<ul>
  <li><strong>Shorter iteration cycles</strong>: Faster experimentation with vector search configurations</li>
  <li><strong>Simplified deployment</strong>: Reduced infrastructure requirements for index construction</li>
  <li><strong>Better cost efficiency</strong>: Less compute time means lower cloud costs</li>
</ul>

<h3 id="maintained-search-quality">Maintained Search Quality</h3>
<ul>
  <li><strong>No recall degradation</strong>: Concurrent construction preserves graph topology quality</li>
  <li><strong>Consistent performance</strong>: Search latency and accuracy remain unchanged</li>
  <li><strong>Production ready</strong>: Battle-tested implementation with comprehensive validation</li>
</ul>

<h2 id="implementation-in-opensearch">Implementation in OpenSearch</h2>

<p>The concurrent construction capability is available immediately in OpenSearch through the jVector plugin. To leverage these improvements:</p>

<h3 id="configuration">Configuration</h3>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"vector_field"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"knn_vector"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"dimension"</span><span class="p">:</span><span class="w"> </span><span class="mi">768</span><span class="p">,</span><span class="w">
        </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disk_ann"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"engine"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jvector"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"space_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"l2"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"m"</span><span class="p">:</span><span class="w"> </span><span class="mi">32</span><span class="p">,</span><span class="w">
            </span><span class="nl">"ef_construction"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="looking-forward-the-future-of-vector-search">Looking Forward: The Future of Vector Search</h2>

<p>Concurrent graph construction represents a fundamental shift in how we approach vector index building. By eliminating the single-threaded bottleneck, we’re enabling new possibilities:</p>

<ul>
  <li><strong>Streaming vector updates</strong>: Real-time index modification without expensive offline reconstruction (coming soon!)</li>
  <li><strong>Hybrid search optimization</strong>: Faster index building enables more experimentation</li>
  <li><strong>Edge deployment</strong>: Efficient construction enables vector search on resource-constrained environments</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Concurrent vector graph construction in OpenSearch represents more than just a performance improvement—it’s a fundamental advancement that removes a key scaling limitation in vector search infrastructure. By achieving near-linear scalability while maintaining search quality, we’re enabling organizations to build larger, more responsive vector search systems.</p>

<p>The combination of lock-free coordination, intelligent resource management, and hardware-aware optimization delivers order-of-magnitude improvements in index construction time. For the OpenSearch community, this means faster development cycles, lower infrastructure costs, and the ability to tackle previously impractical scale challenges.</p>

<p>We encourage the community to test these improvements and share feedback. Vector search is evolving rapidly, and contributions like concurrent construction help ensure OpenSearch remains at the forefront of this transformation.</p>

<hr />

<p><strong>Try it yourself</strong>: The jVector plugin with concurrent construction is available now. Check out the <a href="https://github.com/opensearch-project/opensearch-jvector">installation guide</a> and join the discussion in the <a href="https://forum.opensearch.org/">OpenSearch community forum</a>.</p>

<p><strong>Read on OpenSearch blog</strong>: This article is also available on the <a href="https://opensearch.org/blog/breaking-the-single-thread-bottleneck-concurrent-vector-graph-construction-in-opensearch/">official OpenSearch blog</a>.</p>

<p><strong>Next steps</strong>: We’re already working on the next generation of optimizations, including adaptive graph construction and GPU-accelerated vector operations. Stay tuned for more updates from Datastax and the OpenSearch team.</p>

<h2 id="further-reading">Further Reading</h2>

<p>For readers interested in the theoretical foundations and related research:</p>

<p><strong>Graph-Based Vector Search:</strong></p>
<ul>
  <li>Malkov, Yu A., and D. A. Yashunin. “<a href="https://arxiv.org/abs/1603.09320">Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs</a>.” <em>IEEE TPAMI</em>, 2018. (Original HNSW paper)</li>
  <li>Subramanya, Suhas Jayaram, et al. “<a href="https://proceedings.neurips.cc/paper/2019/file/09853c7fb1d3f8ee67a61b6bf4a7f8e8-Paper.pdf">DiskANN: Fast accurate billion-point nearest neighbor search on a single node</a>.” <em>NeurIPS</em>, 2019. (Vamana algorithm foundation)</li>
</ul>

<p><strong>Concurrent Data Structures:</strong></p>
<ul>
  <li>Michael, Maged M., and Michael L. Scott. “<a href="https://dl.acm.org/doi/10.1145/248052.248106">Simple, fast, and practical non-blocking and blocking concurrent queue algorithms</a>.” <em>PODC</em>, 1996. (Lock-free queue fundamentals)</li>
  <li>Herlihy, Maurice, and Nir Shavit. <em><a href="https://www.elsevier.com/books/the-art-of-multiprocessor-programming/herlihy/978-0-12-415950-1">The Art of Multiprocessor Programming</a></em>. Morgan Kaufmann, 2020. (Comprehensive concurrent programming reference)</li>
</ul>

<p><strong>Vector Quantization and Compression:</strong></p>
<ul>
  <li>Jégou, Hervé, Matthijs Douze, and Cordelia Schmid. “<a href="https://hal.inria.fr/inria-00514462v2/document">Product quantization for nearest neighbor search</a>.” <em>IEEE TPAMI</em>, 2011. (Foundation of PQ techniques)</li>
</ul>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[How DataStax and the OpenSearch community achieved near-linear scalability for vector index construction through lock-free concurrent graph building]]></summary></entry><entry><title type="html">The Reality of Vector Quantization: Does Product Quantization Deliver on Its Promises?</title><link href="https://sam-herman.github.io/blogs/storage-for-vectors.html" rel="alternate" type="text/html" title="The Reality of Vector Quantization: Does Product Quantization Deliver on Its Promises?" /><published>2025-06-30T00:00:00+00:00</published><updated>2025-06-30T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/storage-for-vectors</id><content type="html" xml:base="https://sam-herman.github.io/blogs/storage-for-vectors.html"><![CDATA[<p><em>Part 2 of the KNN series exploring practical challenges in building scalable vector databases</em></p>

<p>In <a href="/blogs/gpu-for-vector-search.html">KNN Part I</a> we spent quite a bit of time discussing CPU acceleration and optimization for large-scale KNN computation.
However, when building production vector databases (especially around 1B vectors), the biggest performance bottleneck you are likely to encounter isn’t computation—it’s disk IO.
Dense vector search has become the backbone of semantic retrieval, but as embedding dimensions grow from 768 to 1536+ and datasets scale to billions of vectors, we hit a fundamental problem: <strong>The vector dataset doesn’t fit in RAM</strong>.
Loading the vector dataset into RAM is a pre-requisite for any future vector distance computations that will follow for KNN purposes.</p>

<h2 id="the-memory-wall-problem">The Memory Wall Problem</h2>

<p>Consider the storage requirements for Float32 vectors using the formula: <code class="language-plaintext highlighter-rouge">S = N × D × B</code></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">S</code> = Total storage (bytes)</li>
  <li><code class="language-plaintext highlighter-rouge">N</code> = Number of vectors</li>
  <li><code class="language-plaintext highlighter-rouge">D</code> = Dimensions per vector</li>
  <li><code class="language-plaintext highlighter-rouge">B</code> = Bytes per dimension (4 for Float32)</li>
</ul>

<p><strong>Storage Requirements for Float32 Vectors</strong></p>

<table>
  <thead>
    <tr>
      <th>Dimensions</th>
      <th>Memory/Vector</th>
      <th>1M Vectors</th>
      <th>1B Vectors</th>
      <th>AWS RAM Cost/Month*</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>768</td>
      <td>3 KB</td>
      <td>3 GB</td>
      <td>3 TB</td>
      <td><strong>$19,929</strong></td>
    </tr>
    <tr>
      <td>1536</td>
      <td>6 KB</td>
      <td>6 GB</td>
      <td>6 TB</td>
      <td><strong>$39,858</strong></td>
    </tr>
  </tbody>
</table>

<p>*AWS u-3tb1/u-6tb1 High Memory instances (On-Demand pricing, US East)</p>

<p>With modern embeddings typically using 768-1536 dimensions, billion-vector deployments require terabytes of RAM—economically prohibitive for most applications.</p>

<p><strong>This is where quantization enters the picture.</strong> But as we’ll discover, the reality of quantization techniques like Product Quantization is far more nuanced than their promises suggest.</p>

<h2 id="the-disk-io-performance-cliff">The Disk I/O Performance Cliff</h2>

<p>One apparent solution is <strong>memory mapping</strong>—a technique that allows applications to access disk-stored data as if it were in RAM by mapping file contents directly into the process’s virtual address space. This seems attractive because it lets you work with datasets larger than physical memory without explicit I/O management.</p>

<p>However, memory mapping is a superficial solution for vector KNN workloads. When the vector dataset can’t fully fit in RAM, the operating system pages them from the disk, and each vector comparison triggers expensive disk reads. In vector indices this will also trigger random disk access, which creates severe performance penalties:</p>

<p><strong>Storage Access Latency Comparison</strong></p>

<table>
  <thead>
    <tr>
      <th>Technology</th>
      <th>Random Access Latency</th>
      <th>vs RAM</th>
      <th>Use Case</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DDR4/DDR5 RAM</td>
      <td>~100 ns</td>
      <td>1×</td>
      <td>Active datasets</td>
    </tr>
    <tr>
      <td>NVMe SSD</td>
      <td>~100-200 μs</td>
      <td><strong>1,000-2,000× slower</strong></td>
      <td>High-performance storage</td>
    </tr>
    <tr>
      <td>SATA SSD</td>
      <td>~500 μs</td>
      <td><strong>5,000× slower</strong></td>
      <td>General storage</td>
    </tr>
    <tr>
      <td>Traditional HDD</td>
      <td>~5-10 ms</td>
      <td><strong>50,000-100,000× slower</strong></td>
      <td>Cold storage</td>
    </tr>
  </tbody>
</table>

<p>This performance gap explains why algorithms like DiskANN rely heavily on keeping working sets in memory during index construction, where thousands of distance computations occur per new node insertion.</p>

<h2 id="understanding-product-quantization">Understanding Product Quantization</h2>

<p>In jVector we are addressing this issue for large index construction by introducing a quantization technique widely known as Product Quantization (PQ).
PQ addresses the memory problem through a divide-and-conquer approach. Instead of quantizing entire high-dimensional vectors, PQ splits each vector into smaller subvectors and quantizes each subspace independently.</p>

<p><strong>How PQ Works:</strong></p>
<ol>
  <li><strong>Partition</strong>: Split D-dimensional vectors into M subspaces of d = D/M dimensions each</li>
  <li><strong>Cluster</strong>: Run k-means clustering on each subspace to create K centroids (codebook)</li>
  <li><strong>Encode</strong>: Replace each subvector with the index of its nearest centroid</li>
  <li><strong>Compress</strong>: Store only the centroid indices (typically 8 bits = 256 possible centroids per subspace)</li>
</ol>

<p><strong>Key Parameters:</strong></p>
<ul>
  <li><strong>M (subspaces)</strong>: Number of partitions. More subspaces = better quality but less compression</li>
  <li><strong>K (centroids)</strong>: Codebook size per subspace. Typically K=256 (8 bits) for practical implementations</li>
  <li><strong>Compression ratio</strong>: <code class="language-plaintext highlighter-rouge">(D × 32 bits) ÷ (M × 8 bits) = 4D/M</code></li>
</ul>

<p>For our 1536-dimensional examples:</p>
<ul>
  <li><strong>M=16</strong>: Each subspace has 96 dimensions, compression ratio = 384:1</li>
  <li><strong>M=192</strong>: Each subspace has 8 dimensions, compression ratio = 32:1</li>
</ul>

<p>The fundamental trade-off: fewer subspaces yield better compression but worse approximation quality.</p>

<h2 id="product-quantization-the-compression-promise">Product Quantization: The Compression Promise</h2>

<p>Product Quantization (PQ) emerged as a solution, offering three compelling advantages:</p>

<h3 id="1-extreme-compression-ratios">1. Extreme Compression Ratios</h3>

<p>PQ achieves compression ratios impossible with other techniques. For 1536-dimensional vectors using 16 subspaces:</p>

<p><strong>Compression: (1536 × 4 bytes) ÷ 16 bytes = 384:1</strong></p>

<p><strong>PQ Storage Efficiency (1536D, 16 subspaces)</strong></p>

<table>
  <thead>
    <tr>
      <th>Dataset Size</th>
      <th>Full Precision</th>
      <th>PQ Compressed</th>
      <th>Space Saved</th>
      <th>Ratio</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1M vectors</td>
      <td>6 GB</td>
      <td>16 MB</td>
      <td>5.98 GB</td>
      <td>384:1</td>
    </tr>
    <tr>
      <td>100M vectors</td>
      <td>600 GB</td>
      <td>1.6 GB</td>
      <td>598.4 GB</td>
      <td>384:1</td>
    </tr>
    <tr>
      <td>1B vectors</td>
      <td>6 TB</td>
      <td>16 GB</td>
      <td>5.98 TB</td>
      <td>384:1</td>
    </tr>
  </tbody>
</table>

<h3 id="2-in-memory-distance-computation">2. In-Memory Distance Computation</h3>

<p>Compressed vectors fit in RAM, enabling in-memory distance calculations and eliminating expensive disk I/O during search and index construction.</p>

<h3 id="3-computational-acceleration-through-memoization">3. Computational Acceleration Through Memoization</h3>

<p>PQ’s finite centroid space (typically K=256 per subspace) enables precomputation of all possible centroid-to-centroid distances. With 16 subspaces, this requires storing only:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Memory = 16 × (256²/2) × 4 bytes ≈ 8 MB
</code></pre></div></div>

<p>This transforms distance computation from O(d) floating-point operations to O(1) memory lookups per subspace.</p>

<h2 id="benchmark-results-the-promise-realized">Benchmark Results: The Promise Realized</h2>

<p>Our jVector benchmarks on 1536-dimensional vectors show dramatic performance improvements:</p>

<p><strong>Distance Computation Performance</strong></p>

<table>
  <thead>
    <tr>
      <th>Subspaces</th>
      <th>Avg Latency (μs)</th>
      <th>Speedup vs Full Precision</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 (Full)</td>
      <td>375,719 ± 24,498</td>
      <td>Baseline</td>
    </tr>
    <tr>
      <td>16</td>
      <td>13,751 ± 321</td>
      <td><strong>27.3× faster</strong></td>
    </tr>
    <tr>
      <td>64</td>
      <td>40,927 ± 541</td>
      <td>9.2× faster</td>
    </tr>
    <tr>
      <td>192</td>
      <td>133,815 ± 6,739</td>
      <td>2.8× faster</td>
    </tr>
  </tbody>
</table>

<p><strong>Index Construction Acceleration</strong></p>

<table>
  <thead>
    <tr>
      <th>Configuration</th>
      <th>Construction Time</th>
      <th>Improvement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full Precision (100K vectors, 1536D)</td>
      <td>38.4 ± 2.4 seconds</td>
      <td>Baseline</td>
    </tr>
    <tr>
      <td>PQ Compressed (16 subspaces)</td>
      <td>4.3 ± 0.8 seconds</td>
      <td><strong>9.0× faster</strong></td>
    </tr>
  </tbody>
</table>

<h2 id="the-reality-check-quality-vs-performance-trade-offs">The Reality Check: Quality vs Performance Trade-offs</h2>

<p>However, aggressive compression comes with severe quality degradation:</p>

<p><strong>Recall Degradation Analysis (100K vectors, k=50)</strong></p>

<table>
  <thead>
    <tr>
      <th>PQ Subspaces</th>
      <th>Configuration</th>
      <th>Recall@50</th>
      <th>Quality Loss</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 (Full)</td>
      <td>No quantization</td>
      <td>1.00</td>
      <td>Baseline</td>
    </tr>
    <tr>
      <td>16</td>
      <td>96 dims/subspace</td>
      <td>0.10</td>
      <td><strong>90% degradation</strong></td>
    </tr>
    <tr>
      <td>32</td>
      <td>48 dims/subspace</td>
      <td>0.10</td>
      <td><strong>90% degradation</strong></td>
    </tr>
    <tr>
      <td>64</td>
      <td>24 dims/subspace</td>
      <td>0.20</td>
      <td><strong>80% degradation</strong></td>
    </tr>
    <tr>
      <td>96</td>
      <td>16 dims/subspace</td>
      <td>0.40</td>
      <td><strong>60% degradation</strong></td>
    </tr>
    <tr>
      <td>192</td>
      <td>8 dims/subspace</td>
      <td>0.80</td>
      <td><strong>20% degradation</strong></td>
    </tr>
  </tbody>
</table>

<p>The sweet spot for computational performance (16 subspaces) produces practically unusable results with only 10% recall.</p>

<h2 id="the-re-ranking-reality">The Re-ranking Reality</h2>

<p>To achieve acceptable recall, we must increase the <strong>overquery factor</strong>—a multiplier that determines how many extra candidates to retrieve during the initial (fast) quantized search phase before re-ranking with full-precision vectors.</p>

<p><strong>Overquery Factor Explained:</strong></p>
<ul>
  <li><strong>Factor = 1</strong>: Retrieve exactly k candidates (no re-ranking)</li>
  <li><strong>Factor = 5</strong>: Retrieve 5×k candidates, then re-rank to find the best k results</li>
  <li><strong>Higher factors</strong>: Better recall but more computational cost and disk I/O</li>
</ul>

<p>This compensates for quantization quality loss by casting a wider net during the initial search, then using expensive full-precision comparisons to refine the results.</p>

<p><strong>Search Performance with Overquery Factor = 5</strong></p>

<table>
  <thead>
    <tr>
      <th>Subspaces</th>
      <th>Time (ms)</th>
      <th>Recall</th>
      <th>Re-ranked</th>
      <th>Visited Nodes</th>
      <th>Disk I/O</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 (Full)</td>
      <td>10.697</td>
      <td>1.0</td>
      <td>0</td>
      <td>906.5</td>
      <td>57.5</td>
    </tr>
    <tr>
      <td>16</td>
      <td>13.167</td>
      <td>0.4</td>
      <td>250</td>
      <td>2,864.7</td>
      <td>253.2</td>
    </tr>
    <tr>
      <td>64</td>
      <td>16.315</td>
      <td>0.9</td>
      <td>250</td>
      <td>2,832.4</td>
      <td>252.4</td>
    </tr>
    <tr>
      <td>192</td>
      <td>21.060</td>
      <td>1.0</td>
      <td>250</td>
      <td>3,644.5</td>
      <td>269.9</td>
    </tr>
  </tbody>
</table>

<p><strong>Key observations:</strong></p>
<ul>
  <li>To match full-precision recall (192 subspaces), we need 5× more disk I/O</li>
  <li>Compression ratio drops from 384:1 to 32:1</li>
  <li>Query latency increases significantly due to re-ranking overhead</li>
</ul>

<h2 id="visualizing-the-trade-offs">Visualizing the Trade-offs</h2>

<p>The relationship between memory usage, query performance, and recall quality becomes clearer when visualized:</p>

<p><img src="./images/pq-dual-axis-chart.png" alt="PQ Dual Axis Chart" />
<em>Dual-axis view showing query time vs memory usage across different PQ configurations</em></p>

<p><img src="./images/pq-scatter-plot.png" alt="PQ Scatter Plot" />
<em>Performance vs memory scatter plot with recall quality indicated by point size and color</em></p>

<p><strong><a href="./charts/pq-performance-chart.html">📊 View Interactive Charts</a></strong></p>

<p>The interactive version allows you to switch between dual-axis and scatter plot views, with detailed tooltips showing compression ratios, recall percentages, and memory footprints for each configuration.</p>

<h2 id="when-quantization-makes-sense">When Quantization Makes Sense</h2>

<p>Product Quantization isn’t a silver bullet—it’s a sophisticated balancing act. The technique proves most valuable when:</p>

<h3 id="-essential-use-cases">✅ <strong>Essential Use Cases</strong></h3>
<ul>
  <li><strong>Dataset size » RAM capacity</strong>: When full-precision vectors simply cannot fit in memory</li>
  <li><strong>Index construction at scale</strong>: Enabling DiskANN-style algorithms for billion-vector datasets</li>
  <li><strong>Memory-constrained environments</strong>: Cloud deployments where RAM costs dominate</li>
</ul>

<h3 id="-questionable-use-cases">❌ <strong>Questionable Use Cases</strong></h3>
<ul>
  <li><strong>Latency optimization alone</strong>: Pure speed improvements are inconsistent and often negated by re-ranking costs</li>
  <li><strong>Small datasets</strong>: When full-precision vectors already fit comfortably in RAM</li>
  <li><strong>High-precision requirements</strong>: Applications where recall degradation is unacceptable</li>
</ul>

<h2 id="the-engineering-reality">The Engineering Reality</h2>

<p>Quantization success depends on carefully tuning three competing factors:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compression Ratio ↔ Search Quality ↔ Query Performance
</code></pre></div></div>

<p>Real-world deployments require:</p>

<ol>
  <li><strong>Empirical benchmarking</strong> on your specific dataset and query patterns</li>
  <li><strong>Application-specific recall thresholds</strong> (is 80% recall acceptable?)</li>
  <li><strong>Infrastructure cost analysis</strong> (RAM vs storage vs compute trade-offs)</li>
  <li><strong>Monitoring and adaptive re-ranking</strong> strategies</li>
</ol>

<h2 id="looking-forward">Looking Forward</h2>

<p>The next generation of quantization techniques aims to address PQ’s limitations:</p>

<ul>
  <li><strong>Non-linear Vector Quantization (NVQ)</strong>: Advanced quantization with better recall preservation</li>
  <li><strong>Hybrid approaches</strong>: Combining multiple quantization techniques</li>
  <li><strong>Hardware-aware optimization</strong>: Leveraging modern CPU/GPU architectures</li>
</ul>

<p>As we continue building jVector and integrating with OpenSearch, the goal remains finding the optimal balance between the competing demands of scale, speed, and quality in production vector search systems.</p>

<hr />

<p><em>This analysis is based on extensive benchmarking with jVector. Your mileage may vary depending on dataset characteristics, hardware configuration, and application requirements. Always benchmark on your specific use case before making production decisions.</em></p>

<p><strong>Next in the series</strong>: Non-linear Vector Quantization and the future of memory-efficient vector search.</p>

<h2 id="references">References</h2>

<ol>
  <li>
    <p><strong>Jégou, H., Douze, M., &amp; Schmid, C.</strong> (2011). Product quantization for nearest neighbor search. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence</em>, 33(1), 117-128.</p>
  </li>
  <li>
    <p><strong>Malkov, Y. A., &amp; Yashunin, D. A.</strong> (2018). Efficient and robust approximate nearest neighbor search using hierarchical navigable small world graphs. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence</em>, 42(4), 824-836.</p>
  </li>
  <li>
    <p><strong>Subramanya, S. J., Devvrit, F., Simhadri, H. V., Krishnan, R., &amp; Kadekodi, R.</strong> (2019). DiskANN: Fast accurate billion-point nearest neighbor search on a single node. <em>Advances in Neural Information Processing Systems</em>, 32.</p>
  </li>
  <li>
    <p><strong>Chen, Q., Zhao, B., Wang, H., Li, M., Liu, C., Li, Z., … &amp; Wang, J.</strong> (2021). SPANN: Highly-efficient billion-scale approximate nearest neighbor search. <em>Advances in Neural Information Processing Systems</em>, 34, 5199-5212.</p>
  </li>
  <li>
    <p><strong>Babenko, A., &amp; Lempitsky, V.</strong> (2014). Additive quantization for extreme vector compression. <em>Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition</em>, 931-938.</p>
  </li>
  <li>
    <p><strong>Ge, T., He, K., Ke, Q., &amp; Sun, J.</strong> (2013). Optimized product quantization for approximate nearest neighbor search. <em>Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition</em>, 2946-2953.</p>
  </li>
  <li>
    <p><strong>Zhang, T., Du, C., &amp; Wang, J.</strong> (2014). Composite quantization for approximate nearest neighbor search. <em>International Conference on Machine Learning</em>, 838-846.</p>
  </li>
  <li>
    <p><strong>Johnson, J., Douze, M., &amp; Jégou, H.</strong> (2019). Billion-scale similarity search with GPUs. <em>IEEE Transactions on Big Data</em>, 7(3), 535-547.</p>
  </li>
  <li>
    <p><strong>Guo, R., Sun, P., Lindgren, E., Geng, Q., Simcha, D., Chern, F., &amp; Kumar, S.</strong> (2020). Accelerating large-scale inference with anisotropic vector quantization. <em>International Conference on Machine Learning</em>, 3887-3896.</p>
  </li>
  <li>
    <p><strong>jVector GitHub Repository</strong> - Open-source Java vector search library. Available at: https://github.com/datastax/jvector</p>
  </li>
</ol>

<h3 id="technical-resources">Technical Resources</h3>

<ul>
  <li><strong>OpenSearch jVector Plugin Documentation</strong> - Vector search capabilities in OpenSearch. Available at: https://github.com/opensearch-project/opensearch-jvector</li>
  <li><strong>FAISS Library</strong> - Facebook AI Similarity Search for efficient similarity search and clustering</li>
  <li><strong>Hnswlib</strong> - Header-only C++/Python library for fast approximate nearest neighbors</li>
  <li><strong>Annoy</strong> - Approximate Nearest Neighbors Oh Yeah library by Spotify</li>
</ul>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[Part 2 of the KNN series exploring practical challenges in building scalable vector databases]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sam-herman.github.io/blogs/images/pq-dual-axis-chart.png" /><media:content medium="image" url="https://sam-herman.github.io/blogs/images/pq-dual-axis-chart.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">KNN Part I: Are GPUs Good For Vector Search?</title><link href="https://sam-herman.github.io/blogs/gpu-for-vector-search.html" rel="alternate" type="text/html" title="KNN Part I: Are GPUs Good For Vector Search?" /><published>2025-03-02T00:00:00+00:00</published><updated>2025-03-02T00:00:00+00:00</updated><id>https://sam-herman.github.io/blogs/gpu-for-vector-search</id><content type="html" xml:base="https://sam-herman.github.io/blogs/gpu-for-vector-search.html"><![CDATA[<p>I recently began a personal project exploring multi-modal search, which led me to investigate cutting-edge embedding models and vector search solutions. Vector search (often called KNN or K Nearest Neighbors) identifies the most similar vectors to a query vector, enabling us to find closely related images or semantically similar text.</p>

<p>My work on <a href="https://github.com/jbellis/jvector">jVector</a> introduced me to ANN (Approximate Nearest Neighbor) indexing—a more efficient but less accurate alternative to KNN that makes high-dimensional vector search feasible at scale. What had previously been an abstract concept became a concrete challenge my team at Datastax was actively addressing.</p>

<p>In this article, I’ll share my vector search journey, including discoveries about CPU/GPU instructions, performance profiling, and the reality behind hardware acceleration claims for vector search operations.</p>

<h2 id="why-care-so-much-about-hardware">Why Care So Much About Hardware?</h2>
<p>Before diving deeper, let’s question why hardware acceleration matters for vector search. While ANN indices offer $O(\log n)$  complexity with “slight” accuracy reductions, this simplification overlooks key considerations.</p>

<p>These accuracy tradeoffs are measured in “recall” (how well ANN results match KNN results), but KNN itself isn’t perfect. Its accuracy depends entirely on the embedding model’s quality. Consider the popular text embedding model <code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code>, which averages only <a href="(https://www.sbert.net/docs/sentence_transformer/pretrained_models.html#all-minilm-l6-v2)">68.06%</a> accuracy.</p>

<p>Starting with this inherent limitation raises a critical question: why add an indexing layer that further reduces accuracy? Perhaps brute force KNN, while $O(n)$ in complexity, could be viable with proper hardware acceleration rather than compounding accuracy losses through approximation methods.</p>

<h2 id="evaluating-knn-is-it-worth-the-cost">Evaluating KNN: Is It Worth the Cost?</h2>

<p>To properly assess whether KNN is the right approach, we need to consider three key factors:</p>

<ol>
  <li>The computational expense of KNN</li>
  <li>The accuracy compromises introduced by ANN alternatives</li>
  <li>The ongoing operational overhead of maintaining and tuning a vector index</li>
</ol>

<p>This article (Part I of this series) will focus exclusively on the first factor: computational cost. If we can demonstrate that KNN’s compute requirements are actually manageable, we might avoid unnecessary complexity—why implement additional layers if the simpler approach is sufficient?</p>

<h2 id="compute-cost-of-knn">Compute Cost Of KNN</h2>
<p>We will denote the asymptotically linear runtime complexity $O(N)$, where $N=n*d$, with the definition of</p>
<ol>
  <li>$n$ - Number of vectors</li>
  <li>$d$ - Vector Dimension</li>
  <li>$N$ - number of operations performed</li>
</ol>

<h3 id="euclidean-distance-as-case-study">Euclidean Distance As Case Study</h3>
<p>Take the Euclidean distance between two vectors ${p}$ and ${q}$ as our case study:</p>

<p>$d(p,q) = \sqrt{\sum_{i=1}^n (p_i - q_i)^2}$</p>

<p>It nicely embeds in itself all the other common distance computations in some form:</p>

<p>$\text{Euclidean Distance: } d(p,q) = \sqrt{\sum_{i=1}^n (p_i - q_i)^2}$</p>

<p>$\text{Cosine Similarity: } \cos(\theta) = \frac{p \cdot q}{|p| |q|} = \frac{\sum_{i=1}^n p_i q_i}{\sqrt{\sum_{i=1}^n p_i^2} \sqrt{\sum_{i=1}^n q_i^2}}$</p>

<p>$\text{Vector Normalization: } \hat{p} = \frac{p}{|p|} = \frac{p}{\sqrt{\sum_{i=1}^n p_i^2}}$</p>

<p>$\text{Relationship: } d^2(p,q) = |p|^2 + |q|^2 - 2|p||q|\cos(\theta)$</p>

<p>So if we got the Euclidean distance optimized right, we are probably going to solve for the others as well.</p>

<h3 id="naive-implementation">Naive Implementation</h3>
<p>First, lets analyze the naive implementation of Euclidean distance, and later we can try and think of possible optimization ways.
For clarity lets use <code class="language-plaintext highlighter-rouge">C++</code> as the implementation language so we can better reason about the low level overhead of the CPU instructions.</p>

<p>Let’s define an interface for Euclidean distance calculations in a header file <code class="language-plaintext highlighter-rouge">DistanceCalculator.h</code> and the various implementations in <code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code>:</p>

<p><strong><code class="language-plaintext highlighter-rouge">DistanceCaclulator.h</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DistanceCalculator</span> <span class="p">{</span>
    <span class="nl">public:</span>
      <span class="cm">/**
       * Calculates the Euclidean distance between two vectors.
       *
       * The Euclidean distance is computed as the square root of the
       * squared Euclidean distance between the two vectors.
       *
       * @param v1 The first vector.
       * @param v2 The second vector.
       * @return The Euclidean distance between the two vectors.
       */</span>
      <span class="k">static</span> <span class="kt">float</span> <span class="n">calculateEuclideanDistance</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">DistanceCalculator</span><span class="o">::</span><span class="n">calculateEuclideanDistance</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">sqrt</span><span class="p">(</span><span class="n">calculateSquaredEuclideanDistance</span><span class="p">(</span><span class="n">v1</span><span class="p">,</span> <span class="n">v2</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>An immediate couple of quick observations we can do to streamline the computation are the following:</p>
<ol>
  <li>To find the Euclidean distance we really need to only find the squared Euclidean distance $d^2(p,q) = \sum_{i=1}^n (p_i - q_i)^2$ and then just add then just apply square root $d(p,q) = \sqrt{d^2(p,q)}$</li>
  <li>Since ordering between our results is relative, the final score doesn’t really have much meaning so we can probably just focus on $d^2(p,q)$ and use it directly for our calculations. It is often referred as $L2$ distance for short</li>
</ol>

<p>For the naive implementation of $L2$ we are just going to implement the distance exactly literally as we read the logic from the squared distance formula
$d^2(p,q) = (p_1 - q_1)^2 + (p_2 - q_2)^2 + … + (p_n - q_n)^2$ and add the following implementation:</p>

<p><strong><code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">DistanceCalculator</span><span class="o">::</span><span class="n">calculateSquaredEuclideanDistance</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span>
                                                            <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">distance</span> <span class="o">+=</span> <span class="n">std</span><span class="o">::</span><span class="n">pow</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">-</span> <span class="n">v2</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">),</span> <span class="mi">2</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the following section we will look at possible optimizations with and without various hardware acceleration techniques and will have a benchmark and later analysis of the benchmark results.</p>

<h2 id="optimization-techniques">Optimization Techniques</h2>

<p>The naive example presented in the <a href="#naive-implementation">Naive Implementation</a> section may be later optimized differently depending on the different compiler used.
However, we would still bring up potential manual optimizations that can be done in the code and later evaluate performance in our benchmark to see whether those optimizations worth the effort on the readability overhead of our code.</p>

<p>Note: while the below optimizations might make perfect sense in theory, we will later benchmark and test each one of them to confirm those actually hold true!</p>

<h3 id="unrolled-loop">Unrolled Loop</h3>
<p>One issue for <a href="#naive-implementation">Naive Implementation</a> like the above is the repeated conditional checks for the loop termination <code class="language-plaintext highlighter-rouge">i &lt; v1.size()</code>.
Common technique that is used in this case is known as “Loop Unrolling”. What loop unrolling actually means is that we have a loop that goes in wider increments and avoids the additional overhead of loop termination check in each step.
Let’s add a typical loop unrolling implementation:</p>

<p><strong><code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">DistanceCalculator</span><span class="o">::</span><span class="n">calculateSquaredEuclideanDistanceWithLoopUnrolling</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span>
                                                                             <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">v2</span><span class="p">.</span><span class="n">size</span><span class="p">());</span> <span class="c1">// Ensure both vectors have the same size</span>

  <span class="kt">size_t</span> <span class="n">size</span> <span class="o">=</span> <span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
  <span class="kt">size_t</span> <span class="n">unrolledSize</span> <span class="o">=</span> <span class="n">size</span> <span class="o">-</span> <span class="p">(</span><span class="n">size</span> <span class="o">%</span> <span class="mi">4</span><span class="p">);</span>

  <span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">unrolledSize</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">diff0</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="kt">float</span> <span class="n">diff1</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
    <span class="kt">float</span> <span class="n">diff2</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">];</span>
    <span class="kt">float</span> <span class="n">diff3</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">3</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">3</span><span class="p">];</span>

    <span class="n">distance</span> <span class="o">+=</span> <span class="n">diff0</span> <span class="o">*</span> <span class="n">diff0</span> <span class="o">+</span> <span class="n">diff1</span> <span class="o">*</span> <span class="n">diff1</span> <span class="o">+</span> <span class="n">diff2</span> <span class="o">*</span> <span class="n">diff2</span> <span class="o">+</span> <span class="n">diff3</span> <span class="o">*</span> <span class="n">diff3</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="n">unrolledSize</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="n">distance</span> <span class="o">+=</span> <span class="n">diff</span> <span class="o">*</span> <span class="n">diff</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The above code reduces the number of loop termination conditions executed by a factor of <code class="language-plaintext highlighter-rouge">4x</code>!</p>

<h3 id="simd-unrolled-loop">SIMD unrolled loop</h3>
<p>With our unrolled loop implementation we were able to reduce the number of termination check on the loop.
Another optimization techniques we can add on top of that is leveraging special hardware acceleration method available in many modern CPUs and GPUs called “Single Instruction Multiple Data” (aka. SIMD for short).</p>

<h5 id="what-simd-does">What SIMD Does?</h5>
<p>SIMD allows a single CPU instruction to perform the same operation on multiple data points simultaneously, rather than processing them one by one. For L2 distance calculations, this is particularly valuable because:</p>
<ol>
  <li><strong>Parallel Processing</strong>: When calculating L2 distance, you need to perform the same operation (subtraction, squaring, addition) across many vector dimensions. SIMD can process 4, 8, 16 or even more dimensions at once, depending on the instruction set.</li>
  <li><strong>Operation Batching</strong>: Instead of executing each mathematical operation separately for each dimension, SIMD batches them together.</li>
</ol>

<p>For the purposes of this basic evaluation, I’m using an M series Mac with Apple Silicon, I will use the SIMD native libraries on the mac that interface with its custom NEON architecture that can take a 128 bit (4 floats) vector width per operation.
Keep in mind however that popular CPU instruction sets exist for SIMD with different memory widths, to name a few:</p>
<ul>
  <li><strong>AVX-512</strong>: Can process 16 single-precision floats at once</li>
  <li><strong>AVX2/AVX</strong>: Can process 8 single-precision floats at once</li>
  <li><strong>SSE</strong>: Can process 4 single-precision floats at once</li>
</ul>

<p><strong><code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">DistanceCalculator</span><span class="o">::</span><span class="n">calculateSquaredEuclideanDistanceWithSIMD</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span>
                                                                    <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* 1. Assert the Dimensions:*/</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">v2</span><span class="p">.</span><span class="n">size</span><span class="p">());</span> <span class="c1">// Ensure both vectors are of the same size</span>

  <span class="cm">/* 2. Determine Sizes: */</span>
  <span class="kt">size_t</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="c1">// The total size of the vectors.</span>
  <span class="c1">// The largest multiple of 4 that fits within size.</span>
  <span class="c1">// This is necessary because SIMD processes 4 floats at a time (with 128-bit operations when using SIMD intrinsics).</span>
  <span class="kt">size_t</span> <span class="n">simdSize</span> <span class="o">=</span> <span class="n">dimensions</span> <span class="o">/</span> <span class="mi">4</span><span class="p">;</span>

  <span class="k">const</span> <span class="k">auto</span><span class="o">*</span> <span class="n">v1_</span> <span class="o">=</span> <span class="n">v1</span><span class="p">.</span><span class="n">data</span><span class="p">();</span>
  <span class="k">const</span> <span class="k">auto</span><span class="o">*</span> <span class="n">v2_</span> <span class="o">=</span> <span class="n">v2</span><span class="p">.</span><span class="n">data</span><span class="p">();</span>

  <span class="cm">/* 3. Initialize Variables:*/</span>
  <span class="c1">// To store the final squared Euclidean distance value.</span>
  <span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>

  <span class="c1">// A SIMD register initialized to zero.</span>
  <span class="n">float32x4_t</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">vdupq_n_f32</span><span class="p">(</span><span class="mf">0.0</span><span class="n">f</span><span class="p">);</span>

  <span class="cm">/* 4. Process 4 floats at a time using NEON */</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">simdSize</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Constructs a SIMD vector from v1 elements.</span>
    <span class="k">const</span> <span class="n">float32x4_t</span> <span class="n">vec1</span> <span class="o">=</span> <span class="n">vld1q_f32</span><span class="p">(</span><span class="n">v1_</span> <span class="o">+</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">4</span><span class="p">);</span>
    <span class="c1">// Constructs a SIMD vector from v2 elements.</span>
    <span class="k">const</span> <span class="n">float32x4_t</span> <span class="n">vec2</span> <span class="o">=</span> <span class="n">vld1q_f32</span><span class="p">(</span><span class="n">v2_</span> <span class="o">+</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">4</span><span class="p">);</span>
    <span class="c1">// Calculate differences</span>
    <span class="n">float32x4_t</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">vsubq_f32</span><span class="p">(</span><span class="n">vec1</span><span class="p">,</span> <span class="n">vec2</span><span class="p">);</span> <span class="c1">// Performs element-wise subtraction of vec1 and vec2.</span>
    <span class="c1">// Square differences and add to sum</span>
    <span class="c1">// vmlaq_f32 performs multiply-accumulate: sum += diff * diff</span>
    <span class="n">sum</span> <span class="o">=</span> <span class="n">vmlaq_f32</span><span class="p">(</span><span class="n">sum</span><span class="p">,</span> <span class="n">diff</span><span class="p">,</span> <span class="n">diff</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">distance</span> <span class="o">=</span> <span class="n">vaddvq_f32</span><span class="p">(</span><span class="n">sum</span><span class="p">);</span> <span class="c1">// Sum all elements in the vector</span>

  <span class="c1">// Handle remaining elements (if dimensions not divisible by 4)</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="n">dimensions</span> <span class="o">/</span> <span class="mi">4</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">const</span> <span class="kt">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">v1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">v2</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="n">distance</span> <span class="o">+=</span> <span class="n">diff</span> <span class="o">*</span> <span class="n">diff</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s take a moment to reason about the code above. Previously for vectors $A$ and $B$ the L2 calculation logic looked something like this (pseudocode):</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>For each dimension i:
  Calculate difference: diff_i <span class="o">=</span> A_i - B_i
  Square the difference: squared_diff_i <span class="o">=</span> diff_i <span class="k">*</span> diff_i
  Add to running <span class="nb">sum</span>: <span class="nb">sum</span> +<span class="o">=</span> squared_diff_i
Take square root of <span class="nb">sum</span>
</code></pre></div></div>

<p>And now it looks more like this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Load multiple dimensions of A into SIMD register
Load multiple dimensions of B into SIMD register
Subtract B from A <span class="k">for </span>all dimensions simultaneously
Square all differences simultaneously
Sum all squared differences <span class="o">(</span>often using specialized horizontal <span class="nb">sum </span>instructions<span class="o">)</span>
Take square root of result
</code></pre></div></div>

<p>The main difference here is that we saved instructions to load registers. 
Here we used 4x fewer instructions than the previous examples to load values into CPU registers to perform the calculations, which means (if everything works as expected) an additional 4x performance speedup.</p>

<h3 id="gpu-unrolled-loop">GPU unrolled loop</h3>
<p>Similar to the hardware acceleration with SIMD approach, we might want to take the idea of parallelism of distance computations to the limit by leveraging a GPU.</p>

<h4 id="what-are-the-potential-advantages-of-gpus-for-distance-computations">What are the <em>potential</em> advantages of GPUs for distance computations?</h4>
<ul>
  <li><strong>Massive Parallelism</strong>: GPUs have thousands of cores designed for parallel operations.</li>
  <li><strong>Specialized Instructions</strong>: Modern GPUs include optimized instructions for vector operations and matrix mathematics.</li>
  <li><strong>High Memory Bandwidth</strong>: GPUs can transfer large vector datasets quickly, which is crucial when comparing against large vector databases.</li>
  <li><strong>Tensor Cores</strong>: Advanced GPUs (like NVIDIA’s with Tensor Cores) provide specialized hardware for matrix operations, further accelerating these calculations.</li>
</ul>

<h4 id="potential-gpu-trade-offs">Potential GPU Trade-offs</h4>
<p>While in theory GPU sounds like the perfect candidate for distance computation acceleration, there are in fact hidden limitations (as we will also see pretty soon in the benchmarks):</p>
<ul>
  <li><strong>Data Transfer Overhead</strong>: Moving data between CPU and GPU memory can create bottlenecks for smaller searches</li>
  <li><strong>Kernel Launch Overhead</strong>: Initiating an operation on the GPU might have additional overhead for <em>GPU Kernel</em> <a href="https://www.hpcs.cs.tsukuba.ac.jp/icpp2019/data/posters/Poster17-abst.pdf">launch</a>. This can get quite costly and can negate the potential parallelism advantages for small computations.</li>
</ul>

<p>We will come back  to explore the balance between tradeoffs and advantages of GPU utilization for vector search computation later on when we go over the benchmarks and their results.</p>

<p>For now lets take a look at the implementation of GPU acceleration for Euclidean distance on the Apple Silicon Mac.
First and foremost we would need to define a computational GPU kernel for the Euclidean distance using Apple’s Metal framework:
<strong><code class="language-plaintext highlighter-rouge">SquaredDistanceKernel.metal</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;metal_stdlib&gt;</span><span class="cp">
</span><span class="k">using</span> <span class="k">namespace</span> <span class="n">metal</span><span class="p">;</span>

<span class="n">kernel</span> <span class="kt">void</span> <span class="nf">calculateSquaredEuclideanDistance</span><span class="p">(</span>
    <span class="k">const</span> <span class="n">device</span> <span class="kt">float</span> <span class="o">*</span><span class="n">inA</span> <span class="p">[[</span> <span class="n">buffer</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="p">]],</span>
    <span class="k">const</span> <span class="n">device</span> <span class="kt">float</span> <span class="o">*</span><span class="n">inB</span> <span class="p">[[</span> <span class="n">buffer</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">]],</span>
    <span class="n">device</span> <span class="kt">float</span> <span class="o">*</span><span class="n">squaredDiffs</span> <span class="p">[[</span> <span class="n">buffer</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="p">]],</span>
    <span class="n">uint</span> <span class="n">id</span> <span class="p">[[</span> <span class="n">thread_position_in_grid</span> <span class="p">]]</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">inA</span><span class="p">[</span><span class="n">id</span><span class="p">]</span> <span class="o">-</span> <span class="n">inB</span><span class="p">[</span><span class="n">id</span><span class="p">];</span>
    <span class="n">squaredDiffs</span><span class="p">[</span><span class="n">id</span><span class="p">]</span> <span class="o">=</span> <span class="n">diff</span> <span class="o">*</span> <span class="n">diff</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We would later need to compile that kernel to reuse it within our calculator in the following manner:
<strong><code class="language-plaintext highlighter-rouge">DistanceCalculator.cpp</code></strong></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">DistanceCalculator</span><span class="o">::</span><span class="n">calculateSquaredEuclideanDistanceWithMetal</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">,</span>
                                                                     <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">v2</span><span class="p">)</span> <span class="k">const</span> <span class="p">{</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">v2</span><span class="p">.</span><span class="n">size</span><span class="p">());</span> <span class="c1">// Ensure both vectors are of the same size</span>
  <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">vectorSize</span> <span class="o">=</span> <span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">dimension_</span><span class="p">);</span>

  <span class="c1">// Update the GPU buffers with new data by copying from host memory.</span>
  <span class="n">memcpy</span><span class="p">(</span><span class="n">bufferV1_</span><span class="o">-&gt;</span><span class="n">contents</span><span class="p">(),</span> <span class="n">v1</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">v1</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">));</span>
  <span class="n">memcpy</span><span class="p">(</span><span class="n">bufferV2_</span><span class="o">-&gt;</span><span class="n">contents</span><span class="p">(),</span> <span class="n">v2</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">v2</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">));</span>
  
  <span class="c1">// Encode Metal commands</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">commandBuffer</span> <span class="o">=</span> <span class="n">commandQueue_</span><span class="o">-&gt;</span><span class="n">commandBuffer</span><span class="p">();</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">computeEncoder</span> <span class="o">=</span> <span class="n">commandBuffer</span><span class="o">-&gt;</span><span class="n">computeCommandEncoder</span><span class="p">();</span>

  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">setComputePipelineState</span><span class="p">(</span><span class="n">pipelineState_</span><span class="p">);</span>
  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">setBuffer</span><span class="p">(</span><span class="n">bufferV1_</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">setBuffer</span><span class="p">(</span><span class="n">bufferV2_</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">setBuffer</span><span class="p">(</span><span class="n">bufferDiffs_</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>



  <span class="c1">// Dispatch threads</span>
  <span class="k">auto</span> <span class="n">threadGroupSize</span> <span class="o">=</span> <span class="n">MTL</span><span class="o">::</span><span class="n">Size</span><span class="o">::</span><span class="n">Make</span><span class="p">(</span><span class="n">pipelineState_</span><span class="o">-&gt;</span><span class="n">maxTotalThreadsPerThreadgroup</span><span class="p">(),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
  <span class="k">auto</span> <span class="n">threadGroups</span> <span class="o">=</span> <span class="n">MTL</span><span class="o">::</span><span class="n">Size</span><span class="o">::</span><span class="n">Make</span><span class="p">((</span><span class="n">vectorSize</span> <span class="o">+</span> <span class="n">threadGroupSize</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">threadGroupSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>

  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">dispatchThreadgroups</span><span class="p">(</span><span class="n">threadGroups</span><span class="p">,</span> <span class="n">threadGroupSize</span><span class="p">);</span>
  <span class="n">computeEncoder</span><span class="o">-&gt;</span><span class="n">endEncoding</span><span class="p">();</span>

  <span class="c1">// Commit and wait for the computation to complete</span>
  <span class="n">commandBuffer</span><span class="o">-&gt;</span><span class="n">commit</span><span class="p">();</span>
  <span class="n">commandBuffer</span><span class="o">-&gt;</span><span class="n">waitUntilCompleted</span><span class="p">();</span>

  <span class="k">auto</span> <span class="o">*</span><span class="n">diffs</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">float</span> <span class="o">*&gt;</span><span class="p">(</span><span class="n">bufferDiffs_</span><span class="o">-&gt;</span><span class="n">contents</span><span class="p">());</span>
  <span class="kt">float</span> <span class="n">squaredDistance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
  <span class="c1">// Read the result</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">vectorSize</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">squaredDistance</span> <span class="o">+=</span> <span class="n">diffs</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">squaredDistance</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Note</strong>: for the code above to be more concise, a lot of boilerplate of GPU initialization code was removed to focus on the core concepts of this blog article.</p>

<h2 id="benchmarks">Benchmarks</h2>
<p>Now is the time to actually collect empirical results from the performance of all the above algorithms!
But first, lets take a moment to discuss benchmark methodology.</p>

<p>We have two groups of algorithms to test so far:</p>
<ol>
  <li>No Hardware Acceleration
    <ul>
      <li>naive</li>
      <li>unrolled loop</li>
    </ul>
  </li>
  <li>Hardware accelerated
    <ul>
      <li>SIMD</li>
      <li>GPU (with Metal Kernel for M series Mac)</li>
    </ul>
  </li>
</ol>

<p>We will use the  <a href="#compute-cost-of-knn">parameters</a> defined earlier to consider the following edge cases:</p>
<ol>
  <li>Large datasets - $n$ » $d$</li>
  <li>High dimensionality - $d$ ~ $n$</li>
</ol>

<p>The benchmark results visualization shows performance comparisons across different implementations:</p>
<ul>
  <li>Metal</li>
  <li>Naive</li>
  <li>Unrolled</li>
  <li>SIMD</li>
</ul>

<p><img src="knn_benchmark_output_11.png" alt="img.png" />
<img src="knn_benchmark_output_12.png" alt="img.png" /></p>

<p><a href="./knn_benchmark_output_1.html">View full interactive visualization</a></p>

<h3 id="key-observations">Key Observations</h3>

<ol>
  <li>
    <p><strong>Performance scaling by dimension</strong>: As vector dimensions increase from 128 to 10,000, performance degrades across all methods, but at very different rates.</p>
  </li>
  <li><strong>GPU (Apple Metal) performance</strong>:
    <ul>
      <li>Metal shows the most consistent performance across different scenarios</li>
      <li>However, it has extremely high overhead for small datasets (n=1,000)</li>
      <li>For high dimensionality (d=10,000), it becomes competitive with other methods</li>
    </ul>
  </li>
  <li><strong>Unrolled optimization</strong>:
    <ul>
      <li>Performs well across all scenarios</li>
      <li>Maintains good scaling with increased data size</li>
      <li>Degrades as dimensions scale</li>
    </ul>
  </li>
  <li><strong>SIMD acceleration</strong>:
    <ul>
      <li>SIMD consistently outperforms other techniques across all dimension sizes</li>
      <li>Most effective for small to medium-sized dimensions</li>
      <li>Degrades as dimensions scale</li>
    </ul>
  </li>
  <li><strong>Naive implementation</strong>:
    <ul>
      <li>Performs relatively well only for low dimensions (d=128)</li>
      <li>Performance collapses catastrophically at higher dimensions</li>
      <li>Shows the worst scaling behavior of all methods</li>
    </ul>
  </li>
</ol>

<h1 id="conclusion-are-gpus-the-answer-for-vector-search">Conclusion: Are GPUs the Answer for Vector Search?</h1>

<p>We began our investigation with a simple hypothesis: GPU architecture, with its massive parallelism, should theoretically excel at accelerating brute force KNN vector search operations. Our benchmark results, however, tell a more complex story.</p>

<p>While GPUs do indeed scale more effectively with high-dimensional vectors, their performance advantages only become significant at dimensions exceeding 10,000 and with very large datasets. This presents a practical challenge, as most modern embedding models operate in dimensional spaces well below this threshold—typically maxing out around 4,000 dimensions.</p>

<p>Surprisingly, our tests revealed that in these more common dimensional ranges, GPU implementations actually underperform compared to even basic CPU optimizations like loop unrolling. The overhead associated with data transfer to and from the GPU, combined with initialization costs, effectively negates the theoretical advantages for all but the most extreme use cases.</p>

<p>This unexpected outcome raises important questions for vector search optimization. Is there still untapped potential in GPU acceleration for KNN that our implementation hasn’t captured? Or should we pivot our focus toward approximate nearest neighbor (ANN) algorithms that might offer better performance-accuracy tradeoffs?</p>

<p>In my next article, I’ll explore these questions and determine whether GPU acceleration for vector search can be salvaged or if we need to look elsewhere for performance gains.</p>]]></content><author><name>Samuel Herman</name></author><summary type="html"><![CDATA[I recently began a personal project exploring multi-modal search, which led me to investigate cutting-edge embedding models and vector search solutions. Vector search (often called KNN or K Nearest Neighbors) identifies the most similar vectors to a query vector, enabling us to find closely related images or semantically similar text.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sam-herman.github.io/blogs/knn_benchmark_output_12.png" /><media:content medium="image" url="https://sam-herman.github.io/blogs/knn_benchmark_output_12.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>