Introduction
Single-strategy retrieval systems rarely survive production workloads. Vector search excels at semantic understanding but struggles with exact matches, acronyms, or structured constraints. Keyword/BM25 search guarantees lexical precision but misses conceptual relevance. Metadata filtering adds business context (department, date, status, version) but isn't a retrieval strategy on its own.
Hybrid retrieval solves this by orchestrating multiple retrieval paradigms, fusing their results, and applying intelligent reranking. When combined with LangGraph, you get a stateful, observable, and fault-tolerant RAG pipeline that can branch, retry, stream, and adapt to real-time constraints.
In this article, we'll build an end-to-end hybrid RAG system using LangGraph, anchored in a real-world enterprise IT support use case.
Real-World Use Case: Enterprise IT Support Knowledge Assistant
Scenario
An IT operations engineer submits the following query:
"How do I fix Error 503 on Kubernetes clusters after upgrading to v1.28? Looking for resolved tickets from the infrastructure team."
Challenges
Semantic intent: "fix", "Kubernetes upgrade", "cluster instability"
Exact matches: Error 503, v1.28, Kubernetes
Business constraints:
team=infrastructure,status=resolved,created_after=2025-01-01Latency requirement:
<800msend-to-end for real-time agent assistance
Solution Architecture
Query Analyzer: Extracts intent, keywords, and metadata filters
Parallel Retrieval: Vector + BM25 + Strict Metadata Filter
Fusion & Rerank: Reciprocal Rank Fusion (RRF) + Cross-Encoder
Generation: LLM synthesizes answer with citations
Orchestration: LangGraph manages state, parallelism, streaming, and checkpoints
Architecture Overview
LangGraph excels in this architecture because:
Nodes run in parallel when edges converge.
State is explicitly typed and accumulates results safely.
Checkpointing enables retries, streaming, and human-in-the-loop fallbacks.
Observability is built-in through LangGraph traces.
Step-by-Step LangGraph Implementation
1. Dependencies & Setup
pip install langgraph langchain langchain-core langchain-openai rank_bm25 numpy
2. State Definition
LangGraph requires explicit state typing. We use Annotated with operator.add so parallel retrieval nodes accumulate documents instead of overwriting them.
from typing import TypedDict, List, Dict, Any, Annotated
import operator
from langchain_core.documents import Document
class HybridRAGState(TypedDict):
query: str
metadata_filters: Dict[str, Any]
vector_docs: Annotated[List[Document], operator.add]
keyword_docs: Annotated[List[Document], operator.add]
metadata_docs: Annotated[List[Document], operator.add]
reranked_docs: List[Document]
answer: str
citations: List[Dict[str, str]]
3. Node Implementations
Query Analysis & Metadata Extraction
from langchain_openai import ChatOpenAI
import json
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def analyze_query(state: HybridRAGState):
prompt = f"""
Extract metadata filters and search intent from this query:
"{state['query']}"
Return ONLY a JSON object with keys:
department, status, min_date, error_code, component.
Use null for missing fields.
Example:
{{
"department": "infrastructure",
"status": "resolved",
"min_date": "2025-01-01",
"error_code": "503",
"component": "kubernetes"
}}
"""
response = llm.invoke(prompt)
filters = json.loads(response.content)
return {"metadata_filters": filters}
Parallel Retrieval Nodes
In production, replace these with actual FAISS, Elasticsearch, OpenSearch, or pgvector implementations.
Vector Retrieval
def retrieve_vector(state: HybridRAGState):
docs = vector_store.similarity_search_with_score(
query=state["query"],
k=5,
filter=state["metadata_filters"]
)
return {
"vector_docs": [doc for doc, _ in docs]
}
Keyword Retrieval
def retrieve_keyword(state: HybridRAGState):
docs = bm25_retriever.invoke(
state["query"],
k=5
)
return {"keyword_docs": docs}
Metadata Retrieval
def retrieve_metadata(state: HybridRAGState):
docs = metadata_store.query(
filters=state["metadata_filters"],
limit=3
)
return {"metadata_docs": docs}
Fusion & Reranking (RRF + Cross-Encoder)
Reciprocal Rank Fusion
import numpy as np
from sentence_transformers import CrossEncoder
reranker = CrossEncoder(
"cross-encoder/ms-marco-MiniLM-L-6-v2"
)
def reciprocal_rank_fusion(
vector_docs,
keyword_docs,
metadata_docs,
k=60
):
scores = {}
def rrf_rank(docs, start_rank=1):
for i, doc in enumerate(docs):
doc_id = doc.metadata.get(
"id",
doc.page_content[:50]
)
rank = start_rank + i
scores[doc_id] = (
scores.get(doc_id, 0)
+ 1 / (k + rank)
)
rrf_rank(vector_docs)
rrf_rank(keyword_docs, 10)
rrf_rank(metadata_docs, 20)
sorted_ids = sorted(
scores,
key=scores.get,
reverse=True
)
doc_map = {
d.metadata.get(
"id",
d.page_content[:50]
): d
for d in (
vector_docs +
keyword_docs +
metadata_docs
)
}
return [
doc_map[doc_id]
for doc_id in sorted_ids
if doc_id in doc_map
]

Join the conversation! Your thoughts help the community grow.