Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

単一の Web ページを取得してインデックスする

最終更新 Markdown で表示Agent セットアップ

このガイドでは、Browser Run/content エンドポイント で単一 Web ページのレンダリング済み HTML を取得し、Items APIAI Search インスタンスの 組み込みストレージ にアップロードする Worker を作ります。AI Search は、ほかのアップロード済みドキュメントと同じようにページをインデックスし、検索可能にします。Worker はインデックス済みページを照会する /search エンドポイントも公開します。1 つのサービスでインデックスと検索の両方を行います。

このパターンを使うとき

1 ページ、または少数の手作業で選んだページをオンデマンドでインデックスする場合に、このパターンを使います。サイト全体をクロールして継続的にインデックスするには、代わりに AI Search の ウェブサイトデータソース を使います。

Browser Run と AI Search インスタンスはどちらもバインディング経由で到達するため、間に公開エンドポイントを置かずに、1 つの Worker でページの取得とインデックスができます。

前提条件

  1. Cloudflare アカウント に登録します。
  2. Node.js をインストールします。

Node.js のバージョンマネージャー

権限の問題を避け、Node.js のバージョンを切り替えられるよう、Voltanvm などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。

アップロード先の AI Search インスタンスも必要です。作成方法は はじめに を参照してください。このガイドはインスタンスの組み込みストレージにアップロードするため、外部データソースは不要です。

1. Worker プロジェクトを作成する

create-cloudflare CLI(C3)で新しい Worker プロジェクトを作成します。C3 は、Cloudflare 向けの新しいアプリケーションのセットアップとデプロイを支援するコマンドラインツールです。

次を実行し、fetch-and-index という名前の新しいプロジェクトを作成します。

npm create cloudflare@latest -- fetch-and-index

セットアップでは、次のオプションを選びます。

  • What would you like to start with? では、Hello World example を選びます。
  • Which template would you like to use? では、Worker only を選びます。
  • Which language do you want to use? では、TypeScript を選びます。
  • Do you want to use git for version control? では、Yes を選びます。
  • Do you want to deploy your application? では、No を選びます(デプロイ前にいくつか変更します)。

アプリケーションディレクトリに移動します。

cd fetch-and-index

2. Wrangler を設定する

Wrangler 設定ファイル に 2 つのバインディングを追加します。Browser Run 用の browser バインディング と、アップロード用の AI Search 名前空間バインディング です。/content エンドポイントは browser バインディング経由で実行されるため、Puppeteer などのパッケージをインストールする必要はありません。

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "fetch-and-index",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-09-20",
  "browser": {
    "binding": "BROWSER",
    "remote": true
  },
  "ai_search_namespaces": [
    {
      "binding": "AI_SEARCH",
      "namespace": "default",
      "remote": true
    }
  ]
}
name = "fetch-and-index"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"

[browser]
binding = "BROWSER"
remote = true

