TinkerPop でグラフ操作 - Groovy
前回、Neo4j の Cypher を使って実施したグラフ操作を Apache TinkerPop を使って Groovy (@Grab
を使用)で実装してみました。
- Groovy 2.5.0 beta1
- Apache TinkerPop 3.2
Apache TinkerPop はグラフ処理のためのフレームワークで、Neo4j 等の様々なグラフ DB ※ に対して共通のインターフェースを提供します。
※ グラフ DB だけではなく、 Cassandra、HBase、DynamoDB 等をサポートする ライブラリも提供されています
ソースは http://github.com/fits/try_samples/tree/master/blog/20170718/
a. 設定ファイル
TinkerPop にはインメモリーの TinkerGraph が用意されていますが、前回と同様に Neo4j を使う事にします。
ただ、前回と違って Neo4j をサーバー起動せずに組み込み実行します。
TinkerPop には Graph
オブジェクトを汎用的に生成する手段として org.apache.tinkerpop.gremlin.structure.util.GraphFactory
が用意されています。
GraphFactory.open(<設定ファイル>)
を使えば、依存ライブラリと設定ファイルを差し替えて DB を切り替える事もできそうなので、今回はこの方法を使います。
Neo4j を組み込み利用する場合、gremlin.graph
に org.apache.tinkerpop.gremlin.neo4j.structure.Neo4jGraph
を設定して gremlin.neo4j.directory
に DB ファイルを出力するディレクトリを指定します。 (Neo4j をサーバー起動した場合の data/databases/graph.db)
setting.properties
gremlin.graph=org.apache.tinkerpop.gremlin.neo4j.structure.Neo4jGraph gremlin.neo4j.directory=neo4jdb
b. グラフデータ作成
前回 と同様のデータを作成する処理を実装してみます。
Groovy で実行する際の注意点として、neo4j-tinkerpop-api-impl 等は依存ライブラリとして Groovy 2.4.11 のライブラリを含んでおり、このバージョン以外の groovy コマンドで実行すると org.codehaus.groovy.control.MultipleCompilationErrorsException
が発生してしまいます。
そこで今回は、Groovy 2.5.0 beta1 で実行できるように @GrabExclude
を使って groovy-xml
等を除くようにしています。
グラフの操作は GraphFactory.open()
で取得した Graph
オブジェクトに対して実施します。
ノードの追加は addVertex
、エッジの追加は addEdge
メソッドで行う事ができ、property
メソッドで任意の属性を設定できます。
トランザクションは tx
メソッドで開始します。※
※ TinkerGraph のように tx メソッドをサポートしていないものもありますので (その場合に tx() を呼び出すとエラーになる) 実際は tx のサポート有無をチェックしてから 呼び出すようにした方が安全だと思います (例) if (g.features().graph().supportsTransactions()) { g.tx().withAutoCloseable { t -> ・・・ } }
add-data.groovy
@Grapes([ @Grab('org.apache.tinkerpop:neo4j-gremlin:3.2.5'), @Grab('org.neo4j:neo4j-tinkerpop-api-impl:0.6-3.2.2'), @Grab('org.slf4j:slf4j-nop:1.7.25'), @GrabExclude('org.codehaus.groovy:groovy-xml'), @GrabExclude('org.codehaus.groovy:groovy-swing'), @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() } }
withAutoCloseable
は AutoCloseable
のリソースをクローズするための Groovy の機能です(TinkerPop の API ではありません)
実行結果
> groovy add-data.groovy setting.properties
c. 経路の探索
前回 の経路探索の処理を TinkerPop の API で実装してみます。
traversal().V()
でノードを対象とした GraphTraversal<Vertex,Vertex>
を取得でき、has(<プロパティ名>, <プロパティ値>)
等を使ってノードの条件を指定できます。
(c-1) 複数エッジ(条件なし)
まず、エッジは気にせずに指定ノードから指定ノードまでのパス(経路)を取得する処理を実装してみます。
終点のノードまで複数のノードで繋がっている場合は repeat(・・・).until(<終点ノードの条件>)
で探せます。
エッジの条件を指定しない場合は repeat(__.out()).until(・・・)
で取得できます。(__
はクラス名です)
パスを取得するには path()
を使います。
Path
からは objects()
でパスに含まれるノード(やエッジ)を List<Object>
で取得できます。
なお、以下の処理でトランザクションは必要ありませんが、一応トランザクションを使っています。 (JanusGraph をマルチスレッドで使うケースではトランザクションが必要になったので)
find-data-simple.groovy
@Grapes([ @Grab('org.apache.tinkerpop:neo4j-gremlin:3.2.5'), @Grab('org.neo4j:neo4j-tinkerpop-api-impl:0.6-3.2.2'), @Grab('org.slf4j:slf4j-nop:1.7.25'), @GrabExclude('org.codehaus.groovy:groovy-xml'), @GrabExclude('org.codehaus.groovy:groovy-swing'), @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(__.out()) .until(__.has('oid', end)) // 終点ノードの条件 .path() p.each { println it.objects().collect(toStr).join(' -> ') } } }
実行結果
user2 から service2.post へのパスを取得してみます。
> groovy find-data-simple.groovy setting.properties user2 service2.post User[2]{vp[oid->user2], vp[name->user2]} -> Group[4]{vp[oid->group1], vp[name->group1]} -> Operation[9]{vp[oid->service2.post], vp[name->post]}
__.out()
を使った事で、パスにはノードの情報だけが含まれエッジの情報を含んでいません。
(c-2) 複数エッジ(条件あり)
エッジの条件を指定するには repeat
でエッジも指定します。
A ノードと B ノードが C エッジで繋がっている (A)-[C]->(B)
のような状態で、A の __.outE()
が C エッジで、C エッジの inV()
が B ノードとなります。
そのため、repeat(__.outE().as('e').inV())
とすれば、複数の外向きエッジで繋がっているノードを検索する事ができます。
ここで as(<ステップラベル名>)
を使って、該当するエッジにラベル名を付けておき、where
での条件判定(PERMIT エッジを含むかどうか)に使います。
__.select('e')
の結果は GraphTraversal<Vertex, ArrayList<Edge>>
のようになるので __.select('e').hasLabel(・・・)
とはできません。 (ClassCastException になります)
unfold()
で GraphTraversal<Vertex, ArrayListGraphTraversal<Vertex, Edge>
にして hasLabel
を使えば、該当のラベルを持つエッジを含んでいるかどうかを条件判定できます。
find-data.groovy
・・・ 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')) // PERMIT エッジを含んでいるかどうかの判定 .path() p.each { println it.objects().collect(toStr).join(' -> ') } } }
実行結果
> groovy find-data.groovy setting.properties user2 service2.post User[2]{vp[oid->user2], vp[name->user2]} -> PART_OF[4]{} -> Group[4]{vp[oid->group1], vp[name->group1]} -> PERMIT[10]{} -> Operation[9]{vp[oid->service2.post], vp[name->post]}
__.outE()
を使った事でエッジの情報もパスに含まれるようになりました。