Lessons from a real-world legal-tech RAG system evaluation, generalized for practitioners in specialized domains (legal, medical, finance, scientific research, enterprise knowledge management).
Introduction
When most teams start building a retrieval-augmented generation (RAG) system, they begin with a comfortable assumption: “I’ll use the best-performing model from MTEB or BEIR, plug it in, and ship.”
This assumption fails hard in specialized domains.
Public benchmarks (MTEB, BEIR, others) are designed for general text retrieval. They excel at predicting model performance on web search, academic papers, and news articles. But if your domain has:
- Specialized vocabulary and query patterns (citations, technical references, codes)
- Domain-specific ground truth (legal relevance ≠ web search relevance)
- Operational constraints (quota limits, latency requirements, cost sensitivity)
…then the #1 model on MTEB might rank #12 on your domain, and you’d discover it only after shipping to production.
This article documents what a real-world domain-specific RAG evaluation looks like: the process, the pitfalls, the trade-offs, and the decisions. It’s based on a detailed examination of a production benchmark built for a legal-tech platform — a large, multi-million-chunk corpus evaluated across several retrieval sub-tasks, with hundreds of queries and dozens of configurations tested.
The lessons are domain-agnostic. Whether you’re building RAG for legal, medical, finance, or enterprise knowledge management, this framework applies.
Part 1: Why Public Benchmarks Fail in Specialized Domains
The MTEB/BEIR Problem
MTEB (Massive Text Embedding Benchmark) includes 56 datasets, mostly English, with heavy weighting toward:
- General web search
- Academic paper retrieval
- News article classification
- Semantic textual similarity on generic text
BEIR (Benchmark for Information Retrieval) is broader but follows similar patterns: strong on general IR, sparse on specialized domains.
What’s missing:
- Specialized languages (MTEB has minimal German, Mandarin, Japanese legal/medical content)
- Specialized retrieval tasks (statute citation lookup, medical billing codes, financial ticker searches)
- Domain-specific quality metrics (legal relevance ≠ “topically similar”)
The Real-World Gap
Here’s what we observed in a legal-tech platform evaluation:
MTEB top performer: An embedding model that ranked #1–3 on MTEB benchmarks.
Domain-specific benchmark result: Same model ranked #12–15 on a 462-query legal-domain evaluation.
Why: MTEB tests semantic similarity on essays and web articles. Domain evaluation tested:
- Statute citation lookup (“Find the court decision interpreting § 85 ZPO”)
- Headnote matching (structured legal summaries)
- Open-ended legal questions
On citation queries specifically:
- Lexical (BM25) retrieval: 0.36 nDCG@10
- Top MTEB dense model: 0.07 nDCG@10
- BM25 wins by 5×
MTEB doesn’t test this pattern because web search doesn’t have “statute citation queries.” Legal does.
The Cost of Assuming
If you ship based on MTEB alone:
- You pick the wrong model (high cost, low quality)
- You discover it only after production deployment
- You’re now stuck re-embedding a 9M-document corpus and re-indexing (cost: $700–1,000 + engineering time)
- Users experience quality regression
Lesson: Public benchmarks are useful sanity checks (“is this model terrible on general text?”), but they’re not sufficient for domain-specific decisions.
Part 2: Building a Domain-Specific Benchmark
Step 1: Identify your core retrieval tasks
Before you build anything, answer: What are the 3–5 most important things your users search for?
For a legal system, this might be:
- Citation lookups (“Find decisions citing statute X”)
- Headnote search (“Find decisions with headnote about topic Y”)
- Free-text legal questions (“How do courts interpret statute X?”)
- Case law precedent (“Find decisions establishing principle Z”)
- Statute cross-references (“Which statutes reference statute X?”)
For a medical system:
- Diagnosis code lookup (ICD-10 codes)
- Drug interaction searches
- Clinical guideline retrieval
- Clinical trial matching
- Medical literature search by symptom/condition
For a financial system:
- Ticker/CIK lookup (exact match)
- SEC filing retrieval by company
- Earnings analysis by topic
- Regulatory filing search
- Financial ratio comparison
Don’t guess. Ask your users. For legal, interview 3–5 practicing lawyers. For medical, talk to 3–5 clinicians. Ask: “What do you search for most often? What frustrates you about search?”
Step 2: Build a small, labeled corpus
You don’t need millions of examples. You need a representative sample of thousands.
For a 9M-document production corpus, a 27K-document benchmark (0.3% sample) is sufficient to:
- Rank models against each other
- Identify which configurations work best
- Measure quality differences
- Estimate production performance (with caveats)
How to build it:
- Stratified random sample from production corpus (10–20% per stratum/content-type)
- Clean and deduplicate
- Use your production chunking policy (same chunk size, overlap, etc.)
- Label ground truth: for each of your 50–100 test queries, manually identify relevant documents
Why stratified sampling matters: If your corpus is 70% decisions, 20% statutes, 10% commentary, your benchmark should follow the same distribution. Otherwise, you’ll be measuring performance on unrepresentative data.
Step 3: Select and measure candidate models
Test at least:
- 1 baseline (BM25 keyword search)
- 3–5 dense embedding models (different families: Cohere, OpenAI, open-source, domain-specific)
- 2–3 hybrid combinations (BM25 + each dense model)
- 2–3 reranking approaches (if relevant)
For each, measure:
- Quality: nDCG@10, MRR@10, Recall@100 (pick metrics aligned with your use cases)
- Cost: tokens required, $/corpus
- Latency: p50/p99 query latency (under realistic concurrency)
Don’t test in isolation. Run all configs on the same corpus, same queries, same evaluation harness. This removes confounds and makes comparisons fair.
Step 4: Compute exact token counts
This is worth 30 minutes of effort and saves $100K in budgeting errors.
from transformers import AutoTokenizer
# For each model you're testing:
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
# or cohere, voyage, openai, etc.
total_tokens = 0
for doc in corpus:
tokens = len(tokenizer.encode(doc))
total_tokens += tokens
print(f"Total tokens: {total_tokens:,}")
print(f"Cost at $0.12/1M: ${total_tokens / 1e6 * 0.12:.2f}")
Real-world example: two tokenizers on the same 9.16M-chunk corpus differed by 1.51B tokens (a 32% difference). Guessing would have cost you $180 in unbudgeted embedding cost, or underfunding by that much.
Step 5: Build a test suite
Your benchmark’s credibility rests on tests, not on numbers.
What to test:
- Label canonicalization: typos in ground truth silently corrupt everything
- Cache freshness: stale cached vectors corrupt A/B comparisons
- Truncation policies: oversize inputs cause silent API failures
- Rate limits: transient errors misclassified as permanent failures
- Batch composition: document/query mixing invalidates token counts
Example: One real-world evaluation had a defect where transient Bedrock errors (network timeouts, temporary service unavailability) were treated as permanent failures. Result: two full embedding runs (~5,000 vectors each) were silently discarded. The defect surfaced only when output files went missing. A test suite would have caught this immediately.
Target: At least 200–300 assertions covering all critical paths through your evaluation.
Step 6: Document limitations loudly
Your benchmark is credible not because it claims to be perfect, but because it’s honest about what it doesn’t measure.
Template for your README:
## What this benchmark measures
- Retrieval quality on a 27,328-document sample from production
- Comparison between 15 embedding models, BM25 baseline, 6 hybrid configurations
- Cross-encoder reranking performance
- Exact cosine similarity (not ANN index performance)
## What this benchmark does NOT measure
- Generated-answer accuracy (only retrieval relevance)
- Latency under production concurrency (lab measurements only)
- Approximate nearest neighbor (ANN) recall loss
- Document extraction quality (assumes pre-extracted text)
- Production index serving performance
- Cost at production scale (scaled from sample)
## Known limitations
- Corpus sample is 0.3% of production (built for ranking systems, not predicting recall)
- Relevance labels from single judge family (cross-family agreement: κ=0.46)
- No human legal audit of label correctness (in progress)
- State court decisions limited to 14% of full text (known limitation)
## Caveat
Treat absolute nDCG@10 scores as optimistic. Gaps between models are more reliable than absolute numbers.
This honesty builds trust. A benchmark that admits its limits is more credible than one that hides them.
Part 3: Key Findings and Patterns
Pattern 1: Hybrid Retrieval (BM25 + Dense) Is Not a Fallback
The finding: On queries that search by exact reference (statute number, medical code, financial ticker), lexical (BM25) retrieval dominates. On open-ended semantic queries, dense embeddings are slightly better. Hybrid fusion combines both strengths.
Measured results (from legal domain evaluation):
| System | Citation queries | Open-ended Q&A | Weighted average |
|---|---|---|---|
| BM25 alone | 0.36 | 0.75 | 0.7924 |
| Dense only (top model) | 0.21 | 0.80 | 0.7987 |
| Hybrid (BM25 + dense, fused with RRF) | 0.30 | 0.82 | 0.8153 |
Key insight: Hybrid wins overall even though it’s not the top performer on either task alone. It’s the best across all queries.
Why it matters: Teams often think embeddings render BM25 obsolete. The reality: BM25 is optimal for exact-match queries (citations, codes, references) and your domain likely has many of these. Hybrid fusion is the pragmatic choice.
Lesson for your domain:
Do your users ever search by exact reference? If yes:
- Benchmark BM25 (it’s free; just use Elasticsearch/OpenSearch built-in)
- Test hybrid configurations (BM25 + your top-3 dense models)
- Measure on your actual query types
For legal: citations are ~30% of queries, hybrid is non-negotiable. For medical: ICD/CPT codes are ~20% of queries, hybrid matters. For finance: tickers/CUSIPs are ~25% of queries, hybrid matters.
Pattern 2: Reranking Is the High-ROI Lever
The finding: Swapping rerankers on the same first-stage retrieval stack yields higher quality gains per dollar spent than swapping embedding models.
Measured:
- Reranker swap (Model A → Model B on same first stage): +0.0487 nDCG@10 (p=0.0001), $0 cost (API swap)
- Embedding model upgrade (re-embed + re-index full corpus): +0.0149 nDCG@10 (p=0.058, not significant), ~$700 + 2–4 weeks engineering
Why: Rerankers work on the top-50 already-retrieved documents (high precision, low coverage). Small improvements here compound across all queries. Embedding models work on all 9.16M documents; their impact per $ is diluted.
Lesson for your domain:
Reranking should be your first optimization lever, not your last. Roadmap:
- Month 1: Get basic retrieval working (BM25 + any decent embedding)
- Month 2: Add a cross-encoder reranker (e.g., Voyage Rerank, Cohere Rerank)
- Month 3: Optimize reranking parameters (depth, weights)
- Month 4+: Consider switching embedding models (only if reranking is already optimized)
Pattern 3: Quality ≠ Latency, and Both Matter
The finding: Benchmarking nDCG@10 in isolation misses operational reality. The #1 quality model might be unusable in production if it’s slow.
Example:
- Model A: 0.82 nDCG@10, 2.0s p99 latency ❌ (breaks your <1s SLA)
- Model B: 0.79 nDCG@10, 0.4s p99 latency ✅ (meets SLA, acceptable quality tradeoff)
Why it matters: You’re evaluating in a notebook with cached vectors. Production has network latency, concurrent users, cache misses, and vector DB overhead. A model that scores well in the lab might breach your SLA in production.
Lesson for your domain:
Add latency benchmarking to your evaluation:
import time
for model in models:
latencies = []
for query in test_queries:
start = time.time()
results = retrieve(query, model)
latencies.append(time.time() - start)
p50 = np.percentile(latencies, 50)
p99 = np.percentile(latencies, 99)
nDCG = evaluate_quality(results)
print(f"{model}: nDCG={nDCG:.3f}, p99={p99:.3f}s")
Plot the curve: nDCG vs. p99 latency. Find the elbow point (where you get 95% of quality at acceptable latency). That’s your model.
Pattern 4: Query-Type Routing Beats Model Swaps
The finding: Queries aren’t uniform. Citation queries need different retrieval strategies than open-ended questions. Smart routing can outperform a one-size-fits-all model swap.
Example strategy:
if is_citation_query(query):
results = bm25_retrieval(query) # BM25 dominates here
else:
results = hybrid_retrieval(query) # Hybrid is best overall
Or, for hybrid systems:
if is_citation_query(query):
weight = (BM25=0.7, dense=0.3) # Favor keyword
else:
weight = (BM25=0.4, dense=0.6) # Favor semantic
results = hybrid_retrieval(query, weights=weight)
Why RRF fusion alone doesn’t fix this: It’s tempting to assume hybrid fusion already handles query-type differences — it’s blending both signals, after all. It doesn’t, structurally. Reciprocal Rank Fusion (RRF) fuses by rank, not score magnitude, so a document that’s merely middling on both BM25 and dense can outrank a document that wins BM25 decisively but places poorly on dense. This failure mode is worst on small, topically homogeneous corpora, where dense similarity scores cluster tightly and rank order stops tracking relevance.
A worked, verifiable example of this from an open-source implementation of this evaluation methodology: on a small identifier-query test set, hybrid RRF fusion got 3/5 citation queries right. Lowering rrf_k (tightening rank weighting) didn’t help — still 3/5, because no amount of rrf_k tuning changes RRF’s rank-only design. Routing identifier queries to BM25-only — skipping dense retrieval and fusion entirely for that query type — got 5/5. That’s the mechanism behind Pattern 1’s citation-query numbers above, at a scale small enough to inspect by hand.
One implementation nuance worth flagging: at benchmark-build time, query type is already known — it’s set by whichever archetype generated the query (citation lookup vs. free-text vs. known-item). A config-level per-query-type override (route identifier queries to strategy: bm25, leave everything else on hybrid) is enough to validate the pattern without hand-tuning fusion weights. In production, incoming queries aren’t pre-labeled this way — you still need a live classifier (regex for exact-match patterns is usually sufficient; see step 2 below) to route them the same way.
Measured impact: +0.01–0.03 nDCG@10 improvement on real-world query mixes (legal: +0.025, medical: estimated +0.015, finance: estimated +0.02).
Why it matters: It’s cheaper than model swaps and often higher-impact.
Lesson for your domain:
Implement query classification:
- Identify your query types (3–5 buckets)
- Build a simple classifier (regex for exact-match patterns, neural classifier for semantics)
- Measure retrieval performance per bucket — when a bucket underperforms, compare pre-fusion BM25/dense rankings against the post-fusion result. If a query’s gold document ranks #1 on a single signal but drops after RRF fusion (a “fusion flip”), that’s evidence for a bucket-level routing override, not a reason to keep tuning fusion parameters.
- Optimize retrieval strategy per bucket
- Compare overall performance (weighted average across buckets) vs. one-size-fits-all
Part 4: Common Pitfalls and How to Avoid Them
Pitfall 1: Benchmarking Without Production Validation
The trap: Your benchmark’s 27K documents are 0.3% of production. You rank models on this sample. They perform differently on the full 9.16M corpus (different data distribution, different scale effects).
Real-world example: A model ranked #3 on the benchmark. After re-indexing the full corpus, its nDCG@10 dropped to #8. The reason: the model relied on density/similarity patterns that didn’t generalize to the larger corpus.
How to avoid it:
- Use the benchmark for initial screening (narrow from 15 models to 3–5)
- Pilot the finalists on 10–20% of production corpus
- Measure quality on real production queries (not just benchmark queries)
- A/B test in production (even if offline, use production data)
Pitfall 2: Single-Judge Ground Truth
The trap: You label your benchmark with one LLM judge (or one human expert). The labels are internally consistent (κ=0.90) but might not reflect ground truth. Different judges make different decisions.
Real-world example: One system used a single LLM judge family to label 100 hand-crafted legal questions. Self-consistency was high (κ=0.90). But when they tested the labels against a different judge family, cross-judge agreement dropped to κ=0.46 (“moderate” at best).
Result: The models ranked by this benchmark were optimized for one judge’s preferences, not for objective correctness.
How to avoid it:
- Use multiple judges (3+ LLM families, or LLM + human)
- Calculate inter-judge agreement (Krippendorff’s α, Cohen’s κ)
- Use only queries where judges agree (κ ≥ 0.70)
- Put weakly-agreed queries in a secondary leaderboard with caveats
- For high-stakes domains (legal, medical, finance), include human judges on 10–20% of labels
Pitfall 3: Confusing ANN-Index Latency with Embedding Quality
The trap: You benchmark exact cosine similarity over cached vectors. Production uses an Approximate Nearest Neighbor (ANN) index (HNSW, Product Quantization, others). ANN introduces a recall-loss tradeoff: faster but less accurate.
Real-world: Your benchmark shows nDCG@10 = 0.82. Production (with HNSW at default settings) achieves nDCG@10 = 0.79 (3% loss). You don’t know this until production.
How to avoid it:
- Index your benchmark corpus in both exact and ANN modes
- Run the same queries on both
- Measure the recall-loss gap (e.g., “HNSW loses 2% vs. exact cosine”)
- Tune ANN parameters (ef_construction, ef_search, M for HNSW) on your benchmark
- Report both exact and ANN numbers
Pitfall 4: Ignoring Document Extraction Quality
The trap: You assume your documents are clean because they came from a vendor or legacy system. You never audit extraction quality. Result: 10–15% of your corpus has garbled text (OCR errors, malformed parsing, truncation artifacts).
Real-world impact: No amount of embedding or reranking can fix corrupted text. It’s garbage-in-garbage-out. Yet your benchmark never measures this.
How to avoid it:
- Stratified sample of 100–200 documents per corpus/source/era
- Compare extracted text against original source (PDF, XML, database)
- Measure extraction error rate per stratum
- Flag high-error strata (e.g., “scanned decisions from 1990s: 15% error rate”)
- Budget for manual QA or better extraction tooling if error rates are high
For specialized domains, hire domain experts (lawyers, clinicians, engineers) to spot-check, not just engineers.
Pitfall 5: Corpus Growth Rate Unknown
The trap: Your benchmark evaluates a static corpus. Real production grows. You don’t know:
- How many new documents per month?
- How fast is growth (5% annual? 50%?)?
- Does growth accelerate (seasonal, event-driven, seasonal)?
Result: Your 3-year TCO estimate is a guess. In Year 2, recurring embedding costs surprise you.
How to avoid it:
- Track historical corpus growth: average chunks/week over last 90 days
- Forecast growth: ask your domain experts (are laws growing? medical literature? financial filings?)
- Model growth into cost (Year 1: one-time $700; Year 2: $700 + $50/mo recurring; Year 3: $700 + $100/mo recurring)
- Plan re-evaluation frequency (quarterly? bi-annually?) to catch quality regressions as corpus grows
Pitfall 6: Not Testing Transient Failures
The trap: Your benchmark assumes APIs always work. You never test transient failures (network timeouts, rate limits, temporary service unavailability).
Real-world impact: Bedrock Cohere Embed v4 has a transient error (InternalServerException) that means “try again.” If your error-handling classifies it as permanent, you fail fast and lose already-paid embeddings. Two full runs (each ~5,000 vectors) got silently discarded. Defect surfaced only when output files went missing.
How to avoid it:
- Classify errors explicitly (transient vs. permanent)
- Implement exponential backoff for transient errors
- Test error paths: mock transient failures, verify retry logic
- Build regression tests to prevent this pattern recurring
Part 5: Architectural Decisions
Single-Engine Hybrid vs. Multi-System Architecture
Choice 1: Single Engine (Vespa, OpenSearch, Elasticsearch)
- Pro: BM25 + dense vector search in one query, native RRF fusion, lower latency, simpler ops
- Con: Tighter resource coupling, harder to scale one tier independently
- Cost: €1K–2K/month for a managed service
Choice 2: Dual System (S3 Vectors + Elasticsearch)
- Pro: Pure-vector storage is cheaper, can scale independently
- Con: App-layer RRF fusion, higher latency (two queries per request), more operational complexity
- Cost: ~€500/month storage + €500/month Elasticsearch = €1K/month, but with 2× query overhead
A lighter-weight starting point, before either Choice above: at benchmark stage or early production, you likely don’t need a dedicated search engine at all. LanceDB (embedded, file-based, zero ops) and pgvector (if you already run Postgres) are pluggable dense-retrieval backends in the open-source implementation this article draws on — BM25 stays in-memory, only the dense side moves to a persistent store. No managed-service bill, no second system to run, and a clear migration path to Choice 1/2 once corpus scale or query volume actually justifies the added operational surface.
Lesson: For domains where hybrid retrieval matters (legal, medical, finance), single-engine is worth the extra cost once you’re past that lighter-weight stage. The simplicity and performance gains pay for themselves.
Managed Service vs. Self-Hosted
Choice 1: Managed (AWS OpenSearch Serverless, Elasticsearch Cloud)
- Pro: Ops handled by vendor, auto-scaling, no patching headaches
- Con: Vendor lock-in, less control over tuning, OCU costs unknown at scale
- Cost: €1–2K/month
Choice 2: Self-Hosted (Vespa, OpenSearch on EC2/GKE)
- Pro: Full control, no vendor lock-in, known cost structure
- Con: You handle patching, scaling, backups, monitoring
- Cost: €500–1.5K/month (hardware) + engineering time
Lesson: For startups with <10 people, managed is worth it (frees engineering time). For teams with dedicated DevOps, self-hosted is viable if you size capacity conservatively.
Embedding Model Persistence
Decision: Should you commit to one embedding model for 3 years, or plan for swaps?
Real-world trade-off:
- Committing for 3 years: cheaper infrastructure, simpler planning
- Planning for swaps: $700–1000 per re-embedding, 2–4 weeks per swap, but flexibility to adopt better models
Recommendation: Plan for 12–18 month cycles. New embedding models come out every 3–6 months. Staying on a 2-year-old model locks you into its limitations. But don’t swap constantly; evaluate quarterly, commit for 12–18 months, then re-evaluate.
Part 6: A Practical Roadmap
Phase 1: Foundation (Weeks 1–2)
- [ ] Define your core retrieval tasks (3–5 use cases)
- [ ] Write 5–10 example queries and answers per task
- [ ] Identify your success metrics (nDCG@10? Recall? Cost/query?)
- [ ] Plan your corpus stratification (how to sample representation)
Phase 2: Data & Baselines (Weeks 3–4)
- [ ] Sample 1K–2K documents stratified across your corpus
- [ ] Label ground truth: for each query, which documents are relevant
- [ ] Set up BM25 baseline (free in Elasticsearch/OpenSearch)
- [ ] Run initial evaluation pipeline (quality + cost, no latency yet)
Phase 3: Model Comparison (Weeks 5–7)
- [ ] Select 5–8 embedding models (mix of open-source, API-based, domain-specific)
- [ ] Compute exact token counts for each
- [ ] Run full evaluation: quality, cost, latency
- [ ] Test hybrid configurations (BM25 + top-3 models)
- [ ] Identify configurations statistically tied at top
Phase 4: Rigor & Testing (Weeks 8–10)
- [ ] Build regression test suite (200+ assertions)
- [ ] Cross-validate judge agreement (if using LLM judges)
- [ ] Test on 10% production corpus (not just benchmark corpus)
- [ ] Measure ANN-recall loss (exact vs. HNSW index)
- [ ] Document all limitations and caveats
Phase 5: Production Prep (Weeks 11–12)
- [ ] Choose deployment architecture (single-engine? managed service? self-hosted?)
- [ ] Model 3-year cost (one-time + recurring, growth assumptions)
- [ ] Plan quarterly re-evaluation (schedule for Q2, Q3, Q4, next year)
- [ ] Design for model swap reversibility (can you re-index without downtime?)
Phase 6: Go-Live & Monitor (Weeks 13+)
- [ ] A/B test top-2 configurations on real production traffic
- [ ] Monitor quality metrics (weekly)
- [ ] Alert on quality regression (drop >2%)
- [ ] Quarterly re-evaluation: new models available? User patterns shifted?
Timeline: 3 months from concept to production-ready benchmark.
Part 7: Key Takeaways
For Benchmark Builders
- Public benchmarks are sanity checks, not sufficient. MTEB/BEIR are useful (“this model isn’t terrible on general text”), but domain-specific evaluation is non-negotiable.
- Small corpus, rigorous process > large corpus, loose process. A 27K-document benchmark with robust testing, regression tests, and caveat-driven transparency beats a 1M-document benchmark with unknown ground truth.
- The test suite is the real artifact. Publish your regression tests alongside your numbers. This builds credibility.
- Caveat yourself into trust. A benchmark that admits what it doesn’t measure is more credible than one that claims completeness.
- Exact measurements > estimates. Token counting, inter-judge agreement, ANN-recall loss: measure these exactly. Estimates introduce unnecessary error.
For Model Selectors
- Hybrid retrieval (BM25 + dense) is not a fallback; it’s often optimal. If your domain has exact-match queries, benchmark hybrid fusion.
- Quality is orthogonal to latency. The best model on quality might breach your SLA. Measure both; optimize on the Pareto frontier.
- Reranking is high-ROI. Swap rerankers quarterly. Swap embedding models every 12–18 months. The cost-per-point curve is steep for embeddings.
- Query-type routing can beat model swaps. Classify queries, apply different strategies. Often cheaper than changing the underlying model.
- Don’t commit to a model for 3 years. Commit for 12–18 months, then re-evaluate. New models ship constantly; staying on an old model locks you in.
For Infrastructure Architects
- Single-engine hybrid > multi-system. OpenSearch/Vespa (one engine doing BM25 + dense) beats S3 Vectors + Elasticsearch (two systems + app-layer fusion).
- Managed services have real operational value. AWS OpenSearch Serverless costs more than self-hosted but frees engineering time. For startups, that’s worth it.
- Quota constraints are a real risk. Check your cloud service quota limits early. Request increases preemptively. Have a fallback (third-party APIs, different service).
- Plan for corpus growth. Measure growth rate (chunks/week). Model recurring embedding costs. This is often the surprise cost in Year 2.
- Avoid lock-in where cheap (e.g., use standard query syntax), accept it where expensive (re-indexing). Migration from OpenSearch is expensive (full re-index), but single-engine migration (Vespa → OpenSearch) is cheaper than dual-system migration (S3 Vectors + Elasticsearch → OpenSearch).
Part 8: Avoiding the Sunk-Cost Trap
Here’s a common pattern: you build a benchmark, rank models, pick one, ship to production, and 6 months later, a new model ships that’s 10% better. Now you’re stuck.
Why? Because re-embedding and re-indexing your corpus costs $700–1,000 and 2–4 weeks of engineering time. That’s not a sunk cost; it’s a decision cost. Many teams rationalize not paying it (“we’re already live, the old model is good enough”), and lock themselves into suboptimal systems.
How to avoid this:
- Design for reversibility from day one. Your first-stage embedding model should be swappable (API-based, not baked into a learning model). Your reranker should definitely be swappable (it’s your single highest-ROI lever).
- Plan for regular model updates. Budget quarterly re-evaluation (maybe not a re-index, but at least a cost/quality check). This teaches you whether a swap is worth it before you’re forced into it.
- Keep fallback options. If your primary model becomes unavailable or unaffordable, have a backup (one model tier below, a fallback provider, a less-optimized configuration). Don’t get surprised at production scale.
- Instrument quality metrics. Monitor nDCG@10 (or your domain metric) weekly. Alert when it drops >2%. This tells you when a model is degrading, giving you time to plan a swap before users notice.
Conclusion
Building a domain-specific RAG system is not a research problem; it’s an engineering and product problem. The differences between your domain and MTEB/BEIR are real, measurable, and the reason why “best-on-MTEB” doesn’t reliably predict “best-for-your-domain.”
The good news: evaluating your domain is not hard. It takes 3 months and methodical work, not genius.
A practical framework:
- Weeks 1–2: Define your tasks and success metrics
- Weeks 3–4: Build a small, labeled corpus
- Weeks 5–7: Benchmark 5–8 models
- Weeks 8–10: Add rigor (tests, cross-validation, ANN testing)
- Weeks 11–12: Plan infrastructure and cost
- Week 13+: A/B test, monitor, and prepare for quarterly re-evaluation
This is not wasted time. This is the difference between shipping the optimal model for your domain vs. shipping the #12 model because you trusted a generic benchmark.
Further Reading
- MTEB Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
- BEIR: https://github.com/beir-cellar/beir
- Relevance Judging Best Practices: https://www.microsoft.com/en-us/research/publication/evaluating-information-retrieval/
- Token Counting: Tokenizer documentation for your chosen models
- Inter-Judge Agreement: Krippendorff’s α, Cohen’s κ
- ANN Tuning: HNSW parameter guidance for your chosen vector DB
Acknowledgments: This article synthesizes lessons from a detailed examination of a production legal-tech RAG evaluation system. While specific details have been anonymized, the patterns and trade-offs are real and apply broadly to domain-specific RAG across legal, medical, financial, and enterprise contexts.