m's blog

備忘録とかメモとか

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

今回は、前回実装したターゲット選択用のセレクタを使って、特定のPersonFOLLOW関係があるPersonを検索するコードを追加していきます。

また前回同様、アプリの作成を通して、JavaScriptとNeo4jの連携について解説していきたいと思います。

↓ 「Dockerで試すNeo4j」シリーズ記事一覧はこちら

目次

はじめに

今回の記事では、以下の記事で追加したデータを使用します。

データを追加していない場合は、上記の記事を参考にデータを追加してください。

注意事項

トライアルという事で、クライアントからNeo4jにアクセスしていますが、アプリを公開する場合などはサーバ経由でアクセスするようにするなど、調整してください。

構成

解説を進める前に、最終的なファイル構成を紹介しておきたいと思います。

最終的なファイル構成は、以下のようになります。

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

前回紹介したものと同じもので、次回記事で追加するファイルも含んでいます。

また、特にファイルの追加や変更をしない、publicなどのディレクトリの中身は表示していません。

グラフ表示用のデータを取得

では早速、フォロー検索機能を実装していきます。

実装は、ここまでのコードを調整・追加する形で進めて行きます。

まずは、特定のPersonFOLLOW関係でつながっているPersonを検索・取得する関数を追加します。

前回作成したコードに、src/packages/Search/getElementsファイルを追加して、以下のように記述してください。

import { PathSegment, QueryResult } from 'neo4j-driver'
import { Edge, Elements, Node } from "../@types/App";
import { createDriver } from "./createDriver";


export const getElements = async (target: Node<any>): Promise<Elements> => {
    const driver = await createDriver()

    const session = await driver.session()

    const elements: Elements = {
        nodes: [],
        edges: [],
    }

    const result: QueryResult | void = await session
        .run(
            target
                ? `MATCH p=(s)-[:FOLLOW *]->(e {id:$target}) RETURN p, s, e, ID(s) as sid, ID(e) as eid UNION MATCH p=(s {id:$target})-[:FOLLOW *]->(e) RETURN p, s, e, ID(s) as sid, ID(e) as eid`
                : `MATCH p=(s)-[:FOLLOW]->(e) RETURN p, s, e, ID(s) as sid, ID(e) as eid`,
            { target: target?.properties?.id }
        )
        .catch((error) => {
            console.log(error)
        })

    if (!result) {
        session.close()
        await driver.close()
        return elements
    }

    result.records.forEach((record) => {
        const path = record.get('p')
        const e = record.get('e')
        const isTargetEnd = target?.properties?.id === e.properties.id

        path.segments.forEach((segment: PathSegment<any>) => {
            const start: Node<any> | any = segment.start
            const startId = start.identity.toString()
            const startProperties = start.properties
            const end: Node<any> | any = segment.end
            const endId = end.identity.toString()
            const endProperties = end.properties

            const duplicate = elements.edges.find(
                (edge: Edge) =>
                    edge.data.source === startId && edge.data.target === endId
            )
            if (duplicate) {
                return
            }

            const color = (nodeId: string): string => {
                if (nodeId === target?.identity?.toString()) {
                    return '#ea4b4b'
                }
                if (!isTargetEnd) {
                    return '#95e76c'
                }
                return '#718ef8'
            }

            elements.nodes.push({
                data: {
                    id: startId,
                    name: startProperties?.name,
                    label: start.labels[0],
                    color: color(startId),
                },
            })
            elements.nodes.push({
                data: {
                    id: endId,
                    name: endProperties?.name,
                    label: end.labels[0],
                    color: color(endId),
                },
            })
            elements.edges.push({
                data: {
                    source: startId,
                    target: endId,
                    relationship: path.segments[0].relationship.type,
                },
            })
        })
    })

    session.close()
    await driver.close()

    return elements
}

少し長いですが、ポイントをまとめると以下のような感じです。

  • session.run()の部分で、Neo4jにアクセスして、FOLLOW関係を取得
  • 取得した結果をグラフ表示用に整形してreturn

session.run()で使用するクエリは、getElements関数の引数targetがある場合とない場合で異なります。

targetが指定されている場合は、指定されたターゲットに向かう全てのFOLLOWリレーションと、指定されたターゲットから他のノードへ向かう全てのFOLLOWリレーションを検索して結合した結果が返されます。

targetが指定されていない場合は、全てのFOLLOWリレーションが返されます。

また、整形したデータはグラフ表示に使用しますが、グラフ表示については次回記事で解説したいと思います。

