← Обзор / ai-search / how-to
Мультитенантность
В мультитенантном приложении каждый тенант должен видеть только свои собственные данные. AI Search поддерживает два способа изоляции поиска по тенантам: выделить каждому тенанту отдельный экземпляр или использовать один общий экземпляр и фильтровать данные по тенанту во время запроса.
Выберите подход
| Подход | Как это изолирует | Выбирайте этот вариант, если |
|---|---|---|
| Экземпляр для каждого тенанта (рекомендуется) | Каждый тенант получает отдельный экземпляр с собственным хранилищем и индексом | Вам нужна строгая изоляция, либо вы создаёте и удаляете тенанты во время выполнения |
| Общий экземпляр с фильтрацией | Один экземпляр содержит всех тенантов; фильтр метаданных ограничивает каждый запрос | У вас много небольших тенантов, и вам нужна максимально простая настройка |
Предварительные требования
Оба подхода используют Cloudflare Worker. Сначала создайте проект, а затем следуйте выбранному варианту.
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.
Создайте проект Worker
Создайте новый проект Worker с помощью create-cloudflare CLI (C3). C3 ↗ это инструмент командной строки, который помогает настраивать и развёртывать новые приложения в Cloudflare.
Создайте новый проект с именем tenant-search выполнив:
npm create cloudflare@latest -- tenant-searchДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Перейдите в каталог вашего приложения:
cd tenant-searchВариант 1: Один экземпляр на тенанта
Это рекомендуется подход. Каждый тенант получает отдельный экземпляр с собственным хранилищем и поисковым индексом, поэтому один тенант никогда не сможет получить документы другого тенанта.
Во время выполнения создавайте для каждого тенанта изолированный экземпляр AI Search с помощью привязка пространства имён.
Добавьте привязку пространства имён в свой конфигурационный файл 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 опция позволяет wrangler dev перенаправляют запросы к вашим развёрнутым экземплярам, поскольку AI Search не выполняется локально.
Встроенное хранилище
Экземпляр тенанта содержит документы, которые вы загружаете в него напрямую, без внешнего источника данных.
Обновите src/index.ts. Этот Worker определяет тенанта по заголовку запроса, а затем создаёт, наполняет, ищет и удаляет экземпляр этого тенанта.
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 });
},
};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>;Запустите локальный сервер разработки:
npx wrangler devЗатем подключите тенанта, загрузите документ в его экземпляр, выполните поиск и отключите тенанта. Пример x-tenant-id заголовок привязывает каждый запрос к экземпляру этого тенанта:
# 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 индексирует загрузки асинхронно, поэтому перед поиском подождите немного после загрузки.
R2
Если данные ваших тенантов уже хранятся в R2, используйте R2 для хранения экземпляра каждого арендатора вместо загрузки документов во встроенное хранилище. Измените /onboard маршрут из встроенное хранилище пример создания экземпляра на основе R2. Способ настройки зависит от того, как организованы данные.
Один bucket на тенанта: если данные каждого тенанта уже находятся в отдельном bucket, укажите экземпляру на весь этот bucket:
const instance = await env.TENANTS.create({
id: `tenant-${tenantId}`,
type: "r2",
source: `tenant-${tenantId}-bucket`,
});Общий bucket: если данные всех тенантов находятся в одном bucket, организованном по папкам:
- my-bucket
- customers/
- acme/
- globex/
- customers/
Ограничьте каждый экземпляр папкой этого тенанта с помощью фильтрация по пути, поэтому он всегда индексирует и ищет только среди объектов этого арендатора:
const instance = await env.TENANTS.create({
id: `tenant-${tenantId}`,
type: "r2",
source: "my-bucket",
source_params: {
include_items: [`/customers/${tenantId}/**`],
},
});При любом из этих вариантов AI Search индексирует объекты каждого тенанта при следующем синхронизация. Добавляйте документы, записывая их в R2, а не загружая через Worker, и сохраните маршруты search и offboard из примера со встроенным хранилищем: каждый экземпляр всегда возвращает только результаты своего тенанта, а удаление экземпляра удаляет его поисковый индекс, но не затрагивает объекты в R2.
Чтобы попробовать, выполните npx wrangler dev и используйте тот же onboard, search, а также offboard запросы как встроенное хранилище. Пропустите шаг загрузки, поскольку AI Search индексирует объекты каждого тенанта напрямую из R2.
Вариант 2: Общий экземпляр с фильтрацией по метаданным при поиске
Используйте один экземпляр AI Search и организуйте контент по тенантам с помощью путей к папкам. Такой подход работает как с Бакеты R2 и встроенное хранилище. Примените фильтры метаданных во время запроса, чтобы каждый арендатор получал только свои собственные документы.
Этот вариант выполняет поиск в существующем экземпляре, поэтому сначала создайте экземпляр с именем shared-instance и сначала добавьте свой контент. См. Начало работы.
Добавьте привязку экземпляра в свой конфигурационный файл 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Организуйте свой контент по тенантам с помощью уникальных путей к папкам:
- customer-a
- logs/
- contracts/
- customer-b
- contracts/
Обновите src/index.ts и фильтровать по папке тенанта во время запроса:
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);
},
};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>;В этом примере используется фильтр «starts with» чтобы соответствовать всем файлам в папке тенанта, включая вложенные папки.
Запустите локальный сервер разработки:
npx wrangler devОтправляйте запрос от имени каждого тенанта. Worker ограничивает поиск папкой этого тенанта, поэтому результаты никогда не пересекаются:
curl http://localhost:8787/ -H "x-tenant-id: customer-a"
curl http://localhost:8787/ -H "x-tenant-id: customer-b"Развернуть
Войдите в свой аккаунт Cloudflare, затем разверните Worker, чтобы сделать его доступным в интернете:
npx wrangler login
npx wrangler deploy