[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = true

browser バインディングの quickAction メソッドには、互換性日付 2026-03-24 以降が必要です。リモートモードなしのローカル開発には対応していません。browser バインディングで remote = true を設定すると、wrangler dev のリモートモードが有効になります。AI Search バインディングの remote オプションは、デプロイ済みインスタンスへのアップロードをプロキシします。AI Search はローカルでは動作しないためです。

3. Worker のコードを追加する

src/index.ts を更新します。この Worker には 2 つのルートがあります。?url= パラメーター付きのリクエストはそのページのレンダリング済み HTML を取得してインデックスし、/search?q= へのリクエストはインデックス済みコンテンツを照会します。my-instance をインスタンス名に置き換えます。

src/index.jsjs
// The instance that indexes the fetched page.
const INSTANCE_NAME = "my-instance";

// Build a stable item key that ends in .html, so AI Search converts the HTML
// to Markdown before indexing it.
function itemKey(pageUrl) {
	const slug = `${pageUrl.hostname}${pageUrl.pathname}`
		.replace(/[^a-zA-Z0-9]+/g, "-")
		.replace(/^-+|-+$/g, "");
	return `${slug || "index"}.html`;
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Search route: query the indexed content and return the matching chunks.
		if (url.pathname === "/search") {
			const query = url.searchParams.get("q");
			if (!query) {
				return new Response("Add a ?q= query parameter", { status: 400 });
			}
			const results = await env.AI_SEARCH.get(INSTANCE_NAME).search({ query });
			return Response.json({
				query: results.search_query,
				results: results.chunks.map((chunk) => ({
					key: chunk.item.key,
					score: chunk.score,
					text: chunk.text,
				})),
			});
		}

		// Index route: fetch a URL's rendered HTML and index it.
		const target = url.searchParams.get("url");
		if (!target) {
			return new Response(
				"Add a ?url= parameter to index a page, or use /search?q= to search",
				{ status: 400 },
			);
		}

		const pageUrl = new URL(target);

		// Fetch the fully rendered HTML with the Browser Run /content endpoint.
		// networkidle2 waits until the page has no more than two network
		// connections for at least 500 ms, giving client-side JavaScript time
		// to render the content.
		const response = await env.BROWSER.quickAction("content", {
			url: pageUrl.toString(),
			gotoOptions: {
				waitUntil: "networkidle2",
				timeout: 30000,
			},
		});

		if (!response.ok) {
			const detail = (await response.text()).slice(0, 500);
			return new Response(
				`Browser Run failed with ${response.status}: ${detail}`,
				{ status: 502 },
			);
		}

		// The /content endpoint returns a JSON envelope with the rendered HTML
		// in the result field.
		const data = await response.json();

		if (!data.success || typeof data.result !== "string") {
			return new Response("Browser Run returned an unsuccessful response", {
				status: 502,
			});
		}

		const html = data.result;

		// Upload the rendered HTML to built-in storage. uploadAndPoll waits until
		// the page is indexed and searchable.
		const item = await env.AI_SEARCH.get(INSTANCE_NAME).items.uploadAndPoll(
			itemKey(pageUrl),
			html,
			{ timeoutMs: 60_000 },
		);

		return Response.json({ key: item.key, status: item.status });
	},
};
src/index.tsts
export interface Env {
	BROWSER: BrowserRun;
	AI_SEARCH: AiSearchNamespace;
}

// The instance that indexes the fetched page.
const INSTANCE_NAME = "my-instance";

// Build a stable item key that ends in .html, so AI Search converts the HTML
// to Markdown before indexing it.
function itemKey(pageUrl: URL): string {
	const slug = `${pageUrl.hostname}${pageUrl.pathname}`
		.replace(/[^a-zA-Z0-9]+/g, "-")
		.replace(/^-+|-+$/g, "");
	return `${slug || "index"}.html`;
}

export default {
	async fetch(request, env): Promise<Response> {
		const url = new URL(request.url);

		// Search route: query the indexed content and return the matching chunks.
		if (url.pathname === "/search") {
			const query = url.searchParams.get("q");
			if (!query) {
				return new Response("Add a ?q= query parameter", { status: 400 });
			}
			const results = await env.AI_SEARCH.get(INSTANCE_NAME).search({ query });
			return Response.json({
				query: results.search_query,
				results: results.chunks.map((chunk) => ({
					key: chunk.item.key,
					score: chunk.score,
					text: chunk.text,
				})),
			});
		}

		// Index route: fetch a URL's rendered HTML and index it.
		const target = url.searchParams.get("url");
		if (!target) {
			return new Response(
				"Add a ?url= parameter to index a page, or use /search?q= to search",
				{ status: 400 },
			);
		}

		const pageUrl = new URL(target);

		// Fetch the fully rendered HTML with the Browser Run /content endpoint.
		// networkidle2 waits until the page has no more than two network
		// connections for at least 500 ms, giving client-side JavaScript time
		// to render the content.
		const response = await env.BROWSER.quickAction("content", {
			url: pageUrl.toString(),
			gotoOptions: {
				waitUntil: "networkidle2",
				timeout: 30000,
			},
		});

		if (!response.ok) {
			const detail = (await response.text()).slice(0, 500);
			return new Response(
				`Browser Run failed with ${response.status}: ${detail}`,
				{ status: 502 },
			);
		}

		// The /content endpoint returns a JSON envelope with the rendered HTML
		// in the result field.
		const data = (await response.json()) as {
			success: boolean;
			result?: string;
		};

		if (!data.success || typeof data.result !== "string") {
			return new Response("Browser Run returned an unsuccessful response", {
				status: 502,
			});
		}

		const html = data.result;

		// Upload the rendered HTML to built-in storage. uploadAndPoll waits until
		// the page is indexed and searchable.
		const item = await env.AI_SEARCH.get(INSTANCE_NAME).items.uploadAndPoll(
			itemKey(pageUrl),
			html,
			{ timeoutMs: 60_000 },
		);

		return Response.json({ key: item.key, status: item.status });
	},
} satisfies ExportedHandler<Env>;

