GraphNativeWorkflowApplicationのPoC
KnowledgeGraphの活用の可能性
Knowledge Graphの活用方法にはまだまだ未知なことが多いと思います。
柔軟にパーソナル情報を保持するだけでなく、ビジネスロジックや実行データをグラフで表現しながら、ワークフローをグラフで実現していくPoCがあったので実行してみよう・・と思ったのですが、クエリがめちゃ長かったので、クエリの要約をしたものになります。
Neo4j/Cypherを使用したグラフネイティブワークフローアプリというMediumの記事が元ネタなので、内容や思想は下記を参照。
参考クエリ
全体の要約
従業員とロールを作成、案件を作成、内部の予算承認、公開、ベンダー選定、資料提供、落札、トレースみたいなことをcypherクエリでグラフにデータを入れながらワークフローを実現しています。
スクリプトのステップ
初期化(全消し & 既存制約の削除)
// 既存のノード/リレーションを削除
MATCH (n) DETACH DELETE n;
// 主要な一意制約を防衛的に削除(存在する場合)
DROP CONSTRAINT unique_Role IF EXISTS;
DROP CONSTRAINT unique_TenderType IF EXISTS;
DROP CONSTRAINT unique_Tender IF EXISTS;
DROP CONSTRAINT unique_Vendor IF EXISTS;
DROP CONSTRAINT unique_Bid IF EXISTS;
ドメイン・コレクション(集合ノード)の作成
(Tendersの状態 / TenderTypes / 従業員ディレクトリ / 役割 / Vendorsの状態)
目的:状態や種別を“かご(集合)ノード + :HAS”で管理し、状態遷移は「付け替え」で表現。
クエリ
// 状態コレクション(Tender)
MERGE (:TenderS {Name:"TenderStates"})
WITH 1 AS _
MERGE (:NewTenderS {Name:"NewTenderS"})
MERGE (:ApprovedTenderS {Name:"ApprovedTenderS"})
MERGE (:PublishedTenderS {Name:"PublishedTenderS"})
MERGE (:AwardedTenderS {Name:"AwardedTenderS"})
MERGE (:ClosedTenderS {Name:"ClosedTenderS"});
// 種別コレクション(TenderType)
CREATE CONSTRAINT unique_TenderType IF NOT EXISTS
FOR (t:TenderType) REQUIRE t.Name IS UNIQUE;
MERGE (:TenderTypeS {Name:"TenderTypeS"})
MERGE (:TenderType {Name:"Open"})
MERGE (:TenderType {Name:"Selective"})
MERGE (:TenderType {Name:"Limited"});
// 役割コレクション(Role)
CREATE CONSTRAINT unique_Role IF NOT EXISTS
FOR (r:Role) REQUIRE r.Name IS UNIQUE;
MERGE (:RoleS {Name:"RoleS"})
FOREACH (name IN ["Requester","Publisher","Level1Approver","Level2Approver","Level3Approver"] |
  MERGE (:Role {Name:name})
);
// Vendor状態コレクション
CREATE CONSTRAINT unique_Vendor IF NOT EXISTS
FOR (v:Vendor) REQUIRE v.ShortName IS UNIQUE;
MERGE (:VendorS {Name:"VendorStates"})
MERGE (:PendingVendorS {Name:"PendingVendorS"})
MERGE (:ApprovedVendorS {Name:"ApprovedVendorS"})
MERGE (:RejectedVendorS {Name:"RejectedVendorS"});
// 各コレクションと要素の :HAS(見通し良さ優先。重複しても害なし)
MATCH (ts:TenderS {Name:"TenderStates"})
MATCH (a:NewTenderS),(b:ApprovedTenderS),(c:PublishedTenderS),(d:AwardedTenderS),(e:ClosedTenderS)
MERGE (ts)-[:HAS]->(a)
MERGE (ts)-[:HAS]->(b)
MERGE (ts)-[:HAS]->(c)
MERGE (ts)-[:HAS]->(d)
MERGE (ts)-[:HAS]->(e);
MATCH (tys:TenderTypeS {Name:"TenderTypeS"}),(o:TenderType {Name:"Open"}),(s:TenderType {Name:"Selective"}),(l:TenderType {Name:"Limited"})
MERGE (tys)-[:HAS]->(o)
MERGE (tys)-[:HAS]->(s)
MERGE (tys)-[:HAS]->(l);
MATCH (rs:RoleS {Name:"RoleS"})
WITH rs
CALL {
  WITH rs
  WITH rs
  UNWIND ["Requester","Publisher","Level1Approver","Level2Approver","Level3Approver"] AS rn
  MATCH (r:Role {Name:rn})
  MERGE (rs)-[:HAS]->(r)
} IN TRANSACTIONS OF 5 ROWS;
MATCH (vs:VendorS {Name:"VendorStates"}),(p:PendingVendorS),(ap:ApprovedVendorS),(rj:RejectedVendorS)
MERGE (vs)-[:HAS]->(p)
MERGE (vs)-[:HAS]->(ap)
MERGE (vs)-[:HAS]->(rj);
検証用クエリ
// Tender 状態コンテナと配下の状態ノード
MATCH (ts:TenderS)-[:HAS]->(s)
RETURN ts.Name AS container, collect(distinct labels(s)) AS states;
// TenderType の一覧
MATCH (:TenderTypeS)-[:HAS]->(t:TenderType)
RETURN collect(t.Name) AS tenderTypes, count(t) AS typeCount;
// Role 一覧(RoleS からの HAS)
MATCH (:RoleS)-[:HAS]->(r:Role)
RETURN collect(r.Name) AS roles, count(r) AS roleCount;
// Vendor 状態の構造
MATCH (vs:VendorS)-[:HAS]->(s)
RETURN vs.Name AS container, collect(distinct labels(s)) AS vendorStates;
// 制約が張れたか
SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties
WHERE name CONTAINS 'unique_'
RETURN name, labelsOrTypes, properties
ORDER BY name;

