03 β Embedding Mismatch
The query and document embeddings are computed in different semantic spaces (domain drift, language variation, or asymmetric encoding), leading to poor retrieval despite semantic relevance.
Q1. What is embedding mismatch and why does it cause retrieval failure? [Basic]
π‘ Show Answer
Answer:
Embedding mismatch occurs when a query and its relevant documents are embedded into different semantic spaces, causing low cosine similarity despite semantic relevance. The embedding model fails to recognize that they should be close.
Root issue:
Embeddings are learned from training data. If a general-purpose embedding model (trained on web text) is applied to a specialized domain (medical, financial, legal), it may not have learned the relationships between domain-specific terms.
| Scenario | Example | Symptom |
|---|---|---|
| Domain mismatch | Query: "What is metformin?" (medical), Embedding trained on general web text | Similarity score is low despite document being highly relevant |
| Language drift | Query in formal English, Documents in colloquial English | Different token distributions β low similarity |
| Vocabulary shift | Query: "API authentication", Docs: "token-based authorization" | Synonyms not learned by model β similarity is 0.5 when should be 0.95 |
| Asymmetric encoding | Query is short (5 tokens), Document is long (500 tokens) | Embeddings average over different context window sizes |
| New concepts | Query mentions "retrieval-augmented generation" (term coined ~2020) | Pre-trained model (trained on 2019 data) never saw this term |
This is distinct from retrieval failure (wrong docs retrieved) because the information exists and is relevant, but the embedding model's representation of similarity is wrong.
Q2. What are observable symptoms of embedding mismatch in production? [Basic]
π‘ Show Answer
Answer:
Symptoms of embedding mismatch can be detected through multiple signals:
| Symptom | Detection Method | Example |
|---|---|---|
| Low similarity scores across all results | Monitor cosine similarity distribution | Query returns documents with max similarity=0.55 when good matches should be ~0.85 |
| Manual audit reveals correct doc ranked low | Periodically check: does ground-truth doc rank in top-5? | You search for "How to implement RAG", correct doc exists but ranked at position 23 |
| Domain-specific queries fail but general queries succeed | A/B by query type | Queries about "blockchain smart contracts" have low success, but "What is Python?" works fine |
| Synonyms not recognized | Test query expansion: do synonyms rank the same? | Query: "Web server scaling" vs "HTTP load balancing" β different results despite near-identical meaning |
| Language variation causes divergence | Test linguistic variants | Query: "How do I authenticate?" vs "How do I do authentication?" β Different rankings |
| Performance degrades in new domain | Recall@k drops after domain shift | Retrain on medical docs, recall drops from 0.90 to 0.65 |
| User clicks don't match ranking | Track: where do users click vs where results ranked? | Top result in retrieval ranking not clicked, user clicks result ranked 8th |
Production signals:
def detect_embedding_mismatch_signals(query, retrieved_chunks, user_feedback):
"""Flag potential mismatch."""
signals = {
'low_max_similarity': np.max([c['similarity'] for c in retrieved_chunks]) < 0.6,
'low_mean_similarity': np.mean([c['similarity'] for c in retrieved_chunks]) < 0.5,
'user_clicked_lower_ranked': user_feedback.get('clicked_rank', 0) > 5,
'user_explicit_failure': user_feedback.get('rating', 5) <= 2,
}
if sum(signals.values()) >= 2:
log_mismatch_alert(query)
Q3. What causes embedding mismatch? [Intermediate]
π‘ Show Answer
Answer:
Embedding mismatch stems from fundamental differences in how models represent meaning:
1. Domain Shift
Embedding models are trained on general-purpose corpora (web text, Wikipedia). When applied to specialized domains, the learned representations don't capture domain-specific semantics.
General-purpose (web) embedding:
"bank" β "financial institution, river edge, slope"
Heavy weight on finance meaning
Domain: Riverbank management
"bank" should mean "river edge"
Embedding model's representation is misaligned
Result: Query "riverbank erosion" doesn't match doc about "bank stabilization"
Quantifiable: Fine-tuned embeddings on domain data outperform general models by 10-40% Recall@k on domain benchmarks.
2. Vocabulary and Tokenization Mismatch
Different models tokenize text differently:
Query (using BERT tokenizer):
"retrieval-augmented generation"
Tokens: ["retrieval", "-", "augmented", "generation"]
Document (using a different tokenizer):
"retrieval augmented generation" (no hyphen)
Tokens: ["retrieval", "augmented", "generation"]
Different token sequences β Different embeddings
3. Asymmetric Query-Document Encoding
Most embedding models average token embeddings into a single vector. Short queries and long documents have different averaging distributions:
Short query (5 tokens):
"How to scale?"
Averaged over 5 tokens
Long document (500 tokens):
"Scaling databases involves sharding, replication, caching... [continues]"
Averaged over 500 tokens
The long document's embedding is noisier (averaged over many tokens).
Similarity calculation treats them equally despite asymmetry.
4. Language and Phrasing Variation
Same concept, different phrasings:
Query A: "How do I authenticate users?"
Query B: "How do I do user authentication?"
Query C: "What is the auth process?"
All mean the same thing, but token-level differences can cause embedding divergence.
Embedding similarity:
sim(A, B) = 0.92 (syntactic overlap)
sim(A, C) = 0.72 (lower, but should be high)
sim(B, C) = 0.75
5. Temporal Drift
Embedding models trained on older data may not understand newer terminology:
Model trained: 2019
New terminology: "ChatGPT" (released Nov 2022), "RAG" (refined 2023)
Query (2024): "How does RAG differ from fine-tuning?"
Model sees: "RAG" = unknown subword tokens
Embedding is degraded.
Compare: Model trained in 2024 handles "RAG" naturally.
6. Cross-Lingual Mismatch
Multilingual models balance languages poorly; one language may be underrepresented:
Multilingual model trained on 80% English, 10% Spanish, 10% French
Spanish query: "ΒΏCΓ³mo autenticar usuarios?"
Spanish document: "AutenticaciΓ³n de usuarios mediante JWT..."
Model learned strong English associations, weaker Spanish.
Similarity: Lower than it should be for in-language matching.
7. Instruction-Tuning Asymmetry
Some embedding models expect a specific query format (e.g., "Represent the question:"):
Model: all-MiniLM-L6-v2 (no instruction format required)
Query: "How to scale a database?"
Doc: "Horizontal scaling adds more servers..."
Similarity: 0.70
Model: e5-base-v2 (instruction-tuned)
Query (correct format): "Represent the question: How to scale a database?"
Doc (correct format): "Represent the document: Horizontal scaling adds more servers..."
Similarity: 0.92
Using wrong format β Embedding mismatch.
Q4. How do you detect embedding mismatch in a production system? [Intermediate]
π‘ Show Answer
Answer:
Detection Method 1: Embedding Quality Benchmarks (MTEB)
Use standard benchmarks to understand embedding model strengths/weaknesses:
from mteb import MTEB
# MTEB: Massive Text Embedding Benchmark
# Evaluates embeddings on 50+ datasets across 8 task categories
model_name = "all-mpnet-base-v2"
model = SentenceTransformer(model_name)
# Evaluate on retrieval tasks
evaluation = MTEB(tasks=["TREC-COVID", "DBpedia", "SCIFACT"])
results = evaluation.run(model)
# Results structure
# {
# 'TREC-COVID': {'NDCG@10': 0.58, ...},
# 'DBpedia': {'NDCG@10': 0.45, ...},
# 'SCIFACT': {'NDCG@10': 0.67, ...}
# }
# Check if your domain matches these tasks
# If your domain β MTEB task, expect lower performance
Key MTEB benchmark domains:
| Task | Domain | Top Model | Score |
|---|---|---|---|
| TREC-COVID | Medical/scientific retrieval | text-embedding-3-large | 0.74 |
| SCIFACT | Scientific fact verification | text-embedding-3-large | 0.72 |
| DBpedia | General knowledge retrieval | text-embedding-3-large | 0.69 |
| SciDocs | Scientific paper retrieval | text-embedding-3-large | 0.71 |
| MrTyDi | Multilingual (12 languages) | text-embedding-3-large | 0.53 |
Interpretation: If your domain is not on MTEB, your embedding model's performance is uncertain β high mismatch risk.
Detection Method 2: Similarity Score Distribution Analysis
def analyze_similarity_distribution(queries, documents, embedding_model):
"""Check if similarity distribution matches expected pattern."""
similarities = []
for query in queries:
query_emb = embedding_model.encode(query)
for doc in documents:
doc_emb = embedding_model.encode(doc['text'])
sim = cosine_similarity([query_emb], [doc_emb])[0][0]
is_relevant = doc['is_relevant_to_query']
similarities.append({
'similarity': sim,
'is_relevant': is_relevant
})
# Distribution analysis
relevant_sims = [s['similarity'] for s in similarities if s['is_relevant']]
irrelevant_sims = [s['similarity'] for s in similarities if not s['is_relevant']]
print(f"Relevant docs: mean={np.mean(relevant_sims):.3f}, std={np.std(relevant_sims):.3f}")
print(f"Irrelevant docs: mean={np.mean(irrelevant_sims):.3f}, std={np.std(irrelevant_sims):.3f}")
# Good separation: mean_relevant >> mean_irrelevant
separation = np.mean(relevant_sims) - np.mean(irrelevant_sims)
if separation < 0.2:
print(f"β οΈ Poor separation: {separation:.3f} (should be > 0.3)")
log_mismatch_alert("Embeddings not well-separated")
return {
'relevant_mean': np.mean(relevant_sims),
'irrelevant_mean': np.mean(irrelevant_sims),
'separation': separation
}
Detection Method 3: Synonym/Paraphrase Consistency
def test_synonym_consistency(embedding_model):
"""Check if synonyms have high similarity."""
synonym_pairs = [
("authenticate user", "user login"),
("database sharding", "horizontal partitioning"),
("API key", "access token"),
("machine learning", "artificial intelligence"),
]
for query1, query2 in synonym_pairs:
emb1 = embedding_model.encode(query1)
emb2 = embedding_model.encode(query2)
sim = cosine_similarity([emb1], [emb2])[0][0]
print(f'"{query1}" vs "{query2}": {sim:.3f}')
if sim < 0.85:
print(f" β οΈ Low similarity for near-synonyms β mismatch risk")
Example output:
"authenticate user" vs "user login": 0.94 β
"database sharding" vs "horizontal partitioning": 0.68 β οΈ
"API key" vs "access token": 0.71 β οΈ
Detection Method 4: Domain-Specific Evaluation Set
Create a small labeled evaluation set in your domain:
def create_domain_eval_set(domain_name, num_pairs=100):
"""
Manually or semi-automatically create query-document pairs
labeled with relevance.
"""
eval_set = [
{
'query': 'How to set up JWT authentication?',
'documents': [
{'text': 'Bearer tokens and JSON Web Tokens...', 'relevant': True},
{'text': 'Session-based auth uses cookies...', 'relevant': False},
]
},
# ... 100 query-document pairs
]
# Evaluate embedding model on this set
ndcg = evaluate_ndcg(embedding_model, eval_set)
print(f"Domain-specific NDCG@5: {ndcg:.3f}")
if ndcg < 0.75:
print("β οΈ Below threshold for production (75%), consider fine-tuning")
return eval_set
def evaluate_ndcg(embedding_model, eval_set):
"""Compute NDCG@5 on evaluation set."""
ndcg_scores = []
for item in eval_set:
query = item['query']
documents = item['documents']
# Rank documents by embedding similarity
query_emb = embedding_model.encode(query)
ranked = sorted(
documents,
key=lambda d: cosine_similarity(
[query_emb],
[embedding_model.encode(d['text'])]
)[0][0],
reverse=True
)
# Compute NDCG
dcg = sum(
(1 if doc['relevant'] else 0) / np.log2(i+2)
for i, doc in enumerate(ranked[:5])
)
idcg = sum(1 / np.log2(i+2) for i in range(min(5, sum(1 for d in documents if d['relevant']))))
ndcg = dcg / idcg if idcg > 0 else 0
ndcg_scores.append(ndcg)
return np.mean(ndcg_scores)
Detection Method 5: A/B Testing Embedding Models
def compare_embedding_models(queries, ground_truth, models_to_test):
"""Test multiple embedding models on your domain."""
results = {}
for model_name in models_to_test:
model = SentenceTransformer(model_name)
recall_at_5 = []
for query, relevant_doc_ids in ground_truth.items():
# Retrieve with model
retrieved = model_based_retrieval(query, model, k=5)
retrieved_ids = {d['id'] for d in retrieved}
# Compute recall
recall = len(retrieved_ids & set(relevant_doc_ids)) / len(relevant_doc_ids)
recall_at_5.append(recall)
results[model_name] = np.mean(recall_at_5)
# Print ranking
for model, recall in sorted(results.items(), key=lambda x: x[1], reverse=True):
print(f"{model}: Recall@5 = {recall:.2%}")
return results
Q5. What causes embedding mismatch across different dimensions (domain, language, model architecture)? [Intermediate]
π‘ Show Answer
Answer:
Embedding mismatch has multiple independent causes across three dimensions:
Dimension 1: Domain Shift
Within-domain shift: Vocabulary and concepts change over time within a single domain:
Year 2015:
"Deep learning" embedding β "neural networks", "CNNs", "RNNs"
Year 2023:
"Deep learning" embedding β "transformers", "LLMs", "diffusion models"
Model trained in 2015 applied to 2023 data β Mismatch
Cross-domain shift: A model trained on one domain (web) applied to another (medical):
Web-trained embedding:
"virus" β "computer malware", "security threat"
Medical domain:
"virus" β "pathogen", "infection", "immune response"
Same word, completely different semantic neighbors β High mismatch
Quantification:
from sklearn.metrics.pairwise import cosine_distances
def measure_domain_shift(model, domain_a_texts, domain_b_texts):
"""Compute distance between domain embeddings."""
emb_a = model.encode(domain_a_texts)
emb_b = model.encode(domain_b_texts)
# Compute within-domain and cross-domain distances
within_a = np.mean(cosine_distances(emb_a, emb_a))
within_b = np.mean(cosine_distances(emb_b, emb_b))
cross_domain = np.mean(cosine_distances(emb_a, emb_b))
shift_magnitude = cross_domain - (within_a + within_b) / 2
print(f"Domain shift magnitude: {shift_magnitude:.3f}")
return shift_magnitude
# Example
domain_a = ["machine learning", "neural networks", "training data"]
domain_b = ["bacterial infection", "antibiotic resistance", "viral load"]
shift = measure_domain_shift(model, domain_a, domain_b)
# High shift β Model not suitable for both domains
Dimension 2: Asymmetric Query vs Document Encoding
The problem:
Most embedding models pool token embeddings into a single vector via mean/cls. But queries and documents have different characteristics:
# Simple mean-pooling
def encode_text(tokens):
token_embeddings = [embed(token) for token in tokens]
return np.mean(token_embeddings, axis=0)
query_tokens = ["How", "to", "scale", "database"] # 4 tokens
query_emb = encode_text(query_tokens) # averaged over 4
doc_tokens = ["Horizontal scaling involves sharding, replication, caching, ..."] # 50+ tokens
doc_emb = encode_text(doc_tokens) # averaged over 50+
# Different averaging contexts β Embedding mismatch
sim(query, doc) lower than it should be
Solutions:
# Asymmetric encoding (e5-base, text-embedding-3)
model = SentenceTransformer('intfloat/e5-base-v2')
# Model includes instruction prompts
query_emb = model.encode("query: How to scale database")
doc_emb = model.encode("passage: Horizontal scaling...")
# Instructions guide model to handle asymmetry
sim(query, doc) is higher and more accurate
Dimension 3: Language and Multilingual Mismatch
Monolingual models in multilingual settings:
Model: all-mpnet-base-v2 (trained heavily on English)
Query (Spanish): "ΒΏCΓ³mo escalar una base de datos?"
Document (Spanish): "El escalamiento horizontal distribuye datos..."
Model learned:
- English embeddings: Dense, well-trained
- Spanish embeddings: Sparse (less training data)
Result: Spanish query and doc have lower similarity than equivalent English pair
Multilingual model imbalance:
Multilingual model trained on:
- 60% English
- 20% Chinese
- 15% Spanish
- 5% other
Language-specific performance:
- English NDCG@5: 0.82
- Chinese NDCG@5: 0.65 β Lower-resource language suffers
- Spanish NDCG@5: 0.68 β Imbalance causes mismatch
Query in Spanish, document in Chinese β Even worse alignment
Cross-lingual mismatch:
Query (English): "How to authenticate?"
Document (French): "Comment authentifier l'utilisateur?"
Cross-lingual model must bridge languages.
Quality degrades compared to within-language retrieval.
Similarity: 0.65 (cross-lingual)
vs. 0.90 (same language)
Dimension 4: Model Architecture Mismatch
Different model families:
BERT-based (all-MiniLM-L6-v2):
"database" embedding trained with masked LM objective
Contrastive (Sentence-Transformers):
"database" embedding trained with InfoNCE loss
Different architectures β Different learned representations
β Embeddings in different spaces
In practice:
model_1 = SentenceTransformer('all-MiniLM-L6-v2') # BERT-based
model_2 = SentenceTransformer('all-mpnet-base-v2') # MPNet-based
query = "machine learning"
doc = "artificial intelligence"
sim_1 = cosine_similarity(
[model_1.encode(query)],
[model_1.encode(doc)]
)[0][0] # 0.82
sim_2 = cosine_similarity(
[model_2.encode(query)],
[model_2.encode(doc)]
)[0][0] # 0.91
# Different models give different results for same query-doc pair
Summary: Multi-Dimensional Mismatch Space
Domain Shift
β
Language
β
Query-Doc Asymmetry β [Mismatch] β Model Architecture
β
Temporal Drift
To mitigate, assess all four dimensions for your use case.
Q6. What strategies mitigate embedding mismatch? [Intermediate]
π‘ Show Answer
Answer:
Strategy 1: Fine-Tuning Embeddings on Domain Data
Fine-tune a pre-trained embedding model on labeled (query, document) pairs from your domain:
from sentence_transformers import SentenceTransformer, models, losses, InputExample
from torch.utils.data import DataLoader
# Step 1: Load pre-trained model
base_model = SentenceTransformer('all-mpnet-base-v2')
# Step 2: Prepare training data
training_data = [
InputExample(
texts=["How do I scale a database?", "Horizontal scaling distributes data..."],
label=0.95 # High similarity (relevant pair)
),
InputExample(
texts=["How do I scale a database?", "Python tutorial for beginners"],
label=0.1 # Low similarity (irrelevant pair)
),
# ... 1000s of domain-specific examples
]
train_dataloader = DataLoader(training_data, shuffle=True, batch_size=16)
# Step 3: Fine-tune
train_loss = losses.CosineSimilarityLoss(model=base_model)
base_model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=1,
warmup_steps=100
)
# Step 4: Evaluate on domain test set
domain_test_set = [...] # Query-doc pairs with relevance labels
metrics = evaluate_ndcg(base_model, domain_test_set)
print(f"Fine-tuned model NDCG@5: {metrics['ndcg']:.3f}")
Cost-benefit:
| Aspect | Cost | Benefit |
|---|---|---|
| Training data | 500-1000 labeled pairs | +10-30% Recall@k |
| Compute | 1-2 GPU hours | Domain-optimized |
| Ongoing | Needs retraining on new data | Stays relevant |
Strategy 2: Asymmetric Embeddings
Use models designed for query-document asymmetry (e.g., e5-large-v2, text-embedding-3):
from sentence_transformers import SentenceTransformer
# Instruction-tuned model handles asymmetry
model = SentenceTransformer('intfloat/e5-large-v2')
query_embedding = model.encode(
"query: How to implement JWT authentication?",
convert_to_tensor=True
)
doc_embedding = model.encode(
"passage: Bearer tokens and JSON Web Tokens enable stateless authentication...",
convert_to_tensor=True
)
similarity = cosine_similarity([query_embedding], [doc_embedding])[0][0]
# Higher than non-instruction-tuned model due to asymmetric encoding
# Compared to non-asymmetric:
model_basic = SentenceTransformer('all-mpnet-base-v2')
sim_basic = 0.72
# Asymmetric: 0.88, improvement = +16%
Instruction formats vary by model:
# e5 models
query_emb = model.encode("query: " + query_text)
doc_emb = model.encode("passage: " + doc_text)
# text-embedding-3 (OpenAI)
# Built-in asymmetry, no special prompt needed
query_emb = model.encode(query_text)
doc_emb = model.encode(doc_text)
# BGE models
query_emb = model.encode("Represent this query for searching: " + query_text)
doc_emb = model.encode("Represent this document for searching: " + doc_text)
Strategy 3: Hybrid Query and Document Expansion
Expand queries and documents to reduce mismatch:
def expand_for_mismatch_mitigation(text, text_type='query', llm_model=None):
"""Expand text with synonyms and related concepts."""
if text_type == 'query':
prompt = f"""Generate 2 alternative phrasings of this question that preserve meaning:
"{text}"
Return only the phrasings, one per line."""
elif text_type == 'document':
prompt = f"""Generate a 1-sentence summary and 2 key concept keywords for this text:
"{text[:200]}..."
Return in format:
Summary: <summary>
Keywords: <keyword1>, <keyword2>"""
expansions = llm_model(prompt)
return expansions
# Example
query = "How to authenticate users?"
expanded = expand_for_mismatch_mitigation(query, 'query')
# "How do I authenticate users?" + "User authentication methods" + "Login mechanisms"
# Embed both original and expanded, take max similarity
original_sim = compute_similarity(query, doc, model)
expanded_sims = [
compute_similarity(expanded, doc, model)
for expanded in expansions
]
best_similarity = max([original_sim] + expanded_sims)
Strategy 4: Multi-Modal or Instruction-Tuned Models
Use models trained on diverse tasks/modalities:
# MTEB-evaluated models (tested across 50+ tasks)
# Less likely to have domain-specific mismatch
models_ranked = [
('text-embedding-3-large', 0.86), # Best overall
('text-embedding-3-small', 0.85),
('bge-large-en-v1.5', 0.84),
('e5-large-v2', 0.83),
('all-mpnet-base-v2', 0.77), # Older, less robust
]
# For domain, check MTEB performance on similar tasks
# If similar task exists and score is high (>0.75), lower mismatch risk
Strategy 5: Domain-Specific Pre-Training
If you have large unlabeled domain corpus, pre-train embeddings:
from sentence_transformers import SentenceTransformer, models
# Step 1: Load unlabeled domain documents
domain_corpus = [
"Database sharding distributes data across multiple servers...",
"Horizontal scaling adds more database replicas...",
# ... thousands of domain documents
]
# Step 2: Use contrastive loss with in-domain corpus
# Create synthetic pairs: anchor + positive (from same document) + negatives
# Step 3: Fine-tune on unlabeled data using self-supervised loss
model = SentenceTransformer('all-mpnet-base-v2')
# This is expensive but highly effective for specialized domains
# Improves recall@5 by 15-25% on that domain
Strategy 6: Metadata-Guided Retrieval
Use document metadata to disambiguate and reduce mismatch:
def metadata_aware_retrieval(query, embedding_model, metadata_filters=None):
"""Retrieve with embedding + metadata constraints."""
query_emb = embedding_model.encode(query)
# Retrieve from filtered subset
candidates = db.search(
query_emb,
k=100,
filters=metadata_filters # e.g., {'domain': 'finance', 'year': 2024}
)
# Re-rank within filtered set
# Avoids matching high similarity scores from unrelated domains
return candidates[:5]
# Example
# Query: "What is the Fed rate?"
# Without metadata: Might return "Federal Investigation Bureau" docs
# With metadata {domain='finance'}: Returns only financial docs
Comparison of Strategies
| Strategy | Effort | Time to Deploy | Quality Gain | Best For |
|---|---|---|---|---|
| Switch to better model | Low (just change model) | 1 day | +5-15% | Quick win, applicable to all domains |
| Fine-tune on labeled data | Medium (need 500-1000 pairs) | 1 week | +10-30% | Specialized domains with budget |
| Use asymmetric model | Low | 1 day | +5-10% | Query-heavy workloads |
| Query expansion | Low | 1 day | +3-8% | Complementary to other strategies |
| Pre-train on corpus | High (GPU-intensive) | 2-4 weeks | +15-25% | Large private corpus, long-term |
| Metadata filtering | Medium | 1 week | +5-10% | Heterogeneous knowledge bases |
Q7. How do you choose and evaluate embedding models for your domain? [Advanced]
π‘ Show Answer
Answer:
Evaluation Workflow
Step 1: Identify Similar MTEB Benchmark Tasks
# MTEB benchmark includes 50+ datasets across domains
# Find tasks similar to your domain
your_domain = "medical QA"
mteb_similar_tasks = [
('TREC-COVID', 'medical retrieval'),
('SciFact', 'scientific fact verification'),
('NF-Corpus', 'scientific full text'),
]
# Check which models perform best on similar tasks
mteb_rankings = {
'text-embedding-3-large': {'TREC-COVID': 0.74, 'SciFact': 0.72},
'text-embedding-3-small': {'TREC-COVID': 0.71, 'SciFact': 0.69},
'bge-large-en-v1.5': {'TREC-COVID': 0.70, 'SciFact': 0.68},
}
# Models ranking high on similar tasks likely good for your domain
Step 2: Create Domain-Specific Evaluation Set
def create_evaluation_set(domain, num_queries=200):
"""
Create labeled (query, document) pairs from your domain.
Methods:
1. Manual: Experts label 200 query-doc pairs
2. Semi-automatic: Use existing QA datasets in domain + label gaps
3. Synthetic: Use LLM to generate queries from docs
"""
eval_set = []
# Method 1: Manual labeling (highest quality)
for doc in domain_documents:
query = input(f"Write a query relevant to: {doc[:100]}...")
eval_set.append({
'query': query,
'documents': [
{'text': doc, 'relevant': True},
{'text': random_other_doc, 'relevant': False},
{'text': random_other_doc2, 'relevant': False},
]
})
return eval_set
# Example: Medical domain
eval_set_medical = [
{
'query': 'What are the contraindications for metformin?',
'documents': [
{'text': 'Metformin is contraindicated in renal failure...', 'relevant': True},
{'text': 'Insulin is used for Type 1 diabetes...', 'relevant': False},
]
},
# ... 200 queries
]
Step 3: Compute Metrics on Your Evaluation Set
from sentence_transformers import SentenceTransformer
import numpy as np
def evaluate_embedding_model(model_name, eval_set):
"""Evaluate model on your domain-specific eval set."""
model = SentenceTransformer(model_name)
ndcg_scores = []
recall_scores = []
for item in eval_set:
query = item['query']
documents = item['documents']
# Encode query and documents
query_emb = model.encode(query, convert_to_tensor=True)
doc_embeddings = model.encode(
[d['text'] for d in documents],
convert_to_tensor=True
)
# Compute similarities
similarities = (query_emb @ doc_embeddings.T).cpu().numpy()
# Rank documents
ranked_indices = np.argsort(-similarities[0])
# Compute NDCG@5
dcg = sum(
(1 if documents[i]['relevant'] else 0) / np.log2(rank+2)
for rank, i in enumerate(ranked_indices[:5])
)
idcg = sum(1 / np.log2(i+2) for i in range(min(5, sum(1 for d in documents if d['relevant']))))
ndcg = dcg / idcg if idcg > 0 else 0
ndcg_scores.append(ndcg)
# Compute Recall@5
relevant_indices = {i for i, d in enumerate(documents) if d['relevant']}
retrieved_indices = set(ranked_indices[:5])
recall = len(relevant_indices & retrieved_indices) / len(relevant_indices) if relevant_indices else 0
recall_scores.append(recall)
return {
'model': model_name,
'ndcg@5': np.mean(ndcg_scores),
'recall@5': np.mean(recall_scores),
'ndcg_std': np.std(ndcg_scores),
}
# Compare multiple models
models_to_evaluate = [
'all-mpnet-base-v2',
'intfloat/e5-base-v2',
'intfloat/e5-large-v2',
'BAAI/bge-large-en-v1.5',
'text-embedding-3-small',
]
results = []
for model_name in models_to_evaluate:
try:
metrics = evaluate_embedding_model(model_name, eval_set_medical)
results.append(metrics)
print(f"{model_name}: NDCG@5={metrics['ndcg@5']:.3f}, Recall@5={metrics['recall@5']:.3f}")
except Exception as e:
print(f"{model_name}: Failed ({e})")
# Rank models
ranked = sorted(results, key=lambda x: x['ndcg@5'], reverse=True)
for rank, model in enumerate(ranked, 1):
print(f"{rank}. {model['model']}: {model['ndcg@5']:.3f}")
Step 4: Cost-Latency Analysis
def analyze_embedding_cost_latency(model_names, corpus_size_docs=100000):
"""Compare cost and latency of different models."""
analysis = {}
for model_name in model_names:
model = SentenceTransformer(model_name)
# Latency per embedding
import time
sample_texts = ["Sample document"] * 100
start = time.time()
model.encode(sample_texts)
elapsed = time.time() - start
latency_per_doc_ms = (elapsed / 100) * 1000
# Model size (proxy for memory and cost)
model_params = sum(p.numel() for p in model.parameters()) / 1e6 # millions
# Cost estimates
indexing_cost = (corpus_size_docs / 1000) * 0.01 * (model_params / 100) # arbitrary units
query_latency_ms = latency_per_doc_ms * 1.5 # queries often slower
analysis[model_name] = {
'latency_ms': query_latency_ms,
'model_size_params_m': model_params,
'relative_cost': indexing_cost,
}
return analysis
# Example output
analysis = analyze_embedding_cost_latency(models_to_evaluate)
for model, metrics in analysis.items():
print(f"{model}: {metrics['latency_ms']:.1f}ms latency, {metrics['model_size_params_m']:.0f}M params")
Decision Matrix
Choose based on your constraints:
def recommend_model(quality_target, latency_target_ms, cost_budget):
"""Recommend model based on constraints."""
candidates = {
'text-embedding-3-large': {
'quality': 0.90, # Best
'latency': 50,
'cost': 'high',
},
'text-embedding-3-small': {
'quality': 0.87,
'latency': 20,
'cost': 'low',
},
'intfloat/e5-large-v2': {
'quality': 0.88,
'latency': 80, # Slower
'cost': 'low',
},
'BAAI/bge-large-en-v1.5': {
'quality': 0.85,
'latency': 70,
'cost': 'low',
},
'all-mpnet-base-v2': {
'quality': 0.78,
'latency': 15,
'cost': 'very low',
},
}
recommendations = []
for model_name, metrics in candidates.items():
if (metrics['quality'] >= quality_target and
metrics['latency'] <= latency_target_ms):
recommendations.append((model_name, metrics))
# Rank by cost
recommendations.sort(key=lambda x: x[1]['cost'])
if recommendations:
return recommendations[0][0]
else:
return None # No model meets all constraints
Final Recommendation Table
| Scenario | Recommended Models | Rationale |
|---|---|---|
| Production, high quality | text-embedding-3-large, e5-large-v2 | MTEB #1 ranked, domain-specific fine-tuning possible |
| Production, low latency | text-embedding-3-small, all-mpnet-base-v2 | <20ms latency, reasonable quality |
| Domain-specific (medical, finance) | e5-large-v2 + fine-tuning | Fine-tune on labeled domain data for +15-25% improvement |
| Multilingual | multilingual-e5-large, text-embedding-3 | Support 50+ languages |
| Open-source only | bge-large-en-v1.5, e5-large-v2 | Best open models, no API dependency |
| Very large scale (>1B docs) | text-embedding-3-small, all-MiniLM | Balance indexing cost and quality |
Q8. How do you implement fine-tuning of embeddings for your domain? [Advanced]
π‘ Show Answer
Answer:
Full Fine-Tuning Workflow
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
import numpy as np
from sklearn.metrics import ndcg_score
class DomainEmbeddingFinetuner:
def __init__(self, base_model_name='all-mpnet-base-v2', domain_name='medical'):
self.base_model = SentenceTransformer(base_model_name)
self.domain_name = domain_name
def prepare_training_data(self, query_doc_pairs_with_labels):
"""
Convert labeled data to InputExample format.
Input format:
[{
'query': 'How to treat diabetes?',
'doc': 'Insulin therapy is...',
'similarity': 0.95, # 0-1, 1=highly relevant
}, ...]
"""
training_examples = []
for pair in query_doc_pairs_with_labels:
example = InputExample(
texts=[pair['query'], pair['doc']],
label=pair['similarity']
)
training_examples.append(example)
return training_examples
def finetune(self, training_data, val_data, epochs=1, batch_size=16):
"""Fine-tune the embedding model."""
# Prepare data
train_examples = self.prepare_training_data(training_data)
# Create data loader
train_dataloader = DataLoader(
train_examples,
shuffle=True,
batch_size=batch_size
)
# Define loss function (regression on similarity scores)
train_loss = losses.CosineSimilarityLoss(
model=self.base_model,
tie_p=0.05 # Regularization
)
# Fine-tune
self.base_model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=epochs,
warmup_steps=len(train_examples) // (batch_size * 10),
output_path=f'./checkpoints/{self.domain_name}-embeddings',
save_best_model=True,
evaluator=None, # Optional: add dev set evaluator
)
def evaluate_on_test_set(self, test_data):
"""Evaluate fine-tuned model."""
ndcg_scores = []
for item in test_data:
query = item['query']
docs = item['documents'] # List of dicts with 'text' and 'relevant'
# Encode
query_emb = self.base_model.encode(query, convert_to_tensor=True)
doc_embeddings = self.base_model.encode(
[d['text'] for d in docs],
convert_to_tensor=True
)
# Compute similarities
similarities = (query_emb @ doc_embeddings.T).cpu().numpy()[0]
# Create relevance labels
y_true = [1 if d['relevant'] else 0 for d in docs]
# Compute NDCG@5
y_scores = similarities
ndcg = ndcg_score([y_true], [y_scores], k=5)
ndcg_scores.append(ndcg)
return {
'ndcg@5': np.mean(ndcg_scores),
'ndcg_std': np.std(ndcg_scores),
}
# Usage example
finetuner = DomainEmbeddingFinetuner(base_model_name='all-mpnet-base-v2', domain_name='medical')
# Prepare training data
training_pairs = [
{'query': 'Metformin contraindications?', 'doc': 'Metformin is contraindicated in renal failure...', 'similarity': 0.98},
{'query': 'Metformin contraindications?', 'doc': 'Insulin is used for Type 1 diabetes...', 'similarity': 0.05},
# ... 500-1000 pairs
]
validation_pairs = [...] # Separate set for monitoring
# Fine-tune
finetuner.finetune(training_pairs, validation_pairs, epochs=1, batch_size=32)
# Evaluate
test_data = [...]
metrics = finetuner.evaluate_on_test_set(test_data)
print(f"Fine-tuned NDCG@5: {metrics['ndcg@5']:.3f}")
# Compare to baseline
baseline_model = SentenceTransformer('all-mpnet-base-v2')
baseline_metrics = finetuner.evaluate_on_test_set_with_model(test_data, baseline_model)
print(f"Baseline NDCG@5: {baseline_metrics['ndcg@5']:.3f}")
print(f"Improvement: {(metrics['ndcg@5'] - baseline_metrics['ndcg@5']) / baseline_metrics['ndcg@5']:.1%}")
Data Collection Strategies
Strategy 1: Mining from Existing QA Logs
def mine_positive_pairs_from_logs(qa_logs, similarity_threshold=0.85):
"""
Extract positive (query, doc) pairs from user interactions.
Heuristic: If user spent time reading a doc after searching a query,
it's likely relevant.
"""
positive_pairs = []
for log in qa_logs:
query = log['query']
clicked_doc_id = log['clicked_doc'] # User clicked this
clicked_doc = fetch_doc(clicked_doc_id)
dwell_time = log['dwell_time_seconds']
# If user spent >30 seconds on doc, assume relevant
if dwell_time > 30:
positive_pairs.append({
'query': query,
'doc': clicked_doc['text'],
'similarity': 0.95, # High confidence
})
return positive_pairs
Strategy 2: Weak Labeling
def weak_label_pairs(documents, embedding_model):
"""
Use heuristics to automatically label (query, doc) pairs.
Heuristic: If doc text contains several query terms, it's relevant.
"""
pairs = []
for doc in documents:
# Generate synthetic query from doc (using LLM or extraction)
query = generate_query_from_doc(doc)
# If query β doc retrieval works well, doc is good for query
# This creates positive (query, doc) pair
pairs.append({
'query': query,
'doc': doc['text'],
'similarity': 0.85, # Medium-high confidence
})
return pairs
Strategy 3: Expert Annotation
def create_expert_labeled_set(num_queries=200, domain='medical'):
"""
Have domain experts label query-document relevance.
High-quality but expensive approach.
"""
pairs = []
for _ in range(num_queries):
query = get_random_query(domain)
candidate_docs = retrieve_candidates(query, k=10)
# Expert labels relevance: 0 (irrelevant), 0.5 (partial), 1 (highly relevant)
for doc in candidate_docs:
label = expert_label_relevance(query, doc) # Human input
pairs.append({
'query': query,
'doc': doc['text'],
'similarity': float(label),
})
return pairs
Monitoring Fine-Tuning Quality
def monitor_finetuning_progress(finetuned_model, baseline_model, validation_set):
"""Track improvement over baseline during fine-tuning."""
finetuned_ndcg = evaluate_model(finetuned_model, validation_set)
baseline_ndcg = evaluate_model(baseline_model, validation_set)
improvement = (finetuned_ndcg - baseline_ndcg) / baseline_ndcg
print(f"Baseline NDCG@5: {baseline_ndcg:.3f}")
print(f"Fine-tuned NDCG@5: {finetuned_ndcg:.3f}")
print(f"Improvement: {improvement:.1%}")
# Check for overfitting
if improvement > 0.50: # >50% gain seems too good
print("β οΈ Possible overfitting, validation set may be leaking to training")
return improvement
Trade-offs
| Aspect | Cost | Benefit |
|---|---|---|
| Data collection | 10-50 hours (expert annotation) | Precise labels |
| Fine-tuning | 1-2 GPU hours | +10-30% domain-specific improvement |
| Reindexing | 2-4 hours (for large corpus) | Embeddings must be recomputed |
| Maintenance | Periodic retraining on new data | Keeps model fresh |
When NOT to fine-tune:
- Domain is very general (news, web search) β base model already good
- Training data is small (<100 labeled pairs) β risk of overfitting
- Latency is critical β fine-tuning may slow down inference
Q9. How do you prevent embedding mismatch in multi-domain or evolving systems? [Advanced]
π‘ Show Answer
Answer:
Challenge: Multi-Domain Systems
When a single embedding model serves multiple domains (e.g., product catalog for both medical and retail), mismatch risk is high:
Shared embedding model:
- Medical domain: Low performance (0.65 NDCG@5)
- Retail domain: Medium performance (0.75 NDCG@5)
- Overall: Suboptimal for both
Domain-specific models:
- Medical: 0.85 NDCG@5
- Retail: 0.88 NDCG@5
- But: 2x complexity, 2x cost
Strategy 1: Domain-Aware Retrieval
Use metadata to route queries to appropriate embeddings:
class DomainAwareRetriever:
def __init__(self):
self.embeddings = {
'medical': SentenceTransformer('scifact-embeddings-finetuned'),
'retail': SentenceTransformer('retail-product-embeddings-finetuned'),
'default': SentenceTransformer('all-mpnet-base-v2'),
}
def retrieve(self, query, documents):
"""Detect domain from query, use appropriate embeddings."""
# Detect domain
domain = detect_domain(query) # Rule-based or ML-based classifier
# Select appropriate embedding model
embedding_model = self.embeddings.get(domain, self.embeddings['default'])
# Retrieve with domain-specific model
query_emb = embedding_model.encode(query)
results = vector_search(query_emb, documents, k=5)
return results
def detect_domain(query):
"""Detect domain from query text."""
# Simple rule-based approach
medical_keywords = {'contraindications', 'diagnosis', 'treatment', 'metformin'}
retail_keywords = {'price', 'shipping', 'product', 'buy'}
query_lower = query.lower()
if any(kw in query_lower for kw in medical_keywords):
return 'medical'
elif any(kw in query_lower for kw in retail_keywords):
return 'retail'
else:
return 'default'
Strategy 2: Adapter Modules (Parameter-Efficient Fine-Tuning)
Instead of fine-tuning entire model for each domain, use lightweight adapters:
from sentence_transformers import SentenceTransformer
import torch.nn as nn
class EmbeddingWithAdapters:
def __init__(self, base_model_name='all-mpnet-base-v2', domains=['medical', 'retail']):
self.base_model = SentenceTransformer(base_model_name)
self.adapters = nn.ModuleDict({
domain: self.create_adapter() for domain in domains
})
def create_adapter(self, input_dim=768, reduction=8):
"""Lightweight adapter: linear β ReLU β linear."""
return nn.Sequential(
nn.Linear(input_dim, input_dim // reduction),
nn.ReLU(),
nn.Linear(input_dim // reduction, input_dim)
)
def encode(self, texts, domain='default'):
"""Encode with domain-specific adapter."""
# Get base embedding
embeddings = self.base_model.encode(texts)
# Apply adapter
if domain in self.adapters:
adapter = self.adapters[domain]
adapted_embeddings = embeddings + adapter(embeddings) # Residual connection
return adapted_embeddings
else:
return embeddings
# Fine-tuning adapters (much cheaper than full fine-tuning)
# Each adapter: ~50k parameters vs ~200M for full model
Strategy 3: Continuous Evaluation and Drift Detection
Monitor embedding quality over time:
class EmbeddingQualityMonitor:
def __init__(self, baseline_metrics_per_domain):
self.baseline = baseline_metrics_per_domain # e.g., {'medical': 0.85, 'retail': 0.82}
def check_drift(self, domain, current_ndcg, window_hours=24):
"""Detect if embedding quality is degrading."""
baseline = self.baseline[domain]
# If current NDCG drops >5% from baseline, flag drift
if current_ndcg < baseline * 0.95:
print(f"β οΈ Embedding drift detected in {domain} domain")
print(f" Baseline: {baseline:.3f}, Current: {current_ndcg:.3f}")
log_drift_alert(domain, baseline, current_ndcg)
return True
return False
def continuous_quality_monitoring(retriever, eval_sets_by_domain):
"""Continuously monitor embedding quality."""
monitor = EmbeddingQualityMonitor(baseline_metrics_per_domain)
while True:
for domain, eval_set in eval_sets_by_domain.items():
ndcg = evaluate_domain(retriever, domain, eval_set)
monitor.check_drift(domain, ndcg)
log_metric(f'embedding_quality_{domain}', ndcg)
time.sleep(3600) # Check hourly
Strategy 4: Online Hard Negative Mining
Continuously improve embeddings by mining challenging examples:
def online_hard_negative_mining(query, retrieval_results, ground_truth_relevant):
"""
If retrieval returns incorrect result ranked high, that's a hard negative.
Use it to fine-tune embeddings.
"""
hard_negatives = []
for result in retrieval_results[:5]:
if result['id'] not in ground_truth_relevant:
# This was ranked high but is actually irrelevant
# It's a hard negative
hard_negatives.append({
'query': query,
'doc': result['text'],
'similarity': 0.0, # Definitely irrelevant
})
# Use hard negatives to refine embeddings
if hard_negatives:
refine_embeddings_with_hard_negatives(hard_negatives)
def refine_embeddings_with_hard_negatives(hard_negatives, batch_size=32):
"""
Periodically retrain embeddings with collected hard negatives.
This prevents model from forgetting how to distinguish tricky cases.
"""
# Collect over time (e.g., daily)
hard_neg_batch = hard_negatives
if len(hard_neg_batch) >= batch_size:
# Fine-tune embedding model on hard negatives
# (same fine-tuning process, but with hard examples)
print(f"Refining embeddings with {len(hard_neg_batch)} hard negatives...")
# finetuner.finetune(hard_neg_batch, epochs=1)
Strategy 5: Temporal Adaptation
Handle concept drift as terminology evolves:
def handle_temporal_drift(embedding_model, documents_over_time):
"""
As new terminology emerges (e.g., "GenAI" ~2023), model may become stale.
Periodically retrain on recent data.
"""
# Group documents by era
docs_2022 = [d for d in documents_over_time if d['year'] == 2022]
docs_2023 = [d for d in documents_over_time if d['year'] == 2023]
docs_2024 = [d for d in documents_over_time if d['year'] == 2024]
# Test on recent data
recent_ndcg = evaluate_model(embedding_model, docs_2024)
old_ndcg = evaluate_model(embedding_model, docs_2022)
if recent_ndcg < old_ndcg * 0.90:
print(f"β οΈ Temporal drift: Performance on 2024 data ({recent_ndcg:.3f}) < 2022 ({old_ndcg:.3f})")
print(" Retraining on recent data...")
# Retrain on recent documents
recent_data = docs_2023 + docs_2024
finetuner.finetune(recent_data, epochs=1)
Monitoring Checklist
embedding_quality_slo = {
'all_domains': {
'minimum_ndcg@5': 0.75,
'maximum_domain_variance': 0.10, # Don't let some domains degrade
},
'per_domain': {
'medical': {'minimum_ndcg@5': 0.82},
'retail': {'minimum_ndcg@5': 0.80},
},
'drift_detection': {
'check_frequency': '1h',
'allowed_degradation': 0.05, # 5% drop before alert
},
'retraining': {
'frequency': 'quarterly',
'trigger_on_drift': True,
}
}
Q10. What is the cost-performance-latency trade-off for embedding solutions, and how do you optimize? [Advanced]
π‘ Show Answer
Answer:
Embedding systems involve a three-way trade-off: cost (inference + storage), quality (NDCG/Recall), and latency (query response time).
Cost Breakdown
def estimate_embedding_system_cost(
corpus_size_gb=100,
monthly_queries=1_000_000,
embedding_model='all-mpnet-base-v2',
):
"""Estimate annual cost of embedding retrieval system."""
# Document statistics
avg_doc_chars = 5000
num_docs = (corpus_size_gb * 1e9) / avg_doc_chars
embedding_dim = 768 # typical
# Embedding generation cost (one-time indexing + queries)
embedding_generation_cost_per_call = 0.00001 # Assuming efficient inference
# Initial indexing: embed all documents
indexing_cost = num_docs * embedding_generation_cost_per_call
# Query-time: embed every query
query_embeddings_cost = monthly_queries * 12 * embedding_generation_cost_per_call
total_embedding_cost = indexing_cost + query_embeddings_cost
# Storage cost
embedding_storage_gb = (num_docs * embedding_dim * 4 bytes) / 1e9 # float32
vector_db_cost_per_gb_per_year = 10 # Pinecone, Weaviate, etc.
annual_storage_cost = embedding_storage_gb * vector_db_cost_per_gb_per_year
# Compute cost (GPU/CPU inference)
annual_compute_cost = 50_000 # Rough: dedicated GPU + infrastructure
# Total
total_annual_cost = total_embedding_cost + annual_storage_cost + annual_compute_cost
cost_per_query = total_annual_cost / (monthly_queries * 12)
return {
'embedding_cost': total_embedding_cost,
'storage_cost': annual_storage_cost,
'compute_cost': annual_compute_cost,
'total_annual': total_annual_cost,
'cost_per_query': cost_per_query,
'cost_per_doc': total_annual_cost / num_docs,
}
# Example: 100GB corpus, 1M monthly queries
cost = estimate_embedding_system_cost(corpus_size_gb=100, monthly_queries=1_000_000)
print(f"Annual cost: ${cost['total_annual']:.2f}")
print(f"Cost per query: ${cost['cost_per_query']:.6f}")
print(f"Cost per document: ${cost['cost_per_doc']:.6f}")
# Output (approximate):
# Annual cost: $125,000
# Cost per query: $0.00104
# Cost per document: $0.00125
Model Selection: Cost vs. Quality
# Embed models ranked by MTEB score (quality) and inference speed (latency)
models = {
'all-MiniLM-L6-v2': {
'quality': 0.76,
'latency_ms': 10,
'params_m': 22,
'relative_cost': 1.0, # Baseline
},
'all-mpnet-base-v2': {
'quality': 0.81,
'latency_ms': 50,
'params_m': 109,
'relative_cost': 1.5,
},
'intfloat/e5-small-v2': {
'quality': 0.78,
'latency_ms': 15,
'params_m': 33,
'relative_cost': 1.2,
},
'intfloat/e5-large-v2': {
'quality': 0.88,
'latency_ms': 80,
'params_m': 335,
'relative_cost': 3.0,
},
'text-embedding-3-small': {
'quality': 0.87,
'latency_ms': 40, # Typical for API
'params_m': 100,
'relative_cost': 2.0, # API pricing
},
'text-embedding-3-large': {
'quality': 0.92,
'latency_ms': 60,
'params_m': 200,
'relative_cost': 4.0,
},
}
# Cost-quality Pareto frontier
def find_pareto_frontier(models):
"""Models where you can't improve quality without increasing cost."""
pareto = []
for name, metrics in models.items():
is_dominated = False
for other_name, other in models.items():
if other_name == name:
continue
# Is other model strictly better (higher quality, lower cost)?
if (other['quality'] > metrics['quality'] and
other['relative_cost'] < metrics['relative_cost']):
is_dominated = True
break
if not is_dominated:
pareto.append((name, metrics))
return pareto
frontier = find_pareto_frontier(models)
print("Pareto frontier (optimal models):")
for name, metrics in sorted(frontier, key=lambda x: x[1]['relative_cost']):
print(f" {name}: quality={metrics['quality']:.2f}, cost={metrics['relative_cost']:.1f}x")
# Output:
# Pareto frontier (optimal models):
# all-MiniLM-L6-v2: quality=0.76, cost=1.0x
# intfloat/e5-small-v2: quality=0.78, cost=1.2x
# all-mpnet-base-v2: quality=0.81, cost=1.5x
# intfloat/e5-large-v2: quality=0.88, cost=3.0x
# text-embedding-3-large: quality=0.92, cost=4.0x
Optimization Strategies
Strategy 1: Selective Fine-Tuning
Instead of fine-tuning the largest model, fine-tune a smaller one:
Without fine-tuning:
- all-mpnet-base-v2: NDCG=0.81, cost=1.5x
With fine-tuning on domain data:
- all-MiniLM + fine-tuning: NDCG=0.85 (+4%), cost=1.0x + fine-tune overhead
- all-mpnet + fine-tuning: NDCG=0.88 (+7%), cost=1.5x + fine-tune overhead
Result: Fine-tuned small model β large model quality at lower cost
Strategy 2: Caching and Pre-computation
Cache embeddings for frequently queried documents:
class CachedEmbeddingRetriever:
def __init__(self, embedding_model):
self.model = embedding_model
self.embedding_cache = {} # {doc_id: embedding}
def retrieve(self, query, documents, use_cache=True):
"""Retrieve with optional embedding cache."""
query_embedding = self.model.encode(query) # Always compute query
for doc in documents:
doc_id = doc['id']
# Check cache
if doc_id in self.embedding_cache and use_cache:
doc_embedding = self.embedding_cache[doc_id]
else:
doc_embedding = self.model.encode(doc['text'])
self.embedding_cache[doc_id] = doc_embedding
similarity = cosine_similarity([query_embedding], [doc_embedding])[0][0]
# ...
Strategy 3: Batch Processing for Queries
For asynchronous systems, batch queries to amortize embedding cost:
async def batch_retrieve(queries, batch_size=100):
"""Process queries in batches for efficiency."""
all_results = []
for i in range(0, len(queries), batch_size):
batch = queries[i:i+batch_size]
# Batch embed (more efficient than single queries)
query_embeddings = embedding_model.encode(batch, batch_size=batch_size)
# Retrieve for each query
for query, qe in zip(batch, query_embeddings):
results = vector_search(qe, documents, k=5)
all_results.append(results)
return all_results
# Latency: 100 queries: ~200ms (batched) vs ~5000ms (sequential)
Strategy 4: Dimension Reduction
Use dimensionality reduction (PCA) to reduce embedding size:
from sklearn.decomposition import PCA
# Original embeddings: 768 dimensions
full_embeddings = embedding_model.encode(documents)
# Reduce to 256 dimensions (67% smaller)
pca = PCA(n_components=256)
reduced_embeddings = pca.fit_transform(full_embeddings)
# Storage savings: 768 β 256 bytes per embedding (67% reduction)
# Quality loss: Typically <2% NDCG drop
# Trade-off: Tiny quality loss for significant storage/latency savings
Strategy 5: Hybrid Retrieval with Sparse-First Filtering
Use BM25 to pre-filter, then dense reranking on subset:
Traditional (all dense):
Query β Embed query (10ms) β Dense search 1M docs (50ms) = 60ms
Hybrid with sparse pre-filter:
Query β BM25 filter to 1000 (5ms) β Embed query (10ms) β
Dense search 1000 (20ms) β Rerank top-20 (10ms) = 45ms
Result: 25% latency reduction, same quality
Decision Matrix
Choose based on your SLOs:
| SLO | Recommended Approach | Estimated Cost |
|---|---|---|
| Quality β₯ 0.85 NDCG, Latency < 50ms | all-mpnet-base-v2 + hybrid retrieval | 1.5x |
| Quality β₯ 0.90 NDCG, Latency < 100ms | e5-large-v2 fine-tuned + caching | 3.5x |
| Quality β₯ 0.92 NDCG, Cost-insensitive | text-embedding-3-large + domain fine-tune | 4.0x |
| Latency-critical (<20ms), Quality β₯ 0.75 | all-MiniLM-L6-v2 + sparse-first + cache | 1.0x |
| Low-cost, best effort (< $10k/year) | all-MiniLM + sparse retrieval only | 0.8x |
ROI Analysis: Is Fine-Tuning Worth It?
def calculate_finetuning_roi(baseline_quality, finetuned_quality, annual_queries, cost_per_quality_point=1000):
"""Calculate ROI of fine-tuning embeddings."""
quality_improvement = (finetuned_quality - baseline_quality) * 100 # percentage points
finetuning_cost = 5000 # One-time
# Value of improvement (assuming each quality point is worth $X)
improvement_value = quality_improvement * cost_per_quality_point
roi = (improvement_value - finetuning_cost) / finetuning_cost
print(f"Quality improvement: {quality_improvement:.1f}%")
print(f"Finetuning cost: ${finetuning_cost:.0f}")
print(f"Improvement value: ${improvement_value:.0f}")
print(f"ROI: {roi:.0%}")
return roi > 0 # Worth it if positive ROI
# Example: Improve from 0.78 to 0.86 NDCG
# 8 percentage points * $1000/point = $8000 benefit
# Cost: $5000
# ROI: (+$8000 - $5000) / $5000 = 60% β Worth it