太陽がまぶしかったから

C'etait a cause du soleil.

Obsidian を BigQuery に同期してクラウド上の AI Agent から Gemini 推論付き SQL で脳のデジタルツインからコネクティング・ドッツを提案させる

データが BigQuery に入ってさえいれば良い時代

BigQuery のマネージド AI 関数として「AI.CLASSIFY」「AI.SCORE」「AI.IF」が実装された。プロンプトの最適化やモデルのエンドポイント及びパラメータのチューニングまで BigQuery 側が自動で行ってくれる。オブジェクトテーブルと組み合わせれば画像・音声・動画といったマルチモーダルデータも SQL で扱える。

BigQuery に Gemini が統合されて、SQL を書くだけでAIによる抽出、分類、スコアリングなどができる時代になっている。さらに BigQuery に入ったデータは MCP Server 経由で AI Agent から直接クエリ可能であり、ナレッジベースを BigQuery に載せておけば RAG の検索バックエンドとしてそのまま機能する。

いわゆる Vector Search に頼ったRAGついては未だに懐疑的なのだけども、フルマネージドで実行できる基盤があり、今後も機能拡張されていくことを考えると、BigQueryにさえ入れておけば高速なクラウドアクセスが可能であり、開発工数をかけずとも技術的な恩恵を得られる可能性が高くなっていく。そんなわけで、まずは自分のナレッジベースを BigQuery に載せて Gemini で横断検索したり生成させてみようと思った。

Claude Code から使うのであればローカルファイルだけでも十分だけども、クラウド上に AI Agent を常時起動しており、クラウド上の分離環境で自身のナレッジのみが詰め込まれた Obsidian を読めるようにすることが重要となる。Mac Miniは買わない。

GCS 外部テーブルで DML ゼロの同期パイプライン

具体的な仕組みとして、Obsidian は Git Plugin経由で GitHub に同期するようになっているため、GitHub上の Obsidian Vault のファイルを GitHub → GCS → BigQuery へ差分同期するパイプラインを作っている。

ここで大事なのは、BigQuery にいちいち INSERT や UPSERT をしていないということだ。GCS 上の個別 JSON ファイルを更新するだけで、BigQuery はそれを外部テーブルとして参照する。DML はゼロ、書き込みコストもゼロ。GCS がデータの Single Source of Truth であり、BigQuery はそこに窓を開けているだけだ。全体の流れはこうなる。

sequenceDiagram
    participant Scheduler as Cloud Scheduler
    participant Fn as Cloud Run Functions
    participant BQ as BigQuery sync_state
    participant GH as GitHub API
    participant GCS as GCS

    Scheduler->>Fn: HTTP POST (OIDC認証)
    Fn->>BQ: 前回 commit hash 取得
    BQ-->>Fn: last_commit
    Fn->>GH: Compare API (last_commit...HEAD)
    GH-->>Fn: 差分ファイル一覧 (A/M/D)
    loop 変更ファイルごと
        Fn->>GH: Contents API (base64)
        GH-->>Fn: ファイル内容
        Fn->>Fn: Frontmatter / 本文分離
        Fn->>GCS: 個別JSON upsert
    end
    opt 削除ファイルあり
        Fn->>GCS: blob 削除
    end
    Fn->>BQ: sync_state 更新 (commit hash)
    Note over GCS,BQ: BigQuery は GCS 外部テーブルとして自動的に最新データを参照(DML不要)

Cloud Scheduler が6時間おきに Cloud Run Functions を OIDC 認証付きの HTTP POST で叩き、Cloud Run Functions は BigQuery の sync_state テーブルから前回成功時のコミットハッシュを取得する。GitHub の Compare API でそのハッシュと HEAD の差分を取り、変更された .md ファイルだけを Contents API で取得してフロントマターと本文を分離し、個別の JSON として GCS に書き込む。BigQuery の documents テーブルは GCS の JSON を外部テーブルとして参照しているだけなので、GCS を更新した時点でクエリ結果に反映される。

差分同期と初回アップロードのすみわけ

注意点がひとつある。初回に GitHub API 経由で Vault の全ファイルを取得しようとすると、Contents API をファイル数だけ叩くことになりレート制限に引っかかる。そこで初回だけはローカルの Vault ディレクトリから直接 GCS に一括アップロードし、その時点の GitHub 最新コミットハッシュを sync_state に記録する構成にした。以降は Compare API で変更ファイルだけを拾う運用になる。

同期方式 用途 データ取得元 API 負荷
ローカル一括アップロード 初回投入 ローカル Vault なし(直接読み込み)
GitHub 差分同期 定期実行(6h) GitHub Compare API 差分ファイル数のみ

データベースのリカバリ戦略と同じ構造だ。フルバックアップをローカルから取り、以降はトランザクションログで差分取得する。ローカルアップロードにはドライランモードもあり、対象ファイルを事前に確認してからバッチサイズ単位で GCS に投入できる。

Frontmatter 分離とディレクトリ構造の保持

GCS に書き出す JSON のデータモデルにも工夫がある。Markdown をそのまま放り込むのではなく、python-frontmatter でフロントマターと本文を分離し、タイトル・タグ・カテゴリを構造化した上で、フロントマター全体も JSON 文字列として保持している。