.html のアイテムキーにより、AI Search はコンテンツを Markdown 変換 に通します。インデックス前にヘッダーやフッターなどのボイラープレートを取り除きます。

4. フィルタリング用のメタデータを付ける

この手順は任意です。この Worker はアップロードを制御するため、各ページにタイトルやセクションなどの構造化 メタデータ を付け、それらのフィールドで 検索をフィルター できます。組み込みクローラーだけではできないことです。

まず、インスタンスにカスタムメタデータフィールドを定義します。インスタンスをこれから作成する場合は、create に渡します。

npx wrangler ai-search create my-instance --type builtin --custom-metadata title:text --custom-metadata section:text

既存インスタンスにフィールドを追加するには、ダッシュボードの Settings、または update() バインディングメソッドを使います。インスタンスは最大 5 つのカスタムフィールドに対応し、各フィールドは textnumberbooleandatetime 型のいずれかです。スキーマを変更すると、既存ドキュメントが再インデックスされます。

次に、Browser Run の /json エンドポイント で、同じページからそれらのフィールドを抽出します。同じ browser バインディング経由で実行され、指定したスキーマに合う構造化 JSON を返します。手順 3 の fetch ハンドラーで、レンダリング済みの html を得たあと、アップロードの前に次を追加します。

// Extract structured metadata from the page with the /json endpoint.
// response_format constrains the model to the fields you defined above.
// Treat extraction as best-effort: if it fails, index the page without metadata.
const metadata: Record<string, string> = {};
try {
	const jsonResponse = await env.BROWSER.quickAction("json", {
		url: pageUrl.toString(),
		prompt: "Extract the page title and its top-level section.",
		response_format: {
			type: "json_schema",
			json_schema: {
				type: "object",
				properties: {
					title: { type: "string" },
					section: { type: "string" },
				},
				required: ["title"],
			},
		},
	});

	const extracted = (await jsonResponse.json()) as {
		result?: Record<string, unknown>;
	};

	// Metadata values must be strings, so coerce each value and drop empty ones.
	for (const [key, value] of Object.entries(extracted.result ?? {})) {
		if (value) metadata[key] = String(value);
	}
} catch {
	// Ignore extraction errors and index the page without metadata.
}

次に、アップロードオプションで metadata を渡します。

const item = await env.AI_SEARCH.get(INSTANCE_NAME).items.uploadAndPoll(
	itemKey(pageUrl),
	html,
	{ timeoutMs: 60_000, metadata },
);

インデックス後は、たとえば特定セクションのページにクエリを制限できます。クエリ構文は フィルタリング を参照してください。

5. 実行とデプロイ

ローカル開発サーバーを起動します。browser バインディングで remote = true が設定されているため、wrangler dev/content エンドポイントをリモートモードで実行します。

npx wrangler dev

URL を渡してページをインデックスします。

curl "http://localhost:8787/?url=https://example.com/"

応答にはアイテムキーとステータス(インデックス完了後は completed)が含まれます。

{ "key": "example-com.html", "status": "completed" }

同じ Worker の /search エンドポイントで、インデックス済みコンテンツを照会します。

curl "http://localhost:8787/search?q=what+is+this+domain+for"
{
	"query": "what is this domain for",
	"results": [
		{
			"key": "example-com.html",
			"score": 0.75,
			"text": "# Example Domain\nThis domain is for use in documentation examples..."
		}
	]
}

Cloudflare アカウントでログインし、Worker をデプロイしてインターネットからアクセスできるようにします。

npx wrangler login
npx wrangler deploy

次のステップ

役に立ちましたか?