従業員と役割の割り当て(Employees & Roles)
意図:社員を作成し、集合(Role)からの 受け関係でロールを付与する(動的プロパティはノードに持たせないという設計原則)。
クエリ:19名の:Employeeを作成し、(:Role {Name:"Requester"|...})-[:HAS]->(:Employee)でロール付与。
検証用クエリ
UNWIND [
  {Name:"Alice"}, {Name:"Bob"}, {Name:"Carol"}, {Name:"Dave"}, {Name:"Eve"},
  {Name:"Frank"}, {Name:"Grace"}, {Name:"Heidi"}, {Name:"Ivan"}, {Name:"Judy"},
  {Name:"Mallory"}, {Name:"Niaj"}, {Name:"Olivia"}, {Name:"Peggy"}, {Name:"Rupert"},
  {Name:"Sybil"}, {Name:"Trent"}, {Name:"Victor"}, {Name:"Wendy"}
] AS row
MERGE (:Employee {Name:row.Name});
// 役割割り当て例(自由に調整)
MATCH (r1:Role {Name:"Requester"}), (r2:Role {Name:"Publisher"}), (a1:Role {Name:"Level1Approver"}), (a2:Role {Name:"Level2Approver"}), (a3:Role {Name:"Level3Approver"})
MATCH (e1:Employee {Name:"Alice"}),(e2:Employee {Name:"Bob"}),(e3:Employee {Name:"Carol"}),(e4:Employee {Name:"Dave"}),(e5:Employee {Name:"Eve"})
MERGE (r1)-[:HAS]->(e1)
MERGE (r2)-[:HAS]->(e2)
MERGE (a1)-[:HAS]->(e3)
MERGE (a2)-[:HAS]->(e4)
MERGE (a3)-[:HAS]->(e5);
検証用クエリ
// 従業員数
MATCH (e:Employee) RETURN count(e) AS employeeCount;
// 各 Role に紐づく従業員
MATCH (r:Role)-[:HAS]->(e:Employee)
RETURN r.Name AS role, collect(e.Name) AS members
ORDER BY role;
// 可視化(グラフビュー推奨)
MATCH (r:Role)-[:HAS]->(e:Employee)
RETURN r,e;


ベンダー作成と状態管理(Pending/Approved/Rejected)
意図:申請→審査→承認/却下の Vendor vetting を状態集合の移動で表現。
クエリ:(:PendingVendorS)-[:HAS]->(:Vendor {...}) で申請登録し、審査に応じて :ApprovedVendorS か :RejectedVendorS に付け替える(:HAS の付け替え/削除)。
クエリ
// 申請(Pendingに入れる)
UNWIND [
  {ShortName:"VEND-A", Name:"Vendor A"},
  {ShortName:"VEND-B", Name:"Vendor B"},
  {ShortName:"VEND-C", Name:"Vendor C"}
] AS v
MERGE (ven:Vendor {ShortName:v.ShortName})
  ON CREATE SET ven.Name = v.Name
