When RAG retrieval misses, most teams swap chunkers and rerankers forever without touching the actual ranking function: the embedding model. Off-the-shelf embedders were trained on web-general text; your domain's vocabulary ("basis risk", "NPA slippage", your internal product codenames) collapses into undifferentiated vector soup.
Tier: Advanced · Workflow area: Fine-tuning & Model Adaptation
Contrastive fine-tuning of a small open embedder on mined (query, positive, negative) triples is a few hours of CPU/free-GPU work and routinely buys double-digit recall lift — this playbook does it end to end and proves the lift on your own benchmark.
Skip if you have no retrieval benchmark yet — build the labeled (query → relevant chunks) set first; tuning an embedder without recall numbers is flying blind. Also skip if your corpus re-embeds are prohibitively expensive and hybrid search + reranking has not been tried.
sentence-transformers (v3+) and a small open base embedder — BAAI/bge-base-en-v1.5 or intfloat/e5-base-v2 class; base size trains comfortably on a Kaggle T4 and even on CPU overnight.Freeze the baseline. Embed the corpus with the off-the-shelf model, run the held-out queries, record recall@5, recall@20, and MRR@10. This table is the "vs off-the-shelf" claim your report hinges on.
Mine training triples from your own data. Positives: the labeled relevant chunks from the train split, plus (query, chunk) pairs users clicked or cited. Hard negatives: for each query, retrieve top-50 with the baseline model and take high-ranked chunks that are not labeled relevant — these near-misses are where the learning signal lives. Random negatives teach almost nothing.
Audit for false negatives. Hard-negative mining's classic failure: a "negative" that is actually relevant but unlabeled. Skim 30 mined negatives; if more than ~10% look actually relevant, tighten mining (skip ranks 1–5, take ranks 10–50) or LLM-screen negatives with a relevance prompt and human-check a sample.
Train with MultipleNegativesRankingLoss.
from sentence_transformers import SentenceTransformer, SentenceTransformerTrainer, losses
model = SentenceTransformer("BAAI/bge-base-en-v1.5")
loss = losses.MultipleNegativesRankingLoss(model)
# dataset columns: anchor (query), positive (chunk), negative (hard negative)
Key settings: the largest batch size that fits (MNRL uses in-batch negatives — bigger batches = harder task = better model; 32–64 on a T4 for base-size), learning_rate=2e-5, 1–2 epochs, warmup 10%. Keep the query/passage prefix conventions of the base model (e5 needs query:/passage:; bge uses a query instruction) — dropping them silently degrades both baseline and tune.
Evaluate during and after training. Wire an InformationRetrievalEvaluator on a validation slice so you can early-stop, then run the full held-out benchmark: same corpus, re-embedded with the tuned model, same queries, same metrics. Report deltas per query category — domain-jargon queries should jump, generic queries should hold.
Check for general-capability collapse. A domain-tuned embedder can forget general English similarity. Run 20 generic sanity queries (paraphrase pairs, unrelated pairs) and confirm similarity orderings are still sane; if not, lower LR/epochs or mix 10–20% general triples (e.g., from MS MARCO) into training.
Ship with a re-embedding plan. Version the model, re-embed the corpus, and record the pairing (embedder version ↔ index version) in config — mixed-version indexes produce silent garbage. Feed the tuned retriever back through your full RAG evaluation (pb-35) to confirm end-to-end answer-quality lift.
Hard negatives mined from your own baseline's near-misses are the highest-value data you will create this quarter — the model learns precisely the distinctions your current retrieval gets wrong.
An embedding model fine-tuned on client documents encodes their vocabulary distribution — treat the model file as client-derived IP unless the contract says otherwise.