Apache AGEでグラフデータを扱ってみる

Azureでグラフデータを扱えるDBとしてCosmos DB for Apache Gremlinがあるが、同じようなことはPostgreSQLのエクステンションであるApache AGEでも可能なので、ちょっと試してみた。Apache AGEは、グラフデータをCypherというグラフデータのクエリ言語で扱えるようにするエクステンションで、CypherクエリをPostgreSQLのQueryツリーに変換する、グラフデータで必要となるデータ型や演算機能などを提供するものだ。この辺りはPostGISが、地理情報システムで必要なデータ型やダイクストラ法などの演算を提供するのと非常によく似ている。

Apache AGE入りPostgreSQLを動かす

Quick Startを参考にすると良い。

PostgreSQLをインストール後にAGEをビルドする、あるいはDockerコンテナを利用することが可能なので、今回はmacOS上でお手軽にコンテナを使ってみる。Docker Desktopを先にダウンロードしてインストールしておく。

Dockerでお手軽に

残念ながらaarch64は無いので、linux/amd64のイメージになるが、性能が問題になることは無いだろう。

docker pull apache/age

おもむろに起動。

docker run \
  --name age  \
  -p 5455:5432 \
  -e POSTGRES_USER=$(whoami) \
  -e POSTGRES_PASSWORD=passw0rd \
  -e POSTGRES_DB=postgres \
  -d apache/age

接続する。

docker exec -it age psql -d postgres -U $(whoami)
psql (16.3 (Debian 16.3-1.pgdg120+1))
Type "help" for help.

postgres=#

メタ情報のチェック

Apache AGEが入っていることを確認する。

\dx
                 List of installed extensions
  Name   | Version |   Schema   |         Description          
---------+---------+------------+------------------------------
 age     | 1.5.0   | ag_catalog | AGE database extension
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

Apache AGEで何が提供されているかを見てみる。

\dx+ age
Objects in extension "age"
Object description                                                                              
---------------------------------------------------------------------------
 cast from ag_catalog.agtype to ag_catalog.graphid
 cast from ag_catalog.agtype to bigint
 cast from ag_catalog.agtype to boolean
 cast from ag_catalog.agtype to double precision
 cast from ag_catalog.agtype to integer
 cast from ag_catalog.agtype to integer[]
 cast from ag_catalog.agtype to smallint
 cast from ag_catalog.agtype to text
 cast from ag_catalog.graphid to ag_catalog.agtype
 cast from bigint to ag_catalog.agtype
 cast from boolean to ag_catalog.agtype
 cast from double precision to ag_catalog.agtype
 function ag_catalog.age_abs("any")
 function ag_catalog.age_acos("any")
......

サーチパスの設定

サーチパスにageのカタログを追加する。

SET search_path = ag_catalog, "$user", public;

面倒なら、ALTER USERでユーザ・ロールにサーチパスを設定すると良い。postgresql.confに設定するなら、docker execで。

基本的なクエリを実行してみる

グラフの作成

まずはグラフを作成する。

SELECT create_graph('graph_name');
NOTICE:  graph "graph_name" has been created
 create_graph 
--------------
 
(1 row)

グラフを確認する。

SELECT * FROM ag_graph;
 graphid |    name    | namespace  
---------+------------+------------
   16972 | graph_name | graph_name
(1 row)

vertex / edgeの作成

グラフデータでは、頂点をvertex、頂点と頂点を結ぶ線分をedgeと呼ぶ。まず、vertexを1つ作成する。

SELECT * 
FROM cypher('graph_name', $$
    CREATE (n)
$$) as (v agtype);
 v 
---
(0 rows)

“0 rows”となっているが、作成されていることに注意。

vertexにはラベルを付けることが出来る。

SELECT * 
FROM cypher('graph_name', $$
    CREATE (:label)
$$) as (v agtype);
 v 
---
(0 rows)

グラフに対するクエリにはMATCH句を使う。

SELECT * 
FROM cypher('graph_name', $$
    MATCH (v)
    RETURN v
$$) as (v agtype);
                                  v                                  
---------------------------------------------------------------------
 {"id": 281474976710657, "label": "", "properties": {}}::vertex
 {"id": 844424930131969, "label": "label", "properties": {}}::vertex