後述しますが、今回は、データをリスト表示するところまでにとどめたいと思います。

「Search」コンポーネントの調整

続いて、前回記事で追加したSearchコンポーネントを調整して、フォロー検索機能を追加します。

src/packages/Search/Search.tsxを以下のように書き換えてください。

import Autocomplete from "@material-ui/lab/Autocomplete";
import { Elements, Node } from "../@types/App";
import { Button, TextField } from "@material-ui/core";
import React, { ChangeEvent, useState } from "react";
import { getElements } from "./getElements";

type Props = {
    persons: Node<any>[] | []
    handleSearchButtonClick: (elements: Elements) => void
}

export default function Search({ persons, handleSearchButtonClick }: Props) {
    const [target, setTarget] = useState<Node<any>>(null)

    const handleTargetChange = (event: ChangeEvent<{}>, newTarget: Node<any>) => setTarget(newTarget)

    return <div>
        <Autocomplete
            id="target"
            value={target}
            onChange={handleTargetChange}
            options={persons}
            getOptionLabel={(person: Node<any>) => person?.properties?.id || ''}
            style={{ width: 300 }}
            renderInput={(params) => (
                <TextField {...params} label="target" variant="outlined"/>
            )}
        />
        <Button onClick={async () => handleSearchButtonClick(await getElements(target))} variant={'outlined'}>
            search
        </Button>
    </div>
}

主な変更は、以下の検索実行用のボタンの部分です。

        <Button onClick={async () => handleSearchButtonClick(await getElements(target))} variant={'outlined'}>
            search
        </Button>

ボタンがクリックされると、先ほど定義したgetElements関数によってフォロー情報が取得されます。

取得されたフォロー情報を引数として、SearchコンポーネントのプロパティhandleSearchButtonClickが実行されます。

handleSearchButtonClickは次の項目で実装しますが、引数となるフォロー情報をステートとて保持するような関数です。

仕上げ

最後に、ここまでのコードをsrc/App.tsxで統合します。

src/App.tsxを以下のように書き換えてください。

import React, { useEffect, useState } from 'react';
import { Elements, Node } from "./packages/@types/App";
import { getAllPersons } from "./packages/Search/getAllPersons";
import Search from "./packages/Search/Search";

function App() {
    const [elements, setElements] = useState<Elements>({ nodes: [], edges: [] })
    const [persons, setPersons] = useState<Node<any>[] | []>([])

    useEffect(() => {
        (async () => setPersons(await getAllPersons()))()
    }, [])

    return (
        <div>
            <div>
                <Search persons={persons} handleSearchButtonClick={(elements) => setElements(elements)}/>
            </div>
            {elements.nodes.length > 0 && <div>
                <hr/>
                <h3>nodes</h3>
                <ul>
                    {elements.nodes.map((node) => <li>{JSON.stringify(node.data)}</li>)}
                </ul>
                <hr/>
                <h3>edges</h3>
                <ul>
                    {elements.edges.map((edge) => <li>{JSON.stringify(edge.data)}</li>)}
                </ul>
            </div>}
        </div>
    )
}

export default App;

Searchコンポーネントの調整に合わせて、SearchコンポーネントにhandleSearchButtonClickプロパティの指定を追加しています。

これにあわせて、elementsステートの定義も追加しています。

また、更新されるelementsの確認用に、elements.nodeselements.edgesをリスト表示する部分を追加しています。

動作確認

フォロー検索用のコードの追加・調整が完了したので、実際に動かして動作を確認してみます。

以下のコマンドで、Reactアプリをスタートしてください。

yarn start

アプリを起動したら、http://localhost:3000/にアクセスしてください。

ターゲット入力欄で「Ichiro」を選択、または入力して、「Search」ボタンをクリックしてください。

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

以下のような、「nodes」一覧と「edges」一覧が表示されれば、実装成功です。

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

【Neo4j】Dockerで試すNeo4j【第5回/JavaScript編 その2】

サンプルコード

以下のリポジトリに「Dockerで試すNeo4j」シリーズ全話分のコードを設置しています。

うまく動かない場合は、上記のリポジトリをクローンして試してみてください。

まとめ

以上、Neo4jとReactを使って、フォロー検索機能を追加してみました。

今回は、フォロー検索結果をリスト表示で確認しまいたが、次回はいよいよ、フォロー検索した結果をグラフ表示させてみたいと思います。

↓ 「Dockerで試すNeo4j」シリーズ記事一覧はこちら