0
Help us understand the problem. What are the problem?

posted at

updated at

Amazon Neptuneから取得したグラフデータをvis-network-reactで表示する

こちらは、ゆるWeb勉強会@札幌 Advent Calendar 2021 の 19日目の記事です。

テーマ

Amazon Neptuneからデータを取得し、
Amplifyで作成したサイトでグラフを表示するまでを試します。
※ハンズオンを終え、もう少し触ってみたい方向けです。細かい手順を省略しています。

  • 任意のデータをAmazon Neptuneに保存
  • AWS Amplifyで作成したサイトからNeptuneに接続、データ取得
  • グラフ表示のライブラリはvis.jsベースのvis-network-react 1.3.6

image.png

背景

AWSさんのハンズオンで環境の構築~表示確認までは出来ました。
グラフデータの表示が面白かったので、表示したいグラフデータを少し変更して、表示出来るまでを試してみました。

ハンズオンはこちらです。
コードも用意されているので、流れをつかみたい方にお勧めです。

AWS亀田さんのNeptuneハンズオンです。 Amazon Neptune、AWS Lambda function、Amazon API Gateway、S3の環境を作成し、 S3に配置した静的コンテンツからNeptuneのデータを取得し、グラフデータを表示しています。 JavaScriptのライブラリは無し、グラフ表示のライブラリはvis.js 4.21です。

AWS AmplifyでAmazon Neptuneからデータを取得し表示するサンプルです。 JavaScriptのライブラリはReact、グラフ表示のライブラリはreact-vis-forceです。

グラフ表示ライブラリの設定

今回はvis.jsを使用します。

Reactで作成するため、以下を使用しました。
https://github.com/visjs/vis-network-react

グラフの表示イメージを確認出来るサイトです。
https://visjs.github.io/vis-network/examples/

vis-network-reactのインストール

npm i vis-network-react

ダミーデータの表示

グラフ表示用のコンポーネントに値を渡します。

<VisNetworkReactComponent style={graphStyle} data={data} />

渡すデータは、「ノード:nodes」、「エッジ:edges」という名前で渡します。

ノード(node):別名バーテックス、頂点。点や丸で表現されるエンティティー。「ラベル」を付けて種別を分類することが多い。

エッジ(edge):別名リレーションシップ、辺。ノード間の関係性を表す。方向とタイプを有する。

プロパティ(property):別名、属性。ノードとエッジにおける属性情報。データはkey/value形式で保持される。

引用:グラフデータベースとは何か ~ネットワーク状のデータ構造から瞬時に情報を検索するDBを解説

TestPage.js
import React, { useState, useEffect } from "react";

import { API } from "aws-amplify";
import * as queries from "../graphql/queries";

import VisNetworkReactComponent from "vis-network-react";


const TestPage = () =>{

  // dummy vis-network-react
  const defaultdata = {
    nodes: [
      { id: "user1", label: "佐藤", group: "User" },
      { id: "user2", label: "鈴木", group: "User" },
      { id: "user3", label: "高橋", group: "User" },
      { id: "user4", label: "田中", group: "User" },
      { id: "user5", label: "渡辺", group: "User" },
      { id: "kwd1", label: "AWS", group: "KeyWord", color: "#f7f5db", shape: "box" },
      { id: "kwd2", label: "AWS勉強中", group: "KeyWord", color: "#f7f5db", shape: "box" },
      { id: "kwd3", label: "コンテナ好き", group: "KeyWord", color: "#f7f5db", shape: "box" },
      { id: "kwd4", label: "猫好き", group: "KeyWord", color: "#f7f5db", shape: "box" },
      { id: "kwd5", label: "犬好き", group: "KeyWord", color: "#f7f5db", shape: "box" },
    ],
    edges: [
      { from: "user1", to: "kwd2" },
      { from: "user1", to: "kwd1" },
      { from: "user2", to: "kwd4" },
      { from: "user2", to: "kwd1" },
      { from: "user3", to: "kwd3" },
      { from: "user3", to: "kwd1" },
      { from: "user4", to: "kwd1" },
      { from: "user5", to: "kwd1" },
      { from: "user5", to: "kwd5" },
    ],
  };

  const graphStyle = {
    width: '600px',
    height: '600px',
  };

  const [data, setData] = useState(defaultdata);

  return (
    <>
      <VisNetworkReactComponent
        style={graphStyle}
        data={data}
      />
    </>
  );
}

export default TestPage;

ダミーデータの表示イメージです。
グラフ表示の確認がとれたため、Neptune投入用のデータを作成していきます。

image.png

Neptune用グラフデータの作成

