Галоўная / Артыкулы

Шматкліенцкі RAG на Amazon S3 Vectors: канвеер

Шматкліенцкі RAG на Amazon S3 Vectors: адзін індэкс на кліента, абмежаваныя ўліковыя даныя STS, выдаленне па ключы, аўдыт CloudTrail і код ізаляцыі, які застаецца на вас.

Выдавец: REACTAPP.TOP11 хв чытання2524 слоў

Крыніцы: Multi-Tenant RAG on Amazon S3 Vectors - Part 3: The Pipeline

Даўчы кожнаму арэндніку службы RAG свой сабскайны рэсурс, замест аб’еднанага індэкса з тагам арэндніка, AWS забезпечвае ізоляцыю, якую можа пераканацца аўдытар кліента. Структура такая: кожна вімога супараднавана з вызовам API, правілам або рэкордам у логах, пасля чаго йду тыя часткі, якія застаюцца ў кодзе прыемлівача. Асновная ідэя — ізоляцыя належыць до шару зберагача, а не да клазу WHERE tenant = ?, пра які хтось можа забыць.

Макет

Адна пасобна счэта AWS мае адну кантэўню вектараў на кожны обслужваны рэгіон, і ў кожной кантэўні знаходзіцца адзін індэкс на кожнага арэндніка. Указаны фрагменты — гэта ілюстратыўны код на TypeScript, які выкарыстоўвае @aws-sdk/client-s3vectors і @aws-sdk/client-bedrock-runtime; таму пераканайцеся, што назвы параметраў супадаюць з назвамі ў чынным SDK.

Ingest:  S3 (tenant prefix) ──▶ Lambda: extract + chunk ──▶ SQS ──▶ Lambda: embed (Bedrock Titan V2)
                                                                     └──▶ PutVectors → index/org-<tenant>
Query:   eID provider (OIDC) ──▶ API authorizer (tenant, role) ──▶ STS session policy (one index ARN)
                        ──▶ embed question ──▶ QueryVectors(index/org-<tenant>, filter=classification)
                        ──▶ LLM, grounded on returned chunk_text, citations = document_id + version
Audit:   CloudTrail data events, resource type AWS::S3Vectors::Index, queried by resources.ARN

Спяшчаны каталог рэгуляцый (законы, правілы, рэкамендаціі) копіюецца ў кожны індэкс арэндніка. Йго копіюванне коштае некалькі, і гэта дапамагае захаваць даны кожнага арэндніка як самастоятэльныя.

Стварэнне індэкса

Працэс прадачы арэндніка адбываецца за адні календарны момент, пры чым прымуецца два незворачныя рашэнні: размер усадкі і спіс ключоў метададзей, якія не можна фільтрацыяваць.

import { S3VectorsClient, CreateIndexCommand } from "@aws-sdk/client-s3vectors";
const s3v = new S3VectorsClient({ region: "eu-west-1" });
await s3v.send(new CreateIndexCommand({
  vectorBucketName: "kb-eu-west-1",      // the bucket is regional → residency
  indexName: "org-b",                       // the index is the tenant → isolation
  dataType: "float32",
  dimension: 1024,                          // Titan Text Embeddings V2, 1024-d
  distanceMetric: "cosine",
  metadataConfiguration: {
    nonFilterableMetadataKeys: ["chunk_text"],   // returned, never scanned, never filterable
  },
}));

Новы ўказальнік атрымвае ARN, які складаецца з назвы службы, рэгіону (eu-west-1), абяцкі, кантэйнера kb-eu-west-1 і, нарэшце, index/org-b. Рэгіон прымешчаецца безпосерэдна ў гэты стрык, таму перыякі ўказальнікі кожнага кантэйнера дае вам адчынны звярнік пра месца размешчэння дадзэйна.

Працэўнае перадача дадзэйна: самі выберыце ключы

