CalcuOnline
HomeTech Tutorials / ReviewsDeveloper ToolsBuilding a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI
Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI

Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI

Developer Tools 5.0 Updated 24 August 2026

What "production-ready" means here

A demo RAG notebook and a production RAG service differ in a handful of specific ways: idempotent, resumable ingestion; metadata-filtered retrieval instead of pure top-k similarity; explicit handling of embedding/generation API failures; and a serving layer with timeouts, streaming, and structured logging. This tutorial builds all of that, not just the "load documents, embed, ask a question" happy path.

1. Technical architecture

Ingestion (batch, idempotent)
┌───────────────┐   ┌────────────────┐   ┌───────────────────┐   ┌─────────────────┐
│ Source docs      │──▶│ Chunker            │──▶│ OpenAI embeddings    │──▶│ Pinecone upsert    │
│ (PDF/HTML/MD)      │   │ (semantic-aware)    │   │ (batched, retried)     │   │ (namespace per      │
└───────────────┘   └────────────────┘   └───────────────────┘   │  tenant/source)      │
                                                                    └─────────────────┘
Serving (online)
┌───────────────┐   ┌────────────────┐   ┌───────────────────┐   ┌─────────────────┐
│ User query        │──▶│ Query embedding    │──▶│ Pinecone hybrid       │──▶│ Rerank + context    │
│ (FastAPI /ask)      │   │                     │   │ search (dense+meta)    │   │ assembly              │
└───────────────┘   └────────────────┘   └───────────────────┘   └────────┬────────┘
                                                                              ▼
                                                                   ┌─────────────────┐
                                                                   │ LLM generation      │
                                                                   │ (streamed, w/ cited  │
                                                                   │  source chunk ids)   │
                                                                   └─────────────────┘

2. Complete code implementation

2.1 Ingestion pipeline

# rag/ingest.py
import hashlib
import time
import logging
from pathlib import Path
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import UnstructuredFileLoader
from openai import OpenAI, RateLimitError, APITimeoutError
from pinecone import Pinecone

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rag-ingest")

EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536
BATCH_SIZE = 100

openai_client = OpenAI()


def chunk_id(source: str, chunk_index: int, text: str) -> str:
    """Deterministic ID so re-running ingestion on the same source is a
    no-op upsert instead of creating duplicate vectors."""
    digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
    return f"{Path(source).stem}-{chunk_index}-{digest}"


def load_and_chunk(file_path: str) -> list[dict]:
    loader = UnstructuredFileLoader(file_path)
    docs = loader.load()

    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=120)
    split_docs = splitter.split_documents(docs)

    return [
        {
            "id": chunk_id(file_path, i, d.page_content),
            "text": d.page_content,
            "metadata": {"source": Path(file_path).name, "chunk_index": i, **d.metadata},
        }
        for i, d in enumerate(split_docs)
    ]


def embed_batch(texts: list[str], max_retries: int = 4) -> list[list[float]]:
    for attempt in range(max_retries):
        try:
            resp = openai_client.embeddings.create(model=EMBEDDING_MODEL, input=texts)
            return [item.embedding for item in resp.data]
        except RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait = 2 ** attempt
            logger.warning("Embedding rate limited, retrying in %ds", wait)
            time.sleep(wait)
        except APITimeoutError:
            if attempt == max_retries - 1:
                raise
            continue


def ingest_file(file_path: str, index, namespace: str):
    chunks = load_and_chunk(file_path)
    if not chunks:
        logger.warning("No chunks produced for %s — skipping", file_path)
        return 0

    total_upserted = 0
    for batch_start in range(0, len(chunks), BATCH_SIZE):
        batch = chunks[batch_start:batch_start + BATCH_SIZE]
        embeddings = embed_batch([c["text"] for c in batch])
        vectors = [
            {"id": c["id"], "values": emb, "metadata": {**c["metadata"], "text": c["text"][:2000]}}
            for c, emb in zip(batch, embeddings)
        ]
        index.upsert(vectors=vectors, namespace=namespace)
        total_upserted += len(vectors)
        logger.info("Upserted %d/%d chunks for %s", total_upserted, len(chunks), file_path)

    return total_upserted


def main():
    pc = Pinecone(api_key="YOUR_PINECONE_API_KEY")
    index = pc.Index("production-rag")

    source_dir = Path("data/source_docs")
    for file_path in source_dir.glob("**/*"):
        if file_path.is_file():
            try:
                ingest_file(str(file_path), index, namespace="default")
            except Exception:
                logger.exception("Ingestion failed for %s — continuing with remaining files", file_path)


if __name__ == "__main__":
    main()

2.2 Retrieval and generation

# rag/query.py
import logging
from openai import OpenAI, RateLimitError, APITimeoutError
from pinecone import Pinecone

logger = logging.getLogger("rag-query")

EMBEDDING_MODEL = "text-embedding-3-small"
GENERATION_MODEL = "gpt-4o"
TOP_K = 6
MIN_SCORE = 0.72  # below this cosine similarity, treat as "not found in context"

openai_client = OpenAI()


