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

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

システム刷新という話 第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のテック組織について書いてみたいと思います。

システム刷新という話

「システム刷新」という言葉を、
皆さんはどんな場面で聞いたことがあるでしょうか?

はじめまして。
私は、みんなのマーケット株式会社(以下、minma)で、エンジニアリングマネージャーをしています。
ちょうど一年前、この場所にやってきました。

minmaでは、約15年にわたって「くらしのマーケット」というサービスを運営しています。
約400を超えるサービスカテゴリ。
それぞれの暮らしのすぐそばで、今日も静かに使われています。

長く続くサービスには、必ず時間が積もります。
ユーザーが増え、店舗が増え、
その一つひとつに応えるように、システムも姿を変えてきました。

開発のやり方が変わり、
運用の考え方が変わり、
使う技術も、時代とともに移り変わっていく。

その変化に、minmaのエンジニアたちは、
壊さないように、止めないように、
静かに、誠実に向き合い続けてきました。

一方で、社会もまた、15年の間に大きく変わりました。
気候、価値観、働き方。
それに合わせて、会社も、人も、少しずつ形を変えてきたのだと思います。

けれど、変化が長く続くと、
ほんの小さな遅れや、わずかな判断のズレが、
気づかないうちに積み重なっていきます。

システムは、「今」に追いつけないまま、
その場ごとの判断で、なんとか応え続けてきました。

良かれと思って重ねた対応は、
いつしかシステムの中心に、大きく重たい"核"をつくります。

小さな変更でも、中心が揺れる。
大きな変更には、長い時間が必要になる。
気づけば、動く速さは、少しずつ落ちていました。

もちろん、立ち止まっていたわけではありません。
これまでに、何度もリニューアルを行ってきました。
小さなものも、大きなものも。

そのたびに、手応えはありました。
「良くなった」
「これで大丈夫だ」

それでも、時間が経つと、
また同じような問題が、似た場所から顔を出す。

この感覚に、私は既視感を覚えました。

それは、ダイエットに似ています。

一時的に運動量を増やせば、体は変わります。
数値も、見た目も、確かに結果は出る。

けれど、生活そのものが変わらなければ、
時間とともに、元の状態へと戻っていきます。

そしてまた、
同じ決意をして、
同じことを繰り返す。

終わりのない宿題のようです。

システムも、同じなのだと思います。
バージョンアップや技術刷新は、確かに必要です。
けれど、それだけでは体質は変わらない。

ダイエットが続くのは、
運動や食事だけでなく、
暮らしそのものが変わったときです。

無理をしなくても続くこと。 止めなくていいこと。

システム刷新も、
一度きりのイベントではなく、
続いていく前提であるべきだと、私は考えています。

健全な文化があり、
その中で自然に選ばれる設計があり、
少しずつ、しかし確実に、前へ進んでいく。

このブログでは、
minmaのシステム刷新について、
成功だけでなく、迷いや違和感も含めて、
エッセイのように綴っていこうと思います。

エンジニアだけでなく、
デザイナー、プランナー、ディレクター
ものづくりに関わるすべての人に、
どこか引っかかる一文が残れば、それで十分です。

もし、
よければ、一緒に歩いてください。
システム刷新という、長く静かな旅を。

次は、minmaのシステムが抱えている課題を、
もう少し物語として紹介してみようと思います。