スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Cypherでリコメンド

今回はNeo4jのCypherの使い方をリコメンドを例に紹介します。

■環境
Neo4j version 2.0 Community

■図1
rec01.png

上の図では、それぞれの購入アイテムが以下のような状況です。
ユーザと購入アイテムを「PURCHASED」というリレーションシップで関連付けています。

ユーザA
 ┗アイテム1
 ┗アイテム2
ユーザB
 ┗アイテム2
 ┗アイテム3
ユーザC
 ┗アイテム2
 ┗アイテム3
 ┗アイテム4

そして、今回はユーザBへリコメンドするアイテムをCypherで問合せます。
想定されえるリコメンド対象は以下の2つのアイテムです。

アイテム2にてリンクしているユーザAの購入アイテム → アイテム1
アイテム2,3にてリンクしているユーザCの購入アイテム → アイテム4

当然のことながら既にユーザBが購入済みのアイテム2、アイテム3はリコメンドの対象としません。

では、リコメンド対象を取得するCypherを以下に記述します。

match (user1{name:'ユーザB'})-[:PURCHASED]->(items)
<-[:PURCHASED]-(others)-[:PURCHASED]->(recommends)
return recommends

まず、(user1{name:'ユーザB'})-[:PURCHASED]->(items)の部分でユーザBの購入アイテムを表現しており、
その後の<-[:PURCHASED]-(others)で、ユーザBの購入商品と同じ商品を購入しているユーザ(others)を表現しています。
最後の(others)-[:PURCHASED]->(recommends)でリコメンド対象となるアイテムを取得しています。

しかし、実行結果を確認すると以下の5件が取得されています。

-----
アイテム1
アイテム3
アイテム4
アイテム2
アイテム4
-----

う~ん、何故でしょう?
既に購入済みのアイテム2、アイテム3が取得されています。
Neo4jには一意性の特性があり、既に辿ってきた関連性と同じものは検索しないようになっています。
それでは、一意性の特性がどのようなものか以下の図2を使って説明します。

■図2
friends.png

図2のケースで、Aさんの友達の友達、すなわちCさんを検索するCypherは以下となります。

MATCH (user{ name: 'Aさん' })-[r1:FRIEND]-()-[r2:FRIEND]-(friend_of_a_friend)
RETURN friend_of_a_friend

クエリの実行結果は以下の通りです。

-----
Cさん
-----

意識はしていませんが、実は既にこの時点で一意性の特性が有効になっています。
どう言う事か、上記のクエリを使って解説します。

まずは、(user{ name: 'Aさん' })-[r1:FRIEND]-()で、Bさんを取得(友達)。
次の()-[r2:FRIEND]-(friend_of_a_friend)でCさんを取得(友達の友達)。
しかし、よく考えてみてください。
問合せた内容は「友達の友達」です。そして、Bさん(友達)の友達は以下の2人となります。

Bさん→Aさん
Bさん→Cさん

そうです。ここでCさんだけ取得できるようになっているのは一意性が発揮されているのです。
では度々ですが、更に詳しく一意性とはどのようなものか、先ほどのクエリを使って解説します。
図3
friends2.png

最初のBさんを取得する部分の「MATCH (user{ name: 'Aさん' })-[r1:FRIEND]-()」で、r1にAさんとBさんの関連が格納されます。
そして「()-[r2:FRIEND]-(friend_of_a_friend)」でBさんとCさん(Bさんの友達)の関連が格納されます。
このr2を格納する時にBさんのもう一人の友達、すなわちAさんも格納されそうですが、一度辿ってきた関連のr1と同じものは、後のクエリでは格納されない動作をします。
これが一意性の特性です。
このような特性により、2つ以上の深さのある関連性(友達の友達等)の検索を行いやすくしています。

さて、一意性の特性があるなら、最初のリコメンドのクエリも購入済みのアイテムは取得しないのではないかと考えられます。
私が最初にイメージしたCypherの動作は下図のようなものでした。

図4 Cypher動作イメージ(誤解)
rec02.png

アイテム2、アイテム3はそれぞれリコメンド対象の検索過程で関連を既に辿っているので抽出はされてこないのではないかと。
しかし、そこには落とし穴がありまして、実際は下図のような動作をしています。

図5 Cyper動作イメージ(本来)
rec03.png

なんと、緑色と青色の別のパス経由で既に購入済みのアイテムが抽出されています。
一意性の特性は既に辿ったとこのある関連については、重複しないように動作するというものです。
言いかえれば、辿った事のない関連については取得します。
辿った事があるかどうかは、
「クエリの抽出対象となるノードを見つけるまでに経由したノードとリレーションと全く同じパス」
である事が条件となる為、今回のような購入済みのアイテムが抽出されてくるのです。

