📖

【Neo4J/Cypher】SNSユーザーの関係分析に使えそうなクエリ集

2023/06/07に公開

巷ではあまり盛り上がってなさそうなグラフデータベース(Neo4j)であるが、習得すれば複雑な関係が前提となるものでも個人開発で作れるのでないか。

例えば、SNSでよく見かけるおすすめユーザーのロジックに使えたり、機械学習ではそもそもの必要なデータ量が多すぎてハードルが高いといった場合の代替案に使えるのではないかと思って調査していた時期がある。

その際、実際に手元で動かして試行錯誤した内容をまとめる。

検証用の元データの準備

※)必要ない方はスキップしてください。

段取りとしてはまず、MySQLなどのRDBMS上にTwitterのようなSNSを構成するデータベースを定義する。
その後、初期データなどもさくっと投入しておき、そこから同じ関係性を構築したデータをNeo4jに展開するというやり方を取った。

データの数もユーザー数15〜20、投稿(ポスト)が10程度と、クエリ結果が想定通りであるか目視しやすい数に抑えた。

  • postsテーブルはTwitterでいう「つぶやき」のような、ユーザーによる投稿の情報を持つデーブル
  • userがいいねしたpostは多対多の関係としてfavoriteテーブルで保持する
  • userがフォローしたuserは多対多の関係としてfollowテーブルで保持する

▼usersテーブル

column_name type
id unSignedBigInt
last_name varchar(45)
first_name varchar(45)

▼postsテーブル

column_name type
user_id unSignedBigInt
content text

▼favoriteテーブル(post:user=多対多の中間テーブル)

column_name type
id unSignedBigInt
followed_id unSignedBigInt
following_id unSignedBigInt

※分かりづらいがfollowed_idが被フォロー側のIDでfollowing_idがフォローする側のID

▼followテーブル(user:user=多対多の中間テーブル)

column_name type
id unSignedBigInt
user_id unSignedBigInt
post_id unSignedBigInt

ユーザーAのフォロワーを抽出

▼クエリ例

user_id=1のユーザーをフォローしているユーザーを探索。

MATCH (u1:User)-[:following]->(u2:User {user_id:1}) 
RETURN u1

▼解説

Cypherの基本的なMATCH構文なので特に説明することなし。

ユーザーAとユーザーBの両方のフォロワーを抽出

▼クエリ例

MATCH句をカンマで繋げる方法

MATCH (a:User)-[:following]->(b:User),(a:User)-[:following]->(c:User) 
WHERE b.user_id = 1 AND c.user_id = 2
RETURN a

▼解説

2つの経路を満たすノードを抽出する際に、下記のような指定をすることはできない。

MATCH (n:User) 
WHERE n.user_id = 1 AND n.user_id = 2 
RETURN n

各ノードは単一で照合されるので、このパターンでは「user_id=1でかつ、user_id=2のプロパティを持つユーザー」を探索していることになる。単一ノード内の同一プロパティが異なる値を持っているということは有り得ないので、パターンマッチの結果は0件となる。

MATCH (a:User)-[:following]->(b:User),(a:User)-[:following]->(c:User) 
WHERE b.user_id = 1 AND c.user_id = 2
RETURN a

上記においては(a:User)-[:following]->(b:User)でbをフォローしているaにマッチした後、(a:User)-[:following]->(c:User)で「bをフォローしているaがcをフォローしていること」が条件文となる。

もしくは、下記のような方法もある。

MATCH (a:User)-[r:following]->(b:User) 
WHERE (b.user_id IN [1,2])
WITH a, count(*) AS num
WHERE num = 2
RETURN a

この方法ではまず、WHERE...IN~構文を使ってuser_idが1か2をフォローしているユーザーのパターンマッチを行う。その後、WITH句でノードaとマッチした経路全てを数えたものをnumとして引き継ぎ、numが2のもの、つまり両方フォローしているユーザーに絞り込んでいる。

count(*)のアスタリスクはワイルドカードを示すが、上記のケースにおいてはcount([a,r,b])と同義。
つまり、aからbの方向にrの関係が成立しているパターンの数を数えている。

上記に2つについてはこちらのstackoverflowの回答を参考にした。

AとBの両方の投稿にいいねしたユーザーを抽出

▼クエリ例

MATCH (a:User)-[r:favorite]->(b:Post)-[:postedBy]->(c:User)
WHERE (c.user_id IN [1,3])
WITH a, count(*) AS num
WHERE num = 2
RETURN a,num

▼解説

先ほど両方フォローしているユーザーに絞り込んだように、Aに発信された投稿とBに発信された投稿の両方に絞り込めば良い。

ユーザーAのフォロワーとその投稿に対するいいね数を抽出

▼クエリ例

下記はuser_id=3のユーザーのフォロワーと、その投稿にいいねしてした数を抽出している。
Postラベルのノードが投稿主のID(user_id)プロパティを持っていることが前提となっている。

MATCH (u:User)-[r:favorite]->(p:Post) 
WHERE p.user_id = 3 
CALL {
    WITH u MATCH (u)-[r:favorite]->(p:Post) 
    WHERE p.user_id = 3 
    RETURN count(distinct r) as post_count
    } 
SET u.post_count = post_count 
WITH * 
WHERE (u)-[:following]->(:User {user_id:3}) 
RETURN distinct(u), post_count

▼解説

まず、CALLの手前までのMATCH~WHERE構文で、「user_id=3の投稿にいいねしているユーザー」のパターンマッチを行っている。

次に出てくるCALL {}はサブクエリを実行するための構文であり、WITH句を使って{}で示されたスコープの外にある変数をインポートすることができる。

受け取ったデータについては行単位で処理を行い、{}内のRETURNで返した値についてはサブクエリの外側でも使えるようになる。

SETはノード内のプロパティ値を変更できる構文で、上記においては最終的に返却する各ユーザーノードのプロパティに、いいねした投稿の数をセットしている。

最後にuser_id=3をフォローしているユーザーに絞り込み、クエリの結果として該当ユーザーといいねした数を取得している。
(distinctはいらないかも)

A&Bのフォロワーでかつ両者の投稿へのいいね合計数を抽出

▼クエリ例

1つ前の「ユーザーAのフォロワーとその投稿に対するいいね数を抽出」でユーザーAが増えたパターンである。

MATCH (u1:User)-[r1:following]->(u2:User) 
WHERE u2.user_id in [1,3]
CALL {
    WITH u1
    MATCH (u1)-[r2:following]->(u2:User)
    WHERE u2.user_id IN [1,3]
    return count(r2) as num
}
WITH *
WHERE num = 2
WITH *
MATCH (u1)-[r3:favorite]->(p:Post) 
WHERE p.user_id IN [1,3] 
CALL {
    WITH u1 MATCH (u1)-[r4:favorite]->(p:Post) 
    WHERE p.user_id IN  [1,3] 
    RETURN count(distinct r4) as post_count
    } 
RETURN distinct(u1), post_count

▼解説

やってることはこれまで紹介した例の組み合わせ。
先ほどのクエリとは逆にまずはuser_idが1か3をフォローしているユーザーとフォロー数を出し、フォロー数2でフィルタリング。

その後はユーザーAの投稿へのいいねと、ユーザーBの投稿へのいいねの合計を集計すれば良いので、WHERE...IN~構文を使ったフィルタリングを行っている。

以上。

Discussion