Help us understand the problem. What is going on with this article?

JanusGraphでインデックスを使う

JanusGraphでのインデックス(Composite Index)の張り方を説明する。
今回はインデックス・バックエンドが必要なMixed Indexについては触れない。

英語読めるなら公式のドキュメント読めばいいのだけれども。
Indexing for Better Performance

インデックスがない場合のクエリ応答時間

お試し用のデータとして、以下のようなグラフを作成する。グラフというよりはただのテーブルである。頂点数は1000,10000,100000の3パターンを試した。

ID (Property)name (Property)age
4235272 A0000003 3
4239368 A0000004 43
4243464 A0000011 94
4247560 A0000024 77
4251656 A0000025 22
...

nameはA0000001から連番で設定される。ageは0~99までランダムに与えられるようにした。なお、10万頂点の場合、登録に10分くらいかかる。

以下の、特定の頂点を1個だけ取り出す簡単なクエリを発行する。

g.V().has("name", "A0000001").valueMap()

プログラム
from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
from gremlin_python.process.traversal import T, P, TextP
import random
import timeit
import sys


domain = "localhost"  # GremlinサーバーのIPアドレスorドメイン名
port = 8182  # Gremlinサーバーのポート番号(デフォルトでは8182)
traversal_name = "g"  # TraversalGraphSourceの変数名(デフォルトでg)
url = f"ws://{domain}:{port}/gremlin"  # 接続するURL


class GraphConnection:
    def __init__(self):
        self.connection = DriverRemoteConnection(url, traversal_name)
        self.g = traversal().withRemote(self.connection)
        self.limit = 100000

    def __del__(self):
        self.close()

    def close(self):
        self.connection.close()
        self.connection = None
        self.g = None

    def drop(self):
        # グラフの頂点をドロップする
        # 頂点数が多くなると g.V().drop()で時間とメモリの消費が厳しすぎるので
        # limitを使って少しずつ削除する
        # 頂点数を得る(g.V().count())のも高コストなので、頂点数の上限はself.limitとする
        per = 100
        loop = self.limit // per
        for _ in range(loop):
            self.g.V().limit(per).drop().iterate()

    def makeData(self, num):
        if num > self.limit:
            raise Exception(f"[ERROR]upper limit is {self.limit}.")
        if not self.connection:
            raise Exception("Connection is not established.")

        self.drop()
        for i in range(num):
            if i % 10 == 0:
                print(i)
            name = f"A{i:07}"
            age = random.randint(0, 100)
            self.g.addV("person").property("name", name).property("age", age).iterate()

    def showVertices(self, num=None):
        if not self.connection:
            raise Exception("Connection is not established.")

        vertices = self.g.V().valueMap(True)
        n = 0
        while vertices.hasNext():
            vertex = vertices.next()
            print(vertex)
            n += 1
            if num and n >= num:
                break

    def getVertex(self, name):
        if not self.connection:
            raise Exception("Connection is not established.")

        vertex = self.g.V().has("name", name).valueMap("name", "age").next()
        return vertex


def new_vertices(num):
    graph = GraphConnection()
    graph.makeData(num)


def show(num):
    graph = GraphConnection()
    graph.showVertices(num)


def measure(num):
    graph = GraphConnection()
    total = timeit.timeit(lambda: graph.getVertex("A0000001"), number=num)
    average = total / num
    print(f"total={total:.6}, average={average:.6}")


def main():
    if len(sys.argv) < 3:
        print(f"usage:")
        print(f"  {sys.argv[0]} -new num   : initialize graph vertices")
        print(f"  {sys.argv[0]} -p num     : show vertices")
        print(f"  {sys.argv[0]} -q num     : query time measurement")
    elif sys.argv[1] == "-new":
        num = int(sys.argv[2])
        new_vertices(num)
    elif sys.argv[1] == "-p":
        num = int(sys.argv[2])
        show(num)
    elif sys.argv[1] == "-q":
        num = int(sys.argv[2])
        measure(num)


if __name__ == '__main__':
    main()

それぞれのグラフで100回ずつ測定しての平均値は以下のようになった

