gremlin-server コンテナを立ち上げる②　ー　neo4j embeded

# はじめに

[gremlin-server コンテナを立ち上げる](https://qiita.com/tkosht/items/c10350592bdb3ec4a54b) の続編です。
neo4j のファイル群を使って、gremlin サーバ上のグラフデータを永続化する方法について整理します。


# TL; DR

- 勘違いしていた点
    - BOLT　接続は、リモートNeo4jサーバ へプロキシすると思っていたが、そうではない
        - Cypher を gremlin で使えるようにするらしい （試していないです・・）
- 設定ファイルの変更点
    - gremlin-server.yml をベースに、gremlin-server-neo4j.yml を作成
        - もちろん、サンプルの gremlin-server-neo4j.yaml も参考
    - docker-compose.override.yml の entrypoint の設定ファイルパスを変更
- Python 経由で使っても、問題なく動作する
- コード一式（設定ファイル変更前）は、[こちら](https://github.com/tkosht/gremlin.git)

# 環境

| 対象 | バージョン |
| --- | ------- |
| OS | macOS Big Sur (11.5.2) |
| docker | Docker version 20.10.8, build 3967b7d |
| docker-compose | docker-compose version 1.29.2, build 5becea4c |
| python | Python 3.8.10 |


# やったこと

設定ファイルを変更し、`docker-compose.override.yml` で設定ファイルへのパスを変更しました。

## 設定ファイルの変更

`gremlin-server.yml` との差分

```diff
~/p/gremlin ❯❯❯ diff conf/gremlin/server/gremlin-server{,-neo4j}.yml                                               ✘ 1 
17a18,27
> # Note that TinkerPop does not include Neo4j dependencies in its
> # distributions. This file cannot be used as a configuration file to 
> # Gremlin Server unless Neo4j dependencies are installed on the 
> # Gremlin Server path with:
> #
> # bin/gremlin-server.sh -i org.apache.tinkerpop neo4j-gremlin x.y.z
> # 
> # Note that unless under a commercial agreement with Neo4j, Inc., 
> # Neo4j is licensed AGPL. 
> 
23c33
<   graph: conf/tinkergraph-empty.properties}
---
>   graph: conf/neo4j.properties}
27c37
<                org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {},
---
>                org.apache.tinkerpop.gremlin.neo4j.jsr223.Neo4jGremlinPlugin: {},
~/p/gremlin ❯❯❯                                                                                                    
```

`.properties` ファイルの指定を変更し、`Neo4jGremlinPlugin` のところを変えました。（前半の差分は、コメントなので略）


## `docker-compose.override.yml` の変更

以下に、サービス `gremlin-server` の部分だけを抜粋します。

```yml
  gremlin-server:
    build:
      context: ./
      dockerfile: docker/gremlin/server/Dockerfile
    image: $project.gremlin-server
    container_name: $project.gremlin-server
    ports:
      - "8182:8182"
    volumes:
      - ./data/:/data/
      - ./conf/gremlin/server/gremlin-server.yml:/opt/gremlin-server/conf/gremlin-server.yml
      - ./conf/gremlin/server/gremlin-server-neo4j.yml:/opt/gremlin-server/conf/gremlin-server-neo4j.yml
      - ./conf/gremlin/server/neo4j.properties:/opt/gremlin-server/conf/neo4j.properties
    entrypoint: >
      /bin/bash bin/gremlin-server.sh conf/gremlin-server-neo4j.yml
      # /bin/bash bin/gremlin-server.sh conf/gremlin-server.yml

```

## 永続化確認

[github リポジトリ](https://github.com/tkosht/gremlin.git) をクローンし、上記の設定変更が終わっているとします。


### サービス群の起動

```zsh
make
```

### サービス群の起動確認

起動できていることを確認します。以下のように `gremlin.gremlin-server` が起動していればOKです。
※　`gremlin.app` は、python の VSCode用開発環境用のコンテナ（Ubuntu 20.04）です

```zsh
~/p/gremlin ❯❯❯ make ps
docker-compose ps
         Name                       Command               State                    Ports                  
----------------------------------------------------------------------------------------------------------
gremlin.app              /bin/bash                        Up                                              
gremlin.gremlin-server   /bin/bash bin/gremlin-serv ...   Up      0.0.0.0:8182->8182/tcp,:::8182->8182/tcp
~/p/gremlin ❯❯❯ 

```

### データがないことを確認

```zsh
~/p/gremlin ❯❯❯ make gremlin-console < src/gremlin/ref.gremlin
(省略)
gremlin> g.V().values('name')
gremlin> g.V().count()
==>0
gremlin> :exit
~/p/gremlin ❯❯❯                                                        
```

### データ投入

```zsh
~/p/gremlin ❯❯❯ make gremlin-console < src/gremlin/test.gremlin
(省略)
gremlin> :remote connect tinkerpop.server conf/remote.yml
==>Configured gremlin-server/172.24.0.2:8182
gremlin> :remote console
==>All scripts will now be sent to Gremlin Server - [gremlin-server/172.24.0.2:8182] - type ':remote console' to return to local mode
gremlin> :remote connect tinkerpop.server conf/remote.yml
==>Configured gremlin-server/172.24.0.2:8182
gremlin> :remote console
==>All scripts will now be sent to Gremlin Server - [gremlin-server/172.24.0.2:8182] - type ':remote console' to return to local mode
gremlin> g.addV('person').property('name','hello')
==>v[38]
gremlin> g.addV('person').property('name','world')
==>v[39]
gremlin> g.V().values('name')
==>hello
==>world
gremlin> g.V().count()
==>2
gremlin> :exit
~/p/gremlin ❯❯❯ 
```

2 ノードが追加されていることがわかります。


### 再起動（停止＆起動）

永続化されていることを確認するため、一度 再起動（停止＆起動）をしておきます。

```zsh
~/p/gremlin ❯❯❯ make reup
docker-compose down
Stopping gremlin.gremlin-server ... done
Stopping gremlin.app            ... done
Removing gremlin.gremlin-server ... done
Removing gremlin.app            ... done
Removing network gremlin_default
docker-compose up -d app gremlin-server
Creating network "gremlin_default" with the default driver
Creating gremlin.app            ... done
Creating gremlin.gremlin-server ... done
~/p/gremlin ❯❯❯ 
```

### 再起動後の確認（永続化確認）

```zsh
~/p/gremlin ❯❯❯ make gremlin-console < src/gremlin/ref.gremlin
(省略)
gremlin> :remote connect tinkerpop.server conf/remote.yml
==>Configured gremlin-server/172.25.0.3:8182
gremlin> :remote console
==>All scripts will now be sent to Gremlin Server - [gremlin-server/172.25.0.3:8182] - type ':remote console' to return to local mode
gremlin> g.V().values('name')
==>hello
==>world
gremlin> g.V().count()
==>2
gremlin> :exit
~/p/gremlin ❯❯❯ 
```

gremlin-server の 再起動（起動＆停止）後でも、再起動前のデータが入っていることが確認できました。
つまり、永続化できていることを意味します。


## python で動作確認

めでたく永続化できたので、Python から使っても、特段異常が起きないことを確認します。

まず、import と Gremlin 操作を 隠蔽するクラス（GremlinProvider）を作っておきます。

```python
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection


class GremlinProvider(object):
    def __init__(self):
        self.g = traversal().withRemote(
            DriverRemoteConnection("ws://gremlin-server:8182/gremlin", "g")
        )

    def clear_nodes(self):
        self.g.V().drop().iterate()
        return self

    def add_person(self, name: str):
        node = self.g.addV("person").property("name", name).next()
        return node

    def add_age(self, name: str, age: int):
        assert name and 0 <= age <= 150
        node = self.g.V().has("person", "name", name).property("age", age).next()
        return node

    def add_job(self, name: str, job: str):
        assert name and job
        node = self.g.V().has("person", "name", name).property("job", job).next()
        return node

    def get_name(self, v):
        name = self.g.V(v).values("name").toList()[0]
        return name

    def get_age(self, v):
        age = self.g.V(v).values("age").toList()[0]
        return age

    def add_knows(self, v1, v2, weight=0.75):
        edge = self.g.V(v1).addE("knows").to(v2).property("weight", weight).iterate()
        return edge

    def find_person_node(self, name: str):
        node = self.g.V().has("person", "name", name).next()
        return node

    def find_person_whom_knows(self, center: str):
        whom_knows = self.g.V().has("person", "name", center).out("knows").toList()[0]
        return whom_knows

```

次が、実験コードです。

```python
if __name__ == "__main__":
    grp = GremlinProvider()
    grp.clear_nodes()

    # marko を登録
    v1 = grp.add_person("marko")
    assert grp.get_name(v1) == "marko"
    print("v1:", grp.get_name(v1))
    # v1: marko

    # stephen を登録
    v2 = grp.add_person("stephen")
    assert grp.get_name(v2) == "stephen"
    print("v2:", grp.get_name(v2))
    # v2: stephen

    # 年齢を追加・更新
    grp.add_age("marko", 35)  # insert
    grp.add_age("marko", 31)  # update
    grp.add_age("stephen", 32)

    # 職業を追加・更新
    grp.add_job("marko", "SWE")
    grp.add_job("stephen", "SWE")

    # リレーション（knows） を追加
    e1 = grp.add_knows(v1, v2, 0.1)
    print(e1)
    # [['V', v[34]], ['addE', 'knows'], ['to', v[35]], ['property', 'weight', 0.1], ['none']]

    # marko を検索
    marko = grp.find_person_node("marko")
    print("marko:", grp.get_name(marko))
    # marko: marko

    # marko が知っている人を検索
    v = grp.find_person_whom_knows("marko")
    print("marko knows:", grp.get_name(v))
    print("marko knows2:", grp.g.V(v1).outE().inV().values("name").toList()[0])
    # marko knows: stephen
    # marko knows2: stephen

    # ノードオブジェクトから年齢を取得
    print("marko.age:", grp.get_age(v1))
    assert grp.get_age(v1) == 31
    print("stephen.age:", grp.get_age(v2))
    assert grp.get_age(v2) == 32
    # marko.age: 31
    # stephen.age: 32

    # 職業が ”SWE" である人物の名前を取得(リスト)
    print("SWE:", grp.g.V().has("person", "job", "SWE").values("name").toList())
    # SWE: ['marko', 'stephen']
```

上記の２ブロックのコードを、1つのファイル(`src/trial2.py`)にして実行します。
(`make bash` を実行後、`python -m src.trial2` を実行)


```zsh
~/p/gremlin ❯❯❯ make bash
docker-compose up -d app gremlin-server
gremlin.app is up-to-date
gremlin.gremlin-server is up-to-date
docker-compose exec app bash
dsuser@7d907178380d:~/workspace$ python -m src.trial2
v1: marko
v2: stephen
[['V', v[36]], ['addE', 'knows'], ['to', v[37]], ['property', 'weight', 0.1], ['none']]
marko: marko
marko knows: stephen
marko knows2: stephen
marko.age: 31
stephen.age: 32
SWE: ['marko', 'stephen']
dsuser@7d907178380d:~/workspace$ 
```

以上により、ざっくりと、以下のことが確認できたことになります。

- node オブジェクトの生成
- 与えられた node オブジェクトの name プロパティを取得
- 与えられた 2つの node オブジェクトから edge オブジェクトの生成
- name から node オブジェクトを検索（取得）
- 与えられた node オブジェクトから、edge を辿って、エッジの先の node オブジェクトを取得
- 既に登録した node に対して、プロパティを追加・更新（age, job）
- ノードオブジェクトから、各種プロパティ（age, job）を取得
- 特定のプロパティ値を持つ node の name リストを取得


# まとめ

- 永続化のために、Neo4j ファイルを使うようにするには、設定ファイルを少し変更するだけで対応できる
    - BOLT 接続は、リモートの Neo4j に接続するためのものではない（らしい/未確認）点に注意
- ある程度、Graph で操作したいことを、Python から実行できることを確認（ See `src/trial2.py` ）
    - node, property へのアクセス、追加、更新は概ね問題ない


# 参考文献

- [Gremlinと戯れる](https://qiita.com/stoneriverheart/items/4dfd17cdab43e9b48063)
- [gremlinpythonの使い方覚書](https://qiita.com/bc_yuuuuuki/items/81e827aca6f8888b07a2)
- [Recipes - tinkerpop.apache.org](https://tinkerpop.apache.org/docs/current/recipes/)