Server-Side Kotlin & JanusGraph / Cassandraで戯れてみた(前編)

2024/04/13に公開

はじめに

@いけふくろうです。
SIerとWeb系の二刀流です。
自社プロダクト開発、スタートアップ企業様の案件、SI案件、社内業務(広報、HR、社内講師)をおこなっております。

主としては、Webエンジニア / エンジニアリングマネージャーのような役回りで業務をおこなっております。

本記事の前編では、JanusGraphとCassandraの構築についてを執筆しました。

背景

グラフデータベースを知ったのは3年半前くらいでした。
最初聞いた時の自分の頭の中としては、GraphQLのこと??と変換していたのですが、根本的に異なる技術であることが分かり、理解するまでに少し時間がかかりました。

当時、そして今でも、自身の周りでグラフデータベースを利用している話はほとんど耳にしませんでした。しかし、最近プロダクトでJanusGraphを選定している方とお話しする機会があり、その会話をきっかけに、JanusGraphとCassandraについて自身の経験や知見を整理したいと思いました。

対象読者

  • グラフデータベースに興味がある方
  • JanusGraphやCassandraを活用されている方や活用を検討されている方

環境

  • JanusGraph 1.0.0
  • Cassandra 4.1.4

グラフデータベースとは

グラフ構造を備えたデータベースであり、ノード(Vertex)、エッジ(Edge)、プロパティ(Property)の要素によって、ノード間の関係性を表現するデータベースのことです

Vetex・Edge・Property

リレーショナルデータベースでは、データの関係性は正規化をおこなうことで表現することは可能です。
データの関係性を取得するためには、SQLで複数のテーブル間結合が必要になります。

グラフデータベースでは、データ間の関係性はエッジで事前に定義されているため、結合操作が不要になります。エッジを通して、ノード間を迅速に移動するように最適化されているということになります。

という概念ではありますが、グラフデータベースにすれば、シンプルかつ高速化するといった銀の弾丸ではなく、データのカーディナリティー、関係性に対して、最適なスキーマ定義(グラフデータベースの設計)をしなければ、逆に検索が遅くなったりしかねない(実務で経験あり)ので、難しいところだと思っています。

ユースケースとしては、不正検知、レコメンドといったことで活用事例があると目にします。

https://www.publickey1.jp/blog/20/rdbpr.html

JanusGraphとCassandra

JanusGraphは、グラフデータベースエンジンであり、データを永続化させるためのストレージとしては、複数サポートされています。
NoSQLのデータベースであるCassandoraは、その中のひとつです。

ラフなアーキテクチャー図です
アーキテクチャー図

JanusGraphは、Apache TinkerPopというグラフ計算のためのフレームワークを組み込んでいるグラフデータベースエンジンです。
Apache TinkerPopフレームワークには、Gremlinサーバーが含まれており、トラバーサル言語であるGremlinを使用して、データベース内のデータにアクセスすることになります。

Gremlinは複雑なグラフ構造内でのデータ操作やクエリを表現するために設計されており、リレーショナルデータベースでいうところのSQLに相当するものと言えるでしょう。

とうとう、JanusGraphは2023年の10月にv1.0.0がリリースされました!!

https://github.com/JanusGraph/janusgraph/releases/tag/v1.0.0

グラフデータベース設計

今回は、シンプルな内容でスキーマ定義を実施しました

スキーマ定義

UserCategoryProductのノードを定義し、ユーザーの商品の購入回数、閲覧回数をエッジで管理するグラフデータベースの設計です。

用途としては、BigQueryなどのデータウェアハウスに蓄積されているデータをインプットにして、グラフデータベースへ投入し、データ分析をおこなうといった想定をしました。

Dockerコンテナの構築

JanusGraphとCassandraのDockerコンテナ定義

JanusGraphとCassandraのDockerコンテナを構築し、Gremlinを使い、グラフデータベースへアクセスできる環境を構築していきます

docker-compose-cql.yml
version: "3"

