Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Embedding Models & Semantic Search

How embedding models power semantic search: bi-encoders vs cross-encoders, re-ranking, hybrid search combining keywords and vectors, and choosing embeddings for retrieval quality.

Advanced4 min readUpdated
On this page

Embeddings turn text into vectors whose closeness reflects meaning — the foundation of semantic search and RAG. This tutorial goes deeper than "embeddings enable search": bi-encoders versus cross-encoders, why hybrid search wins, and how to actually choose an embedding model for retrieval quality.

Key Takeaways

  • Bi-encoders embed query and document separately (fast, precomputable) — they power vector search.
  • Cross-encoders score query-document pairs together (accurate, slow) — they power re-ranking.
  • Hybrid search fuses keyword and vector search to cover each other's blind spots.
  • Choose an embedding model by measuring retrieval hit rate on your data, not by benchmark.

Bi-encoders: fast, precomputable

A bi-encoder embeds the query and each document independently. Because documents are embedded ahead of time and stored, search is just comparing the query vector against stored vectors — fast enough for interactive use over millions of documents.

This is what every vector store uses. Its limitation is that query and document never "see" each other during embedding, so the relevance signal is coarser than it could be.

Cross-encoders: accurate, slow

A cross-encoder takes the query and a document together and outputs a relevance score. Because it processes them jointly, it captures subtle relevance a bi-encoder misses — but it cannot precompute anything, so scoring every document against a query is far too slow for first-pass retrieval.

The two-stage pattern: a fast bi-encoder retrieves candidates, a slow cross-encoder re-ranks them accurately.

The winning pattern: retrieve wide, re-rank narrow

Combine both. Use the bi-encoder to fetch a generous candidate set fast, then the cross-encoder to re-rank that small set accurately:

Retrieve then re-rank
// 1. Bi-encoder retrieves 30 candidates cheaply from millions.
List<Document> candidates = vectorStore.search(query, 30);
 
// 2. Cross-encoder re-ranks those 30 accurately, keeping the best 5.
List<Document> best = reranker.rerank(query, candidates, 5);

Hybrid search: keywords plus vectors

Vector search understands meaning but is blind to exact tokens — it will happily miss the error code ERR_4021 because it does not "mean" anything semantically. Keyword search (BM25) catches exact matches but misses paraphrases. Hybrid search runs both and fuses the results:

// Run keyword and vector search, then fuse rankings (e.g. reciprocal rank
// fusion). Recovers exact-identifier queries that pure vector search drops.
List<Document> keywordHits = bm25Search(query, 20);
List<Document> vectorHits = vectorStore.search(query, 20);
List<Document> fused = reciprocalRankFusion(keywordHits, vectorHits);

Hybrid search is especially valuable for technical content full of exact identifiers — product SKUs, error codes, function names, API paths — where users often search for the exact string.

Choosing an embedding model

The decision is empirical, not theoretical:

  1. Shortlist by domain fit (general vs specialised), language coverage, dimension and cost.
  2. Build a question set of real queries with the passage that should answer each.
  3. Measure hit rate — for each candidate model, what fraction of questions retrieve the correct passage in the top k?
  4. Pick the winner on your data.
// The only benchmark that matters: your queries against your documents.
for (EmbeddingModel model : candidates) {
    double hitRate = evaluateRetrieval(model, goldenQuestions);
    log.info("model={} hitRate@5={}", model, hitRate);
}

A model that tops a public embedding leaderboard can lose to a smaller one on your specialised corpus. Your hit rate is the ground truth. See testing AI applications.

Domain and multilingual considerations

  • Specialised domains (legal, medical, code) sometimes do better with a domain-tuned embedding model than a general one — measure both.
  • Multilingual content needs a multilingual embedding model, or retrieval across languages fails.
  • Symmetric vs asymmetric — some models are tuned for short-query-to-long-document matching (asymmetric), which suits RAG better than models tuned for similar-length comparison.

Next

Frequently Asked Questions

What is the difference between a bi-encoder and a cross-encoder?
A bi-encoder embeds the query and each document separately, so document embeddings can be precomputed and searched fast — this is what powers vector search. A cross-encoder processes the query and a document together, producing a much more accurate relevance score but far slower, since it cannot precompute. The standard pattern uses a bi-encoder to retrieve candidates fast and a cross-encoder to re-rank them accurately.
What is hybrid search and why does it help?
Hybrid search combines keyword search (like BM25) with vector search and fuses the rankings. It helps because vector search misses exact matches — product codes, error numbers, rare names — that keyword search catches, while keyword search misses paraphrases that vector search catches. Together they cover each other's blind spots.
When should I add re-ranking to semantic search?
When retrieval returns roughly relevant results but the best passage is often not ranked first. A cross-encoder re-ranker scores each candidate against the query directly and reorders them, which is usually the single biggest quality improvement over plain vector search. Retrieve wide with the bi-encoder, then re-rank narrow with the cross-encoder.
How do I choose an embedding model for retrieval?
Measure retrieval hit rate on a representative question set rather than trusting benchmarks. Consider the domain (general vs specialised), language coverage, dimension and cost. The best embedding model is the one that most often puts the correct passage in your top results for your actual queries — which you can only know by testing on your data.

Related tutorials