Startseite / Artikel

Aufbau eines Multi-Tenant RAG auf Amazon S3 Vectors: Der Pipeline-Prozess

Multi-tenant RAG auf Amazon S3 Vectors: ein Index pro Mieter, auf den Anforderungen zugeschnittene STS-Zertifikate, Löschung über Schlüssel, CloudTrail-Auditing sowie der von Ihnen weiterhin gehaltene Isolierungscode.

Verlag: REACTAPP.TOP11 Min. Lesezeit2524 Wörter

Quellen: Multi-Tenant RAG on Amazon S3 Vectors - Part 3: The Pipeline

Durch die Bereitstellung eines eigenen, adressierbaren Ressourcens für jeden Mieter eines RAG-Dienstes anstelle eines gemeinsamen Index mit einem Mieter-Tag wird die Isolierung zu etwas, das AWS durchsetzt und das von einem Prüfer des Kunden überprüft werden kann. So sieht die Struktur aus: Jede Anforderung wird einer API-Aufruf, einer Richtlinie oder einer Protokollinze zugeordnet, gefolgt von den Teilen, die weiterhin im Anwendungscode enthalten sind. Die Grundidee ist, dass die Isolierung zur Speicherschicht gehört und nicht zu einer WHERE tenant = ?-Klausel, die jemand vergessen könnte.

Layout

Ein AWS-Konto enthält einen Vector-Bucket pro bedienter Region, und innerhalb jedes Buckets befindet sich ein Index pro Mieter. Die Beispiele sind veranschaulichende TypeScript-Codebeispiele, die @aws-sdk/client-s3vectors und @aws-sdk/client-bedrock-runtime verwenden; überprüfen Sie daher die Parameternamen im Einklang mit der aktuellen SDK-Version.

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

Das gemeinsame Regulierungsverzeichnis (Gesetze, Vorschriften, Leitlinien) wird in jeden Mieterindex kopiert. Die Duplikation kostet wenig und sorgt dafür, dass die Daten jedes Mieters eigenständig bleiben.

Index erstellen

Die Onboarding-Prozedur für einen Mieter erfolgt durch einen einzigen Anruf, bei dem zwei unwiderrufliche Entscheidungen getroffen werden: die Eingebettungsdimension sowie die Liste der nicht filterbaren Metadaten-Schlüssel.

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

Der neue Index erhält einen ARN, der aus dem Service-Name, der Region (eu-west-1), dem Konto, dem Bucket kb-eu-west-1 sowie schließlich index/org-b gebildet wird. Die Region ist direkt in der Zeichenkette enthalten, was bedeutet, dass eine Auflistung der Indexe jedes Buckets kostenlos einen Bericht zur Datenspeicherung liefert.

Einlesen: Wählen Sie die Schlüssel selbst aus

Von allem im Pipeline ist die Wahl des Vektor-Schlüssels am wichtigsten. Lassen Sie den Speicher die Schlüssel erfinden – dann wird „Dieses Dokument löschen“ zu einer Suche nach seinen Teilen, gefolgt vom Löschen dessen, was die Suche findet – und eine annähernde Suche kann niemals beweisen, dass nichts zurückbleibt. Erleiten Sie den Schlüssel aus der Dokument-ID, dann ist das Löschen eine einfache Abfrage.

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
}

Achten Sie darauf, was die Metadaten weglassen: den Mieter. In dem Chunk steht nichts über „org-b“. Der Chunk befindet sich in org-b, und das ist es, was ihn zu B’s macht. Wenn man auch einen Mieter-Tag hinzufügen würde, käme man wieder zu den Schwächen von tagbasierten Designs, bei denen ein falscher Tag genauso aussieht wie ein richtiger, ohne jeden Vorteil.

Die Schreibvorgänge von PutVectors sind stark konsistent, und jeder Aufruf wird mit mindestens 128 KB abgerechnet, daher sollten Schreibvorgänge in Gruppen zusammengefasst werden.

Zur Vollständigkeit: Embedding.

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

Die Inferenzläufe finden in der von Ihnen bezeichneten Region statt, und Bedrock speichert keine Anfragen oder verwendet sie nicht zur Schulung, was zum Konzept der Residenz gehört.

Abfragen mit Zugangsdaten, die einen Index angeben

In dieser Architektur befindet sich die Grenze des Mieters nicht als Filter in der Abfrage. Es handelt sich dabei um das Ziel der Abfrage – das einzige Ziel, das mit den Zugangsdaten des Anfragenden geöffnet werden darf. Die API erstellt für jede Anfrage neue Zugangsdaten.

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

Eine Sitzungspolitik kann nur eine Rolle einengen und niemals darüber hinaus Berechtigungen gewähren. Die Anmeldeinformationen gelten für einige Minuten und verlassen niemals die API. In großem Maßstab erreicht die auf Attributen basierende Zugriffskontrolle dasselbe, indem ein Tag im Index mit einem Principal-Tag abgeglichen wird, sodass die Politik nicht mit der Liste der Mieter wächst.

Danach kommt die Abfrage selbst, wobei die Kontrolle innerhalb eines Mieters als Filter dargestellt wird:

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
}

Es gibt also zwei Grenzen und zwei Mechanismen. IAM am ARN entscheidet, auf welchen Tenant zugegriffen werden kann. Ein Metadatenfilter, der direkt im Suchvorgang angewendet wird und nicht danach, bestimmt, welche Klassifizierung der Benutzer innerhalb dieses Tenants sieht. Das Nachträgliche Filtern, also das Herunterladen von zehn Ergebnissen und das Wegwerfen dessen, was der Benutzer nicht sehen darf, zerstört heimlich die Treffgenauigkeit für eingeschränkte Rollen, ohne dass dies angegeben wird. Das Filtern im Suchvorgang liefert hingegen zehn Ergebnisse, die der Benutzer sehen darf. Die zulässigen Klassifizierungen werden aus den Angaben des Autorisators berechnet und niemals aus dem Anfragekörper.

Belege und Zitate fallen aus der Struktur der Antwort heraus. Dem Modell werden nur die chunk_text-Werte der zurückgegebenen Vektoren angezeigt, und jeder Beleg ist die Dokument-ID sowie die Version, die aus denselben Metadaten abgerufen werden. Was zitiert wird, ist genau das, was das Modell gelesen hat, da es sich um dieselben Bytes handelt.

Der absichtliche Fehler

Es gibt einen Test, der einen Entwurf unterscheidet, bei dem die Anwendung die Grenze definiert, von einem, bei dem die Plattform dies tut. Weisen Sie die Anwendung auf den falschen Index, während sie Benutzerdaten im tenantbezogenen Umfang verwendet:

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

Nichts kehrt zurück. In einem Design, das auf Anwendungsfiltern beruht, würde derselbe Fehler die Daten von A mit einem Status 200 zurückgeben. Behalten Sie dies als Integrationstest bei, da es der günstigste Beweis dafür ist, dass das Problem nicht in Ihrem Code liegt. Der abgelehnte Aufruf erscheint außerdem im Audit-Log mit errorCode: AccessDenied bezüglich index/org-a.

Löschung nach Schlüssel, Überprüfung durch Schlüssel

Die Löschung verwendet das bei der Eingabe erstellte Manifest, wobei keine Suche stattfindet.

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

Ein Prüfer kann die zweite Schleife mit eigenen Leserechten ausführen, die auf den Index-ARN dieses Mieters beschränkt sind. Der Aufruf ist identisch, nur der Ausführende unterscheidet sich, und das Ergebnis stammt von AWS statt von einem von dem Anbieter bereitgestellten Screenshot. Der Aufruf von GetVectors auf dem Index eines anderen Mieters führt zu AccessDenied, was erneut den Isolierungs-Test darstellt.

Um einen ganzen Mieter zu löschen, genügt DeleteIndex. Zwei betriebliche Hinweise: Ein gelöschter Vektor fällt sofort aus den Abfrageresultaten heraus, kann jedoch weiterhin zur Größe des Indexes sowie zur Berechnung der Scannkosten beitragen – und zwar bis zu einem Tag lang. Außerdem sollten bei der Einpflege einer neuen Dokumentversion die Schlüssel der vorherigen Version über denselben Weg entfernt werden und nicht über eine separate Reinigungsaktion.

Prüfung: CloudTrail-Datenereignisse nach ARN

