JanusGraph でグラフ操作 - Groovy

TinkerPop の API と互換性があり Cassandra 等をストレージとして使用できる JanusGraph というグラフデータベースがあります。

今回は、「TinkerPop でグラフ操作 - Groovy」 のサンプルコードを JanusGraph 用に変更してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20170814/

はじめに

今回はグラフデータの保存先として Cassandra を使うため、Cassandra を事前に実行しておきます。

a. 設定ファイル

TinkerPop の org.apache.tinkerpop.gremlin.structure.util.GraphFactory を使用する場合、gremlin.graphorg.janusgraph.core.JanusGraphFactory を設定し、JanusGraph 用の設定(storage.backend 等)を加えれば JanusGraph で使えます。

Cassandra を使う場合は storage.backendcassandra を設定します。

setting.properties
gremlin.graph=org.janusgraph.core.JanusGraphFactory

storage.backend=cassandra
storage.hostname=127.0.0.1

b. グラフデータ作成

JanusGraph は TinkerPop API と互換性があるため 前回 の処理内容を特に変える必要はなく、@Grapes の依存ライブラリ構成を JanusGraph 用に変えるだけです。

add-data.groovy
// 前回との違いは @Grapes の内容のみ
@Grapes([
    @Grab('org.janusgraph:janusgraph-cassandra:0.1.1'),
    @Grab('org.slf4j:slf4j-simple:1.7.25'),
    @GrabExclude('xml-apis#xml-apis;1.3.04'),
    @GrabExclude('com.github.jeremyh#jBCrypt;jbcrypt-0.4'),
    @GrabExclude('org.slf4j#slf4j-log4j12'),
    @GrabExclude('ch.qos.logback#logback-classic'),
    @GrabExclude('org.codehaus.groovy:groovy-swing'),
    @GrabExclude('org.codehaus.groovy:groovy-xml'),
    @GrabExclude('org.codehaus.groovy:groovy-jsr223')
])
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory

def conf = args[0]

def addNode = { g, type, id, name = id ->
    def res = g.addVertex(type)

    res.property('oid', id)
    res.property('name', name)

    res
}

def createData = { g -> 
    def p = addNode(g, 'Principals', 'principals')

    def u1 = addNode(g, 'User', 'user1')
    def u2 = addNode(g, 'User', 'user2')
    def ad = addNode(g, 'User', 'admin')

    def g1 = addNode(g, 'Group', 'group1')

    [u1, u2, ad, g1].each {
        it.addEdge('PART_OF', p)
    }

    u2.addEdge('PART_OF', g1)

    def r = addNode(g, 'Resources', 'resources')

    def s1 = addNode(g, 'Service', 'service1')

    def s2 = addNode(g, 'Service', 'service2')
    def s2o1 = addNode(g, 'Operation', 'service2.get', 'get')
    def s2o2 = addNode(g, 'Operation', 'service2.post', 'post')

    [s2o1, s2o2].each {
        s2.addEdge('METHOD', it)
    }

    [s1, s2].each {
        r.addEdge('RESOURCE', it)
    }

    u1.addEdge('PERMIT', s1)
    g1.addEdge('PERMIT', s2o2)
    ad.addEdge('PERMIT', r)
}

GraphFactory.open(conf).withAutoCloseable { g ->
    g.tx().withAutoCloseable { tx ->
        createData(g)

        tx.commit()
    }
}
実行結果
> groovy add-data.groovy setting.properties

・・・
[main] INFO com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor - AddHost: 127.0.0.1
[main] INFO org.janusgraph.diskstorage.Backend - Initiated backend operations thread pool of size 8
[main] INFO org.janusgraph.diskstorage.log.kcvs.KCVSLog - Loaded unidentified ReadMarker start time 2017-08-12T14:17:38.078Z into org.janusgraph.diskstorage.log.kcvs.KCVSLog$MessagePuller@3bdb2c78

c. 経路の探索

こちらも @Grapes の内容を変えるだけです。(確認のためログ出力するようにしています)

find-data.groovy
// 前回との違いは @Grapes の内容のみ
@Grapes([
    @Grab('org.janusgraph:janusgraph-cassandra:0.1.1'),
    //@Grab('org.slf4j:slf4j-nop:1.7.25'),
    @Grab('org.slf4j:slf4j-simple:1.7.25'),
    @GrabExclude('xml-apis#xml-apis;1.3.04'),
    @GrabExclude('com.github.jeremyh#jBCrypt;jbcrypt-0.4'),
    @GrabExclude('org.slf4j#slf4j-log4j12'),
    @GrabExclude('ch.qos.logback#logback-classic'),
    @GrabExclude('org.codehaus.groovy:groovy-swing'),
    @GrabExclude('org.codehaus.groovy:groovy-xml'),
    @GrabExclude('org.codehaus.groovy:groovy-jsr223')
])
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__

def conf = args[0]
def start = args[1]
def end = args[2]

def toStr = {
    "${it.label()}[${it.id()}]{${it.properties().join(', ')}}"
}