services:
  cassandra:
    image: cassandra:3
    container_name: jce-cassandra
    ports:
      - "9042:9042"
      - "9160:9160"
    networks:
      - jce-network

  janusgraph:
    image: docker.io/janusgraph/janusgraph:latest
    container_name: jce-janusgraph
    environment:
      JANUS_PROPS_TEMPLATE: cql
      janusgraph.storage.hostname: jce-cassandra
    volumes:
      - ./docker/graphdb:/docker-entrypoint-initdb.d
    ports:
      - "8182:8182"
    networks:
      - jce-network

networks:
  jce-network:
Justfile
down:
  docker-compose -f "docker-compose-cql.yml" down --rmi all --volumes --remove-orphans

build:
  docker-compose -f "docker-compose-cql.yml" up -d

Dockerコンテナの定義をおこない、Justfile内にコマンドを用意しました

イニシャルデータ生成のスクリプト作成

スキーマ定義初期投入データのスクリプトを作成しました。
リレーショナルデータベースにおけるDDLとDMLに相当します。

スキーマ定義スクリプト

docker/graphdb/0_schema.groovy
graph = JanusGraphFactory.open('/opt/janusgraph/conf/janusgraph-cql-server.properties')

// 管理インターフェースを取得する
mgmt = graph.openManagement()

// Userノード
userId = mgmt.makePropertyKey('userId').dataType(Integer.class).make()
email = mgmt.makePropertyKey('email').dataType(String.class).make()
name = mgmt.makePropertyKey('name').dataType(String.class).make()
age = mgmt.makePropertyKey('age').dataType(String.class).make()
gender = mgmt.makePropertyKey('gender').dataType(String.class).make()
location = mgmt.makePropertyKey('location').dataType(String.class).make()
user = mgmt.makeVertexLabel('User').make()

// Categoryノード
categoryId = mgmt.makePropertyKey('categoryId').dataType(Integer.class).make()
categoryName = mgmt.makePropertyKey('categoryName').dataType(String.class).make()
category = mgmt.makeVertexLabel('Category').make()

// Productノード
productId = mgmt.makePropertyKey('productId').dataType(Integer.class).make()
productName = mgmt.makePropertyKey('productName').dataType(String.class).make()
sellingPrice = mgmt.makePropertyKey('sellingPrice').dataType(Float.class).make()
costPrice = mgmt.makePropertyKey('costPrice').dataType(Float.class).make()
product = mgmt.makeVertexLabel('Product').make()


// PURCHASEDエッジ
purchasedDate = mgmt.makePropertyKey('purchasedDate').dataType(Date.class).make()
purchaseCount = mgmt.makePropertyKey('purchaseCount').dataType(Integer.class).make()
purchased = mgmt.makeEdgeLabel('PURCHASED').make()

// VIEWEDエッジ
viewedDate = mgmt.makePropertyKey('viewedDate').dataType(Date.class).make()
viewCount = mgmt.makePropertyKey('viewCount').dataType(Integer.class).make()
viewed = mgmt.makeEdgeLabel('VIEWED').make()

// BELONGS_TOエッジ
belongs_to = mgmt.makeEdgeLabel('BELONGS_TO').make()

// インデックス作成
mgmt.buildIndex('byUserId', Vertex.class).addKey(userId).unique().buildCompositeIndex()
mgmt.buildIndex('byCategoryId', Vertex.class).addKey(categoryId).unique().buildCompositeIndex()
mgmt.buildIndex('byProductId', Vertex.class).addKey(productId).unique().buildCompositeIndex()

mgmt.commit()
graph.close()

初期投入データスクリプト

docker/graphdb/1_seeds.groovy
import java.text.SimpleDateFormat

graph = JanusGraphFactory.open('/opt/janusgraph/conf/janusgraph-cql-server.properties')
g = graph.traversal()

// Userデータの作成
g.addV('User').property('userId', 1).property('email', 'user1@example.com').property('name', 'User 1').property('age', 21).property('gender', 'male').property('location', '東京都').next()
g.addV('User').property('userId', 2).property('email', 'user2@example.com').property('name', 'User 2').property('age', 22).property('gender', 'female').property('location', '熊本県').next()
g.addV('User').property('userId', 3).property('email', 'user3@example.com').property('name', 'User 3').property('age', 23).property('gender', 'male').property('location', '福岡県').next()

