Product catalog as the quickstart: each Product document
carries its category, its supplier and the supplier’s region, and its
tags — gathered across up to 2 hops.
VentStream writes to a target. OpenSearch and Elasticsearch are
the supported targets today — they share the
_bulk API, so the same
engine handles both. The examples here use OpenSearch; set the endpoint
with VS_OS_ENDPOINT.Source requirements
VentStream uses thedb.cdc.* procedures, so use a release listed in the
Neo4j CDC documentation. This includes
supported Neo4j Enterprise releases and the AuraDB Business Critical and
Virtual Dedicated Cloud tiers. Enable CDC for each database. For denormalized
projections, DIFF is the recommended enrichment mode:
DIFF vs FULL enrichment
txLogEnrichment controls how much each change records in the transaction log:
DIFF— records only what changed (changed properties; the full before-state on deletes; labels and keys remain available).FULL— records a complete before+after copy of every changed entity.
DIFF for projection mode. The connector re-runs the projection Cypher
against the live graph after a change; the CDC record identifies what changed
but is not used as the projected document. FULL therefore adds transaction-log
volume without changing the projection result.
Bootstrap is unaffected because the snapshot reads the graph directly.
Choose FULL only if you consume the raw CDC tail (no projection spec)
and a downstream consumer needs the complete entity state on every event —
there, DIFF updates carry just the changed fields. VentStream’s
denormalize/projection mode (this connector) does not need that.
Production prerequisites
In the demo the agent connects asneo4j — the default
admin user — so it can enable CDC and read everything. A real
production user usually isn’t an admin, so split the setup:
A non-admin engine user
Grant a dedicated read role rather than handing the engine theneo4j
admin account:
MATCH grant to only the labels/relationships your projection
reads if you’d rather not grant graph-wide read.
Why a dedicated role instead of the
neo4j admin? Security — least
privilege. The engine only reads (CDC + MATCH), so it should run as a
read-only role. If its credentials leak, the exposure is read access to
the graph, not admin over the DBMS — and you can revoke or rotate it
without touching the admin account. The engine works fine as the admin
user (the demo does exactly that) — but in production, don’t.No server-side resource to clean up. Unlike a Postgres slot, the
Neo4j CDC cursor lives on the agent’s PVC, not the server — retiring
an agent leaves nothing behind on Neo4j. The flip side: if an agent is
down longer than the database’s transaction-log retention
(
db.tx_log.rotation.retention_policy), its cursor expires and it must
re-bootstrap. Size retention to cover your worst-case agent downtime.Enabling
txLogEnrichment enriches transactions going forward only —
it does not back-enrich existing data. That’s fine: the snapshot
bootstrap (VS_NEO4J_BOOTSTRAP_MODE=snapshot) reads existing nodes
directly, and CDC carries every change after enrichment is on.TLS / Bolt cert
SetVS_NEO4J_TLS_MODE=verify_full to require encrypted Bolt and validate both
the certificate chain and hostname. The engine selects the strict
neo4j+s:// or bolt+s:// scheme automatically.
The projection
- bind the primary as
p(the engine injects the anchor), - return
primaryEid(a string element ID) anddoc(the document map).
fan_out_max_hops: 2 lets a change up to 2 hops from a Product recompute
that Product’s document — covering Category (1 hop), Supplier (1 hop),
and the Supplier→Region chain (2 hops). The full version is in
demo/stack/specs/products.yaml.
Run the agent
Category and Region, each referenced by many products). This is
what keeps a single edge change from cascading across the graph. See
Fan-out.
The temporal-contract gotcha
The catalog spec above has no validity windows, so every edge shows up immediately. But many graphs gate relationships on a time window —fromDate/thruDate on the edge — and that introduces a subtle trap.
The fan-out does fire (you’ll see recomposed=N in the log) — the
projection just returns nothing for that edge because null <= now
evaluates to null (falsy). Fix: set fromDate on edges your spec gates:
fromDate; only ad-hoc Cypher inserts
tend to forget. To diagnose: query the edge and check
r.fromDate IS NOT NULL.