INTEGRITY Dokumentace

Multitenancy

V multitenantní aplikaci musí každý tenant vidět pouze svá vlastní data. AI Search nabízí dva způsoby, jak vyhledávání izolovat podle tenanta: buď každému tenantovi přidělit vlastní instanci, nebo sdílet jednu instanci a filtrovat podle tenanta v době dotazu.

Zvolte přístup

Přístup Jak to izoluje Zvolte jej, když
Instance na tenanta (doporučeno) Každý tenant získá samostatnou instanci s vlastním úložištěm a indexem Potřebujete silnou izolaci nebo za běhu vytváříte a mažete tenanty
Sdílená instance s filtrováním Jedna instance obsahuje všechny tenanty; filtr metadat omezuje rozsah každého dotazu Máte mnoho malých tenantů a chcete co nejjednodušší nastavení

Předpoklady

Oba přístupy využívají Cloudflare Worker. Nejprve vytvořte projekt a poté postupujte podle zvolené možnosti.

  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ší.

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 tenant-search spuštěním:

npm create cloudflare@latest -- tenant-search

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

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

cd tenant-search

Možnost 1: Jedna instance na tenanta

Toto je doporučeno přístup. Každý tenant získá vlastní instanci s vlastním úložištěm a vyhledávacím indexem, takže jeden tenant nikdy nemůže získat dokumenty jiného tenanta.

Za běhu vytvořte pro každého tenanta izolovanou instanci AI Search pomocí vazba jmenného prostoru.

Příchozí požadavky nájemců
Tenant ATenant BTenant C
Fan-in: každý požadavek nájemce dorazí ke stejnému Workeru Worker: směruje každý požadavek podle ID tenanta prostřednictvím vazby namespace env.AI_SEARCH.get(id)Worker Čára do rozsahu jmenného prostoru
namespace: tenants Rozdělení: vazba získá nebo vytvoří jednu instanci pro každého tenanta Jedna izolovaná instance pro každého nájemce

Přidejte vazbu jmenného prostoru do svého Konfigurační soubor Wrangler:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "ai_search_namespaces": [
    {
      "binding": "TENANTS",
      "namespace": "default",
      "remote": true
    }
  ]
}
[[ai_search_namespaces]]
binding = "TENANTS"
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ě.

Vestavěné úložiště

Instance každého tenanta obsahuje dokumenty, které do ní nahrajete přímo, bez externího zdroje dat.

Aktualizujte src/index.ts. Tento Worker určí tenanta podle hlavičky požadavku a poté vytvoří, naplní, prohledá a smaže jeho instanci.

src/index.js
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Identify the tenant from the request header.
		const tenantId = request.headers.get("x-tenant-id");

		if (!tenantId) {
			return new Response("Missing x-tenant-id header", { status: 400 });
		}

		// Create a new instance for the tenant.
		if (url.pathname === "/onboard" && request.method === "POST") {
			const instance = await env.TENANTS.create({
				id: `tenant-${tenantId}`,
			});
			return Response.json({ success: true, instance: await instance.info() });
		}

		// Upload a document to the tenant's instance.
		if (url.pathname === "/upload" && request.method === "POST") {
			const formData = await request.formData();
			const file = formData.get("file");

			const item = await env.TENANTS.get(`tenant-${tenantId}`).items.upload(
				file.name,
				await file.arrayBuffer(),
			);
			return Response.json({ success: true, item });
		}

		// Search the tenant's instance. Search is isolated to their instance.
		if (url.pathname === "/search") {
			const query = url.searchParams.get("q") || "";

			const results = await env.TENANTS.get(`tenant-${tenantId}`).search({
				messages: [{ role: "user", content: query }],
			});
			return Response.json(results);
		}

		// Delete the tenant's instance and all its data.
		if (url.pathname === "/offboard" && request.method === "DELETE") {
			await env.TENANTS.delete(`tenant-${tenantId}`);
			return Response.json({ success: true });
		}

		return new Response("Not found", { status: 404 });
	},
};
src/index.ts
export type Env = {
	TENANTS: AiSearchNamespace;
};

export default {
	async fetch(request, env): Promise<Response> {
		const url = new URL(request.url);

		// Identify the tenant from the request header.
		const tenantId = request.headers.get("x-tenant-id");

		if (!tenantId) {
			return new Response("Missing x-tenant-id header", { status: 400 });
		}

		// Create a new instance for the tenant.
		if (url.pathname === "/onboard" && request.method === "POST") {
			const instance = await env.TENANTS.create({
				id: `tenant-${tenantId}`,
			});
			return Response.json({ success: true, instance: await instance.info() });
		}

		// Upload a document to the tenant's instance.
		if (url.pathname === "/upload" && request.method === "POST") {
			const formData = await request.formData();
			const file = formData.get("file") as File;

			const item = await env.TENANTS.get(`tenant-${tenantId}`).items.upload(
				file.name,
				await file.arrayBuffer(),
			);
			return Response.json({ success: true, item });
		}

		// Search the tenant's instance. Search is isolated to their instance.
		if (url.pathname === "/search") {
			const query = url.searchParams.get("q") || "";

			const results = await env.TENANTS.get(`tenant-${tenantId}`).search({
				messages: [{ role: "user", content: query }],
			});
			return Response.json(results);
		}

		// Delete the tenant's instance and all its data.
		if (url.pathname === "/offboard" && request.method === "DELETE") {
			await env.TENANTS.delete(`tenant-${tenantId}`);
			return Response.json({ success: true });
		}

		return new Response("Not found", { status: 404 });
	},
} satisfies ExportedHandler<Env>;

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