// Categoryデータの作成
g.addV('Category').property('categoryId', 1).property('categoryName', '肉').next()
g.addV('Category').property('categoryId', 2).property('categoryName', '果物').next()
g.addV('Category').property('categoryId', 3).property('categoryName', '調味料').next()

// Productデータの作成
g.addV('Product').property('productId', 1).property('productName', '商品1').property('sellingPrice', 101.0f).property('costPrice', 51.0f).next()
g.addV('Product').property('productId', 2).property('productName', '商品2').property('sellingPrice', 102.0f).property('costPrice', 52.0f).next()
g.addV('Product').property('productId', 3).property('productName', '商品3').property('sellingPrice', 103.0f).property('costPrice', 53.0f).next()
g.addV('Product').property('productId', 4).property('productName', '商品4').property('sellingPrice', 104.0f).property('costPrice', 54.0f).next()
g.addV('Product').property('productId', 5).property('productName', '商品5').property('sellingPrice', 105.0f).property('costPrice', 55.0f).next()


// PURCHASEDエッジの作成
// userId: 1の購入履歴
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 1).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-01T12:00:00')).property('purchaseCount', 1).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 2).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-01T13:00:00')).property('purchaseCount', 2).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 3).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-01T14:00:00')).property('purchaseCount', 3).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 4).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-01T15:00:00')).property('purchaseCount', 4).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 5).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-01T16:00:00')).property('purchaseCount', 5).iterate()

// userId: 2の購入履歴
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 1).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-02T12:00:00')).property('purchaseCount', 2).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 2).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-02T13:00:00')).property('purchaseCount', 4).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 3).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-02T14:00:00')).property('purchaseCount', 6).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 4).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-02T15:00:00')).property('purchaseCount', 8).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 5).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-02T16:00:00')).property('purchaseCount', 10).iterate()

// userId: 3の購入履歴
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 1).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-03T12:00:00')).property('purchaseCount', 3).iterate()
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 2).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-03T12:00:00')).property('purchaseCount', 6).iterate()
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 3).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-03T12:00:00')).property('purchaseCount', 9).iterate()
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 4).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-03T12:00:00')).property('purchaseCount', 12).iterate()
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 5).addE('PURCHASED').from('u').property('purchasedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2024-01-03T12:00:00')).property('purchaseCount', 15).iterate()


// VIEWEDエッジの作成
// userId: 1の閲覧履歴
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 1).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-01T12:00:00')).property('viewCount', 5).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 2).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-02T12:00:00')).property('viewCount', 10).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 3).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-03T12:00:00')).property('viewCount', 15).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 4).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-04T12:00:00')).property('viewCount', 20).iterate()
g.V().has('User', 'userId', 1).as('u').V().has('Product', 'productId', 5).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-05T12:00:00')).property('viewCount', 25).iterate()

// userId: 2の閲覧履歴
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 1).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-11T12:00:00')).property('viewCount', 5).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 2).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-12T12:00:00')).property('viewCount', 10).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 3).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-13T12:00:00')).property('viewCount', 15).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 4).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-14T12:00:00')).property('viewCount', 20).iterate()
g.V().has('User', 'userId', 2).as('u').V().has('Product', 'productId', 5).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-15T12:00:00')).property('viewCount', 25).iterate()

// userId: 3の閲覧履歴
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 1).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-21T12:00:00')).property('viewCount', 5).iterate()
g.V().has('User', 'userId', 3).as('u').V().has('Product', 'productId', 2).addE('VIEWED').from('u').property('viewedDate', new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse('2023-12-22T12:00:00')).property('viewCount', 10).iterate()