頂点数 クエリ1回の平均応答時間[ミリ秒]
1000 15
10000 130
100000 1605

あからさまに遅い。インデックスなしで稼働させるのは無理がある気がする。

実験用サーバーの立ち上げ方/リセット方法

いろいろ試すためにDocker Composeを使ってJanusGraphサーバーを立ち上げられるようにする。適当なディレクトリにdocker-compose.ymlファイルを作成し、以下のように記述する。

docker-compose.yml
version: '3'
services:
    indexjanus:
        image: janusgraph/janusgraph
        ports:
            - 8182:8182
        volumes:
            - janusgraph-index-test:/var/lib/janusgraph
volumes:
    janusgraph-index-test:

立ち上げる場合は

$ docker-compose up -d

停止する場合は

$ docker-compose down

データを抹消したい(リセットしたい)場合は

$ docker-compose down -v

インデックスを張る

サーバーを立ち上げたら、先ほどのプログラムで1頂点以上を突っ込んでおく。グラフが空だと、スキーマを自分で1から作らなければならず、面倒くさいため(本当は最初にスキーマを作るべきだが)。Gremlinコンソールから操作して、作業を開始する。

まず、接続する。

gremlin> :remote connect tinkerpop.server conf/remote.yaml session
==>Configured localhost/127.0.0.1:8182-[347fb18b-1f41-4006-93fb-f0c67d26e508]

次に、現在のスキーマを確認する

gremlin> :> m = graph.openManagement()  // 設定用のインターフェースを取得
==>org.janusgraph.graphdb.database.management.ManagementSystem@668c7485
gremlin> :> m.printSchema()
==>------------------------------------------------------------------------------------------------
Vertex Label Name              | Partitioned | Static                                             |
---------------------------------------------------------------------------------------------------
person                         | false       | false                                              |
---------------------------------------------------------------------------------------------------
Edge Label Name                | Directed    | Unidirected | Multiplicity                         |
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
Property Key Name              | Cardinality | Data Type                                          |
---------------------------------------------------------------------------------------------------
name                           | SINGLE      | class java.lang.String                             |
age                            | SINGLE      | class java.lang.Integer                            |
---------------------------------------------------------------------------------------------------
Vertex Index Name              | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
Edge Index (VCI) Name          | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
Relation Index                 | Type        | Direction | Sort Key       | Order    |     Status |
---------------------------------------------------------------------------------------------------

Property Key Namenameageが登録されていることを確認する。また、Vertex Index Nameの欄が空であることを確認する。現状インデックスは何も作成されていない。

次に、nameプロパティについてインデックスを作成する。

gremlin> :> name = m.getPropertyKey("name")
==>name
gremlin> :> m.buildIndex('by_name', Vertex.class).addKey(name).buildCompositeIndex()
==>by_name
gremlin> :> m.commit()
==>null

インデックス名はここではby_nameとした。別にどんな名前でもいいが、プロパティとまったく同じ名前は推奨しない(混乱するので)。

ここで暫く待つ。作成された(INSTALLED)インデックスが登録された状態(REGISTERED)になるまで。この時間が結構バラツキがあり、法則性も良くわからない。

インデックスの状態が変化するまで待機する関数awaitGraphIndexStatusを使うのが手っ取り早い。ただし、Gremlinコンソールの標準設定により、30秒応答がないとエラーを吐いてしまうので、:remote config timeoutコマンドで予め時間を延ばしておかないといけない(単位はミリ秒で指定)。

gremlin> :remote config timeout 180000
==>Set remote timeout to 180000ms
gremlin> :> ManagementSystem.awaitGraphIndexStatus(graph, 'by_name').status(SchemaStatus.REGISTERED).timeout(3, java.time.temporal.ChronoUnit.MINUTES).call()

状態がREGISTEREDになるまで、待機する。上記の場合、時間は3分にしてある。成功すれば以下のように応答が返ってくる(success=true)。

==>GraphIndexStatusReport[success=true, indexName='by_name', targetStatus=[REGISTERED], notConverged={}, converged={name=REGISTERED}, elapsed=PT10.526S]

