Advanced RAG with LangChain: Re-Ranking and Hybrid Queries

By Carlos Montiel | Enterprise AI Specialist
Leer en español →
Published: 2026-07-28 | By: Carlos Montiel | Reading time: ~4 minutes

A basic RAG with pure vector similarity fails in production more often than demos suggest. This guide covers the two techniques that raise precision the most: hybrid search and re-ranking.

Why "naive" RAG falls short

A retrieval pipeline based solely on cosine similarity over embeddings has a well-known blind spot: it fails with exact terms — product codes, proper names, item numbers — because embeddings capture semantics, not lexical matching. A customer searching for "error E-4021" might not retrieve the right document if the embedding semantically associates it with other similar errors. The standard solution in production systems is combining lexical retrieval (BM25) with vector retrieval.

Hybrid search with EnsembleRetriever

LangChain solves this with `EnsembleRetriever`, which combines multiple retrievers and fuses results using Reciprocal Rank Fusion (RRF):

from langchain_community.retrievers import BM25Retriever from langchain.retrievers import EnsembleRetriever bm25_retriever = BM25Retriever.from_documents(documents) bm25_retriever.k = 10 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6], ) docs = ensemble_retriever.invoke("error E-4021 in the billing module")

The `weights` aren't universal: in technical domains with lots of exact vocabulary (logs, error codes, SKUs), raising BM25's weight to 0.5-0.6 usually improves recall. In more conversational domains, a dominant vector weight works better.

Re-ranking with cross-encoders

Hybrid search improves recall (bringing relevant documents into the candidate set), but not necessarily the ordering. That's what a re-ranker is for: a cross-encoder that evaluates the (query, document) pair directly, much more precise but computationally more expensive than vector search, which is why it's applied only to the top-k candidates, not the entire base.

from langchain.retrievers import ContextualCompressionRetriever from langchain_community.document_compressors import FlashrankRerank compressor = FlashrankRerank(top_n=4) retriever_with_reranking = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever, ) final_docs = retriever_with_reranking.invoke("error E-4021 in the billing module")

`FlashrankRerank` is a lightweight option that runs locally with no external API call, useful when the latency or cost of a managed re-ranker (like Cohere Rerank) isn't justified for the project's volume. For higher precision, `CohereRerank` via `langchain-cohere` delivers better results on multilingual benchmarks, relevant if the content is in Spanish.

Integrating the full pipeline in LCEL

The final pipeline combines hybrid retrieval, re-ranking, and generation into a single declarative chain:

from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser prompt = ChatPromptTemplate.from_template( "Answer using only the context.\n\nContext:\n{context}\n\nQuestion: {question}" ) def format_docs(docs): return "\n\n".join(f"[Source: {d.metadata.get('source')}]\n{d.page_content}" for d in docs) advanced_rag = ( {"context": retriever_with_reranking | format_docs, "question": RunnablePassthrough()} | prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser() )

Including the source (`metadata.get("source")`) in the formatted context isn't cosmetic: it lets the model cite its source in the answer, which reduces perceived hallucinations and makes auditing easier in regulated domains (finance, healthcare, legal) — something that, in our projects with clients in the region, turns out to be a non-negotiable requirement.

Chunking: the variable with the biggest impact

No re-ranking technique compensates for poorly done chunking. We recommend `RecursiveCharacterTextSplitter` with a `chunk_size` between 500-800 tokens and 10-15% `chunk_overlap` for general text, but for structured documents (contracts, technical manuals) a structure-aware splitter like `MarkdownHeaderTextSplitter` is better, preserving the section hierarchy as metadata on each chunk, dramatically improving the relevance of the retrieved context.

Evaluation: don't optimize blind

Any change to the retrieval pipeline should be measured against an evaluation set with known "gold" questions and documents, using metrics like recall@k and MRR (Mean Reciprocal Rank) before and after the change. LangSmith lets you version these evaluation datasets and run automatic comparisons, a topic we cover in detail in our article dedicated to observability.

Carlos Montiel
Enterprise AI Solutions Architect
Specialist in LLMs, Agents, and Orchestration
guatemalia.com/en/#contact · info@guatemalia.com

Need to implement AI at your company?

Carlos Montiel is an enterprise AI solutions architect. He implements LLMs, Agents, RAG, and orchestrators for companies across Guatemala and Latin America. Reach out for a consultation.

Contact Carlos Montiel

info@guatemalia.com