A pharmaceutical quality team investigates a failed performance qualification test. The PQ protocol for electronic batch record token expiration handling failed — the client-side modal submitted a signature using a cached stale JWT. The deviation is logged. A CAPA is initiated. A change control is opened to patch the EBR system.

Six months later, a new CSV engineer asks: “What’s the current status of the token expiration fix, and which systems are affected?”

In a flat RAG system, this question fragments across vector similarity matches that may surface the original deviation, the CAPA, or the change control — but rarely all three in the correct causal sequence. The relationships between these records are invisible to embedding-based search.

This is the problem Hybrid GraphRAG solves.

The Architecture: Three Stores, Three Truths

The core design principle is separation of concerns across three specialized databases, each serving as the authoritative source for one type of truth:

Store Role Question It Answers Query Pattern
PostgreSQL 16 System of Record “What is the current approved state?” ACID transactions, tsvector full-text, GROUP BY
Qdrant Semantic Retrieval “What content is semantically similar?” Dense + sparse hybrid search with RRF fusion
Memgraph Knowledge Graph “How are things connected?” Cypher multi-hop BFS, atomic GraphRAG

PostgreSQL is the canonical source. Qdrant and Memgraph are derived indexes that rebuild from PostgreSQL — never the other way around. This is the architectural invariant that makes the system auditable under 21 CFR Part 11.

                               ┌────────────────────────────────────────────────────────┐
                               │                    USER QUERY / AGENT                  │
                               └──────────────────────────┬─────────────────────────────┘

                                             ┌────────────▼────────────┐
                                             │    Intent Classifier    │
                                             │    & Query Rewriter     │
                                             └──────┬──────┬─────┬─────┘
                                                    │      │     │
                 ┌──────────────────────────────────┘      │     └──────────────────────────────────┐
                 │ (Semantic Search)                       │ (Multi-Hop / Traceability)             │ (Relational / Audit)
                 ▼                                         ▼                                        ▼
   ┌───────────────────────────┐             ┌───────────────────────────┐            ┌───────────────────────────┐
   │          QDRANT           │             │         MEMGRAPH          │            │        POSTGRESQL         │
   │  - Dense + Sparse vectors │             │  - In-Memory C++ Graph    │            │  - System of Record       │
   │  - Prefetch + RRF Fusion  │             │  - Atomic GraphRAG query  │            │  - ACID Audit Logs        │
   │  - Tenant / GxP Filtering │             │  - Traceability BFS / WSP │            │  - tsvector Full-Text     │
   │  - Sub-5ms ANN Latency    │             │  - MAGE Community Clusters│            │  - Relational Aggregates  │
   └─────────────┬─────────────┘             └─────────────┬─────────────┘            └─────────────┬─────────────┘
                 │                                         │                                        │
                 └──────────────────┬──────────────────────┘                                        │
                                    │                                                               │
                                    ▼                                                               │
                     ┌─────────────────────────────┐                                                │
                     │    Hybrid Fusion & Rerank   │                                                │
                     │    (RRF + Cross-Encoder)    │                                                │
                     └──────────────┬──────────────┘                                                │
                                    │                                                               │
                                    ▼                                                               │
                     ┌─────────────────────────────┐                                                │
                     │        EvidenceGate         │◄───────────────────────────────────────────────┘
                     │ (Zero-Trust Live ACL &      │
                     │  Supersession Revalidation) │
                     └──────────────┬──────────────┘


                     ┌─────────────────────────────┐
                     │   Context Assembly & LLM    │
                     │  (Grounded Citations + Path)│
                     └─────────────────────────────┘

The Intent Router: Not All Queries Are Equal

Before retrieval begins, the system classifies the query intent. This determines how aggressively to weight vector search versus graph traversal:

Intent Trigger Keywords Primary Store Vector Weight Graph Weight
TRACEABILITY_AUDIT “traceability”, “URS-”, “test failed”, “V-model” Memgraph + Postgres 0.3 0.7
IMPACT_ANALYSIS “deviation”, “CAPA”, “root cause”, “ripple effect” Memgraph 0.4 0.6
GLOBAL_GOVERNANCE “all systems”, “GAMP category”, “overview” Postgres + Memgraph 0.5 0.5
SEMANTIC_SEARCH (default) Qdrant + Memgraph 0.7 0.3

