グラフDB - Neo4j のレファレンスをまとめる

# この記事は
グラフDBをいじってみたくなったので、Neo4jを触ろうとした際の備忘録である。
レファレンスを読んでみた感じ面白そう。特に独自のQuery LanguageであるCypherはとても興味深い。
公式に載ってることしか書いてないので、そっちを読みましょう。
https://neo4j.com/docs/developer-manual/current/

# 必要となる概念
グラフDBはグラフ理論に基づいたデータ構造を扱うためのデータベースである。
そのため取り扱われる概念も基本的にグラフ理論によって導入されたものがほとんどである。
いくつかの概念はRDBにて用いられているものに呼び変えられているが、RDBを知らなくても（そんな人がグラフDBを扱おうとは思わないだろうが）十分理解ができる。

- Node
    - Entityのこと。LabelとPropertyを持つ
- Relationship
    - Node間(自己参照も含む)を結ぶ線のこと
- Label
    - Entityの名前
- Property
    - Entityの属性
    - 取りうる値
        - Numeric(integer/float)
        - Boolean
        - String
        - List(上記三種の)
    - Null(Property値ではなく、Propertyに値が入っていない状態を指す)
- Paths
    - Nodeと(optionalで)Relationの単位
    - Relationの数がPathのLengthとなる。
        - 自己参照型のNodeであればLength=1
        - Node単体であればLength=0

# CypherのSummary
Neo4j独自のQuery Language。影響を受けている言語は下記とのこと。

- where(制約)とorderby(ソート)はSQLから
- regex(パターンマッチ)はSPARQLから
- list周りのsemanticsはHaskel/Pythonから

基本的なSyntaxは下記。

- Node
    - (hoge)
- Relationship
    - (a)--(b)
        - 無向性
    - (a)->(b)
        - 有向性
    - (a)-[rel]-(b)
        - 特定のrelationshipを指定する場合
    - (a)-[*..5]-(b)
        - a/b間でlengthが5以下のpathsを返す

- Match
    - DB内の起点となるNode群を指定する
    - Where句やReturn句で使いたいNode群にはこのタイミングで参照を取る必要がある
- With
    - Match句で取得した情報をどのように取得したいかを指定する
- Where
    - Match句で取得したNode群に絞込を掛ける
- Return
    - 実際にDBから取得したいProperty
- Create[Delete]
    - Node/Relationを作成削除する
- Set[Remove]
    - Label/Propertyを作成更新削除する
- Merge
    - Node/PatternをRDBで言うところのUpsertする
        - PatternはMatch句で取得したNode群を指す

```
// Johnの友達の友達(friend of friend)の一覧を取得する// MATCH句の()内にLabelとその制約を与えるイメージ
MATCH (john {name: 'John'})-[:friend]->()-[:friend]->(fof)
RETURN john.name, fof.name
```

```
// 指定のnameを持つNodeがfollowしている、nameが'S'から始まるNodeの一覧を取得する
MATCH (user)-[:friend]->(follower)
WHERE user.name IN['Joe', 'John', 'Sara', 'Maria', 'Steve'] AND follower.name =~ 'S.*'
RETURN user.name, follower.name
```

```
// 友達が4人以上いるJohnを返す
MATCH (n {name: 'John'})-[:frined]-(friend)
WITH n, count(friend) AS friendsCount
WHERE friendsCount > 3
RETURN n, friendsCount
```

```
// 友達の数をJohnのfriendsCount propertyにSETし、それを返す
MATCH (n {name: 'John'})-[:friend]-(friend)
WITH n, count(friend) AS friendsCount
SET n.friendsCount = friendsCount
RETURN n.friendsCount
```

```
// User Entityを3つ作成、それらをfriend relationshipで結ぶ
CREATE (adam:User {name: 'Adam'}), (pernilla:User {name: 'Pernilla'}), (david:User {name: 'David')},
(adam)-[:friend]->(perniilla), (pernilla)-[:friend]->(david)
```

```
// User:Adamの友達の友達(David)を返す
MATCH (user:User {name: 'Adam'})-[r1:friend]-()-[r2:friend]-(fof)
RETURN fof.name AS fofName
```

```
// Case 
MATCH (n)
RETURN
CASE n.eyes
WHEN 'blue' THEN 1
WHEN 'brown' THEN 2
ELSE 3 END AS result
```

# Parameterを利用したQuery

```
// parameter
{
  "name" : "Johan"
}

// query
MATCH (n:Person)
WHERE n.name = $name
RETURN n
```

```
// parameter
{
  "regex" : ".*h.*"
}

// query
MATCH (n:Person)
WHERE n.name =~ $regex
RETURN n.name
```

```
// parameter
{
  "props" : {
    "name" : "Andres",
    "position" : "Developer"
  }
}

// query
CREATE ($props)
```

