くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

システム刷新という話 第5話

システム刷新という話 第5話

“たどる”から“次の行動を考える”へのドキュメント管理の話 2/2 (Agentic RAG化)

図1 ある企業の面接の様子
こんにちは。

minmaのエンジニアリングマネージャーのユジンです。

前回は、
長年蓄積された障害履歴や設計資料をRAGへ取り込み、
意味の近い情報を検索し、
さらにGraph DBによって情報同士の関係をたどる仕組みについて紹介しました。

今回は、
そのRAGを土台に、
集めた情報が判断に十分かを評価し、
不足している情報を追加で探しながら、
次のTodoを考えるAgentic RAGへ発展させた話です。

ただし、
Agenticな仕組みにすること自体が目的ではありません。

私たちが確認したいのは、
こうした技術によって、
実際の業務をどの程度、
どのように改善できるのかという点です。

1. 良い答えは、良い質問から生まれる

図2 面接官が回答を受け、次の質問を考える場面

仕事柄、
エンジニア採用の面接をする機会があります。

例えば、
候補者から 障害対応を経験しました という回答があったとします。
面接官は、
そこで終わらず、回答に応じて質問を続けます。

どのような障害でしたか?

回答を聞いた後、さらに質問します。

原因をどのように特定しましたか?
再発防止のために、どのような対応をしましたか?

面接官は、
最初から決められた質問を読み上げるだけではありません。

回答から不足している情報を見つけ、
次に何を確認すべきかを考えながら、
判断に必要な情報をそろえていきます。

ただし、面接官が探しているのは、
あらかじめ決められた一つの正解ではありません。

採用するポジションやスキルのレベルに応じた評価基準と照らし合わせながら、
判断に必要な情報がそろっているかを確認しています。

今回作ったAgentic RAGも、
得られた情報が十分かを評価し、
不足していれば追加の質問や検索を行うという、
これに近い動きをします。

2. RAGからAgentらしい処理への段階的な進化

面接に置き換えると、
RAG型のアプリは、履歴書や過去の面接記録から、
質問に関係する情報を探す役割です。

例えば、次の質問に答えます。

この候補者には、障害対応の経験がありますか?

RAG型のアプリは、
履歴書や面接記録から該当する情報を探し、

前職で、API障害の調査と復旧対応を経験しています
と回答します。

一方、
Agentic RAGは、情報を一度探して回答するだけでは終わりません。

障害対応の経験は確認できました。
ただし、本人がどこまで原因調査を担当したのか、
また、再発防止まで関わったのかは分かりません。
次に、その2点を確認する必要があります

というように、
得られた情報が、判断に十分かを評価します。

情報が足りなければ、
次に確認すべきことを考え、
必要な情報をもう一度検索します。

そして、集めた情報を整理し、
次に確認すべきことやTodoを提示します。

面接官が、
候補者の回答を聞きながら次の質問を変えるように、
Agentic RAGも、
検索結果に応じて次の検索や処理を変えていきます。

項目 RAG Agentic RAG
役割 関連情報を検索し、回答する 情報を評価し、次の行動につなげる
処理 検索 → 回答 計画 → 検索 → 評価 → 必要に応じて再検索
情報不足時 次の調査は人が考える 追加検索や確認事項を提示する
判断基準 質問との関連性 目的・ADR・過去事例
得られるもの 関連情報と回答 判断材料・根拠・Todo

表1 RAGとAgentic RAGの違い

図3 現在の状況、関連ADR、過去事例を組み合わせ、次のTodoへ

3. Agentic RAGの判断材料としてADRを使う

面接官は、
候補者の回答を聞いて、
思いつきだけで次の質問を決めているわけではありません。

募集するポジションに求める役割や、
技術力、問題解決力、チームとの関わり方など、
判断の土台となる評価基準があります。

候補者の回答を評価基準と照らし合わせ、
判断に必要な情報が足りなければ、
次に何を確認するべきかを考えます。

Agentic RAGにも、
これと同じような判断の土台が必要です。

現行システムで守るべき方針や、
システム刷新における意思決定、
その判断理由や制約を参照することで、
現在の状況が設計方針に沿っているか、
判断に必要な情報が不足していないかを確認できます。

そこで、Agentic RAGの判断材料として活用したのが、
ADR(Architecture Decision Record)です。

minmaのADRとは、
システム刷新の設計における重要な意思決定と、
その背景や理由を記録するものになっています。

4. システム刷新の判断を、ADRとして残していく

図4 現在の状況、関連ADR、過去事例を組み合わせ、次のTodoへ

面接では、
候補者への質問や評価基準を、
一度決めたら終わりにするわけではありません。

実際の面接で得られた気づきをもとに、
質問や評価の観点を見直し、
次の面接へ生かしていきます。

システム刷新も同じです。

minmaでは現在、
システム刷新を進めるなかで、
設計上の重要な判断と、
その理由をADRとして残していく取り組みを進めています。

どのような選択肢を検討したのか。
なぜ、その構成や技術を選んだのか。
どのような制約や影響があるのか。

こうした背景まで記録することで、
後から参加したメンバーも、
現在の設計に至った理由を確認できます。

ただし、 ADRは記録するだけでは、
十分に活用されません。

システム刷新が進み、
ADRが増えていくほど、
現在の変更に関係するADRを見つけ、
その内容を状況と照らし合わせることが難しくなります。

今回のAgentic RAGでは、
現在の状況、
関連するADR、
過去の事例を組み合わせ、
設計方針との違いや不足している情報を整理します。

そして、
次に確認すべきことを Todo として提示できる仕組みを目指しました。

5. 技術的に動くだけでは、業務改善にはつながらない

