跳转到内容
搜索文档

抓取并索引单个网页

最后更新 查看 MarkdownAgent 设置

本指南将构建一个 Worker,该 Worker 使用 Browser Run/content 端点 获取单个网页的已渲染 HTML,并使用 Items API 将其上传到 AI Search 实例的内置存储。随后,AI Search 会对该页面进行索引,使其与其他任何已上传文档一样可以被搜索。该 Worker 还公开了一个 /search 端点来查询已索引的页面,因此单个服务既能进行索引又能进行搜索。

何时使用此模式

当你需要按需索引单个页面或一小组精选页面时,请使用此模式。要爬取并持续索引整个网站,请改用 AI Search 网站数据源 (website data source)

Browser Run 和 AI Search 实例都是通过绑定 (bindings) 来访问的,因此单个 Worker 就可以抓取页面并进行索引,而无需在中间暴露公共端点。

先决条件

  1. 注册 Cloudflare 账户
  2. 安装 Node.js

Node.js 版本管理器

使用 Voltanvm 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 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 配置文件 中添加两个绑定:一个用于 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-08-17",
  "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-17"

[browser]
binding = "BROWSER"
remote = true

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

browser 绑定的 quickAction 方法需要 2026-03-24 或更晚的兼容性日期,并且在没有 remote 模式的本地开发中不受支持。在 browser 绑定上设置 remote = truewrangler dev 启用了远程模式。AI Search 绑定上的 remote 选项会将上传代理发送到你已部署的实例,因为 AI Search 不在本地运行。

3. 添加 Worker 代码

更新 src/index.ts。此 Worker 具有两个路由:带有 ?url= 参数的请求会抓取该页面的已渲染 HTML 并对其进行索引;对 /search?q= 的请求则会查询已索引的内容。将 my-instance 替换为你实例的名称。

src/index.jsjs
// 索引抓取页面的实例。
const INSTANCE_ID = "my-instance";

// 构建一个以 .html 结尾的稳定 item key,以便 AI Search 在索引前将 HTML 转换为 Markdown。
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);

		// 搜索路由:查询已索引的内容并返回匹配的块 (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,
				})),
			});
		}

		// 索引路由:抓取 URL 的已渲染 HTML 并对其进行索引。
		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);

		// 使用 Browser Run /content 端点抓取完全渲染的 HTML。
		// networkidle2 会等待直至页面在至少 500 ms 内不超过两个网络连接,从而给客户端 JavaScript 留出渲染内容的时间。
		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 },
			);
		}

		// /content 端点返回包含 result 字段中已渲染 HTML 的 JSON 包裹。
		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;

		// 将已渲染的 HTML 上传到内置存储。uploadAndPoll 会等待直至页面被索引且可搜索。
		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.tsts
export interface Env {
	BROWSER: BrowserRun;
	AI_SEARCH: AiSearchNamespace;
}

// 索引抓取页面的实例。
const INSTANCE_ID = "my-instance";

// 构建一个以 .html 结尾的稳定 item key,以便 AI Search 在索引前将 HTML 转换为 Markdown。
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);

		// 搜索路由:查询已索引的内容并返回匹配的块 (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,
				})),
			});
		}

		// 索引路由:抓取 URL 的已渲染 HTML 并对其进行索引。
		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);

		// 使用 Browser Run /content 端点抓取完全渲染的 HTML。
		// networkidle2 会等待直至页面在至少 500 ms 内不超过两个网络连接,从而给客户端 JavaScript 留出渲染内容的时间。
		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 },
			);
		}

		// /content 端点返回包含 result 字段中已渲染 HTML 的 JSON 包裹。
		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;

		// 将已渲染的 HTML 上传到内置存储。uploadAndPoll 会等待直至页面被索引且可搜索。
		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 控制上传,你可以使用结构化元数据 (metadata)(如标题和章节)来丰富每个页面,然后按这些字段过滤搜索。这是内置爬虫自身无法做到的。

首先,在你实例上定义自定义元数据字段。如果你现在正在创建实例,请将其传递给 create

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

要向现有实例添加字段,请在仪表板中的 Settings(设置) 下使用,或者使用 update() 绑定方法。一个实例最多支持五个自定义字段,且每个字段可以是 textnumberbooleandatetime 类型。更改 schema 会重新索引现有的文档。

下一步,使用 Browser Run 的 /json 端点 从同一个页面提取这些字段。它运行在相同的 browser 绑定上,并返回符合你提供 schema 的结构化 JSON。在步骤 3 的 fetch 处理函数中,当你拥有已渲染的 html 且在上传之前,添加:

// 使用 /json 端点从页面提取结构化元数据。
// response_format 会将模型限制为你上述定义的字段。
// 将提取视为尽力而为:如果失败,则在没有元数据的情况下索引页面。
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>;
	};

	// 元数据值必须是字符串,因此对每个值进行强制转换并丢弃空值。
	for (const [key, value] of Object.entries(extracted.result ?? {})) {
		if (value) metadata[key] = String(value);
	}
} catch {
	// 忽略提取错误并在没有元数据的情况下索引页面。
}

然后在上传选项中传递 metadata

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

一旦完成索引,你就可以将查询限制在例如给定章节中的页面。有关查询语法,请参阅过滤 (Filtering)

5. 运行与部署

启动本地开发服务器。由于在 browser 绑定上设置了 remote = truewrangler 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

后续步骤

这篇文档对您有帮助吗?