では、どのようにして購入済みのアイテムを抽出対象外とするのかというと、NOT関数とパターンを利用します。
Cypherのクエリは以下の通りです。

match (user1{name:'ユーザB'})-[:PURCHASED]->(items)
<-[:PURCHASED]-(others)-[:PURCHASED]->(recommends)
where not user1-[:PURCHASED]->(recommends)
return recommends

NOT関数はWHERE句の中で使用します。

not user1-[:PURCHASED]->(recommends)

実行結果
-----
アイテム1
アイテム4
アイテム4
-----

recommendsの中には抽出してきたアイテムが格納されており、こちらの中から既にユーザBがリレーション(PURCHASED)で関連しているノードと一致するものを否定するという指示になります。
これでやっと購入済みのアイテムをフィルタリングする事ができました。

ちなみに実行結果としてアイテム4が2件取得されていますが、この結果を応用すればアイテム毎のリコメンドの優先度(おすすめ度)を含めた実行結果を取得する事が出来ます(今回だとアイテム4 > アイテム1の優先度)。
こちらの方法については読者の方で考えてみてください。

■参考
Neo4jマニュアル - 7.4. Uniqueness
スポンサーサイト

Neo4jのRelationshipのプロパティの集計

最近、仕事でグラフデータベースのNeo4jを使っているので、Neo4jのネタも少しずつ増やしていこうかと思います。
今回は、Neo4jのリレーションのプロパティ値をCypherで集計する方法を紹介します。

■環境
Neo4j ver.2.0

■方法
以下のように複数のノードがプロパティを保持しているリレーションで関連付いている場合に始点のノードから
終点のノードの間に存在するリレーションのプロパティ値の合計を求める方法を説明します。
(下記の場合1+2+3で合計は6)

図:
(始点)‐1->(N1)-2->(N2)-3->(終点)

判例:
(名前) …ノード、()の中はノードのname属性の値
-数字-> …リレーション、数値はprop属性の値

では、まずは始点から終点までの全てのノード、リレーションを取得するクエリから。

match p=(s{name:'始点'})-[rel*]->(e{name:'終点'})
return p;

今回は始点と終点の間に複数ノードが存在する為、リレーションの指定に*を利用します。
この*は直接関連付いているノードの先にあるノードを検索できるようになります。
シンタックスは、以下のようになっており、どこまで先を検索するかの最大、最小値が
指定できるようになっています。
ちなみにNeo4jの世界では先のノードへ移ることをホップする(跳び越える)と表現しています。

Syntax:
-[:TYPE*minHops..maxHops]->

ここまで出来れば以下のクエリでプロパティ値が集計出来そうなのですが、こちらのクエリを実行すると
エラーとなってしまいます。

match p=(s{name:'始点'})-[rel*]->(e{name:'終点'})
return sum(rel.prop)
エラー内容:
Type mismatch: rel already defined with conflicting type Collection

sum()関数はノードのプロパティ値を集計する場合には正しく動作しますが、リレーションのプロパティ値の
集計にはどうやら使えないようです。
エラーメッセージからするとタイプミスマッチとなっているので、ノードのコレクションを想定していたところに
リレーションのコレクションが設定された為、エラーとなっているように解釈できそうな。。。(解釈が正しいかどうかは不明)

このようなケースでも集計が行えるようにNeo4jは「REDUCE」関数を用意してくれています。
このREDUCE関数を利用した集計クエリは以下の通りです。

match p=(s{name:'始点'})-[rel*]->(e{name:'終点'})
return reduce(totalProp = 0, relcol in rel | totalProp + relcol.prop) as total

REDUCE関数のシンタックスは以下の通りです。

Syntax:
REDUCE( accumulator = initial, identifier in collection | expression )

accumulator…合計値を格納する識別子、initialで初期値を設定する事ができます。
identifier…コレクションを格納する識別子。
collection…集計対象とするコレクションを指定(identifierに格納されます)。
expression…集計結果を算出する計算式を設定します。計算結果がaccumulatorに格納します。

今回のケースではtotalPropsをプロパティの集計値、match句で取得したリレーションのコレクションをrelcolに格納し、
計算式「totalProp + relcol.prop」によってprop属性の合計値がtotalPropに格納されるように設定しています。

以上です。

参考:
Neo4jマニュアル‐REDUCE関数
プロフィール

まこち

Author:まこち
スマートフォンのアプリ開発やWebサイト構築等を仕事や趣味でやっています。
最近はグラフデータベースも始めました。

最新記事
最新コメント
最新トラックバック
月別アーカイブ
カテゴリ
検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。