leveraging the power of graph databases in php

97
Leveraging the Power of Graph Databases in PHP Jeremy Kendall Nashville PHP November 2014

Upload: jeremy-kendall

Post on 13-Jul-2015

356 views

Category:

Technology


0 download

TRANSCRIPT

Leveraging the Power of Graph Databases

in PHP

Jeremy Kendall Nashville PHP

November 2014

Obligatory Intro Slide

Also - New Father

What Kind of Database?

Graphs != Charts

https://www.flickr.com/photos/markgroves/3065192499/

Graphs != Charts

http://stephenwildish.tumblr.com/post/101408321763/friday-project-witch-moral-compass

Graph Databases

Graph Databases• Data Model!

• Nodes with properties

• Typed relationships

Graph Databases• Data Model!

• Nodes with properties

• Typed relationships

• Strengths!

• Highly connected data

• ACID

Graph Databases• Data Model!

• Nodes with properties

• Typed relationships

• Strengths!

• Highly connected data

• ACID

• Weaknesses!

• Paradigm shift

Graph Databases• Data Model!

• Nodes with properties

• Typed relationships

• Strengths!

• Highly connected data

• ACID

• Weaknesses!

• Paradigm shift

• Examples!

• Neo4j, Titan, OrientDB

Why Care?

Why Care?• All the NoSQL Joy

• Schema-less

• Semi-structured data

Why Care?• All the NoSQL Joy

• Schema-less

• Semi-structured data

• Escape from JOIN Hell

Why Care?• All the NoSQL Joy

• Schema-less

• Semi-structured data

• Escape from JOIN Hell

• Speed

Why Care?

Why Care?

• Relationships have 1st class status

Why Care?

• Relationships have 1st class status

• Just as important as the objects connecting them

Why Care?

• Relationships have 1st class status

• Just as important as the objects connecting them

• You can have properties & labels

Why Care?

• Relationships have 1st class status

• Just as important as the objects connecting them

• You can have properties & labels

• Multiple relationships

Why Care?

Speed

Depth MySQL Query Time Neo4j Query Time Records Returned

2 0.028 (28 MS) 0.04 ~900

3 0.213 0.06 ~999

4 10.273 0.07 ~999

5 92.613 0.07 ~999

1,000 people with an average 50 friends each

Crazy SpeedDepth MySQL Query Time Neo4j Query Time Records Returned

2 0.016 (16 MS) 0.01 ~2500

3 30.27 0.168 ~125,000

4 1543.505 1.359 ~600,000

5 Stopped after 1 hour 2.132 ~800,000

1,000,000 people with an average 50 friends each

Neo4j + Cypher

Cypher

• Neo4j’s declarative query language

• Easy to pick up

• Some clauses and concepts familiar from SQL

Simple Example

Goal

Create Some NodesCREATE (jk:Person { name: "Jeremy Kendall" })!CREATE (gs:Company { name: "Graph Story" })!!CREATE (tn:State { name: "Tennessee" })!CREATE (memphis:City { name: "Memphis" })!CREATE (nashville:City { name: "Nashville" })!!CREATE (hotchicken:Food { name: "Hot Chicken" })!CREATE (bbq:Food { name: "Barbecue" })!CREATE (photography:Hobby { name: "Photography" })!CREATE (language:Language { name: "PHP" })!!// . . . snip . . .!

Create Some Relationships

// . . . snip . . .!!CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! !// . . . snip . . .!

Create Some Relationships

// . . . snip . . .!!CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! !// . . . snip . . .!

Create Some Relationships

// . . . snip . . .!!CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! !// . . . snip . . .!

Create Some Relationships

// . . . snip . . .!!CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! !// . . . snip . . .!

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Example Cypher Query

MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!WITH p, l!MATCH (p)-[:WORKS_AT]->(j)!WITH p, l, j!MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!RETURN p, l, j, o

Query Result

Neo4j + PHP

Neo4jPHP• PHP wrapper for the Neo4j REST API

• Installable via Composer

• Used internally at Graph Story

• Used in this presentation

• Well tested

• https://packagist.org/packages/everyman/neo4jphp

Also see: NeoClient• Written by Neoxygen

• Alternative PHP wrapper for the Neo4j REST API

• Installable via Composer

• Under review for internal use at Graph Story

• Well tested

• https://packagist.org/packages/neoxygen/neoclient

Connecting$neo4jClient = new \Everyman\Neo4j\Client(! ‘yourgraph.example.com’, ! 7473!);!!$neo4jClient->getTransport()! ->setAuth('username', 'password')! ->getTransport()->useHttps();

Creating a Node and Label

