Skip to content
mlmentorship

Two-tower retrieval

Separate query and catalog encoding, preserve candidate and model-version contracts, and measure approximate retrieval before reranking.

Published · 7 min read ·Role-specific ·Intermediate
Reading depth

Summary

A two-tower model independently encodes queries and items, then scores their vector interaction. Independent item encoding permits reuse until its inputs or representation contract change. Index approximation, candidate eligibility, negative semantics, and model-version compatibility remain separate correctness obligations.

Architecture

query  → query_tower  → q ∈ R^d
item   → item_tower   → i ∈ R^d
score  = q · i  (or cosine)

The towers can share weights; normalization and vector dimension are design choices, not definitions. For nonzero unit vectors, dot product equals cosine; otherwise magnitude changes dot-product ranking. Stored item features must not require query-specific joint computation under this architecture.

Learning objective

Encode items before the request; encode the query during it

Item encoding happens before the request; query encoding happens during itOffline item vectors populate the index. The live request encodes its query and searches that versioned index without re-encoding every item.Encode items before the request;search their retained indexitemsindexLive request boundaryqueryQuery encoding happens now; itemvectors already exist.
Derivation
Offline item encoding feeds the index used by online query retrieval The upper offline lane sends every catalog item through the item tower and writes the resulting item vectors to an approximate nearest-neighbor index during a batch refresh. The lower online lane sends one incoming query through the query tower to produce vector q. The index compares q with its stored item vectors and returns top-K candidate IDs. Text labels and a dashed batch-refresh path distinguish the two lifecycles without relying on color. 1 · OFFLINE OR WHEN THE CATALOG / MODEL CHANGES ALL CATALOG ITEM FEATURES ITEM TOWER run in batch ITEM VECTORS i₁, i₂, …, iₙ batch refresh writes stored vectors 2 · ONLINE FOR EACH REQUEST ONE QUERY + context QUERY TOWER run once QUERY VECTOR q ∈ Rᵈ ANN INDEX q · stored i TOP-K IDS to ranker No item-tower call occurs on this request path.
the dashed refresh path computes and stores catalog vectors. The request path computes a query and searches those compatible stored vectors. The original schematic follows Yi et al., TensorFlow Recommenders, and Google Cloud.
Check: Which computation was already done before this request? What version change could require rebuilding its stored item vectors?
Read it this way: Offline item vectors populate the index. The live request encodes its query and searches that versioned index without re-encoding every item.
Which encoding happens during this live request?

Training

With query matrix and item matrix , scores are . Require positive temperature and declare acceptable positives and eligible candidates per row.

In-batch sampled softmax

The one-positive, temperature-one case uses:

For , positive A , and valid negative B , scores are one and zero. Positive probability is and loss is . If B is also acceptable, treating it as a known negative is wrong.

Deduplicate item IDs first; this implementation maximizes total probability of the acceptable-positive set, not each positive’s individual probability. It uses Boolean masks and float64 scores with representable score gaps:

import numpy as np

def retrieval_loss(query, items, positive_mask, valid_mask, temperature=1.0):
	query = np.asarray(query, dtype=np.float64)
	items = np.asarray(items, dtype=np.float64)
	positive = np.asarray(positive_mask)
	valid = np.asarray(valid_mask)
	if query.ndim != 2 or items.ndim != 2:
		raise ValueError("Expected two embedding matrices")
	if min(query.shape) == 0 or min(items.shape) == 0:
		raise ValueError("Embedding axes must be nonempty")
	if query.shape[1] != items.shape[1]:
		raise ValueError("Embedding dimensions differ")
	shape = (len(query), len(items))
	if positive.shape != shape or valid.shape != shape:
		raise ValueError("Mask shape differs from score shape")
	if positive.dtype != np.bool_ or valid.dtype != np.bool_:
		raise ValueError("Masks must be Boolean")
	if np.any(positive & ~valid) or not np.all(positive.any(axis=1)):
		raise ValueError("Each row needs eligible positive candidates")
	if not np.isfinite(temperature) or temperature <= 0:
		raise ValueError("Temperature must be finite and positive")
	if not np.isfinite(query).all() or not np.isfinite(items).all():
		raise ValueError("Embeddings must be finite")
	with np.errstate(over="raise", invalid="raise", divide="raise"):
		scores = query @ items.T / temperature
		scores = np.where(valid, scores, -np.inf)
		relative = scores - scores.max(axis=1, keepdims=True)
		denominator = np.logaddexp.reduce(relative, axis=1)
		numerator = np.logaddexp.reduce(
			np.where(positive, relative, -np.inf), axis=1
		)
		return float(np.mean(denominator - numerator))

Both candidates acceptable gives loss zero; rejecting every positive raises instead of silently creating an undefined objective.

Need Joint Token Detail

New task: Distinguish subtle query-item phrase relations lost by independent compression.

Picture and change: Evaluate a bounded cross-encoder or late-interaction reranker with its extra work and storage.

Check: A reranker cannot recover an omitted relevant item, so inspect candidate recall separately.

Use when: Added pair detail improves the measured decision under its budget.

Change the Item Model

New task: Retrain the item encoder or tokenizer.

Picture and change: Re-encode/reindex or prove a compatible migration; version the query tower, analyzer, scoring policy, and index together.

Check: Rotating only the query from to reverses A/B scores against the unchanged index. Equal vector dimension is insufficient evidence of compatibility.

Use when: Stored representation semantics change; retain an atomic rollback bundle and enforce current eligibility/deletions after retrieval.

Importance-corrected sampled softmax

Sampled-softmax correction must specify proposal probabilities, inclusion multiplicities, positive handling, and the intended full-catalog objective. Subtracting a log-proposal term is not a universal exposure-bias cure; follow the estimator’s derivation, such as Yi et al..

Hard negative mining

High-scoring alternatives may be unlabeled positives; validate their negative status and evaluate the changed sampling distribution.

Two-stage architecture

Candidate generation followed by richer ranking is one useful arrangement, not a required architecture or guaranteed latency target. Approximate search trades recall, memory, and work under its actual index and workload; sublinear scaling is not unconditional.

Tradeoffs vs. cross-encoder

PropertyTwo-towerCross-encoder
Pair computationIndependent encodings plus vector interactionJoint query/item computation
Reusable catalog stateStored item vectorsArchitecture-dependent; joint scores still need the query
Quality/costMeasure for task and budgetMeasure for task and budget

Common pitfalls

  • No interactions: dot product is an interaction, but not arbitrary joint token computation.
  • Incompatible updates: changing only the query tower can also invalidate the index.
  • Unproven quality: reranking and negative mining require evaluation, not an assumed improvement.
  • Embedding spaces. Vector representations and indexing.
  • RAG overview. Retrieval-augmented generation uses two-tower for the retrieval step.