Сярод усьго, што ўключаецца ў працэс, выбор вектарнага ключа мае найбольшое значэнне. Калі дазволіць хранілышу самаму ствараць ключы, то заповедзь «адчыніць гэты дакумент» ператвараецца на пошук яго частак, пасля чаго адчынаюцца тыя, якія знайдзены, а апрыксіматычны пошук ніколі не можа падтвердзіць, што нічога не засталося. Ключ следует вывадзіць з ідэнтыфікатора дакумента, а адчыненне стае простым пошукам.

import { PutVectorsCommand } from "@aws-sdk/client-s3vectors";
async function ingestDocument(tenant: string, doc: Document, chunks: string[]) {
  const vectors = await Promise.all(chunks.map(async (text, i) => ({
    key: `${doc.id}-k${String(i + 1).padStart(3, "0")}`,      // chosen by us
    data: { float32: await embed(text) },
    metadata: {
      document_id: doc.id,                 // filterable
      version: doc.version,                // filterable → appears on the citation
      classification: doc.classification, // filterable → role filter at query time
      chunk_text: text,                    // non-filterable → rides with the vector
    },
  })));  for (const batch of chunk(vectors, 500)) {                    // ≤ 500 per call
    await s3v.send(new PutVectorsCommand({
      vectorBucketName: "kb-eu-west-1", indexName: `org-${tenant}`, vectors: batch,
    }));
  }
  await manifest.put(tenant, doc.id, doc.version, vectors.map(v => v.key));   // written down
}

Зверніце ўвагу, што не паказваюць метаданыя: арэндніка. У жаднай частцы няма напісу "org-b". Частка знаходзіцца ў org-b, і самэ гэта робіць яе B-ю. Дадзенне таксонаму арэндніка таксама прынесла б знову слабасі дызайнаў, базаваных на таксонах, дзе некарэктны таксон выглядае абавесці як правільны, без жадных пераваг.

PutVectors дзеянні ёсць абсалютна надзеяныя, і кожны вызов платаецца як мінімум за 128 KB, таму групаваць дзеянні трэба ў пакеты.

Шчодо вбудоввання, для полноты картыны:

import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
const bedrock = new BedrockRuntimeClient({ region: "eu-west-1" });
async function embed(text: string): Promise<number[]> {
  const res = await bedrock.send(new InvokeModelCommand({
    modelId: "amazon.titan-embed-text-v2:0",
    contentType: "application/json",
    body: JSON.stringify({ inputText: text, dimensions: 1024, normalize: true }),
  }));
  return JSON.parse(new TextDecoder().decode(res.body)).embedding;
}

Вычысленнія адбываюцца ў рэгіоне, які вы выбіраеце, і Bedrock не зберагае запітоў і не выкарыстоўвае іх для навчання, што ёсць часткай концэпціі рэзідэнціі.

Запыты з крантэдынямі, якія называюць адзін індэкс

У такой структуры межы тэнанта не выражаюцца ў запытэ як фільтр. Гэта ўсё тое месца, куды направляецца запыт, едынственная мета, яку можна ачысці за дапамогою крантэдыняў вызывача. API стварае новыя крантэдынія для кожнага запыту.

import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
function indexArn(tenant: string) {
  return `arn:aws:s3vectors:eu-west-1:${ACCOUNT}:bucket/kb-eu-west-1/index/org-${tenant}`;
}async function tenantCredentials(tenant: string) {
  const res = await sts.send(new AssumeRoleCommand({
    RoleArn: QUERY_ROLE_ARN,                       // broad role
    RoleSessionName: `q-${tenant}`,
    DurationSeconds: 900,
    Policy: JSON.stringify({                       // session policy: intersection, never a widening
      Version: "2012-10-17",
      Statement: [{
        Effect: "Allow",
        Action: ["s3vectors:QueryVectors", "s3vectors:GetVectors"],
        Resource: indexArn(tenant),                // exactly one ARN
      }],
    }),
  }));
  return res.Credentials!;
}