(2 rows)

2つのvertex (vertices) にedgeを作成する。

SELECT * 
FROM cypher('graph_name', $$
    MATCH (a:label), (b:label)
    WHERE a.property = 'Node A' AND b.property = 'Node B'
    CREATE (a)-[e:RELTYPE]->(b)
    RETURN e
$$) as (e agtype);
 e 
---
(0 rows)

edgeにはプロパティ、属性を設定できる。属性付きのedgeを作成する。

SELECT * 
FROM cypher('graph_name', $$
    MATCH (a:label), (b:label)
    WHERE a.property = 'Node A' AND b.property = 'Node B'
    CREATE (a)-[e:RELTYPE {property:a.property + '<->' + b.property}]->(b)
    RETURN e
$$) as (e agtype);
 e 
---
(0 rows)

ここまででedgeが2つ作成されていることを確認する。

SELECT *
FROM cypher('graph_name', $$
    MATCH (e)
    RETURN e
$$) as (e agtype);
                                  e                                  
---------------------------------------------------------------------
 {"id": 281474976710657, "label": "", "properties": {}}::vertex
 {"id": 844424930131969, "label": "label", "properties": {}}::vertex
(2 rows)

vertex / edgeの削除

edgeを削除する。

SELECT *
FROM cypher('graph_name', $$
    MATCH (e)
    DELETE e
$$) as (e agtype);
 e 
---
(0 rows)

削除されているかを確認する。

SELECT *
FROM cypher('graph_name', $$
    MATCH (e)
    RETURN e
$$) as (e agtype);
 e 
---
(0 rows)

vertexを削除する。

SELECT *
FROM cypher('graph_name', $$
    MATCH (v)
    DELETE v
$$) as (v agtype);
 v 
---
(0 rows)

グラフの削除

グラフを削除する。drop_graph()の2つ目の引数はcascade。trueにすると、関連するオブジェクトも削除する。

SELECT drop_graph('graph_name', true);
NOTICE:  drop cascades to 4 other objects
DETAIL:  drop cascades to table graph_name._ag_label_vertex
drop cascades to table graph_name._ag_label_edge
drop cascades to table graph_name.label
drop cascades to table graph_name."RELTYPE"
NOTICE:  graph "graph_name" has been dropped
 drop_graph 
------------
 
(1 row)

航空路線図をグラフにしてみる

基本的なクエリが分かったので、データを投入してみる。よくある航空路線図を、OpenFlightsのデータで見てみる。なお、このデータは2014年から更新されていないので、現在の路線を反映するものではない。

データ投入用Pythonスクリプト

特に難しいことはしていないが、いくつかポイントをコードの後にまとめる。

#!/usr/bin/env python3.11

# builtin modules
import os
import sys
import urllib.request

cur_python = f"python{sys.version_info[0]}.{sys.version_info[1]}"

# third party modules
try:
    import pandas as pd
except ModuleNotFoundError:
    os.system(f'{cur_python} -m pip install pandas')
    import pandas as pd

try:
    import psycopg as pg
    from psycopg.rows import dict_row, namedtuple_row
except ModuleNotFoundError:
    os.system(f'{cur_python} -m pip install psycopg')
    import psycopg as pg
    from psycopg.rows import dict_row, namedtuple_row

