Pharo SmalltalkからNeo4jを使う

# はじめに

[Pharo Smalltalk](https://pharo.org/)でデータを永続化する場合、さまざまな選択肢がありますが、グラフDBも有力な候補のうちの一つです。

Smalltalkはオンメモリにオブジェクトを浮かべていく環境なので、アプリの開発が進むと、わりと複雑な構造のオブジェクトのグラフがイメージ内にできていきます。これをRDBなどに保存しようとすると、特に問題になるのは表とオブジェクトとのインピーダンスミスマッチです。[Glorp](https://github.com/pharo-rdbms/glorp)などのOR Mapperもあるのですが、マッピングの管理や維持がなかなか大変です。
一方グラフDBなら、オブジェクトが織りなすグラフ構造を、変換せずに素直に永続化していくことができます。ややこしいマッピングに悩まされることはありません。

以後は、PharoとグラフDBの連携の例として、[SCypherGraph](https://github.com/mumez/SCypherGraph)という自作のライブラリを紹介していきたいと思います。SCypherGraphを使えば、代表的なグラフDBである[Neo4j](https://neo4j.com/)に、Pharoから気軽にアクセスできます。

# SCypherGraphのPharoへの導入

Pharoそのもののインストールについては割愛します。
Advent Calendarに良い記事がありましたので、適宜参照してください。

- [「PharoLauncherでSmalltalk環境を作る(2020年末版）」](https://qiita.com/kuritar/items/fa97e1cfc0b0afa2205f)

SCypherGraphを入れるにはPharoのPlaygroundで以下を"do it"します。(なお、PlaygroundはControl+o+wで開きます。MacではCommandキーです。以後は読み替えるようにしてください)

```smalltalk
Metacello new
  baseline: 'SCypherGraph';
  repository: 'github://mumez/SCypherGraph:main/src';
  load.
```

これでSCypherGraph本体と依存するライブラリ群がGitHubから取得され、Pharoにインストールされます。

# Neo4jのインストールと起動

[Neo4j Download Center](https://neo4j.com/download-center/)から、Community Serverを選択し、アーカイブファイルを取得して展開します。

bin以下に、パスを通し、`neo4j`(windowsの場合は`neo4j.bat`)を`console`指定で実行します。なお、実行にはJDK 11以降が必要です。

```bash
neo4j console
```

ブラウザで http://localhost:7474/ と指定すると、Webベースの管理用UIが開きます。初回のみ、管理者用パスワードを設定する必要があります。(以後はバージョン4.2.1を想定して説明していきます。バージョンが進むとUIが多少違うことがあるかもしれません)

![2020-12-23 22_23_42-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/1aeddf06-972a-8fb7-65d5-2a94ae7e5c52.png)


デフォルトでは

> Username: neo4j
> Password: neo4j

となっていますが、Connectボタンを押して上記でログインすると、すぐに変更を求められます。

今回は

> Password: neoneo

とでもしておきます。

![2020-12-23 22_30_46-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/82376f1e-5f05-75c4-b0ce-334017f7b17c.png)

# Neo4jへのデータ投入

パスワードを変更すると、管理コマンド等を評価できるペインが出てきます。
これを使い、"Movie Graph"というサンプルデータを手始めに投入しておきましょう。

一番上にある`$`のプロンプトが出ているペインに

```cypher
:use neo4j
```

と打ち実行します。(再生ボタンかControl + enter)

![2020-12-20 19_05_12-.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/4dc5ca03-5d50-1189-5a5c-d9063e474d77.png)
これはデフォルトのデータベースを`system`から`neo4j`に切り替えるためのものです。(`system`のデータベースにはサンプルデータを投入できないので、切り替えています)

続いてチュートリアルを開きます。プロンプトが出ているペインに

```cypher
:play movie-graph
```
と打ち実行します。

複数ページに渡るチュートリアルがすぐ下に表示されます。2ページ目に移り左上のCREATEの近くの再生ボタンを押すと、上部のペインにCypherというグラフ操作言語のコードが貼り付けられます。

![2020-12-20 19_01_37-neo4j@neo4j___localhost_7687_system - Neo4j Browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/11ee9f06-fa9f-81e7-ff06-06da125b52eb.png)

貼り付けられたCypherを実行しましょう。これでデータが投入されます。
![2020-12-23 22_11_23-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/49fb7698-e65f-04ad-d63d-3db514b95feb.png)


結果がグラフィカルに表示されるようになりました。

![2020-12-12 23_29_55-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/db97c5a3-5445-bb9b-4381-4ffbb2c0de26.png)

# SCypherGraphによるグラフデータへのアクセス

では、投入された"Movie Graph"のデータを、SCypherGraphでNeo4jにつないで取り出してみましょう。PharoのPlaygroundで以下のコードを書き"print it"します*[^1]。(コード全体を選んでControl + p)

```Smalltalk
db := SgGraphDb new.
db settings username: 'neo4j'; password: 'neoneo'.
db allLabels. "print it"
```
[^1]: なんらかの理由でIPアドレスやポート番号を変えたい場合は`db settings targetUri: 'bolt://127.0.0.1:7687'.`で設定できます

`SgGraphDb >> allLabels` で、DB内部にあるノードのラベル一覧を得ることができます。結果を見ると'Movie'と 'Person'のラベルが存在することがわかりました。

![2020-12-13 21_10_48-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/3dd8d08f-1dce-ed72-1e09-f82fb88a98ac.png)

## ノードの取得

'Movie'というラベルのノードにはどういったものがあるのか、Transcriptに表示してみたいと思います。Control+o+tでTranscriptを開いてから、Playgroundにさらに追記して"do it"してみましょう。(コードを選んでControl + d)

```Smalltalk
(db nodesLabeled: 'Movie') 
   do: [ :each | self traceCr: each properties ].
```

`SgGraphDb >> nodesLabeled:` で、特定のラベルが付けられたノード群を取り出すことができます。各Movieノードは、リリース年やキャッチコピー(tagline)など、さまざまな情報をプロパティとして持っていることが確認できます。
(結果を`do:`でイテレートしているので、`traceCr:`によりTranscriptにノードごとに改行されて表示されます。)

![2020-12-13 21_23_03-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/c2ec89de-03b2-19f4-0270-5b1a16a13777.png)

Movieの量が多すぎるので、今度は`where:`もつけて条件を指定してみます。以下を追記して、"inspect it"してみましょう。(Control+i)

```smalltalk
matrix := (db nodesLabeled: 'Movie' where: [:each | each @ 'title' = 'The Matrix']) first.
matrix properties.
```

`where:`のブロック内で、'title'プロパティの値が'The Matrix'であるものに限定しています。結果は`OrderedCollection`で返ってきますが、一つだけなので`first`で取り出しています。

`properties`でプロパティ一覧が取り出され、インスペクタが開きます。

![2020-12-13 21_48_32-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/eaf58096-56b7-9edf-1861-3c9a8aa13e85.png)

「マトリックス」はもう20年以上前の映画なのですね。

## リレーションの取得

リレーションとは、ノード間に貼られている関連のことです。Neo4jではリレーションにタイプや方向の概念があります。また、リレーションにもプロパティを設定できます。

'The Matrix'に向かって張られている関連を一覧してみることにします。

```smalltalk
matrix inRelationships. "print it"
```

`ACTED_IN`, `DIRECTED`, `PRODUCED`といったタイプの関連が張られていることがわかります。俳優、ディレクター、プロデューサーなどの人物が関わっているようですね。

![2020-12-15 22_24_56-Pharo Virtual Machine! (C__Users_ume_Documents_Pharo_images_insttest_insttest.im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/c6762b1f-85c4-b6a2-37b3-9fd2ba7a76c9.png)

一方で、'The Matrix'から伸びている関連はありませんでした。

```smalltalk
matrix outRelationships. "print it"
```

![2020-12-15 22_31_14-Pharo Virtual Machine! (C__Users_ume_Documents_Pharo_images_insttest_insttest.im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/12b9070f-53cc-e783-64df-ce86bdb88025.png)

では、もう少し条件を指定して、演じた(`ACTED_IN`)人達の名前を一覧してみましょう。

```smalltalk
(matrix inRelationshipsTyped: 'ACTED_IN')
  collect: [:each | each endNode @ 'name']. "print it"
```

関連の終端ノード側はPersonノードになっており、プロパティ'name'を持っています。
`collect:`で集められ、文字列の配列として結果が得られることを確認できました。

![2020-12-16 22_25_16-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/1d3aaa6f-65df-d813-6f42-58ffcd13493b.png)

`where:`を入れて更に絞り込んでみます。ブロック引数には、検索用の条件指定に使えるよう、開始ノード、ノード間の関連、終端ノードが渡されるようになっています。

`ACTED_IN`の関連にはプロパティ'roles'があります。ある映画で一人二役以上ということもあるので、配列になっているのでしょう。'Neo'を演じた人を探してみます。

```smalltalk
(matrix inRelationshipsTyped: 'ACTED_IN' where: [ :start :rel :end | (rel @ 'roles') = #('Neo') ])
  collect: [ :each | each endNode properties ]. "inspect it"
```
インスペクタを見ると、

![2020-12-21 22_55_57-Pharo Virtual Machine! (C__Users_ume_Documents_Pharo_images_insttest_insttest.im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/6017dee8-fb95-4976-9c05-3cac3d230ea1.png)

'Neo'を演じたのは'Keanu Reeves'、1964年生まれということがわかりました。

# SCypherGraphによるグラフデータの更新

ノードとリレーションをいい感じに取り出せたので、次は更新系の操作をしてみたいと思います。

## ノードの作成

まず、ジャンル(Genre)というラベルのノードを新設することにします。
'SF'と'Action'の二つをとりあえず作成します。プロパティとして、名前('name')の他に、短い説明('description')も付けておきます。

```smalltalk
sf := db mergeNodeLabeled: 'Genre' properties: {'name'->'SF'. 'description'->'Science Fiction'}. "inspect it"
```

```smalltalk
action := db mergeNodeLabeled: 'Genre' properties: {'name'->'Action'. 'description'->'Exciting Actions'}. "inspect it"
```

上記のように`SgGraphDb >> mergeNodeLabeled:properties:`でノードの作成が可能です。作成された結果のノードが即座に返るようになっています。

`createNodeLabeled:properties:`でもノードを作成できますが、こちらを使うと毎回新しいノードが作られ、実行の度にGenreが重複して増えていってしまいます。`mergeNodeLabeled:properties:`では、すでにDBに存在するノードがあると単にそれを返すようになっており、同じノードが作られることはありません。そのため通常はmerge系を使うことが多いでしょう。

![2020-12-16 23_11_52-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/be48b622-2109-754e-8ea5-452a6b2b1369.png)

作成した各ノードについてのインスペクタが開いていると思います。インスペクタ下部のペインではSmalltalkの式を評価できるので、`self properties`と打って"print it"してみましょう。ノードに付与したプロパティの値を確認できます。

## リレーションの作成

次はジャンルと映画とを、リレーションで結びつけてみたいと思います。

マトリックスは、SFでもあり、アクション映画でもある感じなので、2つのGenreノードに対して関連を張ることになるでしょう。

関連タイプは`HAS_GENRE`とします。以下のように`SgNode >> relateOneTo:typed:properties:`を使います。

```smalltalk
matrixToSf := matrix relateOneTo: sf typed: 'HAS_GENRE' properties: {'score'-> 6}.
matrixToAction := matrix relateTo: action typed: 'HAS_GENRE' properties: {'score'-> 7}. "do it"
```

分類されるジャンルの傾向がどのくらい強いかの度合いとして'score'というプロパティも入れてみました。

リレーションは`SgNode >> relateTo:typed:properties:`で作成することもできますが、こちらは実行の度に新たなリレーションが作成されてしまいます。マトリクスから同じSFのノードに重複した関連があっても意味がありませんので、`relateOneTo`が今回は適しています。ノードのmerge系操作と同じく、すでに同種のリレーションがある場合はそれを返してくれます。

確認のため、生成されたリレーションの開始ノードと終了ノードから、プロパティを取り出してみます。

```smalltalk
{matrixToSf startNode @ 'title'. matrixToSf endNode @ 'name'}. "print it"
```

想定通り、'The Matrix'から'SF'に関連が張られたことがわかりますね。

![2020-12-17 22_53_02-Pharo Virtual Machine! (C__Users_ume_Documents_Pharo_images_insttest_insttest.im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/719cdd57-6f21-4465-0a28-d05d2ea18ac6.png)

先ほどは空だった、matrixから伸びていく関連を一覧してみましょう。

```smalltalk
matrix outRelationships. "print it"
```

今度は2つのリレーションを確認できます。

![2020-12-17 22_58_49-Pharo Virtual Machine! (C__Users_ume_Documents_Pharo_images_insttest_insttest.im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/4025c099-e9bb-ef74-f68e-2be3c144fcec.png)

## 生のCypherを実行する

SCypherGraphの裏では、グラフ操作言語のCypherが動的に生成され、Neo4jに送り込まれています。Cypherを知らずとも、オブジェクトにシンプルにメッセージを送るだけで、グラフデータの読み取りや更新が一通りできるというのが、SCypherGraphの良いところです。

しかし実際には、パフォーマンスチューニングなどの観点から、Cypherを意識して実行したい場面もあります。
多少長くて複雑なCypherを書くことになったとしても、欲しい要素のみをまとめて取り出せれば、クエリの実行回数が減り、パフォーマンス的に有利になるからです。

そのためSCypherGraphでは、生のCypherをNeo4jに渡して実行させることも可能になっています。

まずは単純なCypherで試してみましょう。

```smalltalk
db runCypher: 'UNWIND range(1, 10) AS n RETURN n*n'. "inspect it"
```

`UNWIND`で`range`から数値のリストを生成し、それぞれを2乗した結果を`RETURN`で返すというものです。

実行すると`SbCypherResult`のインスペクタが開きます。下部のペインに`self fieldValues`と書いて"print it"で、結果を確認できます。

![2020-12-18 14_59_37-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/458e15f4-fb2c-b653-7cf9-e4f4241c324a.png)

パラメータを渡す形でのCypherの実行もサポートしています。下記の`$from`と`$to`という部分がパラメータです。`arguments:`により値を与えています。

```smalltalk
db runCypher: 'UNWIND range($from, $to) AS n RETURN n*n' 
   arguments: {'from'->2. 'to'->5}. "inspect it"
```

![2020-12-18 14_58_20-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/25a0e4a6-d964-d3f8-fc3c-b0d3d648718b.png)

## SCypherでCypherを生成して実行する

より複雑なCypherについては、どういったアプローチがあるでしょうか。
SCypherGraphは内部で[SCypher](https://github.com/mumez/SCypher)というライブラリを用いており、メッセージ送信によってCypherを動的かつ柔軟に生成することができます。

少し込み入ったクエリとして、「'Tom'で始まる俳優が、2000年封切りの映画で共演した俳優の一覧を得る」という例を考えてみましょう。

生のCypherを書くとこういった感じです。

```cypher
MATCH (p:Person)-[act1:ACTED_IN]->(m:Movie {released:2000})<-[act2:ACTED_IN]-(o:Person)
WHERE (p.name STARTS WITH 'Tom')
RETURN p.name, o.name, m.title ORDER BY p.name
```

`MATCH`で、ノードとリレーションとの連なりのパターンを指定しています。`WHERE`ではプロパティ値が'Tom'で始まるという細かな条件をさらに加えています。`RETURN`で、俳優さんの名前、映画のタイトルといった、必要な情報のみを取り出しています。

検索パターンが固定であれば、このCypherをソースコード上にハードコードで埋めておく方式もあるかもしれません。しかしいくつか検索のバリエーションが増えてくると、対応していくのはつらいことになります。

では、SCypherで上記のCypherを動的に作成してみましょう。

```smalltalk

m := 'm' asCypherObject. "映画"
p := 'p' asCypherObject. "俳優"
o := 'o' asCypherObject. "共演者"

"2000年封切りの映画で2人の俳優がつながっているパターン"
pathPattern := (p node: 'Person') - ('act1' asCypherObject rel: 'ACTED_IN' ) -> (m node: 'Movie' props: {'released'->2000})  <- ('act2' asCypherObject rel: 'ACTED_IN' ) - (o node: 'Person').

"俳優が、パラメータで指定した名前で始まるかの条件指定"
actorNameParam := 'actorName' asCypherParameter.
where := (p @ 'name') starts: actorNameParam.

"結果を俳優の名前、共演者の名前、映画のタイトルの順で返す指定" 
return := (p @ 'name'), (o @ 'name'), (m @ 'title').

"Cypherクエリの組み立て"
query := CyQuery match: pathPattern where: where return: return orderBy: (p @ 'name') skip: 0 limit: 100. "print it"
```

直接書いたCypherよりも長くなりましたが、マッチさせるパターンや、`where`、`return`などが変数となり、組み替えることが容易になっているのが見て取れると思います。

また、Smalltalkのメッセージ送信ではありますが、パターンやwhereの書き方などがCypherとほぼ一対一に対応しているため、処理を連想しやすく、相互の変換がしやすくなっています。

一連のコードを選んで"print it"すると、以下のようなCypherが作られることを確認できます。

```cypher
MATCH (p:Person)-[act1:ACTED_IN]->(m:Movie {released:2000})<-[act2:ACTED_IN]-(o:Person)
WHERE (p.name STARTS WITH $actorName)
RETURN p.name, o.name, m.title ORDER BY p.name SKIP 0 LIMIT 100 
```

パラメータを使うようになっていますが、先ほど示したCypherとほぼ同じですね。

![2020-12-18 22_17_49-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/ed166a91-1aca-db7e-86aa-034cc6833455.png)

では`SbGraphDb >> runCypher:arguments:`で実行してみます。

```smalltalk
result := db runCypher: query arguments: { actorNameParam -> 'Tom' }.
(result fieldValues groupedBy: [ :each | each at: 1 ]). "inspect it"
```

インスペクタで見やすいように、`groupedBy:`を使って俳優の名前でグルーピングしています。
'Tom Cruise'が'Jerry Maguire'で8名、'Tom Hanks'が'Cast Away'で1名と共演しているという結果になりました。

![2020-12-18 22_23_58-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/a8653ec3-ea6f-bbc0-1612-aee75b182c17.png)

せっかくなので、`where`の条件を少し変えてみましょう。

```smalltalk
where := ((p @ 'born') > 1970) and: ((o @ 'born') > 1970). "do it"
```

`where`を設定しなおしたので、クエリを再生成する必要があります。`query := ...`と `result := ...`の両方を選んで"inspect it"してください。

これで、2000年当時に互いに30歳未満で共演した人達が取得できました。

![2020-12-18 22_51_04-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/5bbe7bcc-064a-45a1-8cf9-89ebd2f9e06c.png)

# アプリでの適用事例は?

実戦投入はまだなのですが、中古建機売買サービスの[Allstocker.com](https://allstocker.com/)では、私が過去に作成した[Neo4reSt](https://github.com/mumez/Neo4reSt)というNeo4jクライアントが、高度な検索を行うために使われています。
Neo4reStはNeo4jのレガシーなREST APIを使うものでした。一方SCypherGraphは、Neo4jネィティブの[Bolt](https://boltprotocol.org/)というバイナリプロトコルを採用しており、だいたい3倍ほど速度も向上しています。そのため、折を見てSCypherGraphに移行していこうと思っているところです。

# おわりに

[SCypherGraph](https://github.com/mumez/SCypherGraph)を使えば、Neo4jにアクセスし、グラフデータを自在に操れることがわかっていただけたのではないでしょうか。
いざという時には、動的に生成したCypherを直接送り込むこともできるので、柔軟な利用が可能になっています。

ドキュメント類がまだ未整備ですが、この記事を皮切りとして充実させていこうと思っています。次の[Smalltalk勉強会](https://www.smalltalk-users.jp/Home)で話すことになるかもしれません。

それでは良いクリスマス+新年をお過ごしください!