何がしたかったのか
例えばこんなグラフDBのデータがあったとします
CREATE
(q1:Question{questionId: randomUUID()}),
(q1)<-[:BELONGS_TO]-(a1:Answer{choice:1}),
(q1)<-[:BELONGS_TO]-(a2:Answer{choice:2}),
(q1)<-[:BELONGS_TO]-(a3:Answer{choice:3}),
(q1)<-[:BELONGS_TO]-(a4:Answer{choice:4}),
(q2:Question{questionId: randomUUID()}),
(q2)<-[:BELONGS_TO]-(a5:Answer{choice:1}),
(q2)<-[:BELONGS_TO]-(a6:Answer{choice:2}),
(q2)<-[:BELONGS_TO]-(a7:Answer{choice:3}),
(q2)<-[:BELONGS_TO]-(a8:Answer{choice:4})
4択問題があって、その質問(Question)と回答(Answer)が紐づいています。
とあるユーザが4択問題に回答した時は、こんなクエリを実行します
MATCH
(u:User{userId:'hoge'})
CREATE
(u)-[:CHOSE]->(a1),
(u)-[:CHOSE]->(a6)
これでユーザーhogeさんが、質問1に対して「1」、質問2に対して「2」と答えた状態が保存されます。
ユーザーhogeさんと全問同じ回答のユーザを取得したい
これが今回の目的です。
何も考えずに書いてみる
MATCH
(u:User{userId:'hoge'})-[:CHOSE]->(a:Answer),
(a)<-[:CHOSE]-(users:User)
RETURN
users
これだと「hogeさんと一つでも同じ回答をしたユーザ」を取得してしまいますから、「全問同じ回答のユーザ」ではありませんね。
WHERE ALL を使う
MATCH (u:User{userId:"hoge"})-[:CHOSE]->(a:Answer)
WITH collect(a) as answers
WHERE ALL(answer in answers WHERE (u2:User)-[:CHOSE]-(answer))
return u2
ここでWITHとALL()を使ってみます。
ALL()は、LISTの全要素が与えられた条件を満たす場合にtrueを返します。
それを応用して、ここではhogeさんが選んだAnswerのLISTの全要素に対して[:CHOSE]を貼っているユーザを抽出する
というクエリを書いています。
ALL()を使うためにはLIST化する必要があるので、collect(ノード) as answers(LIST)で前処理を施しています。
これで表題の問題は解決です。
もうちょっとクエリの効率を改善する
この状態だとクエリの効率が悪いので、少しだけ改善を試みます。
MATCH (u:User{userId:"1234"})-[:CHOSE]->(a:Answer)
WITH collect(a) as answers
WITH head(answers) as head, tail(answers) as answers
MATCH (u2:User)-[:CHOSE]-(head)
WHERE ALL(answer in answers WHERE (u2)-[:CHOSE]-(answer))
return u2
追加したのは以下2行です
WITH head(answers) as head, tail(answers) as answers
MATCH (u2:User)-[:CHOSE]-(head)
head()は与えられたLISTの最初の要素のみ抽出し、tail()は与えられたLISTの最初の要素以外
を抽出します。(なんかセットに使われる事を前提に作られたリスト関数に見えるけど、tailを独立して使う事あるのかな・・・)
要はなんでもいいからhogeさんの回答と同じ回答を1つ持つユーザに、最初に絞っておく
ことで、一問でも異なる回答をしたユーザを最初に弾いて、後の計算量を減らしています。2択問題だとユーザが絞りきれないのでイマイチかもしれませんが、1〜10のスケールとか、一致する確率が低い問題になるほど効率化が図れるのではないでしょうか。
まとめ
- all()を使う
- head/tailを使うと、LISTマッチ系のクエリは少し効率化できる場合がある
以上、apoc使わずintersectionを実装するとこんな感じだよ、という紹介でした