はじめに
この記事はAmazon Neptuneのノートブックセットに用意されているチュートリアルの日本語訳解説です。
Overview
▷ Amazon Neptuneへようこそ!
Amazon Neptune は、フルマネージド型のグラフサービスであり、ユーザーはグラフデータを大規模に分析、構築、および洞察を導き出すことができます。
Amazon Neptune は、グラフデータベース、グラフ分析エンジン、グラフ機械学習、および高度に接続されたデータセットの保存と分析に必要なライブラリとツールで構成されています。
▷ Amazon Neptune の主な機能は何ですか?
Amazon Neptune は、次の 3 つの主要な機能で構成されています。
-
Neptune Database
エンタープライズ機能と統合のフルセットを備えた専用のグラフデータベースを提供します。
Neptune Database は、高可用性、災害復旧、動的スケーリング、およびエンタープライズアプリケーションに必要なその他の機能を必要とするミッションクリティカルなグラフアプリケーションを構築するための最適なツールです。
サンプルアプリケーション、ユースケース、およびガイダンスをカバーするノートブックのセットが 01-Neptune-Database に用意されています。
- Neptune Analytics は、グラフ分析とアルゴリズム、低レイテンシーの分析クエリ、Vector Similarity Search (VSS) とグラフトラバーサルの組み合わせを提供する分析エンジンです。
サンプルアプリケーション、ユースケース、およびガイダンスをカバーするノートブックのセットが 02-Neptune-Analytics に用意されています。
-
Neptune ML を使用すると、数週間ではなく数時間で大きなグラフで機械学習モデルを構築してトレーニングできます。
これを実現するために、Neptune ML は、Amazon SageMaker と Deep Graph Library (DGL) (オープンソース) を搭載したグラフニューラルネットワーク (GNN) テクノロジーを使用します。
サンプルアプリケーションと可能な機械学習予測のタイプをカバーするノートブックのセットを提供しており、 03-Neptune-ML にあります。
▷ Amazon Neptune で何ができますか?
-
Neptune Database と Neptune Developer Tools を活用した大規模なアプリケーションの構築および運用
ミッションクリティカルなシステムを構築している開発者とデータエンジニアは、Neptune Database と Neptune Developer Tools を活用して、さまざまな地理的リージョンで大規模なアプリケーションを構築および運用する必要があります。
製品推奨エンジン、ID およびアクセス管理システム、コンプライアンス システムなどのシステムには、世界をリードするマネージド グラフ データベースからのみ利用できる、地理的に分散した機能が常に必要です。
分析的またはアルゴリズムによる拡張が必要なシステムでは、Neptune Analytics を活用してこれらの重要な洞察を導き出すこともできます。
-
Neptune Analytics と Neptune Notebook を活用したインサイトの検出
データを探索し、データを操作してインサイトを導き出そうとしているデータサイエンティストや開発者は、Neptune Analytics と Neptune Notebook を活用する必要があります。
これらの機能により、ユーザーは Pandas、Jupyter、Python などの使い慣れたツールを使用してデータを操作し、詐欺、違法行為、最適化の機会などを示すデータ内の相互作用と行動パターンを検出して特定できます。
-
Neptune ML を活用したグラフニューラルネットワークによる接続と分類の設計、構築、最適化、予測
リアルタイムの予測を実行したいと考えている機械学習エンジニアやデータサイエンティストは、Neptune ML を活用して、最先端のグラフニューラルネットワークを利用して接続と分類を設計、構築、最適化、予測できます。
特徴テーブルの拡張を検討している ML エンジニアは、Neptune 分析を使用して、クラスタリング、中心性、パスファインディングなどの一般的なアルゴリズムを使用して、接続されたデータから重要な特徴を導き出すことができます。
-
Neptune Analytics を活用した RAG アプリケーションの構築
Retrieval Augmented Generation (RAG) アプリケーションを構築する開発者は、Neptune Analytics を活用して、埋め込みによって提供される高密度データ表現に対して VSS 検索を実行し、これらの結果をグラフのコンテキスト認識データ表現と組み合わせることができます。
開発者は、これらの機能を組み合わせて、VSS を介して関連するデータの場所をすばやく取得し、グラフ探索を使用してそれらの接続に関連する豊富なコンテキスト情報を提供するアプリケーションを開発できます。
▷ Amazon Neptune には他にどのようなツールがありますか?
上記の機能に加えて、Amazon Neptune には、
-
Neptune Notebooks
開発者やデータサイエンティストがグラフデータのクエリ、視覚化、操作を行うことができるマネージド IDE ツールキットです。
-
Neptune Graph Explorer
オープンソースのグラフ視覚化機能であり、アナリストにグラフデータを視覚的に探索して操作するためのノーコードツールを提供します
-
Neptune 開発者ツール
Java、Python、.NET、Javascript、Go を使用しているお客様がグラフアプリケーションを迅速かつ簡単に開発できるようにするライブラリ、SDK、およびドライバーのセットです。
-
Neptune コネクタ
Neptune Database と他の AWS サービス (AWS Glue、Sagemaker、Lambda、Athena、DMS、Backup など) との統合を容易にするツールと統合のセットです。
▷ Neptune Notebooksとは何ですか?
ワークベンチは、Jupyter ノートブックと、Neptune を簡単に使い始めるための一連のツールをホストする対話型環境です。
▷ 次のステップ
ノートブックの仕組みについての基本を学んだので、以下はNeptuneの旅を続けるためのおすすめのノートブックです。
- Neptune Database を活用するミッションクリティカルなシステムの構築を検討している
01-Neptune-Database
- グラフ分析とアルゴリズム、低レイテンシ分析クエリ、またはグラフ探索を伴うベクトル類似検索 (VSS) の使用を検討している
02-Neptune-Analytics
- 大きなグラフで機械学習モデルを構築してトレーニングする
03-Neptune-ML
01-Neptune-Database
01-Getting-Started
01-About-the-Neptune-Notebook
02-Using-Gremlin-to-Access-the-Graph
03-Using-RDF-and-SPARQL-to-Access-the-Graph
04-Social-Network-Recommendations-with-Gremlin
05-Dining-By-Friends-in-Amazon-Neptune
02-Visualization
03-Sample-Application
01-About-the-Neptune-Notebook
Neptune Notebooksとは何ですか?
ワークベンチは、Jupyter ノートブックと、Neptune を簡単に使い始めるための一連のツールをホストする対話型環境です。
Neptune Notebooks にすでに精通している場合は、次の一般的なサンプルグラフアプリケーションを探索してください。
- 不正グラフの概要
- ナレッジグラフの概要
- アイデンティティグラフの概要
- セキュリティグラフの概要
クラスターの状態を確認してみてください
%status マジックを使用して、クラスターの正常性を確認したり、エンジンのバージョンなどの追加情報を取得したりできます。
%status
SPARQL クエリを発行してみてください
SPARQL クエリは、%%sparql セルマジックを使用して発行できます。
グラフにいくつかの項目を追加してから、それらを取得しましょう。
%%sparql
INSERT DATA {
<http://s-1000-1> <http://p-1> <http://o-1> .
<http://s-1000-2> <http://p-2> <http://o-2> .
<http://s-1000-3> <http://p-3> <http://o-3> .
<http://s-1000-4> <http://p-4> <http://o-4> .
<http://s-1000-5> <http://p-5> <http://o-5> .
<http://s-1000-6> <http://p-6> <http://o-6> .
<http://s-1000-7> <http://p-7> <http://o-7> .
<http://s-1000-8> <http://p-8> <http://o-8> .
<http://s-1000-9> <http://p-9> <http://o-9> .
<http://s-1000-10> <http://p-10> <http://o-10> .
<http://s-1000-11> <http://p-11> <http://o-11> .
<http://s-1000-12> <http://p-12> <http://o-12> .
<http://s-1000-13> <http://p-13> <http://o-13> .
<http://s-1000-14> <http://p-14> <http://o-14> .
<http://s-1000-15> <http://p-15> <http://o-15> .
<http://s-1000-16> <http://p-16> <http://o-16> .
<http://s-1000-17> <http://p-17> <http://o-17> .
<http://s-1000-18> <http://p-18> <http://o-18> .
<http://s-1000-19> <http://p-19> <http://o-19> .
<http://s-1000-20> <http://p-20> <http://o-20> .
}
%%sparql
SELECT * WHERE {
?s ?p ?o
} LIMIT 10
このクエリの説明は、次のように %%sparql
cell magic の line コンポーネントで指定することで確認できます。
%%sparql explain
SELECT * WHERE {
?s ?p ?o
} LIMIT 10
Gremlin クエリの発行
Gremlin クエリは、%%gremlin
セル マジックを使用して発行できます。
いくつかの頂点を追加してから、それらを取得しましょう。
%%gremlin
g.addV('person').property('name', 'dan')
.addV('person').property('name', 'mike')
.addV('person').property('name', 'saikiran’)
%%gremlin
g.V().limit(10)
%%gremlin
g.V().valueMap().limit(10)
%%sparql
と同様に、クエリで切り替えることができる explain モードがあります。
%%gremlin explain
g.V().limit(10)
さらに、Gremlinにはプロファイルモードがあり、explainと同様に実行できます。
%%gremlin profile
g.V().limit(10)
openCypher クエリの発行
%%opencypher
(または単に %%oc
) セルマジックを使用して openCypher クエリを発行できます。
グラフにいくつかの要素を入力してから、それらを取得しましょう。
%%oc
CREATE (a:Language {name:"Gremlin"})
CREATE (b:Language {name:"Cypher"})
CREATE (c:Language {name:"SPARQL"})
%%oc
MATCH (n)
RETURN n
LIMIT 10
他のクエリ言語のマジックと同様に、%%oc
はクエリの説明プランの取得をサポートします。
%%oc explain
MATCH (n)
RETURN n
LIMIT 10
02-Using-Gremlin-to-Access-the-Graph
次のチュートリアルでは、Gremlin を使用して Neptune グラフに頂点、エッジ、プロパティなどを追加する手順を説明し、Neptune 固有の Gremlin 実装のいくつかの違いを強調します。
ラベルとプロパティを持つ頂点を追加
%%gremlin
g.addV('person').property('name', 'justin’)
頂点には、GUID を含む文字列 ID が割り当てられます。すべての頂点 ID は Neptune の文字列です。
カスタム ID を持つ頂点を追加する
%%gremlin
g.addV('person').property(id, '1').property('name', 'martin’)
id プロパティは引用符で囲まれていません。
これは、頂点の ID のキーワードです。ここでの頂点 ID は、番号 1 を含む文字列です。
通常のプロパティ名は引用符で囲む必要があります。
プロパティを変更するか、存在しない場合はプロパティを追加します。
%%gremlin
g.V('1').property(single, 'name', 'marko’)
ここでは、前の手順で頂点の name プロパティを変更しています。
これにより、name プロパティから既存の値がすべて削除されます。
single を指定しなかった場合は、name プロパティに値が追加されます (まだ追加されていない場合)。
プロパティを追加しますが、プロパティに既に値がある場合はプロパティを追加します。
%%gremlin
g.V('1').property('age', 29)
Neptune は、デフォルトのアクションとして set cardinality を使用します。
このコマンドは、age プロパティに値 29 を追加しますが、既存の値は置き換えられません。
age プロパティに既に値がある場合、このコマンドはプロパティに 29 を追加します。たとえば、age プロパティが 27 の場合、新しい値は [ 27, 29 ] になります。
複数の頂点を追加する
%%gremlin
g.addV('person').property(id, '2').property('name', 'vadas').property('age', 27).next()
g.addV('software').property(id, '3').property('name', 'lop').property('lang', 'java').next()
g.addV('person').property(id, '4').property('name', 'josh').property('age', 32).next()
g.addV('software').property(id, '5').property('name', 'ripple').property('lang', 'java').next()
g.addV('person').property(id, '6').property('name', 'peter').property('age', 35)
複数のステートメントを同時に Neptune に送信できます。
ステートメントは、改行 ('n')、スペース (' ')、セミコロン (';')、または何もない(例: g.addV('person').next()g.V() は有効です)。
Gremlin Console は改行 ('n') ごとに個別のコマンドを送信するため、その場合はそれぞれが個別のトランザクションになります。
この例では、読みやすくするために、すべてのコマンドが別々の行に書かれています。
改行 ('n') 文字を削除して、Gremlin Console を介して 1 つのコマンドとして送信します。
最後のステートメント以外のすべてのステートメントは、 .next() や .iterate() などの終了ステップで終了する必要があり、そうでない場合は実行されません。Gremlin Console では、これらの終了手順は必要ありません。
一緒に送信されるすべてのステートメントは 1 つのトランザクションに含まれ、一緒に成功または失敗します。
エッジを追加
%%gremlin
g.V('1').addE('knows').to(__.V('2')).property('weight', 0.5).next()
g.addE('knows').from(__.V('1')).to(__.V('4')).property('weight', 1.0)
エッジを追加する2つの異なる方法を次に示します。
- モダングラフの残りの部分を追加します
%%gremlin
g.V('1').addE('created').to(__.V('3')).property('weight', 0.4).next()
g.V('4').addE('created').to(__.V('5')).property('weight', 1.0).next()
g.V('4').addE('created').to(__.V('3')).property('weight', 0.4).next()
g.V('6').addE('created').to(__.V('3')).property('weight', 0.2)
- 頂点の削除
%%gremlin
g.V().has('name', 'justin').drop()
トラバーサルの実行
%%gremlin
g.V().hasLabel('person’)
値 (valueMap()) を使用してトラバーサルを実行します。
%%gremlin
g.V().has('name', 'marko').out('knows').valueMap()
marko が "認識している" すべての頂点のキーと値のペアを返します。
複数のラベルを指定します。
%%gremlin
g.addV("Label1::Label2::Label3")
Neptune は、頂点の複数のラベルをサポートしています。ラベルを作成するときは、複数のラベルを 「::」で区切って指定できます。
この例では、3 つの異なるラベルを持つ頂点を追加します。
hasLabel ステップは、この頂点を hasLabel("Label1")、hasLabel("Label2")、hasLabel("Label3") の 3 つのラベルのいずれかと照合します。
区切り記号は、この使用専用に予約されています。
hasLabel ステップで複数のラベルを指定することはできません。
たとえば、hasLabel("Label1::Label2") は何も一致しません。
時刻/日付を指定します。
%%gremlin
g.V().property(single, 'lastUpdate', datetime('2018-01-01T00:00:00’))
Neptune は Java Date をサポートしていません。代わりに datetime()
関数を使用してください。datetime()
は、ISO8061準拠の datetime 文字列を受け入れます。
YYYY-MM-DD
、YYYY-MM-DDTHH:mm
、YYYY-MM-DDTHH:mm:SS
、および YYYY-MM-DDTHH:mm:SSZ
の形式をサポートしています。
頂点、プロパティ、またはエッジを削除します。
%%gremlin
g.V().hasLabel('person').properties('age').drop().iterate()
g.V('1').drop().iterate()
g.V().outE().hasLabel('created').drop()
.next() ステップは .drop() では機能しません。代わりに .iterate() を使用してください。
03-Using-RDF-and-SPARQL-to-Access-the-Graph
SPARQLは、Web用に設計されたグラフデータ形式であるResource Description Framework(RDF)のクエリ言語です。
Amazon Neptune は SPARQL 1.1 と互換性があります。つまり、Neptune DB インスタンスに接続し、SPARQL 1.1 Query Language 仕様で説明されているクエリ言語を使用してグラフをクエリできます。
SPARQL のクエリは、返す変数を指定する SELECT 句と、グラフで照合するデータを指定する WHERE 句で構成されます。
Neptune DB インスタンスへの SPARQL クエリの HTTP エンドポイントは https://your-neptune-endpoint:port/sparql です。
%%sparql マジックを使用して、以下の SPARQL UPDATE を発行します
%%sparql
INSERT DATA { <https://test.com/s> <https://test.com/p> <https://test.com/o> . }
次のように入力して、%%sparql マジックを使用して SPARQL QUERY を送信します。
%%sparql
SELECT ?s ?p ?o WHERE {?s ?p ?o} LIMIT 10
前の例では、制限が 10 の ?s ?p ?o クエリを使用して、グラフ内のトリプル (主語 - 述語 - 目的語) を最大 10 個返します。他のクエリを実行するには、別の SPARQL クエリに置き換えます。
レスポンスのデフォルトの MIME タイプは、Neptune へのクエリで application/sparql-results+json です。
別のMIMEタイプを使用したい場合は、-m/--media-typeパラメータで指定できます。たとえば、結果を SPARQL XML 形式で取得するには、次のコマンドを実行します。
%%sparql -m application/sparql-results+xml
SELECT ?s ?p ?o WHERE {?s ?p ?o} LIMIT 10
04-Social-Network-Recommendations-with-Gremlin
この例では、いくつかの単純な Gremlin クエリを使用して、強力なソーシャル ネットワーク予測機能を構築します。
ここで説明した手法は、ソーシャル以外の他の領域で予測を構築するために使用できます。
多くのソーシャルネットワークアプリケーションに共通する機能は、People-You-May-Know(またはPeople-You-May-Want-To-Know)(PYMKと略されることもあります)を推奨する機能です。
Amazon Neptune を使用すると、トライアドクロージャと呼ばれるよく理解されている現象を使用して PYMK 機能を実装できます。
三大閉包とは、データが時間とともに変化すると、グラフ内の非常に局所的なレベルの要素が安定した三角形を形成する傾向です。この動作は、あらゆる種類の異なるドメインのグラフで観察できます。これは、多くの同性愛に基づくレコメンデーションシステム、つまり類似性がつながりを生むという事実を利用するシステムの基礎となっています。
この例では、ソーシャルネットワークのコンテキストでトライアドクロージャを使用することを見ていきます。
ビル、テリー、サラがメンバーであるソーシャルネットワークがあると想像してみましょう。
テリーはビルとサラの両方と友達です。つまり、テリーとサラにはビルに共通の友人がいるということです。
彼らには共通のビルがいるため、サラとビルはすでにお互いを知っているか、近い将来お互いを知る可能性が高くなります。
グラフを見るだけでも、彼らが友達になる手段と動機の両方を持っていることがわかります。
ビルと一緒にいると、サラとテリーが出会う手段になります。そして、ビルを信頼しているからこそ、ビルが友人である人々を信頼する動機があり、もし出会ったとしても、つながりを形成してトライアングルを閉じる可能性が高くなるのです。
ソーシャルネットワークのコンテキストでは、トライアドクロージャを使用してPYMKを実装できます。特定のユーザーがシステムにログインすると、グラフでそのユーザーの頂点を検索し、友人の友人ネットワークをトラバースして、三角形を閉じる機会を探すことができます。ユーザーから身近な友人を経由して、現在つながっていない人までの経路が多ければ多いほど、ユーザーはその人をすでに知っているか、その人と知り合うことで利益を得る可能性が高くなります。
- セットアップ
開始する前に、セルマジック %%gremlin とその後のドロップクエリを使用して、Neptune クラスターから既存のデータをすべてクリアします。
%%gremlin
g.V().drop()
Neptune Notebooks によって公開されるセルマジックは、デフォルトで ~/graph_notebook_config.json の下にある設定を使用します。Sagemaker インスタンスの初期化時に、この設定は、接続されているクラスターから派生した環境変数を使用して生成されます。
設定内容は2つの方法で確認できます。ファイル自体を印刷することも、開いたノートブックで使用されている構成を探すこともできます。
%%bash
cat ~/graph_notebook_config.json
%graph_notebook_config
- ソーシャルネットワークを作成する
次に、小さなソーシャルネットワークを作成します。次のスクリプトは 1 つのステートメントで構成されていることに注意してください。ここでのすべての頂点とエッジは、1 つのトランザクションのコンテキストで作成されます。
%%gremlin
g.
addV('User').property('name','Bill').property('birthdate', '1988-03-22').
addV('User').property('name','Sarah').property('birthdate', '1992-05-03').
addV('User').property('name','Ben').property('birthdate', '1989-10-21').
addV('User').property('name','Lucy').property('birthdate', '1998-01-17').
addV('User').property('name','Colin').property('birthdate', '2001-08-14').
addV('User').property('name','Emily').property('birthdate', '1998-03-05').
addV('User').property('name','Gordon').property('birthdate', '2002-12-04').
addV('User').property('name','Kate').property('birthdate', '1995-02-12').
addV('User').property('name','Peter').property('birthdate', '2001-02-27').
addV('User').property('name','Terry').property('birthdate', '1989-10-02').
addV('User').property('name','Alistair').property('birthdate', '1992-06-30').
addV('User').property('name','Eve').property('birthdate', '2000-05-13').
addV('User').property('name','Gary').property('birthdate', '1998-09-20').
addV('User').property('name','Mary').property('birthdate', '1997-01-27').
addV('User').property('name','Charlie').property('birthdate', '1989-11-02').
addV('User').property('name','Sue').property('birthdate', '1994-03-08').
addV('User').property('name','Arnold').property('birthdate', '2002-07-23').
addV('User').property('name','Chloe').property('birthdate', '1988-11-04').
addV('User').property('name','Henry').property('birthdate', '1996-03-15').
addV('User').property('name','Josie').property('birthdate', '2003-08-21').
V().hasLabel('User').has('name','Sarah').as('a').V().hasLabel('User').has('name','Bill').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Colin').as('a').V().hasLabel('User').has('name','Bill').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Terry').as('a').V().hasLabel('User').has('name','Bill').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Peter').as('a').V().hasLabel('User').has('name','Colin').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Kate').as('a').V().hasLabel('User').has('name','Ben').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Kate').as('a').V().hasLabel('User').has('name','Lucy').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Eve').as('a').V().hasLabel('User').has('name','Lucy').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Alistair').as('a').V().hasLabel('User').has('name','Kate').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Gary').as('a').V().hasLabel('User').has('name','Colin').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Gordon').as('a').V().hasLabel('User').has('name','Emily').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Alistair').as('a').V().hasLabel('User').has('name','Emily').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Terry').as('a').V().hasLabel('User').has('name','Gordon').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Alistair').as('a').V().hasLabel('User').has('name','Terry').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Gary').as('a').V().hasLabel('User').has('name','Terry').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Mary').as('a').V().hasLabel('User').has('name','Terry').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Henry').as('a').V().hasLabel('User').has('name','Alistair').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Sue').as('a').V().hasLabel('User').has('name','Eve').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Sue').as('a').V().hasLabel('User').has('name','Charlie').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Josie').as('a').V().hasLabel('User').has('name','Charlie').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Henry').as('a').V().hasLabel('User').has('name','Charlie').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Henry').as('a').V().hasLabel('User').has('name','Mary').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Mary').as('a').V().hasLabel('User').has('name','Gary').addE('FRIEND').to('a').property('strength',1).
V().hasLabel('User').has('name','Henry').as('a').V().hasLabel('User').has('name','Gary').addE('FRIEND').to('a').property('strength',2).
V().hasLabel('User').has('name','Chloe').as('a').V().hasLabel('User').has('name','Gary').addE('FRIEND').to('a').property('strength',3).
V().hasLabel('User').has('name','Henry').as('a').V().hasLabel('User').has('name','Arnold').addE('FRIEND').to('a').property('strength',1).
next()
- レコメンデーションの作成
次に、特定のユーザーに対する PYMK レコメンデーションを作成しましょう。
次のクエリでは、ユーザーを表す頂点を検索しています。
次に、FRIEND リレーションシップをトラバースして (リレーションシップの方向は気にしないので、both() を使用しています)、そのユーザーの直接のフレンドを見つけます。
次に、グラフ内の別のホップをトラバースして、現在ユーザーに接続していない友人の友人を探します (つまり、閉じていない三角形を探します)。
次に、これらの候補フレンドへのパスをカウントし、ユーザーの直接のフレンドの1人を介して候補に到達できる回数に基づいて結果を並べ替えます。
%%gremlin
g.V().hasLabel('User').has('name', 'Terry').as('user').
both('FRIEND').aggregate('friends').
both('FRIEND').
where(P.neq('user')).where(P.without('friends')).
groupCount().by('name').
order(Scope.local).by(values, Order.desc).
next()
- 友情の強さを使用してレコメンデーションを改善する
もし、共鳴するほど強い友情の絆だけに基づいて提案したいとしたらどうでしょうか?
グラフの作成に使用した Gremlin を見ると、各 FRIEND エッジに strength プロパティがあることがわかります。
次のクエリでは、走査によってこの強度プロパティに述語が適用されます。トラバーサルをエッジに配置し、述語を適用するために both() ではなく bothE() を使用することに注意してください。強度が 1 より大きい場合にのみ進行します。
%%gremlin
g.V().hasLabel('User').has('name', 'Terry').as('user')
.bothE('FRIEND')
.has('strength', P.gt(1)).otherV()
.aggregate('friends')
.bothE('FRIEND')
.has('strength', P.gt(1)).otherV()
.where(P.neq('user')).where(P.without('friends'))
.groupCount().by('name')
.order(Scope.local).by(values, Order.desc)
.next()
私たちは、直接の友人にトラバースする場合でも、弱い友人関係を割り引くため、このクエリは、ユーザーと直接のつながりが弱い人をレコメンデーションすることになることがあります。
しかし、それは私たちの社会的領域の文脈では理にかなっています。
私たちの親しい友人の一人は、私たちが弱いつながりを持っている人々の一人と強い友情を持っています。したがって、時間の経過とともに、この弱い結合は強くなると予測できます。
05-Dining-By-Friends-in-Amazon-Neptune
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0
- 背景
Manning Publications の『Graph Databases in Action』は、一般的なグラフ開発パターンを使用してアプリケーションを構築する方法のフレームワークを読者に提供する書籍です。
このフレームワークを教えるために、この本では、これらのパターンを使用して、友人と評価を使用してパーソナライズされたレストランの推奨事項を提供する架空のアプリケーション DiningByFriends を構築します。
これを実現するために、DiningByFriendsは、ユーザーのソーシャルネットワーク、レコメンデーションエンジン、パーソナライゼーションエンジンという3つの一般的なグラフの使用例の開発に従います。
これらのユースケースはそれぞれ、既存のスキルを活用してこれまで以上に複雑な質問に答えるような方法で、互いに積み重なって構築されています。
次のノートブックでは、Amazon Neptune を使用して DiningByFriends アプリのクエリを作成する方法を示します。
このノートブックでは、クエリを構築するための処理とフレームワーク (本書で詳しく説明されています) には焦点を当てません。代わりに、このノートブックでは、Amazon Neptune 内でこれらのユースケースを実現する方法に焦点を当てます。
Amazon Neptune のユニークな側面の 1 つは、同じデータに対して Apache TinkerPop Gremlin クエリと openCypher クエリの両方を実行できることです。
以下のノートブックでは、この本から最も重要なクエリを両方の言語で選択して紹介しています。これにより、2 つのクエリ言語の違いを確認できます。
各例では、最初に書籍の Gremlin トラバーサルを示し、必要な変更について説明します。
次に、同等のopenCypherクエリを示し、2つの言語間で注意したいハイライトと違いについて説明します。
結果を確認するために、どちらでも好きな方、または両方を実行してもかまいません。
- はじめに
このノートブックの使用を開始するために、ブックで使用されているデータを %seed コマンドに追加しましたので、以下のコマンドを使用してクラスターをロードできます。
注 - このノートブックでは、このコマンドが実行される前にクラスターが空だったことを前提としています。
データがロードされたので、Amazon Neptune を使用する場合の DiningByFriends の動作を見てみましょう。
- ソーシャルネットワーク
この本の第 3 章と第 4 章では、DiningByFriends のソーシャル ネットワークを作成して、単純なグラフベースのアプリケーションを構築するために必要な手順を説明しました。
ソーシャルネットワークは、人々がグラフのノードとして表され、ノード間の接続がエッジで表される、最も標準的なユースケースの一部と見なされることがよくあります。
DiningByFriendsのソーシャルネットワークデータ全体を見てみましょう。
通常、結果が数百を超えるネットワークは理解できない傾向があるため、これはお勧めしません。このソーシャルネットワークグラフは、現実的ですが、通常作業するものよりもはるかに小さい表現です。
%%gremlin -p v,oute,inv -d first_name
g.V().hasLabel('person').outE('friends').inV().path().by(elementMap())
%%opencypher -d first_name
MATCH p=(:person)-[:friends]->(:person)
RETURN p
- テッドの友達は?
「Graph Databases in Action」では、グラフの問題とその解決方法を考えるための基本的なフレームワークの学習に多くの時間が費やされました。
この本では、以下に示す「テッドの友達は誰ですか?」に答えるためのグレムリントラバーサルを開発する手順を説明しました。
Neptune で作業する場合、そのトラバーサルをコピーしてその答えを得ることができます。
%%gremlin
g.V().has('person', 'first_name', 'Ted').
out('friends').valueMap('first_name')
%%opencypher
MATCH (:person {first_name:'Ted'})-[:friends]->(p)
RETURN p.first_name
Gremlin と openCypher の違いについては詳しく説明しませんが、注意すべき重要な違いの 1 つは、Gremlin では first_name を List として返すのに対し、openCypher はそれを単一の値として返すことです。
- テッドの友達の友達は誰ですか?
前のクエリと同様に、本で開発されたクエリをコピーして貼り付け、Amazon Neptune で使用できます。
%%gremlin
g.V().has('person', 'first_name', 'Ted').
repeat(
out('friends')
).times(2).
values('first_name')
%%opencypher
MATCH (:person {first_name:'Ted'})-[:friends]->()-[:friends]->(p:person)
RETURN DISTINCT p.first_name
このクエリでは、Gremlin と openCypher の両方が 1 つの値を返すことに気付くかもしれません。Gremlin の場合、これは valueMap() ステップの代わりに values() ステップを使用しているためです。
ただし、OpenCypherバージョンはまだキーと値のペアのままで、Gremlinバージョンは単なる文字列であることに気付くかもしれません。
- テッドとデニスはどのようにつながっていますか?
グラフデータベースで最も強力な機能の1つは、データに対して再帰的なトラバーサルを実行して未知の接続を見つける機能です。この機能は、Gremlinでrepeat()を使用することと、openCypherで可変長パス[:1..4]を使用することの両方で利用できます。
この本では、パスの構築が重要である理由と、パスを使用するときに発生する可能性のある問題の種類について説明しました。
書籍から Gremlin のトラバーサルをコピーして貼り付けることもできますが、以下のクエリでは、表示を改善するためにいくつかの変更が加えられています。
具体的には:
both("friends").simplePath() を bothE("friends").otherV().simplePath() に変更し、結果の視覚化にエッジ方向を含めるようにしました。
表示用のプロパティを返すために、by(elementMap()) が path() に追加されました
%%gremlin -d first_name
g.V().has("person", "first_name", 'Ted').
until(has("person", "first_name", 'Denise')).
repeat(
bothE("friends").otherV().simplePath()
).path().by(elementMap())
%%opencypher -d first_name
MATCH p=(src:person {first_name:'Ted'})-[:friends*1..]-(dst:person {first_name:'Denise'})
RETURN p
上記のopenCypherクエリを見ると、非常に直感的に見えますが、サイクルに関しては潜在的な複雑さが隠れています。
Gremlinでは、simplePath()ステップを追加して、再帰的トラバーサルですでに訪れた頂点を除外し、グラフ内の循環を回避できるようにします。
openCypherは異なるアプローチを取り、同じパスで同じエッジを複数回通過するのを防ぐだけです。つまり、頂点間に 2 つのエッジがある場合 (たとえば、Dave と Kelly の間に 2 つの友人のエッジがある場合)、それぞれが一意のパスとして扱われ、同じ頂点のセットを繰り返し循環する可能性があります。一般に、これを回避する最善の方法は、可変長パス式に最大数を追加することです (例: [:friends*1..4])。
- レコメンデーション
第 8 章では、既知のウォークイングラフを使用して、DiningByFriends のレコメンデーションエンジンを構築しました。
ウォークは、頂点のセットとインシデント エッジで構成されるグラフ内の特定のタイプのパスであり、既知のウォークは、特定の頂点とインシデント エッジを指定するパスです。
レコメンデーションエンジンは一般的なグラフの使用例であり、既知のウォークは、データ内の接続を使用して複雑な現実世界の質問に対する答えを提供する 1 つの方法です。
レストランを見て、DiningByFriendsデータ内のデータを確認しましょう。
%%gremlin -d name
g.V().hasLabel('restaurant').bothE().otherV().path().by(elementMap())
%%opencypher -d name
MATCH p=(:restaurant)-[]-()
RETURN p
- レストランの最新レビューは?
グラフ内の既知のウォークの最も単純なパターンの 1 つは、頂点からインシデント エッジに移動し、次に隣接する頂点に移動することです。
この機能を Amazon Neptune で表示するには、本で開発した Gremlin トラバーサルをコピーして貼り付けます。
%%gremlin
g.V().has("restaurant", "restaurant_id", 8).
in("about").
order().
by("created_date").
limit(3).
elementMap("rating", "body", "created_date")
%%opencypher
MATCH (:restaurant {restaurant_id:8})<-[:about]-(r:review)
RETURN r.rating, r.body, r.created_date
ORDER BY r.created_date DESC
LIMIT 3
openCypher クエリを同等の Gremlin トラバーサルと比較すると、openCypher の ASCII アートベースのパターンマッチング構文 (例: ()-[]->()) は、クエリでこの種の既知のウォークを処理する際に、クエリを読みやすく理解しやすくするのに優れていることがわかります。
- 人はどのレストランを高く評価していますか?
次に示すのは、結果のグループ化と射影を使用して、より複雑な結果セットを作成する、より複雑な既知のウォークの例です。
%%gremlin
g.V().has("person", "person_id", 8).
out("lives").
in("within").
where(inE("about")).
group().
by(identity()).
by(in("about").values("rating").mean()).
unfold().
order().
by(values, Order.desc).
limit(10).
project("name", "address", "rating_avg").
by(select(keys).values("name")).
by(select(keys).values("address")).
by(select(values))
%%opencypher
MATCH (:person {person_id:8})-[:lives]->(:city)<-[:within]-(r:restaurant)<-[:about]-(v:review)
WITH r, avg(v.rating) AS rating_average
RETURN r.name AS name,
r.address AS address, rating_average
ORDER BY rating_average DESC
LIMIT 10
2つの異なるクエリを見ると、openCypherバージョンのクエリでは、平均評価を得るために結果を明示的にグループ化する必要がないことに気付くでしょう。
openCypherでは、クエリのこの部分でWITH r, avg(v.rating) AS rating_average行われるように、キーと関連する集計を返すときにグループ化が暗黙的に実行されます。
Gremlin トラバーサルでは、特定のレストランの平均評価を取得するために、このグループ化を明示的に実行する必要があります。
- 特定の料理を提供している人の近くに最も評価の高いレストランは何ですか?
本書の既知の歩行の最後の例は、いくつかの分岐を横断して質問をフィルタリングして回答する既知の歩行の例です。
これは、Gremlin の探索を本から直接コピーして貼り付けることができる別のクエリの例です。
%%gremlin
g.V().has('person','person_id',1).
out('lives').
in('within').
where(out('serves').has('name', within('diner', 'bar'))).
where(inE('about')).
group().
by(identity()).
by(__.in('about').values('rating').mean()).
unfold().
order().
by(values, desc).
limit(1).
project('name', 'address', 'rating_average', 'cuisine').
by(select(keys).values('name')).
by(select(keys).values('address')).
by(select(values)).
by(select(keys).out('serves').values('name'))
%%opencypher
MATCH (p:person)-[:lives]->(:city)<-[:within]-(r:restaurant)-[:serves]->(c:cuisine)
WHERE p.person_id = 1 AND c.name IN ['diner', 'bar']
WITH r
MATCH (r)<-[:about]-(v:review)
WITH r, avg(v.rating) AS rating_average
RETURN r.name AS name,
r.address AS address, rating_average
ORDER BY rating_average DESC
LIMIT 1
Gremlin と openCypher のトラバーサルの違いの 1 つは、Gremlin ではグラフをトラバースし、where() を使用してブランチをフィルタリングし、最後に結果をグループ化することです。
openCypherのパターンマッチング構文では、これを少し異なる方法でアプローチする必要があります。
openCypherはGremlinと同じ分岐機能を提供していないため、最初に基本パターン(その人の都市で料理を提供するレストランを見つける)を一致させ、このパターンをフィルタリングして必要な料理の種類を見つけ、次にそれらのフィルタリングされた結果に追加の一致を行ってレビューを取得し、平均を計算する必要があります。
- パーソナライゼーション
レストランのレコメンデーションのパーソナライゼーションは、レコメンデーションエンジンに対する協調フィルタリングアプローチの一種で、ユーザーの好き嫌いに関する情報(この場合は、友達の好き嫌いを使用します)を使用してレコメンデーションを提供します。
- 友達はどのレストランをおすすめしますか?
この質問に答えるには、本書で説明されているものとは異なるアプローチを取る必要があります。
この本では、Gremlin 内のセッションを活用して、サブグラフを Gremlin サーバーのメモリに取り込み、レコメンデーションを計算しました。
このアプローチにはいくつかの利点がありますが、本で説明されているように、セッションの使用は Amazon Neptune ではサポートされていません。
幸いなことに、これがこの最終目標を達成する唯一の方法ではありません。別の方法は、サブグラフを作成し、以下のトラバーサルで行ったのと同じトラバーサル内の推奨事項でフィルタリングすることです。
また、このトラバーサルをいくつかの異なる person_id 値で試して、結果が特定のユーザーに対してどのようにパーソナライズされるかを確認することをお勧めします。
%%gremlin
g.V().
has('person', 'person_id', 8).
both('friends').
both('wrote').as('v').
both('about').
where(both('within').has('city', 'name', 'Houston')).
group().by().by(select('v').values('rating').mean()).unfold().
order().by(values, desc).limit(3).
project("rating_average","name","address","restaurant_id").
by(__.select(values)).
by(__.select(keys).values("name")).
by(__.select(keys).values("address")).
by(__.select(keys).values("restaurant_id"))
%%opencypher
MATCH (p:person {person_id:8})-[:friends]-()-[:wrote]-(v)-[:about]-
(r:restaurant)-[:within]-(c:city {name:'Houston'})
RETURN avg(v.rating) as rating_average, r.name AS name, r.address AS address, r.restaurant_id AS restaurant_id
ORDER BY rating_average DESC
LIMIT 3
- まとめ
このノートブック全体でわかるように、Apache TinkerPop Gremlin と openCypher はどちらも、長所と短所を持つ強力なグラフ クエリ言語です。
理解すべき最も重要なことは、どの言語を選択しても、グラフ データを操作してアプリケーションを構築するためのフレームワークとアプローチは同じであるということです。
今後の予定
- 以下について理解を深めつつ、追記していきます。。。
01-Neptune-Database
02-Neptune-Analytics
03-Neptune-ML