Zum Inhalt

Qdrant Collections — Tenant-isolierte Vektorsuche

Überblick

Das kora-platform-Retrieval nutzt pro Tenant zwei Arten von Qdrant- Collections:

  • Chatbot-eigene Collection: kora_<tenant_id>_<chatbot_id> — enthält die Dokumente, die für genau diesen Chatbot hochgeladen wurden.
  • Shared-Collection pro Tenant: kora_<tenant_id>_shared — enthält Dokumente, die für alle Chatbots dieses Tenants verfügbar sind (z.B. allgemeine Geschäftsbedingungen, tenant-weite Richtlinien).

Bei einer User-Query an einen Chatbot sucht das Retrieval parallel in beiden Collections und gibt die Top-K-Ergebnisse gemerged nach Score zurück (Fan-Out-Retrieval).

Tenant-Isolation: Der Collection-Name enthält direkt die Tenant-UUID. Kombiniert mit dem Service-Level-Cross-Tenant-Guard und RLS auf DB-Ebene ergibt sich eine dreischichtige Defense-in-Depth-Isolation, die in Integration-Tests mit 200 parallelen Queries (0 Cross-Hits) verifiziert wurde.

Komponenten im Stack

Service Rolle
kora-platform-qdrant Vektor-Store (Collections + Points)
kora-platform-embedder Embedding-Service (multilingual-e5-large)
kora-platform-api Orchestrierung (Upload, Retrieval, Lifecycle)
kora-platform-scheduler ofelia, triggert Cleanup täglich 03:00

Dokumente hochladen

Uploads gehen durch den DocumentUploader-Service. Aktuell via Python- API (Routen kommen mit Block 5). Manuelles Beispiel für Smoke-Tests siehe tests/integration/test_qdrant_tenant_isolation.py — die two_tenants_with_data-Fixture zeigt den End-to-End-Flow (Tenant + Chatbot anlegen, DocumentUploader.upload() in Chatbot- und Shared- Collection).

Retrieval abrufen

Analog via Python-API (PlatformRetrieval.search()). HTTP-Routen kommen mit Block 5 (Chatbot-Templates & CRUD) bzw. Block 7 (Operator-UI).

Chatbot-Lifecycle

Soft-Delete

Markiert den Chatbot als gelöscht (chatbots.deleted_at = now()), ohne Qdrant-Daten zu entfernen. Grace-Period: 30 Tage.

Effekte:

  • PlatformRetrieval.search() wirft ChatbotNotFoundError
  • DocumentUploader.upload() ist technisch weiter möglich (kein deleted_at-Check im Upload-Pfad), sollte aber operational vermieden werden

Soft-Delete wird aktuell nicht über ein Make-Target exposed (kommt mit den API-Routen). Manuell via DB:

docker exec kora-platform-postgres psql -U kora_platform kora_platform \
  -c "UPDATE chatbots SET deleted_at = now() WHERE id = '<chatbot-uuid>';"

Hard-Delete (Cleanup)

Entfernt Chatbots, deren Soft-Delete älter als die Grace-Period ist. Löscht sowohl DB-Row als auch Qdrant-Collection.

Automatisch täglich um 03:00 via Scheduler:

docker logs kora-platform-scheduler --tail 20 | grep cleanup
# zeigt letzte Job-Runs

Manuell jederzeit:

make platform-exec cmd="kora-platform cleanup-expired-chatbots"
# oder mit custom grace:
make platform-exec cmd="kora-platform cleanup-expired-chatbots --grace-days 0"

--grace-days 0 löscht alle soft-deleted Chatbots sofort. Nützlich für Tests oder wenn ein Chatbot bewusst endgültig weg soll.

Collection-Management

Übersicht aller Collections

docker exec kora-platform-qdrant \
  curl -s http://localhost:6333/collections | jq '.result.collections[].name'

Collections eines bestimmten Tenants

TENANT_ID=<uuid>
docker exec kora-platform-qdrant \
  curl -s http://localhost:6333/collections \
  | jq ".result.collections[] | select(.name | startswith(\"kora_${TENANT_ID}_\"))"