Палітры сесій можа толькі скасаваць павелічэнні праваў ролі, але ніколі не можа даць праваў, якія выходзяць за межы гэтай ролі. Кранцыдэнціі застаюцца чыннымі калькі хвілін і ніколі не пакідаюць API. У масштабных сістэмах кантроль даступу на аднойчыні з атрыбутамі досягае таго ж рэзультата за дапамогою тага ў індэксе, які паўтараецца з тагам прынцыпала, таму палітры не растуць разам з спісам тэнантаў.

Далей йдзе сама запытка, пры чым кантроль унутрана ў тэнанце выражаецца як фільтр:

import { QueryVectorsCommand } from "@aws-sdk/client-s3vectors";
async function retrieve(tenant: string, role: Role, question: string) {
  const client = new S3VectorsClient({ region: "eu-west-1", credentials: await tenantCredentials(tenant) });
  const res = await client.send(new QueryVectorsCommand({
    indexArn: indexArn(tenant),
    queryVector: { float32: await embed(question) },
    topK: 10,
    filter: { classification: { $in: allowedClassifications(role) } },   // evaluated during search
    returnMetadata: true,
    returnDistance: true,
  }));
  return res.vectors ?? [];    // each: key, distance, metadata incl. chunk_text
}

Там ўсьмо два межы і два механізмы. IAM на ARN выявляе, да каго тэнэнту можна дасягнуць. Фільтр метаданых, які прыменяецца пад час самага пошуку, а не пасля яго, выбірае, якую класыфікацыю будзе паказана ў тым тэнэнце. Фільтрацыя пасля пошуку, тыяму, калі запрашваюцца дзесяць рэзультатаў і адкідаецца тое, што корыстнік не можа бачыць, таямна паслабляе можлівасць адзыскання інфармацыі для ролей з обмежэнымымы правамі, і ніколі пра гэта не сказвае. Фільтрацыя пад час пошуку вяртае дзесяць рэзультатаў, якія корыстнік мае право бачыць. Дазволеныя класыфікацыі вырахоўваныя на адной падставе твэрджэнняў автарызатора, а не на адной падставе тэла запіту.

Апавяданні прычын і цітаты не ўключаюцца ў фармат адпаведзі. Модэлю паказваецца толькі chunk_text з вярнуцых вектараў, а кожная цітата — гэта ідэнтыфікатор дакумента і версія, якія чытаныя з тых сабе метаданых. Тое, што цітуецца, — гэта абоўсумо тое, што прачытаў модэль, адтолькі што гэтыя даныя ўскладненыя з тых сабе байтов.

Намерны баг

Існуе тэст, які разлічвае дызайн, у яком прыемнік контролюе межы, ад дызайну, у яком гэта робіць платформа. Наведзіце прыемнік на некарэктны індэкс, калі ён зберагае кранцыпты, прызначаныя конкрэтнаму корыстніку:

const client = new S3VectorsClient({ region: "eu-west-1", credentials: await tenantCredentials("b") });
await client.send(new QueryVectorsCommand({ indexArn: indexArn("a"), queryVector: { float32: q }, topK: 10 }));
// → AccessDeniedException: User ... is not authorized to perform: s3vectors:QueryVectors on resource: .../index/org-a

Нічага не вяртаецца. У дыяграме, якая выкарыстоўвае фільтрацыю за прыкладаннямі, той самы баг мог бы вярнуць даныя A з кодам 200. Заставіце гэта як тэст інтэграцыі, адколькі гэта самы просты доказ таго, што проблема не ў вашам кодзе. Адхілены вызов таксама прадстаўляецца ў журнале аўдыту з errorCode: AccessDenied для index/org-a.

Выдаленне па ключу, перакананне па ключу

Выдаленне адбываецца на адной з мэтадаў, запісаных пад час запрашэння, і ў гэтым працэсе не выкарыстоўваецца пошук.