Die Erstellung und Löschung von Indizes (CreateIndex, DeleteIndex) sind Verwaltungsereignisse, die CloudTrail standardmäßig aufzeichnet. Aufzeichnungen einzelner Abfragen sind Datenergebnisse: Sie müssen aktiviert werden, und jede davon wird abgerechnet. Überspringt man diesen Schritt, ist jede Behauptung einer Prüfbarkeit bedeutungslos.

// 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.

Ein Abfragen-Event, gekürzt:

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

„Wer hat die Daten von Mieter A gelesen?“ wird zu einem Prädikat in CloudTrail Lake oder 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 erzeugt den Eintrag bereits im Moment des Eintreffens der Anfrage, noch vor dem eigenen Handler der Anwendung, und der Plattformbetreiber hat keine Möglichkeit, ihn zu ändern. Wenn man dem Prüfer Lesezugriff auf die Protokolle gewährt oder die Ereignisse dieses Mieters exportiert, kann er die Frage ohne Hilfe beantworten. Das ist es, was sich für die Prüfanforderungen ändert: Der Prüfer überprüft selbst, und niemand muss dem Anbieter Glauben schenken.

Vier Ergänzungen, die zum Modell passen

Eine KMS-Schlüssel pro Index. Vector-Buckets können SSE-KMS verwenden, sodass der Index jedes Mieters mit einem vom Kunden verwalteten Schlüssel verschlüsselt werden kann, der ausschließlich diesem Mieter zugewiesen ist. Jeder Schlüssel weist einen ARN sowie eine Region auf, was ein weiteres Merkmal der Standortbezogenheit darstellt. Wenn diese Funktion deaktiviert wird, bleibt alles darunter unlesbar verschlüsselt – einschließlich der Kopien –, was eine strengere Löschung darstellt als das, was DeleteIndex allein bietet.

Export. ListVectors kann einen gesamten Index, einschließlich nicht filterbarer Texte, über bis zu 16 parallele Segmente streamen. Mit Zugangsdaten, die auf seinen eigenen ARN beschränkt sind, kann ein Mieter seine vollständige Wissensbasis mitnehmen. Dies erfüllt die Anforderungen an Portabilität gemäß Artikel 20 der DSGVO und beseitigt durch einen einzigen Aufruf die Abhängigkeit.

Ein privater Netzwerkpfad. Senden Sie den S3 Vectors-Datenverkehr über einen VPC-Endpunkt, damit Abfragen nicht über das öffentliche Internet laufen. Sicherheitsbeauftragte bei Mietern, die NIS2 oder einen Branchensicherheitscode unterliegen, werden genau darum bitten.

Kosten pro Mieter. Durch Kostenzuordnungs-Tags für jeden Index erscheint die Vektor-Schicht in der AWS-Rechnung als eigene Posten pro Mieter und nicht als geschätzte Wertangabe. Dasselbe System ermöglicht auch die ABAC-basierte Gestaltung der Sitzungspolitik.

Was die Indexgrenze nicht löst

Alle oben genannten Antworten beziehen sich darauf, welcher Mieter auf welche Daten zugreifen kann, und AWS sorgt dafür. Die folgenden Punkte fallen nicht unter diese Grenze, und dort wurde der größte Teil der Entwicklungszeit tatsächlich investiert.

Rollen innerhalb eines Mieters. Der Klassifizierungsfilter deckt dies ab. Zwei Regeln sind wichtig: Der Filter muss innerhalb der Abfrage angewendet werden und nicht danach, und die Rangstufe der Rolle muss vom Autorisierer und niemals vom Client übernommen werden.

Existenzleckage. Teams im öffentlichen Sektor stoßen auf einen Fehlermodus, den andere nur selten erleben: Das Modell darf nicht einmal offenbaren, dass ein eingeschränktes Dokument existiert, denn der Satz „Ich kann Ihnen den Bericht zum Wohlergehen des Kindes dieser Familie nicht zeigen“ ist bereits eine Offenlegung an sich. Daher gibt die Abruffunktion niemals einen Teil zurück, den der Benutzer nicht sehen kann – was durch den Filter gewährleistet wird –, und die Anfrage deutet niemals darauf hin, dass etwas zurückgehalten wird. Das Modell antwortet mit dem, was es hat, oder gibt zu, nichts zu haben. Dies liegt bei der Abruffunktion und nicht bei der Anfrage, weil die Abruffunktion getestet werden kann, während die Formulierung des Modells nicht getestet werden kann.

