How do I load a large graph into an empty database?¶
You have a large input and an empty database, and loading it the ordinary way, one batch at a time, is too slow. Each batch asks the database, for every vertex and edge, whether it already exists.
A bulk load answers that question once, before writing anything. GraFlo casts the whole input to a staging directory and folds every vertex and edge to one row by its identity. It then writes each row once, with no lookups. The graph it ends with is the one an ordinary load of the same input would produce.
flowchart LR
csv[CSV file] -- cast --> stage[(artifacts/staging)]
stage -- fold --> stage
stage -- write once --> neo4j[(Neo4j)]
What you need¶
- GraFlo with the
bulkextra (pip install 'graflo[bulk]'). - The repository's Neo4j container, see
docker/README.md.
The data¶
data/works.csv lists papers with their author and venue.
Paper W2 appears twice: the second row corrects a typo in its title.
manifest.yaml makes a work vertex per paper, an author
and a venue, and an edge from each work to its author and to its venue.
Steps¶
1. Load in bulk¶
ingest.py asks for bulk mode and names the staging directory:
engine.define_and_ingest(
manifest=manifest,
target_db_config=conn_conf,
ingestion_params=IngestionParams(mode="bulk", staging_dir=staging),
recreate_schema=True,
)
From the command line, the same load is
uv run ingest --db-config-path db.yaml --schema-path manifest.yaml --bulk --staging-dir artifacts/staging.
2. See what the fold did¶
The script prints artifacts/staging/counts.json: five work rows were staged
and four folded, since both rows of W2 became one vertex holding the later
title. Ada wrote two papers, so she is one author; the two written_by rows
of W2 are one edge. The load then checked that Neo4j holds exactly these
counts.
3. Resume a stopped load¶
Stop the script during the write and run it again with the same staging
directory. It does not cast or fold again, skips the chunks it wrote, and
writes again the one chunk whose commit it did not record. To start over,
delete artifacts/staging.
What next¶
Bulk load lists what bulk mode cannot load, which databases support it, and how to tune it.
Files¶
The example lives in examples/13-bulk-load.
manifest.yaml
schema:
metadata:
name: works
graph:
vertex_config:
vertices:
- name: work
properties:
- id
- title
- year
identity:
- id
- name: author
properties:
- name
identity:
- name
- name: venue
properties:
- name
identity:
- name
edge_config:
edges:
- source: work
target: author
relation: written_by
- source: work
target: venue
relation: published_in
db_profile: {}
ingestion_model:
resources:
- name: works
pipeline:
- vertex: work
from:
id: id
title: title
year: year
- vertex: author
from:
name: author
- vertex: venue
from:
name: venue
bindings:
connectors:
- regex: "^works.*\\.csv$"
sub_path: data
resource_name: works
ingest.py
"""How do I load a large graph into an empty database?
Reads ``manifest.yaml`` and loads ``data/works.csv`` into Neo4j in bulk: the
input is cast and staged in ``artifacts/staging``, folded to one row per vertex
and edge, then written once. Run it from this directory:
uv run python ingest.py
Run it again to resume a stopped load; delete ``artifacts/staging`` to start over.
"""
import json
from pathlib import Path
from suthing import FileHandle
from graflo import GraphManifest
from graflo.connections import Neo4jConfig
from graflo.hq import GraphEngine
from graflo.hq.caster import IngestionParams
manifest = GraphManifest.from_config(FileHandle.load("manifest.yaml"))
manifest.finish_init()
# Connection settings of the Neo4j container started from docker/neo4j.
conn_conf = Neo4jConfig.from_docker_env()
staging = Path("artifacts/staging")
engine = GraphEngine(target_db_flavor=conn_conf.connection_type)
engine.define_and_ingest(
manifest=manifest,
target_db_config=conn_conf,
ingestion_params=IngestionParams(mode="bulk", staging_dir=staging),
# The target must be empty: drop whatever an earlier run left there.
recreate_schema=True,
)
for run in json.loads((staging / "counts.json").read_text())["runs"]:
kind = run["type"]
name = kind["name"] if kind["kind"] == "vertex" else " -> ".join(kind["edge_id"])
print(f"{name}: {run['staged']} staged, {run['folded']} folded")