背景
グラフデータベースNeo4jに触る機会がありNeo4j Graph Academyを受講した。忘れそうなポイントをこのページにメモとして残す。
Cypher
MATCH/OPTIONAL MATCH
OPTIONAL MATCH
: 該当のクエリが存在する場合にのみ結果を返す (存在しない場合はnullを返す)
MATCH (m:Movie) WHERE m.title = "Kiss Me Deadly" MATCH (m)-[:IN_GENRE]->(g:Genre)<-[:IN_GENRE]-(rec:Movie) OPTIONAL MATCH (m)<-[:ACTED_IN]-(a:Actor)-[:ACTED_IN]->(rec) RETURN rec.title, a.name
特定のプロパティのみを持つノードを返す場合
MATCH (m:Movie) WHERE m.title CONTAINS 'Matrix' RETURN m { .title, .released } AS movie
MERGE/DELETE
新しいノード・エッジを作成/削除する。以下の性質があるので注意。
MERGE
は条件に該当するノード・エッジが既に存在する場合は実行されない。MERGE
はエッジの方向性を指定しないと「左側のノードから右側のノード」という方向性がデフォルトでアサインされる *1DETACH DELETE
:DELETE
ではエッジを持つノードを削除できない様になっている。こういったノードを削除する場合に使う。
// 以下の様に実行すると全てのノード・エッジが消える (=グラフが完全消去される) MATCH (n) DETACH DELETE n
WHERE
以下の演算子が存在
=
: Equal<>
: Not equal>
/<
: Greater / Less thanOR
: OR演算子A STARTS WITH B
: 文字列AがBで始まっているかA ENDS WITH B
: 文字列AがBで終わっているかA CONTAINS B
: 文字列AにBが含まれているか
プロパティが存在している場合は以下のように検証可能
MATCH (p:Person) WHERE p.died IS NOT NULL
SET系
MERGE
/MATCH
で指定したノード・エッジのプロパティを追加する
SET
: 常に実行されるON CREATE SET
:MERGE
によってノード・エッジが作られた時にのみ実行されるON MATCH SET
:MERGE
実行時に既にノード・エッジが存在する場合にのみ実行される
// Find or create a person with this name MERGE (p:Person {name: 'McKenna Grace'}) // Only set the `createdAt` property if the node is created during this query ON CREATE SET p.createdAt = datetime() // Only set the `updatedAt` property if the node was created previously ON MATCH SET p.updatedAt = datetime() // Set the `born` property regardless SET p.born = 2006 RETURN p
SET
ではプロパティを追加するだけでなくノードにラベル (Node Type) を追加する場合にも使える
MATCH (p:Person) WHERE exists ((p)-[:DIRECTED]-()) SET p:Director
プロパティを消す場合にも使える
MATCH (m:Movie) SET m.genres = null
WITH
WITHを使うことでMATCHの前に特定の値で変数を定義することができる
WITH 'Tom Hanks' AS actorName MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE p.name = actorName RETURN m.title AS movies
WITHを挟むとRETURN文に記載できる変数名がリセットされる。以下のケースのようにRETURNで返したい変数 (e.g., p
, m
) がある場合はWITHに入れる必要がある。
WITH 'Clint Eastwood' AS a, 'high' AS t MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WITH p, m, toLower(m.title) AS movieTitle WHERE p.name = a AND movieTitle CONTAINS t RETURN p.name AS actor, m.title AS movie
WITHを使った事例: 平均スコアの計算
MATCH (:Movie {title: 'Toy Story'})-[:IN_GENRE]->(g:Genre)<-[:IN_GENRE]-(m) WHERE m.imdbRating IS NOT NULL WITH g.name AS genre, count(m) AS moviesInCommon, sum(m.imdbRating) AS total RETURN genre, moviesInCommon, total/moviesInCommon AS score ORDER By score DESC
UNWIND
プロパティのリストに含まれる項目ごとに操作を実行することができる。モデルをリファクタリングする際に使用する事例が紹介されていた。
MATCH (m:Movie) UNWIND m.genres AS genre MERGE (g:Genre {name: genre}) MERGE (m)-[:IN_GENRE]->(g) SET m.genres = null
CREATE CONSTRAINT
特定のプロパティ (e.g., ID) に重複を許さないなどの制約を設けることができる。検索の速度改善にもつながるらしい。
CREATE CONSTRAINT Genre_name IF NOT EXISTS FOR (x:Genre) REQUIRE x.name IS UNIQUE
ORDER BY
結果を何らかのプロパティでソートする場合に使う
DESC
: 降順
MATCH (p:Person) WHERE p.born IS NOT NULL RETURN p.name AS name, p.born AS birthDate ORDER BY p.born DESC
2つ以上の条件を使う場合
MATCH (m:Movie)<-[ACTED_IN]-(p:Person) WHERE m.imdbRating IS NOT NULL RETURN m.title, m.imdbRating, p.name, p.born ORDER BY m.imdbRating DESC, p.born DESC
CASE-WHEN
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name = 'Henry Fonda' RETURN m.title AS movie, CASE WHEN m.year < 1940 THEN 'oldies' WHEN 1940 <= m.year < 1950 THEN 'forties' WHEN 1950 <= m.year < 1960 THEN 'fifties' WHEN 1960 <= m.year < 1970 THEN 'sixties' WHEN 1970 <= m.year < 1980 THEN 'seventies' WHEN 1980 <= m.year < 1990 THEN 'eighties' WHEN 1990 <= m.year < 2000 THEN 'nineties' ELSE 'two-thousands' END AS timeFrame
UNWIND
リストを分解、行へと分ける
MATCH (m:Movie)-[:ACTED_IN]-(a:Actor) WHERE a.name = 'Tom Hanks' UNWIND m.languages AS lang RETURN m.title AS movie, m.languages AS languages, lang AS language
CALL
サブクエリをつくる。例えば下記のように実行すればMovieのRatingについての検索範囲を絞る (=効率を上げる) ことが可能。
CALL { MATCH (m:Movie) WHERE m.year = 2000 RETURN m ORDER BY m.imdbRating DESC LIMIT 10 } MATCH (:User)-[r:RATED]->(m) RETURN m.title, avg(r.rating)
以下のように途中にサブクエリを挟むことも可能
MATCH (m:Movie) CALL { WITH m MATCH (m)<-[r:RATED]-(u:User) WHERE r.rating = 5 RETURN count(u) AS numReviews } RETURN m.title, numReviews ORDER BY numReviews DESC
UNION
2つのクエリの出力を連結する。2つのクエリは同じ項目をリターンする必要がある。
UNION ALL
:ALL
をつけないと2つのクエリで重複する行は除外される (DISTINCTと同等)が、つけると重複も結果として出力される
MATCH (m:Movie) WHERE m.year = 2000 RETURN {type:"movies", theMovies: collect(m.title)} AS data UNION ALL MATCH (a:Actor) WHERE a.born.year > 2000 RETURN { type:"actors", theActors: collect(DISTINCT a.name)} AS data
Methods
exists
: 引数のクエリに該当するノード・エッジが存在するかTRUE/FALSEを返すnot exists
: 引数のクエリに該当するノード・エッジが存在しない場合にTRUEを返すkeys
: ラベル・リレーションに存在するプロパティの一覧を返すcoalesce
: 第一引数がnullでなければ第一引数を、nullであれば第二引数を返すsplit
: string型を特定の文字でsplitしてlist型へ変換するtoInteger
: int型へ変換type
: ラベル・リレーションのタイプを返すsize
: リストのサイズを返すdate
/datetime
/time
: 現時刻をdate/datetime/time型で返すshortestPath
: 最短経路を返すtrim
: stringの先頭・末尾にある空白を取り除く
以下の関数はAggregation (group by)する
count
: 行数をカウントするcollect
: 該当するノード・エッジが複数ある場合にリストとして返すmin
/max
/stddev
/avg
/sum
: 数値処理
MATCH (p:Person)-[r]->(m:Movie) WHERE p.name = 'Tom Hanks' RETURN m.title AS movie, type(r) AS relationshipType
exists
MATCH (p:Person) WHERE exists ((p)-[:ACTED_IN]-()) SET p:Actor
split
/coalesce
String型のプロパティ (e.g., "A|B") をリスト型 (e.g., ["A", "B"]) へ変換する事例が紹介されていた。
MATCH (m:Movie) SET m.countries = split(coalesce(m.countries,""), "|"), m.languages = split(coalesce(m.languages,""), "|"), m.genres = split(coalesce(m.genres,""), "|")
collect
MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE p.name ='Tom Cruise' RETURN collect(m.title) AS tomCruiseMovies, size(collect(m)) AS tomCruiseMovieCount
shortestPath
MATCH p = shortestPath((p1:Person)-[*]-(p2:Person)) WHERE p1.name = "Eminem" AND p2.name = "Charlton Heston" RETURN p
MATCH p = shortestPath((p1:Person)-[:ACTED_IN*]-(p2:Person)) WHERE p1.name = "Eminem" AND p2.name = "Charlton Heston" RETURN p
MATCH (p:Person {name: 'Eminem'})-[:ACTED_IN*1..4]-(others:Person) RETURN others.name
APOC系
APOC (Awesome Procedures on Cypher) ライブラリの関数は厳密にはCypherの関数ではないので CALL
で呼び出す必要がある
apoc.meta.nodeTypeProperties
: 各ノードが有するプロパティの情報 (e.g., データ型) を返すapoc.meta.relTypeProperties
: 各エッジが有するプロパティの情報 (e.g., データ型) を返す
CALL apoc.meta.nodeTypeProperties()
Parameters
:param
でパラメータを設定、Cypherクエリの中で使うことが可能
// パラメータの設定 :param number: 10 :param actorName: 'Tom Hanks' // 設定されたパラメータを確認する :params: // :params {} // 全パラメータを削除 MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE p.name = $actorName RETURN p, m
JavascriptとかPythonとかで以下のように使える
def get_actors(tx, movieTitle): # (1) result = tx.run(""" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE m.title = $title RETURN p """, title=movieTitle) # Access the `p` value from each record return [ record["p"] for record in result ] with driver.session() as session: result = session.read_transaction(get_actors, movieTitle="Toy Story")
Graph modeling
Naming conventions
ラベル・リレーション・プロパティには以下の命名規則を使うことがベストプラクティスとされている。
- ラベル (Node type): CamelCase (e.g., Company, HighSchool)
- リレーション (Edge type): 大文字 & アンダースコア (e.g., FOLLOWS, MARRIED_TO)
- プロパティ: camelCase (e.g., deptId, firstName)
Importing CSV
LOAD CSV
LOAD CSV
WITH HEADERS
: ヘッダー行が存在する場合に指定するFIELDTERMINATOR
: csvでない場合に使う。区切り文字を指定する。
CALL { LOAD CSV WITH HEADERS FROM 'https://data.neo4j.com/importing/2-movieData.csv' AS row WITH row WHERE row.Entity = "Join" AND row.Work = "Acting" MATCH (p:Person {tmdbId: toInteger(row.tmdbId)}) MATCH (m:Movie {movieId: toInteger(row.movieId)}) MERGE (p)-[r:ACTED_IN]->(m) ON CREATE SET r.role = row.role SET p:Actor }
Neo4j Data Importer
GUIで動かせるツール。ローカル環境にあるCSV形式のデータ (1行目にヘッダが必須) をNeo4jへインポートできる。よくできているがエンジニアが使うかは微妙と感じる。リスト型のプロパティはインポートできないなどプログラムでのデータインポートと比較して制限がある。
*1:Undirected edgeの作り方がよくわからない。同じクエスチョンがGithubでissueとして投稿されている https://github.com/neo4j/neo4j/issues/13009