Neo4jをActiveRecord感覚で扱えるActiveGraphを触ってみた

こんにちは。@mshibuyaです。
グラフデータベースNeo4j向けのActiveModel準拠のOGM(Object Graph Mapping, ORMのグラフ版ですね)であるActiveGraphを触ってみる機会があったのでご紹介します。

なおこの記事は[Rails Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rails)の6日目のエントリーです。

# グラフデータベースとは
データ構造としての[グラフ](https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%A9%E3%83%95_(%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0))を扱うことに最適化されたデータベースシステムです。ノードとエッジから表現され、様々な物事同士の関連関係を効果的に扱うことができます。[Amazon Neptune](https://aws.amazon.com/jp/neptune/)としてフルマネージドのグラフデータベースサービスも登場したりしていて、徐々に利用範囲を広げつつあるのではないでしょうか。

とはいえ、このへんの話はあんまり詳しくないので深くは語れないです。なんかグラフ理論ってかっこよさげだしよくない？程度のノリです…

ActiveGraphはグラフデータベースNeo4jをバックエンドとして利用します。もともとNeo4j.rbと呼ばれていたものが最近名前が変わったようですね。


# 触ってみる

Docker ComposeでNeo4jを立ち上げます。ActiveGraphが対応済みであるバージョン4.0を選択

```yaml
version: '3'
services:
  neo4j:
    image: neo4j:4.0
    ports:
      - "7474:7474"
      - "7687:7687"
    volumes:
      - ./neo4j:/data
```

neo4j-ruby-driverに必要なseaboltをinstall

```bash
$ brew install michael-simons/homebrew-seabolt/seabolt
```

rails newする

```bash
$ rails new graph_app -T -O -m https://raw.githubusercontent.com/neo4jrb/activegraph/master/docs/activegraph.rb
```

config/neo4j.ymlを作成

```yaml
development:
  url: bolt://localhost:7687

test:
  url: neo4j://localhost:7688
```

モデルを作成

app/model/person.rb

```ruby
class Person
  include ActiveGraph::Node

  property :name
end
```

app/model/friendship.rb

```ruby
class Friendship
  include ActiveGraph::Relationship

  from_class :Person
  to_class :Person

  property :since, type: DateTime
end
```

このへんで `Neo4j::Driver::Exceptions::AuthenticationException (Permission denied)` が出て困ったんですが、Neo4j側でパスワードを変更したら出なくなりました。とりあえず深追いはしません。。

rails consoleから遊んでみます

```irb
irb(main):001:0> john = Person.create name: 'john'
 CYPHER CREATE (n:`Person`) SET n = $props RETURN n | {:props=>{:uuid=>"ae128a36-4e0b-4879-9213-9d5ec0b0b726", :name=>"john"}}
 BOLT: 0ms
irb(main):002:0> paul = Person.create name: 'paul'
 CYPHER CREATE (n:`Person`) SET n = $props RETURN n | {:props=>{:uuid=>"5c8987da-de09-4673-b366-a002b491be65", :name=>"paul"}}
 BOLT: 0ms
irb(main):003:0> Friendship.create(from_node: john, to_node: paul, since: Time.now)
 BOLT: 0ms
 BOLT: 0ms
 CYPHER
  MATCH
    (from_node),
    (to_node)
  WHERE
    (ID(from_node) = $from_node_id) AND
    (ID(to_node) = $to_node_id)
  CREATE (from_node)-[rel:`FRIENDSHIP` $rel_create_props]->(to_node)
  SET rel.`since` = $setter_rel_since
  RETURN rel | {:from_node_id=>0, :to_node_id=>2, :setter_rel_since=>1607243457, :rel_create_props=>{}}
 BOLT: 0ms
=> #<Friendship since: Sun, 06 Dec 2020 08:30:57 +0000>
irb(main):004:0> people = 1.upto(100).map {|i| Person.create name: "person#{i}" }
...
irb(main):005:0> 300.times.each { Friendship.create from_node: people.sample, to_node: people.sample }
...
irb(main):006:0> Person.query_as(:p).where('size((p)-->()) > 5').count # 5人以上friendのいるPersonの数を取得
 CYPHER
  MATCH (p:`Person`)
  WHERE (size((p)-->()) > 5)
  RETURN count(*)
 BOLT: 0ms
=> 6
```

ちょっと凝ったクエリをしようとするとNeo4jのクエリ言語であるCypherの知識が求められはするんですが、なかなか面白いです。最短距離を求めるだとか、リコメンデーションに利用したりとか想像は膨らみます…

本当は最近RailsAdminに寄せられた[Neo4j対応](https://github.com/sferik/rails_admin/pull/3271)を試してみるところまで行きたかったのですが、時間がないので今日はここまでで。

よいRailsライフをお過ごしくださいー