def main() -> None:
    # load data
    try:
        route_data = pd.read_csv("https://raw.githubusercontent.com/jpatokal/openflights/master/data/routes.dat", header=None)
    except urllib.error.HTTPError:
        sys.exit("Data not found.")

    # connect to the database
    try:
        with pg.connect(
            host="localhost",
            port=5455,
            dbname="postgres",
            user=os.environ.get('USER'),
            password="passw0rd",
            options="-c search_path=ag_catalog,'$user',public"
        ) as con:
            with con.cursor(row_factory=namedtuple_row) as cur:
                # drop the graph if it exists
                cur.execute("SELECT drop_graph('routes', true)")
                # create the graph
                cur.execute("SELECT create_graph('routes')")
                row_cnt = len(route_data.index)
                for row in route_data.itertuples():
                    print(f"Processing {row[0]+1}/{row_cnt} routes")
                    # create the vertices
                    codes = [row[3], row[5]]
                    aids = [n.replace("\\N", "") for n in [row[4], row[6]]]
                    for code, aid in zip(codes, aids):
                        # MERGE doesn't need to check if the airport exists
                        cur.execute(f'''SELECT *
                            FROM cypher('routes', $$
                            MERGE (n:airport {{code:'{code}', aid:'{aid}'}})
                            $$) as (n agtype)''')
                    # create the edge between the airports
                    alid = row[2].replace("\\N", "")
                    cur.execute(f'''SELECT *
                        FROM cypher('routes', $$
                        MATCH (n:airport), (m:airport)
                        WHERE n.code = '{row[3]}' AND m.code = '{row[5]}'
                        CREATE (n)-[r:route {{airline:'{row[1]}', alid:'{alid}'}}]->(m)
                        $$) as (r agtype)''')
                # check the number of airports and routes
                cur.execute("SELECT * FROM cypher('routes', $$ MATCH (n:airport) RETURN COUNT(n) $$) as (n agtype)")
                res_airport_cnt = cur.fetchone()
                cur.execute("SELECT * FROM cypher('routes', $$ MATCH (a)-[e]->(b) RETURN COUNT(e) $$) as (e agtype)")
                res_route_cnt = cur.fetchone()
                print(f"# of airports / routes registered: {res_airport_cnt} / {res_route_cnt}")
    except pg.OperationalError as e:
        print(e)
        sys.exit("Failed to connect to the database.")

if __name__ == "__main__":
    main()
# of airports / routes registered: Row(n='3425'), Row(e='67663')

少々時間がかかるかも知れないが、3425の空港と、67663の経路が登録される。このような、ノードとコネクションの数を比較した時に、コネクション数が圧倒的に多いデータにはグラフが向いている。

コードのポイント

  • 18行:psycopgは、psycopg2より新しい、バージョン3を指す。新バージョンの方が総じてパフォーマンスが良い。psycopg2でサポートされていた機能のいくつかはまだ欠けているが、今回は特に問題が無い。
  • 34行:with文のコンテキスト内はトランザクションとして扱われる。このコードの途中でエラーがあると、vertex / edgeは1件も作成されない状態になる。また、connect()のoptionsでsearch_pathを指定していることに注意。
  • 44行:グラフの存在をチェックする関数は執筆時点で見つからなかったので、いきなりdrop_graph()している。ag_catalogでグラフの存在をチェック出来るが、今回はやっていない。
SELECT * FROM ag_catalog.ag_graph;
 graphid |  name  | namespace 
---------+--------+-----------
   26354 | routes | routes
(1 row)
  • 54行:このスクリプトでは元のデータファイルをそのまま読み込んでおり、何度も同じ空港が出現する。いちいち存在をチェックするとパフォーマンスが悪いので、CREATEではなくMERGEを使っている。cypherにおけるMERGEは、INSERT ON CONFLICT DO UPDATEに相当するもので、PostgreSQL 15で実装されたMERGEとは異なることに注意。

クエリしてみる

このデータには乗り継ぎ便もごく少数(11便)含まれているがほとんどは直行便なので、そのままクエリすると直行便がヒットすると思って良い。

さて。「成田(NRT)からシアトル(SEA)に行く」

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "NRT"})-[r]->(:airport {code: "SEA"})
    RETURN r
$$) as (r agtype);
-----------------------------------------------------------------------------------------------------------------------------------------------------------
 {"id": 1125899906864135, "label": "route", "end_id": 844424930133160, "start_id": 844424930132558, "properties": {"alid": "2009", "airline": "DL"}}::edge
 {"id": 1125899906885778, "label": "route", "end_id": 844424930133160, "start_id": 844424930132558, "properties": {"alid": "324", "airline": "NH"}}::edge
 {"id": 1125899906899849, "label": "route", "end_id": 844424930133160, "start_id": 844424930132558, "properties": {"alid": "5209", "airline": "UA"}}::edge
(3 rows)

Delta(DL)、全日空(NH)、United(UA)が路線を持っているようだ。