Embedding、Vector DB、Graph DBに、
Agentic RAGの反復処理を組み合わせることで、
関連情報を探し、状況を整理し、
次に確認すべきことを提示できるようになります。

しかし、
仕組みが動いたことだけでは、
業務を改善できたとは言えません。

私たちが本当に解決したいのは、
例えば次のような、実際の業務課題です。

特定の時間帯にAPIの応答が遅くなり、一部のリクエストでタイムアウトが発生している。

RAG型のアプリは、
過去の類似インシデントや、
関連する設計資料を探します。

過去にも、バッチ処理の実行時間帯に、データベースの応答が遅くなった事例があります。

これは調査の手がかりになりますが、
今回も同じ原因だと判断することはできません。

そこでAgentic RAGは、
現在分かっている情報をもとに、
次に確認すべきことを整理します。

  1. タイムアウトが発生しているAPIと影響範囲を確認する
  2. 対象時間帯のアクセス数を確認する
  3. データベースの負荷とスロークエリを確認する
  4. 同じ時間帯に実行されたバッチを確認する
  5. 外部APIなど、ほかの依存先の応答時間を確認する
  6. 直近の変更履歴と関連するADRを確認する

そして、確認結果に応じて、
次に調べる内容を変えていきます。

例えば、
バッチの実行時間とAPIの遅延時間が一致し、
同じ時間帯にデータベースの負荷も上昇していた場合、
Agentは過去の類似事例や関連するADRを参照し、
次のように整理します。

現在の状況:
・バッチの実行時間とAPIの遅延時間が一致している
・同じ時間帯にデータベースの負荷が上昇している
・外部APIの応答時間には大きな変化がない

関連情報:
・過去にも、バッチ実行時のDB負荷上昇とAPI遅延が重なった類似事例がある
・処理の責務や実行基盤に関するADRがある

次のTodo:
1. 対象バッチの処理内容と接続先を確認する
2. バッチの停止や実行時間の変更による業務影響を確認する
3. 検証環境での再現、または実行時間の一時的な変更により、負荷が改善するか確認する
4. 恒久対応として、処理やリソースの分離を検討する

追加で確認が必要なこと:
・現在の構成を採用した理由
・関連するADRが現在も有効か
・変更による他サービスへの影響

このように、
過去の事例を見つけて終わるのではなく、
現在の状況との共通点や違いを確認し、
考えられる仮説と、次に調べるべきことを提示します。

ここで重要なのは、
Agentic RAG型のアプリが、
勝手に本番環境を変更するわけではないことです。

Agentが行うのは、
現在の状況、判断材料、対応の選択肢、
それぞれのリスクを整理するところまでです。

サービスに影響する操作や最終判断は、
minmaのエンジニアが確認したうえで行います。

6. Agentic RAGの品質を、どう高めるか

面接では、
経験のある面接官でも、
最初から必ず良い質問ができるとは限りません。

面接後には、
判断に必要な情報を確認できたか、
質問が誘導的になっていなかったか、
評価基準に沿って判断できたかを振り返ります。

その結果をもとに、
質問や面接の進め方を改善していきます。

図5 計画・検索・評価・見直しを繰り返し、判断と次の行動につなげる流れ

Agentic RAGの品質を高める過程も、
これに似ています。

Agentic RAGは、
検索ツールを接続しただけで、
安定して良い提案ができるわけではありません。

安定した結果を得るためには、
目的や利用できる情報、判断手順に加えて、
人へ確認する条件や、
十分な情報がそろったと判断する条件を
設計する必要があります。

今回の障害調査では、
例えば次のような流れを基本にしながら、
得られた情報に応じて確認内容を変えていきます。

  1. 症状と影響範囲を整理する
  2. 直近の変更履歴を確認する
  3. メトリクスとログを確認する
  4. Vector DBから類似インシデントを検索する
  5. Graph DBから関連システムとADRをたどる
  6. 過去の事例と現在の状況との差を確認する
  7. 不足情報と次のTodoを整理する
  8. 人が判断できる形で結果を提示する

7. 「探す」から「次の行動を考える」へ

これまでの流れを整理すると、
次のようになります。

Embeddingによって、
文章の意味を数値として表現する。
Vector DBによって、
意味の近い情報を探せるようにする。
Graph DBによって、
情報同士の関係をたどれるようにする。
ADRによって、
システム刷新における意思決定と、その根拠を残す。  

そしてAgentic RAGによって、
得られた情報が十分かを評価し、
不足していれば追加で情報を探します。

必要な情報がそろったら、
現在の状況や過去の事例、
関連するADRを組み合わせ、
次に確認すべきことを提示します。

記録する探すたどる
そして、たどる から 次に何をするかを考えるへ。

今回のAgentic RAG化は、
RAGに新しい機能を追加して終わることが
目的ではありません。

これまで蓄積してきた知識を、
必要なときに探せるだけでなく、
実際の判断や次の行動につなげられる形へ変えるための取り組みです。

最後に

今回私たちが作ったのは、
エンジニアの仕事をすべてAIに任せるものではありません。

必要な情報を検索して整理し、
情報が不足していれば追加で確認し、
判断材料や次の行動を提示する。

人とAIが協業しながら、
より良い判断につなげるための
Agentic RAG型のアプリです。

Agentic RAGでは、
LLMの性能だけでなく、
判断の土台となるデータの品質も重要です。

インシデント情報やADRに誤りや不足があれば、
適切な検索や提案にはつながりません。

また、
Embeddingの精度や設計、
Vector DBの検索精度、
Graph DBの構造、
Agentの判断手順によっても、
得られる結果は変わります。

