neo4jが楽しすぎるので勉強してみた (2)

[neo4jが楽しすぎるので勉強してみた (1)](https://qiita.com/MariMurotani/items/8b1c4f4e1b9b4dbb793f)
に引き続き、neo4jを使った開発で理解したことなどについてまとめて行きます！

## グラフのプロジェクション
膨大なデータに対して今まで紹介したアルゴリズムを使った探索をすると負荷がかかってしまいます。
そのため、必要な分のグラフのプロジェクション(投影)をしてデータ分析をする必要があります。
Cypherクエリ使ったプロジェクションとNativeProjectionの2種類が用意されていますが、
データの絞り込みをするという観点では、Cypher Projectionを利用するしかなさそうです。
プロジェクショングラフはインメモリで保存されるため、高速にデータを分析することが可能です。
プロジェクショングラフのインスタンスはneo4jの再起動か明示的な削除命令で削除できます。
RDBのViewに似ている機能ですね！

![Screenshot 2025-02-04 at 16.27.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/4a9bf83b-e8ff-7b47-bfa1-7dbc379704be.png)

*1 使えるプロパティも数値やベクトルに限定されている


### Native Projection
json形式で簡単に記述でき可読性が高いですが、条件フィルタの適用などができません

```cypher
CALL gds.graph.project(
  'myFoodSubAromaProjectionNative',
  {
     FoodSubType: {},
     Compound: {},
     Aroma: {}
  },
  {
    CONTAINS: {
      orientation: 'UNDIRECTED'
  },
    SCENTED: {
      orientation: 'UNDIRECTED'
  }
)
YIELD graphName, 
nodeCount, relationshipCount;
```

### Cypher Projection　
複雑なフィルタリング条件式に対応できる反面、記述式が複雑です

```cypher
CALL gds.graph.project.cypher(
  'myFoodSubAromaProjectionCypher', *1
  'MATCH (n) WHERE n:FoodSubType OR n:Compound OR n:Aroma
RETURN id(n) AS id, labels(n) AS labels', *2
  'MATCH (n)-[r:CONTAINS|SCENTED]-(m) RETURN id(n) AS source, id(m) AS target, type(r) AS type', *3
  { validateRelationships: false } *4
)
YIELD graphName, nodeCount, relationshipCount;
```

1. グラフ名
2. ノードクエリ
　グラフに含めるノードを記述
   - id: ノードの一意の識別子
   - labels: ノードのラベル（リスト形式が推奨）
（オプション）プロパティを含める場合は任意の名前。
3. リレーションシップクエリ
　グラフに含めるノードを記述
   - source: リレーションシップの開始ノードのID
   - target: リレーションシップの終了ノードのID
   - type: リレーションシップのタイプ
4. validateRelationships
	defaultがtrueでリレーションシップの両端のノードを確認する

## PageRankで重要食品を探す (1)
筆者が、[分子ガストロノミー](https://ja.wikipedia.org/wiki/%E5%88%86%E5%AD%90%E3%82%AC%E3%82%B9%E3%83%88%E3%83%AD%E3%83%8E%E3%83%9F%E3%83%BC) に傾倒しているため、このようなデータ構造のネットワークを構築しています！
あまり詳しくは説明しませんが、アロマを共有しているものを組み合わせると美味しいという土台の理論を下にデータベースの構築と分析をしています。
今回は、(1)で紹介したアルゴリズムの中から、皆さんご存知のPageRankを実践してみます。
P他のノードからリンクされているノードほど重要であるとみなされ、
更にリンクしているノード自体が重要であればそのリンクの影響力がさらに強くなるというロジックです。
つまり、算出しようとしているランキングは、「家に置いておくと、色々なものに合わせやすい食材」という事になります。

![Screenshot 2025-02-04 at 16.34.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/1bbddb2f-d0bb-b7d8-ee64-2a382b50f43a.png)

まずは、レモンに紐づくレモンのサブタイプ(レモンの皮とか、ジュースとか)や香り成分を検索して確認してみます。

```cypher
MATCH 
 (f:Food{id:'lemon'})-
 [:HAS_SUBTYPE]->
 (fs:FoodSubType)-
 [:SCENTED]->
 (a:Aroma)
RETURN f,fs,a;
```

[neo4jのクライアントツール](https://neo4j.com/docs/operations-manual/current/installation/)が便利に利用できます。
とても美しくて使いやすいです。

![Screenshot 2025-02-04 at 16.37.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/e44a0aa0-31b0-6989-1ebe-e28749bd06d7.png)

次に、重要な食品ランキングを算出してみます。
実際に発行したCypherクエリはこのような感じです。

```cypher
// 除外するカテゴリを指定
WITH ['dishes', 'snack_foods', 'frozen_desserts', 'other_beverages', 'confectioneries', 'beverages'] AS excludedGroups

// 中心性分析を実行：PageRankアルゴリズムを使用
CALL gds.pageRank.stream('myFoodSubAromaProjection')
YIELD nodeId, score
ORDER BY score DESC
WITH excludedGroups, COLLECT(nodeId) AS foodSubNodeIds, COLLECT(score) AS scores

// 特定カテゴリを除外してFoodSubTypeを取得
UNWIND range(0, size(foodSubNodeIds) - 1) AS idx
MATCH (f:Food)-[:HAS_SUBTYPE]->(fs:FoodSubType)
WHERE id(fs) = foodSubNodeIds[idx] AND NOT f.group IN excludedGroups
WITH f, AVG(scores[idx]) AS avgScore, COLLECT(fs.name) AS subTypes
RETURN f.id, f.name, avgScore, subTypes
ORDER BY avgScore DESC
LIMIT 100
```

#### 1. プロジェクションされたグラフの呼び出し
```cypher
CALL gds.pageRank.stream('myFoodSubAromaProjection')
```
グラフ myFoodSubAromaProjection に対して PageRank アルゴリズム を実行し、その結果をストリームとして出力します。

#### 2. streamからnodeとpageRankのscoreを出力
```cypher
YIELD nodeId, score
```
[YIELD](https://neo4j.com/docs/cypher-manual/current/clauses/call/#query-call-yield)は、Neo4jのCALLで得られる結果の中から特定のカラムを取得します。

#### 3.  スコア順に並べ、特定カテゴリを除外
```cypher
ORDER BY score DESC
WITH excludedGroups, COLLECT(nodeId) AS foodSubNodeIds, COLLECT(score) AS scores
```
[COLLECT](https://neo4j.com/docs/cypher-manual/current/functions/aggregating/#functions-collect)を利用すると配列の形式に変換できます。
[WITH](https://neo4j.com/docs/cypher-manual/current/clauses/with/)を指定すると、後続処理で変数が利用できます。

#### 4. foodSubNodeIdsを展開して
```cypher
UNWIND range(0, size(foodSubNodeIds) - 1) AS idx
  MATCH (f:Food)-[:HAS_SUBTYPE]->(fs:FoodSubType)
  WHERE id(fs) = foodSubNodeIds[idx] AND NOT f.group IN excludedGroups
```
[UNWIND](https://neo4j.com/docs/cypher-manual/current/clauses/unwind/)を利用すると、リストを個々の要素に分解し、それらをクエリの処理対象として展開できます。
forループのような役割です。
後続処理の[MATCH](https://neo4j.com/docs/cypher-manual/current/clauses/match/)〜[WHERE](https://neo4j.com/docs/cypher-manual/current/clauses/where/)でindexを利用しているのが分かります。

#### 5. 結果を返却します
```cypher
WITH f, AVG(scores[idx]) AS avgScore, COLLECT(fs.name) AS subTypes
RETURN f.name, avgScore, subTypes
ORDER BY avgScore DESC
LIMIT 100
```
[WITH](https://neo4j.com/docs/cypher-manual/current/clauses/with/)で返却に必要な変数を列挙したり[AVG](https://neo4j.com/docs/cypher-manual/current/functions/aggregating/#functions-avg)を使い平均したりします。
[RETURN](https://neo4j.com/docs/cypher-manual/current/clauses/return/)で実際にクライアントに返却する値を返します。
[ORDER BY](https://neo4j.com/docs/cypher-manual/current/clauses/order-by/)でソートをします。
[LIMIT](https://neo4j.com/docs/cypher-manual/current/clauses/limit/)で返却する件数の指定をします。

#### 6. 結果の確認
クエリを発行すると、下記のような結果が得られました

| f.name        | avgScore      |
|-------------|--------------|
| Cornmint    | 1.048684942  |
| Allspice    | 1.008469312  |
| Spearmint   | 0.9632756378 |
| Black walnut| 0.9135476196 |
| Cumin       | 0.8862517587 |
| Red tea     | 0.8410131497 |
| Coffee mocha| 0.7756915666 |
| Orange mint | 0.7753240457 |
| Cardamom    | 0.7699933064 |


いかがでしたでしょうか？
クエリを覚えるのが少し大変そうですが、RDBで隠れているエッジを分析して増やしたり、
それに対して分析したりする事で、今まで検索できなかった結果が検索できそうですね。
また、ベクトル計算用の関数を利用すると、ディープラーニングのモデルに投入する特徴量を同じものを活用して検索できるので、かなり幅広い活用方法が見いだせると思いました！

### リファレンス
- Syntaxリファレンス
https://neo4j.com/docs/cypher-manual/current/clauses/
- 関数リファレンス
https://neo4j.com/docs/cypher-manual/current/functions/
- その他
https://neo4j.com/blog/upgrade-your-graph-data-science-library-now/