ところで、OpenFlightsのデータは「方向」を認識しないので、上のクエリとは逆に「シアトルから成田に行く」も同じ結果となる。

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "SEA"})-[r]->(:airport {code: "NRT"})
    RETURN r
$$) as (r agtype);
-----------------------------------------------------------------------------------------------------------------------------------------------------------
 {"id": 1125899906864364, "label": "route", "end_id": 844424930132558, "start_id": 844424930133160, "properties": {"alid": "2009", "airline": "DL"}}::edge
 {"id": 1125899906885859, "label": "route", "end_id": 844424930132558, "start_id": 844424930133160, "properties": {"alid": "324", "airline": "NH"}}::edge
 {"id": 1125899906900245, "label": "route", "end_id": 844424930132558, "start_id": 844424930133160, "properties": {"alid": "5209", "airline": "UA"}}::edge
(3 rows)

じゃあ、「羽田(HND)からシアトルに行く」路線はというと

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "HND"})-[r]->(:airport {code: "SEA"})
    RETURN r
$$) as (r agtype);
-----------------------------------------------------------------------------------------------------------------------------------------------------------
 {"id": 1125899906863455, "label": "route", "end_id": 844424930133160, "start_id": 844424930132681, "properties": {"alid": "2009", "airline": "DL"}}::edge
(1 row)

当時はデルタが飛ばしていたようだ。

「自宅からだと羽田の方が便利なんだけど、Deltaはイヤだなぁ。1 hop、乗り継ぎ便でシアトルまで行けないかな?」

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "HND"})-[r*2]->(:airport {code: "SEA"})
    RETURN r
$$) as (r agtype);
                                                                                                                                                           r                                                                                                                                                            
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906904410, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "5347", "airline": "VS"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906879381, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "3090", "airline": "KL"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906875252, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "2822", "airline": "IB"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906863800, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "2009", "airline": "DL"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906857395, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "1355", "airline": "BA"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906855419, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "2350", "airline": "AY"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906852278, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "137", "airline": "AF"}}::edge]
 [{"id": 1125899906904362, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "5347", "airline": "VS"}}::edge, {"id": 1125899906848505, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "24", "airline": "AA"}}::edge]
 [{"id": 1125899906899194, "label": "route", "end_id": 844424930132314, "start_id": 844424930132681, "properties": {"alid": "5209", "airline": "UA"}}::edge, {"id": 1125899906904641, "label": "route", "end_id": 844424930133160, "start_id": 844424930132314, "properties": {"alid": "5331", "airline": "VX"}}::edge]
 [{"id": 1125899906899194, "label": "route", "end_id": 844424930132314, "start_id": 844424930132681, "properties": {"alid": "5209", "airline": "UA"}}::edge, {"id": 1125899906899633, "label": "route", "end_id": 844424930133160, "start_id": 844424930132314, "properties": {"alid": "5209", "airline": "UA"}}::edge]
 [{"id": 1125899906899194, "label": "route", "end_id": 844424930132314, "start_id": 844424930132681, "properties": {"alid": "5209", "airline": "UA"}}::edge, {"id": 1125899906863708, "label": "route", "end_id": 844424930133160, "start_id": 844424930132314, "properties": {"alid": "2009", "airline": "DL"}}::edge]
 [{"id": 1125899906899194, "label": "route", "end_id": 844424930132314, "start_id": 844424930132681, "properties": {"alid": "5209", "airline": "UA"}}::edge, {"id": 1125899906854359, "label": "route", "end_id": 844424930133160, "start_id": 844424930132314, "properties": {"alid": "439", "airline": "AS"}}::edge]
 [{"id": 1125899906899194, "label": "route", "end_id": 844424930132314, "start_id": 844424930132681, "properties": {"alid": "5209", "airline": "UA"}}::edge, {"id": 1125899906848369, "label": "route", "end_id": 844424930133160, "start_id": 844424930132314, "properties": {"alid": "24", "airline": "AA"}}::edge]
......
(135 rows)

135パターンある。

「JALが良いんだけど?」

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "HND"})-[r*2 {airline:"JL"}]->(:airport {code: "SEA"})
    RETURN r
