こんにちは!開発本部でバックエンドエンジニアとしてインターンをしている岩瀬(@gantaso4704)です。
今回は、PR TIMESに投稿されたプレスリリースを IndexNowAPIを用いて、Bingの検索エンジンに即座にインデックスを行うバッチジョブを作成したので、その紹介をしたいと思います。
背景
2021年ごろに、Microsoft Bingは新しい検索エンジンのインデックスのための仕組み「IndexNow」を公開しました。
IndexNowを利用すると、コンテンツの追加、更新、削除情報をクロールなしで検索エンジンに即座に通知することができるそうです。
この新しい方法は、従来のクロール方式と比較してコンテンツのインデックスがより迅速に行えるとのことで、PR TIMESも導入を検討し、実際に取り入れることとなりました。
IndexNowの使い方
IndexNowを使って指定したURLを検索エンジンにインデックスさせる方法自体はとてもシンプルでした。次のような形のリクエストを送るだけです。
以下にリクエストのサンプルを示します。
POST /IndexNow HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: www.bing.com
{
//webサイトのホスト名
"host": "www.example.org",
//APIKEY
"key": {{API_KEY}},
//APIKEYの情報が記述されているテキストファイルまでのパス
"keyLocation": "https://www.example.org/{{API_KEY}}.txt",
//インデックスさせたいURL
"urlList": [
"https://www.example.org/url1",
"https://www.example.org/folder/url2",
"https://www.example.org/url3"
]
}
ここで私がハマった点を二点共有させていただこうと思います。
APIKEYの情報が書かれた{{API_KEY}}.txtファイルをホスティングする。
‘’host” に指定した、webサイトのホストのルートディレクトリに{{API_KEY}}.txtというファイル名でホスティングすることで、検索エンジン側から所有権を確認できるようにしておく必要があります。
ここをちゃんと設定しておかないと、403(Forbidden)のエラーが返されます。
また、一番最初にAPIをPOSTする際、keyLocationに指定したパスにAPIKEY.txtがホスティングされているか確認されるので、ここでちゃんと正しい場所にAPIKEYがホスティングしていないと、2回目以降たとえ正しくホスティングし直したとしても、それ以降は403を返すようになってしまうようなので注意が必要です。
これに気づかず、ローカル環境で色々と検証した使えないAPIKEYを用いて、本番環境にリリースしてしまったせいで、NewRelicに大量のErrorLogを出してしまいました。
ですので、IndexNowを導入しようとする際は、コードを書き始めるより前に、APIKEYをホスティングしておくことをお勧めします。
’host’に指定した値と’’keyLocation’’,”urlList”で指定するURLのホスト名を一致させる。
先ほど説明したように、{API_KEY}}.txtがホスティングされているホストでしか、URLをインデックスさせることはできません。こちらも正しく設定していないと、403が返されます。
実装方法
IndexNowを用いたシステムを作るにあたって
- 今回の仕組みのためだけに、コストをかけて新しい仕組みを導入したくない。
- リアルタイム性はそこまで重要ではなく、数分程度なら遅れても大丈夫
という前提がありました。
前提1をクリアするために、以前ElasticSearchからOpenSearchに移行した際に作られたテーブルを流用することにしました(OpenSearchへの移行についてまとめられた記事はこちら)

