1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-16

概要

グラフDBをブラウザ上で試してみた。
kuzu-shellに使われているkuzu-wasmを利用。

この時点のソース

kuzu-wasmを動かす

vite設定

wasmを動かすために、excludeの設定と、serverのヘッダ設定の追加が必要であった。(SharedArrayBufferを利用しているため)

Prerequisite: 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の型定義が見つからなかったので暫定的に自分で定義した。

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ファイルをDBに読み込むために、いったんwasm上に書き込む必要がある点に注意。

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;

実行結果

users
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"
        }
    }
]
rels
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"
        }
    }
]

可視化

npm i @neo4j-nvl/react layout-base

vite設定

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',
    },
  },
});

サンプルコード

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
kuzu-wasm
shell-js
quarto-graph-experiments
@neo4j-nvl/react

無料で手軽にNeo4j aura で Graph DBを使ってみる
Cypher よく使うクエリ

Neo4jことはじめ
neo4j をインストールしてから Cytoscape と組み合わせるまでの話

Neo4jの可視化ライブラリまとめ
React + TypeScriptで使えるネットワークグラフ系ライブラリ3つ + α
ネットワークグラフ描画ライブラリ7個まとめ
antvis/G6

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?