$$) as (r agtype);
 r 
---
(0 rows)

残念ながら、JALだけでは無理。「じゃあ、OneWorldのAmericanでも良いよ?」

SELECT *
    FROM cypher('routes', $$
    MATCH (:airport {code: "HND"})-[r*2]->(:airport {code: "SEA"})
    WHERE r[0].airline = "JL" AND r[1].airline = "AA"
    RETURN r
$$) as (r agtype);
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [{"id": 1125899906877228, "label": "route", "end_id": 844424930133213, "start_id": 844424930132681, "properties": {"alid": "2987", "airline": "JL"}}::edge, {"id": 1125899906849407, "label": "route", "end_id": 844424930133160, "start_id": 844424930133213, "properties": {"alid": "24", "airline": "AA"}}::edge]
 [{"id": 1125899906877227, "label": "route", "end_id": 844424930132259, "start_id": 844424930132681, "properties": {"alid": "2987", "airline": "JL"}}::edge, {"id": 1125899906849015, "label": "route", "end_id": 844424930133160, "start_id": 844424930132259, "properties": {"alid": "24", "airline": "AA"}}::edge]
 [{"id": 1125899906877217, "label": "route", "end_id": 844424930132436, "start_id": 844424930132681, "properties": {"alid": "2987", "airline": "JL"}}::edge, {"id": 1125899906848505, "label": "route", "end_id": 844424930133160, "start_id": 844424930132436, "properties": {"alid": "24", "airline": "AA"}}::edge]
(3 rows)

「3パターンあるみたいだけど、乗り継ぎはどこ?」

SELECT *
    FROM cypher('routes', $$
    MATCH p = (:airport {code: "HND"})-[r1]->(b)-[r2]->(:airport {code: "SEA"})
    WHERE r1.airline = "JL" AND r2.airline = "AA"
    RETURN nodes(p)
$$) as (r agtype);
                                                                                                                                                     r                                                                                                                                                     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132436, "label": "airport", "properties": {"aid": "507", "code": "LHR"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132259, "label": "airport", "properties": {"aid": "3364", "code": "PEK"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930133213, "label": "airport", "properties": {"aid": "3469", "code": "SFO"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
(3 rows)

HND -> LHR(ロンドン・ヒースロー) -> SEA、HND->PEK(北京)->SEA、HND->SFO(サンフランシスコ)->SEAの3通りがあったことが分かる。「不便だなぁ、エアラインはどこでも良いや、HND->SAN(サンディエゴ)->SEAは無いの?」(ワガママなお客様である)

SELECT *
    FROM cypher('routes', $$
    MATCH p = (:airport {code: "HND"})-[r1]->(:airport {code: "SAN"})-[r2]->(:airport {code: "SEA"})
    RETURN nodes(p)
$$) as (r agtype);
 r 
---
(0 rows)

当時は無い。「じゃあ、LAX(ロサンゼルス)は?」

SELECT *
    FROM cypher('routes', $$
    MATCH p = (:airport {code: "HND"})-[r1]->(:airport {code: "LAX"})-[r2]->(:airport {code: "SEA"})
    RETURN nodes(p)
$$) as (r agtype);
                                                                                                                                                     r                                                                                                                                                     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
 [{"id": 844424930132681, "label": "airport", "properties": {"aid": "2359", "code": "HND"}}::vertex, {"id": 844424930132314, "label": "airport", "properties": {"aid": "3484", "code": "LAX"}}::vertex, {"id": 844424930133160, "label": "airport", "properties": {"aid": "3577", "code": "SEA"}}::vertex]
(15 rows)

15パターンあった。

まとめ

実際のフライトチケットでは出発・到着時刻を考慮する必要があるため、このデータでは全然役に立たない。また乗り継ぎがある場合には乗り継ぎ空港内での移動時間など、考慮すべきことはさらに増えるが、それらもedgeとして(例:HND 3rd term GT117 -> HND 1st term GT57)データを構築すれば、ルート検索として使えることが分かるかと思う。

試してみた範囲ではApache AGEは問題なく動作しており、そろそろ本番でも採用出来そうに見える。

追記:可視化の方法

タイトルとURLをコピーしました