0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

IndexNow APIをPythonで実装する — Bing/Yandexへの即時クロール通知の仕組み

0
Posted at

3行まとめ

  • IndexNowは、ページを更新・追加したタイミングでBing・Yandexなど対応検索エンジンに「このURLが変わった」と即時通知できるAPI。Googleは非対応なので、Google向けはSearch Consoleのサイトマップ・自然クロールに任せる棲み分けが必要になる
  • 認証はAPIキーをサイト直下に<key>.txtとして公開配置するだけ。OAuthもシークレット鍵の管理も不要で、GitHub Secretsに何も登録せずに使える
  • 公的オープンデータを使った住所ベースの災害リスクDB型サイトで、mainマージ時に公開済み全URLを自動送信するGitHub Actionsを組んだ。1リクエストの上限は10,000 URLなので、大量ページ運用ではバッチ分割と送信間隔の調整が必要になる

数万ページ規模のデータベース型サイトを個人開発で運営していると、月次バッチでスコアを再計算するたびに大量のページが更新される。検索エンジンの自然クロールを待つだけだと反映が遅いので、更新のたびに能動的に知らせる仕組みとしてIndexNowを実装した。

IndexNowとは — 検索エンジンへの「プッシュ通知」

IndexNowは、Microsoft(Bing)とYandexが中心になって策定したプロトコルで、サイト側からAPIを叩いてURLの更新を即座に通知できる。仕組みはシンプルで、host・key・keyLocation・urlListをJSONでPOSTするだけ。

_INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow"

payload = {
    "host": SITE_HOST,
    "key": INDEXNOW_KEY,
    "keyLocation": KEY_LOCATION,
    "urlList": urls,
}
resp = client.post(
    _INDEXNOW_ENDPOINT,
    json=payload,
    headers={"Content-Type": "application/json; charset=utf-8"},
    timeout=30,
)

対応済みのAPIエンドポイント(api.indexnow.org)に送っておけば、参加している各検索エンジン(Bing・Yandexなど)に通知が伝播する仕組みになっている。注意点はGoogleがIndexNowに対応していないこと。Google向けの即時反映手段はIndexNowでは代替できず、Search Consoleへのサイトマップ登録と自然クロールに任せるしかない。検索エンジンごとに反映経路が分かれることを前提に設計する必要がある。

認証はキーファイルを置くだけ

IndexNowの認証は驚くほど単純で、ランダムなキー文字列をサイトのルート直下に<key>.txtとして公開配置し、リクエスト時にそのキーと配置場所(keyLocation)を一緒に送るだけ。サーバー側は「そのキーのファイルが本当にそのドメイン配下に置かれているか」を見て本人確認する。

_INDEXNOW_KEY = "生成した32文字のランダムな16進文字列"
_SITE_BASE = "https://example.com"
_KEY_LOCATION = f"{_SITE_BASE}/{_INDEXNOW_KEY}.txt"

このキー自体は「公開されている前提」の設計なので、GitHub SecretsやAPIキー管理の対象にする必要がない。site/public/<key>.txtにキー文字列そのものを置いておくだけで済み、OAuthのような認可フローも不要。SEO系のAPIとしては珍しいくらい実装コストが低い。

URLリストの組み立て方

送信するURLリストは、DBの公開フラグから動的に組み立てる。

def build_urls(db) -> list[str]:
    """公開済みエリアのURLリストを生成する。"""
    rows = db.execute(
        """
        SELECT DISTINCT a.pref_slug, a.city_slug, a.area_slug
        FROM areas a
        WHERE (a.is_published = 1 OR a.dynamic_published = 1)
          AND a.pref_slug IS NOT NULL AND a.city_slug IS NOT NULL AND a.area_slug IS NOT NULL
        ORDER BY a.pref_slug, a.city_slug, a.area_slug
        """
    ).fetchall()

    urls: list[str] = []
    seen_prefs: set[str] = set()
    seen_cities: set[str] = set()
    for pref_slug, city_slug, area_slug in rows:
        if pref_slug not in seen_prefs:
            urls.append(f"{_SITE_BASE}/{pref_slug}/")
            seen_prefs.add(pref_slug)
        city_key = f"{pref_slug}/{city_slug}"
        if city_key not in seen_cities:
            urls.append(f"{_SITE_BASE}/{pref_slug}/{city_slug}/")
            seen_cities.add(city_key)
        urls.append(f"{_SITE_BASE}/{pref_slug}/{city_slug}/{area_slug}/")
    return urls

(実装では都道府県コードでの絞り込みオプションや、政令指定都市の4階層パスにも対応しているが、ここでは基本構造だけ抜き出している)

都道府県トップ・市区町村ページ・町丁目ページの3階層それぞれについて、重複をsetで除きながらURLリストを組み立てる。単に「全URLを毎回総送信」ではなく、is_published = 1(静的公開)とdynamic_published = 1(R2動的配信)の両方を対象にしているのがポイントで、配信経路が違っても検索エンジンへの通知は一本化している。

バッチ送信とレート

IndexNowは1リクエストあたり最大10,000 URLという上限があるため、それを超える場合はバッチ分割が必要になる。

_BATCH_SIZE = 10_000  # IndexNow の上限

batches = [urls[i : i + _BATCH_SIZE] for i in range(0, len(urls), _BATCH_SIZE)]

with httpx.Client() as client:
    for i, batch in enumerate(batches, 1):
        submit_batch(client, batch, i, len(batches))
        if i < len(batches):
            time.sleep(3)

レスポンスは200または202が成功。バッチ間に3秒のスリープを入れているのは、短時間に大量リクエストを送って弾かれることを避けるための保険。--dry-runオプションで送信せずに件数だけ確認できるようにもしてあり、数万件規模のURLリストを本番投入する前に必ず確認できる作りにしている。

GitHub Actionsでの自動化

mainブランチへのマージをトリガーに、ページ内容に関わる変更があったときだけ自動送信する。

on:
  push:
    branches: [main]
    paths:
      - 'etl/data/data.db'
      - 'site/src/**'
  workflow_dispatch: {}

concurrency:
  group: indexnow-notify
  cancel-in-progress: false  # 送信途中で打ち切らない(部分送信を避ける)

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true   # URLリスト生成にDBの実体データを使うため
      - uses: astral-sh/setup-uv@v5
      - run: uv run python -m scripts.notify_indexnow
        working-directory: etl

concurrencyで同時実行を1本に制限し、cancel-in-progress: falseにしているのもポイント。送信途中で別のpushが割り込んでキャンセルされると、URLリストが一部だけ通知された中途半端な状態になりかねないので、実行中のジョブは最後まで完走させる設定にしている。IndexNow自体が非同期通知(送ってから実際のクロールまでタイムラグがある)という前提もあり、Cloudflareのビルド完了と厳密に同期させる必要もない。

まとめ

  • IndexNowはキーファイルを公開配置するだけで使える、実装コストの低い即時クロール通知API。ただしGoogleは非対応なので、検索エンジンごとに反映経路が分かれることを前提に設計する
  • URLリストはDBの公開フラグから動的に組み立て、10,000件上限に合わせてバッチ分割・送信間隔を調整する
  • GitHub Actionsで「ページ内容が変わるpushのときだけ」自動送信することで、月次バッチのスコア更新のような大量差分にも人手を介さず追従できる

この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?