A query like “Show the full traceability chain for URS-EBR-001” routes heavily toward Memgraph (70% graph weight). A query like “What does ALCOA+ require for data integrity?” routes toward Qdrant semantic search (70% vector weight).

This is not a hard switch — it is a weight adjustment in the Reciprocal Rank Fusion scoring.

The Qdrant collection stores both dense embeddings (384-dimensional vectors from all-MiniLM-L6-v2 via FastEmbed) and sparse token-weight vectors (BM25-style term frequency hashing). The hybrid search executes as parallel prefetch queries fused with Reciprocal Rank Fusion:

# Parallel prefetch: dense + sparse
prefetch_queries = [
    models.Prefetch(query=dense_vector, using="dense", filter=query_filter, limit=limit * 3),
    models.Prefetch(
        query=models.SparseVector(indices=sparse_indices, values=sparse_values),
        using="sparse", filter=query_filter, limit=limit * 3
    )
]

# RRF fusion
search_result = client.query_points(
    collection_name=collection_name,
    prefetch=prefetch_queries,
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    limit=limit
)

Payload pre-filtering ensures that superseded documents (is_superseded == False) and system-specific queries (system_name == 'EBR') are filtered at the index level, not post-retrieval. This is critical for GxP — you cannot surface an obsolete SOP alongside an active one and expect the LLM to distinguish them.

The Qdrant dashboard shows the collection with its payload indexes on doc_code, doc_type, system_name, gxp_category, lifecycle_status, and is_superseded:

Qdrant dashboard showing the lifescience_qms_docs collection with dense and sparse named vectors

Memgraph: Multi-Hop Knowledge Graph

Memgraph stores the structural relationships that vector search cannot capture. The graph model encodes the GAMP 5 V-Model lifecycle:

(Requirement)-[:IMPLEMENTS_URS]->(FunctionalSpec)
(FunctionalSpec)-[:SPECIFIES_FS]->(DesignSpec)
(DesignSpec)-[:VERIFIES_DS]->(TestScript)
(TestScript)-[:TRIGGERED_DEVIATION]->(Deviation)
(Deviation)-[:RESOLVED_BY_CAPA]->(CAPA)
(CAPA)-[:REQUIRES_CHANGE_CONTROL]->(ChangeControl)
(ChangeControl)-[:MODIFIES_SYSTEM]->(System)

The multi-hop traceability query traverses this chain in a single Cypher statement:

MATCH (u:Requirement)
WHERE toLower(u.code) CONTAINS toLower($keyword)
OPTIONAL MATCH (f:FunctionalSpec)-[:IMPLEMENTS_URS]->(u)
OPTIONAL MATCH (ds:DesignSpec)-[:SPECIFIES_FS]->(f)
OPTIONAL MATCH (t:TestScript)-[:VERIFIES_DS]->(ds)
OPTIONAL MATCH (t)-[:TRIGGERED_DEVIATION]->(dev:Deviation)
OPTIONAL MATCH (dev)-[:RESOLVED_BY_CAPA]->(capa:CAPA)
RETURN u.code, f.code, ds.code, t.code, t.status,
       dev.code, dev.severity, capa.code

Memgraph also supports Atomic GraphRAG — vector search entry points directly within Cypher using vector_search.search(). This lets the system find semantically similar chunks and immediately expand into the surrounding graph neighborhood:

CALL vector_search.search("chunk_vec", $limit, $query_embedding)
YIELD node AS start_chunk, similarity
OPTIONAL MATCH path = (start_chunk)-[*1..2]-(connected)
RETURN start_chunk.code, similarity,
       [n IN nodes(path) | {label: labels(n)[0], code: n.code}] AS context_entities

The Memgraph Lab visualization shows the full knowledge graph — requirement nodes, test scripts, deviations, CAPAs, and change controls connected by typed relationships:

Memgraph Lab showing the knowledge graph with requirement traceability chains, deviations, and CAPA linkages

