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
ファイルを作成し、以下のように記述する。
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 Name
にname
とage
が登録されていることを確認する。また、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
を処分するとよい。
インデックスの状態遷移については公式に図があるので、参照していただきたい。
自動化
こういった作業を毎回手作業でやるのは辛いので、スクリプトを書いて自動化する。Gremlinコンソールでは:load
コマンドを使ってファイルからコマンドのリストを自動実行できる。
: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
をつなげるだけ。あとは同じ。