すくらっぷ あんど びるどー(したい)

AI・Obsidian・個人開発の試行錯誤ログ。 作業メモを、あとで使える知識に変えるための記録。

Obsidianで500件超の大量メモを自動索引化する方法|PythonでMOC風の入口ページを作る

はじめに

Obsidianにメモが増えてくると、最初に困るのは「どこに書いたか思い出せない」ことである。

検索、タグ、プロパティは便利である。しかし、メモを毎回きれいに分類できるとは限らない。特に500件を超えると、「何という名前で書いたか」「どのフォルダに置いたか」を思い出せない場面が増える。

そこで、メモ本文は動かさず、Obsidian上で開ける索引ページだけを自動生成することにした。

この記事は要約版である。全体像、考え方、実行方法、注意点を短くまとめる。スクリプト全文と細かい設計意図は、別記事「Obsidianメモ索引自動生成スクリプト詳細版。コメント付き全文と設計意図」に分けた。

完成イメージ

作るものは、メモ本文を移動しない索引ページである。

Before:
メモが増えすぎて、どこに何があるか分かりにくい
タグやプロパティを毎回付ける運用は続きにくい
チャット欄でAIに聞いても、候補を安定して出せない

After:
メモ索引.md を開けば、メモ全体の入口が見える
最近更新、フォルダ別、五十音グループから辿れる
メモ本文は動かさず、リンク一覧だけを更新できる

生成後の入口は次のようになる。

00_人間用/メモ/
├─ README.md
└─ 索引/
   ├─ メモ索引.md
   ├─ 最近更新.md
   ├─ フォルダ別.md
   ├─ 英数記号.md
   ├─ あ行.md
   ├─ か行.md
   ├─ さ行.md
   ├─ た行.md
   ├─ な行.md
   ├─ は行.md
   ├─ ま行.md
   ├─ や行.md
   ├─ ら行.md
   ├─ わ行.md
   └─ その他.md

今回の環境では、534件のメモから索引を生成した。

前提環境

この記事を更新した時点での確認環境は次の通りである。

項目 内容
確認日 2026-06-23
OS Microsoft Windows 11 Home 10.0.26200
シェル PowerShell 7.5.5
AIツール Codex
ローカルLLM 今回の記事化・検証では未使用
関連ツール Obsidian 1.9.12.0、Python 3.10.11、blogsync 0.20.1
Node.js / npm PATH上では未検出
対象サービス Obsidian Vault、はてなブログ
実行場所 Obsidian、Codex、PowerShell、<BLOG_REPO>

作成動機はローカルAIで検索しようとして失敗したからこれを作成した。

最初はチャット欄でAIに「英語のメモを探して」のように頼む案を考えたのだが、結構遅いみたいなのがあった。あと正直ターミナルを毎回使用して検索するのが面倒だったので、できればチャット欄でやりたかった。 つまり、AIに探させる前に、人間にもAIにも見える入口が必要だった。 その入口として作ったのが、メモ索引である。

全体構成

処理の流れは単純である。

00_人間用/メモ/ にある Markdown を集める
↓
各ファイルのタイトル、フォルダ、更新日を読む
↓
最近更新、フォルダ別、先頭文字グループに分ける
↓
00_人間用/メモ/索引/ にリンク一覧を書く
↓
README.md から索引へ辿れるようにする

重要なのは、メモ本文を移動しないことである。索引はあくまで入口であり、既存メモの置き場や書き方を壊さない。

要約版のコード

実際のスクリプト全文は詳細版に載せる。ここでは、何をしているかが分かる最小の骨格だけ示す。

この要約版も、そのままコピーして動かせる形にしている。タイトル抽出では return line[2:].strip() で見出し文字列を返す。ここが line[2:].strip() だけになると、タイトルを返さないバグになる。

from pathlib import Path

VAULT_ROOT = Path(__file__).resolve().parents[2]
MEMO_ROOT = VAULT_ROOT / "00_人間用" / "メモ"
INDEX_ROOT = MEMO_ROOT / "索引"


def extract_title(path: Path) -> str:
    text = path.read_text(encoding="utf-8", errors="ignore")
    for line in text.splitlines():
        if line.startswith("# "):
            # "# " を取り除いた見出し文字列をタイトルとして返す。
            return line[2:].strip()
    return path.stem


def make_link(path: Path, title: str, table_cell: bool = False) -> str:
    rel = path.relative_to(VAULT_ROOT).as_posix()
    if rel.endswith(".md"):
        rel = rel[:-3]
    # Markdown表の中では、Wikiリンク別名の | を \| にしないと列が崩れる。
    separator = r"\|" if table_cell else "|"
    return f"[[{rel}{separator}{title}]]"