WITH ven
MATCH (p:PendingVendorS)
MERGE (p)-[:HAS]->(ven);
// 審査結果:A/Bを承認、Cを却下(例)
MATCH (ap:ApprovedVendorS),(rj:RejectedVendorS),(p:PendingVendorS)
MATCH (a:Vendor {ShortName:"VEND-A"}),(b:Vendor {ShortName:"VEND-B"}),(c:Vendor {ShortName:"VEND-C"})
MERGE (ap)-[:HAS]->(a)
MERGE (ap)-[:HAS]->(b)
MERGE (rj)-[:HAS]->(c)
WITH p,a,b,c
MATCH (p)-[ph:HAS]->(x)
WHERE x IN [a,b,c]
DELETE ph;
検証用クエリ
// 各状態にいる Vendor 一覧
MATCH (s)-[:HAS]->(v:Vendor)
WHERE labels(s)[0] IN ['PendingVendorS','ApprovedVendorS','RejectedVendorS']
RETURN labels(s)[0] AS state, collect(v.ShortName) AS vendors
ORDER BY state;
// Pending から外れているか(0 が理想)
MATCH (:PendingVendorS)-[:HAS]->(v:Vendor)
RETURN count(v) AS stillPending;

Tender の作成(Requester)
意図:リクエスタが案件(Tender)を起票し、種別(:TenderType)やドキュメント、予算、提出期限などを紐づける。
クエリ
CREATE CONSTRAINT unique_Tender IF NOT EXISTS
FOR (t:Tender) REQUIRE t.TenderId IS UNIQUE;
UNWIND [
  {TenderId:"T-001", Title:"Headend Upgrade", Budget:  50000, DueDate: date() + duration({days:30}), Type:"Open"},
  {TenderId:"T-002", Title:"New Rolling Stock", Budget: 750000, DueDate: date() + duration({days:45}), Type:"Selective"}
] AS row
MERGE (t:Tender {TenderId:row.TenderId})
  ON CREATE SET t.Title=row.Title, t.Budget=row.Budget, t.DueDate=row.DueDate
WITH t, row
MATCH (nt:NewTenderS), (ty:TenderType {Name:row.Type})
MERGE (nt)-[:HAS]->(t)
MERGE (t)-[:HAS_TYPE]->(ty)
MERGE (t)-[:HAS_DOCS]->(td:TenderDocS)
MERGE (td)-[:HAS]->(:Doc {Name: t.Title + " Specs.pdf"});
検証用クエリ
// New に入っている Tender 一覧
MATCH (:NewTenderS)-[:HAS]->(t:Tender)
RETURN t.TenderId, t.Title, t.Budget, t.DueDate
ORDER BY t.TenderId;
// 種別リンク・仕様書リンクの確認
MATCH (t:Tender)-[:HAS_TYPE]->(ty:TenderType)
OPTIONAL MATCH (t)-[:HAS_DOCS]->(:TenderDocS)-[:HAS]->(d:Doc)
RETURN t.TenderId, ty.Name AS type, collect(d.Name) AS docs
ORDER BY t.TenderId;
// 可視化
MATCH (ts:NewTenderS)-[:HAS]->(t:Tender)-[:HAS_TYPE]->(ty:TenderType)
OPTIONAL MATCH (t)-[:HAS_DOCS]->(td:TenderDocS)-[:HAS]->(d:Doc)
RETURN ts,t,ty,td,d;

Tender の承認フロー(Level1/2/3 Approver)
意図:予算レンジに応じた承認者(Level1/2/3)が承認し、New → Approved へ遷移。
クエリ
MATCH (nt:NewTenderS)-[:HAS]->(t:Tender)
WITH DISTINCT t
CALL {
  WITH t
  WITH t,
       CASE
         WHEN t.Budget <= 100000 THEN "Level1Approver"
         WHEN t.Budget <= 500000 THEN "Level2Approver"
         ELSE "Level3Approver"
       END AS roleName
  MATCH (:Role {Name: roleName})-[:HAS]->(emp:Employee)
  MERGE (emp)-[:APPROVED {Date: datetime()}]->(t)
  RETURN 1 AS applied
}
WITH DISTINCT t
MATCH (nt:NewTenderS)-[r:HAS]->(t)
DELETE r
WITH t
MATCH (ap:ApprovedTenderS)
MERGE (ap)-[:HAS {Date: datetime()}]->(t);
検証用クエリ
// 承認イベント(誰がどれを承認したか)
MATCH (emp:Employee)-[a:APPROVED]->(t:Tender)
RETURN emp.Name AS approver, t.TenderId AS tender, a.Date AS approvedAt
ORDER BY tender, approvedAt;
// New に残っていないか(0 が理想)
MATCH (:NewTenderS)-[:HAS]->(t:Tender)
RETURN count(t) AS stillNew;
// Approved に入ったか
MATCH (:ApprovedTenderS)-[:HAS]->(t:Tender)
RETURN collect(t.TenderId) AS approvedTenders;