そのため、
データを整備し、検索結果や提案を評価しながら、
仕組みを継続的に改善していく必要があります。

minmaでは、
こうした仕組みを一緒に考え、
システム刷新を進めてくれるエンジニアを募集しています。

技術を導入するだけでなく、
実際の業務でどのように役立てるかまで考えたい方と、
一緒に取り組んでいけたらうれしいです。


Appendix

本文では、Agentic RAGの考え方と設計思想を中心に紹介しました。
Appendixでは、今回のアプリで使用した技術と実装構成を、エンジニア向けにまとめます。

A. 全体のデータフロー

図6 全体のデータフロー

B. 技術スタック

レイヤー 技術 役割
LLM Amazon Bedrock(Claude Sonnet 4.5) 回答生成・調査計画の立案・検索結果の評価
Embedding Amazon Titan Text Embeddings V2(1,024次元) テキストのベクトル変換
Vector/Graph DB Neo4j ベクトル検索・関係グラフの探索
Session Redis 会話履歴・会話状態・共有スナップショットの管理
Web FastAPI+Jinja2 APIの提供・Web UIの生成
LLM抽象化 LangChain LLM呼び出し・ツール実行・Agent処理の共通化
認証 Cloudflare Access ユーザー認証・管理者判定
ADR管理 GitLab Issues(ADR) 設計上の意思決定と、その背景・根拠を記録
障害報告書管理 GitLab Issues(Postmortem) インシデントの経緯・原因・対応・再発防止策を管理

表2 技術スタック

C. ReAct Agentが持つ5つのツール

ReActは、推論(Think)、行動(Act)、観察(Observe)を繰り返しながら、
必要な情報を集め、回答につなげるフレームワークです。

図7 ReActフロー

ツール 検索方式 何を探すか
search_incidents ベクトル検索 過去の障害報告書(Postmortem)
search_adr ベクトル検索 GitLab ADR(設計方針・意思決定の記録)
search_cp_rules 全件取得 障害レベルの判定基準(Level 1〜4)
search_services キーワード検索 サービス情報・関連インシデント
aggregate_incidents 集計 年別・障害レベル別のインシデント件数

表3 ReAct Agentが使用するツール

LLMには、質問、直近の会話、これまでのツール実行結果を渡します。
過去の実行結果を踏まえて次の処理を選べる点が、
単純なRAGとの大きな違いです。

D. 実装上の工夫

temperature=0.0
出力のランダム性を抑え、
ツール選択や回答のばらつきを小さくしています。
同じ条件で、できるだけ再現性の高い動作を得ることが目的です。

temperature tool選択 何を探すか リスク
0.0 選択が安定しやすい 再現性を高めやすい 必ず同じ結果になるとは限らない
0.3〜0.7 選択に幅が出る 複数の調査方針を試しやすい ツール選択が変化しやすい
1.0以上 ばらつきが大きい 多様な出力になりやすい 業務用途では制御しにくい

表4 temperature

集計系の高速パス
「件数を教えて」のような質問はLLMにツール選択させず直接集計します。
不要なLLM呼び出しを省き、応答速度を改善しています。

Postmortem・ADRをGitLab Issuesで管理
ADRをMarkdownファイルではなくGitLab Issuesで管理することで、
更新日時の追跡・差分検知・Admin UIからのワンクリック同期を実現しています。
ドキュメントやラベルがついたIssueだけを対象とし、
GitLabで更新されると次回同期時にNeo4jへ反映されます。

Redisによる会話の一貫性管理
LLMに会話の文脈を持たせるため、ユーザーのメールをキーに履歴をRedisへJSON保存。
直近20件とsystemプロンプトだけ保持。

図8 ADR検索・回答例 1
図9 ADR検索・回答例 2
図10 管理ページ、ADR一覧(同期済)

システム刷新という話 第4話

“探す”から、“たどる”へのドキュメント管理の話 1/2 (RAG化)

お久しぶりです。
minmaのエンジニアリングマネージャーのユジンです。

前回は、
バックエンドとフロントエンドのあいだにある「橋」の話を書きました。

今回は、
その橋のさらに奥側、
“知識”と“判断”についての話をしてみようと思います。


1.増え続けるもの

図1 積み重なった情報を、必要な知識へ運びたい

サービスが長く続くと、
増えていくものがあります。

コード。
ログ。
設計書。
障害対応履歴。
Slackの会話。
Wiki。
運用メモ。
そして、誰かの個人メモ。

minmaでも、15年以上サービスを運営する中で、
本当にたくさんの情報が積み重なってきました。

それは本来、
チームが積み重ねてきた“知識”です。

けれどある頃から、
私たちは少しずつ、
同じ感覚を持つようになりました。

情報はある。  
でも、見つからない。  
見つかっても、古い。  
そして、活かしきれない。

2.システムトラブルは、いつも“初見”に見える

ある日、システムのトラブルが発生します。

  • API timeout
  • DB latency
  • Queue滞留

誰かがログを見る。
誰かがSlackを遡る。
誰かが 前にも似たことありませんでしたっけ と言う。

そして数十分後、
誰かが半年以上前のincidentを見つける。

これ、前回もDB latencyが原因でしたね!

その瞬間、
状況が急に整理される。

けれど同時に、
こうも感じました。

最初から、それが分かっていれば、もっと早く動けたのではないか


3.RAGを導入した背景

図2 障害情報を組み込んだRAGの基本構成

私たちが最初に取り組んだのは
RAG(Retrieval-Augmented Generation)
でした。

目的はシンプルです。

散らばった情報を、もっと探しやすくしたい

AI Chat(LLM)
に直接答えさせるのではなく、
まず関連情報を検索し、

