Neo4jファンクション作成チュートリアル: パスの文字列表記ファンクションの作成

Neo4jには、UserFunctionという機能があり、ユーザーが独自にファンクション(関数)を定義できます。DBMSの`CREATE FUNCTION`相当のものです。Neo4jのファンクションは、SQLでは書けません。Javaで書く必要があります。本記事では、公式サンプルよりも若干実務に役立つファンクションを作りながら、解説していきます。

> 本記事では、Cypherを知っているという前提で説明しています。
>
> * [Cypherのノード(Node)の基本を理解する](http://qiita.com/makopo/items/d0585e4e52985fcaf66a)
> * [Cypherのリレーションシップ(Relationship)の基本を理解する](http://qiita.com/makopo/items/76ae5be981c90c06877e)

## 作成するファンクション（パスの文字列表記）

次のような感じで使う、`example.pathjoin()`を作ります。

```js
MATCH (a{name:'あ'})
MATCH (o{name:'お'})
MATCH p=(a)-[*]->(o)
RETURN example.pathjoin(p,'name','>')
```

上記の例では、`name`が「あ」のノードと、`name`が「お」のノードの間のパスを`>`で繋げた文字列（例：`あ>い>う>え>お`）を返します。

## Javaでの作業

### テンプレートプロジェクトのダウンロード

[Neo4j Procedure Template](https://github.com/neo4j-examples/neo4j-procedure-template)をダウンロードします。以降の作業は、このプロジェクトに対して実施します。

```
$ git clone --depth 1 https://github.com/neo4j-examples/neo4j-procedure-template
```

### Javaクラスの作成

適当なクラスを作成します。クラス名・パッケージ名は何でも結構です。

### メソッドの作成

作成したクラスの中に、メソッドを追加します。[@UserFunctionのJavadocページ](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/procedure/UserFunction.html)の中の、「Input declaration」と「Output declaration」を参照し、引数と復帰値に使って良い型を確認します。

今回は、`org.neo4j.graphdb.Path`(`p`)、`String`(`'name'`)、`String`(`'>'`)を引数に取り、生成された文字列を`String`で返すことにします。メソッド定義は、次のようになります。

```java
public String pathJoin(Path p, String key, String delimiter)
```

### アノテーションの追加

Neo4jに、Cypherのファンクションと認識してもらえるように、UserFunction, Descriptionアノテーションをメソッドに、Nameアノテーションを引数に追加します。

#### [@UserFunction](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/procedure/UserFunction.html)（メソッド）

アノテーションの引数には、Cypherで使用するファンクション名を指定します。引数なしで`@UserFunction`とだけ書くと、`パッケージ名.メソッド名`がCypherでのファンクション名になります。

```java
@UserFunction("example.pathjoin")
```

#### [@Description](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/procedure/Description.html)（メソッド）

`call dbms.functions`の`description`欄に表示される文字列を指定します。

```java
@Description("指定したPathオブジェクトについて、各ノードのkeyの値をdemiliterで連結し、文字列で返します。")
```

#### [@Name](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/procedure/Name.html)（引数）

`call dbms.functions`の`signature`欄に表示される文字列を指定します。`defaultValue`を指定すると、引数を省略した場合のデフォルト値となります。

```java
public String pathJoin(@Name("path") Path p,
                       @Name(value = "key in property", defaultValue = "name") String key,
                       @Name(value = "delimiter", defaultValue = ",") String delimiter) {
```

### 中身の実装

[Path](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/graphdb/Path.html)や[Node](http://neo4j.com/docs/java-reference/3.1/javadocs/org/neo4j/graphdb/Node.html)など、使うクラスやインターフェースの[Javadoc](https://neo4j.com/docs/java-reference/current/javadocs/)を参照しながら実装していきます。完成すると、次のようになります。

```java:SampleFunction.java
package example;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.UserFunction;

import java.util.List;
import java.util.stream.Collectors;

public class SampleFunction {

    @UserFunction("example.pathjoin")
    @Description("指定したPathオブジェクトについて、各ノードのkeyの値をdemiliterで連結し、文字列で返します。")
     public String pathJoin(@Name("path") Path p,
                           @Name(value = "key in property", defaultValue = "name") String key,
                           @Name(value = "delimiter", defaultValue = ",") String delimiter) {
        return ((List<Node>) p.nodes())
                .stream().map(
                node -> node.getProperty(key).toString()
            ).collect(Collectors.joining(delimiter));
    }
}
```

> 話を単純にするため、対象とするプロパティのキー(例:`name`)が必ずある前提で作成しています。
>
> Java8を前提とした実装となっていますので、Java7でNeo4jを動作させている場合は適宜読み替えてください。

### Jarファイルの作成

テンプレートに付属の`pom.xml`を使って、Mavenでビルドします。

```
$ mvn package
```

`target`ディレクトリにJarファイルが作成されます。

> デフォルトで`procedure-template-1.0.0-SNAPSHOT.jar`という名前で作成されますので、気に入らない場合は`pom.xml`内の`<artifactId>`や`<version>`を変更してください。

## Neo4jでの作業

### プラグインJarファイルの配置

先ほど作成したJarファイルを、Neo4jインストールディレクトリの`plugins/`ディレクトリの中に置きます。

### Neo4jの再起動

Neo4jを再起動します。

### ファンクションの存在確認

Webインターフェース(http://localhost:7474) を開き、以下を入力します。

```
call dbms.functions
```

次のように、ファンクションの凡例と説明が表示されます。

![dbms.functions](https://qiita-image-store.s3.amazonaws.com/0/10290/721c6164-c358-129e-8bbb-9b40bea83295.png)

> `example.join`はテンプレートに最初からあるファンクションです。

## テスト

### テストデータのロード

Webインターフェースのコンソールに、以下のリンク先のCypherクエリをコピー＆ペーストします。
https://gist.github.com/Makopo/59c5479214769c4fc476715c3b85f8c7

図のようなツリー状のデータができあがります。

![データ](https://qiita-image-store.s3.amazonaws.com/0/10290/b11b608d-b216-4c87-1bee-e56c5912f39b.png)

### クエリ実行

まずは、「あ」から「お」までのパスの要素を、`>`で繫いでみます。

```js
MATCH (a{name:'あ'})
MATCH (o{name:'お'})
MATCH p=(a)-[*]->(o)
RETURN example.pathjoin(p,'name','>')
```

![結果１](https://qiita-image-store.s3.amazonaws.com/0/10290/db5e1978-b562-b02c-c366-e38cffe92039.png)

「あ」から「お」までのパスは1個しかありませんので、結果は1個だけ返ってきます。

次に、「あ」から任意の要素までのパスの要素を、`→`で繫いでみます。

```js
MATCH (a{name:'あ'})
MATCH p=(a)-[*]->()
RETURN example.pathjoin(p,'name','→')
```

![結果２](https://qiita-image-store.s3.amazonaws.com/0/10290/0704c9ae-c1d0-a397-a906-0f0ebc753ce8.png)

「あ」を起点として繋がっている要素は全部で11個。それぞれのパスについて、ファンクションを使って文字列表記に変換されたものが表示されています。