Vol. 01 — 2026

Zero-Hallucination RAG: Pydantic + pgvector India

Zero-Hallucination RAG: Pydantic + pgvector India

Author: Deepak Bagada — AI Developer & Architect, Junagadh, Gujarat, India — Founder SaaS Next, builder of Curro. I ship hallucination-free RAG for Gujarat SMEs from Junagadh. Connect linkedin.com/in/deepak-bagada · deepakbagada.in — Last reviewed 2026-09-01.

Zero-hallucination RAG in India means the model never answers from memory — it answers only from retrieved chunks, validated by Pydantic guards, grounded by pgvector HNSW + hybrid search (vector + full-text), and gated by a 90-day ledger before any irreversible action. I run AI Development from Junagadh where a hallucinated GSTIN is not a typo — it is a failed filing and a Razorpay reversal. Pydantic + pgvector keeps it inside your VPC.

A Surat bot answered "HSN 5208 = 5%" when the chunk said 12%. Search was correct; the LLM ignored it. Rule since: if not in retrieved context and schema-validated, it does not ship. Same typed-contract as MCP = USB-C for AI agents — 80% enterprise in 2026.

Why RAG Hallucinates — And Why India Billing Breaks First

RAG hallucinates because generation is unchecked, not because retrieval is weak.

1. Retrieved but ignored. Correct HSN, ledger, or Zoho ID is in top-k, but the LLM paraphrases from weights to "be helpful." Without a post-generation guard, helpfulness beats grounding.

2. Chunk soup without provenance. 500-char chunks stripped of doc_name, tenant_id cannot answer "which circular gave 18%?". Per Strategy Mosaic July 2025, enterprise RAG fails on provenance.

3. Vector-only fails on Indian lexicon. HSN 5208 vs 5209 is one digit and a different slab; Gujarati "kapas" vs English "cotton" is one concept. Pure cosine misses the first, pure BM25 misses the second. You need both.

Add India reality: 89% mobile (rajeshRNAir Jan-Mar 2026), Hindi/Gujarati queries on English docs, and INV-8841 vs INV-8842 is ₹8,499 vs ₹84,900. A hallucinated invoice auto-sent via WhatsApp creates an irreversible UPI link under RBI Apr 21 2026 rules. That is why our RAG never writes to Razorpay or Zoho without HITL.

Fix is not a bigger model. It is a schema gate.

Pydantic Guard Pattern — No Citation, No Answer

Every answer passes two gates: input guard (tenant isolation) and output guard (citation-grounded). The LLM is untrusted; the schema is trusted. Invented chunk_id? Rejected. Low confidence? Abstain with closest chunks.

Pattern I run on a ₹6,000 VPS and Pi 5 fallback:

# pydantic_guard.py — Pydantic v2 guards
from pydantic import BaseModel, Field, ValidationError
from typing import List, Literal

class RetrievedChunk(BaseModel):
    chunk_id: str
    doc_name: str
    text: str = Field(min_length=20, max_length=4000)
    score: float = Field(ge=0, le=1)

class RAGQuery(BaseModel):
    tenant_id: str = Field(pattern=r"^[a-z0-9-]{6,40}$")
    query: str = Field(min_length=3, max_length=800)

class GroundedAnswer(BaseModel):
    answer: str = Field(min_length=20, max_length=3000)
    citations: List[str] = Field(min_length=1, description="chunk_ids from retrieved set")
    confidence: Literal["high", "medium", "low"]
    abstained: bool = False
    tenant_id: str

def verify_grounded(answer: GroundedAnswer, retrieved_ids: set[str]) -> GroundedAnswer:
    if not set(answer.citations).issubset(retrieved_ids):
        raise ValidationError.from_exception_data("citations", "citation not in retrieved context")
    if answer.confidence == "low":
        return GroundedAnswer(
            answer="I don't have that in your documents. Closest: see citations. Escalate to HITL?",
            citations=list(retrieved_ids)[:2],
            confidence="low", abstained=True, tenant_id=answer.tenant_id,
        )
    return answer