その結果を
今の状況に合わせたContext
として渡す。

質問 ⇨ 検索 ⇨ 関連情報 ⇨ LLM ⇨ 回答

これによって

  • 過去incident
  • ポストモーテム
  • 障害対応履歴

などを、
まとめて扱えるようになりました。


4.意味を探すための検索

図3 EmbeddingによるSemantic Searchの流れ

最初にぶつかったのは、 従来の検索方法の限界でした。

実際には、
DB負荷によってサービス接続が遅くなっている
という同じ文脈で話していることも多いです。

しかし、 従来のKeyword検索では

  • 単語が完全一致しない
  • 表現が少し違う
  • 書いた人によって言い方が違う

だけで、
必要な情報へたどり着けないことがありました。

例えば
'DB latency'
で検索しても、

slow query
connection timeout
Database response delay

のような関連情報が、
検索結果に出てこないことがあります。

つまり、
私たちが本当に探したかったのは、

同じ単語ではなく、
同じ意味でした。

図4 Embeddingを保持したChunk

そこで導入したのが、
EmbeddingによるSemantic Searchでした。

Embeddingでは、
文章を“意味を持った数値”へ変換します。

"Payment API timeout"
           ↓
[0.13, -0.42, 0.91 ...]

のように、
文章をベクトル(数値の並び)へ変換します。

意味が近い文章ほど、
ベクトル同士の距離も近くなります。

つまり、
DB latencyDatabase 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

図にすると、現在の構造はこのようになります。

図5 全体関係設計図
図6 Graph DB(Neo4j)の全体ノード関係図

障害レベル基準は、(CPRule)の内容をもとに分けています。

図7 障害レベル基準と補足参照情報の分類

- Level 
  - 1(致命的/Critical)
  - 2(高 / High)
  - 3(中 / Medium)
  - 4(低 / Low)
- Reference
  - 共通ルール
  - 参考リンク
  - 注記

図7の分類設計をもとに、実際には図8のようにIncidentとLevelを紐づけました

図8 障害情報と、レベルの関係図

障害レベル毎に紐づいている障害情報と、
レベルを渡して関係がある情報を確認できるようになりました。

Level1からLevel4までは、
インシデントの障害レベル判定に使います。
共通ルール、参考リンク、注記は、
Incidentの補足参照情報として保存します。

つまり、
今回のGraph DB導入は、
単なるDB選定ではありませんでした。

それは、
自分たちのシステムを、どのような関係として見るのか
を設計する作業に近いものでした。


9.なぜGraph DBだったのか?

図9 Graph DB(Neo4j)にVector検索とGraph検索を統合
理由は、今回扱いたかったものが、

単なる「データ保存」ではなく、
「探索」だったからです。

たとえば、
過去の障害を調べるときに見たいのは、
単独のIncidentだけではありません。

  • この障害は、どのServiceに影響したのか
  • 同じServiceで過去に似た障害はあったのか
  • どのCPRuleに該当する障害レベルなのか
  • どのような対応Todoがあったのか
  • 検索用テキストとして、どのChunkに紐づいているのか

図10 Graph DBとVector DBを組み合わせた最終RAG構成

こうした情報は表として横に並べるよりも、
関係としてたどれる方が自然です。

特に今回の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. 最後に(次回へのつながり)

図11 知識ベースを使ったAgent化への発展

ここまでで、
私たちは

  • 過去incidentを探す
  • 関係をたどる
  • 「システムトラブルのレベルの算定基準」からレベル判断する

ところまではできるようになりました。

けれど、
本当にやりたいのは、
単なる検索ではありません。

過去を探すだけではなく

過去の判断を使って、今の意思決定を支援する

ことです。

次に目指しているのは:

  • ADR(Architecture Decision Record)
  • 過去の設計判断
  • 制約
  • 方針

などもつなげながら、

“今の問題に対して、次に何をすべきか”

を提案できるAgentです。

次回は、
さらに進化した話を書いてみたいです。
GraphRAGからAgentへ、
どこを変えると“判断支援”になるのかについて、
書いてみようと思います。

図12 RAGで検索をしてみました。

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 = 関係をたどる

それぞれ得意なことが違うため、
置き換えるというより、
役割を分けて使うのが自然でした。


エンジニアが再び出店者サミットに参加してみた

こんにちは、SRE のあべです。

2 年前に紹介した「出店者サミット」に、再び参加してきました!

今回は、福岡で開催された九州・沖縄サミットです。

記念写真

ちなみに、福岡県天神の"てんちか"と呼ばれる地下街にはくらしのマーケットの広告が流れてました。

てんちかで目を惹きつける広告が流れています

前回の記事はこちら ↓

tech.curama.jp

前回の記事では、エンジニア目線での発見や出店者との交流についてまとめましたが、今回はさらにパワーアップしたサミットの体験を紹介したいと思います。

改めて、サミットとは

「くらしのマーケット」に出店している出店者の方々が参加する、出店者コミュニティイベントです。

東京・大阪・名古屋・福岡など全国各地で開催されており、毎回多くの出店者が集まります。

基本的には出店者が企画・運営しており、発表やグループワークを通じて、

サービスページの作り方や集客方法などについて互いに情報共有し、学び合う・交流の場となっています。

再び参加した理由

2021 年の「くらしのマーケットアワード」で出店者の熱量に触れ、その後のサミットで実際に多くの出店者の方と交流したことが印象的でした。

そこでの交流や会話を通じてプロダクト開発に還元したいと思い、今回の参加を決めました。

エンジニアは普段の業務ではなかなか出店者と会話をする機会がないため、直接会ってお話がしたいと思っています。