# List型

```
RETURN [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
//=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

RETURN range(0, 10)[3]
//=> 3

RETURN range(0, 10)[0..3]
//=> [0, 1, 2]
```

# Map型

```
RETURN { key: 'Value', listKey: [{ inner: 'Map1' }, { inner: 'Map2' }]}
//=> {listKey -> [{inner -> "Map1"},{inner -> "Map2"}], key -> "Value"}

MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)
WITH actor, count(movie) AS nrOfMovies
RETURN actor { .name, nrOfMovies }
//=> {name -> "Martin Sheen", nrOfMovies -> 2}
```

# Match Statement
Aggregate元の指定。SQLではFromに該当する。

```
// Oliver Stoneと隣接する全てのmovie
MATCH (director { name: 'Oliver Stone' })--(movie)
RETURN movie.title
```

```
// Person labelのOliver Stoneと隣接する全てのMovie labelを持つnodeを返す
MATCH (:Person { name: 'Oliver Stone' })--(movie:Movie)
RETURN movie.title
```

```
// Movie LabelのWall Streetにacted_inなnodeを返す
MATCH (wallstreet:Movie { title: 'Wall Street' })<-[:acted_in]-(actor)
RETURN actor.name
```

```
// wallstreetにacted_inまたはdirectedなnodeを返す
MATCH (wallstreet { title: 'Wall Street' })<-[:acted_in|:directed]-(person)
RETURN person.name
```

```
// 特定のProperty値を持つ経路を返す
MATCH p =(charlie:Person)-[* { blocked:false }]-(martin:Person)
WHERE charlie.name = 'Charlie Sheen' AND martin.name = 'Martin Sheen'
RETURN p
//=> [Node[0]{name:"Charlie Sheen"},:X[20]{blocked:false},Node[20]{},:X[21]{blocked:false},Node[1]{name:"Martin Sheen"}]
```

```
// MartinとOliverとの最短経路を返す
MATCH (martin:Person { name: 'Martin Sheen' }),(oliver:Person { name: 'Oliver Stone' }), p = shortestPath((martin)-[*..15]-(oliver))
RETURN p
//=> [Node[1]{name:"Martin Sheen"},:ACTED_IN[1]{role:"Carl Fox"},Node[5]{title:"Wall Street"},:DIRECTED[3]{},Node[3]{name:"Oliver Stone"}]
```

# Optional Match Statement
Matchと異なり、一致するパターンが見つからなかった時にnullを返す。
SQLで言うところの、Outer JoinのようなStatement。

```
MATCH (a:Movie { title: 'Wall Street' })
OPTIONAL MATCH (a)-->(x)
RETURN x
```

# Return Statement
実際に返す値を指定する。SQLで言うところのSelect。

```
MATCH (n { name: 'A' })-[r:KNOWS]->(c)
RETURN r
```

# With Statement
MatchでAggregateしたものに集約関数等を用いる際には、With Statementを用いる。

```
MATCH (david { name: 'David' })--(otherPerson)-->()
WITH otherPerson, count(*) AS foaf
WHERE foaf > 1
RETURN otherPerson.name
```

```
MATCH (n)
WITH n
ORDER BY n.name DESC LIMIT 3
RETURN collect(n.name)
```

# Unwind Statement
Listをrowとして変形する。

```
UNWIND [1, 2, 3] AS x
RETURN x
```

```
// Parameter
{
  "events" : [ {
    "year" : 2014,
    "id" : 1
  }, {
    "year" : 2014,
    "id" : 2
  } ]
}

// Query
UNWIND $events AS event
MERGE (y:Year { year: event.year })
MERGE (y)<-[:IN]-(e:Event { id: event.id })
RETURN e.id AS x
ORDER BY x
```

# Where Statement
SQLでのWhereと同様の役割を果たす。
AND/OR/XOR/NOT演算子が使える。

```
MATCH (n)
WHERE n.name = 'Peter' XOR (n.age < 30 AND n.name = 'Tobias') OR NOT (n.name = 'Tobias' OR n.name = 'Peter')
RETURN n.name, n.age
```

Label型によるfileter

```
MATCH (n)
WHERE n:Swedish
RETURN n.name, n.age
```

Propertyの存在有無でfilter

```
MATCH (n)
WHERE exists(n.belt)
RETURN n.name, n.belt
```

string matchによるfilter

```
MATCH (n)
WHERE n.name STARTS WITH 'Pet'
RETURN n.name, n.age
```

正規表現matchによるfilter

```
MATCH (n)
WHERE n.name =~ 'Tob.*'
RETURN n.name, n.age
```

path patternによるfilter

```
MATCH (tobias { name: 'Tobias' }),(others)
WHERE others.name IN ['Andres', 'Peter'] AND (tobias)<--(others)
RETURN others.name, others.age
```