EvidenceGate: Zero-Trust Retrieval

This is the component that makes the system safe for regulated environments. Every candidate chunk retrieved from Qdrant or Memgraph is intercepted before LLM prompt assembly and validated against the live PostgreSQL state:

Retrieved Candidates (Qdrant & Memgraph)


       ┌────────────────────┐
       │    EvidenceGate    │ ─── Queries live PostgreSQL System of Record
       └──────────┬─────────┘

       ┌──────────┴──────────┐
       ▼                     ▼
[APPROVED & EFFECTIVE]    [SUPERSEDED / OBSOLETE]
       │                     │
       ▼                     ▼
Prompt Assembly        Rejected & Logged
(Citations injected)   (Audit reason recorded)

The validation checks three conditions against PostgreSQL:

  1. Existence — the chunk code must exist in the canonical documents + chunks tables
  2. Lifecycle status — must be EFFECTIVE or APPROVED (not DRAFT, OBSOLETE, or QUARANTINED)
  3. Supersessionis_superseded must be FALSE

If a chunk fails any check, it is rejected with a reason code (e.g., DOCUMENT_IS_SUPERSEDED, INVALID_LIFECYCLE_STATUS) and logged to the audit trail. The LLM never sees it.

This solves the stale index problem: if an SOP is placed on QA hold after a deviation, the vector embeddings in Qdrant may still return it as a top hit. EvidenceGate catches this at query time by checking the live PostgreSQL state.

The 5-Step Closed-Loop Workflow

The Pydantic AI agent orchestrates a five-step workflow that closes the knowledge loop:

                        ┌───────────────────────────────┐
                        │        1. Query Intake        │
                        └───────────────┬───────────────┘


                        ┌───────────────────────────────┐
                   ┌───►│   2. Vector/Graph Retrieval   │
                   │    └───────────────┬───────────────┘
                   │                    │
                   │                    ▼
Knowledge          │    ┌───────────────────────────────┐
Feedback           │    │  3. Agent Response & Feedback │
Loop               │    │       (Pydantic AI)           │
                   │    └───────────────┬───────────────┘
                   │                    │
                   │                    ▼
                   │    ┌───────────────────────────────┐
                   │    │ 4. Ticket Escalation/Resolut. │
                   │    └───────────────┬───────────────┘
                   │                    │
                   │                    ▼
                   │    ┌───────────────────────────────┐
                   └────┤ 5. Auto-Ingestion & Indexing  │
                        └───────────────────────────────┘

Step 1 (Query Intake): Accepts the query, user role, query type, and human-in-the-loop toggle. Assigns a tracking ID (TICK-XXXXXX) for 21 CFR Part 11 provenance.

Step 2 (Retrieval): Executes parallel retrieval across Qdrant (dense + sparse hybrid), Memgraph (multi-hop traversal), and PostgreSQL (EvidenceGate verification).

Step 3 (Agent Response): The Pydantic AI agent — powered by DeepSeek-v4-flash via OpenAI-compatible API — synthesizes a structured HelpdeskResolution with confidence score, GxP risk rating, verified citations, and graph traversal paths.

Step 4 (Escalation): Routes based on risk. Critical deviations escalate to the QA Lead. Standard inquiries with high confidence auto-resolve. Human-in-the-loop mode forces PENDING_HUMAN_IN_LOOP for electronic signature approval.

Step 5 (Knowledge Feedback Loop): This is the self-learning step. The resolution is auto-ingested as a new canonical document into PostgreSQL, projected as vector embeddings into Qdrant, and linked as entities in Memgraph. The next query on a similar topic immediately retrieves this learned experience. Resolution accuracy increments by ~1.8% per cycle.

The structured output from the Pydantic AI agent enforces type safety:

class HelpdeskResolution(BaseModel):
    resolution_summary: str
    confidence_score: float = Field(ge=0.0, le=1.0)
    gxp_risk_level: Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    citations: List[CitationItem]
    graph_paths: List[str]
    action_verdict: Literal["AUTO_RESOLVED", "ESCALATED_TO_QA", "PENDING_HUMAN_IN_LOOP"]
    knowledge_feedback_generated: bool
    feedback_details: Optional[KnowledgeUpdatePayload]