今年は、大都市で開催されるサミットとは別に「地域交流会」を開催していて、「まずは会うこと」を目的に地方の中小都市を巡り講座や交流をしています。

地域交流会について、社長の浜野が解説しています ↓

open.spotify.com

実際に、地域交流会をきっかけにサミットに参加された方もいて、コミュニティの規模もそうですが、エネルギー・熱量がさらに上がっていることがわかりました。

感じたこと

出店者の AI 活用

プログラムの中に出店者が AI を活用して、出店者ページのブログを更新するという AI 活用講座がありました。

  • 生成 AI のモデルごとの活用比較
  • 音声入力
  • 画像生成

など利用して、ブログを作成するハンズオン発表をしていました。また、講座資料もすべて AI で作成したそうです。

くらしのマーケットでも、ユーザー向け・出店者向け・社内ツールで AI を利用した機能展開を始めていたり、出店者向けの AI 講座を開催しています。

その中で、出店者自らも AI を活用したページづくりをしていることから、改めて AI 活用がトレンドであり、必要性を実感しました。

出店者の研究熱心さは健在

前回同様、出店者の方々はサービスページのつくり方、写真や動画の工夫、説明文の最適化など、細部にまでこだわっていることがわかりました。

さらに各地域でのページづくり勉強会の開催もあり、その研究が日常的に続いていることに驚かされます。

また、どのように星 5 の口コミをもらうかという接客の観点での会話もあり、オンライン集客からオフラインでのユーザー体験を良くする話も聞くことができました。

エンジニアとして、日々の開発がどう影響しているのかを直接聞ける貴重な機会となりました。

また、今回はみんなのマーケットから、出店者向けの売上アップ講座やイベント全般の運用を担当している和田が、くらしのマーケットでのサービスページの重要性について登壇しました。

インターネット集客における重要な考え方やノウハウの共有に、出店者のみなさんがスマホで写真を撮ったり、メモを取りながら熱心に勉強している様子が印象的でした。

私は今年も、グループワークに参加させてもらい、「売上アップのコツ」や「嬉しかった口コミ」などお話を聞かせていただきました。

出店者にオシリを叩かれるあべ

叩かれたあと真剣にグループワークに参加するあべ

最後に

今回のサミットでは、これまでのサミットでお話した出店者の方が次のステージへ進んでいる姿も見られました。

その変化を実際に聞けることが、このイベントの醍醐味だと思います。

また、出店者の方々もくらしのマーケット社員に会いたいと思ってくれていて、実際に会うと喜んでいただけるのも嬉しかったです。

エンジニアもサミットのように「サービスを使う人と直接話す機会」は、開発したプロダクトがどう価値を生み出しているかを実感できる貴重な場です。

今回も、学びと気づきが多い体験となりました。

これからも、くらしのマーケットのサービスをより良くしていくために、開発チームとして努力していきたいと思います。

最後に、みんなのマーケットでは、くらしのマーケットの開発を一緒に盛り上げてくれるエンジニアを募集しています。

詳しくは、ぜひこちらをご覧ください。

システム刷新という話 第3話

minmaの開発組織の話

突然ですが、

みなさんはサッカーは好きですか?
私は好きです。
観るのも、
やるのも、
どちらも好きです。

週末はつい試合を観てしまいますし、
ワールドカップの年になると、寝不足になります。
そして2026年。 またワールドカップがやってきます。

そんなタイミングもあって、
今回は少し変わった形で、
私たちの話を書いてみようと思いました。

こんにちは。
エンジニアリングマネージャーのユジンです。

技術の話をそのまま説明するのではなく、
サッカーに例えて、
開発組織の話をしてみようと思います。

まず、この図を見てください。
一見するとフォーメーション図ですが、
実はこれは組織図です。
実際に、2026年の組織変更を検討した際に
私が社内共有で使っていた資料そのものです。

今日はその
「ある一試合」の話から始めたいと思います。

実はその試合、
今の開発組織の状況と、驚くほどよく似ていたんです。


第1部

1.キックオフ前

キックオフ10分前。

ナイトゲームのスタジアムは、まだ少しひんやりしていた。
ライトに照らされた芝は薄く湿っていて、踏むたびに小さな音がする。

観客席はゆっくりと埋まり始め、
ざわざわとした声が、波のように広がっていく。

オレンジ色のユニフォームを着た選手たちが、
ピッチの中央に集まった。

今日も、いつも通り。

誰かがストレッチをしている。
誰かがスパイクの紐を結び直している。
誰かは無言で空を見上げている。

やがて全員が肩を組み、スクラムを組む。

今日も、いつも通り。

同じフォーメーション。
同じメンバー。
同じ戦術。

最近の成績は2勝2敗。 悪くはない。 でも、突き抜けてもいない。

"いこう"

短い一言。

それだけで十分だった。

2.前半

ホイッスル。

試合開始。

立ち上がりは悪くなかった。
パスもつながる。
足も軽い。

"今日はいけるかもしれない"
そんな空気が、確かにあった。

でも、10分を過ぎたあたりから、
少しずつ違和感が出てくる。

相手のプレスが速い。
ボールを受けた瞬間、もう寄せられている。

奪われる。
追いかける。
また守る。
気づけば、自陣に押し込まれていた。

攻撃の時間がほとんどない。
ずっと走っているのに、なぜか前に進めない。

前半18分。最初の失点。
ゴールネットが揺れる。

その音だけが、
やけに大きく聞こえた。

一瞬、誰も声を出さない。
"まだ1点だ。"

誰かが手を叩く。
無理やり前を向く。

再開。
でも流れは変わらない。

前半30分、 2失点目!