import { DeleteVectorsCommand, GetVectorsCommand } from "@aws-sdk/client-s3vectors";
async function eraseDocument(tenant: string, docId: string, version: number) {
  const keys = await manifest.get(tenant, docId, version);
  for (const batch of chunk(keys, 500)) {
    await s3v.send(new DeleteVectorsCommand({ vectorBucketName: "kb-eu-west-1", indexName: `org-${tenant}`, keys: batch }));
  }
  // verify — strongly consistent, so this is valid immediately
  for (const batch of chunk(keys, 100)) {
    const res = await s3v.send(new GetVectorsCommand({ vectorBucketName: "kb-eu-west-1", indexName: `org-${tenant}`, keys: batch }));
    if ((res.vectors ?? []).length) throw new Error(`erasure incomplete: ${res.vectors!.length} keys remain`);
  }
  await audit.record({ tenant, docId, version, keyCount: keys.length, verifiedAt: new Date() });
}

Аўдытар можа парадзець выкананне другога цыклу за дапамою сваіх толькі для чытання крантэсаў, якія абмежаны ARN індэкса таго самага тэнанта. Заклеканне ў тым жа фармаце, толькі прынцыпал разны, а рэзультат прыходзіць з AWS, а не з скрыншота, які надае прадавець. Выклеканне GetVectors для індэкса іншага тэнанта вяртае AccessDenied, што зноў являе сабою тэст ізоляцыі.

Ёжыдневнае выключэнне цэлага тэнанта адбываецца проста за дапамою DeleteIndex. Две прыметы для керавання: выдалены вектар негайна ўступае ў скарынку рэзультатаў запыту, хоць ён можа яшчэ спадзявацца на урахоўванне ў размерзе індэкса, а значыць і ў расчытках за сканаванне, прыблізна прыблізна на дзень. Таксама, калі прыймаецца новая версія дакумента, ключы пакананай версіі трэба выдаліць па тым жа шляху, а не за дапамою аддзельнай задачы на чысткаванне.

Аўдыт: Падзеі дадзэнняў CloudTrail па ARN

Стварэнне і выдаленне індэксаў (CreateIndex, DeleteIndex) ўзначаюць кантрольныя запісы, і CloudTrail зазвычай іх фіксуе. Запісы ад окольных запытанняў ўзначаюць дадзенневыя запісы: іх трэба увімкнуць, і за кожны з іх ставіцца плата. Калі прыхіліцца да іншага падходу, будзь-яя прэтэнзія да можлівасці аўдыту будзе безаснованай.

// CDK
new cloudtrail.Trail(this, "Trail", { sendToCloudWatchLogs: false })
  .addEventSelector(cloudtrail.DataResourceType.S3_VECTORS_INDEX, [
    `arn:aws:s3vectors:eu-west-1:${account}:bucket/kb-eu-west-1/index/*`,
  ]);
// Check the CDK enum/name for the S3 Vectors index resource type; the underlying resource type is AWS::S3Vectors::Index.

Падзея запытку, адчысленая:

{
  "eventSource": "s3vectors.amazonaws.com",
  "eventName": "QueryVectors",
  "eventTime": "2026-09-09T10:41:07Z",
  "userIdentity": { "type": "AssumedRole", "arn": "arn:aws:sts::123456789012:assumed-role/kb-query/q-b" },
  "resources": [{ "type": "AWS::S3Vectors::Index",
                  "ARN": "arn:aws:s3vectors:eu-west-1:123456789012:bucket/kb-eu-west-1/index/org-b" }],
  "errorCode": null
}

„Хто чытаў даныя арэндніка А?“ стае аднам з прадыкатаў у CloudTrail Lake або Athena:

SELECT eventTime, eventName, userIdentity.arn, errorCode
FROM   cloudtrail_events
WHERE  eventSource = 's3vectors.amazonaws.com'
  AND  element_at(resources, 1).arn LIKE '%/index/org-a'
ORDER  BY eventTime DESC;

