
Article
プライベート AI Google Drive を Go + SQLite で自作した話
家族の記録、子どもの学校書類、医療関係の書類、日々のジャーナル——気づけばこれらが Dropbox と Google Drive と iCloud に散らばっていた。「あの書類どこに入れたっけ」と毎回探す。クラウドの検索は弱いし、AI に「去年の娘の医療費まとめて」と頼もうにも、そもそもデータが渡せない。
これが Atlas を作り始めた動機だ。
なぜ自作したのか
市販のクラウドストレージへの不満は主に 3 点だった。
プライバシー: 子どもの学校書類や家族の医療記録を Google のサーバーに置くことへの根本的な不安。Gemini が「写真を分析してサマリーを作ります」と言ってきたとき、正直ぞっとした。
AI との連携: 自分のデータを AI に活用させたい。「去年の娘の矯正歯科の経過まとめて」「塾の入室説明会の資料あった?」——こういう質問に即座に答えてほしいが、クラウドサービスの AI 連携は閉じた生態系の中でしか動かない。自分のデータを自分の AI で使いたい。
検索品質: Dropbox の検索は英語に強くて日本語に弱い。Google Drive は画像の中のテキストを探せるが、ファイル間の関連付けはできない。「塾関係の書類を全部」という横断検索が難しい。
設計思想: ファイルシステムが正ソース
設計で最初に決めたのは 「ファイルシステムが正ソース」 という原則だ。
データベース (.atlas/cache.db) は検索を速くするためのキャッシュに過ぎない。壊れたらファイルから再構築できる。これが絶対条件だった。
理由はシンプルで、AI パイプラインは失敗する。ネットワークが切れる、モデルがクラッシュする、バグが潜んでいる。そのたびに「DB とファイルどちらが正しいのか」と悩みたくない。ファイルが常に正しい、これだけ覚えていればいい。
v0.1 から v0.2 への転換で最大の変更はここだった。v0.1 は SQLite に全情報を持たせようとしていた。v0.2 では Markdown + Git が正ソース、cache.db は派生物として完全に位置づけを変えた。
state = 物理フォルダ
ファイルの「状態」を表すのにデータベースのカラムは使わない。置き場所がそのまま状態を表す。
~/Atlas/
├── inbox/ ← とりあえず放り込む。未処理
├── sources/ ← AI 処理済み
│ ├── education/
│ ├── family/
│ ├── journals/
│ └── ...
├── entities/ ← 人物・組織・場所のまとめページ
├── concepts/ ← トピック・テーマのまとめページ
├── .sources/ ← 元ファイル (PDF・画像) の保管場所
└── .atlas/
└── cache.db ← FTS5 + embeddings (派生物)
inbox/ にあるファイルは未処理、sources/education/ にあれば「教育関係・AI 処理済み」。状態をクエリするのに SQL を書く必要がない。ls inbox/ で現在の処理待ちが全て分かる。
frontmatter でメタ情報管理
全ての Markdown ファイルは YAML frontmatter を持つ。これが「ファイルシステムが正ソース」の実装だ。
---
id: 550e8400-e29b-41d4-a716-446655440000
type: source
source: /.sources/塾の入室説明会.pdf
tags: [education, 学習塾, 娘]
summary: 近隣の学習塾の入室説明会資料。2026年4月入室テストのスケジュール、月謝、授業形式について記載。
entities:
- name: 娘
type: person
- name: 学習塾
type: organization
events:
- title: 塾の入室テスト
start: 2026-04-12T10:00:00+09:00
created: 2026-04-07T10:00:00+09:00
updated: 2026-04-07T10:00:00+09:00
---
# 学習塾の入室説明会(2026年4月)
## 概要
近隣の学習塾の入室説明会に参加した。...
cache.db が壊れても、この frontmatter があれば完全に再構築できる。これが核心だ。
技術スタック
Go シングルバイナリを選んだ。理由は単純で、Mac Studio に常駐させるサービスとして Python や Node.js の依存管理をしたくなかった。go build で吐き出された単一バイナリを launchd に渡す——それだけでいい。
atlas-api ← HTTP サーバー + 処理キュー
atlas-indexer ← cache.db 再構築・FTS 更新
atlas-batch ← 夜間バッチ (人間の編集検出 → AI 更新)
SQLite は FTS5 全文検索と sqlite-vec によるベクトル検索を使う。外部サービス不要で、シングルファイルでバックアップも楽だ。
-- FTS5 全文検索
CREATE VIRTUAL TABLE files_fts USING fts5(
title, tags, content,
content=files, content_rowid=rowid
);
-- ベクトル検索 (sqlite-vec)
CREATE VIRTUAL TABLE embeddings USING vec0(
file_id TEXT PRIMARY KEY,
embedding FLOAT[1024]
);
検索は FTS5 + ベクトル検索を RRF (Reciprocal Rank Fusion) で融合するハイブリッド方式を取っている。「娘の矯正歯科」のような正確なキーワードは FTS5 が拾い、「去年の娘の健康記録」のような意味的クエリはベクトル検索が補う。
さらに entities/ や concepts/ のハブページを最優先にするランキングを入れた:
GET /api/v1/search?q=娘
├── Phase 1: entities/娘.md が最上位 (ハブ優先)
└── Phase 2: 娘に言及する source 全件 (FTS + Vector RRF)
AI パイプライン
inbox にファイルを放り込むと、非同期で Gemma 4 31B (ローカル MLX) が処理する。
inbox/塾の入室説明会.pdf を投入
↓ POST /api/v1/process (クライアントがトリガー)
↓ Gemma 4 31B (ローカル MLX, port 11434)
│
├─ sources/education/塾の入室説明会.md を生成
│ summary / tags / entities / events を frontmatter に書き込む
│
├─ entities/娘.md を更新 (関連ドキュメントを追記)
├─ entities/学習塾.md を更新 or 新規作成
├─ concepts/中学受験.md を更新 or 新規作成
│
├─ 元ファイルを .sources/ に移動
├─ inbox/ から元ファイルを削除
│
└─ git commit (Author: atlas-ai)
"ingest: 塾の入室説明会.pdf → sources/education/"
AI の出力はファイルに書き込まれる。DB には書かない。AI パイプラインが中断しても、処理済みの Markdown は残る。
Git の author を atlas-ai にするのはポイントで、夜間バッチが「人間が書いた差分」を AI に食わせるとき、atlas-ai のコミットは除外する。自分が書いたものを自分で再処理する無限ループを防ぐためだ。
Chat API: tool-calling エージェント
検索するだけでなく、自然言語で質問に答える Chat API も作った。
POST /api/v1/chat
{ "message": "去年の娘の矯正歯科の記録まとめて" }
内部では tool-calling エージェントループが回る:
1. LLM がクエリを受け取る
2. search_hybrid tool を呼ぶ → 関連ファイルを取得
3. get_file tool で内容を読む
4. 必要なら追加検索
5. 最終的に日本語で回答を生成
Tool は 6 つ用意した:
| Tool | 役割 |
|---|---|
search_hybrid |
FTS5 + ベクトル融合検索 |
get_file |
ファイル本文取得 |
list_recent |
最近のファイル一覧 |
get_metadata |
frontmatter 取得 |
patch_file |
ファイル更新 |
update_metadata |
frontmatter 更新 |
クライアント
iOS アプリ (SwiftUI) と TUI クライアント (BubbleTea) を作った。どちらも HTTP API 経由で、クライアント側にロジックはない。
iOS からの典型的な使い方:
- 書類をカメラで撮影、inbox にアップロード
- 「処理」ボタンをタップ → AI が自動でメタ情報を抽出
- Chat FAB から「この書類について聞く」
- 回答と引用元のリンクが返ってくる
インフラ: Mac Studio 常駐
Mac Studio (M4 Max 64GB)
├── atlas-api (launchd, port 8081)
├── MLX Gemma 4 31B (launchd, port 11434)
└── MLX e5-large (launchd, port 11435, embeddings)
Tailscale
└── 家族の iPhone からどこからでもアクセス
launchd の plist を書いておくと Mac を再起動しても自動で起動する。監視コマンドを定期的に叩いて生きているか確認する、という管理が不要になった。
アーキテクチャ図
外部クライアント (iOS / TUI / n8n / Telegram)
│
│ Tailscale
▼
┌─────────────────────────────────────────────┐
│ Mac Studio │
│ │
│ atlas-api (port 8081) │
│ ├── /files CRUD + 検索 │
│ ├── /process AI 処理キック │
│ ├── /chat tool-calling エージェント │
│ └── /events カレンダー連携 │
│ │
│ MLX Gemma 4 31B (port 11434) │
│ MLX e5-large embed (port 11435) │
└─────────────────────────────────────────────┘
│
▼
~/Atlas/ (ローカル SSD・Git リポジトリ)
├── inbox/ ← 投入口
├── sources/ ← AI 処理済み Markdown
├── entities/ ← 人物・組織・場所
├── concepts/ ← トピック・テーマ
├── .sources/ ← 元ファイル (PDF・画像)
└── .atlas/
└── cache.db ← FTS5 + embeddings (派生物)
実際の運用例
平日の朝、学校からのプリントを iPhone でスキャンして inbox にアップロードする。「処理」ボタンを押して Atlas に手渡す。30 秒ほどで Gemma が summary と tags と entities を生成して sources/education/ に分類する。
週末に「今月の娘の学校行事まとめて」と Chat に打ち込むと、カレンダーの events frontmatter を拾って一覧を返してくれる。
書類を探す時間がほぼゼロになった。それだけで作った甲斐があった。
まとめ
- ファイルシステムが正ソース — DB が壊れても困らない設計
- state = 物理フォルダ — パスを見れば状態がわかる
- frontmatter でメタ情報管理 — Markdown ファイル単体で完結
- Go シングルバイナリ + SQLite — 依存なし、運用が楽
- Gemma 4 ローカル — プライバシー完全保護、完全オフライン動作
プライバシーとデータ主権を保ちながら AI を活用したい人には、自作が現時点の最善解だと思っている。クラウドサービスが「あなたのデータをサーバーに送らずにローカルで AI 処理」を本気で実現するまでは。