Zwischensprachliche Ähnlichkeit. Ein Korpus, das eine lokale Sprache mit englischen Rechtstexten mischt, erzielt bei gleicher Relevanz deutlich unterschiedliche Werte im Vergleich zu einem einzelnen Embedding-Modell. Ein auf einer Sprache abgestimmter fester Kosinus-Schwellenwert führt ohne Vorwarnung dazu, dass gute Ergebnisse in der anderen Sprache verschwinden. Die Lösung bestand darin, den absoluten Schwellenwert durch ein Fenster im Verhältnis zum besten Ergebnis in jeder Ergebnismenge zu ersetzen. Mehrsprachige Anwender sollten dies zunächst messen, da der richtige Wert vom Korpus abhängt.

Keine Schlüsselwortsuche. Identifikatoren wie Abschnittsnummern oder Fallnummern sollten in filterbare Metadaten aufgenommen und genau abgeglichen werden, anstatt eingebettet und dem Zufall überlassen zu werden. Falls eine echte Schlüsselwortsuche erforderlich ist, sollte OpenSearch vorangestellt werden und S3 Vectors als Speichermotor dienen.

Grenzen der Blöcke. Die weniger beeindruckende Erkenntnis: Zwei Wochen Arbeit daran, wo Dokumente geteilt werden, verbesserten die Antwortqualität mehr als Monate Arbeit an der Sucharchitektur. Die Blöcke sollten nach der Dokumentstruktur, also Abschnitten und Klauseln, definiert werden und nicht allein nach der Anzahl der Tokens.

Änderung des Embedding-Modells. Die Dimension wird bei der Erstellung des Index festgelegt, wodurch ein Wechsel der Modelle bedeutet, dass für jeden Nutzer ein neuer Index erstellt werden muss. Berechnen Sie die Embeddings erneut aus den ursprünglichen Dokumenten, die noch in S3 liegen, wechseln Sie die Konfiguration und führen Sie anschließend DeleteIndex auf dem alten Index aus. Dank der nutzerbezogenen Struktur erfolgt die Umstellung schrittweise, und durch die Löschung wird deutlich, dass die alte Kopie verschwunden ist.

Bedrock Knowledge Bases. Sie können Vektoren in S3 Vectors speichern, was eine solide Standardlösung darstellt. Die eigene Dateneingabe lohnt sich aus einem einzigen Grund: Die Knowledge Bases wählen die Vektor Schlüssel selbst aus, und der zuvor beschriebene Löschnachweis hängt davon ab, dass Sie diese auswählen.

Aufgeführte Anforderungen nochmals betrachtet

Fünf der sechs Anforderungen resultieren davon, wo die Daten gespeichert sind und wer darauf zugreifen kann – nicht vom von dem Team geschriebenen Code. Das war das Ziel, und deshalb kann eine solche Plattform sicherlich auch an jemanden angeboten werden, der dem Anbieter nicht vertraut.

Der hier gezeigte Code dient nur zur Veranschaulichung. Vergleichen Sie ihn mit der aktuellen AWS SDK v3 und CDK, da das angezeigte CloudTrail-Ereignis verkürzt dargestellt wird, und überprüfen Sie in der neuesten Dokumentation die Unterstützung für VPC-Endpunkte sowie das Index-Tagging für S3 Vectors. Mieter, Dokumente, Regionen und Kontoinformationen sind Platzhalter; es werden weder Produktivvolumina noch Bewertungsschwellenwerte aufgeführt.

Verwandte Artikel

  • Qualitätsbewusste RAG-Verarbeitung: Routen von Dokumenten nach Lesbarkeit — Ein deklarativer Fluss für Dokumentenpipelines, der Sprache, Duplikate sowie die Flesch-Lesbarkeit bewertet und anschließend die Dokumente an zwei OpenSearch-Indizes sendet, um Suchfunktionen für Kunden und Entwickler bereitzustellen.
  • Isolation von Mehrfachmieter-Instanzen für RAG auf Amazon S3 Vectors: Warum der ARN wichtig ist — Warum ein Index pro Mieter mit eigenem ARN gegenüber Metadatenfiltern eine bessere Isolation, Residenzregelung, Löschung, Audits und Nachverfolgbarkeit für RAG auf Amazon S3 Vectors im Einklang mit GDPR bietet.