Manuelles Löschen einer Collection (Notfall)

Sollte nicht routinemäßig nötig sein. Nur bei korrupten Collections oder Rollback-Szenarien. Entfernt nur Qdrant-Daten, nicht den DB- Eintrag:

COLLECTION_NAME=kora_<tenant>_<chatbot>
docker exec kora-platform-qdrant \
  curl -X DELETE http://localhost:6333/collections/$COLLECTION_NAME

Wichtig: Wenn du eine Collection manuell löschst, zeigt der DB-Row weiterhin auf sie. Die nächste Upload-Operation würde sie automatisch neu anlegen (via QdrantManager.ensure_collection idempotent).

Embedder-Service

Health

docker exec kora-platform-embedder \
  curl -s http://localhost:8090/health
# erwartet: {"status":"ok","model":"intfloat/multilingual-e5-large"}

Restart (bei Problemen)

docker compose -p kora-platform -f docker-compose.platform.yml \
  --env-file .env.platform restart embedder

Cold-Start-Zeit

Erste Start-Sekunde nach Container-Create: ~30–40s (Modell-Load ins RAM). Nach erstem Start: wenige Sekunden pro Restart (Modell bleibt im Image- Layer gecacht).

Scheduler-Jobs hinzufügen

Siehe infra/scheduler/README.md für das generische Pattern. Kurz- fassung: neuen [job-exec "<name>"]-Eintrag in infra/scheduler/config.ini, dann docker restart kora-platform-scheduler.

Troubleshooting

Retrieval liefert keine Hits

  1. Collection existiert? curl localhost:6333/collections/<name>
  2. Collection hat Points? curl localhost:6333/collections/<name>points_count prüfen
  3. Chatbot nicht soft-deleted? SELECT id, deleted_at FROM chatbots WHERE id = '<uuid>';
  4. Embedder erreichbar? docker exec kora-platform-api curl -s http://embedder:8090/health

ChatbotNotFoundError trotz existierendem Chatbot

Wahrscheinlich deleted_at IS NOT NULL. Siehe Soft-Delete-Abschnitt. Rollback per UPDATE:

UPDATE chatbots SET deleted_at = NULL WHERE id = '<uuid>';

CrossTenantAccessError

Der aufrufende Code hat einen tenant_id/chatbot_id-Mismatch. Das ist ein Bug im Caller, nicht in der Retrieval-Logik. Die Fehlermeldung wird mit Warning-Level geloggt (retrieval.cross_tenant_attempt), also Logs nach dem Muster durchsuchen.

Scheduler triggert Job nicht

  1. Scheduler läuft? docker ps --filter name=kora-platform-scheduler
  2. Config geladen? docker logs kora-platform-scheduler --tail 20
  3. Docker-Socket zugänglich? Der Scheduler braucht /var/run/docker.sock mounted (read-only reicht)

Embedder-Container restarted

Logs checken:

docker logs kora-platform-embedder --tail 50

Häufige Ursache: Memory-Limit (4GB) zu knapp bei großen Batches. Batch- Size in der api-Config reduzieren (KORA_EMBEDDER_BATCH_SIZE).

Backup (noch nicht implementiert)

Backup-Strategie ist als TODO-Block-4b geplant, nicht Teil dieses Blocks. Siehe offene TODOs.

Bis dahin: keine automatischen Qdrant-Snapshots. Im Ernstfall (Disaster Recovery) müssten die Collections aus den Source-Dokumenten neu aufgebaut werden, was bei produktiven Tenants nicht-trivial ist. Das ist ein anerkanntes Gap.

Referenzen

  • Konzept: Fundament §6 — Design-Diskussion, Naming-Varianten, Fan-Out
  • Code: src/kora_platform/services/qdrant_manager.py, src/kora_platform/services/document_uploader.py, src/kora_platform/services/retrieval.py, src/kora_platform/services/chatbot_lifecycle.py
  • Integration-Test: tests/integration/test_qdrant_tenant_isolation.py (4/4 PASS, 200 parallele Queries, 0 Cross-Hits)
  • Verwandt: Compose-Invocations, Bind-Mount-Discipline