← Обзор / ai-search / how-to
Загрузка и индексирование отдельных веб-страниц
В этом руководстве вы создадите Worker, который получает отрисованный HTML одной веб-страницы с помощью Browser Run /content конечная точка и загружает его в AI Search экземпляра встроенное хранилище с помощью API элементов. Затем AI Search индексирует страницу, чтобы она стала доступной для поиска, точно так же, как любой другой загруженный документ. Worker также предоставляет /search эндпойнт, который выполняет запросы к проиндексированным страницам, так что один сервис одновременно индексирует и выполняет поиск.
Когда использовать этот шаблон
Используйте этот шаблон, чтобы проиндексировать одну страницу или небольшой набор отдельно выбранных страниц по запросу. Чтобы сканировать и непрерывно индексировать весь сайт, используйте AI Search источник данных веб-сайта взамен.
И Browser Run, и экземпляр AI Search доступны через привязки, поэтому один Worker может получить страницу и проиндексировать её без публичной конечной точки между ними.
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
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Для настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Перейдите в каталог вашего приложения:
cd fetch-and-index2. Настройте 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 с именем вашего экземпляра.
// 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 });
},
};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