INTEGRITY Dokumentace

Načíst a zaindexovat jednotlivé webové stránky

Tento návod sestaví Worker, který načte vykreslené HTML jedné webové stránky pomocí Browser Run /content koncový bod a nahraje ho do AI Search instance vestavěné úložiště pomocí Items API. AI Search poté stránku zaindexuje, takže je prohledatelná stejně jako každý jiný nahraný dokument. Worker navíc zpřístupňuje /search koncový bod, který dotazuje indexované stránky, takže jedna služba zároveň indexuje i vyhledává.

Kdy použít tento vzor

Tento vzor použijte k indexování jedné stránky nebo malé ručně vybrané sady stránek na vyžádání. Chcete-li procházet a průběžně indexovat celý web, použijte AI Search zdroj dat webu místo toho.

Browser Run i instance AI Search jsou dostupné prostřednictvím bindings, takže jeden Worker dokáže načíst stránku a zaindexovat ji bez nutnosti veřejného endpointu mezi nimi.

Předpoklady

  1. Zaregistrujte si účet Cloudflare ↗.
  2. Nainstalujte Node.js ↗.

Správce verzí Node.js

Použijte správce verzí Node, jako je Volta ↗ nebo nvm ↗ abyste se vyhnuli problémům s oprávněními a mohli měnit verze Node.js. Wrangler, o kterém se dozvíte dále v této příručce, vyžaduje verzi Node 16.17.0 nebo novější.

Také potřebujete instanci AI Search, do které budete nahrávat. Její vytvoření najdete v Začínáme. Tento návod nahrává soubory do vestavěného úložiště instance, takže instance nepotřebuje externí zdroj dat.

1. Vytvořte projekt Worker

Vytvořte nový projekt Worker pomocí create-cloudflare CLI (C3). C3 ↗ je nástroj příkazové řádky, který vám pomůže nastavit a nasadit nové aplikace na Cloudflare.

Vytvořte nový projekt s názvem fetch-and-index spuštěním:

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

Při nastavení vyberte následující možnosti:

Přejděte do adresáře své aplikace:

cd fetch-and-index

2. Nakonfigurujte Wrangler

Přidejte obě vazby do svého Konfigurační soubor Wrangler: vazba pro prohlížeč pro Browser Run a AI Search namespace binding pro nahrávání. /content koncový bod běží přes browser binding, takže není nutné instalovat Puppeteer ani žádný jiný balíček.

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "fetch-and-index",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-08-28",
  "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-08-28"

[browser]
binding = "BROWSER"
remote = true

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

browser bindingu quickAction metoda vyžaduje datum kompatibility 2026-03-24 nebo novější a bez režimu remote není podporováno v lokálním vývoji. Nastavení remote = true na vazbě prohlížeče zapíná vzdálený režim pro wrangler dev. remote volba na vazbě AI Search přeposílá nahrávání na vaši nasazenou instanci, protože AI Search neběží lokálně.

3. Přidejte kód Workeru

Aktualizujte src/index.ts. Tento Worker má dvě trasy: požadavek s ?url= parametr načte vykreslené HTML dané stránky a zaindexuje je, a požadavek na /search?q= dotazuje indexovaný obsah. Nahraďte my-instance s názvem vaší instance.

src/index.js
// The instance that indexes the fetched page.
const INSTANCE_ID = "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_ID).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_ID).items.uploadAndPoll(
			itemKey(pageUrl),
			html,
			{ timeoutMs: 60_000 },
		);

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

// The instance that indexes the fetched page.
const INSTANCE_ID = "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_ID).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_ID).items.uploadAndPoll(
			itemKey(pageUrl),
			html,
			{ timeoutMs: 60_000 },
		);

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

.html klíč položky říká AI Search, aby obsah zpracovala pomocí Převod do Markdownu, který před indexováním odstraní prvky šablony, jako je hlavička nebo patička.

4. Připojte metadata pro filtrování

Tento krok je volitelný. Protože nahrávání řídí tento Worker, můžete každou stránku obohatit o strukturovaná metadata, jako je její název a sekce, a poté Filtrování vyhledávání podle těchto polí. To vestavěný prohledávač sám o sobě nedokáže.

Nejprve definujte vlastní položky metadat na své instanci. Pokud instanci právě vytváříte, předejte je do create:

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

Chcete-li do existující instance přidat položky, použijte řídicí panel v části Nastavení, nebo update() metoda vazby. Instance podporuje až pět vlastních polí a každé pole může být text, number, boolean, nebo datetime typu. Změna schématu způsobí reindexaci stávajících dokumentů.

Dále použijte Browser Run /json koncový bod a získejte tak tato pole ze stejné stránky. Funguje přes stejný browser binding a vrací strukturovaný JSON odpovídající vámi zadanému schématu. V fetch obslužnou rutinu z kroku 3, poté co máte vykreslený html a před nahráním přidejte:

// 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.
}

Poté předejte metadata v možnostech nahrávání:

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

Po zaindexování můžete dotazy omezit například na stránky v dané sekci. Další informace najdete v Filtrování pro syntaxi dotazu.

5. Spusťte a nasaďte

Spusťte místní vývojový server. Protože remote = true je nastaveno na vazbě prohlížeče, wrangler dev spustí /content koncový bod ve vzdáleném režimu:

npx wrangler dev

Zaindexujte stránku předáním její URL adresy:

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

Odpověď obsahuje klíč položky a její stav (completed po zaindexování):

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

Poté zaindexovaný obsah dotazujte prostřednictvím /search koncový bod:

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..."
		}
	]
}

Přihlaste se pomocí svého účtu Cloudflare, poté nasaďte svůj Worker, aby byl dostupný na internetu:

npx wrangler login
npx wrangler deploy

Další kroky

Endpoint /content nástroje Browser Run

Načtěte plně vykreslené HTML stránky po spuštění JavaScriptu, s možnostmi pro chování při načítání a blokování.

Items Workers binding

Úplná referenční dokumentace k nahrávání, výpisu a mazání dokumentů ve vestavěném úložišti.

Datový zdroj typu web

Automaticky procházejte a indexujte doménu, kterou vlastníte, podle jejího sitemapu.