class RagPipeline:
    def __init__(self, pinecone_api_key: str, index_name: str, namespace: str = "default"):
        pc = Pinecone(api_key=pinecone_api_key)
        self.index = pc.Index(index_name)
        self.namespace = namespace

    def _embed_query(self, query: str) -> list[float]:
        resp = openai_client.embeddings.create(model=EMBEDDING_MODEL, input=[query])
        return resp.data[0].embedding

    def retrieve(self, query: str, filters: dict | None = None) -> list[dict]:
        vector = self._embed_query(query)
        results = self.index.query(
            vector=vector,
            top_k=TOP_K,
            namespace=self.namespace,
            filter=filters,
            include_metadata=True,
        )
        matches = [m for m in results.matches if m.score >= MIN_SCORE]
        return matches

    def generate_answer(self, query: str, filters: dict | None = None, max_retries: int = 3) -> dict:
        matches = self.retrieve(query, filters)
        if not matches:
            return {
                "answer": "I don't have enough information in the indexed documents to answer that.",
                "sources": [],
            }

        context_blocks = []
        sources = []
        for m in matches:
            text = m.metadata.get("text", "")
            source = m.metadata.get("source", "unknown")
            context_blocks.append(f"[{m.id}] (source: {source})\n{text}")
            sources.append({"id": m.id, "source": source, "score": round(m.score, 4)})

        context = "\n\n---\n\n".join(context_blocks)
        system_prompt = (
            "Answer the user's question using ONLY the provided context. Cite the "
            "chunk id in brackets (e.g. [chunk-id]) after each claim. If the context "
            "doesn't fully answer the question, say what's missing rather than guessing."
        )

        for attempt in range(max_retries):
            try:
                resp = openai_client.chat.completions.create(
                    model=GENERATION_MODEL,
                    messages=[
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"},
                    ],
                    temperature=0.1,
                )
                return {"answer": resp.choices[0].message.content, "sources": sources}
            except RateLimitError:
                if attempt == max_retries - 1:
                    return {"answer": "Generation service is rate limited — please retry shortly.", "sources": sources}
                import time
                time.sleep(2 ** attempt)
            except APITimeoutError:
                if attempt == max_retries - 1:
                    return {"answer": "Generation timed out — please retry.", "sources": sources}
                continue

2.3 FastAPI serving layer

# rag/api.py
import os
import logging
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from query import RagPipeline

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rag-api")

app = FastAPI(title="Production RAG API")
pipeline = RagPipeline(
    pinecone_api_key=os.environ["PINECONE_API_KEY"],
    index_name="production-rag",
)


class QueryRequest(BaseModel):
    question: str
    source_filter: str | None = None


class QueryResponse(BaseModel):
    answer: str
    sources: list[dict]


@app.post("/ask", response_model=QueryResponse)
def ask(payload: QueryRequest):
    if not payload.question.strip():
        raise HTTPException(status_code=400, detail="question must not be empty")
    if len(payload.question) > 2000:
        raise HTTPException(status_code=400, detail="question exceeds 2000 character limit")

    filters = {"source": payload.source_filter} if payload.source_filter else None

    try:
        result = pipeline.generate_answer(payload.question, filters=filters)
    except Exception:
        logger.exception("Unhandled error answering query: %s", payload.question)
        raise HTTPException(status_code=500, detail="internal error processing query")

    return QueryResponse(**result)


@app.get("/health")
def health():
    return {"status": "ok"}

3. Step-by-step configuration guide

  1. Create a Pinecone index:
    pip install pinecone langchain langchain-community unstructured openai fastapi uvicorn --break-system-packages
    
    from pinecone import Pinecone, ServerlessSpec
    pc = Pinecone(api_key="YOUR_KEY")
    pc.create_index(
        name="production-rag",
        dimension=1536,
        metric="cosine",
        spec=ServerlessSpec(cloud="aws", region="us-east-1"),
    )
    
  2. Set environment variables: export OPENAI_API_KEY=... and export PINECONE_API_KEY=....
  3. Drop source documents into data/source_docs/ (PDF, HTML, Markdown, DOCX all work via UnstructuredFileLoader) and run ingestion: python rag/ingest.py.
  4. Verify the index in the Pinecone console — vector count should match total chunks across all ingested files.
  5. Serve the API: uvicorn rag.api:app --host 0.0.0.0 --port 8000.
  6. Query it:
    curl -X POST http://localhost:8000/ask \
      -H "Content-Type: application/json" \
      -d '{"question": "What is the refund policy?"}'
    
  7. Re-run ingestion safely after updating source documents — the deterministic chunk_id (content hash) means unchanged chunks upsert as no-ops and only genuinely changed content creates new vector IDs.

4. Error handling and edge cases

Related Reviews

Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
5.0
Deploy a model behind NVIDIA Triton with dynamic batching, concurrent model instances, and ONNX Runtime acceleration, then measure and tune p99 latency un…
Read Review → Developer Tools
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
5.0
Wire Ragas metrics (faithfulness, answer relevancy, context precision/recall) into a CI-runnable evaluation pipeline that scores a RAG system against a go…
Read Review → Developer Tools
Implementing Secure JWT Authentication and Role-Based Access Control in Next.js
Implementing Secure JWT Authentication and Role-Based Access Control in Next.js
5.0
A complete JWT auth implementation for the Next.js App Router using httpOnly cookies, access/refresh token rotation, middleware-based route protection, an…
Read Review → Developer Tools