ユーザーと、そのユーザーが興味を持っているキーワードを可視化する想定で
データを作成します。

データ定義

ノード(Vertex)

ノード名 説明
User ユーザー名
KeyWord キーワード名

エッジ(Edge)

エッジ名 Source(矢印の元) Target(矢印の先) 説明
Selected User KeyWord ユーザーが関心のあるキーワード

ノード(User)のデータ

項目名 データ型 説明 備考
~id String ID 一意
name String ユーザー名
~label String ラベル名

ノード(KeyWord)のデータ

項目名 データ型 説明 備考
~id String ID 一意
name String キーワード名
group String キーワードグループ (未使用)
~label String ラベル名

エッジ(Selected)のデータ

項目名 データ型 説明 備考
~id String ID 一意
~label String ノードのプロパティ(name)
~from String 接続元ノードのid
~to String 接続先ノードのid
date Date 登録日付 自動入力

Neptune投入用のデータ作成

作成し、S3バケットに配置します。
AWS CLIコマンドを実行し、データのバルクロードを行います。

Gremlin用のフォーマット

Userノードのサンプル
User.csv
~id,name:String,~label
"usr1","佐藤",User
"usr2","鈴木",User
"usr3","高橋",User
"usr4","田中",User
"usr5","渡辺",User

Keywordノードのサンプル
Keyword.csv
~id,name:String,group:String,~label
"kw1","AWS","1",keyWord
"kw2","AWS勉強中","1",keyWord
"kw3","コンテナ好き","1",keyWord
"kw4","猫好き","2",keyWord
"kw5","犬好き","2",keyWord
"kw6","読書","2",keyWord
"kw7","旅行","2",keyWord
"kw8","音楽","2",keyWord
"kw9","運動","2",keyWord

エッジのサンプル
User_Selected_KeyWord_edges.csv
~id,~label,~from,~to,date:Date
1,Selected,"usr1","kw1",
2,Selected,"usr1","kw4",
3,Selected,"usr1","kw2",
4,Selected,"usr1","kw7",
5,Selected,"usr2","kw1",
6,Selected,"usr2","kw2",
7,Selected,"usr2","kw3",
8,Selected,"usr2","kw6",
9,Selected,"usr3","kw1",
10,Selected,"usr3","kw2",
11,Selected,"usr3","kw5",
12,Selected,"usr3","kw7",
13,Selected,"usr4","kw1",
14,Selected,"usr4","kw5",
15,Selected,"usr4","kw2",
16,Selected,"usr4","kw8",
17,Selected,"usr5","kw1",
18,Selected,"usr5","kw3",
19,Selected,"usr5","kw6",
20,Selected,"usr5","kw9",

Neptuneにデータ投入

以下のコマンドを、sourceのファイル名を書き換えて用意したcsvの回数分実行します。

AWS CLI例(AWS Cloud9などでNeptuneと同一VPC内から実行)

curl -X POST \
-H 'Content-Type: application/json' \
https://Neptuneのエンドポイント:8182/loader -d '
{
"source" : "s3://S3バケット名/読み込むcsvファイル名",
"format" : "csv",
"iamRoleArn" : "NeptuneクラスターにアタッチしたIAMロールのARN",
"region" : "リージョン名(東京はap-northeast-1)",
"failOnError" : "FALSE",
"parallelism" : "MEDIUM",
"updateSingleCardinalityProperties" : "FALSE",
"queueRequest" : "TRUE"
}
'

Neptune Notebooksでデータの投入状況を確認

ハンズオンで使用したノートブックからクエリを実行し、
データの投入状況を確認します。

ノードの確認

g.V().valueMap()

image.png

エッジの確認

g.E().toList()

image.png

無事登録されていました。

データを削除したい場合

データを削除したい場合は、drop()を実行します。
以下は一括削除のコマンドです。ご注意ください。

g.V().drop().iterate()
g.E().drop().iterate()

データ取得用Lambda関数の作成

ノードとエッジの値を個別に取得し、整形したものをjson形式で返却します。

returnData = { nodes: data_nodes, edges: data_edge }
return JSON.stringify(returnData);

ノードはUser、KeyWordの2種類ありますが、
取得したいプロパティ名をnameに揃えているため、1回で取得します(省エネ)。

const nodes = await g.V()
  .limit(100)
  .valueMap()
  .with_(withTokens)
  .toList();

変数に格納する際は、id、labelはそのままでOKでした。
プロパティ(nameなど)は配列になっていたため、name.toString()で値を取得しています。

data_nodes = nodes.map(row => (
  {id: row.id, label: row.name.toString(), group: row.label}
));

