跳转到内容
搜索文档

跨多个实例搜索

最后更新 查看 MarkdownAgent 设置

AI Search 可以在一个请求中查询多个实例,合并结果,并使用结果来自的实例标记每个结果。本指南使用该功能一同搜索两个知识库:每一个用户都可以搜索的共享 **general(通用)**知识库,加上包含单户客户私有内容的 **tenant-specific(租户专用)**知识库。单个查询会返回来自两者的相关内容。

将每个租户的内容保持在其自身的实例中是隔离租户的推荐方式(请参阅多租户搜索隔离)。跨实例搜索允许你在查询时将租户的实例与共享内容结合起来,因此你无需将共享内容复制到每个租户的实例中。

你将构建的内容: 一个一同搜索共享的 general-knowledge 实例和按租户实例的 Worker,然后按来源对结果进行分组。

先决条件

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

Node.js 版本管理器

使用 Voltanvm 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。

你跨其搜索的实例必须属于同一个命名空间 (namespace)。本指南将为你创建它们。

1. 创建 Worker 项目

使用 create-cloudflare CLI (C3) 创建新的 Worker 项目。C3 是一种命令行工具,旨在帮助你设置并部署新的应用程序到 Cloudflare。

通过运行以下命令创建一个名为 multi-source-search 的新项目:

npm create cloudflare@latest -- multi-source-search

进行设置时,请选择以下选项:

  • 对于 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 multi-source-search

2. 配置 Wrangler

AI Search 命名空间绑定 添加到你的 Wrangler 配置文件。跨实例搜索是命名空间绑定上的一个方法,因此单个绑定就能访问命名空间中的每个实例。

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "multi-source-search",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-08-17",
  "ai_search_namespaces": [
    {
      "binding": "AI_SEARCH",
      "namespace": "default",
      "remote": true
    }
  ]
}
name = "multi-source-search"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"

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

remote 选项允许 wrangler dev 将请求代理到已部署的实例,因为 AI Search 不在本地运行。

3. 添加 Worker 代码

更新 src/index.ts。此 Worker 从请求标头中识别租户,然后在一次调用中搜索共享实例和该租户的实例。每个返回的块 (chunk) 都带有 instance_id,因此 Worker 可以按来源对结果进行分组。

src/index.jsjs
// 所有租户都可以搜索的共享实例。
const GENERAL_INSTANCE = "general-knowledge";
// 每个租户也有其自身的实例,仅保存该租户的内容。
const tenantInstance = (tenantId) => `tenant-${tenantId}`;

// 种子内容,以便每个实例在首次查询时都返回内容。在实际应用中,你会提前预置实例及其内容。
const GENERAL_DOC = `# Support hours
Support is available Monday to Friday, 9am to 5pm UTC for all customers.`;
const TENANT_DOC = `# Your plan
This account is on the Enterprise plan with a dedicated success manager.`;

export default {
	async fetch(request, env) {
		// 识别租户并读取查询。
		const tenantId = request.headers.get("x-tenant-id");
		if (!tenantId) {
			return new Response("Missing x-tenant-id header", { status: 400 });
		}
		const query = new URL(request.url).searchParams.get("q");
		if (!query) {
			return new Response("Add a ?q= query parameter", { status: 400 });
		}

		// 本 Demo 的一次性设置:创建两个实例并为各自填充种子数据。
		await ensureInstance(
			env,
			GENERAL_INSTANCE,
			"support-hours.md",
			GENERAL_DOC,
		);
		await ensureInstance(env, tenantInstance(tenantId), "plan.md", TENANT_DOC);

		// 在一次调用中搜索共享实例和该租户的实例。
		// AI Search 会扇出 (fan out) 到两者、合并结果,并用其来自的 instance_id 标记每个 chunk。你可以传递 1 到 10 个实例 ID。
		const results = await env.AI_SEARCH.search({
			query,
			ai_search_options: {
				instance_ids: [GENERAL_INSTANCE, tenantInstance(tenantId)],
			},
		});

		// 使用 instance_id 标记将共享结果与租户结果区分开来。
		const fromGeneral = results.chunks.filter(
			(chunk) => chunk.instance_id === GENERAL_INSTANCE,
		);
		const fromTenant = results.chunks.filter(
			(chunk) => chunk.instance_id === tenantInstance(tenantId),
		);

		return Response.json({
			query: results.search_query,
			general: fromGeneral.map((chunk) => ({
				key: chunk.item.key,
				text: chunk.text,
			})),
			tenant: fromTenant.map((chunk) => ({
				key: chunk.item.key,
				text: chunk.text,
			})),
			// 如果某个实例失败,另一个实例仍会返回。失败会落在此处。
			errors: results.errors,
		});
	},
};

// 创建带有内置存储的实例并填充一份文档种子。每次请求调用都是安全的:如果实例已存在则 create() 会抛出异常,因此 try/catch 会在就位后跳过设置。
async function ensureInstance(env, id, key, content) {
	try {
		await env.AI_SEARCH.create({ id });
		await env.AI_SEARCH.get(id).items.uploadAndPoll(key, content, {
			timeoutMs: 60_000,
		});
	} catch {
		// 实例已存在且已被填充种子数据。
	}
}
src/index.tsts
export interface Env {
	AI_SEARCH: AiSearchNamespace;
}

