Skip to content

Build1 publisher3 min readPublished

Putting the name inside a MERGE pattern forks one person into two nodes

A dev.to post on five Cypher patterns for agent memory lands hardest on the write path. Agents re-extract the same entity from many documents, and a MERGE keyed on a mutable name creates a fresh node each time the string changes.

The Engineer · Build desk

Illustration accompanying Putting the name inside a MERGE pattern forks one person into two nodes

What happened

  • A dev.to post sets out five Cypher patterns for graph-backed agent memory and states that all of them run against any Bolt or Cypher endpoint.
  • MERGE matches on the entire pattern it is given, so a MERGE keyed on both id and name creates a second node as soon as the name string changes.
  • The opening example is a query that quietly returned 40,000 rows because two unrelated patterns were matched in the same clause.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Choosing the identifying property becomes a precondition of the schema, not a cleanup pass later, because everything else has to move out of the merge key and into SET.
  • exposure A pipeline with more than one writer can fork an entity's memory without raising anything; the duplicate answers queries like any other node.
  • cost Retrieval radius is paid for in prompt tokens, and a single high-degree node sets how expensive a two-hop walk can get on a bad day.
  • decision Writing source and confidence onto the relationship at merge time decides whether justification arrives with the answer or has to be rebuilt by hand afterwards.

MERGE is a match, then a create when the match misses, in one atomic step [5]. What bites is the definition of the match. The pattern you write is the pattern the engine looks for, all of it [7]. So `MERGE (p:Person {id: $id, name: $name})` stops matching the moment the extractor hands over a different spelling, and you get a second Person node carrying the same id [7]. The post's rule is to merge on the identifying property only, then SET the rest [8], with `ON CREATE SET` writing name and first_seen and `ON MATCH SET` touching last_seen [6].

That failure is quiet. The same entity gets extracted from three different documents and the same relationship inferred twice, according to the post [3]. Push three name variants of one person through a full-pattern MERGE and you hold three nodes sharing one id, with no error raised [2]. CREATE would duplicate outright, and check-then-insert races [4].

The uniqueness constraint is the half that is easy to defer to a later migration. The post is explicit that without a constraint on the merge property, two concurrent merges can still both create [9].

Read the two variable-length patterns next to each other. The dependency walk caps at `[:DEPENDS_ON*1..4]` [10]. The retrieval subgraph caps at `[*1..2]` [16]. Each query picks its own bound. The post's argument against an unbounded `*` is that in a social-ish graph almost everything is reachable within six hops, so the traversal degrades into what it calls "scan the graph" [12]. That is a claim about a dense graph. Whether it transfers to your DEPENDS_ON edges depends on the out-degree of your own services, and the post's advice for the unknown case is to start at 3 and measure [13]. DISTINCT plus `min(length(path))` is what stops the same dependency coming back once per route [10][14].

Radius costs prompt tokens. Two hops is usually plenty, and three often doubles the context for very little extra signal, per the post [17]. One unexpectedly popular hub node can drag half the graph into the prompt, so cap the result size explicitly [18].

A plain MATCH on citations drops the whole document row when the document has none, and the post notes that this only shows up on the one record the demo uses [15].

Provenance goes on the relationship as it is written: source document, extraction method, confidence and asserted_at, all inside `ON CREATE SET` [19]. A retrieval query then returns the path together with a list comprehension over its relationships, so there is no separate audit table and no reconstructing the reasoning by hand [20].

The 40,000-row query at the top of the post is a cartesian product [1]. `MATCH (p:Person), (c:Company)` pairs every Person with every Company [21], and 200 nodes of each label are enough to reach that count [1]. The fixes are connecting the patterns with a relationship or splitting them across WITH boundaries, and the post says most engines warn [21]. Before blaming the database, run PROFILE: it shows rows per operator, and the culprit is usually an unindexed label scan sitting at the bottom [22].

All of it is standard Cypher against any Bolt endpoint [2]. The post adds that its author has been using CognoDB for prototypes [23].

What to watch

  • A measured depth bound from a real dependency graph would test whether the six-hop reachability figure carries outside social-shaped data.
  • Confirmation that the Bolt endpoint you point at actually enforces uniqueness constraints, since the MERGE advice rests on that enforcement.
  • Whether the cartesian-product warning the post relies on engines emitting reaches your driver logs at all.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories