INTEGRITY Dokumentace

Vyhledávání napříč více instancemi

AI Search umí v jednom požadavku prohledat několik instancí, sloučit výsledky a každý výsledek označit instancí, ze které pochází. Tento návod to využívá k prohledání dvou znalostních bází zároveň: sdílené obecné znalostní báze, kterou může prohledávat každý uživatel, plus specifický pro tenanta znalostní báze, která obsahuje soukromý obsah jednoho zákazníka. Jediný dotaz vrátí relevantní obsah z obou.

Udržování obsahu každého tenanta ve vlastní instanci je doporučeným způsobem izolace tenantů (viz Multi-tenant izolace vyhledávání). Vyhledávání napříč instancemi pak umožňuje zkombinovat instanci daného tenanta se sdíleným obsahem přímo v okamžiku dotazu, takže sdílený obsah nemusíte kopírovat do instance každého tenanta.

Co vytvoříte: Worker, který prohledává sdílený general-knowledge instanci a instanci pro jednoho tenanta zároveň a poté seskupí výsledky podle jejich zdroje.

Předpoklady

  1. Zaregistrujte si účet Cloudflare ↗.
  2. Nainstalujte Node.js ↗.

Správce verzí Node.js

Použijte správce verzí Node, jako je Volta ↗ nebo nvm ↗ abyste se vyhnuli problémům s oprávněními a mohli měnit verze Node.js. Wrangler, o kterém se dozvíte dále v této příručce, vyžaduje verzi Node 16.17.0 nebo novější.

Instance, které prohledáváte, musí patřit do stejného namespace. Tento návod je za vás vytvoří.

1. Vytvořte projekt Worker

Vytvořte nový projekt Worker pomocí create-cloudflare CLI (C3). C3 ↗ je nástroj příkazové řádky, který vám pomůže nastavit a nasadit nové aplikace na Cloudflare.

Vytvořte nový projekt s názvem multi-source-search spuštěním:

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

Při nastavení vyberte následující možnosti:

Přejděte do adresáře své aplikace:

cd multi-source-search

2. Nakonfigurujte Wrangler

Přidejte AI Search namespace binding do vašeho Konfigurační soubor Wrangler. Prohledávání napříč instancemi je metoda bindingu jmenného prostoru, takže jediný binding dosáhne na každou instanci v jmenném prostoru.

{
  "$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 volba umožňuje wrangler dev předávají požadavky na vaše nasazené instance, protože AI Search neběží lokálně.

3. Přidejte kód Workeru

Aktualizujte src/index.ts. Tento Worker určí tenanta podle hlavičky požadavku a poté v jednom volání prohledá sdílenou instanci i instanci daného tenanta. Každá vrácená část obsahu obsahuje instance_id, aby Worker mohl výsledky seskupit podle zdroje.

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() je vyhledávání na úrovni jmenného prostoru. Liší se od env.AI_SEARCH.get(id).search(), který prohledává jednu instanci. Předání instance_ids rozešle dotaz napříč těmito instancemi a vrátí jeden sloučený, seřazený seznam chunků. Protože každý chunk obsahuje instance_id, vždy víte, zda výsledek pochází ze sdílené instance, nebo z instance tenanta.

Pokud jedna instance selže, například proto, že dané ID neexistuje, ostatní instance se přesto vrátí a chyba se uvede v errors namísto vyvolání výjimky. Díky tomu chybějící instance tenanta představuje částečný výsledek, a ne tvrdou chybu.

4. Spusťte to

Spusťte místní vývojový server:

npx wrangler dev

Odešlete požadavek s hlavičkou tenanta a dotazem. První požadavek chvíli trvá, protože vytváří a inicializuje instance:

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

Odpověď odděluje výsledky ze sdílené instance a instance tenanta:

{
	"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..."
		}
	]
}

Pokud se všechny instance zdaří, odpověď neobsahuje žádné errors je pole. Pokud instance selže, errors array uvádí chyby, zatímco výsledky ostatních instancí se stále vracejí.

5. Vygenerujte odpověď napříč oběma instancemi

Chcete-li místo nezpracovaných částí obsahu vrátit jedinou písemnou odpověď podloženou oběma instancemi, použijte chatCompletions se stejným instance_ids. Načte data ze všech uvedených instancí a poté z kombinovaného kontextu vygeneruje jednu odpověď:

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;

Odpověď navíc obsahuje načtené chunks (každý označený svým instance_id) abyste mohli citovat zdroje, a errors pole pro každou instanci, která selhala.

6. Nasaďte

Přihlaste se pomocí svého účtu Cloudflare, poté nasaďte svůj Worker:

npx wrangler login
npx wrangler deploy

Další kroky

Multi-tenant izolace vyhledávání

Poskytněte každému tenantovi vlastní instanci, nebo sdílejte jednu instanci s filtrováním podle metadat.

Jmenné prostory

Jak se instance seskupují a jak je adresuje vazba jmenného prostoru.

Vazba Workers pro Search

Úplná referenční dokumentace k vyhledávání a chatování pro jednu instanci i více instancí.