Database Architecture¶
Graffold uses a split database architecture that separates graph storage from vector search. This decouples the concerns of relationship traversal and semantic similarity, allowing each layer to scale independently.
Architecture Overview¶
flowchart LR
subgraph APP["Graffold API"]
QA[Query Agents]
IS[Ingestion Service]
SI[SearchIndex Interface]
end
subgraph GRAPH["Graph Layer — Memgraph"]
MG[(Memgraph)]
MAGE[MAGE Algorithms]
end
subgraph VECTOR["Vector Layer — Cloudflare Vectorize"]
VZ[(Vectorize Index)]
CF_API[REST API]
end
QA -->|Cypher queries| MG
QA -->|similarity search| SI
IS -->|nodes & relationships| MG
IS -->|embeddings| SI
SI --> CF_API --> VZ
MG --> MAGE
Graph Layer: Memgraph¶
Role: Stores all nodes, relationships, and properties. Handles Cypher queries, graph traversals, community detection, and path algorithms.
| Aspect | Detail |
|---|---|
| Engine | Memgraph (in-memory graph database) |
| Image | memgraph/memgraph-mage:latest (includes MAGE algorithm library) |
| Protocol | Bolt (compatible with Neo4j drivers) |
| Query language | openCypher |
| Algorithms | MAGE — PageRank, community detection, shortest path, betweenness centrality |
| Isolation | Dedicated container per knowledge graph (max/enterprise tiers) |
| Memory | 256 MB–1 GB per instance depending on tier |
Why Memgraph over Neo4j?¶
- In-memory performance — sub-millisecond traversals for interactive queries
- Lower resource footprint — runs comfortably in 256 MB containers
- MAGE algorithms — native graph algorithms without APOC dependency
- Bolt-compatible — same driver code works with Neo4j and Memgraph via
DatabaseInterfaceabstraction - Container isolation — trivial to spin up per-tenant instances via Docker
Configuration¶
# Environment variables
DATABASE_TYPE=memgraph
NEO4J_URI=bolt://memgraph:7687 # Uses NEO4J_* vars for Bolt compatibility
NEO4J_USER="" # Memgraph default: no auth
NEO4J_PASSWORD=""
Multi-Instance Deployment (Hetzner Dev)¶
The dev environment runs 5 Memgraph instances for multi-tenant demos:
| Instance | Port | Domain |
|---|---|---|
| demo0 | 7687 | Default / sandbox |
| demo1 | 7688 | Demo tenant 1 |
| demo2 | 7689 | Demo tenant 2 |
| demo3 | 7690 | Demo tenant 3 |
| demo4 | 7691 | Biology (primary KG) |
Vector Layer: Cloudflare Vectorize¶
Role: Stores and queries vector embeddings for semantic similarity search. Offloads all vector operations from the graph database.
| Aspect | Detail |
|---|---|
| Service | Cloudflare Vectorize |
| Index name | graffold-embeddings |
| Dimensions | 768 (all-distilroberta-v1 embeddings) |
| Similarity | Cosine |
| API | REST (Vectorize v2) |
| Insert format | NDJSON |
| Management | Index created via wrangler CLI, not application code |
Why Cloudflare Vectorize?¶
- Memgraph has no native vector indexes — unlike Neo4j's HNSW indexes, Memgraph doesn't support vector search natively
- Edge-deployed — low-latency queries from Cloudflare's global network
- Managed scaling — no infrastructure to maintain for vector search
- Cost-effective — pay-per-query pricing vs. provisioning dedicated vector DB instances
- Decoupled scaling — vector index size grows independently of graph memory
Configuration¶
# Environment variables
CF_ACCOUNT_ID=<cloudflare-account-id>
CF_API_TOKEN=<cloudflare-api-token>
VECTOR_BACKEND=vectorize # Tells agents to use Vectorize for vector search
How It Works¶
- Ingestion — After entity/relationship extraction, the embedding pipeline generates 768-dimensional vectors for nodes and text chunks, then upserts them to Vectorize via NDJSON POST.
- Query — When a query agent needs semantic similarity (e.g., "find proteins similar to BRCA1"), it calls
CloudflareVectorizeSearchIndex.vector_search()which hits the Vectorize REST API. - Hybrid retrieval — Vector results from Vectorize are combined with graph traversal results from Memgraph to produce ranked, contextual answers.
Vector Metadata Schema¶
Each vector stored in Vectorize carries metadata for result enrichment:
{
"id": "node-uuid-or-chunk-id",
"values": [0.012, -0.034, ...],
"metadata": {
"node_id": "uuid",
"name": "BRCA1",
"label": "Protein",
"text": "BRCA1 is a tumor suppressor...",
"source_pmid": "12345678"
}
}
SearchIndex Abstraction¶
The SearchIndex interface (src/core/search.py) abstracts over all vector/fulltext backends:
# Backend selection at runtime
def create_search_index(driver, backend, database) -> SearchIndex:
if backend == "vectorize":
return CloudflareVectorizeSearchIndex()
if backend == "memgraph":
return MemgraphSearchIndex(driver) # fulltext only
if backend == "falkordb":
return FalkorDBSearchIndex(driver)
return Neo4jSearchIndex(driver, database)
When DATABASE_TYPE=memgraph, agents automatically use Cloudflare Vectorize for vector search and Memgraph for fulltext/graph queries. This is configured via the VECTOR_BACKEND=vectorize environment variable or detected automatically when the graph backend is Memgraph.
Data Flow: Split Query Path¶
sequenceDiagram
participant U as User
participant A as Query Agent
participant V as Cloudflare Vectorize
participant M as Memgraph
U->>A: "What proteins interact with BRCA1?"
A->>V: vector_search(embedding("BRCA1"), topK=10)
V-->>A: [{node_id: "abc", score: 0.95, name: "BRCA1"}, ...]
A->>M: MATCH (p:Protein {node_id: "abc"})-[r:INTERACTS_WITH]->(t) RETURN t
M-->>A: [TP53, RAD51, PALB2, ...]
A->>A: Merge vector + graph results, re-rank
A-->>U: "BRCA1 interacts with TP53, RAD51, PALB2..."
Data Flow: Ingestion¶
sequenceDiagram
participant I as Ingestion Pipeline
participant M as Memgraph
participant E as Embedding Model
participant V as Cloudflare Vectorize
I->>M: MERGE (p:Protein {name: "BRCA1", uniprot_id: "P38398"})
I->>M: MERGE (d:Disease {name: "Breast Cancer"})
I->>M: MERGE (p)-[:ASSOCIATED_WITH {confidence: 0.92}]->(d)
I->>E: encode("BRCA1 tumor suppressor protein")
E-->>I: [0.012, -0.034, ...] (768d vector)
I->>V: POST /insert {id: "node-uuid", values: [...], metadata: {...}}
V-->>I: {success: true}
Backend Compatibility Matrix¶
| Feature | Neo4j | Memgraph | FalkorDB |
|---|---|---|---|
| Graph storage | ✅ | ✅ | ✅ |
| Cypher queries | ✅ | ✅ (openCypher) | ✅ |
| Native vector indexes | ✅ (HNSW) | ❌ | ❌ |
| Vector search | Native | CF Vectorize | CF Vectorize |
| Fulltext indexes | ✅ | ✅ (via MAGE) | ✅ |
| Graph algorithms | APOC/GDS | MAGE (built-in) | Limited |
| Multi-tenant isolation | Label-prefix | Dedicated container | Named graphs |
| Memory model | Disk + cache | In-memory | In-memory (Redis) |
| Tier availability | Enterprise | Max / Enterprise | Pro |
Deployment Topology¶
Development (Local)¶
┌─────────────────────────────────────────┐
│ Docker Compose (memgraph profile) │
│ │
│ ┌──────────┐ ┌───────┐ ┌─────────┐ │
│ │ Memgraph │ │ Redis │ │ FastAPI │ │
│ │ :7687 │ │ :6379 │ │ :8000 │ │
│ └──────────┘ └───────┘ └─────────┘ │
└─────────────────────────────────────────┘
│ │
│ ┌─────────┴──────────┐
│ │ Cloudflare Vectorize│
│ │ (graffold-embeddings)│
│ └────────────────────┘
│
┌────┴─────┐
│ Memgraph │
│ Lab │
│ :3000 │
└──────────┘
Production (Hetzner CX33)¶
┌──────────────────────────────────────────────────────┐
│ Hetzner CX33 (4 vCPU, 8 GB RAM) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Caddy (reverse proxy + TLS) │ │
│ │ dev.graffold.com → FastAPI │ │
│ │ wiki.graffold.com → /opt/graffold/wiki/ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ demo0 │ │ demo1 │ │ demo2 │ ...×5 │
│ │ :7687 │ │ :7688 │ │ :7689 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌───────┐ ┌──────────┐ │
│ │ FastAPI │ │ Redis │ │ MG Lab │ │
│ │ :8000 │ │ :6379 │ │ :3000 │ │
│ └──────────┘ └───────┘ └──────────┘ │
└──────────────────────────────────────────────────────┘
│
┌─────────┴──────────┐
│ Cloudflare Vectorize│
│ (edge-deployed) │
└────────────────────┘
Fallback Behavior¶
| Scenario | Behavior |
|---|---|
| Vectorize credentials missing | Vector search returns empty results; fulltext + graph traversal still work |
| Memgraph container down | Provisioner retries; queries fail with connection error |
| Neo4j backend selected | Uses native HNSW vector indexes instead of Vectorize |
VECTOR_BACKEND not set + Memgraph |
Auto-detects and uses Vectorize if CF_ACCOUNT_ID is set |