← Обзор / ai-search / how-to
Поиск сразу по нескольким экземплярам
AI Search может опрашивать несколько экземпляров в одном запросе, объединять результаты и помечать каждый результат экземпляром, из которого он получен. В этом руководстве такой подход используется для совместного поиска по двум базам знаний: общей Общие база знаний, доступная для поиска каждому пользователю, а также относящийся к конкретному тенанту база знаний, содержащая приватный контент одного клиента. Один запрос возвращает релевантный контент из обеих сразу.
Чтобы изолировать тенантов, рекомендуется хранить содержимое каждого из них в отдельном экземпляре (см. Мультитенантная изоляция поиска). Поиск сразу по нескольким экземплярам позволяет объединить экземпляр арендатора с общим содержимым прямо во время запроса, поэтому копировать общее содержимое в экземпляр каждого арендатора не требуется.
Что вы создадите: Worker, который выполняет поиск в общем general-knowledge экземпляр и экземпляр отдельного тенанта вместе, а затем группирует результаты по источнику.
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
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Для настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Перейдите в каталог вашего приложения:
cd multi-source-search2. Настройте 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 мог группировать результаты по источнику.
// 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.
}
}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