AWS стварае запісь як толькі надходзіць вызов, раней за працэўнік аплікацыі, і аператар платформы не можа яе зменіць. Калі даць аўдытару права на чытанне этых запісей або экспортавацію падзей гэтага корыстніка, ён можа сам адпавесці на запытанне. Самэ гэта і змінюеяцца ў вакуменцы аўдыту: аўдытар перакананы, і нікому не трэба верыць словам постаўшчыка.

Чатыры дадатковыя элементы, якія падходзяць да модэлю

Одна кантрольная канцэферыя KMS на кожны індэкс. Кантэйнеры Vector можна вікорыстоўваць з SSE-KMS, таму індэкс кожнага корыстніка можа быць зашыфраваны кантрольной канцэферыяй, карыстоўванай самым корыстнікам і прызначанай толькі яму. Кожная кантрольная канцэферыя мае ARN і рэгіон, што ўжо ўтварае другі элемент рэзідэнціі. Калі яе выключыць, усё, што зашыфравана яе дапамогай, застанецца недачытальным, укладаючы ў сябе і копіі, што ўсё строгае, чым можа зробіць DeleteIndex сама по сабе.

Экспорт. ListVectors можа трансляваць цэлы індэкс, укладаючы ў сябе таксама тэкст, які нельга фільтраваць, праз да 16 паралельных каналаў. За дапамогою атрыбутаў, прызначаных для конкрэтнага ARN, аб’ект можа запрацаваць з усім своім базам дакументаў. Гэта задовольняе трэбаванні статты 20 GDPR па перэноснасці дадзеных і пазбавляе аб’екта залежнасці ад паводзкаў проста за адні выклік.

Прыватны маршрут сеткі. Надаюць можлівасць адправляць трафік S3 Vectors праз канцэнтры VPC, так што запыты не праходзяць праз публічны Інтэрнэт. Спеціялісты па безпецы ў аб’ектах, якія падпадаюць пад нормы NIS2 або секторальныя стандарты безпекі, будуць прасіць самэ гэта.

Косцы за аб’ект. За дапамогою тагоў для распадзелу косцоў на кожны індэкс, слой вектараў паказваецца ў счытку AWS за кожны аб’ект як окрэмы пункт, а не як апрыбутаваная оцэнка. Тыя ж тагі дазволяюць выкарыстоўваць фармат ABAC для правіл сесій.

Што не рашаея граніцы індэкса

Усі вышэпераказаныя адказы стосуюцца таго, якія даныя можа адразуваць кожны арэнднік, і AWS контролюе гэта. Наступныя элементы не падпадаюць пад гэтыя правіла, і самэўсёлі большая частка часу, выдзейванага на інжынерную працу, была спрамована на ўсё гэта.

Ролі ўнутры арэндніка. Фільтр класыфікацыі карыстаецца для гэтага. Важныя два правілы: фільтр трэба застаўляць у самай запытцы, а не пасля яе, а ранг ролі трэба браць з апраўнікавача, а не з кліента.

Адыянац экзістэнціі. Команды гасударственага сектара сталкваліцца з такім спосабам неяксанасці, які іншыя рэдка кантрактуе: модэль не павінна нават паведамляць пра наявнасць дакумента з обмежэным доступам, адколькі фраза «Я не можу паказаць вам атлас благослужэння дзецяў для гэтай сям’і» сама па сабе ўжо ўважаецца адкрыццю інфармацыі. Таму пры выкарыстоўванні механізма адыянацу ніколі не вяртаецца частка дакумента, якую корыстувач не можа паказаць, што гарантуецца фільтрам, а запіт ніколі не натыкаецца на натыякі пра тое, што ўтрымваецца. Модэль адпавядае на аснове таго, што у ёй ёсць, алеўжо і адзначае, што у ёй нічога няма. Гэта кантролюецца пры адыянацы, а не пры запыце, таму што адыянац можна перапрацаваць, а формулюванні модэлі — нельга.