{
  "file_path": "blog/published/example.md",
  "dir_path": "blog/published",
  "file_name": "example",
  "category": "blog",
  "title": "記事タイトル",
  "tags": ["AI", "プログラミング"],
  "content": "本文テキスト...",
  "frontmatter": "{\"date\": \"2026-02-28\", \"status\": \"published\"}"
}

タイトルの抽出にはフォールバック戦略がある。フロントマターの title → 本文中の H1 見出し → ファイル名の順に優先度を設けている。Obsidian のノートはフロントマターがないものも多いため、この優先順位がないと BigQuery 側でタイトルなしのレコードだらけになる。

dir_path と category を分離して持っているのは BigQuery 側で WHERE dir_path = 'blog/published'WHERE category = 'blog' のようなフィルタを効率よく書くためだ。tags は REPEATED フィールドにしているため UNNEST(tags) でタグ別集計もできる。フロントマターは JSON 文字列として保持しているため JSON_VALUE(frontmatter, '$.status') で任意のフィールドにアクセスでき、スキーマの事前定義が不要になる。

素直に BigQuery のネイティブテーブルに MERGE 文で同期しようとしたのだけど、まずはファイル参照のみにした。それこそベクトル埋め込みなどが必要になった際に差分更新などを含めて本格的に考慮することになるだろう。

BigQuery ML で Gemini に SQL から要約させる

外部テーブルにデータが載ってしまえば、あとは BigQuery ML の出番だ。リモートモデルとして Gemini を登録し、SQL から直接呼び出せる。

CREATE OR REPLACE MODEL `obsidian_vault.gemini`
REMOTE WITH CONNECTION `us.gemini_conn`
OPTIONS (endpoint = 'gemini-3-flash-preview');

例えば全文検索でKindleのハイライトから書籍を絞り込み、Gemini に箇条書きで抽出させるクエリがこうなる。

DECLARE target_keyword STRING DEFAULT 'エイリアン・インテリジェンス';

WITH search_results AS (
  SELECT title, content
  FROM `obsidian_vault.documents`
  WHERE category = 'kindle_highlight' 
    AND content LIKE FORMAT('%%%s%%', target_keyword)
),
prompts AS (
  SELECT
    title, 
    FORMAT("""
      以下のドキュメントから「%s」に関連する重要な記述を箇条書きで抜き出してください。
      
      # 制約事項:
      - Markdown形式の箇条書きのみを出力してください。
      - 「はい、抽出しました」などの挨拶や導入文、補足説明は一切不要です。
      
      # 書籍タイトル
      %s

      # ドキュメント:
      %s
    """, target_keyword, title, content) AS prompt
  FROM search_results
)

SELECT
  title,
  ml_generate_text_llm_result as result
FROM ML.GENERATE_TEXT(
  MODEL `obsidian_vault.gemini`,
  TABLE prompts,
  STRUCT(2000 AS max_output_tokens, TRUE AS flatten_json_output)
);

SQL を書くだけで自身のナレッジベースを横断検索し、Gemini に意味的な抽出をさせられる。あとはAPIを実行するスクリプトについて Skill 化でもしておくだけだ。SEARCH関数をうまく使えないかと思ったが、日本語かつファイル検索だと期待通りになりにくいので最初の絞り込みは単純な全文検索とした。

今回は Kindle のハイライトを対象にしているが、なかなか良い感じに文脈を踏まえた引用がなされているのではないだろうか。全文検索が前提でも出会いを広げるためには検索語を拡張するクエリファンアウトを実装したり、逆にドキュメント側に類語を入れるなどで運用上は十分だと考えている。対象行をハードに絞り込まないで Geimini を起動する富豪的 AI Ops は貧民に厳しすぎる。

Google Cloud の公式ブログでは AI.GENERATE や AI.EMBED の一般提供開始と Gemini 3.0 対応が発表されており、エンベディング生成やベクトル類似度検索も SQL で完結する方向に進んでおり、さらなる高度化についても確認して検討していきたいところだ。

Obsidian を BigQuery に入れて AI Agent と知識が同期する

ここまでの全体像を整理すると、このようなパイプラインになっている。

レイヤー ツール 役割
入力(dev環境) X(鍵アカウント) 脊髄反射の断片、引用RT
入力(stg環境) はてなブログ 構成・編集を経た論考
入力(doc環境) kindle_highlight 書籍による外部知識。ただし僕が読んでいる
ローカル統合 Obsidian Zettelkasten、バックリンク
バージョン管理 GitHub Git同期、差分検知の起点
データレイク GCS(外部テーブル) Single Source of Truth
AI Agent BigQuery + Gemini SQL検索、任意の抽出

X のツイートもブログ記事もKindleハイライトも、Obsidian という共通のフォーマットに正規化され、GitHub を経由して GCS に流れ、BigQuery の外部テーブルとして Gemini から参照可能になる。どの段階でもファイルはただの Markdown か JSON であり、ロックインされるプロプライエタリなフォーマットは存在しない。

このようにすることで自分の脳のデジタルツインをクラウド上の AI Agent から参照することができ、それを元に様々な文章やプログラムを自律的に生成して教えてくれる。基本的には自分で読んだり書いたりした概念を組み合わせているため、全くわからない話が出てこないことが面白いし、有用になると感じている。

全く新しい概念は X や note などから取得すれば良くて、ナレッジAI Agent に期待しているのは既存知識のコネクティング・ドッツである。これ自体が自己満足のガーデニングのようなものだけど、自己満足するのは人間の仕事である。