電脳世界のケーキ屋さん

考えの甘い甘党エンジニアがいろいろ書くブログ

neo4j で出力される warning について(builds a cartesian product)

はじめに

適当にクエリを作成していると以下の警告を受けた

This query builds a cartesian product between disconnected patterns.

未接続のパターンでデカルト積を構築するクエリだけど本当にいいの? という警告である. 意味が分からなかったので調べてみたことを記載しておく.

事象

下記クエリを実行しようとすると冒頭に述べた警告を受けた.
クエリ自体は問題なく実行される.

match (tag1:tag), (tag2:tag)
where id(tag1) = 26995 and tag2.name = "test-tag"
MERGE (tag1) - [:rel] -> (tag2)
return tag1,tag2

原因と処置

stackoverflow に説明があった.

要約すると計算時間が非効率だからやめとこうよという話のようである.
上記クエリの場合は WHERE 句において tag1tag2 に入り得る全てのパターンを精査する処理となる.
結果として計算量が tag1tag2 に入るノード数の直積となる.今回の場合は tag ラベルの付与されたノード数の二乗の計算量が発生する.
O(n^2) はあまりよろしくない計算量であり, ノード数が増えるほどパフォーマンスの低下が顕著になる.

このデカルト積が生じるパターンはアンチパターンとしてSQL界隈では有名なものらしい.

解決策としては MATCH 句を分割するのが手っ取り早い.

MATCH (tag1:tag)
WHERE id(tag1) = 26995
MATCH (tag2:tag)
WHERE ag2.name = "test-tag"
MERGE (tag1) - [:rel] -> (tag2)
return tag1,tag2

おわりに

RDBにおいてもクエリ実行時の意図しないデカルト積の発生は問題のあるものであるらしい.
この記事のおかげで自分がデータベースについて何も知らないことが露見してしまった.
もう少し精進することとしよう.

neo4j の MERGE クエリを知った

はじめに

MERGE クエリは neo4j でノードが存在しない場合は新規にノードを作成し, 存在する場合は何も行わないという処理を行いたい時に使う.

MERGEの基本

単一ノードを作成する場合は簡単で, CREATEMERGE に置き換えるだけでよい.
下記のクエリを二回実行すると, 1回目は指定したノードが作成されるが, 2回目は何も変更はないと通知されるはずである.

MERGE (testNode:test {description: "this is test"})

MERGE はプロパティによるマッチングでは全プロパティをマッチさせる必要はない.
ただし, 指定したプロパティが存在しなかった場合は新規にそのプロパティを持つノードの作成を行う.

下記の2クエリをそれぞれ実行するケースを考える.

  1. MERGE (testNode1:test {first: 1})
  2. MERGE (testNode2:test {first: 1, second: 2})

  3. 1, 2 と実行した場合

    • ノードは2つ作成されることになる
    • 後に実行する2のクエリでは対象ノードが存在しないと判定される
  4. 2, 1 と実行した場合
    • ノードは1つ作成される
    • 後に実行する1のクエリでは対象ノードが既に存在していることになる

ちなみに, CREATE はカンマ区切りで複数ノードを同時に作成できるが, MERGE は構文エラーとなって実行できない.

リレーションも作成する時

この場合は少々複雑になる.
下記のクエリを実行してノードを2つ作成した状態にする.

MERGE (testNode1:test {description: "this is test1"})
MERGE (testNode2:test {description: "this is test2"})

この状態で以下の MERGE クエリを実行するとどうなるだろうか.

MERGE (testNode1:test {description: "this is test1"}) - [:relation] -> (testNode2:test {description: "this is test2"})

結果としては :relation で接続されたノード2つが 新規に 作成される.
これは自分にとっては少し予想外であった.
てっきりリレーションだけ新規に作成されると考えていたからである.
MERGE:relationtestNode2 に接続された testNode1 というのは存在しないと判断されたと考えれば納得は行くが, 直感的ではなかった(自分の直感が単純過ぎるだろうか)

さて, リレーションだけを新規に作成したい場合は少し工夫が必要となる.
下記のように実行すると想定通りの動きをした(スペルミスなどで何度もやりなおすはめになったが…).

MERGE (testNode1:test {description: "this is test1"})
MERGE (testNode2:test {description: "this is test2"})
MERGE (testNode1) - [:relation] -> (testNode2)

おわりに

CREATE UNIQUE 句というのも存在するらしいが MERGE 句を使えと wiki には書いてある.

参考文献

http://neo4j.com/docs/developer-manual/current/cypher/clauses/create-unique/

Python からneo4jで作成したノードのIDを取得する方法

はじめに

Python歴2日くらいの初心者が Python の neo4j-driver を触って四苦八苦した話である.
目的としては CREATE 句によって作成したノードのIDをPython側で知ることであった.

実行環境

  • Python : 3.7.0a2
    • neo4j-driber : 1.5.0
  • neo4j : 3.2.5

neo4j 側

下記のクエリでは作った直後のノードの情報はPython側には返ってきてくれない.

CREATE (:test {content:"this is test"})

どうやったら戻せるかで1時間くらい悩んだ結果, RETURN を用いれば良いことに気付いた.

CREATE (x:test {content:"this is test"}) RETURN x

これでPython側に結果として返ってくる.

Python

Python 側で値を実際に取り出すのに苦労した.そこにあるのに取り出せない非常にもどかしい状況だった.
neo4j-driver ではステートメントの結果を BoltStatementResult という型で返してくる.
そもそもこれがどういう型なのか把握するのに時間を消費した.
この公式ドキュメントneo4j.v1.StatementResult というクラスがそれに該当するということが判明.

とりあえず for record in resultprint 出力すると下記の結果を得た.

(<Node id=26527 labels={'test'} properties={'content': 'this is test'}>,)

なにやら括弧に包まれ過ぎていて, これまた解読するのに時間がかかった.
上記の表現はつまりノードクラスを1つ保持するタプル型の変数出力であることを理解した.
タプル型はインデクサでアクセスできるようなので, x[0] で素の Node クラスを取得できることになる.
あとは Node.id で値を得られる訳である.

結論として, CREATE句によって作成したノードのIDをprintしたい場合は以下のようにすれば実現できる.

statement = "CREATE (x:test {content: {content}}) RETURN x"
with driver.session() as session:
  with session.begin_transaction() as tx:
    for record in tx.run(statement, {"content": "this is test"})
      print(record.values()[0].id)

追記

neo4j側のクエリを下記のようにするともう少しスマートかもしれないしそうでないかもしれない.

CREATE (x:test {content:"this is test"}) RETURN ID(x)

こうすると Python側にはIDの値だけが戻ってくる.
この場合は下記のようにアクセスすれば取得できる.

print(record["ID(x)"])

キーがそのまま過ぎて扱い辛い. スマートになったといえば疑問ではあるがこういう手法もある.

WITH 句を利用するともう少し綺麗にできる.

CREATE (x:test {content:"this is test"})
  WITH ID(x) as id
  RETURN id

おわりに

今回はユーザに「ID~~でデータベースに登録しました」という通知を行いたかったため, このようなことに思い至った次第である.
ただ, neo4j はグラフ型データベースであり, そもそもインデックスをこのように振り回すのはナンセンスなのかもしれない.
neo4j についても Python についてもまだまだ初心者にすら至っていないようなレベルなので, また何か改善案があれば追記する.