Tender の公開(Publisher)
意図:Publisher が Approved → Published に遷移させ、公開済み一覧に載せる。
クエリ:(:ApprovedTenderS)-[:HAS]->(t)を(:PublishedTenderS)-[:HAS]->(t)に切替。
クエリ
MATCH (ap:ApprovedTenderS)-[:HAS]->(t:Tender)
MATCH (pub:Role {Name:"Publisher"})-[:HAS]->(e:Employee)
WITH DISTINCT t, e
MERGE (e)-[:PUBLISHED {Date: datetime()}]->(t)
WITH t
MATCH (ap:ApprovedTenderS)-[r:HAS]->(t)
DELETE r
WITH t
MATCH (pb:PublishedTenderS)
MERGE (pb)-[:HAS {Date: datetime()}]->(t);
検証用クエリ
// 公開イベント(Publisher→Tender)
MATCH (e:Employee)-[p:PUBLISHED]->(t:Tender)
RETURN e.Name AS publisher, t.TenderId AS tender, p.Date AS publishedAt
ORDER BY tender;
// Published に入っているか
MATCH (:PublishedTenderS)-[:HAS]->(t:Tender)
RETURN collect(t.TenderId) AS publishedTenders;
// 可視化
MATCH (t:Tender)
OPTIONAL MATCH (ap:ApprovedTenderS)-[:HAS]->(t)
OPTIONAL MATCH (pb:PublishedTenderS)-[:HAS]->(t)
RETURN t, ap, pb;

招待と招待受諾(InvitedVendorS / AcceptedInvitationS)
意図:Tenderごとの招待集合にVendorを格納し、各Vendor側でも「受諾した招待」の集合に紐づける。
クエリ
// Tender側:招待集合にVendorを格納
MATCH (t:Tender {TenderId:"T-001"}), (pb:PublishedTenderS)-[:HAS]->(t)
MERGE (t)-[:HAS_INVITEES]->(ivs:InvitedVendorS)
WITH t, ivs
MATCH (a:Vendor {ShortName:"VEND-A"}),(b:Vendor {ShortName:"VEND-B"})
MERGE (ivs)-[:HAS]->(a)
MERGE (ivs)-[:HAS]->(b);
// Vendor側:受諾(受諾集合にTenderを紐づけ)
MATCH (v:Vendor {ShortName:"VEND-A"})
MERGE (v)-[:HAS_ACCEPTED_INVITATIONS]->(ai:AcceptedInvitationS)
WITH v, ai
MATCH (t:Tender {TenderId:"T-001"})
MERGE (ai)-[:HAS {Date: datetime()}]->(t);
検証用クエリ
// Tender 側の招待先一覧
MATCH (t:Tender {TenderId:"T-001"})-[:HAS_INVITEES]->(:InvitedVendorS)-[:HAS]->(v:Vendor)
RETURN t.TenderId, collect(v.ShortName) AS invitedVendors;
// Vendor 側の受諾済み Tender 一覧
MATCH (v:Vendor {ShortName:"VEND-A"})-[:HAS_ACCEPTED_INVITATIONS]->(:AcceptedInvitationS)-[:HAS]->(t:Tender)
RETURN v.ShortName AS vendor, collect(t.TenderId) AS acceptedTenders;
// 可視化
MATCH (t:Tender {TenderId:"T-001"})-[:HAS_INVITEES]->(ivs:InvitedVendorS)-[:HAS]->(v:Vendor)
OPTIONAL MATCH (v)-[:HAS_ACCEPTED_INVITATIONS]->(ai:AcceptedInvitationS)-[:HAS]->(t)
RETURN t, ivs, v, ai;

