Aurora blog

バイオインフォ・バイオテクノロジーにまつわる情報

Knowledge graph①: Neo4j Graph Academy備忘録

背景

グラフデータベース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はエッジの方向性を指定しないと「左側のノードから右側のノード」という方向性がデフォルトでアサインされる *1
  • DETACH DELETE: DELETEではエッジを持つノードを削除できない様になっている。こういったノードを削除する場合に使う。
// 以下の様に実行すると全てのノード・エッジが消える (=グラフが完全消去される)
MATCH (n)
DETACH DELETE n

WHERE

以下の演算子が存在

  • =: Equal
  • <>: Not equal
  • >/<: Greater / Less than
  • OR: 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