// BELONGS_TOエッジの作成
g.V().has('Product', 'productId', 1).as('p').V().has('Category', 'categoryId', 1).addE('BELONGS_TO').from('p').iterate()
g.V().has('Product', 'productId', 2).as('p').V().has('Category', 'categoryId', 2).addE('BELONGS_TO').from('p').iterate()
g.V().has('Product', 'productId', 3).as('p').V().has('Category', 'categoryId', 3).addE('BELONGS_TO').from('p').iterate()
g.V().has('Product', 'productId', 4).as('p').V().has('Category', 'categoryId', 1).addE('BELONGS_TO').from('p').iterate()
g.V().has('Product', 'productId', 5).as('p').V().has('Category', 'categoryId', 2).addE('BELONGS_TO').from('p').iterate()

g.tx().commit()
graph.close()
:remote connect tinkerpop.server conf/remote.yaml session

// 管理インターフェースを取得する
:> mgmt = graph.openManagement()

// Userノード
:> userId = mgmt.makePropertyKey('userId').dataType(Integer.class).make()
:> email = mgmt.makePropertyKey('email').dataType(String.class).make()
...
:> mgmt.commit()

:remote connect tinkerpop.server conf/remote.yaml session
Gremlinサーバーのセッションに接続させた上で、記述のコマンドを実行させています。
sessionを付与することで、トランザクションを自身で管理する必要があり、ロールバックが可能であることとデータの変更を反映させる場合には、明示的にコミットが必要となります。

:>は、:submitコマンドの省略形です。
Gremlin Consoleの公式ドキュメントもしくは:helpコマンドで確認できます

:submit (:> ) Send a Gremlin script to Gremlin Server

gremlin> :help

For information about Groovy, visit:
    http://groovy-lang.org

Available commands:
  :help       (:h  ) Display this help message
  ?           (:?  ) Alias to: :help
  :exit       (:x  ) Exit the shell
  :quit       (:q  ) Alias to: :exit
  import      (:i  ) Import a class into the namespace
  :display    (:d  ) Display the current buffer
  :clear      (:c  ) Clear the buffer and reset the prompt counter
  :show       (:S  ) Show variables, classes or imports
  :inspect    (:n  ) Inspect a variable or the last result with the GUI object browser
  :purge      (:p  ) Purge variables, classes, imports or preferences
  :edit       (:e  ) Edit the current buffer
  :load       (:l  ) Load a file or URL into the buffer
  .           (:.  ) Alias to: :load
  :save       (:s  ) Save the current buffer to a file
  :record     (:r  ) Record the current session to a file
  :history    (:H  ) Display, manage and recall edit-line history
  :alias      (:a  ) Create an alias
  :grab       (:g  ) Add a dependency to the shell environment
  :register   (:rc ) Register a new command with the shell
  :doc        (:D  ) Open a browser window displaying the doc for the argument
  :set        (:=  ) Set (or list) preferences
  :uninstall  (:-  ) Uninstall a Maven library and its dependencies from the Gremlin Console
  :install    (:+  ) Install a Maven library and its dependencies into the Gremlin Console
  :plugin     (:pin) Manage plugins for the Console
  :remote     (:rem) Define a remote connection
  :submit     (:>  ) Send a Gremlin script to Gremlin Server
  :bytecode   (:bc ) Gremlin bytecode helper commands
  :cls        (:C  ) Clear the screen.

For help on a specific command type:
    :help command

gremlin>

Gremlinのコマンドやノードとエッジのデータ構造の理解については、下記が参考になりました

https://tinkerpop.apache.org/docs/current/reference/

https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html

https://dkuppitz.github.io/gremlin-cheat-sheet/101.html

Dockerコンテナ構築

 % just build
docker-compose -f "docker-compose-cql.yml" up -d
[+] Running 2/2
 ⠿ Container jce-cassandra   Started        0.6s
 ⠿ Container jce-janusgraph  Started        0.6s
% docker-compose -f "docker-compose-cql.yml" ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
jce-cassandra       "docker-entrypoint.s…"   cassandra           running             7000-7001/tcp, 0.0.0.0:9042->9042/tcp, 7199/tcp, 0.0.0.0:9160->9160/tcp
jce-janusgraph      "docker-entrypoint.s…"   janusgraph          running             0.0.0.0:8182->8182/tcp
%