GraphFactory.open(conf).withAutoCloseable { g ->
    g.tx().withAutoCloseable {
        def p = g.traversal().V()
            .has('oid', start)
            .repeat(__.outE().as('e').inV())
            .until(__.has('oid', end))
            .where(__.select('e').unfold().hasLabel('PERMIT'))
            .path()

        p.each {
            println it.objects().collect(toStr).join(' -> ')
        }
    }
}
実行結果
> groovy find-data.groovy setting.properties user2 service2.post

・・・
[main] WARN org.janusgraph.graphdb.transaction.StandardJanusGraphTx - Query requires iterating over all vertices [(oid = user2)]. For better performance, use indexes
・・・
User[4304]{vp[oid->user2], vp[name->user2]} -> PART_OF[4d6-3bk-4r9-3ao]{} -> Group[4272]{vp[oid->group1], vp[name->group1]} -> PERMIT[6c6-3ao-9hx-37c]{} -> Operation[4152]{vp[oid->service2.post], vp[name->post]}

インデックスを使った方が性能的に望ましいとの警告ログが出力されました。

d. インデックスの作成

警告ログを解消するためにインデックスを使ってみます。

TinkerPop で(汎用的な)インデックス作成 API を見つけられなかったので、インデックス作成に関しては JanusGraph の専用 API を使う必要がありそうです。

JanusGraph のインデックス機能には以下の 2通りがあり、指定のプロパティ値を持つノードやエッジを見つける用途には (a) を使えば良さそうです。

  • (a) Graph Indexes
  • (b) Vertex-centric Indexes

そして、(a) では以下のようなインデックスを使えます。

名称 概要 生成メソッド名
Composite Index 外部のインデックスエンジンを使わずに高速 buildCompositeIndex
Mixed Index 外部のインデックスエンジン(Elasticsearch 等)を使って数値の範囲検索や全文検索などが可能 buildMixedIndex

今回は oid プロパティに対して (a) の Composite Index を作ってみます。

(a) Graph Indexes は JanusGraphManagementbuildIndex メソッドで作成します。 その戻り値 IndexBuilderbuildCompositeIndex メソッドで Composite Index となります。 (ユニークインデックスとする場合は unique メソッドを呼び出します)

ここで、インデックスを作成しただけでは、既存データのインデックス化は行われないようなので、インデックス作成後に既存データのインデックス化も行うようにしています。

create_index.groovy
@Grapes([
    @Grab('org.janusgraph:janusgraph-cassandra:0.1.1'),
    @Grab('org.slf4j:slf4j-simple:1.7.25'),
    @GrabExclude('xml-apis#xml-apis;1.3.04'),
    @GrabExclude('com.github.jeremyh#jBCrypt;jbcrypt-0.4'),
    @GrabExclude('org.slf4j#slf4j-log4j12'),
    @GrabExclude('ch.qos.logback#logback-classic'),
    @GrabExclude('org.codehaus.groovy:groovy-swing'),
    @GrabExclude('org.codehaus.groovy:groovy-xml'),
    @GrabExclude('org.codehaus.groovy:groovy-jsr223')
])
import org.janusgraph.core.JanusGraphFactory
import org.janusgraph.core.schema.SchemaAction
import org.apache.tinkerpop.gremlin.structure.Vertex

def conf = args[0]

def graph = JanusGraphFactory.open(conf)

graph.withAutoCloseable { g ->
    // JanusGraphManagement の取得
    def manage = g.openManagement()

    def oidKey = manage.getPropertyKey('oid')

    // インデックスが未作成の場合にインデックスを作成
    if (manage.getGraphIndex('oidIndex') == null) {
        // oid を対象とした Composite Index の作成
        manage.buildIndex('oidIndex', Vertex)
            .addKey(oidKey)
            .buildCompositeIndex()

        manage.commit()
        // インデックス作成の完了待ち
        manage.awaitGraphIndexStatus(g, 'oidIndex').call()
    }

    manage = g.openManagement()
    // 既存データのインデックス化
    manage.updateIndex(manage.getGraphIndex('oidIndex'), SchemaAction.REINDEX).get()

    manage.commit()
}
インデックス作成結果
> groovy create_index.groovy setting.properties

・・・
[pool-19-thread-1] INFO org.janusgraph.graphdb.database.management.ManagementSystem$UpdateStatusTrigger - Set status REGISTERED on schema element oidIndex with property keys []
[pool-19-thread-1] INFO org.janusgraph.graphdb.database.management.ManagementLogger - Received all acknowledgements for eviction [1]
・・・
[Thread-3] INFO com.netflix.astyanax.thrift.ThriftKeyspaceImpl - Detected partitioner org.apache.cassandra.dht.Murmur3Partitioner for keyspace janusgraph
[Thread-7] INFO org.janusgraph.graphdb.olap.job.IndexRepairJob - Found index oidIndex
[Thread-3] INFO org.janusgraph.graphdb.database.management.ManagementSystem - Index update job successful for [oidIndex]

これで警告ログは出力されなくなりました。

経路探索の実行結果
> groovy find-data.groovy setting.properties user2 service2.post

・・・
User[4304]{vp[oid->user2], vp[name->user2]} -> PART_OF[4d6-3bk-4r9-3ao]{} -> Group[4272]{vp[oid->group1], vp[name->group1]} -> PERMIT[6c6-3ao-9hx-37c]{} -> Operation[4152]{vp[oid->service2.post], vp[name->post]}