はじめまして会員システムグループのkiqkiqです。
みなさんはリレーショナルデータベース以外のデータベースについてはご存知でしょうか?
データベースの中にはRDB以外にも時系列型やキーバリュー型、カラム指向型などいくつかの種類のデータベースがあります。このブログではこれらのデータベースの中でもグラフデータ型のデータベースについて、SNSなどのソーシャルグラフを題材に、グラフDBに関連する技術やSNSなどで見る基本的な機能の実装を紹介しようと思います。
グラフデータベースについて
グラフデータベースはノードとエッジ、その2つに関する属性の3つの要素でノード間の関係性を扱うことに特化したデータベースで、以下の図のようなグラフ構造を扱うデータベースです。
このグラフデータベースは地図などに用いられる道路網の経路探索やSNSなどの友人関係、ECサイトにおける購買情報などを表現するために用いられており、グラフ構造の機械学習モデルであるGNN(グラフニューラルネットワーク)と組み合わせて使用されることもあります。また、グラフデータのデータ分析における基盤としても活用されています。
このグラフデータベースはノードとエッジの集合としてデータを扱うもので、NoSQLの1つに分類されます。
このブログではグラフデータベースの1つのであるNeo4jとそのクエリ言語のCypherを用いて、SNSのよくある機能を実装していきます。
Cypher
Neo4jではCypherというクエリ言語が用いられます。
Cypherは宣言型のクエリ言語で、グラフデータベース用のSQLに相当する言語です。CypherではSQLの SELECTに対して MATCH句を用いてデータの検索を行います。基本的な記述方法は、解のように記述します。
1 2 |
MATCH (識別子:ノードのラベル)-[:エッジのタイプ]-(:ノードのラベル) RETURN 識別子 |
ノード部分は (:ノードのラベル)エッジ部分は [:エッジのタイプ](リレーションシップのタイプ)と記述します。無向グラフは -、有向グラフは <-、 ->で接続を表現し、 RETURN句の後の識別子を返します。識別子は変数のようなもので、ノードやエッジの :の前に宣言して使用することができます。
これに WHERE句で条件を追加したり、 ORDER BY句で並び替えることができます。複数の処理を組み合わせるときは WITH句で識別子を引き継いで、処理を増やすことができます。
あとはノード・エッジの追加や削除には CREATE、 DELETE、ノードやエッジに対する属性の登録、更新、削除には SET、 REMOVEなどが基本的なクエリです。
ソーシャルグラフのよくある機能を実装
環境
- Python
- Neo4j
- FastAPI
実行環境としては、dockerでFastAPIとneo4jのコンテナを立てた環境になります。
共通処理
まずはこれから説明するAPIで共通して必要になる処理について説明します。
主にDBへの接続処理についてですが、この処理を1つのメソッドにまとめています。
1 2 3 4 5 6 |
from neo4j import GraphDatabase def connection(): uri = "bolt://neo4j_container:7687" driver = GraphDatabase.driver(uri, auth=("neo4j","password")) return (driver) |
一点注意としては GraphDatabase.driverで指定するURLは bolt://コンテナ名:ポート番号で指定するそうです。docker環境の場合コンテナ名はcompose.yamlの container_nameを指定してください。 auth=はcompose.yamlの環境変数でIDとパスワードとして設定しておいたものを指定してください。
また、今回は初期データとして下の図のようなグラフデータを用意しました。これから紹介する各機能はこのグラフデータを基に行なっていきます。
機能1:ユーザーの登録
1つ目のAPIはユーザーの登録機能について説明します。
登録はとてもシンプルで、 CREATE句でノードを追加するだけで、登録することができます。クエリは下記のようなものになります。
1 |
CREATE (:USER{name:$user_name, age:$age}); |
今回はノードのプロパティ(ユーザー情報)には名前と年齢の2つのプロパティを定めました。
それぞれのプロパティに対応した $user_nameと $ageはFastAPIでリクエストされた時のパラメータを格納するために使います。
最終的なAPIの実装は下記のようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@app.get("/add_user") def add_user(user_name: str, age: int): try: query = """ CREATE (:USER{name:$user_name, age:$age}); """ driver = connection() session = driver.session() session.run(query,user_name=user_name,age=age) session.close() return {"ok"} except: return {"error"} |
session.runで $user_nameと $ageに変数を指定して、それぞれの値でノードを作成します。
これをFastAPIの /docsで実行してみます。30歳のHさんを追加する場合、実行されるクエリは下記のようなクエリになります。
1 |
CREATE (:USER{name:"H", age:30}); |
neo4jの /browserで結果を確認すると下の図のようにノードが追加されていることが確認できます。
機能2:フォロー機能
次に紹介するのはユーザー間でのフォロー機能です。
フォロー機能もノード間のエッジを追加するだけなのでシンプルに実装できます。
基本的には指定したノードを検索し、その2点を接続するためのエッジを CREATE句で作成するだけで実装できます。クエリとしては、下記のようなものになります。
1 2 |
MATCH(u1:USER{name: $start_user_name}), (u2:USER{name: $end_user_name}) CREATE (u1)-[:FOLLOW]->(u2); |
API全体の処理はユーザーの登録機能とほとんど変わらないので省略します。
このAPIでHさんがCさんをフォローするようにAPIをリクエストする場合、実行されるクエリは下記のようなクエリになります。
1 2 |
MATCH(u1:USER{name: "H"}), (u2:USER{name: "C"}) CREATE (u1)-[:FOLLOW]->(u2); |
結果を確認するとHさんからCさんに向けてエッジが貼られていることがわかります。
機能3:フォロー(フォロワー)一覧表示
次はフォロー(フォロワー)一覧表示機能を説明します。
フォロー(フォロワー)一覧表示では、 MATCH句でグラフのノードとエッジのラベル、タイプを指定し、 WHERE句で特定のノードに絞るようなクエリで実現できます。実際のクエリは下記のようなものになります。
フォロー一覧表示
1 2 3 |
MATCH (u1:USER) -[:FOLLOW]-> (u2:USER) WHERE u1.name= $user_name RETURN u2 |
フォロワー一覧表示
1 2 3 |
MATCH (u1:USER) -[:FOLLOW]-> (u2:USER) WHERE u2.name=$user_name RETURN u1 |
API全体では、下記のようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@app.get("/search_follower") def search_follower(user_name: str): driver = connection() session = driver.session() query = """ MATCH (u1:USER) -[:FOLLOW]-> (u2:USER) WHERE u2.name=$user_name RETURN u1 """ result = session.run(query,user_name=user_name) user_list = [] for i in result: user_list.append(i['u1']) session.close() return user_list |
フォロー一覧表示のAPIでAさん指定してリクエストする場合は下記のクエリが実行されます。
1 2 3 |
MATCH (u1:USER) -[:FOLLOW]-> (u2:USER) WHERE u1.name= "A" RETURN u2 |
レスポンスとしては以下のデータが返されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[ { "name": "C", "age": "40" }, { "name": "E", "age": "60" }, { "name": "F", "age": "70" } ] |
このようにAさんに対応したノードがエッジを向けている3つのノードが返されているのがわかります。
機能4:フレンドのレコメンド機能(発展)
最後に発展としてSNSなどでよくみる簡単な友達のレコメンド機能をNeo4jのグラフdbで実装してみようと思います。考え方としては「友達から一番接続数(エッジ)の多い友達の友達」を返すようなAPIを実装します。この実装では、クエリがややこしくなってしまうので、これまでと違い無向グラフとしての実装になります。実装内容に関しては友達と友達の友達間でのエッジの本数を”友達の友達”単位でカウントする形になります。
クエリとしては下記のようになります。
1 2 3 4 5 6 7 8 9 10 |
MATCH (n:USER { name: $user_name})-[:FOLLOW*2..2]-(friend_of_friend:USER) WHERE NOT (n:USER { name: $user_name})-[:FOLLOW]-(friend_of_friend:USER) WITH friend_of_friend WHERE NOT friend_of_friend.name = $user_name WITH friend_of_friend MATCH (:USER { name: $user_name})-[:FOLLOW]-(friend:USER) WHERE NOT friend.name = $user_name WITH friend_of_friend,friend MATCH (friend:USER)-[r:FOLLOW]-(friend_of_friend:USER) RETURN friend,friend_of_friend |
このクエリで注意する点としては、1行目のエッジ [:FOLLOW*2..2]の部分で、FOLLOWタイプのエッジを深さ2(半径2のエゴセントリックネットワーク)の範囲まで含めるという記述になります。あとは、 WITH句で必要な識別子を引き継いでいくような記述になります。(もっと簡潔な書き方があるかもしれないです)
API全体の実装としては下記のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@app.get("/recommend_friend") def recommend_friend(user_name: str): driver = connection() session = driver.session() edge_query = """ MATCH (n:USER { name: $user_name})-[:FOLLOW*2..2]-(friend_of_friend:USER) WHERE NOT (n:USER { name: $user_name})-[:FOLLOW]-(friend_of_friend:USER) WITH friend_of_friend WHERE NOT friend_of_friend.name = $user_name WITH friend_of_friend MATCH (:USER { name: $user_name})-[:FOLLOW]-(friend:USER) WHERE NOT friend.name = $user_name WITH friend_of_friend,friend MATCH (friend:USER)-[r:FOLLOW]-(friend_of_friend:USER) RETURN friend,friend_of_friend """ edge_result = session.run(edge_query,user_name=user_name) edge_list = list(set([(i[0]["name"],i[1]["name"]) for i in edge_result])) node_list = [i[1] for i in edge_list] result = collections.Counter(node_list) result = sorted(result.items(), key=lambda x:x[1], reverse=True) session.close() return result |
このAPIでCさんを指定してリクエストすると以下のレスポンスが返されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[ [ "E", 2 ], [ "D", 1 ], [ "F", 1 ] ] |
レスポンスの内容としては友達からの接続数とそのノードを表しており、Eさんが2人の友たちとも接点があるという意味を示しています。そのためCさんにはEさんをレコメンドするのが最適だとわかります。
まとめ
グラフデータベースであるNeo4jを用いてSNSのよくある機能の実装を紹介しました。グラフデータはRDBに比べて直感的で分かりやすいので、アイデアがあればさまざまな分野に応用できると思います。また、このブログではグラフデータベースのNeo4jを紹介しましたが、グラフデータベース以外にもキーバリュー型のデータベースであるRedisや時系列のデータベースであるInfluxDBなどもあるので、興味がある方は調べて見てください。