# Order By Statement

```
MATCH (n)
RETURN n.name, n.age
ORDER BY n.age, n.name
```

# Create Statement

Nodeの作成

```
// Label = Person, Property = name/title
CREATE (n:Person { name: 'Andres', title: 'Developer' })
```

Relationshipの作成

```
// :RELTYPEというRelationshipをa/b間に作成する
MATCH (a:Person),(b:Person)
WHERE a.name = 'Node A' AND b.name = 'Node B'
CREATE (a)-[r:RELTYPE]->(b)
RETURN r
```

Pathの作成

```
CREATE p =(andres { name:'Andres' })-[:WORKS_AT]->(neo)<-[:WORKS_AT]-(michael { name: 'Michael' })
RETURN p
```

Parameterによる作成

```
// Parameter
{
  "props" : [ {
    "name" : "Andres",
    "position" : "Developer"
  }, {
    "name" : "Michael",
    "position" : "Developer"
  } ]
}

// Query
UNWIND $props AS map
CREATE (n)
SET n = map
```

# Delete Statement

Nodeの削除

```
MATCH (n { name: 'Andres' })
DETACH DELETE n
```

Relationshipの削除

```
MATCH (n { name: 'Andres' })-[r:KNOWS]->()
DELETE r
```

# Set Statement

Set Property

```
MATCH (n { name: 'Andres' })
SET n.surname = 'Taylor'
RETURN n
```

Remove Property

```
MATCH (n { name: 'Andres' })
SET n.name = NULL RETURN n
```

Parameterを用いてPropertyを追加

```
MATCH (peter { name: 'Peter' })
SET peter += { hungry: TRUE , position: 'Entrepreneur' }
```

Parameterを用いてPropertyを更新

```
// Parameters
{
  "props" : {
    "name" : "Andres",
    "position" : "Developer"
  }
}

// Query
MATCH (n { name: 'Andres' })
SET n = $props
RETURN n
```

LabelをSet

```
MATCH (n { name: 'Stefan' })
SET n:German
RETURN n
```

# Remove Statement

Nodeを削除

```
MATCH (andres { name: 'Andres' })
REMOVE andres.age
RETURN andres
```

Labelを削除

```
MATCH (n { name: 'Peter' })
REMOVE n:German
RETURN n
```

# Foreach Statement
List型を更新する際などに用いる。

```
MATCH p =(begin)-[*]->(END)
WHERE begin.name = 'A' AND END.name = 'D'
FOREACH (n IN nodes(p)| SET n.marked = TRUE )
```

# Merge Statement
与えた引数をUpsertするイメージ

LabelのMerge

```
MERGE (robert:Critic)
RETURN robert, labels(robert)
```

NodeのMerge

```
MERGE (charlie { name: 'Charlie Sheen', age: 10 })
RETURN charlie
```

ON CREATEによるフック

```
MERGE (keanu:Person { name: 'Keanu Reeves' })
ON CREATE SET keanu.created = timestamp()
RETURN keanu.name, keanu.created
```

ON MATCHによるフック

```
MERGE (person:Person)
ON MATCH SET person.found = TRUE
RETURN person.name, person.found
```

# Union Statement

Union Allは重複を許す。

```
MATCH (n:Actor)
RETURN n.name AS name
UNION ALL
MATCH (n:Movie)
RETURN n.title AS name
```

Unionは重複を許さない。

```
MATCH (n:Actor)
RETURN n.name AS name
UNION
MATCH (n:Movie)
RETURN n.title AS name
```

# CSV

```
"1","ABBA","1992"
"2","Roxette","1986"
"3","Europe","1979"
"4","The Cardigans","1992"

LOAD CSV FROM 'https://neo4j.com/docs/developer-manual/3.3/csv/artists.csv' AS line
CREATE (:Artist { name: line[1], year: toInteger(line[2])})
```

```
"Id","Name","Year"
"1","ABBA","1992"
"2","Roxette","1986"
"3","Europe","1979"
"4","The Cardigans","1992"

LOAD CSV WITH HEADERS FROM 'https://neo4j.com/docs/developer-manual/3.3/csv/artists-with-headers.csv' AS line
CREATE (:Artist { name: line.Name, year: toInteger(line.Year)})
```

# Index

single-property index

```
CREATE INDEX ON :Person(firstname)
```

composite-property index

```
CREATE INDEX ON :Person(firstname, surname)
```

# Constraints

```
CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE
DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE
```

# Driver
JSのクライアントが公式で用意されている。

```
npm install neo4j-driver
```

実行計画やクエリチューニングに関しても記載があったが、今回はスキップということで。
とりあえずサンプルアプリを作ってみて感想を別途書きたい所存。