diff --git a/scripts/reset_qdrant.py b/scripts/reset_qdrant.py index 26d4a50..131e1d8 100644 --- a/scripts/reset_qdrant.py +++ b/scripts/reset_qdrant.py @@ -1,95 +1,156 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Name: scripts/reset_qdrant.py -Version: v1.0.0 (2025-09-04) +Name: scripts/reset_qdrant.py +Version: v1.1.0 (2025-09-04) Kurzbeschreibung: -Löscht entweder die gesamten mindnet-Collections (Wipe) und legt sie gemäß -Projekt-Defaults neu an, oder löscht nur alle Punkte (Truncate), wobei die -Collection-Konfiguration erhalten bleibt. + Sicheres Zurücksetzen der Qdrant-Collections für EIN Projektpräfix. Das Skript + ermittelt zunächst die tatsächlich betroffenen Collections und zeigt eine + Übersicht an. Anschließend wird der User um Bestätigung gebeten (interaktive + Abfrage), bevor eine der beiden Aktionen ausgeführt wird: + - wipe: Löscht die Collections komplett und legt sie gemäß Projekt-Defaults neu an + - truncate: Löscht nur alle Points (Inhalte), behält die Collection-Settings Aufruf (aus Projekt-Root, im venv): -python3 -m scripts.reset_qdrant --mode wipe [--prefix PREFIX] -python3 -m scripts.reset_qdrant --mode truncate [--prefix PREFIX] - + python3 -m scripts.reset_qdrant --mode wipe [--prefix PREFIX] [--yes] [--dry-run] + python3 -m scripts.reset_qdrant --mode truncate [--prefix PREFIX] [--yes] [--dry-run] Parameter: ---mode wipe | truncate ---prefix Collection-Prefix (Default: env COLLECTION_PREFIX oder 'mindnet') - + --mode wipe | truncate (Pflicht) + --prefix Collection-Prefix (Default: env COLLECTION_PREFIX oder 'mindnet') + --yes Keine Rückfrage, direkt ausführen (CI/CD geeignet) + --dry-run Nur anzeigen, was passieren würde; nichts ändern Umgebungsvariablen (optional): -QDRANT_URL, QDRANT_API_KEY, COLLECTION_PREFIX, VECTOR_DIM (Default 384) + QDRANT_URL, QDRANT_API_KEY, COLLECTION_PREFIX, VECTOR_DIM (Default 384) +Sicherheitsmerkmale: + - Betrachtet ausschließlich Collections des angegebenen Präfixes. + - Listet vor Ausführung die tatsächlich existierenden Ziel-Collections auf. + - Fragt interaktiv nach Bestätigung (es sei denn --yes ist gesetzt). -Hinweise: -- Wipe: Collections werden komplett gelöscht und via ensure_collections() neu -erstellt (Notes/Chunks: 384d/COSINE; Edges: 1d/DOT). -- Truncate: löscht nur Points (behält Settings/Indexing). - +Exitcodes: + 0 = OK, 1 = abgebrochen/keine Aktion, 2 = Verbindungs-/Konfigurationsfehler Changelog: -v1.0.0: Initiale Version. + v1.1.0: Interaktive Bestätigung, --yes/--dry-run hinzugefügt, Preview der betroffenen Collections. + v1.0.0: Wipe/Truncate ohne Bestätigungsabfrage. """ from __future__ import annotations import argparse - +import os +import sys +from typing import List from qdrant_client import QdrantClient from qdrant_client.http import models as rest - from app.core.qdrant import QdrantConfig, get_client, ensure_collections, collection_names +def resolve_existing_collections(client: QdrantClient, prefix: str) -> List[str]: + """Ermittelt die *existierenden* Collections für das übergebene Präfix. + Es werden NUR die projektdefinierten Collections betrachtet (notes/chunks/edges). + """ + notes, chunks, edges = collection_names(prefix) + candidates = [notes, chunks, edges] + existing = [c for c in candidates if client.collection_exists(c)] + return existing -def delete_all_points(client: QdrantClient, collections): -match_all = rest.Filter(must=[]) -for col in collections: -if client.collection_exists(col): -client.delete_points(collection_name=col, points_selector=match_all, wait=True) +def confirm_or_abort(action: str, collections: List[str], nonexisting: List[str], assume_yes: bool) -> bool: + print("\n=== Reset-Vorschau ===") + print(f"Aktion: {action}") + if collections: + print("Betroffen (existieren):") + for c in collections: + print(f" - {c}") + else: + print("Betroffen (existieren): — (keine)") + + if nonexisting: + print("Nicht vorhanden (werden bei wipe ggf. neu angelegt):") + for c in nonexisting: + print(f" - {c}") + + if assume_yes: + print("\n--yes gesetzt → führe ohne Rückfrage aus.") + return True + + try: + ans = input("\nFortfahren? (yes/no): ").strip().lower() + except EOFError: + return False + return ans in ("y", "yes") +def delete_all_points(client: QdrantClient, collections: List[str]) -> None: + match_all = rest.Filter(must=[]) + for col in collections: + client.delete_points(collection_name=col, points_selector=match_all, wait=True) -def wipe_collections(client: QdrantClient, collections): -for col in collections: -if client.collection_exists(col): -client.delete_collection(col) - - +def wipe_collections(client: QdrantClient, all_col_names: List[str], existing: List[str]) -> None: + # Lösche nur Collections, die wirklich existieren; die anderen werden anschließend neu angelegt + for col in existing: + client.delete_collection(col) + # ensure_collections legt alle benötigten Collections (notes/chunks/edges) neu an def main(): -ap = argparse.ArgumentParser(description="Wipe oder truncate mindnet-Collections in Qdrant.") -ap.add_argument("--mode", choices=["wipe", "truncate"], required=True, -help="wipe = Collections löschen & neu anlegen; truncate = nur Inhalte löschen") -ap.add_argument("--prefix", help="Collection-Prefix (Default: env COLLECTION_PREFIX oder 'mindnet')") -args = ap.parse_args() + ap = argparse.ArgumentParser(description="Wipe oder truncate mindnet-Collections in Qdrant (mit Bestätigung).") + ap.add_argument("--mode", choices=["wipe", "truncate"], required=True, + help="wipe = Collections löschen & neu anlegen; truncate = nur Inhalte löschen") + ap.add_argument("--prefix", help="Collection-Prefix (Default: env COLLECTION_PREFIX oder 'mindnet')") + ap.add_argument("--yes", action="store_true", help="Ohne Rückfrage ausführen (CI/CD)") + ap.add_argument("--dry-run", action="store_true", help="Nur anzeigen, was passieren würde; nichts ändern") + args = ap.parse_args() + # Qdrant-Konfiguration + try: + cfg = QdrantConfig.from_env() + except Exception as e: + print(f"Konfigurationsfehler: {e}", file=sys.stderr) + sys.exit(2) -cfg = QdrantConfig.from_env() -if args.prefix: -cfg.prefix = args.prefix + if args.prefix: + cfg.prefix = args.prefix + # Client + try: + client = get_client(cfg) + except Exception as e: + print(f"Verbindungsfehler zu Qdrant: {e}", file=sys.stderr) + sys.exit(2) -client = get_client(cfg) -notes, chunks, edges = collection_names(cfg.prefix) -cols = [notes, chunks, edges] + # Ziel-Collections: existierende & nicht existierende (per Namenskonvention) + notes, chunks, edges = collection_names(cfg.prefix) + all_col_names = [notes, chunks, edges] + existing = resolve_existing_collections(client, cfg.prefix) + nonexisting = [c for c in all_col_names if c not in existing] + # Preview & Bestätigung + if not confirm_or_abort(args.mode, existing, nonexisting, args.yes): + print("Abgebrochen – keine Änderungen vorgenommen.") + sys.exit(1) -if args.mode == "wipe": -wipe_collections(client, cols) -ensure_collections(client, cfg.prefix, cfg.dim) -print(f"Wiped & recreated: {cols}") -else: -delete_all_points(client, cols) -print(f"Truncated (deleted all points in): {cols}") - + if args.dry_run: + print("Dry-Run – keine Änderungen vorgenommen.") + sys.exit(0) + # Ausführen + if args.mode == "wipe": + wipe_collections(client, all_col_names, existing) + ensure_collections(client, cfg.prefix, cfg.dim) + print(f"Wiped & recreated (Prefix={cfg.prefix}): {all_col_names}") + else: + if not existing: + print("Keine existierenden Collections zum Truncaten gefunden. Beende ohne Aktion.") + sys.exit(0) + delete_all_points(client, existing) + print(f"Truncated (deleted all points in): {existing}") if __name__ == "__main__": -main() \ No newline at end of file + main()