スキーマ定義とデータが投入されているかを確認するために、Gremlinコンソールを起動します

% docker exec -it jce-janusgraph ./bin/gremlin.sh

         \,,,/
         (o o)
-----oOOo-(3)-oOOo-----
...
gremlin>

管理タスクを実行できるようにした上で、スキーマ定義を出力します

gremlin> :remote connect tinkerpop.server conf/remote.yaml session
...
gremlin> :remote console
...
gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@2cf37194
gremlin> mgmt.printSchema()
==>------------------------------------------------------------------------------------------------
Vertex Label Name              | Partitioned | Static                                             |
---------------------------------------------------------------------------------------------------
User                           | false       | false                                              |
Category                       | false       | false                                              |
Product                        | false       | false                                              |
---------------------------------------------------------------------------------------------------
Edge Label Name                | Directed    | Unidirected | Multiplicity                         |
---------------------------------------------------------------------------------------------------
PURCHASED                      | true        | false       | MULTI                                |
VIEWED                         | true        | false       | MULTI                                |
BELONGS_TO                     | true        | false       | MULTI                                |
---------------------------------------------------------------------------------------------------
Property Key Name              | Cardinality | Data Type                                          |
---------------------------------------------------------------------------------------------------
purchasedDate                  | SINGLE      | class java.util.Date                               |
purchaseCount                  | SINGLE      | class java.lang.Integer                            |
viewedDate                     | SINGLE      | class java.util.Date                               |
viewCount                      | SINGLE      | class java.lang.Integer                            |
userId                         | SINGLE      | class java.lang.Integer                            |
email                          | SINGLE      | class java.lang.String                             |
name                           | SINGLE      | class java.lang.String                             |
age                            | SINGLE      | class java.lang.String                             |
gender                         | SINGLE      | class java.lang.String                             |
location                       | SINGLE      | class java.lang.String                             |
categoryId                     | SINGLE      | class java.lang.Integer                            |
categoryName                   | SINGLE      | class java.lang.String                             |
productId                      | SINGLE      | class java.lang.Integer                            |
productName                    | SINGLE      | class java.lang.String                             |
sellingPrice                   | SINGLE      | class java.lang.Float                              |
costPrice                      | SINGLE      | class java.lang.Float                              |
---------------------------------------------------------------------------------------------------
Graph Index (Vertex)           | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
byUserId                       | Composite   | true      | internalindex  | userId:       ENABLED |
byCategoryId                   | Composite   | true      | internalindex  | categoryId:    ENABLED |
byProductId                    | Composite   | true      | internalindex  | productId:    ENABLED |
---------------------------------------------------------------------------------------------------
Graph Index (Edge)             | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
Relation Index (VCI)           | Type        | Direction | Sort Key       | Order    |     Status |
---------------------------------------------------------------------------------------------------

gremlin>

mgmt.printSchema()にて、ノード(Vertex)、ノードのプロパティー情報、エッジが期待する通りに構築されていることが確認できます

Gremlinのコマンドを使ってデータを確認

投入したデータを確認します

ラベルごとの件数取得

gremlin> g.V().groupCount().by(label)
==>{User=3, Category=3, Product=5}
gremlin>

各ラベルのVertex一覧取得

gremlin> g.V().hasLabel('User').elementMap()
==>{userId=1, email=user1@example.com, name=User 1, age=21, gender=male, location=東京都, id=4120, label=User}
==>{userId=3, email=user3@example.com, name=User 3, age=23, gender=male, location=福岡県, id=4176, label=User}
==>{userId=2, email=user2@example.com, name=User 2, age=22, gender=female, location=熊本県, id=4224, label=User}

gremlin> g.V().hasLabel('Category').elementMap()
==>{categoryId=2, categoryName=果物, id=4200, label=Category}
==>{categoryId=3, categoryName=調味料, id=8320, label=Category}
==>{categoryId=1, categoryName=肉, id=4240, label=Category}