def hitl_required(action: str, amount: float | None = None) -> bool:
    irreversible = {"razorpay_create_link", "zoho_create_invoice", "tally_voucher_create"}
    if action in irreversible: return True
    if amount and amount > 15000: return True  # RBI ₹15K AutoPay threshold
    return False

RAGQuery enforces tenant_id from JWT at gateway, not prompt. verify_grounded() enforces every citation exists in retrieved set; hitl_required() blocks Razorpay/Zoho/Tally and >₹15K until human approval on Telegram. P95 cost: ~3ms validation.

A Rajkot distributor cut post-RAG invoice corrections 34→2/month after only this gate — same embeddings, same pgvector.

For automation where answers trigger tools, see automation expert or talk to me directly.

pgvector HNSW + Hybrid Search That Grounds in India

Postgres is already your ledger — keep vectors there. Laravel 13 ships vector(1536) + whereVectorSimilarTo() + toEmbeddings() per XCO July 20 2026, so DPDP stays VPC-local.

Schema + HNSW

-- Postgres 16 + pgvector 0.8 — data + vectors + ledger in one DB
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  tenant_id text NOT NULL,
  doc_name text NOT NULL,
  chunk_id text UNIQUE NOT NULL,
  content text NOT NULL,
  content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
  embedding vector(1536) NOT NULL,
  created_at timestamptz DEFAULT now()
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX ON documents USING gin (content_tsv);
CREATE INDEX ON documents (tenant_id);

HNSW beats IVFFlat on P95 (34ms vs 110ms for 500K, P50 18ms) — filing-week bursts need it.

Hybrid query in one SQL

# hybrid_search.py — tenant-isolated hybrid
import psycopg2
from openai import OpenAI
client, conn = OpenAI(), psycopg2.connect(dsn="postgresql://app:secret@127.0.0.1:5432/saasnext")

def hybrid_retrieve(query: str, tenant_id: str, k: int = 5):
    qvec = client.embeddings.create(model="text-embedding-3-small", input=query).data[0].embedding
    sql = """
    WITH vector_rank AS (
      SELECT chunk_id, doc_name, content, 1 - (embedding <=> %s::vector) AS v_score
      FROM documents WHERE tenant_id=%s ORDER BY embedding <=> %s::vector LIMIT 40
    ), text_rank AS (
      SELECT chunk_id, ts_rank(content_tsv, websearch_to_tsquery('english', %s)) AS t_score
      FROM documents WHERE tenant_id=%s
    )
    SELECT v.chunk_id, v.doc_name, v.content,
           (0.55*v.v_score + 0.35*COALESCE(t.t_score,0)+0.10*similarity(v.content,%s))::float AS hybrid_score
    FROM vector_rank v LEFT JOIN text_rank t USING (chunk_id)
    ORDER BY hybrid_score DESC LIMIT %s;
    """
    with conn.cursor() as cur:
        cur.execute(sql, (qvec, tenant_id, qvec, query, tenant_id, query, k))
        return [{"chunk_id":r[0],"doc_name":r[1],"text":r[2],"score":float(r[3])} for r in cur.fetchall()]

Weights 0.55 vector / 0.35 ts_rank / 0.10 trigram — tuned in Junagadh. HSN 5208 outranks 5209 via BM25, "kapas" finds cotton via vector. Keep tenant_id=%s on both CTEs — SQL-level isolation per DPDP.

Production Ledger + HITL: The 90-Day Proof From Junagadh

Grounded answer alone is not audit-proof. You must prove retrieval, citation, and approval for 90 days — same data/posts.php:345 invariant as all Junagadh stacks.

