Neo4jとElasticsearchで連携する

# はじめに
Neo4jで保存しているデータに対して、Elasticsearchを使って全文検索したい。

以下の環境で試しました
・Windows10 Home
・Neo4j Community Edition 3.5.5
・Elasticsearch 6.6.2

# 調査
公式に情報があった。
https://neo4j.com/developer/elastic-search/
いくつかプラグインがあるらしい。

・[neo4j-elasticsearch](https://github.com/neo4j-contrib/neo4j-elasticsearch)
Neo4j公式のプラグイン。
現時点では2.2.x, 2.3.x, 3.0.x, and 3.1.xのみをサポート。
最新版では使えない。

★[GraphAware Neo4j-To-Elasticsearch](https://github.com/graphaware/neo4j-to-elasticsearch)
GraphAware社が出しているNeo4j用プラグイン。
Neo4jのデータを自動でElasticSearchにコピーできる。
サポートは有料。

・[GraphAware Graph-Aided-Search](https://github.com/graphaware/graph-aided-search)
GraphAware社が出しているElasticSearch用プラグイン。
~~ElasticSearchでNeo4j内のデータを検索できる。~~
ElasticSearchの検索クエリ内で、Neo4jのデータを使ったスコア付けやフィルタリングができる。
Cypherでスコア付けもできる。
サポートは有料。

今回は[neo4j-to-elasticsearch](https://github.com/graphaware/neo4j-to-elasticsearch)を試した。

# Neo4j-To-Elasticsearch
基本的にReadmeの通りに進める。

まず[ここ](https://products.graphaware.com/)か[ここ](https://graphaware.com/products/)から以下の3つの.jarを探してダウンロードする。（現時点の最新は3.5.4.53）
・GraphAware Neo4j Framework
・GraphAware Neo4j UUID
・GraphAware Neo4j Elasticsearch Integration

ダウンロードした.jarファイルを
$NEO4J_HOME/plugins
にコピーする。

$NEO4J_HOME/conf/neo4j.confに以下を追記する

```neo4j.conf
# おまじない
dbms.unmanaged_extension_classes=com.graphaware.server=/graphaware
com.graphaware.runtime.enabled=true
com.graphaware.module.UIDM.1=com.graphaware.module.uuid.UuidBootstrapper
com.graphaware.module.UIDM.initializeUntil=0

# おまじない
com.graphaware.module.ES.2=com.graphaware.module.es.ElasticSearchModuleBootstrapper
# Elasticsearchのuriとポート
com.graphaware.module.ES.uri=localhost
com.graphaware.module.ES.port=9200
# キューのサイズ。メモリ不足でエラーが出る場合は小さくする
com.graphaware.module.ES.queueSize=10000
# Elasticsearchのbulk APIを使うかどうか。メモリ不足でエラーが出る場合はfalseにしてみる
com.graphaware.module.ES.bulk=true
# System.currentTimeMillis() < initializeUntil にしてNeo4jを起動するとDB全体をインデックスしなおしてくれる
# 普段は0にして、色々いじってるときは2000000000000ぐらいにするといい
com.graphaware.module.ES.initializeUntil=2000000000000
# インデックスするノードの条件。trueだと全ノード。hasLabel("Person")||hasLabel("Company")のように設定できる
com.graphaware.module.ES.node=true

# 後で説明する
# com.graphaware.module.ES.mapping=com.graphaware.module.es.mapping.JsonFileMapping
# com.graphaware.module.ES.file=mapping.json
```

これでとりあえずは動く。
Neo4j を起動するとElasticsearchにノードとリレーションがインデックスされるはず。
（ちなみに、neo4j start ではなく neo4j console で起動するとエラーメッセージを見れるのでおすすめ）
以降、更新したノードも自動でインデックスされる。

↓ Kibanaを使って確認。

```
GET neo4j-index-node/_search
{
  "query": {
    "match_all": {}
  }
}

GET neo4j-index-relationship/_search
{
  "query": {
    "match_all": {}
  }
}
```

Elasticsearch 5.x までならおそらく何も問題が起きないが、Elasticsearch 6.x の場合はこれだけではうまく動かない。

# Elasticsearch 6.x 以降の場合
Elasticsearch 6.x の場合、最初にインデックスされたラベル以外がインデックスされないという現象が起こる。
原因はElasticsearch 6.x からインデックス内に複数のTypeを設定できなくなったため。
Neo4j-To-Elasticsearchではデフォルトでインデックス内にラベルごとのtypeを作ろうとするため、エラーが起きる。

### 対処方法　JsonFileMappingを使う

詳しくは[ここ](https://github.com/graphaware/neo4j-to-elasticsearch/blob/master/docs/json-mapper.md)に乗っているが、追加の設定ファイルで Neo4jのプロパティ→Elasticsearchのプロパティへのマッピングを指定することができる。

neo4j.confにコメントアウトしていた以下の行を追加。

```neo4j.conf
com.graphaware.module.ES.mapping=com.graphaware.module.es.mapping.JsonFileMapping
com.graphaware.module.ES.file=mapping.json
```

neo4j.confと同じディレクトリにmapping.jsonを新規作成する。

```mapping.json
{
  "defaults": {
    "key_property": "uuid",
    "nodes_index": "default-index-node",
    "relationships_index": "default-index-relationship",
    "include_remaining_properties": true
  },
  "node_mappings": [
    {
      "condition": "hasLabel('Person')",
      "index": "nodes-persons",
      "type": "doc",
      "properties": {
        "name": "getProperty('firstName') + ' ' + getProperty('lastName')",
        "created_at": "query('MATCH (n) WHERE id(n) = {id} RETURN toString(n.created_at) as value')"
      }
    },
    {
      "condition": "hasLabel('Company')",
      "index": "nodes-companies",
      "type": "doc",
      "properties": {
        "name": "getProperty('name')"
      }
    }
  ],
  "relationship_mappings": [
    {
      "condition": "isType('WORKS_FOR')",
      "index": "relationships_worksfor",
      "type": "doc"
    }
  ]
}
```

重要なのは"index"の部分で、ラベルごとにindex名を変えてindexとtypeを一対一にしている。
node_mappingsとrelationship_mappingsの中身は各自適当に書き換えてください。
割と自由度が高く、Cypherのクエリを使うこともできる。

以上でElasticsearch 6.x でも動くはずです。

# マッピング
ngramとかkuromoji(形態素解析）とかは使えないの？という疑問を持っている人も多いと思う。
結論から言うと使えます。

ただ、Neo4j側でこのマッピングを設定することはできないので、直接Elasticsearchにマッピングを設定する必要がある。
マッピングは後から変更できないので、既にインデックスしてしまった場合は一度DELETEする。

普通のインデックス同様、以下のように設定できる。

```

PUT nodes-persons
{
   "settings":{
      "analysis":{
        "analyzer":{
          "ja_ngram":{  
              "type":"custom",
              "tokenizer":"ja_ngram_tokenizer"
          }
        },
        "tokenizer":{
          "ja_ngram_tokenizer":{  
            "type":"nGram",
            "min_gram":"1",
            "max_gram":"2",
            "token_chars":[  
                "letter",
                "digit"
            ]
          }
        }
      }
   },
  "mappings": {
    "doc": { 
      "properties": { 
        "name":{
          "type": "text",
          "analyzer":"ja_ngram"
        },
        "created_at": {
          "type": "date",
          "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
        }
      }
    }
  }
}
```

マッピングをPUTした後、Neo4jをrestartすればこれに沿ってインデックスしてくれるはず。

datetimeに関しては少しハマった。Neo4jのDateTimeプロパティはデフォルトでは扱いにくいマッピングが設定されてしまう。またElasticsearchのdateに直接変換することもできないらしい。そのためmapping.jsonでtoStringしたものを、マッピングでフォーマットを指定して読み込んでいます。

# Neo4jからクエリを投げる
以下のようにCypherからElasticsearchにクエリを投げることもできる。
`CALL ga.es.queryNode('{\"query\":{\"match_all\":{}}') YIELD node, score RETURN node, score`

ただ、indexをmapping.jsで変更した場合にはうまく動かないようです…


# おしまい
十分使えそうです。