GraphDB KuzuのWebAssemblyをブラウザ上で動かしneo4j-nvlで可視化してみたメモ

# 概要
グラフDBをブラウザ上で試してみた。
[kuzu-shell](https://kuzu-shell.netlify.app/)に使われている[kuzu-wasm][*1]を利用。

[この時点のソース](https://github.com/hibohiboo/kartagraph-monorepo/blob/0b09d05ea6d7e2d4b5e0c92e5a15b14c8e2405f5/apps/kuzu/src/App.tsx)

# kuzu-wasmを動かす
## vite設定
wasmを動かすために、excludeの設定と、serverのヘッダ設定の追加が必要であった。([SharedArrayBuffer](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)を利用しているため)

> Prerequisite: [Enable Cross-Origin-isolation](https://web.dev/articles/cross-origin-isolation-guide?hl=en#enable_cross-origin_isolation)

```vite.config.ts
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  build: { target: 'esnext' },
  optimizeDeps: {
    exclude: ['@kuzu/kuzu-wasm'],
    esbuildOptions: {
      target: 'es2020',
    },
  },
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});
```

## 型定義
typescriptの型定義が見つからなかったので暫定的に自分で定義した。

```ts:types.ts
declare module '@kuzu/kuzu-wasm' {
  export default function kuzu_wasm(): Promise<Kuzu>;

  export interface Kuzu {
    Database: () => Promise<Database>;
    Connection: (db: Database) => Promise<Connection>;
    FS: {
      writeFile: (path: string, content: string) => void;
    };
  }

  export interface Database {
    close: () => void;
  }

  export interface Connection {
    execute: (query: string) => Promise<QueryResult>;
  }

  export interface QueryResult {
    table: {
      toString: () => string;
    };
  }
}
```


## サンプルコード

[csvファイル](https://github.com/hibohiboo/kartagraph-monorepo/blob/0b09d05ea6d7e2d4b5e0c92e5a15b14c8e2405f5/apps/kuzu/public/data/user.csv)をDBに読み込むために、いったんwasm上に書き込む必要がある点に注意。

```ts:App.ts
import kuzu_wasm from '@kuzu/kuzu-wasm';
import { useEffect } from 'react';

function App() {

  useEffect(() => {
    (async () => {
      const kuzu = await kuzu_wasm();
      const db = await kuzu.Database();
      const conn = await kuzu.Connection(db);

      // Write files
      kuzu.FS.writeFile('/follows.csv', await (await fetch('/data/follows.csv')).text());
      kuzu.FS.writeFile('/city.csv', await (await fetch('/data/city.csv')).text());
      kuzu.FS.writeFile('/lives-in.csv', await (await fetch('/data/lives-in.csv')).text());
      kuzu.FS.writeFile('/user.csv', await (await fetch('/data/user.csv')).text());

      // Create schema
      await conn.execute('CREATE NODE TABLE User(name STRING, age INT64, PRIMARY KEY (name))');
      await conn.execute('CREATE NODE TABLE City(name STRING, population INT64, PRIMARY KEY (name))');
      await conn.execute('CREATE REL TABLE Follows(FROM User TO User, since INT64)');
      await conn.execute('CREATE REL TABLE LivesIn(FROM User TO City)');

      // Insert data
      await conn.execute('COPY User FROM "/user.csv"');
      await conn.execute('COPY City FROM "/city.csv"');
      await conn.execute('COPY Follows FROM "/follows.csv"');
      await conn.execute('COPY LivesIn FROM "/lives-in.csv"');

      // Execute Cypher query
      const response = await conn.execute(
        `
         MATCH (a:User)
         RETURN *;
         `,
      );
      const users = JSON.parse(response.table.toString());
      console.log(users)

      const relsResponse = await conn.execute(
        `
         MATCH (a:User)-[r:Follows]->(b:User)
         RETURN *;
         `,
      );
      const rels = JSON.parse(relsResponse.table.toString());
      console.log(rels);

    })();
  }, []);

  return (
    <div style={{ width: '100%', height: 500 }}>
    </div>
  );
}

export default App;

```

## 実行結果

<details><summary>users</summary><div>

```json:console.log(users)
[
    {
        "a": {
            "_ID": {
                "offset": "0",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Adam",
            "age": "30"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "1",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Karissa",
            "age": "40"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Zhang",
            "age": "50"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "3",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Noura",
            "age": "25"
        }
    }
]
```

</div></details>

<details><summary>rels</summary><div>

```json:console.log(rels)
[
    {
        "a": {
            "_ID": {
                "offset": "0",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Adam",
            "age": "30"
        },
        "b": {
            "_ID": {
                "offset": "1",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Karissa",
            "age": "40"
        },
        "r": {
            "_SRC": {
                "offset": "0",
                "table": "0"
            },
            "_DST": {
                "offset": "1",
                "table": "0"
            },
            "_LABEL": "Follows",
            "_ID": {
                "offset": "0",
                "table": "2"
            },
            "since": "2020"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "0",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Adam",
            "age": "30"
        },
        "b": {
            "_ID": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Zhang",
            "age": "50"
        },
        "r": {
            "_SRC": {
                "offset": "0",
                "table": "0"
            },
            "_DST": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "Follows",
            "_ID": {
                "offset": "1",
                "table": "2"
            },
            "since": "2020"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "1",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Karissa",
            "age": "40"
        },
        "b": {
            "_ID": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Zhang",
            "age": "50"
        },
        "r": {
            "_SRC": {
                "offset": "1",
                "table": "0"
            },
            "_DST": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "Follows",
            "_ID": {
                "offset": "2",
                "table": "2"
            },
            "since": "2021"
        }
    },
    {
        "a": {
            "_ID": {
                "offset": "2",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Zhang",
            "age": "50"
        },
        "b": {
            "_ID": {
                "offset": "3",
                "table": "0"
            },
            "_LABEL": "User",
            "name": "Noura",
            "age": "25"
        },
        "r": {
            "_SRC": {
                "offset": "2",
                "table": "0"
            },
            "_DST": {
                "offset": "3",
                "table": "0"
            },
            "_LABEL": "Follows",
            "_ID": {
                "offset": "3",
                "table": "2"
            },
            "since": "2022"
        }
    }
]
```

</div></details>

# 可視化

```
npm i @neo4j-nvl/react layout-base
```

## vite設定

```ts:vite.config.ts
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  build: { target: 'esnext' },
  optimizeDeps: {
    exclude: ['@kuzu/kuzu-wasm', '@neo4j-nvl/layout-workers'],
    include: [
      '@neo4j-nvl/layout-workers > cytoscape',
      '@neo4j-nvl/layout-workers > cytoscape-cose-bilkent',
      '@neo4j-nvl/layout-workers > @neo4j-bloom/dagre',
      '@neo4j-nvl/layout-workers > bin-pack',
      '@neo4j-nvl/layout-workers > graphlib',
      '@segment/analytics-next',
      'concaveman',
      'layout-base',
    ],
    esbuildOptions: {
      target: 'es2020',
    },
  },
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

```

## サンプルコード

```ts:App.ts
import kuzu_wasm from '@kuzu/kuzu-wasm';
import { InteractiveNvlWrapper } from '@neo4j-nvl/react';
import { useEffect, useState } from 'react';

function App() {
  const [nodes, setNodes] = useState([]);
  const [rels, setRels] = useState([]);

  useEffect(() => {
    (async () => {
      const kuzu = await kuzu_wasm();
      const db = await kuzu.Database();
      const conn = await kuzu.Connection(db);
      // get remote csv to wasm filesystem
      kuzu.FS.writeFile('/follows.csv', await (await fetch('/data/follows.csv')).text());
      kuzu.FS.writeFile('/city.csv', await (await fetch('/data/city.csv')).text());
      kuzu.FS.writeFile('/lives-in.csv', await (await fetch('/data/lives-in.csv')).text());
      kuzu.FS.writeFile('/user.csv', await (await fetch('/data/user.csv')).text());

      // Create schema
      await conn.execute('CREATE NODE TABLE User(name STRING, age INT64, PRIMARY KEY (name))');
      await conn.execute('CREATE NODE TABLE City(name STRING, population INT64, PRIMARY KEY (name))');
      await conn.execute('CREATE REL TABLE Follows(FROM User TO User, since INT64)');
      await conn.execute('CREATE REL TABLE LivesIn(FROM User TO City)');

      // Insert data
      await conn.execute('COPY User FROM "/user.csv"');
      await conn.execute('COPY City FROM "/city.csv"');
      await conn.execute('COPY Follows FROM "/follows.csv"');
      await conn.execute('COPY LivesIn FROM "/lives-in.csv"');

      // Execute Cypher query
      const response = await conn.execute(
        `
         MATCH (a:User)
         RETURN *;
         `,
      );
      const users = JSON.parse(response.table.toString());
      type ID = { offset: string; table: string };
      const getId = (id: ID): string => `id_${id.table}_${id.offset}`;
      const nodes = users.map(({ a }: { a: { _ID: ID; name: string } }) => ({ id: getId(a._ID), caption: a.name }));
      setNodes(nodes);

      const relsResponse = await conn.execute(
        `
         MATCH (a:User)-[r:Follows]->(b:User)
         RETURN *;
         `,
      );
      const rels = JSON.parse(relsResponse.table.toString());

      const relsData = rels.map(
        ({ r }: { a: { _ID: ID; name: string }; r: { _ID: ID; _SRC: ID; _DST: ID; since: string }; b: { _ID: ID; name: string } }) => ({
          from: getId(r._SRC),
          to: getId(r._DST),
          id: getId(r._ID),
          caption: r.since,
        }),
      );
      setRels(relsData);
    })();
  }, []);

  return (
    <div style={{ width: '100%', height: 500 }}>
      <InteractiveNvlWrapper
        nvlOptions={{ useWebGL: false, initialZoom: 2.6 }}
        nodes={nodes}
        rels={rels}
        mouseEventCallbacks={{
          onZoom: true,
          onPan: true,
        }}
      />
    </div>
  );
}

export default App;
```



# 参考
[kuzu](https://docs.kuzudb.com/)
[kuzu-wasm][*1]
[shell-js](https://github.com/unswdb/kuzu-wasm/blob/main/packages/kuzu-shell/src/components/Shell.js)
[quarto-graph-experiments](https://github.com/Analect/quarto-graph-experiments/blob/main/docs/demo/kuzu-quarto-live.qmd)
[@neo4j-nvl/react](https://neo4j.com/docs/nvl/current/react-wrappers/)

[無料で手軽にNeo4j aura で Graph DBを使ってみる](https://qiita.com/FukuharaYohei/items/db7efd10d3d36a0eedb8)
[Cypher　よく使うクエリ](https://qiita.com/tx2/items/f20727612abf09544d6d)

[Neo4jことはじめ](https://qiita.com/tikamoto/items/c3a1bba12e9b83aee42a)
[neo4j をインストールしてから Cytoscape と組み合わせるまでの話](https://qiita.com/hamachi4708/items/5c5d853269b68426704b)

 [Neo4jの可視化ライブラリまとめ](https://www.slideshare.net/mkiuchi4/201769-neo4j#16)
 [React + TypeScriptで使えるネットワークグラフ系ライブラリ3つ + α](https://qiita.com/ko-he-8/items/70b85158404062eabcf4)
[ネットワークグラフ描画ライブラリ7個まとめ](https://qiita.com/SuyamaDaichi/items/c47dc74cfefd92516e28)
[antvis/G6](https://github.com/antvis/G6/blob/v5/README.en-US.md)

[*1]:https://github.com/unswdb/kuzu-wasm