// 所有租户都可以搜索的共享实例。
const GENERAL_INSTANCE = "general-knowledge";
// 每个租户也有其自身的实例,仅保存该租户的内容。
const tenantInstance = (tenantId: string) => `tenant-${tenantId}`;

// 种子内容,以便每个实例在首次查询时都返回内容。在实际应用中,你会提前预置实例及其内容。
const GENERAL_DOC = `# Support hours
Support is available Monday to Friday, 9am to 5pm UTC for all customers.`;
const TENANT_DOC = `# Your plan
This account is on the Enterprise plan with a dedicated success manager.`;

export default {
	async fetch(request, env): Promise<Response> {
		// 识别租户并读取查询。
		const tenantId = request.headers.get("x-tenant-id");
		if (!tenantId) {
			return new Response("Missing x-tenant-id header", { status: 400 });
		}
		const query = new URL(request.url).searchParams.get("q");
		if (!query) {
			return new Response("Add a ?q= query parameter", { status: 400 });
		}

		// 本 Demo 的一次性设置:创建两个实例并为各自填充种子数据。
		await ensureInstance(
			env,
			GENERAL_INSTANCE,
			"support-hours.md",
			GENERAL_DOC,
		);
		await ensureInstance(env, tenantInstance(tenantId), "plan.md", TENANT_DOC);

		// 在一次调用中搜索共享实例和该租户的实例。
		// AI Search 会扇出 (fan out) 到两者、合并结果,并用其来自的 instance_id 标记每个 chunk。你可以传递 1 到 10 个实例 ID。
		const results = await env.AI_SEARCH.search({
			query,
			ai_search_options: {
				instance_ids: [GENERAL_INSTANCE, tenantInstance(tenantId)],
			},
		});

		// 使用 instance_id 标记将共享结果与租户结果区分开来。
		const fromGeneral = results.chunks.filter(
			(chunk) => chunk.instance_id === GENERAL_INSTANCE,
		);
		const fromTenant = results.chunks.filter(
			(chunk) => chunk.instance_id === tenantInstance(tenantId),
		);

		return Response.json({
			query: results.search_query,
			general: fromGeneral.map((chunk) => ({
				key: chunk.item.key,
				text: chunk.text,
			})),
			tenant: fromTenant.map((chunk) => ({
				key: chunk.item.key,
				text: chunk.text,
			})),
			// 如果某个实例失败,另一个实例仍会返回。失败会落在此处。
			errors: results.errors,
		});
	},
} satisfies ExportedHandler<Env>;

// 创建带有内置存储的实例并填充一份文档种子。每次请求调用都是安全的:如果实例已存在则 create() 会抛出异常,因此 try/catch 会在就位后跳过设置。
async function ensureInstance(
	env: Env,
	id: string,
	key: string,
	content: string,
) {
	try {
		await env.AI_SEARCH.create({ id });
		await env.AI_SEARCH.get(id).items.uploadAndPoll(key, content, {
			timeoutMs: 60_000,
		});
	} catch {
		// 实例已存在且已被填充种子数据。
	}
}

env.AI_SEARCH.search() 是命名空间级别的搜索。它不同于 env.AI_SEARCH.get(id).search(),后者仅搜索单个实例。传递 instance_ids 会将查询扇出到这些实例上,并返回一个合并且排序的块列表。因为每个块都包含 instance_id,你始终知道结果来自共享实例还是租户实例。

如果某个实例失败(例如因为 ID 不存在),其他实例仍然会返回,并且失败会在 errors 中报告而不是抛出异常。这使得缺失的租户实例表现为部分结果而非硬性错误。

4. 运行它

启动本地开发服务器:

npx wrangler dev

发送带有租户标头和查询的请求。第一个请求需要一点时间,因为要创建并填充实例:

curl "http://localhost:8787/?q=support+hours+and+my+plan" -H "x-tenant-id: acme"

响应会将来自共享实例和租户实例的结果分离开来:

{
	"query": "support hours and my plan",
	"general": [
		{
			"key": "support-hours.md",
			"text": "# Support hours\nSupport is available..."
		}
	],
	"tenant": [
		{
			"key": "plan.md",
			"text": "# Your plan\nThis account is on the Enterprise plan..."
		}
	]
}

当每个实例都成功时,响应没有 errors 字段。如果某个实例失败,errors 数组会列出失败原因,同时其他实例的结果仍然返回。

5. 基于两个实例生成回答

要返回一个基于两个实例的单篇撰写的回答而不是原始 chunks,请使用带有相同 instance_idschatCompletions。它会从列出的每个实例中进行检索,然后从组合的上下文生成一个响应:

const completion = await env.AI_SEARCH.chatCompletions({
	query,
	ai_search_options: {
		instance_ids: [GENERAL_INSTANCE, tenantInstance(tenantId)],
	},
});

// 生成的回答建立在共享内容与租户内容之上。
const answer = completion.choices[0]?.message.content;

响应还包含检索到的 chunks(每个都用其 instance_id 进行标记),以便你可以引用来源,以及针对任何失败实例的 errors 数组。

6. 部署

登录你的 Cloudflare 账户,然后部署你的 Worker:

npx wrangler login
npx wrangler deploy

后续步骤

这篇文档对您有帮助吗?