問題はいつまでもREGISTEREDにならない場合がある場合が何度かあったことだ。それについては残念ながら解決していない(データベース毎削除してやり直した)。

printSchemaを再度実行して、インデックスの状態を確認する。

gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@42e41e2e
gremlin> :> m.printSchema()
()
---------------------------------------------------------------------------------------------------
Property Key Name              | Cardinality | Data Type                                          |
---------------------------------------------------------------------------------------------------
name                           | SINGLE      | class java.lang.String                             |
age                            | SINGLE      | class java.lang.Integer                            |
---------------------------------------------------------------------------------------------------
Vertex Index Name              | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
by_name                        | Composite   | false     | internalindex  | name:       REGISTERED|
---------------------------------------------------------------------------------------------------
()

ここで、インデックスby_nameの右端のStatusがREGISTEREDとなっていればOK。次はREINDEXを実行して、現在のグラフデータに対してインデックステーブルを更新する。

gremlin> :> by_name = m.getGraphIndex("by_name")
==>by_name
gremlin> :> m.updateIndex(by_name, SchemaAction.REINDEX).get()
==>org.janusgraph.diskstorage.keycolumnvalue.scan.StandardScanMetrics@451782db
gremlin> :> m.commit()
==>null
gremlin> 

これを実行した後、インデックステーブルの更新が終わっていれば再度printSchemaを実行したときに状態がENABLEDとなり、インデックスが有効となる。

gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@42e41e2e
gremlin> :> m.printSchema()
()
---------------------------------------------------------------------------------------------------
Vertex Index Name              | Type        | Unique    | Backing        | Key:           Status |
---------------------------------------------------------------------------------------------------
by_name                        | Composite   | false     | internalindex  | name:         ENABLED |
---------------------------------------------------------------------------------------------------
gremlin> :> m.rollback()
==>null

なお、特に変更しない場合はm.rollbackを使ってmを処分するとよい。

インデックスの状態遷移については公式に図があるので、参照していただきたい。

Index Lifecycle

自動化

こういった作業を毎回手作業でやるのは辛いので、スクリプトを書いて自動化する。Gremlinコンソールでは:loadコマンドを使ってファイルからコマンドのリストを自動実行できる。

makeindex.groovy
:remote connect tinkerpop.server conf/remote.yaml session
:remote config timeout 180000
:> prop_name = "name"
:> index_name = "by_name"
:> m = graph.openManagement()
:> name = m.getPropertyKey(prop_name)
:> m.buildIndex(index_name, Vertex.class).addKey(name).buildCompositeIndex()
:> m.commit()
:> ManagementSystem.awaitGraphIndexStatus(graph, index_name).status(SchemaStatus.REGISTERED).timeout(3, java.time.temporal.ChronoUnit.MINUTES).call()
:> m = graph.openManagement()
:> m.printSchema()
:> by_name = m.getGraphIndex(index_name)
:> m.updateIndex(by_name, SchemaAction.REINDEX).get()
:> m.commit()
:> ManagementSystem.awaitGraphIndexStatus(graph, index_name).status(SchemaStatus.ENABLED).timeout(3, java.time.temporal.ChronoUnit.MINUTES).call()
:> m = graph.openManagement()
:> m.printSchema()
:> m.rollback()
:remote close

このファイルをJanusGraphのルートディレクトリから見える場所に置いておく。例えば./makeindex.groovyに置いたとすると、

$ bin/gremlin.sh

でコンソールを起動したのち、

gremlin> :load ./makeindex.groovy

で自動実行できる。

インデックス作成後のクエリ応答時間

頂点数 インデックスあり[ミリ秒] インデックスなし[ミリ秒]
1000 5 15
10000 4 130
100000 4 1605

これなら実用に耐えそう!👍

複合インデックスの場合は?

name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('by_name_age', Vertex.class).addKey(name).addKey(age).buildCompositeIndex()

addKeyをつなげるだけ。あとは同じ。

chromia
Githubからもらったアイコンが人の顔に見えるのが勿体なくてアイコンを変えられない。
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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした