“探す”から、“たどる”へのドキュメント管理の話 1/2 (RAG化)
お久しぶりです。
minmaのエンジニアリングマネージャーのユジンです。
前回は、
バックエンドとフロントエンドのあいだにある「橋」の話を書きました。
今回は、
その橋のさらに奥側、
“知識”と“判断”についての話をしてみようと思います。
1.増え続けるもの
サービスが長く続くと、
増えていくものがあります。
コード。
ログ。
設計書。
障害対応履歴。
Slackの会話。
Wiki。
運用メモ。
そして、誰かの個人メモ。
minmaでも、15年以上サービスを運営する中で、
本当にたくさんの情報が積み重なってきました。
それは本来、
チームが積み重ねてきた“知識”です。
けれどある頃から、
私たちは少しずつ、
同じ感覚を持つようになりました。
情報はある。 でも、見つからない。 見つかっても、古い。 そして、活かしきれない。
2.システムトラブルは、いつも“初見”に見える
ある日、システムのトラブルが発生します。
API timeoutDB latencyQueue滞留
誰かがログを見る。
誰かがSlackを遡る。
誰かが 前にも似たことありませんでしたっけ と言う。
そして数十分後、
誰かが半年以上前のincidentを見つける。
これ、前回もDB latencyが原因でしたね!
その瞬間、
状況が急に整理される。
けれど同時に、
こうも感じました。
最初から、それが分かっていれば、もっと早く動けたのではないか
3.RAGを導入した背景
私たちが最初に取り組んだのは
RAG(Retrieval-Augmented Generation)
でした。
目的はシンプルです。
散らばった情報を、もっと探しやすくしたい
AI Chat(LLM)
に直接答えさせるのではなく、
まず関連情報を検索し、
その結果を
今の状況に合わせたContext
として渡す。
質問 ⇨ 検索 ⇨ 関連情報 ⇨ LLM ⇨ 回答
これによって
- 過去incident
- ポストモーテム
- 障害対応履歴
などを、
まとめて扱えるようになりました。
4.意味を探すための検索
最初にぶつかったのは、 従来の検索方法の限界でした。
実際には、
DB負荷によってサービス接続が遅くなっている
という同じ文脈で話していることも多いです。
しかし、 従来のKeyword検索では
- 単語が完全一致しない
- 表現が少し違う
- 書いた人によって言い方が違う
だけで、
必要な情報へたどり着けないことがありました。
例えば
'DB latency'
で検索しても、
slow query connection timeout Database response delay
のような関連情報が、
検索結果に出てこないことがあります。
つまり、
私たちが本当に探したかったのは、
同じ単語ではなく、
同じ意味でした。
そこで導入したのが、
EmbeddingによるSemantic Searchでした。
Embeddingでは、
文章を“意味を持った数値”へ変換します。
"Payment API timeout"
↓
[0.13, -0.42, 0.91 ...]
のように、
文章をベクトル(数値の並び)へ変換します。
意味が近い文章ほど、
ベクトル同士の距離も近くなります。
つまり、
DB latencyとDatabase response delayのような、
表現は違うけれど意味が近い文章を、
意味的な距離をもとに検索できるようになります。
ここで重要になるのが、Vector DBでした。
5. 元々はElasticsearchを利用していた
minmaでは以前から、
Elasticsearchを利用していました。
- ログ検索
- 全文検索
- 分析
などで活用しています。
Elasticsearchは非常に強力です。
特に
- inverted index(転置インデックス)
- BM25(Best Matching 25、キーワード検索の代表的手法)
- distributed search(分散検索)
など、
従来型検索に非常に優れています。
さらに最近では dense_vector
によるVector Searchも扱えるようになり、
Keyword Search + Semantic Search
を同時に扱えるようになりました。
そのため、最初のRAG構成では
Elasticsearch + LLM
を中心に考えていました。
6.けれど、別の課題が見えてきた
Embedding検索によって
似ている文章
を探すことは、
かなり速くなりました。
しかし、
障害対応で本当に必要だったのは、
単なる類似検索だけではありませんでした。
- どこへ影響したか(障害でどの影響があるのか)
- 何に依存しているか(もし依存しているところで障害の始発点であるのか)
- 過去のどの障害と関係するか(過去と比べてどう違うのか)
をたどる必要があります。
つまり
意味
だけではなく、
関係
を扱う必要がありました。
7.意味と関係を探すための検索
私たちは、
関係性をより正確にたどるため、
Graph DB (Neo4j)の検討を始めました。
Graph DBは
(Node)-[Relationship]->(Node)
でデータを扱います。
例えば
(Incident)-[:IMPACTS]->(Service) (Service)-[:USES]->(DB)
重要なのは関係そのものを保存できることでした。
8.Graph DBを使うには、構造設計が必要になる
ただし、Graph DBを使えば、
自動的に関係性が見えるようになるわけではありません。
Embedding検索であれば、
文章を検索用テキストにまとめ、
Embedding化すれば、ある程度「意味的に近い情報」を探せます。
一方でGraph DBでは、 事前に、次のようなことを決める必要があります。
- 何をNodeとして扱うのか
- 何をRelationshipとして扱うのか
今回の実装では、
インシデント情報を中心に、次のようなNodeを設計しました。
Node: - Incident : 障害情報の中心 - Service : 影響を受けたサービス - Todo : 対応事項 - CPRule : 障害レベル判定基準 - Chunk : Vector検索用の検索テキスト
Relationshipは、次のように設計しています。
Relationship: - Incident ─ AFFECTS_SERVICE → Service - Incident ─ HAS_TODO → Todo - Incident ─ MATCHES_CP_RULE → CPRule - Incident ─ HAS_DOC → Chunk → Embedding - Incident ─ RELATED_INCIDENT → Incident
図にすると、現在の構造はこのようになります。
障害レベル基準は、(CPRule)の内容をもとに分けています。
- Level - 1(致命的/Critical) - 2(高 / High) - 3(中 / Medium) - 4(低 / Low) - Reference - 共通ルール - 参考リンク - 注記
図7の分類設計をもとに、実際には図8のようにIncidentとLevelを紐づけました
障害レベル毎に紐づいている障害情報と、
レベルを渡して関係がある情報を確認できるようになりました。
Level1からLevel4までは、
インシデントの障害レベル判定に使います。
共通ルール、参考リンク、注記は、
Incidentの補足参照情報として保存します。
つまり、
今回のGraph DB導入は、
単なるDB選定ではありませんでした。
それは、
自分たちのシステムを、どのような関係として見るのか
を設計する作業に近いものでした。
9.なぜGraph DBだったのか?
単なる「データ保存」ではなく、
「探索」だったからです。
たとえば、
過去の障害を調べるときに見たいのは、
単独のIncidentだけではありません。
- この障害は、どのServiceに影響したのか
- 同じServiceで過去に似た障害はあったのか
- どのCPRuleに該当する障害レベルなのか
- どのような対応Todoがあったのか
- 検索用テキストとして、どのChunkに紐づいているのか
こうした情報は表として横に並べるよりも、
関係としてたどれる方が自然です。
特に今回のNeo4jの実装では、Ver. 5.11以降で
Vector Indexが追加されたため、
Graph DBとして使いながら
同時にVector検索にも使っています。
つまり、
構造化された関係はGraphとして扱い、
意味的に近い情報の検索はEmbeddingで扱う構成です。
この構成にすることで、
意味・関係で探す
の両方ができるようになります。
今回のGraph DB設計で重要だったのは、
Graph DBを入れること自体ではなく、
Incidentを中心に
Service、CPRule、Todo、Chunkをどう接続するか
を決めることでした。
10.検索品質を高めるためにやったこと
最後に検討したのが、
LLMの選択でした。
もちろん、
LLMによって回答の品質は変わります。
そのため、
どのLLMを使うべきかも検討しました。
検索結果が、
- 過去incidentを見つけられない
- 関係情報を取得できない
- 古い情報を参照する
- 影響範囲が不足する
つまり “考える能力” より前に、 “正しい情報へ到達できるか”の方が重要でありました。
具体的には
- Embedding品質
- Graph構造
- Relationship設計
- Context構成
- Retrieval精度
などです。
これは、 単なるAI精度の話ではありませんでした。
運用で重要だったのは:
“誰が使っても、 同じ情報へたどり着けるか”
でした。
つまり、 属人的な“勘”を減らし、 探索プロセスを再現可能にすること。
その意味で、
LLM単体の性能よりも、
RAG基盤の設計の方が、
運用品質へ大きく影響していました。
11.「検索」ではなく「調査」を作りたかった
今回、 私たちが作りたかったものは、 単なるAI Chatではありません。
過去の知識を集め、 関係をたどり、 現在の状況を整理し、 判断を支援するものです。
最初は、
“検索”でした。
次に、
“探索”になりました。
そして今、 少しずつ
“調査”
へ変わり始めています。
12. 最後に(次回へのつながり)
ここまでで、
私たちは
- 過去incidentを探す
- 関係をたどる
- 「システムトラブルのレベルの算定基準」からレベル判断する
ところまではできるようになりました。
けれど、
本当にやりたいのは、
単なる検索ではありません。
過去を探すだけではなく
過去の判断を使って、今の意思決定を支援する
ことです。
次に目指しているのは:
- ADR(Architecture Decision Record)
- 過去の設計判断
- 制約
- 方針
などもつなげながら、
“今の問題に対して、次に何をすべきか”
を提案できるAgentです。
次回は、
さらに進化した話を書いてみたいです。
GraphRAGからAgentへ、
どこを変えると“判断支援”になるのかについて、
書いてみようと思います。
Appendix
A. Vector DBの代わりに、RDBMSを使うことはできなかったのか?
最初に出た疑問のひとつが、
これでした。
RDBMSではだめなのか?
通常のRDBMSは、
決まった形式のデータを扱うのが得意です。
例えば、Incidentを次のように保存できます。
| id | title | created_at |
|---|---|---|
| 1 | 決済障害レポート | 2026-02-20 |
| 2 | 予約API障害 | 2026-03-01 |
| 3 | ログイン障害 | 2026-05-12 |
このように、 - ID - タイトル - 日付 - ステータス - 担当者
のような、
構造化されたデータを保存するのは、
RDBMSがとても得意です。
検索もできます。
SELECT * FROM incidents WHERE title LIKE '%決済%';
ただし、今回扱いたかったのは、
単なる文字列検索だけではありませんでした。
- 類似インシデント検索
- 意味検索
- 関連ドキュメントの探索
- 過去対応との比較
のような検索です。
例えば、ユーザーが
予約が完了しない
と入力したとします。
一方で、過去の障害レポートには
予約APIタイムアウト
と書かれているかもしれません。
人間から見ると、
予約が完了しない 予約APIタイムアウト
は近い意味です。
しかし、通常のLIKE検索では、
文字列が一致していないため、
この2つを結びつけるのは得意ではありません。
ただし、RDBMSでも全文検索やVector拡張を利用できます。
PostgreSQLのpgvectorのように、
RDBMS上でVector検索を扱う選択肢もあります。
そのため、
“RDBMSではできない”
という話ではありません。
今回のポイントは、
RDBMSを置き換えることではなく、
役割を分けることでした。
今回必要だったのは、
業務データそのものの更新ではなく、
過去の知識を探し、関係をたどることでした。
そのため、
RDBMSとは別の探索基盤として、
Vector SearchとGraph DBを組み合わせる構成にしました。
B. Embeddingを使うメリットは?
Embeddingは、
文章を“意味を持った数値”へ変換する方法です。
例えば、
"予約APIタイムアウト" ↓ [0.021, -0.442, 0.991, ...]
のように、
文章を数百〜数千次元のベクトルに変換します。
一見すると、
ただの数字の並びです。
けれど、この数字には、
文章の意味的な特徴が含まれています。
例えば、
予約APIタイムアウト 予約が完了しない
は、Embeddingすると
近い位置に配置されやすくなります。
一方で、
プロフィール画像が表示されない
のような内容は、
別の位置に配置されます。
このため、Embeddingを使うと、
文字列一致ではなく、
意味の近さで検索できるようになります。
これが、
Semantic Search(意味検索)です。
C. Embeddingはレコメンドにも使える
Embeddingは、
インシデント検索だけではなく、
レコメンドにも相性が良いです。
ポイントは、
Embeddingした後の数値を、
“位置”として扱えることです。
例えば、くらしのマーケットのサービスを
説明用に2次元で表すと、
次のように考えられます。
エアコンクリーニング → [0.12, 0.84] 水回りクリーニング → [0.20, 0.78] 不用品回収 → [0.70, 0.30] 家具組み立て → [0.75, 0.25]
実際のEmbeddingは、
もっと多くの次元を持ちます。
ただ、説明のために2次元で表すと、
次のようなイメージです。
掃除・清潔 ↑ | エアコンクリーニング | ● | 水回りクリーニング | ● | | | 不用品回収 | ● | 家具組み立て | ● +--------------------------------→ 引っ越し・整理
意味が近いサービスは、
近い位置に配置されます。
例えば、あるユーザーが
エアコンクリーニング を利用した場合、
その位置に近いサービスを探すことで、
次のような候補を出せます。
おすすめ候補 1. ハウスクリーニング 2. 水回りクリーニング 3. 家事代行
これは、
単に同じカテゴリを出しているだけではありません。
Embedding後の位置が近いサービスを探し、
意味的に近いものを推薦している、
という考え方です。
今回のIncident検索でも、
考え方は同じです。
文字列が一致しているかではなく、
意味的に近いかを見ています。
D. 関係性ならRDBMSでも良いのでは?
もうひとつ、
自然に出てくる疑問があります。
“関係性なら、JOINすればRDBMSでもできるのでは?”
関係性はRDBMSでも表現できます。
RDBMSでも、
incident service database dependency
のようにテーブルを分け、
JOINを使えば関係を表現できます。
特に、 - transaction - consistency - operational data
を扱うなら、
RDBMSは非常に強力です。
ただ、今回やりたかったのは、
業務データの正しさを保つことではなく、
障害対応時に関係をたどることでした。
例えば、 - この障害はどのServiceに影響したのか - そのServiceはどのDBに依存しているのか - 同じDBで過去に似た障害はあったのか - そのとき、どの対応Todoが作られたのか
のような探索です。
このような多段の関係探索は、
Graph DBの方が自然に表現できます。
E. くらしのマーケットサービスの「ユーザー・店舗・予約」を例に比較してみる
ここで一度、
RDBMSとGraph DBの役割を、
実サービスに近い形で考えてみます。
例えば、当社のくらしのマーケットの予約サービスでは、
- ユーザー
- 店舗
- 店舗スケジュール
- 予約
- 決済
のようなデータを扱います。
結論から言うと、
このような実サービスの中核データを、
Graph DBへそのまま置き換える判断はしませんでした。
予約や決済は、
更新整合性がとても重要です。
- 同じ時間枠に二重予約が入らないこと
- 予約状態と決済状態が矛盾しないこと
- キャンセル、返金、再予約の状態遷移が壊れないこと
- 障害時にもデータの整合性を保てること
こうした領域は、
RDBMSが非常に強いです。
一方でGraph DBは、
予約データそのものを正しく更新する場所というより、
関係をたどる探索用途に向いています。
例えば、
- このユーザーは過去にどのカテゴリを利用したか
- 同じカテゴリを利用したユーザーは、次に何を予約しているか
- この店舗と似た利用傾向の店舗はどこか
- 障害時に、どのServiceやDBへ影響が広がるか
のような調査です。
つまり今回の考え方は、
RDBMS = 予約・決済・スケジュールなどの業務データを正しく保存する Graph DB = ユーザー、店舗、カテゴリ、予約履歴などの関係をたどる Vector Search = 意味的に近い情報を探す
という役割分担でした。
RDBMSで予約を扱う場合
RDBMSでは、
予約に必要なデータをテーブルとして設計します。
users - user_id - name stores - store_id - store_name store_schedules - schedule_id - store_id - start_time - end_time - status reservations - reservation_id - user_id - schedule_id - reserved_at - status
例えば、空きスケジュールを検索する場合は、
次のようなSQLになります。
SELECT s.schedule_id, s.start_time, s.end_time FROM store_schedules s LEFT JOIN reservations r ON s.schedule_id = r.schedule_id AND r.status IN ('reserved', 'paid') WHERE s.store_id = 1001 AND r.reservation_id IS NULL AND s.start_time >= NOW();
また、ユーザーの予約一覧を取得する場合は、
次のようにJOINで関連テーブルをつなぎます。
SELECT r.reservation_id, st.store_name, ss.start_time, r.status FROM reservations r JOIN store_schedules ss ON r.schedule_id = ss.schedule_id JOIN stores st ON ss.store_id = st.store_id WHERE r.user_id = 5001 ORDER BY ss.start_time DESC;
RDBMSの強みは、
このような業務データに対して、
制約とTransactionを使えることです。
例えば、二重予約を防ぐには、
単にPKやFKを置くだけでは不十分です。
実際には、
schedule_idに対する一意制約- 予約状態を考慮した制約(予約確定時の排他制御)
- Transaction
- 行ロックや楽観ロック
- 決済状態との整合性管理
などを組み合わせて設計します。
RDBMSは、
ACID特性を前提に、
更新整合性を担保しやすいところが強みです。
Graph DBで予約の関係を扱う場合
同じ予約データをGraphとして見ると、
NodeとRelationshipで表現できます。
(User)-[:RESERVED]->(Schedule) (Schedule)-[:BELONGS_TO]->(Store) (Store)-[:HAS_CATEGORY]->(Category)
例えば、
(User {user_id: 5001})-[:RESERVED]->(Schedule {schedule_id: 9001})
(Schedule {schedule_id: 9001})-[:BELONGS_TO]->(Store {store_id: 1001})
(Store {store_id: 1001})-[:HAS_CATEGORY]->(Category {name: "ハウスクリーニング"})
のように表現できます。
ユーザーの予約一覧をたどる場合は、
Cypherでは次のようになります。
MATCH (u:User {user_id: 5001})
-[:RESERVED]->
(sc:Schedule)
-[:BELONGS_TO]->
(st:Store)
RETURN
st.store_name,
sc.start_time
ORDER BY sc.start_time DESC
SQLと比べると、
関係をたどるという意図が、
クエリの形に出やすくなります。
さらに、Graph DBが得意なのは、
複数段の関係をたどる探索です。
例えば、
ある店舗を予約したユーザーが、
他にどのカテゴリを利用しているかを見たい場合は、
次のように書けます。
MATCH (:Store {store_id: 1001})
<-[:BELONGS_TO]-
(:Schedule)
<-[:RESERVED]-
(u:User)
-[:RESERVED]->
(:Schedule)
-[:BELONGS_TO]->
(other:Store)
-[:HAS_CATEGORY]->
(category:Category)
WHERE other.store_id <> 1001
RETURN
category.name AS category,
COUNT(DISTINCT u) AS user_count
ORDER BY user_count DESC
これは、
予約トランザクションを処理するためのクエリではありません。
ユーザー、店舗、予約履歴、カテゴリの関係をたどり、
推薦や分析に使うためのクエリです。
どちらを使うべきか
RDBMSとGraph DBの違いは、
どちらが上位互換か、という話ではありません。
役割が違います。
| 観点 | RDBMS | Graph DB |
|---|---|---|
| データ表現 | 行と列を持つテーブル | Node、Relationship、Property |
| 関係性の扱い | 外部キーで制約し、JOINで取得する | Relationshipとして保存し、Traversalでたどる |
| 強い領域 | 予約、決済、注文、集計、管理画面 | 推薦候補の探索、影響範囲調査、依存関係分析 |
| 整合性 | Transaction、制約、ロックを設計しやすい | 関係探索は自然だが、業務更新の主DBには慎重な設計が必要 |
| スキーマ | 厳格に設計しやすい | 柔軟だが、Node/Relationship設計が重要 |
| クエリ | SQL | Cypher、Gremlin、GQLなど |
今回の結論は、
予約サービスの中核データをGraph DBに置き換える、
というものではありませんでした。
RDBMSは、
予約・決済・スケジュールなどの業務データを守る。
Graph DBは、
その周辺にある関係をたどり、
調査、推薦、影響範囲分析に使う。
F. 最後に
Vector・Graph DB導入は、
単なるDB導入ではありませんでした。
自分たちのシステムを、どのような関係として見るのか
を決める作業に近いものでした。
また、関係を増やしすぎると、
逆に探索が難しくなります。
全部つなぐ
をやってしまうと、
ノイズが増え、
本当に見たい関係が見えにくくなります。
Vector・Graph化は、
何でもつなげれば良い、
というものではありません。
さらに、
RDBMSの代替ではありません。
今回も最終的には、
次のように役割を分けています。
RDBMS = 業務データを正しく保存する Vector Search = 意味的に近い情報を探す Graph DB = 関係をたどる
それぞれ得意なことが違うため、
置き換えるというより、
役割を分けて使うのが自然でした。


























