#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ FILE: scripts/reset_qdrant.py VERSION: 2.1.0 (2025-12-15) STATUS: Active (Core) COMPATIBILITY: v2.9.1 (Post-WP14/WP-15b) Zweck: ------- Sicheres Zurücksetzen der Qdrant-Collections für ein Projektpräfix. Ermöglicht vollständigen Neustart (wipe) oder Löschen nur der Inhalte (truncate). Funktionsweise: --------------- 1. Ermittelt betroffene Collections (notes, chunks, edges für Präfix) 2. Zeigt Preview der existierenden Collections 3. Interaktive Bestätigung (außer mit --yes) 4. Führt Aktion aus: - wipe: Löscht Collections komplett, legt neu an, richtet Indizes ein - truncate: Löscht nur Points, behält Collection-Settings, prüft Indizes 5. Richtet Payload-Indizes idempotent ein (außer mit --no-indexes) Ergebnis-Interpretation: ------------------------ - Preview: Zeigt betroffene Collections vor Ausführung - Exit-Code 0: Erfolgreich - Exit-Code 1: Abgebrochen oder keine Aktion - Exit-Code 2: Verbindungs- oder Konfigurationsfehler Verwendung: ----------- - Vor größeren Migrationen oder Schema-Änderungen - Bei Inkonsistenzen in der Datenbank - Für Entwicklung/Testing (schneller Reset) - CI/CD-Pipelines (mit --yes) Sicherheitsmerkmale: ------------------- - Betrachtet nur Collections des angegebenen Präfixes - Listet betroffene Collections vor Ausführung auf - Interaktive Bestätigung (außer mit --yes) - Dry-Run Modus verfügbar Aufruf: ------- python3 -m scripts.reset_qdrant --mode wipe --prefix mindnet python3 -m scripts.reset_qdrant --mode truncate --prefix mindnet --yes python3 -m scripts.reset_qdrant --mode wipe --dry-run Parameter: ---------- --mode MODE wipe | truncate (Pflicht) - wipe: Collections löschen & neu anlegen - truncate: Nur Points löschen, Settings behalten --prefix TEXT Collection-Präfix (Default: ENV COLLECTION_PREFIX oder mindnet) --yes Keine Rückfrage, direkt ausführen (CI/CD) --dry-run Nur anzeigen, was passieren würde --no-indexes Überspringt ensure_payload_indexes() (Standard: Indizes werden geprüft/ergänzt) Umgebungsvariablen: ------------------- QDRANT_URL, QDRANT_API_KEY, COLLECTION_PREFIX, VECTOR_DIM (Default: 768) Änderungen: ----------- v2.1.0 (2025-12-15): Kompatibilität mit WP-14 Modularisierung - Aktualisiert: Import-Pfade für neue Struktur v1.2.1 (2025-12-11): Fix load_dotenv() für VECTOR_DIM v1.2.0: ensure_payload_indexes() standardmäßig nach wipe/truncate v1.1.0: Interaktive Bestätigung, --yes/--dry-run v1.0.0: Initial Release """ from __future__ import annotations import argparse import os import sys from typing import List # FIX: Dotenv laden from dotenv import load_dotenv from qdrant_client import QdrantClient from qdrant_client.http import models as rest from app.core.qdrant import ( QdrantConfig, get_client, ensure_collections, ensure_payload_indexes, 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 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: """Löscht *alle* Points in den angegebenen Collections. API-Kompatibilität: - neuere qdrant-client: client.delete_points(...) - ältere qdrant-client: client.delete(...) """ match_all = rest.Filter(must=[]) for col in collections: try: if hasattr(client, "delete_points"): client.delete_points(collection_name=col, points_selector=match_all, wait=True) else: client.delete(collection_name=col, points_selector=match_all, wait=True) except Exception as e: print(f"Fehler beim Löschen der Points in {col}: {e}", file=sys.stderr) raise 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(): # FIX: Umgebungsvariablen aus .env laden load_dotenv() ap = argparse.ArgumentParser(description="Wipe oder truncate mindnet-Collections in Qdrant (mit Bestätigung & Index-Setup).") 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") ap.add_argument("--no-indexes", action="store_true", help="Überspringt ensure_payload_indexes()") args = ap.parse_args() # Qdrant-Konfiguration try: # Hier wird jetzt VECTOR_DIM=768 korrekt berücksichtigt cfg = QdrantConfig.from_env() except Exception as e: print(f"Konfigurationsfehler: {e}", file=sys.stderr) sys.exit(2) 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) # 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] # Debug-Info zur Dimension print(f"Info: Nutze Vektor-Dimension: {cfg.dim}") # Preview & Bestätigung if not confirm_or_abort(args.mode, existing, nonexisting, args.yes): print("Abgebrochen – keine Änderungen vorgenommen.") sys.exit(1) 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) if not args.no_indexes: ensure_payload_indexes(client, cfg.prefix) print(f"Wiped & recreated (Prefix={cfg.prefix}): {all_col_names}") if not args.no_indexes: print("Payload-Indizes & Schema-Ergänzungen geprüft/ergänzt (ensure_payload_indexes).") else: if not existing: print("Keine existierenden Collections zum Truncaten gefunden. Beende ohne Aktion.") sys.exit(0) delete_all_points(client, existing) if not args.no_indexes: # Auch nach truncate sicherstellen, dass neue/fehlende Indizes ergänzt werden ensure_payload_indexes(client, cfg.prefix) print(f"Truncated (deleted all points in): {existing}") if not args.no_indexes: print("Payload-Indizes & Schema-Ergänzungen geprüft/ergänzt (ensure_payload_indexes).") if __name__ == "__main__": main()