🚀 スケールする「知識ベース型メモサービス」の裏側を解説してみる
📌 まず前提:このサービスが解決したい問題
普通のメモアプリは、メモが増えるほどこういう現実的な問題を抱える:
-
メモが大量になると取得が遅い
→ DB のクエリが増える(N+1問題) -
関連メモの計算が重い
→ embedding 類似度計算やタグ比較は O(N²) -
全文検索が遅い
→ 正規表現ではスケールしない、インデックスが必要 -
毎回同じデータを返す API が多い
→ キャッシュ必須(特に Redis) -
メモ同士の「意味的な関係」を作りたい
→ embedding + タグでリンク付けする必要がある -
グラフ表示(知識グラフ)が重い
→ 大量ノードの位置計算・クラスタリングが必要
🎯 このファイルの目的(最重要ポイント)
ただメモを CRUD するのではなく、
スケールする “知識ベース” を構築するサービス を作ること。
つまり:
- 高速
- 賢い検索
- 意味的に関連するメモ推薦
- グラフ構造
- コスト最適化
- キャッシュで高速化
これらすべてを実現する 高度なメモ処理エンジン がこのサービス。
技術ハイライトを 解説する
1️⃣ N+1問題の解消($lookup / bulkWrite)
🔥 N+1問題とは?
メモを1件取得 → 関連メモを N件クエリする
→ クエリが O(N) 回
→ 超遅くなる
🔥 解決:MongoDB Aggregation $lookup
$lookup: {
from: 'memos',
let: { relatedIds: '$related_memo_ids' },
pipeline: [
{ $match: { $expr: { $in: ['$_id', '$$relatedIds'] } } }
],
as: 'related_memos_data',
}
これにより:
- 1回のクエリで全部の関連メモを取得可能
- 速度が劇的に改善(例:1ms → 0.2ms)
🔥 bulkWrite で双方向リンクを一括更新
bulkOps.push({
updateOne: {
filter: { _id: otherMemo._id },
update: { $addToSet: { related_memo_ids: memoId } },
},
});
大量の更新を 1回の DB リクエスト にまとめる → 高速 & 安定。
2️⃣ 全文検索(text index + relevance score)
MongoDB の text index を使う:
$text: { $search: query }
さらに:
$addFields: { searchScore: { $meta: 'textScore' } }
$sort: { searchScore: -1 }
→ 検索エンジンのような関連度順ソート が簡単にできる。
3️⃣ Redis 多層キャッシュ戦略
キャッシュを「用途別」に分けて TTL 最適化している。
| 種類 | TTL | 意味 |
|---|---|---|
| MEMO_LIST | 5分 | メモ一覧は頻繁に変わる |
| MEMO_DETAIL | 10分 | 詳細はそこまで更新されない |
| RELATED_MEMOS | 15分 | 計算コストが重いため長め |
| SEARCH | 3分 | 新メモが影響しやすい |
| TAG_CLOUD | 10分 | 集計コストが高い |
| STATS | 30分 | 統計はほとんど変わらない |
→ データの性質に最適化された TTL が性能を支える。
4️⃣ Embedding とタグで「類似メモ」計算
✔ Embedding 類似度(Cosine Similarity)
cosineSimilarity(embeddingA, embeddingB)
✔ タグ類似度(Jaccard 係数)
intersection / union
✔ 2つを合成して “関連度スコア” を生成
combined = embedding * 0.7 + tag * 0.3
これにより:
- 意味が似てる → 高スコア
- タグも同じ → さらに高スコア
Notion AI 風の関連メモ推薦 が可能に。
5️⃣ 知識グラフ(Knowledge Graph)生成
- ノード = メモ
- エッジ = 類似度が一定以上のメモ同士(例:0.6 以上)
✔ Force-Directed Layout(物理シミュレーション)
- ノード同士は反発
- 関連ノードは引き合う
- 自然なクラスタリングが生まれる
→ Obsidian / Notion AI のような知識グラフを再現できる。
6️⃣ Aggregation Pipeline で統計処理を高速化
例:タグ頻度 / 今週のメモ数 / トレンド など
$facet: {
basic: [...],
thisWeek: [...],
topTags: [...],
}
→ 1回のクエリで全統計を取得
→ サーバーもDBも軽くなる
7️⃣ キャッシュ無効化ロジック
変更があり得る部分だけ invalidate:
invalidateMemoCache(userId, memoId)
invalidateUserCache(userId)
→ 全部消さないので高速
→ キャッシュ破綻もしない
→ 一貫して安定した性能を維持