$node = new Node($neo4jClient);!!$label = $neo4jClient->makeLabel('Person');!!$node->setProperty('name', ‘Jeremy Kendall');!!$node->save()->addLabels(array($label));

Searching

// Searching for a label by property!$label = $neo4jClient->makeLabel('Person');!$nodes = $label->getNodes('name', $name);

Querying (Cypher)

$queryString = ! 'MATCH (p:Person { name: { name }}) RETURN p';!!$query = new \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();

Named Parameters

Named Parameters

$queryString = ! 'MATCH (p:Person { name: { name }}) RETURN p';!!$query = new \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();

Named Parameters

$queryString = ! 'MATCH (p:Person { name: { name }}) RETURN p';!!$query = new \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();

Content Modeling: News Feeds

News Feed

• Modeled as a list of posts

• Newest post first

• All subsequent posts follow

• Relationships: LASTPOST and NEXTPOST

LASTPOST

NEXTPOST

The Content Modelclass Content!{! public $node;! public $nodeId;! public $contentId;! public $title;! public $url;! public $tagstr;! public $timestamp;! public $userNameForPost;! public $owner = false;!}

Adding Contentpublic static function add($username, Content $content)!{! $queryString =<<<CYPHER!MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner!CYPHER;!! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();!! return self::returnMappedContent($result);!}

Adding Contentpublic static function add($username, Content $content)!{! $queryString =<<<CYPHER!MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner!CYPHER;!! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();!! return self::returnMappedContent($result);!}

Adding Contentpublic static function add($username, Content $content)!{! $queryString =<<<CYPHER!MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner!CYPHER;!! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();!! return self::returnMappedContent($result);!}

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content

MATCH (user { username: {u}})!OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!DELETE r!CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })!WITH p, collect(lastpost) as lastposts!FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!RETURN p, {u} as username, true as owner

Adding Content$query = new Query(! $neo4jClient,! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )!);!!$result = $query->getResultSet();

Retrieving Contentpublic static function getContent($username, $skip)!{! $queryString = <<<CYPHER!MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4!CYPHER;!! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'skip' => $skip,! )! );!! $result = $query->getResultSet();!! return self::returnMappedContent($result);!}

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!WITH DISTINCT f, u!MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!RETURN p, f.username as username, f = u as owner!ORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Editing Contentpublic static function edit(Content $content)!{! $updatedAt = time();!! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();!! $content->updated = $updatedAt;!! return $content;!}

Editing Contentpublic static function edit(Content $content)!{! $updatedAt = time();!! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();!! $content->updated = $updatedAt;!! return $content;!}

Editing Contentpublic static function edit(Content $content)!{! $updatedAt = time();!! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();!! $content->updated = $updatedAt;!! return $content;!}

Editing Contentpublic static function edit(Content $content)!{! $updatedAt = time();!! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();!! $content->updated = $updatedAt;!! return $content;!}

Editing Contentpublic static function edit(Content $content)!{! $updatedAt = time();!! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();!! $content->updated = $updatedAt;!! return $content;!}

Deleting Contentpublic static function delete($username, $contentId)!{! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );!! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );!! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();!}

Deleting Contentpublic static function delete($username, $contentId)!{! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );!! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );!! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();!}

Deleting Contentpublic static function delete($username, $contentId)!{! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );!! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );!! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();!}

Deleting Contentpublic static function delete($username, $contentId)!{! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );!! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );!! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();!}

Deleting Content: Leaf

// If leaf!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})!WITH c!MATCH (c)-[r]-()!DELETE c, r

Deleting Content: Leaf

// If leaf!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})!WITH c!MATCH (c)-[r]-()!DELETE c, r

Deleting Content: Leaf

// If leaf!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})!WITH c!MATCH (c)-[r]-()!DELETE c, r

Deleting Content: Leaf

// If leaf!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})!WITH c!MATCH (c)-[r]-()!DELETE c, r

Deleting Content: Leaf

// If leaf!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})!WITH c!MATCH (c)-[r]-()!DELETE c, r

Deleting Content: LASTPOST

// If last!MATCH (u:User { username: { username }})-[lp:LASTPOST]->(del:Content { contentId: { contentId }})-[np:NEXTPOST]->(nextPost)!CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!DELETE lp, del, np

Deleting Content: LASTPOST

// If last!MATCH (u:User { username: { username }})-[lp:LASTPOST]->(del:Content { contentId: { contentId }})-[np:NEXTPOST]->(nextPost)!CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!DELETE lp, del, np

Deleting Content: LASTPOST

// If last!MATCH (u:User { username: { username }})-[lp:LASTPOST]->(del:Content { contentId: { contentId }})-[np:NEXTPOST]->(nextPost)!CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!DELETE lp, del, np

Deleting Content: LASTPOST

// If last!MATCH (u:User { username: { username }})-[lp:LASTPOST]->(del:Content { contentId: { contentId }})-[np:NEXTPOST]->(nextPost)!CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!DELETE lp, del, np

Deleting Content: Other

// All other!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)!CREATE UNIQUE (before)-[:NEXTPOST]->(after)!DELETE del, delBefore, delAfter

Deleting Content: Other

// All other!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)!CREATE UNIQUE (before)-[:NEXTPOST]->(after)!DELETE del, delBefore, delAfter

Deleting Content: Other

// All other!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)!CREATE UNIQUE (before)-[:NEXTPOST]->(after)!DELETE del, delBefore, delAfter

Deleting Content: Other

// All other!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)!CREATE UNIQUE (before)-[:NEXTPOST]->(after)!DELETE del, delBefore, delAfter

Deleting Content: Other

// All other!MATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)!CREATE UNIQUE (before)-[:NEXTPOST]->(after)!DELETE del, delBefore, delAfter

Questions?

Thanks! !

[email protected] @JeremyKendall

http://www.graphstory.com