INTEGRITY Документация

Загрузка и индексирование отдельных веб-страниц

В этом руководстве вы создадите Worker, который получает отрисованный HTML одной веб-страницы с помощью Browser Run /content конечная точка и загружает его в AI Search экземпляра встроенное хранилище с помощью API элементов. Затем AI Search индексирует страницу, чтобы она стала доступной для поиска, точно так же, как любой другой загруженный документ. Worker также предоставляет /search эндпойнт, который выполняет запросы к проиндексированным страницам, так что один сервис одновременно индексирует и выполняет поиск.

Когда использовать этот шаблон

Используйте этот шаблон, чтобы проиндексировать одну страницу или небольшой набор отдельно выбранных страниц по запросу. Чтобы сканировать и непрерывно индексировать весь сайт, используйте AI Search источник данных веб-сайта взамен.

И Browser Run, и экземпляр AI Search доступны через привязки, поэтому один Worker может получить страницу и проиндексировать её без публичной конечной точки между ними.

Предварительные требования

  1. Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
  2. Установка Node.js ↗.

менеджер версий Node.js

Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.

Вам также потребуется экземпляр AI Search, в который можно загружать файлы. Чтобы создать его, см. Начало работы. В этом руководстве файлы загружаются во встроенное хранилище экземпляра, поэтому внешний источник данных ему не нужен.

1. Создайте проект Worker

Создайте новый проект Worker с помощью create-cloudflare CLI (C3). C3 ↗ это инструмент командной строки, который помогает настраивать и развёртывать новые приложения в Cloudflare.

Создайте новый проект с именем fetch-and-index выполнив:

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

Для настройки выберите следующие параметры:

Перейдите в каталог вашего приложения:

cd fetch-and-index

2. Настройте Wrangler

Добавьте обе привязки в свой конфигурационный файл Wrangler: привязка browser для Browser Run и Привязка пространства имён AI Search для загрузок. /content эндпойнт работает через привязку браузера, поэтому вам не нужно устанавливать 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-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

Привязки браузера quickAction метод требует дату совместимости 2026-03-24 или новее, и без режима remote это не поддерживается в локальной разработке. Настройка remote = true в привязке браузера включает удалённый режим для wrangler dev. remote опция в привязке AI Search перенаправляет загрузки на ваш развёрнутый экземпляр, поскольку AI Search не работает локально.

3. Добавьте код Worker

Обновите src/index.ts. У этого Worker есть два маршрута: запрос с ?url= параметр получает отрисованный HTML этой страницы и индексирует его, а запрос к /search?q= выполняет запрос к индексированному контенту. Замените my-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 ключ элемента указывает AI Search, что нужно обработать содержимое через Преобразование в Markdown, который перед индексированием удаляет шаблонные элементы, такие как шапка и подвал.

4. Прикрепите метаданные для фильтрации

Этот шаг необязателен. Поскольку загрузку контролирует этот Worker, вы можете дополнить каждую страницу структурированными метаданные, например его название и раздел, а затем фильтрация поиска по этим полям. Встроенный краулер сам по себе на это не способен.

Сначала определите пользовательские поля метаданных для своего экземпляра. Если вы создаёте экземпляр прямо сейчас, передайте их в create:

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

Чтобы добавить поля в существующий экземпляр, используйте dashboard в разделе Настройки, или update() метод привязки. Экземпляр поддерживает до пяти пользовательских полей, и каждое поле может быть text, number, boolean, или datetime типа. Изменение схемы вызывает переиндексацию существующих документов.

Далее используйте Browser Run /json конечная точка и таким образом извлечь эти поля с той же страницы. Он работает через тот же browser binding и возвращает структурированный JSON, соответствующий заданной вами схеме. В fetch обработчик из шага 3, после того как у вас появится отрендеренный 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_ID).items.uploadAndPoll(
	itemKey(pageUrl),
	html,
	{ timeoutMs: 60_000, metadata },
);

После индексирования вы можете, например, ограничить запросы страницами определённого раздела. См. Фильтрация для синтаксиса запроса.

5. Запустите и разверните

Запустите локальный сервер разработки. Поскольку 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

Следующие шаги

Эндпойнт Browser Run /content

Получайте полностью отрендеренный HTML страницы после выполнения JavaScript, с настройками поведения загрузки и блокировки.

Привязка Workers для элементов

Полный справочник по загрузке, получению списка и удалению документов во встроенном хранилище.

Источник данных «Сайт»

Автоматически сканируйте и индексируйте домен, которым вы владеете, следуя его sitemap.