def main() -> None:
    entries = []
    for path in MEMO_ROOT.rglob("*.md"):
        if INDEX_ROOT in path.parents:
            continue
        entries.append((extract_title(path), path))

    INDEX_ROOT.mkdir(parents=True, exist_ok=True)
    lines = ["# メモ索引", ""]
    for title, path in sorted(entries):
        lines.append(f"- {make_link(path, title)}")

    (INDEX_ROOT / "メモ索引.md").write_text("\n".join(lines), encoding="utf-8")


if __name__ == "__main__":
    main()

この最小版では箇条書きにリンクを出すため、make_link(path, title) のままでよい。

Markdown表のセルにリンクを出す場合だけ、make_link(path, title, table_cell=True) を使う。これにより、[[path|label]] ではなく [[path\|label]] を出せる。

この骨格に、実際には次の処理を足している。

  • 最近更新50件のページを作る
  • フォルダ別ページを作る
  • 英数記号、あ行、か行のように先頭文字で分ける
  • 自動生成した索引ページを次回の索引対象から外す
  • 秘密系フォルダを索引対象から外す
  • Markdown表の中ではWikiリンクの |\| にする

手順

まず、Vault内に次の構成を用意する。

<VAULT_PATH>/
├─ 00_人間用/
│  └─ メモ/
│     └─ ここに索引化したいMarkdownを置く
└─ 90_System/
   └─ scripts/
      └─ build_memo_index.py

次に、Vaultルートでスクリプトを実行する。

cd <VAULT_PATH>
py -3 -X utf8 90_System\scripts\build_memo_index.py

このVaultでは、既存の実行コマンドに組み込んだため、次でも更新できる。

.\90_System\scripts\run_ai_memory.cmd memo_index

実行後は、00_人間用/メモ/README.md または 00_人間用/メモ/索引/メモ索引.md を開く。

実行結果

実行すると、次のようなログになる。

Command: memo_index
Memo entries: 534
Output: 00_人間用/メモ/索引

また、公開前に実スクリプトの主要関数を一時ディレクトリ上で単体テストした。

PASS 7 tests:
extract_title
classify_group
make_link
should_skip_path
iter_memos
write_hub_table_links
write_readme_table_links

メモ索引.md の入口には、次のような表ができる。

| 見たいもの | 開くページ | 件数 |
| --- | --- | ---: |
| 最近更新 | [[00_人間用/メモ/索引/最近更新\|最近更新]] | 50 |
| フォルダ別 | [[00_人間用/メモ/索引/フォルダ別\|フォルダ別]] | 534 |

Obsidianの表内では、Wikiリンクの別名区切り | がMarkdown表の列区切りとして解釈される。そのため、表の中では \| にしている。

詰まったところ

一番分かりにくかったのは、ObsidianのWikiリンクとMarkdown表の相性である。

詰まったこと 原因 対処
表の列が崩れる Wikiリンクの別名区切り([[path|label]])が表の列区切り扱いになる 表の中だけ [[path&#124;label]] にする
チャット欄から探しにくい AIに渡す対象一覧がない 先にメモ索引を作る
タグ運用が重い 毎回タグを付ける必要がある ファイル名と見出しから索引を作る

セキュリティ上の注意

索引は本文全文を出さないが、ファイル名、見出し、フォルダ名は出る。

そのため、次の情報は索引対象に含めない方がよい。

  • 実PCのフルパス
  • ユーザー名
  • APIキー
  • OAuthトークン
  • パスワード
  • 非公開プロジェクト名
  • .obsidian/plugins/*/data.json
  • blogsync.yaml

実装では、AI立入禁止privatesecret などのフォルダ名を除外できるようにした。

使ったプロンプト

今回の作業では、次のような意図でAIに指示した。

Obsidianで人間がメモを探しにくくなっている。
タグやプロパティを重く運用するのではなく、
メモの入口になる索引ページを自動生成したい。
人間が開けるページとして、最近更新、フォルダ別、五十音別に分けたい。
実パスや認証情報は記事に出さない。

この指示を受けて、表セル内だけ |\| にするように直した。

Before / After

Before:
メモはあるが、探す入口がない
AIに聞いても、対象一覧がないため探しにくい
タグやプロパティの運用を増やすと重い

After:
メモ索引.md から全体を辿れる
最近更新やフォルダ別で探し始められる
AIに渡すための入口にもできる

詳細版で扱うこと

コードが長いので二つに分けた。詳細版では

  • コメント付きPythonスクリプト全文
  • 除外フォルダの考え方
  • 五十音グループの分け方
  • 自動生成ページを再索引しないための処理
  • Obsidian表内リンクの \| 対応
  • run_ai_memory.cmd memo_index への組み込み

を扱う。てか長すぎる。二万近いのは自分でも読まない。

まとめ

今回の方法では、メモ本文を移動せず、タグやプロパティの運用も増やさず、リンク一覧だけを自動生成した。

タグなどは自分でつけるのは別にいいんだが、system側ですでに使用していたし、それをやると欲しくない情報を拾ってしまって使いにくいので、今回はこういう形になった。

system側で使用してるならそっちで拾わせた方が早いし確実だし、雑な情報はない方がいい。