gremlin> g.V().hasLabel('Product').elementMap()
==>{productId=1, productName=商品1, sellingPrice=101.0, costPrice=51.0, id=4184, label=Product}
==>{productId=2, productName=商品2, sellingPrice=102.0, costPrice=52.0, id=12416, label=Product}
==>{productId=3, productName=商品3, sellingPrice=103.0, costPrice=53.0, id=8336, label=Product}
==>{productId=5, productName=商品5, sellingPrice=105.0, costPrice=55.0, id=12432, label=Product}
==>{productId=4, productName=商品4, sellingPrice=104.0, costPrice=54.0, id=4304, label=Product}
gremlin>

ユーザーが購入した商品と回数を取得

gremlin> g.V().hasLabel('User').as('user').outE('PURCHASED').as('purchase').inV().as('product').select('user', 'purchase', 'product').by('userId').by('purchaseCount').by(valueMap('productId', 'productName')).order().by(select('user'), asc).by(select('product').by('productId'), asc)
==>{user=1, purchase=1, product={productId=[1], productName=[商品1]}}
==>{user=1, purchase=2, product={productId=[2], productName=[商品2]}}
==>{user=1, purchase=3, product={productId=[3], productName=[商品3]}}
==>{user=1, purchase=4, product={productId=[4], productName=[商品4]}}
==>{user=1, purchase=5, product={productId=[5], productName=[商品5]}}
==>{user=2, purchase=2, product={productId=[1], productName=[商品1]}}
==>{user=2, purchase=4, product={productId=[2], productName=[商品2]}}
==>{user=2, purchase=6, product={productId=[3], productName=[商品3]}}
==>{user=2, purchase=8, product={productId=[4], productName=[商品4]}}
==>{user=2, purchase=10, product={productId=[5], productName=[商品5]}}
==>{user=3, purchase=3, product={productId=[1], productName=[商品1]}}
==>{user=3, purchase=6, product={productId=[2], productName=[商品2]}}
==>{user=3, purchase=9, product={productId=[3], productName=[商品3]}}
==>{user=3, purchase=12, product={productId=[4], productName=[商品4]}}
==>{user=3, purchase=15, product={productId=[5], productName=[商品5]}}
gremlin>

ユーザーが閲覧した商品と回数を取得

gremlin> g.V().hasLabel('User').as('user').outE('VIEWED').as('view').inV().as('product').select('user', 'view', 'product').by('userId').by('viewCount').by(valueMap('productId', 'productName')).order().by(select('user'), asc).by(select('product').by('productId'), asc)
==>{user=1, view=5, product={productId=[1], productName=[商品1]}}
==>{user=1, view=10, product={productId=[2], productName=[商品2]}}
==>{user=1, view=15, product={productId=[3], productName=[商品3]}}
==>{user=1, view=20, product={productId=[4], productName=[商品4]}}
==>{user=1, view=25, product={productId=[5], productName=[商品5]}}
==>{user=2, view=5, product={productId=[1], productName=[商品1]}}
==>{user=2, view=10, product={productId=[2], productName=[商品2]}}
==>{user=2, view=15, product={productId=[3], productName=[商品3]}}
==>{user=2, view=20, product={productId=[4], productName=[商品4]}}
==>{user=2, view=25, product={productId=[5], productName=[商品5]}}
==>{user=3, view=5, product={productId=[1], productName=[商品1]}}
==>{user=3, view=10, product={productId=[2], productName=[商品2]}}
gremlin>

カテゴリーに属する商品情報

gremlin> g.V().hasLabel('Category').as('category').in('BELONGS_TO').as('product').select('category', 'product').by(elementMap()).by(elementMap()).order().by(select('category').by('categoryId'), asc).by(select('product').by('productId'), asc)
==>{category={categoryId=1, categoryName=肉, id=4240, label=Category}, product={productId=1, productName=商品1, sellingPrice=101.0, costPrice=51.0, id=4184, label=Product}}
==>{category={categoryId=1, categoryName=肉, id=4240, label=Category}, product={productId=4, productName=商品4, sellingPrice=104.0, costPrice=54.0, id=4304, label=Product}}
==>{category={categoryId=2, categoryName=果物, id=4200, label=Category}, product={productId=2, productName=商品2, sellingPrice=102.0, costPrice=52.0, id=12416, label=Product}}
==>{category={categoryId=2, categoryName=果物, id=4200, label=Category}, product={productId=5, productName=商品5, sellingPrice=105.0, costPrice=55.0, id=12432, label=Product}}
==>{category={categoryId=3, categoryName=調味料, id=8320, label=Category}, product={productId=3, productName=商品3, sellingPrice=103.0, costPrice=53.0, id=8336, label=Product}}
gremlin>