Сэмплярнаадосць між мовамі. Корпус, які сумяшвае местную мову з англійскім прававым тэкстам, паказвае значная разліка ў рэйтынгах, якія дае адзін мадэль абсорбавання, для абзацоў, якія ў роўнай меры ўзаемна адносуюцца. Фіксаваны косайны порог, налаштаваны для адной мовы, тады пасабляе хорашыя рэзультаты для іншай мовы без жадных паведамленняў. Рашэннем было замена абсалютнага порога на вікно, якое залежыць ад найлепшага падабення ў кожным наборэ рэзультатаў. Багатомовныя корыстувальнікі должны спачатку вымерыць гэта, таму што правільная значэнне залежыць ад корпуса.

Няма пошуку па ключоўым словам. Ідэнтыфікаторы, такія як номеры раздзелаў або ідэнтыфікаторы справ, должны быць у фільтруемых метаданых і падбірацца абоўсюды, а не абсорбавацца і залічвацца на вялікай вялічыні. Калі трэба справжні пошук па ключоўым словам, трэба выкорыстаць OpenSearch на першым месцы, а S3 Vectors — як двігун для зберагачвання.

Грані частакаў. Найменш прыгожыя нараджэнні: два тыжні, пасвячаныя выявленню таго, дзе дакументы дзелююцца, павысілі якасць адпаведзей больш, чым месцы, пасвячаныя розрабоце архітэктуры пошуку. Частакі трэба ствараць на адзынце структуры дакумента, тыябо ў відэленах і пунктах, а не толькі на адзынце колькасці токэнаў.

Змена модэлю эмбедынгу. Вымір зафіксаваны пад час стварэння індэкса, таму змена модэля значыць стварэнне новага індэкса для кожнага корыстувальніка. Перасчысціце эмбедынгі з асловых дакументаў, якія яшчэ знаходзяцца ў S3, змяніце настройкі, а пасля запрацаваце DeleteIndex для старога індэкса. Завдзяк структуры па корыстувальніках гэта выконваецца паэтапна, а сама процедура выдалення паказвае, што старая копія знікла.

Базы знанняў Bedrock. Яны можу зберагаць вектары ў S3 Vectors і ўтвараюць надзеяны стандарт. Адкрыцыя сасобаў для самастойнага запоўнення мае сэнс толькі з аднай прычыны: базы знанняў самыя выбіраюць ключы вектараў, а апісаны раней доказ вымышлення залежыць ад таго, чы вы іх самі выберазе.

Паўтарны аналіз трэбаванняў

Пяць з шасціх трэбаванняў вынікаюць з таго, дзе знаходзяцца даны і хто можа да яных даступіцца, а не з коду, які напісала команда. Гэта і была мета, і самэ гэта дозволяе безбедна запрасіць такую платформу тым, хто не даверае постаўчыку.

Код тут мае ілюстратыўны характэр. Парашчыруйце яго з актуальным AWS SDK v3 і CDK, адтолькі ўказаныя здарэнні CloudTrail ёсць скрацаныя, і пераканайцеся ў падтрымцы канцэнтраў VPC і пазначэнні індэксаў для S3 Vectors у найновейшай дакументацыі. Тэнанты, дакументы, рэгіоны і ідэнтыфікаторы аблікав ёсць місцямі для замены, і тут не паказваецца жадных обсягаў для працы чы праграм для ацэнкі.

Супаўязаныя матэрыялы

  • Прыем дакументаў з урахоўваннем якосці RAG: кераванне дакументамі па ўжыванасці — Дэкларатыўны плэйс-хоўд дакументаў, які ацэнюе язык, дуплікаты і ужыванасць за шкалай Флеша, а пасля адправляе дакументы да двух індэксаў OpenSearch для пошуку калектараў і распрацоўшчыкаў.
  • Ізоляцыя тэнантаў для RAG на Amazon S3 Vectors: чаму важліваў ARN — Чаму індэкс для кожнага тэнанта з яго сабойственным ARN лепшы за фільтры метадаў для ізоляцыі, рэзідэнціі, выдалення, аўдыту і вярнавання па стандартам GDPR у RAG на Amazon S3 Vectors.