Lambda関数の全体像です。
index.js(Lambda関数「getGraphData-staging」)
const gremlin = require('gremlin');

exports.handler = async event => {
  const {DriverRemoteConnection} = gremlin.driver;
  const {Graph} = gremlin.structure;
  const dc = new DriverRemoteConnection(
    `wss://${process.env.NEPTUNE_ENDPOINT}:${process.env.NEPTUNE_PORT}/gremlin`,
    {mimeType: 'application/vnd.gremlin-v2.0+json'}
  );
  const graph = new Graph();
  const g = graph.traversal().withRemote(dc);
  const withTokens = '~tinkerpop.valueMap.tokens';

  try {
    let data_nodes = [];
    let data_edge = [];

    const nodes = await g.V()
      .limit(100)
      .valueMap()
      .with_(withTokens)
      .toList();
    data_nodes = nodes.map(row => (
      {id: row.id, label: row.name.toString(), group: row.label}
    ));

    const edge = await g.E().limit(100).toList();

    data_edge = edge.map((r) => {
      return {from: r.outV.id, to: r.inV.id};
    });

    const returnData = { nodes: data_nodes, edges: data_edge };

    dc.close();
    return JSON.stringify(returnData);

  } catch (error) {
    console.log('ERROR', error);
    dc.close();
  }
};

フロントにNeptuneからのデータ取得処理を追加

GraphQLのAPIを使用して接続します。

TestPage.js
  const [data, setData] = useState({nodes: [], edges: []});

  useEffect(() => {
    fetchMemberStatus();
  }, []);

  // neptuneからデータ取得
  const fetchMemberStatus = async () => {

    const apiData = await API.graphql({
      query: queries.getGraphData,
    });

    setData(JSON.parse(apiData.data.getGraphData));

  };

最終的な表示用コードです
TestPage.js
import React, { useState, useEffect } from "react";

import { API } from "aws-amplify";
import * as queries from "../graphql/queries";

import VisNetworkReactComponent from "vis-network-react";


const TestPage = () =>{

  const graphStyle = {
    width: '600px',
    height: '600px',
  };

  const [data, setData] = useState({nodes: [], edges: []});

  useEffect(() => {
    fetchMemberStatus();
  }, []);

  // neptuneからデータ取得
  const fetchMemberStatus = async () => {

    const apiData = await API.graphql({
      query: queries.getGraphData,
    });

    setData(JSON.parse(apiData.data.getGraphData));

  };

  return (
    <>
      <VisNetworkReactComponent
        style={graphStyle}
        data={data}
      />
    </>
  );
}

export default TestPage;

無事Neptuneから取得したデータが表示されました!

image.png

感想

当初はグラフデータのクエリで必要なデータのみ取得するべく、
Gremlinのクエリとにらめっこしていました。

サンプルを見ると、クエリ自体はシンプルだったため、
データの設計が大事だなと思いました。

(気づきポイント)

  • 取得するプロパティ名を揃えると、クエリがすっきりする
ノード名 変更前 変更後
User userName name
KeyWord name name(変更なし)

変更前:
- 取得項目が異なるため、クエリを分けるか、分岐処理が必要

変更後:
- 1回のクエリで取得可能。判断は別の項目(label)で可能

  • 各ノードのidには文字列を含めると管理が容易
ノード名 変更前 変更後
User 数値(1,2,3,4,5...) 文字列+数値(usr1,usr2,usr3,usr4,usr5...)
KeyWord 数値(1,2,3,4,5...) 文字列+数値(kw1,kw2,kw3,kw4,kw5...)

変更前:
- Edgeで、from、toのidがUserノードのものか、KeyWordノードのものか判断が難しい
- nodesとしてUserとKeyWordの配列をマージする際に重複してしまう

変更後:
- Edgeで、from、toのidが特定しやすい
- UserとKeyWordの配列をマージしてもidが重複しない


試した後は、後始末を忘れずに。
Neptuneは停止か削除しておきましょう。
東京リージョンではdb.t3.mediumで0.15USD/時間(2021/12/19現在)です:purse:

参考資料

お世話になったハンズオン
Amazon Neptune グラフデータ可視化ハンズオン
Amazon NeptuneとAWS Amplifyを利用したグラフアプリケーション開発

Neptuneやデータ構造の検討資料
Amazon Neptune Advanced Design Patterns
20200714 AWS Black Belt Online Seminar Amazon Neptune

Amplifyの資料
Amplify学習リソース集
React アプリケーションの構築

クエリ言語(Gremlin)の資料
TinkerPop Documentation
PRACTICAL GREMLIN: An Apache TinkerPop Tutorial

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?