足が重くなってきた。
呼吸が荒い。
それでも、まだ心は折れていなかった。

前半終了間際。
やっと訪れたチャンス。
左サイドを抜け出し、クロス。
ダイレクトシュート。
ゴールキーパーの指先に触れ、ポストをかすめて外れる。

"うわっ…!"

スタジアム全体が息を飲む。
入っていれば、流れは変わったかもしれない。
でも、入らなかった。

それでも、そのワンプレーが妙に希望に見えた。
"いけるかもしれない"

根拠はない。
でも、そう思いたかった。

そして
追加点。
0 : 3

スコアボードの数字が、
冷たく光る。 3点差。

苦しい。
でも、まだ終わった気はしなかった。
"1点返せば、流れは変わる"

誰もが、
そう信じてロッカールームへ向かった。

3.ハーフタイム

汗が引いて、急に寒くなる。
床に座り込み、うつむく選手。
無言で水を飲み続ける選手。

監督がホワイトボードを出す。
"攻撃の枚数、増やす。"
"全員、前に出る。"
"ゴールを取りにいくしかない。"

冷静な分析というより、決断だった。

3点差。

その現実が、判断を急がせた。
リスクがあることは、みんな分かっていた。

でも、他に手が思いつかなかった。

後半

ラインが一気に上がる。
守備のはずの選手も前へ。
ボールを奪ったら、とにかくロングボール。

つなぐ余裕はない。
ただ前へ。
ただ遠くへ。
祈るようなパス。
でも、そのほとんどは相手に跳ね返される。

その瞬間、後ろはがら空き。
サイドバックが全力で戻る。
また戻る。

何度もスプリント。
肺が焼ける。
ボランチは迷子だった。
攻めるのか、
守るのか。

中途半端な位置を走り回るだけ。
フォーメーションなんて、もうない。
ただ混乱だけがあった。

そして失点。

0 : 4

さらに失点。

0 : 5

足をつって倒れる選手。
交代!

入った若手は、まだこのスピードについていけない。
パスがずれる。
タイミングが合わない。
そこを突かれる。

カウンター。

失点。

0 : 6

最後の数分は、誰も覚えていない。
ただ時間が早く過ぎてほしいと思っていた。

ホイッスル。

試合終了。

歓声は相手側だけ。

オレンジの選手たちは、
ただ静かに歩き出す。
誰も目を合わせない。
誰も何も言わない。

しばらくして、
誰かがぽつりと呟いた。

"悪いけど、6点で済んでよかったな。"

その言葉が、
妙に現実的だった。

悔しいというより、
ただ疲れていた。

完全な敗戦だった。

その夜。

スタジアムを出ても、
誰もすぐには帰らなかった。

ロッカールームの前で、
スパイクを脱ぎながら、ただ黙って座っている。

6失点。

完敗。

今日もいつもの通り、
走った。
声も出した。
気持ちも切らさなかった。

それでも勝てなかった。

なぜだろう。

個人の努力が足りなかったのか。
気合が足りなかったのか。

たぶん、
違う。

フォーメーションが崩れていた。
役割が曖昧だった。
守備と攻撃を同時にやろうとして、
どちらも中途半端になっていた。

つまり

"戦い方"そのものが間違っていた。

ここまで読んで、
"なんのサッカー小説だ?"と思った方もいるかもしれません。

でも実は、これはサッカーの話ではありません。

これは、
いま私たちの開発組織で実際に起きていた出来事を、
サッカーに置き換えて書いただけの話です。

技術負債の対応と、
現行サービスの運用と、
新機能の開発を、
同じメンバーで、同時に、全部やろうとしていた。

その結果が

0 : 6

でした。。。


第2部

組織を作り直した理由 ー Conway’s Law と minma の設計思想

前回は、サッカーの試合に例えて
minmaがなぜ押し込まれ続けたのかを書きました。

第2部では、

なぜ負けたのか。
どうチームを組み直したのか。
そして、これからどう戦うのか。

実際に私たちが行った組織設計の話をします。

ここからは、テックブログとして、少し真面目な内容です。

問題の本質は"技術"ではなかった

最初に結論から言います。

当時の課題は、

  • 言語選定
  • フレームワーク
  • クラウド構成
  • CI/CDの自動化

といった技術的な選択ではありませんでした。 もちろん、改善余地はありました。 けれど、それは本質ではなかった。 本質はもっと手前。

市場の変化に対して、組織が追いつけなくなっていたこと。

市場は止まらない

minma は15年以上続くサービスです。

その間に、

  • ユーザーの行動は変わり
  • デバイスは変わり
  • 期待される体験は変わり
  • 競争環境も変わり

市場は常に変化します。

プロダクトも、成長とともに複雑になります。

けれど、組織構造は一度固まると、
簡単には変わりません。

その結果、

市場は動いているのに、
内部の意思決定と開発構造は、
ゆっくりになっていく。

このズレが、徐々に効いてきます

そして気づいたときには、
「押し込まれ続ける試合」になっている。

当時の構造

当時のminmaは、以下を同時に抱えていました。

  • 技術負債の返済
  • 新規機能開発
  • 既存運用
  • 障害対応
  • インフラ改善
  • エンジニア採用・育成

そしてこれらを、

同じメンバーが、同時に、全部やっていた。

つまり、
全員 = 刷新 + 運用 + 障害 + 改善 + 採用 + 育成

一見、効率的に見えます。

全員が全部できる。
柔軟で、強そうに見える。

でも、強度が上がると崩れます。

  • コンテキストスイッチが増える
  • 専門性が育たない
  • 設計が後回しになる
  • 火消しが優先される
  • 技術負債が積み上がる
  • 育成の時間が削られる

