第二章では、SNS投稿と個人ログを共通の知識バックエンドにつなぐ形を考えた。今回は、最初に試す題材を個人ログアプリに絞る。
理由は、一人で記録を追加し、結果を読み返して確かめられるからだ。ここで作る取り込み・検索の処理は、後からSNS投稿を入力にした場合にも使える形にしたい。
今回の問いは、「この工夫を始めた理由は何だったか」を、別の日の記録までたどって確かめられるかである。
この記事は実装前の設計とクエリ案を扱う。以下のログは架空で、図は期待する関係の例。Graphitiによる抽出、クエリの実行、アプリの動作は未検証である。
まず、三日分のログを用意する
| 記録ID | 日時 | 原文 |
|---|---|---|
| log-01 | 2026-09-20 21:00 | 日記から過去の判断理由を探しにくい。日記整理ツールを作って、この困りごとに対応したい。 |
| log-02 | 2026-09-22 21:00 | 日記整理ツールに、日付とテーマで絞り込む機能を追加した。 |
| log-03 | 2026-09-24 21:00 | 日付とテーマで絞り込む機能を使ったら、先月の判断理由を見つけやすくなった。 |
log-03を開いたとき、log-02の試行と、log-01の困りごとまで戻りたい。
探索では「使用後の実感」から逆向きにたどり、機能、ツール、当初の課題へ進む。日付順に三つの文章を並べるだけでなく、何を介して関係しているのかを返す。
この図どおりのノード名や分割になるとは限らない。実際の抽出結果に、同じ意味の接続ができているかを確かめる。また、「見つけやすくなった」はログを書いた本人の実感として扱う。
最初に試す構成
今回の検証用には、次の構成を提案する。性能比較による最終選定ではなく、一連の動作を確かめるための選択である。
| 部分 | 使用するもの | 担当 |
|---|---|---|
| アプリAPI | FastAPI | ログの登録、一覧、取り込み状態、経路取得の窓口 |
| 原文の保存 | SQLite | 入力したログと、グラフ側のIDとの対応を保存 |
| グラフの構築 | Graphiti | 記録を取り込み、エンティティや関係を抽出・統合 |
| グラフの保存と経路探索 | Neo4j | 構築されたグラフを保存し、Cypherで経路を取得 |
Graphitiを試す理由は、記録をエピソード単位で追加でき、関係から出典に戻るための情報を持つからだ。Neo4jはGraphitiの保存先として利用し、明示的な経路取得にも使う。別のGraphRAGライブラリを重ねて導入する構成ではない。
Graphitiが呼び出すLLMと埋め込みモデルは別途必要になる。APIを使うかローカルで動かすかは、実行環境と費用を確認して決める。自宅でアプリを動かすことと、外部にデータを送らないことは同じではない。最初の検証には上の架空ログだけを使う。
原文とグラフは役割が違う。原文はユーザーが入力した記録で、グラフはそこから作られた検索用の構造である。グラフ構築に失敗しても、書いたログまで失われないように分ける。
ログを保存してから、グラフに取り込む
保存ボタンを押す処理では、まずSQLiteへ原文を保存する。その後、保存したログをGraphitiへ渡す。
ログの登録
-> 原文を保存する(pending)
-> 取り込み処理を始める(processing)
-> Graphitiへ渡す
-> 出典エピソードとの対応を保存する(ready)
取り込みに失敗した場合
-> 原文を残し、failedとして表示する
状態名はこのアプリで定義するもので、Graphitiの標準ステータスではない。「保存済み」と「グラフへの取り込み済み」を区別して表示する。
原文側には、第二章の項目に加えて次の対応を持たせる。
| 項目 | 用途 |
|---|---|
record_id |
アプリ側のログID |
revision |
原文の版。初回は1 |
graph_group_id |
この検証用グラフの区分 |
episode_uuid |
Graphiti側の出典エピソードID。取り込み前は未設定 |
index_status |
pending、processing、ready、failed |
Graphitiの公式サンプルでは、add_episodeへ本文とreference_timeなどを渡している。このアプリでは、本文と記録者を一緒に渡し、reference_timeには取り込み時刻ではなくログの日時を使う。
最初は一件ずつ順番に取り込み、対応するepisode_uuidを原文側へ保存する。Graphitiへの書き込み後にアプリが停止すると、両方のDBの状態がずれる可能性はある。その場合は失敗したログを無条件に再投入せず、エピソードが既に存在するかを照合する。再送で同じログを二重登録しないことも検証項目に含める。
「記録のID」と「知識のID」を区別する
第一章の図に出てきた人物や話題は、原文そのものとは異なるノードだ。Graphitiの公式実装では、次のように分かれている。
| グラフ内の要素 | このアプリでの意味 |
|---|---|
Episodicノード |
取り込んだログ。contentに取り込み内容を持つ |
Entityノード |
ログから取り出された人物、ツール、課題など |
MENTIONS関係 |
どのエピソードにどのエンティティが登場するか |
RELATES_TO関係 |
エンティティ間の関係。nameやfactに関係の内容を持つ |
関係のepisodes
|
その関係に結びついた出典エピソードのID一覧 |
したがって、画面でlog-03を選んでも、そのIDをそのまま経路検索の始点にはしない。
- log-03に対応する出典エピソードを取得する。
- そこから
MENTIONSでつながるエンティティを表示する。 - 始点と、関連情報を探して選んだ終点を、エンティティのUUIDで指定する。
最初の画面では「判断理由を見つけやすくなった」に相当するノードを始点、「判断理由を探しにくい」に相当するノードを終点として選ぶ。自然文の質問から自動で始点・終点を決める処理は、その後に追加できる。
経路を取る処理は、検索と分ける
Graphitiの検索で関連する関係を集めることと、二点間を結ぶ経路を取ることは別に扱う。
次は、小さな検証用グラフで使う読み取りクエリ案である。取り込み後の実際のUUIDをパラメータに渡す。アプリのrecord_idや図中の表示名ではない。
MATCH path = (start:Entity {uuid: $start_uuid})-[:RELATES_TO*1..3]-(finish:Entity {uuid: $end_uuid})
WHERE all(node IN nodes(path) WHERE node.group_id = $group_id)
AND all(edge IN relationships(path) WHERE edge.group_id = $group_id)
RETURN [node IN nodes(path) | {uuid: node.uuid, name: node.name}] AS nodes,
[edge IN relationships(path) | {
uuid: edge.uuid,
source_uuid: startNode(edge).uuid,
target_uuid: endNode(edge).uuid,
name: edge.name,
fact: edge.fact,
episode_uuids: edge.episodes,
valid_at: edge.valid_at,
invalid_at: edge.invalid_at,
expired_at: edge.expired_at
}] AS edges
ORDER BY length(path)
LIMIT 10
この例は、向きを問わず最大3本の関係でつながる経路を探す。表示時に関係の本来の向きを失わないよう、接続元と接続先も返す。
ここでは過去を振り返るため、有効期間で絞らず履歴を含める。返された関係を「今も有効」とは表示しない。現在の状態を問い合わせる場合は、有効期間と失効情報の扱いを別に定める必要がある。
これは経路の存在を確かめるクエリで、因果の証明や推薦の順位付けではない。まず関係のnameとfactを読み、意図した接続になっているかを確認する。また、LIMIT 10は探索全体の計算量を保証しない。大きなグラフに広げる際は、通過する関係の条件やタイムアウトも必要になる。
経路の各関係から原文へ戻る
経路と一緒に返された関係のUUIDを使い、その出典を取得する。
MATCH (:Entity)-[edge:RELATES_TO]->(:Entity)
WHERE edge.uuid IN $edge_uuids AND edge.group_id = $group_id
UNWIND coalesce(edge.episodes, []) AS episode_uuid
OPTIONAL MATCH (episode:Episodic {uuid: episode_uuid, group_id: $group_id})
RETURN edge.uuid AS edge_uuid,
episode_uuid,
episode.content AS source_content,
episode.valid_at AS recorded_at
返ったepisode_uuidを原文側の対応表に照合し、アプリのログを開く。これで「この関係はlog-02に基づく」と表示できる。
出典のIDが空の場合や、参照先が見つからない場合は、根拠確認済みとして扱わない。クエリに現れなかった関係も含め、元の経路に含まれる全関係と照合して欠落を表示する。
なお、ここで取得できるのは出典エピソードであって、根拠となる一文の文字位置が保証されるわけではない。最初は原文全体を開き、引用箇所の自動ハイライトは別の検証にする。
アプリに必要な窓口は四つ
次のAPIは、このアプリで作る案である。Graphitiが同名のHTTP APIを提供するという意味ではない。
| API案 | 動作 |
|---|---|
POST /records |
ログを保存し、取り込み対象にする |
GET /records/{record_id} |
原文と取り込み状態を取得する |
GET /records/{record_id}/entities |
そのログに結びつく知識ノードを取得する |
GET /paths?start_uuid=...&end_uuid=... |
経路と、各関係の出典を取得する |
画面ではログ一覧から一件を開き、関連ノードを選んで経路を表示する。経路上の関係を選べば元のログへ戻れる。経路が見つからない場合は「指定した範囲では見つからなかった」と表示し、文章生成で穴を埋めない。
検証環境は自分だけがローカルで使う前提にする。group_idはデータの区分であって認証・認可の代わりではない。将来SNS投稿を扱うときは、サーバー側で閲覧可能な区分を決め、途中のノードと出典も含めて検索対象を制限する。
何ができたら、この構成を使えると言えるか
三件の架空ログを順に投入し、次の順で確認する。
- 保存直後に原文を開ける。グラフの取り込みに失敗しても原文は残る。
- 取り込み後、各ログから出典エピソードと知識ノードを取得できる。
- 「使用後の実感」から「当初の課題」まで、意味の通る経路が見つかる。
- 経路の各関係から、その根拠となるログを開ける。
- 同じログの再送で重複が増えない。無関係なログを足しても、存在しない関係を作らない。
ここまで通ったら、次に訂正と削除を試す。原文の上書きだけではグラフは更新されないため、版の対応を使って影響する関係を特定し、共有される根拠を失わず更新できるかを確かめる。
まとめ
今回は、個人ログアプリの検証用構成をGraphitiとNeo4jで具体化した。原文の保存はアプリ側、構造化と統合はGraphiti、明示的な経路取得はNeo4jに担当させる。
確認したいのは、グラフを描けることだけではない。自分が残したログから経路をたどり、その一本一本の関係を原文で確かめられることだ。
次の実装では、この三件の入力から経路・出典の取得までを通し、ここに書いた期待と実際の出力の差を記録する。
参照資料
2026年9月25日に公式資料・実装を確認した。以下は更新されるmainへのリンクであり、実行検証時には利用バージョンを固定する。本文のCypherは確認した保存構造を基に作成した未実行の案である。