ちなみに、当たり前ですが、下記のようにGPT-5に聞いたら
// このクエリから、何をしているか説明して
MATCH (t:Tender {TenderId:"T-001"}), (pb:PublishedTenderS)-[:HAS]->(t) MERGE (t)-[:HAS_INVITEES]->(ivs:InvitedVendorS) WITH t, ivs MATCH (a:Vendor {ShortName:"VEND-A"}),(b:Vendor {ShortName:"VEND-B"}) MERGE (ivs)-[:HAS]->(a) MERGE (ivs)-[:HAS]->(b);
MATCH (v:Vendor {ShortName:"VEND-A"}) MERGE (v)-[:HAS_ACCEPTED_INVITATIONS]->(ai:AcceptedInvitationS) WITH v, ai MATCH (t:Tender {TenderId:"T-001"}) MERGE (ai)-[:HAS {Date: datetime()}]->(t);
下記のように説明してくれました。クエリから意味を抽出することはできそうな雰囲気です。
公開済みの入札案件(T-001)に「招待先ベンダー集合」を作って招待先(VEND-A/B)をぶら下げる
そのうち VEND-A が「招待を受諾した」ことを、VEND-A 側の受諾集合から T-001 へ関連づけて記録する
入札(Bid)の作成とドキュメント紐づけ、会話ログ
意図:Vendor が受諾済み招待から対象 Tender と仕様ドキュメントを参照し、Bid を作成。Vendor、Tender、BidDocS、さらに会話 :ConversatioN を関連付ける。BidCode は一意。
クエリ
CREATE CONSTRAINT unique_Bid IF NOT EXISTS
FOR (b:Bid) REQUIRE b.BidCode IS UNIQUE;
// VEND-A が T-001 に入札
MATCH (v:Vendor {ShortName:"VEND-A"})-[:HAS_ACCEPTED_INVITATIONS]->(:AcceptedInvitationS)-[:HAS]->(t:Tender {TenderId:"T-001"})
MERGE (v)-[:HAS_ACTIVE_BIDS]->(vbs:ActiveBidS)
MERGE (t)-[:HAS_BIDS]->(tbs:TenderBidS)
WITH v, t, vbs, tbs
MERGE (b:Bid {BidCode: "BID-" + t.TenderId + "-" + v.ShortName})
  ON CREATE SET b.Price = 42000, b.SubmissionDate = date()
MERGE (vbs)-[:HAS {Date: datetime()}]->(b)
MERGE (tbs)-[:HAS {Date: datetime()}]->(b)
MERGE (b)-[:HAS_VENDOR]->(v)
MERGE (b)-[:HAS_TENDER]->(t)
MERGE (b)-[:HAS_DOCS]->(bd:BidDocS)
MERGE (bd)-[:HAS]->(:Doc {Name: b.BidCode + " - Proposal.pdf"})
MERGE (b)-[:HAS_CHAT]->(:Conversation {StartedAt: datetime()});
検証用クエリ
// Bid 本体と価格・提出日
MATCH (b:Bid)-[:HAS_TENDER]->(t:Tender)
MATCH (b)-[:HAS_VENDOR]->(v:Vendor)
RETURN b.BidCode, t.TenderId, v.ShortName, b.Price, b.SubmissionDate
ORDER BY t.TenderId, b.Price ASC;
// Bid の付帯情報(提案書・会話)
MATCH (b:Bid)-[:HAS_DOCS]->(:BidDocS)-[:HAS]->(d:Doc)
OPTIONAL MATCH (b)-[:HAS_CHAT]->(c:Conversation)
RETURN b.BidCode, collect(distinct d.Name) AS bidDocs, min(c.StartedAt) AS chatStartedAt
ORDER BY b.BidCode;
// 可視化
MATCH (b:Bid)-[:HAS_VENDOR]->(v:Vendor),
      (b)-[:HAS_TENDER]->(t:Tender)
OPTIONAL MATCH (b)-[:HAS_DOCS]->(bd:BidDocS)-[:HAS]->(d:Doc)
OPTIONAL MATCH (b)-[:HAS_CHAT]->(c:Conversation)
RETURN b,v,t,bd,d,c;