特に育成は、
緊急ではないが重要な仕事です。

緊急対応が続く組織では、
真っ先に削られる。

その結果、
未来の戦力が育たない。

努力の問題ではありません。

構造的に、市場適応力が落ちる設計でした。

Conway’s Law

ここで重要なのが、
私がずっと信じている法則です。

Conway’s Law

システムを設計する組織は、
その組織のコミュニケーション構造を反映した設計を生み出してしまう

組織が曖昧であれば、

  • モノリス化
  • 責任不明コード
  • 依存の肥大化
  • 技術負債の蓄積

が自然に起きます。

組織が責任単位で分離されていれば、

  • API境界は明確になり
  • サービスは疎結合になり
  • 変更は容易になり

つまり、

アーキテクチャは市場適応力の鏡であり、
その根本は組織設計にある。

組織がモノリスなら、
どんなマイクロサービスも、やがてモノリスに戻る。

技術刷新ではなく、適応構造への刷新

今回私たちが目指したのは、

一度きりの「技術刷新」ではありません。

市場の変化を読み、
継続的に構造を調整できる組織にすること。

そのための設計原則はシンプルでした。

  • トレードオフしない
  • 役割を混在させない
  • 責任を固定する

新しい組織構造

刷新チーム   → 未来を作る(攻撃)
即応チーム   → 今日を守る(守備)
SRE     → 基盤最適化(制御)
QA      → 品質保証(リスク管理)

ポイントは「役割固定」です。

コンテキスト混在を排除し、
責任境界を明確にする。

これにより、

組織分離 → 責任明確 → サービス分離 → 疎結合化

が自然に起きます。

そしてそれが、
市場の変化に適応できる構造を作ります。

現在地

誤解してほしくないのは、
これまでのやり方が間違いだったわけではない、
ということです。

その時点では最適だった。
けれど、
環境が変われば、最適も変わる。

私たちはまだ完成していません

  • 技術負債は残っています
  • 障害もゼロではありません
  • 課題は山積みです

ただ、

以前は「耐える構造」でした。
今は「適応できる構造」に変わりつつあります。

ここが、大きな違いです。

そして、ここで終わりではありません。

今の組織も、
やがてまた変わります。

いまは最適化されたチーム構造かもしれません。

けれど、市場が変われば、
プロダクトが変われば、
組織の規模が変われば、
最適もまた変わります。

最後に、

もしこの記事を読んで、
「この組織設計、ちょっと面白そうだな」
と思った方。

私たちは、
万能な選手を求めているわけではありません。

ポジションを持ち、
その責任に集中できる人を求めています

minmaでは
入団テスト(採用)を常時開催中です。

フォワードも、
ボランチも、
センターバックも、
サイドバックとGKも、

市場は止まりません。

だから私たちも、止まりません。


次回予告

次回は、

各チームの具体的なミッション

実際の取り組み
連携方法を紹介します。

より実践的な内容になる予定です。

システム刷新という話 第2話

キッチンとホールのあいだで(バックエンドとフロントエンドのあいだで)

こんにちは。エンジニアリングマネージャーのユジンです。

今回は、minma がいま抱えている課題について、 少しお話ししてみたいと思います。

minma では、月に一度、業績共有会があります。
その中で、ときどき「くらしのマーケット」のシステムの状況について、
全社の前で共有する機会があります。

ただ、会場にはエンジニアではない方が、8割ほどいらっしゃいます。
そのため、技術の話をそのまま説明しても、なかなか伝わりにくく、
「ちゃんと伝えられなかったな」と感じることが、何度かありました。

そこで今回は、そのときの反省も踏まえて、
「技術の話」をそのまま伝えるのではなく、
小さな物語として、いま起きている課題を共有してみようと思います。

少しでも、現在 minma が直面している状況や、
その難しさを感じていただければ幸いです。


キッチンとホールのあいだで

看板は控えめ、席も少なく、
ただ「料理がおいしい」という評判だけで、
毎年少しずつ客が増えていった。

一年目、二年目、三年目、十年目。
気づけば、店は毎年のように売上を伸ばし、
新しいメニューが増え、
新しいスタッフが増え、
店は静かに“成長”していた。

だが、成長はいつも、少し遅れて問題を連れてくる。


1. 多すぎる言葉、足りない言葉

ある日の昼下がり。
ひとりの客が、穏やかな声で尋ねた。

"すみません。今日のおすすめは、何ですか?"

ホールスタッフはキッチンに向かい、そう伝えた。

しばらくして、キッチンの奥から、分厚い紙束が差し出された。

そこには、料理名と値段だけでなく、

  • レシピ、
  • 原材料の産地、
  • 原価、
  • 調理の工程、
  • 栄養成分、
  • 仕入先の情報まで、

びっしりと書かれていた。

ホールスタッフは紙をめくりながら、少し困った顔をした。

'お客さんが知りたいのは、
「Minmaハンバーグ、1,200円」だけなんだけどな'

別の日には、逆のことも起きた。

キッチンから返ってきたのは、短い一言だけ。

"Minma-209"(料理IDだけが返ってきた)

'料理の名前は?'
'デザートある?'
'飲みものある?'

ホールとキッチンを、何度も何度も往復する。

情報は、多すぎたり(OverFetch)、少なすぎたり(UnderFetch)が
いつのまにか、この店の日常になっていた。

店は成長していた。
けれど、店のコントロールは、少しずつ、むずかしくなっていた。


2. 彼が来てから

そんな頃、一人のホールスタッフがやってきた。

特別に派手なところはなかった。
けれど、彼は、よく“見る”人だった。

客の表情を見て、
どこで迷い、
何を知りたがり、
いつ決めようとしているのかを、静かに読み取っていた。

