スマートフォン、カメラ、家族から共有された写真。 何年分もNASへ保存していると、データは残っていても、見たい一枚へたどり着くのが難しくなります。
日付やフォルダ名が分かっている写真なら探せます。 でも実際に思い出すのは、「みんなで焚き火をした夜」「雨の日の外遊び」「誕生日ケーキ」のような場面です。
そこで、NASにある写真をAIで読み解いて検索用データベースを作る photo-organize と、そのデータをブラウザから探して見られる Photo Browser(リポジトリ名は photo-browser) を作りました。
一文でいうと、次のようなシステムです。
NASに眠る大量の写真を原本の場所から動かさず索引化し、Geminiの日本語説明と意味ベクトルを付けて、思い出を言葉で探せるようにする個人用の写真検索基盤です。
2つのプロジェクトに分けた理由
この仕組みは、写真を準備する側と、日常的に使う側を分けています。
| プロジェクト | 主な役割 |
|---|---|
photo-organize |
写真の走査、日時取得、重複確認、AI解析、ベクトル化、SQLite登録、サムネイル生成 |
photo-browser |
自然文検索、年月別タイムライン、お気に入り、アルバム、写真詳細、類似写真の表示 |
写真のAI解析やサムネイル作成は時間のかかる前処理です。 一方、ブラウザで写真を探したり、アルバムへ追加したりする操作は、普段使いする軽い処理です。
この2つを分けることで、重い処理を毎回やり直さず、作成済みの検索DBをブラウザ側から利用できるようにしました。

photo-organize は「移動」だけではなく「索引化」する側
名前だけを見ると、写真を別フォルダへ並べ替えるスクリプトに見えるかもしれません。 実際、Exifの撮影日時、ファイル名に含まれる日時、ファイルの更新日時を順に確認し、必要なら YYYY/MM の構造へコピーまたは移動する機能があります。
ただし、現在の大規模NAS向けフローでは、写真をコピーせず、原本のパスをそのままSQLiteへ登録する方向を中心にしています。
つまり、元写真の置き場所は変えません。 写真の実体とは別に、「いつ撮ったか」「何が写っているか」「どの言葉と意味が近いか」を持つ検索レイヤーを作ります。
物理的な整理を行う場合も、安全側の設計にしています。
- 初期動作はファイルを変更しない
dry-run - コピー・移動前に保存先と重複候補をレポートで確認
- 同名ファイルがあっても上書きしない
- 実ファイル操作を選んだときは、開始前に中止できる待機時間を入れる
- SHA-256で完全一致の重複を確認できる
写真整理は、一度の事故が大きな損失につながります。 そのため、「便利さ」より先に「原本を壊さないこと」を置きました。
AIが担当していること
現行実装では、AIを主に2か所で使っています。
1. 写真の内容を日本語で説明する
photo-organize が写真をGeminiへ送り、次のような情報をJSONで受け取ります。
- 1〜2文の日本語キャプション
- 家族、子ども、風景、食べ物、イベント、書類などのカテゴリ
- 物、場所、行動、季節、時間帯などの検索用タグ
- 写っている人数
- 屋外かどうか
- 分類結果の信頼度
画像理解には、現在のコード上で gemini-2.5-flash-lite を使っています。
人物については、顔や雰囲気だけで名前を断定しないルールにしています。 確認済みの情報がない限り、個人名を勝手に付けません。
2. 写真と検索語を「意味の近さ」で比べる
日本語キャプションは、gemini-embedding-2 で768次元のベクトルへ変換し、SQLiteの sqlite-vec に保存します。
ブラウザで「焚き火」と入力したときも、検索語を同じ方法でベクトル化します。 そのベクトルと保存済みの写真ベクトルを比較し、意味の近い順に結果を返します。
このため、ファイル名に「焚き火」と書かれていなくても、AIの説明に「キャンプ」「火を囲む」「夜」といった場面が含まれていれば、関連する写真を見つけやすくなります。
Photo Browserでできること

この画像は検索前の初期状態なので、結果欄はまだ空です。 検索を実行すると、AIキャプション、タグ、類似度が付いた写真カードが並びます。