OpenSearchの処理で使われているtableは二種類あり、Log Tableにはコンテンツの追加、更新、削除があったプレスリリースのcompany_id、release_id等がid順に格納されており(PR TIMESでは、company_idとrelease_idによって記事が一意に定められています)Log meter tableには、Log TableのどのIdまでをOpenSearchにインデックスしてあるかが記録されています。
そして、OpenSearchへインデックスさせる処理はこのようになっていました、
- Log meter tableからProcessed Idを取得
- Log tableからProcessed Idより大きいidを持つデータを取得
- それらのデータをOpenSearchにインデックスさせる
- インデックスしたデータの内、idが一番大きいものをLog meter tableへ更新する
これらの処理を1分ごとのバッチジョブで回しています。
今回は、Log meter tableにどこまでIndexNowの処理を行ったか記録するデータ(index_now_release_log)を追加し、OpenSearchでの処理をほとんど流用することで、低コストでIndexNowの処理を書くことができました。
- Log meter tableからProcessed Idを取得
- Log tableからProcessed Idより大きいidを持つデータを取得
- それらのデータをIndexNowを用いてインデックスさせる
- インデックスしたデータの内、idが一番大きいものをLog meter tableへ更新する
変わったのは3番だけで、IndexNow用のAPIClientを作って処理を少しいじるだけで実装自体は完了しました。
そして、前提2に書いたように、リアルタイム性はそれほど重要ではないので、こちらもOpenSearch同様1分毎のcronでバッチ実行にすることで、障害も起こりにくい構成にしました。cron設定は初めて書いたのでとても勉強になりました。
テスト
私はこのインターンがバックエンド初挑戦なので、テスト自体書いたことなかったのですが、今回のタスクで初めてテストに挑戦しました。
テストではHttpClientはRequestを投げず、下に示すようにMockするということさえ知らなかったのですが、色々なテストコードを眺めたり、実際に自分でテストコードを書くうちに、DIの概念や、テストを意識したアーキテクチャの重要性を初めて実感できました。
// MockしたResponseをClientにattachする。
$mock = new Mock([
new Response(200, [])
]);
$client = new Client();
$client->getEmitter()->attach($mock);
$client = new GuzzleClientPsr7Decorator($client);
// IndexNowクラスにClientを引き渡し、API実行
$index_now = new IndexNow($logger, $pdo, $client);
$response = $index_now->callIndexNow($executed_release_list);
// responseをassert
$this->assertInstanceOf('GuzzleHttp\Psr7\Response', $response);
$this->assertTrue(in_array($response->getStatusCode(), [200, 202]));
テストは奥が深い、、
Conversationの嵐
先輩エンジニア三人にレビューをしていただいたというのもあり、とてつもない勢いでレビューコメントが溜まっていきました。
適切なExceptionでcatchして、エラーハンドリングをしなければいけないということだったり、
可読性や、テストしやすいコードを作るため、関数の粒度を細かくしたり、
普段個人開発をする上ではそれほど意識してこなかった、パフォーマンスのことだったり、その他諸々たくさんのありがたいレビューをいただきました。
ひたすらレビュー修正しては、さらにレビューが溜まっていきいつこのタスクは終了するのだろうか?、と思っていましたが、無事終われてよかったです。
おかげで僕の拙いPHP力も少しは上がったような気がしてます。
@dungbuike-prtsさん @meihei3さん @catatsuyさん 本当にありがとうございました。
インデックスは早くなったのか
Bingの検索エンジンがURLをインデックスしているかどうか確認する方法として、Bing Webマスターツールを使うor Bingのサイトにおいて、「url:<調べたいURL>」のように検索をかける、という二つの方法があるらしいのですが、今回は手っ取り早く確認したかったため、後者の方法を取りました。
この方法では、検索した結果がきちんと帰ってくるかどうかでインデックスされているか否かを判定します。


完全手作業で確認したので正確な値ではないのですが、IndexNowAPIが叩かれてから大体5〜15分程度でインデックスされていることが確認できました。ただ、IndexNow導入以前がどれくらい遅いか測定していなかったため、どれくらい早くなったか比較できない事がとても悔やまれます。
まとめ
これまでインターンとして色々な改善タスクをこなしてきたのですが、今回一機能を作るタスクに挑戦できてとても楽しかったです。正直実装自体はそこまで難しいものではありませんでしたが、初めてcron設定をしたり、テストを書いたり、たくさんレビュー修正したりと、とても学びのある開発ができたと思います。今後も開発インターンを続け、PR TIMESの発展に貢献していきたいと思います!