"おすすめは?"

そう聞かれると、彼はキッチンに行き、こう伝えた。

"料理名と、値段と、写真と、ひと言コメントだけください。 "
"それで、十分です。"

返ってきた一枚の紙を手に、
彼は笑顔で席に戻る。

"本日のおすすめは、Minmaハンバーグ、1,200円です。"
"今日はソースを少し改良していますよ。"

客はすぐにうなずき、注文した。

店の流れは、見違えるほどよくなった。

彼は、
多すぎる情報をそっと削り、
足りない情報を先回りして拾い、
いつも“ちょうどいい形”に整えていた。

店長は、彼の背中を見ながら思った。

'この人がいれば、店は回るなぁー'

けれど同時に、別の思いも浮かんでいた。

'でも、こんな人を、
これから先も、何人も見つけられるだろうか'


3. 人だけでは回らなくなって

店は、さらに成長した。

新しい店舗ができ、
新しいメニューが増え、
アプリ注文が始まり、
テイクアウトも増えた。

質問の種類は、日に日に増えていった。

だが――

彼のような人は、ほとんどいなかった。
教えても、同じ動きはなかなか再現できない。
忙しくなると、説明はばらつき、
店舗ごとに答えが変わってしまう。

ある日、新人がキッチンに言った。

"全部ください!"

また、分厚い紙束が戻ってくる。

別の日、別の新人は、

"えっと Minma-209?"

また、長い往復が始まる。

店長は、夜の片付けをしながら、静かにつぶやいた。

"これは、人の問題じゃないな。"
"仕組みの問題だ"


4. 小さなカウンター

その年、店は、小さな決断をした。

ホールとキッチンのあいだに、
小さな“モダンなカウンター”を置くことにした。

それは人ではない。
けれど、こんな役割を持たせた。

  • 客の質問ごとに、必要な情報を定義する。
  • キッチンには、決まった形式で聞く。
  • 料理ごとに、最適な説明を用意する。
  • 誰が使っても、同じ答えが返るようにする。

ホールスタッフは、まずそのカウンターに聞く。

"ランチ画面用のおすすめをください"

カウンターはキッチンに伝える。

"料理名・写真・値段・コメント形式でお願いします"

キッチンは、それに合わせて返す。

新人でも、迷わなくなった。
ベテランでも、翻訳に悩まなくてよくなった。

優秀な人の仕事が、仕組みとして、そっと残された瞬間だった。

店は、また少し、うまく回り始めた。


5. 橋が、城になりかけた頃

しばらくのあいだ、すべては順調に見えた。

品質は安定し、
教育は楽になり、
店舗が増えても、店は回った。

けれど、成長は、また新しい問いを連れてきていた。

メニューが増え、
キャンペーンが増え、
画面が増え、
条件が増え、
例外が増えた。

そのたびに、誰かが言った。

"ここで少し計算しましょう "
"在庫チェックもここで"
"キッチン側は変えにくいので、こっちで吸収しましょう"

いつのまにか、
カウンターは、ただの橋ではなくなっていた。

データを組み立て、
条件を持ち、
キャッシュを持ち、
画面の事情を、深く知りすぎていた。

ある日、キッチンの人が、ぽつりとつぶやいた。

'最近、この店、'
'キッチンより、カウンターが一番忙しそうですね。'


6. 主役は、誰だったのか

問題が起きると、皆が迷った。

'これはキッチン?'
'それともカウンター?'
'ホールの使い方?'

境界は、少しずつ曖昧になっていった。

閉店後、静かな店内で、
店長は椅子に腰かけ、ゆっくりと考えた。

そして、静かに言った。

'ただし――'
'仕組みは、放っておくと、'
'いつのまにか“主役”になってしまう。'

'本当の主役は、'
'キッチンであり、'
'料理であり、'
'客なんだ。'

少し笑って、こう続けた。

'カウンターは、'
'あくまで“橋”でいい。'
'城になってはいけない。'

店長は、しばらく何も言わずに、店を見回した。

いまのカウンターは、橋のままだろうか。
役割は、ちゃんと定義できているだろうか。
キッチンは、本業に集中できているだろうか。
ホールは、シンプルに客と向き合えているだろうか。

答えは、まだ、出ていない。

けれど。

課題を感じ、
どうすればいいのかを考え続けている限り、
この店は、きっと、大丈夫だろう。

そう思いながら、
店長は、明日のメニュー表をそっと閉じた。

こうしてその店は、今日も成長を続けている。
課題を感じながら、
どうすればいいのかを、考え続けながら。


登場単語 / システム対応表

物語の単語 システム
お客 ユーザー
注文・質問 画面操作 / ユースケース
ホールスタッフ フロントエンド
優秀なホールスタッフ 良いフロント設計者 / BFF設計者
キッチン / シェフ バックエンド / ドメイン
料理 ドメインデータ / ビジネス結果
分厚い紙束 OverFetch
IDだけの返答 UnderFetch
小さなモダンカウンター BFF(Backend For Frontend)
城のように育ったカウンター 肥大化したBFF
店長 アーキテクチャ
成長する店 成長するプロダクト・組織

最後に、

長い内容をここまで読んでいただき、ありがとうございました。

minmaでは、成長に伴う複雑さと正面から向き合い、
システムのレイヤごとの役割を明確にしながら、
段階的に改善を進めていく方針を大切にしています。

この物語が、
アーキテクチャや組織づくりを考えるひとつのヒントになれば幸いです。

そして次は、
この「橋」をどう定義し直そうとしているのか。
そして、それを支えるminmaのテック組織について書いてみたいと思います。