CREATE TABLE rag_ledger (
  trace_id text PRIMARY KEY,
  tenant_id text NOT NULL,
  query text NOT NULL,
  retrieved_ids text[] NOT NULL,
  cited_ids text[] NOT NULL,
  confidence text CHECK (confidence IN ('high','medium','low')),
  abstained bool, tool_name text,
  latency_ms int, tokens_used int, policy_decision jsonb,
  created_at timestamptz DEFAULT now()
);
-- weekly replay 500 samples, 2% downgrade gate
SELECT trace_id, query, cited_ids, confidence FROM rag_ledger
WHERE created_at > now()-interval '90 days' ORDER BY created_at DESC LIMIT 500;

HITL ladder (SMEStreet 90-day):

  • Days 1-30 define never-do: Block razorpay_create_link, zoho_create_invoice, tally_voucher_create via OPA, not prompt.
  • Days 31-60 draft mode: Agent drafts answer + citations → Telegram approval. Low confidence auto-abstains.
  • Days 61-90 permit low-risk: Auto-send high confidence with citations inside tenant scope; medium/low and any irreversible stay HITL. Every call ships OTel → 90-day JSONL for DPDP Nov 2025/Nov 2026/May 2027.

A Surat pilot survived a 7-hour fibre cut — RAG stayed VPC-local, ledger queued, export intact. P95 800ms on ₹6K VPS, no egress. Pattern reused in automation expert workflows and MCP USB-C stack.

pgvector vs Qdrant — Honest Table for India 2026

Dimension pgvector (Postgres 16, HNSW) Qdrant Verdict from Junagadh
Latency 500K P50 18ms, P95 34ms P50 8ms, P95 16ms Rust Qdrant faster raw
Hybrid Vector + tsvector in one SQL Payload + sparse separate pgvector simpler
Backup/DR Single pg_dump — data+vectors+ledger Snapshots + DB sync pgvector wins DPDP
VPC / DPDP Already inside — ₹0 extra New cluster, egress pgvector ₹0
Scale To ~2M chunks/tenant fine To 50M+ sharded Qdrant for 10M+
Laravel 13 Native whereVectorSimilarTo HTTP client pgvector native
Cost / mo (500K, 2K q/day) ₹3,200 (RDS already paid) ₹11,700 combined pgvector 72% cheaper
Best for SMEs 5K-500K docs, Junagadh pricing Marketplaces 10M+, sub-10ms 9/10 Gujarat: pgvector

Cross 5M chunks? Benchmark Qdrant. Keep same Pydantic + HITL + ledger.

Frequently Asked Questions

Why does RAG still hallucinate even with good retrieval in India?

Generator ignores retrieval to "be helpful." Pydantic guard requiring citations subset of retrieved IDs blocks it — invent a chunk_id and it fails. Rajkot cut corrections 94% with only this.

When should I use pgvector HNSW vs Qdrant in India 2026?

pgvector for 5K-500K docs/tenant when VPC, single backup, and Laravel 13 native search matter — P95 34ms, one SQL hybrid, ₹0 extra. Qdrant for 10M+ vectors needing sub-10ms and sharding. Both need citation guard + HITL before Razorpay/Zoho.

How does the Pydantic guard stop invented GSTINs or HSN rates?

GroundedAnswer needs citations in retrieved set; verify_grounded() checks containment, else ValidationError. Low confidence abstains with closest chunks. hitl_required() blocks razorpay_create_link/zoho_create_invoice and >₹15K until approval. Nothing reaches filing.

How do you prove hallucination-free RAG for 90 days in a DPDP audit?

Every query emits OTel to rag_ledgertrace_id, tenant_id, retrieved_ids, cited_ids, confidence, abstained, latency_ms, tokens_used, policy_decision — retained 90 days JSONL. Weekly 500-sample replay; >2% downgrade halts promotion. Pi 5 at 62 tok/s keeps ledger queued offline.

Bottom Line: Zero-hallucination RAG in India is a smaller trust boundary: pgvector HNSW + tsvector hybrid in Postgres retrieves HSN 5208 and "kapas," Pydantic GroundedAnswer forces citation or abstain, and HITL + 90-day OTel ledger proves it for DPDP. Ship from Junagadh in one VPC — grounded or not shipped.

← All journal articles Get in touch →