Cassandraの中身

既述のスクリプトでデータ投入の確認ができましたが、果たしてデータの永続化が役割となるCassandraデータベースにデータが存在するか気になりましたので、確認してみます

コンテナに入って、cqlshを使って、キースペース、テーブル、レコードの確認をおこないます

% docker exec -it jce-cassandra /bin/bash
root@76ffa212796a:/# cqlsh
Connected to Test Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.16 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh> describe keyspaces ;

system_schema  system      system_distributed
system_auth    janusgraph  system_traces

cqlsh>

キースペースにjanusgraphが存在していればOKです。
Janusgraphがバックエンドとして、Cassandraを指定した場合に作成されます。
なお、キースペース名は、Janusgraphのコンテナ内にある以下の設定ファイル内に定義されています。

/opt/janusgraph/conf/janusgraph-cql-server.properties
$ cat janusgraph-cql-server.properties
...
# The name of JanusGraph's keyspace.  It will be created if it does not
# exist.
#
# Default:    janusgraph
# Data Type:  String
# Mutability: LOCAL
storage.cql.keyspace = janusgraph
...

janusgraphのキースペースにて、テーブルとレコードの確認をおこないます

cqlsh> use janusgraph ;
cqlsh:janusgraph> describe tables ;

edgestore_lock_  graphindex_lock_         janusgraph_ids
txlog            systemlog                graphindex
edgestore        system_properties_lock_  system_properties

cqlsh:janusgraph>

スキーマ定義、ノードやエッジを登録すると、以下のテーブルに追加されるようです
janusgraph_idsedgestoregraphindex

cqlsh:janusgraph> select * from janusgraph_ids ;

 key                | column1                                                                              | value
--------------------+--------------------------------------------------------------------------------------+-------
 0x0000000000000003 | 0xfffffffffffec77f000615ef5cacbbd761633139303030333336352d62613562663964363266306531 |    0x
...

cqlsh:janusgraph> select count(*) from janusgraph_ids ;

 count
-------
    15

(1 rows)

cqlsh:janusgraph> select * from graphindex ;

 key                                      | column1          | value
------------------------------------------+------------------+----------
                                 0x328986 |             0x00 |   0x61b0
                                 0x308984 |             0x00 |   0x21b0
                 0x10a5a072741e656d6169ec |             0x00 |   0x1085
...

cqlsh:janusgraph> select count(*) from graphindex ;

 count
-------
    61

(1 rows)

cqlsh:janusgraph> select * from edgestore ;

 key                | column1            | value
--------------------+--------------------+--------------------------------------------
 0x0000000000004815 |               0x02 |                               0x0001053880
...

cqlsh:janusgraph> select count(*) from edgestore ;

 count
-------
   393

(1 rows)

解読することは困難ですが、データが存在することは確認できました

おわりに

当時、JanusGraphとCassandraについての情報が少ないこともあり、キャッチアップや詰まった際の問題解決に時間を要しました。

アーキテクチャーの概念、スキーマ定義、構築といった一気通貫の記事があるとキャッチアップの手助けになると考え、こちらの記事を執筆してみました。
執筆を通じて、自分自身も今まで言語化できていなかった点や何となくやっていた点などが浮き彫りになり、学び直すことにより、埋めることができました。

後編では、こちらの記事で構築したグラフデータベースに対して、Spring Boot+KotlinでAPI実装を試してみたいと思います。

以上です。
本記事が何かの一助になれば幸いです。

Discussion