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

Поиск сразу по нескольким экземплярам

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

Чтобы изолировать тенантов, рекомендуется хранить содержимое каждого из них в отдельном экземпляре (см. Мультитенантная изоляция поиска). Поиск сразу по нескольким экземплярам позволяет объединить экземпляр арендатора с общим содержимым прямо во время запроса, поэтому копировать общее содержимое в экземпляр каждого арендатора не требуется.

Что вы создадите: Worker, который выполняет поиск в общем general-knowledge экземпляр и экземпляр отдельного тенанта вместе, а затем группирует результаты по источнику.

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

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

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

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

Экземпляры, по которым выполняется поиск, должны принадлежать одному и тому же пространство имён. Это руководство создаст их за вас.

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

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

Создайте новый проект с именем multi-source-search выполнив:

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

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

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

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-28",
  "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-28"

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

remote опция позволяет wrangler dev перенаправляют запросы к вашим развёрнутым экземплярам, поскольку AI Search не выполняется локально.

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

Обновите src/index.ts. Этот Worker определяет тенанта по заголовку запроса, а затем в одном вызове выполняет поиск в общем экземпляре и в экземпляре этого тенанта. Каждый возвращённый чанк содержит instance_id, чтобы Worker мог группировать результаты по источнику.

src/index.js
// The shared instance that every tenant can search.
const GENERAL_INSTANCE = "general-knowledge";
// Each tenant also has its own instance, holding only that tenant's content.
const tenantInstance = (tenantId) => `tenant-${tenantId}`;

// Seed content, so each instance returns something on the first query. In a
// real app you would provision instances and their content ahead of time.
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) {
		// Identify the tenant and read the query.
		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 });
		}

		// One-time setup for this demo: create both instances and seed each.
		await ensureInstance(
			env,
			GENERAL_INSTANCE,
			"support-hours.md",
			GENERAL_DOC,
		);
		await ensureInstance(env, tenantInstance(tenantId), "plan.md", TENANT_DOC);

		// Search the shared instance and this tenant's instance in one call.
		// AI Search fans out to both, merges the results, and tags each chunk
		// with the instance_id it came from. You can pass 1 to 10 instance IDs.
		const results = await env.AI_SEARCH.search({
			query,
			ai_search_options: {
				instance_ids: [GENERAL_INSTANCE, tenantInstance(tenantId)],
			},
		});

		// Use the instance_id tag to separate shared results from tenant results.
		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,
			})),
			// If one instance fails, the other still returns. Failures land here.
			errors: results.errors,
		});
	},
};

// Create an instance with built-in storage and seed one document. Safe to call
// on every request: create() throws if the instance already exists, so the
// try/catch skips setup once it is in place.
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 {
		// The instance already exists and has been seeded.
	}
}
src/index.ts
export interface Env {
	AI_SEARCH: AiSearchNamespace;
}

// The shared instance that every tenant can search.
const GENERAL_INSTANCE = "general-knowledge";
// Each tenant also has its own instance, holding only that tenant's content.
const tenantInstance = (tenantId: string) => `tenant-${tenantId}`;

// Seed content, so each instance returns something on the first query. In a
// real app you would provision instances and their content ahead of time.
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> {
		// Identify the tenant and read the query.
		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 });
		}

		// One-time setup for this demo: create both instances and seed each.
		await ensureInstance(
			env,
			GENERAL_INSTANCE,
			"support-hours.md",
			GENERAL_DOC,
		);
		await ensureInstance(env, tenantInstance(tenantId), "plan.md", TENANT_DOC);

		// Search the shared instance and this tenant's instance in one call.
		// AI Search fans out to both, merges the results, and tags each chunk
		// with the instance_id it came from. You can pass 1 to 10 instance IDs.
		const results = await env.AI_SEARCH.search({
			query,
			ai_search_options: {
				instance_ids: [GENERAL_INSTANCE, tenantInstance(tenantId)],
			},
		});

		// Use the instance_id tag to separate shared results from tenant results.
		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,
			})),
			// If one instance fails, the other still returns. Failures land here.
			errors: results.errors,
		});
	},
} satisfies ExportedHandler<Env>;

// Create an instance with built-in storage and seed one document. Safe to call
// on every request: create() throws if the instance already exists, so the
// try/catch skips setup once it is in place.
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 {
		// The instance already exists and has been seeded.
	}
}

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. Сгенерируйте ответ по обоим экземплярам

Чтобы получить один связный ответ на основе данных из обоих экземпляров вместо необработанных чанков, используйте chatCompletions с тем же instance_ids. Он получает данные из каждого перечисленного экземпляра, а затем генерирует один ответ на основе объединённого контекста:

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

// The generated answer, grounded in both the shared and tenant content.
const answer = completion.choices[0]?.message.content;

Ответ также включает найденные chunks (каждый из которых помечен своим instance_id) чтобы вы могли цитировать источники, а также errors для каждого экземпляра, который завершился ошибкой.

6. Разверните

Войдите в свой аккаунт Cloudflare, затем разверните Worker:

npx wrangler login
npx wrangler deploy

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

Мультитенантная изоляция поиска

Выделяйте каждому тенанту отдельный экземпляр либо используйте один общий экземпляр с фильтрацией по метаданным.

Пространства имён

Как группируются экземпляры и как их адресует привязка пространства имён.

Привязка Workers для поиска

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