落札決定 → クローズ
目的:最適Bidを選定し、Published → Awarded → Closed を遷移。
クエリ
// 例:最安値を選定
MATCH (t:Tender {TenderId:"T-001"})
MATCH (t)<-[:HAS_TENDER]-(b:Bid)
WITH t, b ORDER BY b.Price ASC
WITH t, collect(b)[0] AS win
MERGE (win)-[:AWARDED {Date: datetime()}]->(t)
// Publishedから外し、Awardedへ
WITH t
MATCH (pb:PublishedTenderS)-[r:HAS]->(t)
DELETE r
WITH t
MATCH (aw:AwardedTenderS)
MERGE (aw)-[:HAS {Date: datetime()}]->(t);
// クローズ(T-001 を Closed へ)
MATCH (t:Tender {TenderId:"T-001"})
OPTIONAL MATCH (aw:AwardedTenderS)-[r2:HAS]->(t)
DELETE r2
WITH t
MATCH (cl:ClosedTenderS)
MERGE (cl)-[rel:HAS]->(t)
ON CREATE SET rel.Date = datetime();
検証用クエリ
// 落札イベント
MATCH (b:Bid)-[aw:AWARDED]->(t:Tender)
RETURN t.TenderId, b.BidCode, aw.Date AS awardedAt
ORDER BY awardedAt;
// ステージ遷移の現在地(Tenderごと)
MATCH (stage)-[:HAS]->(t:Tender)
WHERE labels(stage)[0] IN ['PublishedTenderS','AwardedTenderS','ClosedTenderS','ApprovedTenderS','NewTenderS']
RETURN t.TenderId, collect(distinct labels(stage)[0]) AS currentStages
ORDER BY t.TenderId;
// Published から外れているか(0 が理想 / その Tender に限る)
MATCH (:PublishedTenderS)-[:HAS]->(t:Tender {TenderId:"T-001"})
RETURN count(t) AS stillPublished;
// Awarded → Closed まで入ったか
MATCH (:AwardedTenderS)-[:HAS]->(t:Tender)
RETURN collect(t.TenderId) AS awardedTenders;
MATCH (:ClosedTenderS)-[:HAS]->(t:Tender)
RETURN collect(t.TenderId) AS closedTenders;
// 可視化
MATCH (pb:PublishedTenderS)-[:HAS]->(t:Tender)
OPTIONAL MATCH (aw:AwardedTenderS)-[:HAS]->(t)
OPTIONAL MATCH (cl:ClosedTenderS)-[:HAS]->(t)
OPTIONAL MATCH (b:Bid)-[:AWARDED]->(t)
RETURN pb,aw,cl,t,b;

参照系(UI向けクエリ例)
目的:画面表示・一覧確認用。
クエリ
// 公開中Tender一覧(種別・仕様書名)
MATCH (:PublishedTenderS)-[:HAS]->(t:Tender)
OPTIONAL MATCH (t)-[:HAS_TYPE]->(ty:TenderType)
OPTIONAL MATCH (t)-[:HAS_DOCS]->(:TenderDocS)-[:HAS]->(d:Doc)
RETURN t.TenderId, t.Title, ty.Name AS Type, t.Budget, t.DueDate, collect(d.Name) AS Docs
ORDER BY t.DueDate;
// Vendorが受諾しているTender
MATCH (v:Vendor {ShortName:"VEND-A"})-[:HAS_ACCEPTED_INVITATIONS]->(:AcceptedInvitationS)-[:HAS]->(t:Tender)
RETURN v.ShortName AS Vendor, collect(t.TenderId) AS AcceptedTenders;
// Tenderごとの入札一覧
MATCH (t:Tender {TenderId:"T-001"})<-[:HAS_TENDER]-(b:Bid)-[:HAS_VENDOR]->(v:Vendor)
RETURN t.TenderId, b.BidCode, v.ShortName, b.Price, b.SubmissionDate
ORDER BY b.Price ASC;
検証用クエリ
// Tender のサマリ(タイプ・現在ステージ・期限・仕様書数)
MATCH (t:Tender)
OPTIONAL MATCH (t)-[:HAS_TYPE]->(ty:TenderType)
OPTIONAL MATCH (s)-[:HAS]->(t)
OPTIONAL MATCH (t)-[:HAS_DOCS]->(:TenderDocS)-[:HAS]->(d:Doc)
WITH t, ty, collect(distinct labels(s)[0]) AS stages, count(d) AS docCount
RETURN t.TenderId, t.Title, ty.Name AS type, stages AS stagesNow, t.Budget, t.DueDate, docCount
ORDER BY t.TenderId;
// Bid のサマリ(Tender別の入札状況)
MATCH (t:Tender)
OPTIONAL MATCH (t)<-[:HAS_TENDER]-(b:Bid)-[:HAS_VENDOR]->(v:Vendor)
WITH t, collect({bid:b.BidCode, vendor:v.ShortName, price:b.Price}) AS bids
RETURN t.TenderId, t.Title, bids
ORDER BY t.TenderId;
// 主要イベントのタイムライン(承認→公開→落札)
MATCH (t:Tender)
OPTIONAL MATCH (e1:Employee)-[a:APPROVED]->(t)
OPTIONAL MATCH (e2:Employee)-[p:PUBLISHED]->(t)
OPTIONAL MATCH (b:Bid)-[aw:AWARDED]->(t)
RETURN t.TenderId, 
       collect(distinct {type:'APPROVED', by:e1.Name, at:a.Date})    AS approvals,
       collect(distinct {type:'PUBLISHED', by:e2.Name, at:p.Date})   AS publications,
       collect(distinct {type:'AWARDED',  bid:b.BidCode, at:aw.Date}) AS awards