photo-browser は、Next.jsで作ったローカルWebアプリです。 トップ画面の中心に検索欄を置き、「写真を、言葉で探す。」ことを最短で始められる形にしました。
現在は次の機能が動いています。
- 「焚き火」「誕生日ケーキ」「雨の日」のような自然文での意味検索
- 年・月ごとに写真をたどるタイムライン
- あとで見返したい写真のお気に入り保存
- 好きな名前でまとめられるアルバム
- 撮影日時、カテゴリ、AIキャプション、タグを確認できる写真詳細
- 選んだ写真とベクトルが近い「類似する写真」の表示
- 小・中サイズのWebPサムネイルと、必要なときだけ開く原寸画像
検索用のメインDBは基本的に写真情報を読むために使い、お気に入りとアルバムは別の photo_ui.db に保存します。 日常の操作で、AI解析済みの検索DBを不用意に書き換えないためです。
2026年7月10日の動作確認では、トップ、検索、タイムライン、お気に入り、アルバムの各画面が正常に表示されました。 「焚き火」も5件取得する条件で正常に検索でき、検索API内部の計測は約2.8秒でした。
現在のデータ規模
現在Photo Browserが読んでいるDBには、15,461件の写真・動画情報が登録されています。 そのうち、日本語キャプションと検索ベクトルがそろい、意味検索に使えるものは14,337件です。
この約1.4万件のAI処理には、実測でおよそ32時間かかりました。 ブラウザ用の2サイズのサムネイル生成は、14,337件を8並列で処理し、約47分、約1.94GBでした。
さらに、別の大規模NAS索引では約19.7万件を、写真をコピーせず原本パスのままDBへ登録できています。 ただし、こちらはまだ全件のAI解析を終えていません。
「約19.7万件すべてをAI検索できる」という状態ではなく、まず壊さず索引を作り、小さなサンプルで品質と時間を確認しながら範囲を広げている段階です。
この分け方からも、写真を登録する処理と、AIで内容を理解する処理は別物だと分かりました。 ファイル一覧を作るだけなら比較的軽くても、一枚ずつ画像を読み解く工程は、時間とAPI利用量を考えて進める必要があります。
ローカルで動く部分と、外部AIを使う部分
このシステムはローカル中心ですが、完全オフラインではありません。
ローカルで動くものは、次の部分です。
- NAS上の写真ファイル
- SQLiteの写真DBとUI用DB
sqlite-vecによるベクトル検索- サムネイル生成と配信
- Next.jsの画面とAPI
- タイムライン、お気に入り、アルバム
外部APIを使うのは、写真の内容をGeminiで説明するとき、生成した日本語キャプションをベクトル化するとき、検索語をベクトル化するときです。 そのため、登録時には画像と生成済みキャプションを、検索時には入力した検索語をGemini APIへ送ります。
API利用量が発生するのもこの部分です。 Next.js、SQLite、サムネイル表示そのものに、AI APIの料金がかかるわけではありません。
現在はローカルLLMを検索処理に使っていません。 将来完全ローカルへ寄せるなら、文章だけを扱うLLMではなく、写真を理解できるローカルVLMと、ローカルの埋め込みモデルが必要になります。
技術構成
photo-organize 側はPythonで作っています。 Exifやファイル情報の取得、SQLiteへの登録、Gemini APIの呼び出し、PillowによるWebPサムネイル生成を担当します。
検索データはSQLiteへまとめ、ベクトル部分には sqlite-vec を使いました。 大きな検索サーバーを別に用意せず、ひとつのDBファイルを中心に構成できるのが、この規模の個人用ツールと相性のよいところです。
photo-browser 側は、Next.js 15、React 19、Node.js 24を使用しています。 Node標準の node:sqlite からDBを読み、App RouterのAPIで検索や画像配信を行います。
作ってみて分かったこと
このプロジェクトで一番大きかったのは、写真整理を「ファイルをきれいに並べ替えること」だけで考えなくなったことです。
元写真はNASに残したままでも、検索用の説明、タグ、日時、ベクトル、サムネイルを別に持てば、使い勝手は大きく変えられます。 そして、AIを毎回呼ぶのではなく、重い理解処理を先に済ませ、日常の閲覧は軽いローカルアプリに任せる構成が扱いやすいと感じました。
今後は、大規模NAS索引のAI解析を安全に再開できる仕組み、異常な撮影日時の補正、検索結果の評価、ローカルVLMや埋め込みモデルへの置き換えも試していきたいです。
写真が増えても、「どこへ保存したか」ではなく「どんな思い出だったか」で見つけられる。 Photo Browserを、そんな自分用の写真アーカイブへ育てていきます。