npx wrangler dev

Poté tenanta onboardujte, nahrajte dokument do jeho instance, vyhledejte v ní a nakonec ho odeberte. x-tenant-id hlavička omezuje každý požadavek na instanci daného tenanta:

# Create an isolated instance for tenant "acme"
curl -X POST http://localhost:8787/onboard -H "x-tenant-id: acme"

# Upload a document to acme's instance
curl -X POST http://localhost:8787/upload -H "x-tenant-id: acme" -F "file=@./handbook.pdf"

# Search acme's instance
curl "http://localhost:8787/search?q=vacation+policy" -H "x-tenant-id: acme"

# Delete acme's instance and all of its data
curl -X DELETE http://localhost:8787/offboard -H "x-tenant-id: acme"

AI Search indexuje nahrané soubory asynchronně, počkejte tedy chvíli po nahrání, než začnete vyhledávat.

R2

Pokud data vašich tenantů již existují v R2, zajistěte instanci každého tenanta úložištěm R2 místo nahrávání dokumentů do vestavěného úložiště. Změňte /onboard trasu z vestavěné úložiště příklad pro vytvoření instance založené na R2. Způsob konfigurace závisí na tom, jak jsou data uspořádána.

Jeden bucket na tenanta: pokud jsou data každého tenanta již ve vlastním bucketu, nasměrujte instanci na celý tento bucket:

const instance = await env.TENANTS.create({
	id: `tenant-${tenantId}`,
	type: "r2",
	source: `tenant-${tenantId}-bucket`,
});

Sdílený bucket: pokud jsou data všech tenantů v jednom bucketu, uspořádaná podle složek:

Omezte každou instanci na složku daného tenanta pomocí filtrování cest, takže vždy indexuje a prohledává pouze objekty daného tenanta:

const instance = await env.TENANTS.create({
	id: `tenant-${tenantId}`,
	type: "r2",
	source: "my-bucket",
	source_params: {
		include_items: [`/customers/${tenantId}/**`],
	},
});

V obou uspořádáních AI Search zaindexuje objekty daného tenanta při příštím synchronizace. Dokumenty přidávejte zápisem do R2 místo nahrávání přes Worker a ponechte trasy pro vyhledávání a odebrání z příkladu s vestavěným úložištěm: každá instance vrací vždy jen výsledky vlastního tenanta a smazání instance odstraní její vyhledávací index, zatímco objekty v R2 zůstanou nedotčené.

Chcete-li si to vyzkoušet, spusťte npx wrangler dev a použijte stejný onboard, search, a offboard požadavky jako vestavěné úložiště. Krok s nahráváním přeskočte, protože AI Search indexuje objekty jednotlivých tenantů přímo z R2.

Možnost 2: Sdílená instance s filtrováním metadat při vyhledávání

Použijte jednu instanci AI Search a organizujte obsah podle tenanta pomocí cest ke složkám. Tento přístup funguje s oběma R2 buckety a vestavěné úložiště. Použijte filtry metadat v době dotazu, aby si každý tenant načítal pouze vlastní dokumenty.

Tato možnost prohledává existující instanci, proto si nejprve vytvořte instanci s názvem shared-instance a nejprve přidejte svůj obsah. Více informací najdete v Začínáme.

Přidejte vazbu instance do svého Konfigurační soubor Wrangler:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "ai_search": [
    {
      "binding": "SHARED_INSTANCE",
      "instance_name": "shared-instance",
      "remote": true
    }
  ]
}
[[ai_search]]
binding = "SHARED_INSTANCE"
instance_name = "shared-instance"
remote = true

Organizujte svůj obsah podle tenanta pomocí jedinečných cest ke složkám:

Aktualizujte src/index.ts a filtrujte podle složky tenanta při dotazu:

src/index.js
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 });
		}

		// Filter results to only return documents from this tenant's folder.
		const results = await env.SHARED_INSTANCE.search({
			messages: [{ role: "user", content: "When did I sign my agreement?" }],
			ai_search_options: {
				retrieval: {
					filters: {
						folder: { $gte: `${tenantId}/`, $lt: `${tenantId}0` },
					},
				},
			},
		});

		return Response.json(results);
	},
};
src/index.ts
export type Env = {
	SHARED_INSTANCE: AiSearchInstance;
};

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 });
		}

		// Filter results to only return documents from this tenant's folder.
		const results = await env.SHARED_INSTANCE.search({
			messages: [{ role: "user", content: "When did I sign my agreement?" }],
			ai_search_options: {
				retrieval: {
					filters: {
						folder: { $gte: `${tenantId}/`, $lt: `${tenantId}0` },
					},
				},
			},
		});

		return Response.json(results);
	},
} satisfies ExportedHandler<Env>;

Tento příklad používá filtr „starts with“ aby odpovídal všem souborům ve složce tenanta, včetně podsložek.

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

npx wrangler dev

Odešlete požadavek jako jednotlivý tenant. Worker omezí vyhledávání na složku daného tenanta, takže se výsledky nikdy nepřekrývají:

curl http://localhost:8787/ -H "x-tenant-id: customer-a"
curl http://localhost:8787/ -H "x-tenant-id: customer-b"

Nasadit

Přihlaste se pomocí svého účtu Cloudflare, poté nasaďte svůj Worker, aby byl dostupný na internetu:

npx wrangler login
npx wrangler deploy

Další kroky

Jmenné prostory

Seskupte instance a spravujte je dynamicky prostřednictvím vazby.

Filtrování

Filtrování výsledků vyhledávání podle atributů metadat v okamžiku dotazu.