Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DeCYPHERing Graph Databases

DeCYPHERing Graph Databases

What if you could design a database on paper, and translate that design directly into your application without having to make any changes? What if you could sit down and design that database with a product owner or business analyst, and they could understand that design without any training? That's the future that graph databases has to offer.

In this talk, Stuart will walk you through a real-life graph database that powers one of his client's sites. He'll show you how he built it in Neo4J, how to query it using CYPHER - Neo4J's query language and secret weapon - and how to hook it all up to a PHP web-app.

Presented at PHPEm on 5th April 2018.

2c1dc90ff7bf69097a151677624777d2?s=128

Stuart Herbert

April 05, 2018
Tweet

Transcript

  1. A presentation by @stuherbert
 for @GanbaroDigital DeCYPHERing Graph Databases With

    PHP And Neo4J
  2. Industry veteran: architect, engineer, leader, manager, mentor F/OSS contributor since

    1994 Talking and writing about PHP since 2004 Chief Software Archaeologist @GanbaroDigital About Stuart
  3. Follow me I do tweet a lot about non-tech stuff

    though :) @stuherbert
  4. @GanbaroDigital Why am I giving this talk?

  5. @GanbaroDigital Story time with Stu!

  6. @GanbaroDigital https://flic.kr/p/nUiUbU Canary Wharf

  7. @GanbaroDigital https://flic.kr/p/Tjs2DM

  8. @GanbaroDigital https://flic.kr/p/ciLwbo

  9. @GanbaroDigital

  10. @GanbaroDigital

  11. @GanbaroDigital

  12. @GanbaroDigital

  13. @GanbaroDigital

  14. @GanbaroDigital

  15. @GanbaroDigital After describing their business, they said something profound:

  16. @GanbaroDigital ... “we don’t know how to model that in

    a database.”
  17. @GanbaroDigital

  18. @GanbaroDigital https://flic.kr/p/ee8yvi

  19. @GanbaroDigital A crucial part of building back-office systems is turning

    the business model into a RDBMS model.
  20. @GanbaroDigital ?? ?? What if we could use the same

    model for the business and the database?
  21. @GanbaroDigital ?? ?? What if we could use the same

    model for the business and the database?
  22. @GanbaroDigital Same Model • Same entities • ... with the

    same names • ... and the same terms for connections
  23. @GanbaroDigital I don’t mean use an ORM to pretend it’s

    the same. I mean the database itself uses the same model that the business does.
  24. @GanbaroDigital I don’t mean use an ORM to pretend it’s

    the same. I mean the database itself uses the same model that the business does.
  25. @GanbaroDigital https://flic.kr/p/5LsLri

  26. @GanbaroDigital We can do that using a Graph database.

  27. @GanbaroDigital We can do that using a Graph database.

  28. @GanbaroDigital “ Graph DBs are the BDD of databases.

  29. @GanbaroDigital This isn’t your typical graph database talk.

  30. @GanbaroDigital This isn’t a talk about how to use graph

    algorithms.
  31. @GanbaroDigital Other folks already have that covered.

  32. @GanbaroDigital https://neo4j.com/graphgists/

  33. @GanbaroDigital https://neo4j.com/graphgist/the-panamapapers-example-dataset-president-of-azerbaijan

  34. @GanbaroDigital https://neo4j.com/webinars/

  35. @GanbaroDigital https://www.youtube.com/watch?v=cvTFejfE1-k

  36. @GanbaroDigital This is a talk about building (part of) a

    business on a Graph DB.
  37. @GanbaroDigital ... and how to do this using PHP.

  38. @GanbaroDigital This is my experience. This is how I did

    it, and why.
  39. @GanbaroDigital Other approaches exist. I’m here to learn from you

    too!
  40. @GanbaroDigital Please ask questions as we go.

  41. @GanbaroDigital In This Talk 1. 100 Years Of History 2.

    Querying Graphs With CYPHER 3. BOLTing On PHP
  42. @GanbaroDigital 100 Years Of History

  43. @GanbaroDigital Let’s look at a real example. (Sorry if you

    don’t like football!)
  44. @GanbaroDigital https://chelseafan12.com

  45. @GanbaroDigital https://chelseafan12.com/fanzone/historical-fixtures/

  46. @GanbaroDigital https://chelseafan12.com/fanzone/historical-fixtures/

  47. @GanbaroDigital https://chelseafan12.com/fanzone/player-library/

  48. @GanbaroDigital https://chelseafan12.com/fanzone/player-library/kerry-dixon/

  49. @GanbaroDigital ChelseaFan12 Fanzone • Fixture list back to 1905 •

    Key events from each match • Squad data to match • Rich data on each squad member • + data on Chelsea opponents
  50. @GanbaroDigital Underlying Dataset • 420,000+ records • 3.2 million+ foreign

    keys • average of nearly 8 foreign keys per record
  51. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  52. @GanbaroDigital We’ll use this real dataset to learn how to

    write queries.
  53. @GanbaroDigital Querying Graphs With CYPHER

  54. @GanbaroDigital CYPHER is Neo4J’s query language

  55. @GanbaroDigital CYPHER is awesome*

  56. @GanbaroDigital What does CYPHER look like?

  57. @GanbaroDigital With CYPHER your queries look like a graph diagram.

  58. @GanbaroDigital MATCH (a)-[r]->(b)

  59. @GanbaroDigital How To Read CYPHER • Anything in brackets is

    a record
 (a node in graph terms) • Anything in square brackets is a foreign key
 (a relationship in graph terms) • Relationships can have direction
  60. @GanbaroDigital MATCH (a)-[r]->(b)

  61. @GanbaroDigital How To Read CYPHER • Anything in brackets is

    a record
 (a node in graph terms) • Anything in square brackets is a foreign key
 (a relationship in graph terms) • Relationships can have direction
  62. @GanbaroDigital MATCH (a)-[r]->(b)

  63. @GanbaroDigital How To Read CYPHER • Anything in brackets is

    a record
 (a node in graph terms) • Anything in square brackets is a foreign key
 (a relationship in graph terms) • Relationships can have direction
  64. @GanbaroDigital MATCH (a)-[r]->(b)

  65. @GanbaroDigital How do we find records using CYPHER?

  66. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  67. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  68. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label) { from

  69. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label) { to

  70. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label) { connected by

  71. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label) { to { from { connected by

  72. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  73. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  74. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  75. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)

  76. @GanbaroDigital CYPHER MATCH • ‘a’ and ‘b’ are named results

    • if you want to use it later in your query,
 give it a name • think of labels as record types / table names
  77. @GanbaroDigital “ With CYPHER, you find records by relationships and

    filter them by content
  78. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)
 WHERE a.field = value

  79. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)
 WHERE a.field = value

  80. @GanbaroDigital Tell CYPHER what data to return from the query.

  81. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)
 WHERE a.field = value RETURN b

  82. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)
 WHERE a.field = value RETURN b

  83. @GanbaroDigital MATCH can take multiple
 relationships.

  84. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-[c:label]
 WHERE a.field = value AND c.field =

    value RETURN b
  85. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-[c:label]
 WHERE a.field = value AND c.field =

    value RETURN b
  86. @GanbaroDigital CYPHER supports aggregate queries :)

  87. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-(c:label)
 WHERE a.field = value AND c.field =

    value WITH b, c MATCH (c)-[r3:label]->(d:label)<-[r4:label]-(b) RETURN c, d
  88. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-(c:label)
 WHERE a.field = value AND c.field =

    value WITH b, c MATCH (c)-[r3:label]->(d:label)<-[r4:label]-(b) RETURN c, d
  89. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-(c:label)
 WHERE a.field = value AND c.field =

    value WITH b, c MATCH (c)-[r3:label]->(d:label)<-[r4:label]-(b) RETURN c, d
  90. @GanbaroDigital MATCH (a:label)-[r:label]->(b:label)<-[r2:label]-(c:label)
 WHERE a.field = value AND c.field =

    value WITH b, c MATCH (c)-[r3:label]->(d:label)<-[r4:label]-(b) RETURN c, d
  91. @GanbaroDigital https://neo4j.com/docs/developer-manual/current/cypher/

  92. @GanbaroDigital http://markhneedham.com/blog/tag/cypher/

  93. @GanbaroDigital Querying The Dataset

  94. @GanbaroDigital 3 Actual Queries • A-Z list • Player profile

    cards • Matches played in a season
  95. @GanbaroDigital Query #1 The A-Z List

  96. @GanbaroDigital https://chelseafan12.com/fanzone/player-library/

  97. @GanbaroDigital Which players do we show on the A-Z page?

  98. @GanbaroDigital “ To design a basic CYPHER query, simply say

    what you’re trying to find.
  99. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  100. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  101. @GanbaroDigital “Find players who had a spell at ‘Chelsea’”

  102. @GanbaroDigital MATCH (p:person)

  103. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)

  104. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)-[:at]->(t:team)
 WHERE t.name =~ “(?i)chelsea”

  105. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)-[:at]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN DISTINCT(p)
 ORDER

    BY p.sortname ASC
  106. @GanbaroDigital 3 Things About This Query • Text searching •

    Duplicate rows • Pre-calculated sort order
  107. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)-[:at]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN DISTINCT(p)
 ORDER

    BY p.sortname ASC
  108. @GanbaroDigital String-matching is case-sensitive in Neo4J. I end up using

    regexes for all my string matching.
  109. @GanbaroDigital 3 Things About This Query • Text searching •

    Duplicate rows • Pre-calculated sort order
  110. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)-[:at]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN DISTINCT(p)
 ORDER

    BY p.sortname ASC
  111. @GanbaroDigital Players may have played for the club multiple times.

    Use DISTINCT() to avoid returning duplicate rows.
  112. @GanbaroDigital 3 Things About This Query • Text searching •

    Duplicate rows • Pre-calculated sort order
  113. @GanbaroDigital MATCH (p:person)-[:had_spell]->(:spell)-[:at]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN DISTINCT(p)
 ORDER

    BY p.sortname ASC
  114. @GanbaroDigital Our pre-calculated ‘sortname’ saves any duplication in different client

    apps.
  115. @GanbaroDigital “ CYPHER queries look like how we’d describe what

    we’re trying to find.
  116. @GanbaroDigital The query that’s live today is a little different.

  117. @GanbaroDigital Direct relationships often describe things clearer. Especially when it’s

    complicated.
  118. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  119. @GanbaroDigital Person Spell Match Season Goal Media Biography Competition Team

  120. @GanbaroDigital MATCH (p:person)-[:played_for]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN p
 ORDER

    BY p.sortname ASC
  121. @GanbaroDigital MATCH (p:person)-[:played_for]->(t:team)
 WHERE t.name =~ “(?i)chelsea”
 RETURN p
 ORDER

    BY p.sortname ASC
  122. @GanbaroDigital Just because you’re using a Graph DB, you don’t

    have to traverse longer paths.
  123. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  124. @GanbaroDigital Person Spell Match Season Goal Media Biography Competition Team

  125. @GanbaroDigital “ Create new, shorter paths for anything you use

    regularly.
  126. @GanbaroDigital We can easily add different relationships between the same

    two records.
  127. @GanbaroDigital This is very handy when new requirements arrive!

  128. @GanbaroDigital https://chelseafan12.com/fanzone/player-library/

  129. @GanbaroDigital https://chelseafan12.com/fanzone/player-library/

  130. @GanbaroDigital MATCH (t:team)-[:current_squad]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 RETURN p
 ORDER

    BY p.sortname ASC
  131. @GanbaroDigital MATCH (t:team)-[:current_squad]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 RETURN p
 ORDER

    BY p.sortname ASC
  132. @GanbaroDigital We can do all this in an RDBMS. It’s

    just so much easier to do it in a Graph database.
  133. @GanbaroDigital Graph Relationships • No schema changes / migrations •

    No extra columns for the foreign keys • No NULLs for empty foreign keys • Just get on and do it :)
  134. @GanbaroDigital Shipped and deployed an average of 3 updated schemas*

    per month for 12 months. * equivalent to RDBMS
  135. @GanbaroDigital Zero schema migrations required :-)

  136. @GanbaroDigital Query #2 Profile Cards

  137. @GanbaroDigital How do we build each player’s profile card for

    the A-Z page?
  138. @GanbaroDigital

  139. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  140. @GanbaroDigital Person Spell Match Season Goal Media Biography Competition Team

  141. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  142. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  143. @GanbaroDigital Use WITH to feed the result rows into a

    second query.
  144. @GanbaroDigital The second query can reduce the size of the

    final result set. It can also add new records to the final result set.
  145. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  146. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  147. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  148. @GanbaroDigital Use OPTIONAL MATCH when querying incomplete datasets.

  149. @GanbaroDigital In this case, my client doesn’t have a profile

    picture for every player who has played for a given team.
  150. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  151. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  152. @GanbaroDigital Note how the name of the relationship tells us

    what kind of image the ‘media’ record is.
  153. @GanbaroDigital “In real life, identity often depends on context. Graphs

    make it easy for us to model that.
  154. @GanbaroDigital For example, UK addresses can be many different things

    all at once.
  155. @GanbaroDigital The list of possible addresses would be the records.

    The type of address - the context - would be the relationship name.
  156. @GanbaroDigital CYPHER returns result rows. Use COLLECT to put multiple

    records into a single result row.
  157. @GanbaroDigital MATCH (t:team)-[:played_for]->(p:person)
 WHERE t.name =~ “(?i)chelsea”
 WITH DISTINCT(p), t

    OPTIONAL MATCH (p)-[:profile_pic]->(m:media)-[:team]->(t)
 WHERE m.category = “thumbnail” RETURN p, COLLECT([m]) as profile_pics
 ORDER BY p.sortname ASC
  158. @GanbaroDigital Query #3: Matches In A Season

  159. @GanbaroDigital Which matches were played in a season?

  160. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  161. @GanbaroDigital Person Spell Team Match Season Goal Media Biography Competition

  162. @GanbaroDigital “Find every league match played by Chelsea in the

    2016/2017 season”
  163. @GanbaroDigital “Find every league match played by Chelsea in the

    2016/2017 season”
  164. @GanbaroDigital MATCH (m:match)-[:competition]->(c:competition)
 WHERE c.name = “Premier League”

  165. @GanbaroDigital “Find every league match played by Chelsea in the

    2016/2017 season”
  166. @GanbaroDigital MATCH (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND

    t.name =~ “(?i)Chelsea”
  167. @GanbaroDigital MATCH (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND

    t.name =~ “(?i)Chelsea”
  168. @GanbaroDigital “Find every league match played by Chelsea in the

    2016/2017 season”
  169. @GanbaroDigital ?? ?? How do we find the season?

  170. @GanbaroDigital Football seasons start in one year and finish in

    the next.
  171. @GanbaroDigital “ Design data sets to make records discoverable.

  172. @GanbaroDigital Data dimensions are a classic solution to making records

    discoverable. And they are perfectly suited to graph databases :)
  173. @GanbaroDigital https://www.kimballgroup.com

  174. @GanbaroDigital Data dimensions are standardised data sets that you can

    connect different record types to.
  175. @GanbaroDigital For example, the list of UK addresses could be

    a data dimension.
  176. @GanbaroDigital Use relationships into the data dimension (e.g. “delivery address”,

    “billing address”) to provide identity.
  177. @GanbaroDigital You can use whatever datasets you want as data

    dimensions. The point is to standardise them, and preload them in so that you can link to them.
  178. @GanbaroDigital Different aspects of time are a classic set of

    data dimensions.
  179. @GanbaroDigital We link each ‘season’ to a ‘start year’ and

    an ‘end year’ to make them easy to find.
  180. @GanbaroDigital Person Spell Team Match Season Goal Media Year Competition

  181. @GanbaroDigital MATCH (s:season)-[:started_on_year]->(y:year)
 WHERE y.year = 2016

  182. @GanbaroDigital “Find every league match played by Chelsea in the

    2016/2017 season”
  183. @GanbaroDigital The query could search for things in the same

    order that we’d say it out loud.
  184. @GanbaroDigital Sometimes, it’s better to change the query order to

    find the smaller result sets first.
  185. @GanbaroDigital MATCH (s:season)-[:started_on_year]->(y:year)
 WHERE y.year = 2016
 WITH s
 MATCH

    (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND t.name =~ “(?i)Chelsea”
 AND (m)-[:season]->(s)
  186. @GanbaroDigital MATCH (s:season)-[:started_on_year]->(y:year)
 WHERE y.year = 2016
 WITH s
 MATCH

    (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND t.name =~ “(?i)Chelsea”
 AND (m)-[:season]->(s)
  187. @GanbaroDigital MATCH (s:season)-[:started_on_year]->(y:year)
 WHERE y.year = 2016
 WITH s
 MATCH

    (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND t.name =~ “(?i)Chelsea”
 AND (m)-[:season]->(s)
  188. @GanbaroDigital MATCH (s:season)-[:started_on_year]->(y:year)
 WHERE y.year = 2016
 WITH s
 MATCH

    (t:team)<-[:home_team|away_team]-(m:match) -[:competition]->(c:competition)
 WHERE c.name =~ “(?i)Premier League” AND t.name =~ “(?i)Chelsea”
 AND (m)-[:season]->(s)
  189. @GanbaroDigital In CYPHER, we can use relationships between records in

    WHERE clauses too.
  190. @GanbaroDigital We’ve Covered ... • Finding by relationships • Filtering

    by content & relationships • Aggregate queries • Identity by context • Discoverability by data dimensions
  191. @GanbaroDigital BOLTing On PHP

  192. @GanbaroDigital How do we get at all this graph goodness

    from the world’s best back-office programming language?
  193. @GanbaroDigital composer require graphaware/neo4j-php-client

  194. @GanbaroDigital // connect to Neo4J using the BOLT protocol //

    it’s a little faster than the HTTP API use GraphAware\Neo4j\Client\Client; use GraphAware\Neo4j\Client\ClientBuilder; $client = ClientBuilder::create() ->addConnection(‘default’, ‘bolt://neo4j:7687') ->build();
  195. @GanbaroDigital // build the query using a HEREDOC // use

    { } as placeholders for query parameters
 $query = <<<EOS MATCH (t:team)-[:current_squad]->(p:person)
 WHERE t.name =~ {teamname}
 RETURN p
 ORDER BY p.sortname ASC EOS;
  196. @GanbaroDigital // build the query parameters list $queryParams = [

    ‘teamname’ => ‘(?i)chelsea’, ];
  197. @GanbaroDigital // run the query $result = $client->run($query, $queryParams); //

    the records don’t come back as an assoc array :( $records = $result->getRecords();
  198. @GanbaroDigital https://github.com/graphaware/neo4j-php-client

  199. @GanbaroDigital Summing Up

  200. @GanbaroDigital “ Graph DBs are the BDD of databases.

  201. @GanbaroDigital “ CYPHER is easier to learn than SQL.

  202. @GanbaroDigital “ The majority of queries are much easier to

    write in CYPHER than in SQL.
  203. @GanbaroDigital “ To design a basic CYPHER query, simply say

    what you’re trying to find.
  204. @GanbaroDigital “ Create new, shorter paths for anything you use

    regularly.
  205. @GanbaroDigital “Graph DBs can take advantage of techniques developed for

    RDBMS like data dimensions!
  206. Thank You Any Questions? A presentation by @stuherbert
 for @GanbaroDigital