0
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?

第三章:個人ログから経路と原文を取り出す。GraphitiとNeo4jで考える最小構成

0
Last updated at Posted at 2026-09-25

第二章では、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をそのまま経路検索の始点にはしない。

  1. log-03に対応する出典エピソードを取得する。
  2. そこからMENTIONSでつながるエンティティを表示する。
  3. 始点と、関連情報を探して選んだ終点を、エンティティの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投稿を扱うときは、サーバー側で閲覧可能な区分を決め、途中のノードと出典も含めて検索対象を制限する。

何ができたら、この構成を使えると言えるか

三件の架空ログを順に投入し、次の順で確認する。

  1. 保存直後に原文を開ける。グラフの取り込みに失敗しても原文は残る。
  2. 取り込み後、各ログから出典エピソードと知識ノードを取得できる。
  3. 「使用後の実感」から「当初の課題」まで、意味の通る経路が見つかる。
  4. 経路の各関係から、その根拠となるログを開ける。
  5. 同じログの再送で重複が増えない。無関係なログを足しても、存在しない関係を作らない。

ここまで通ったら、次に訂正と削除を試す。原文の上書きだけではグラフは更新されないため、版の対応を使って影響する関係を特定し、共有される根拠を失わず更新できるかを確かめる。

まとめ

今回は、個人ログアプリの検証用構成をGraphitiとNeo4jで具体化した。原文の保存はアプリ側、構造化と統合はGraphiti、明示的な経路取得はNeo4jに担当させる。

確認したいのは、グラフを描けることだけではない。自分が残したログから経路をたどり、その一本一本の関係を原文で確かめられることだ。

次の実装では、この三件の入力から経路・出典の取得までを通し、ここに書いた期待と実際の出力の差を記録する。

参照資料

2026年9月25日に公式資料・実装を確認した。以下は更新されるmainへのリンクであり、実行検証時には利用バージョンを固定する。本文のCypherは確認した保存構造を基に作成した未実行の案である。

第二章へ / 記事一覧

0
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
0
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?