ORDER BY t.TenderId;

ちなみにクエリと結果からトレースできそうか。
下記から何が言える?
===クエリ
// 主要イベントのタイムライン(承認→公開→落札)
MATCH (t:Tender) OPTIONAL MATCH (e1:Employee)-[a:APPROVED]->(t) OPTIONAL
MATCH (e2:Employee)-[p:PUBLISHED]->(t) OPTIONAL MATCH (b:Bid)-[aw:AWARDED]->(t) RETURN t.TenderId, collect(distinct {type:'APPROVED', by:e1.Name, at:a.Date}) AS approvals, collect(distinct {type:'PUBLISHED', by:e2.Name, at:p.Date}) AS publications, collect(distinct {type:'AWARDED', bid:b.BidCode, at:aw.Date}) AS awards ORDER BY t.TenderId;
===
===result
t.TenderId,approvals,publications,awards T-001,"[ { at: 2025-08-26T09:59:16.360000000Z, by: Carol, type: APPROVED } ]","[ { at: 2025-08-26T10:19:06.877000000Z, by: Bob, type: PUBLISHED } ]","[ { at: 2025-08-26T10:44:32.612000000Z, type: AWARDED, bid: BID-T-001-VEND-A }, { at: 2025-08-26T10:53:02.292000000Z, type: AWARDED, bid: BID-T-001-VEND-A } ]" T-002,"[ { at: 2025-08-26T09:59:16.360000000Z, by: Eve, type: APPROVED } ]","[ { at: 2025-08-26T10:19:06.877000000Z, by: Bob, type: PUBLISHED } ]","[ { at: null, type: AWARDED, bid: null } ]"
ざっくり要約:
T-001
承認:2025-08-26 09:59:16Z、承認者 Carol
公開:2025-08-26 10:19:06Z、公開者 Bob
落札:入札 BID-T-001-VEND-A に対して 2 回 記録(10:44:32Z と 10:53:02Z)
→ 同じBidへの AWARDED リレーションが重複しています(日時を含めて MERGE した/何度か実行したなどが原因)。
リードタイム目安(UTC表示)
承認→公開:約 19分50秒
公開→初回落札:約 25分26秒
落札#1→落札#2:約 8分30秒
T-002
承認:2025-08-26 09:59:16Z、承認者 Eve
公開:2025-08-26 10:19:06Z、公開者 Bob
落札:まだ無し(awards に {at:null, bid:null} が混ざるのは OPTIONAL MATCH 由来の“ダミー要素”)
できそう。
まとめ
基本的にクエリはGPTに作成してもらいましたが、概ね正しかったです。
ただ、たまに失敗もするので、cypherクエリの定型を作成するところまではLLMで、実行する場合はプログラム、結果からトレースや何か不正がないかなどを検知するのはLLMでというバランスが良いのかなーと思いました。
あと、今回のGraphNativeWorkflowApplicationのように全てをcypherとknowledgeGraphで柔軟にデータを更新しつつ、ワークフローを表すのは、人間にはなかなか理解が難しいだろうなと思いました。ただ、LLMとAgentで人がわかりやすい中間表現を作成しながらなら可能性はあると思いました。
本家のクエリには下記のメッセージがところどころに記載されています。AgentのTuning次第で完全自動化可能なプロセスが結構ありそうです。
--- AI Agent Opportunity: I don't need to say, but the following process can be fully automated if AI Agents are well-tuned.
Discussion