The Seed Data: Realistic GxP Scenarios

The system ships with a seed dataset that mirrors real pharmaceutical quality management:

Regulatory Governance: FDA 21 CFR Part 11 (electronic records and signatures), EU Annex 11 (computerised systems), ISPE GAMP 5 (V-Model lifecycle), ALCOA+ data integrity principles.

Target Systems: Electronic Batch Record (EBR), Chromatography Data System (CDS — Empower), LIMS, Manufacturing Execution System (MES — DeltaV), Veeva Vault QMS.

Traceability Chains:

  • URS-EBR-001FS-EBR-014DS-EBR-089OQ-EBR-101 (PASSED)
  • URS-EBR-001FS-EBR-014DS-EBR-089PQ-EBR-202 (FAILED) → DEV-2026-091CAPA-2026-022CC-2026-068

Quality Events: Two complete deviation-CAPA-change control chains covering a token expiration race condition in EBR signatures and a chromatography audit trail gap under high concurrency.

The Web UI

The system includes an interactive web UI at localhost:8090 with an animated 5-step workflow simulator, query type carousel (Standard, Multi-Hop Traceability, Deviation Impact, Complex Incident), human-in-the-loop toggle, live metrics bar, and a Cytoscape.js subgraph visualizer for inspecting knowledge graph nodes and edges:

Hybrid GraphRAG web UI showing the architecture simulator with the 5-step closed-loop workflow, query controls, and live metrics

Docker Compose Deployment

The entire stack runs in Docker Compose with five containers:

Docker processes showing all five Hybrid GraphRAG containers running: PostgreSQL, Qdrant, Memgraph, Memgraph Lab, and the API

Container Image Port Role
hybrid-rag-postgres pgvector/pgvector:pg16 5434 System of record
hybrid-rag-qdrant qdrant/qdrant:v1.13.4 6335 Vector search
hybrid-rag-memgraph memgraph/memgraph-mage:latest 7688 Knowledge graph
hybrid-rag-memgraph-lab memgraph/lab:latest 3006 Graph visualization
hybrid-rag-api Custom (Python 3.12) 8090 FastAPI + Web UI

Launch with a single command:

./run.sh
# Or manually:
docker compose up -d --build
docker compose exec api python -m src.seed.seed_all

The LLM Fallback Chain

The synthesizer implements a four-tier fallback for LLM generation:

  1. DeepSeek-v4-flash (primary) — via OpenAI-compatible API
  2. OpenAI GPT-4o-mini — fallback if DeepSeek key is missing
  3. Ollama local models — for air-gapped environments
  4. Built-in regulatory rule engine — zero external dependency, intent-specific synthesis using retrieved evidence only

The fourth tier is important: the system works without any LLM. The rule engine produces structured regulatory assessments by formatting verified chunks and graph paths into intent-specific templates. This is the deterministic fallback that keeps the system functional during LLM outages.

The Bottom Line

Flat RAG works for simple Q&A over unstructured documents. It breaks the moment you need to trace a requirement through a functional specification, into a design specification, through a test protocol, into a deviation, through a CAPA, and into a change control — and verify that every node in that chain is still active and approved.

Hybrid GraphRAG solves this by giving each database a single job: PostgreSQL owns the truth, Qdrant finds the similarity, Memgraph maps the relationships. EvidenceGate ensures the LLM only sees verified, current, approved evidence. And the knowledge feedback loop means the system gets smarter with every query it resolves.

The codebase is open source under MIT. The seed data encodes realistic pharmaceutical quality scenarios. The Docker Compose stack runs on any machine with 8GB of RAM.

If you are building AI systems for regulated environments — where a hallucinated citation can trigger an FDA 483 observation — this is the architecture pattern to study.


Source code: hybrid-graphrag on GitHub • Architecture: PostgreSQL 16 + Qdrant v1.13.4 + Memgraph MAGE + Pydantic AI • LLM: DeepSeek-v4-flash