db = DatabaseFactory::dev(); $this->repository = new SystemExplorerRepository(); } /** * GET /api/v1/explorer/stats */ public function stats(): void { try { $chunkStats = $this->repository->getChunkStats(); $this->json([ 'success' => true, 'data' => [ 'dokumente' => $this->repository->countDokumente(), 'seiten' => $this->repository->countSeiten(), 'chunks' => $chunkStats, 'taxonomy_categories' => $this->repository->getTopTaxonomyCategories(100), ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/dokumente */ public function listDokumente(): void { try { $dokumente = $this->db->query( 'SELECT d.id, d.title, d.path, d.created_at, d.updated_at, (SELECT COUNT(*) FROM dokumentation WHERE parent_id = d.id) as seiten_count, (SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id IN (SELECT id FROM dokumentation WHERE parent_id = d.id OR id = d.id)) as total_chunks, (SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id IN (SELECT id FROM dokumentation WHERE parent_id = d.id OR id = d.id)) as total_tokens FROM dokumentation d WHERE d.depth = 0 ORDER BY d.title' )->fetchAll(); $this->json([ 'success' => true, 'data' => $dokumente, 'meta' => [ 'total' => count($dokumente), ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/dokumente/{id} */ public function getDokument(string $id): void { try { $stmt = $this->db->prepare( 'SELECT * FROM dokumentation WHERE id = :id AND depth = 0' ); $stmt->execute(['id' => (int) $id]); $dokument = $stmt->fetch(); if ($dokument === false) { $this->json(['success' => false, 'error' => 'Dokument nicht gefunden'], 404); return; } // Seiten $stmt = $this->db->prepare( 'SELECT s.id, s.title, s.path, s.depth, (SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as chunks_count, (SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as token_count FROM dokumentation s WHERE s.parent_id = :id ORDER BY s.title' ); $stmt->execute(['id' => (int) $id]); $seiten = $stmt->fetchAll(); // Taxonomie $stmt = $this->db->prepare( 'SELECT taxonomy_category, COUNT(*) as count FROM dokumentation_chunks WHERE dokumentation_id IN ( SELECT id FROM dokumentation WHERE id = :id OR parent_id = :id2 ) AND taxonomy_category IS NOT NULL GROUP BY taxonomy_category ORDER BY count DESC' ); $stmt->execute(['id' => (int) $id, 'id2' => (int) $id]); $taxonomy = $stmt->fetchAll(); $this->json([ 'success' => true, 'data' => [ 'dokument' => $dokument, 'seiten' => $seiten, 'taxonomy' => $taxonomy, ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/seiten */ public function listSeiten(): void { try { $search = $this->getString('search'); $parentId = $this->getString('parent_id'); $limit = $this->getLimit(100, 50); $offset = $this->getInt('offset'); $sql = 'SELECT s.id, s.title, s.path, s.depth, s.parent_id, p.title as parent_title, (SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as chunks_count, (SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as token_count FROM dokumentation s LEFT JOIN dokumentation p ON s.parent_id = p.id WHERE s.depth > 0'; $countSql = 'SELECT COUNT(*) FROM dokumentation s WHERE s.depth > 0'; $params = []; if ($search !== '') { $sql .= ' AND (s.title LIKE :search OR s.path LIKE :search2)'; $countSql .= ' AND (s.title LIKE :search OR s.path LIKE :search2)'; $params['search'] = '%' . $search . '%'; $params['search2'] = '%' . $search . '%'; } if ($parentId !== '') { $sql .= ' AND s.parent_id = :parent'; $countSql .= ' AND s.parent_id = :parent'; $params['parent'] = (int) $parentId; } $stmt = $this->db->prepare($countSql); $stmt->execute($params); $total = (int) $stmt->fetchColumn(); $sql .= ' ORDER BY p.title, s.depth, s.title LIMIT :limit OFFSET :offset'; $params['limit'] = $limit; $params['offset'] = $offset; $stmt = $this->db->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR); } $stmt->execute(); $seiten = $stmt->fetchAll(); $this->json([ 'success' => true, 'data' => $seiten, 'meta' => [ 'total' => $total, 'limit' => $limit, 'offset' => $offset, ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/seiten/{id} */ public function getSeite(string $id): void { try { $stmt = $this->db->prepare( 'SELECT s.*, p.title as parent_title, p.path as parent_path FROM dokumentation s LEFT JOIN dokumentation p ON s.parent_id = p.id WHERE s.id = :id' ); $stmt->execute(['id' => (int) $id]); $seite = $stmt->fetch(); if ($seite === false) { $this->json(['success' => false, 'error' => 'Seite nicht gefunden'], 404); return; } // Chunks $stmt = $this->db->prepare( 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.taxonomy_category, c.taxonomy_path, c.entities, c.keywords, c.analysis_status, c.qdrant_id FROM dokumentation_chunks c WHERE c.dokumentation_id = :id ORDER BY c.chunk_index' ); $stmt->execute(['id' => (int) $id]); $chunks = $stmt->fetchAll(); // Decode JSON foreach ($chunks as &$c) { $c['entities'] = $this->decodeJson($c['entities'] ?? null); $c['keywords'] = $this->decodeJson($c['keywords'] ?? null); $c['taxonomy_path'] = $this->decodeJson($c['taxonomy_path'] ?? null); } // Unterseiten $stmt = $this->db->prepare( 'SELECT id, title, path, depth FROM dokumentation WHERE parent_id = :id ORDER BY title' ); $stmt->execute(['id' => (int) $id]); $unterseiten = $stmt->fetchAll(); $this->json([ 'success' => true, 'data' => [ 'seite' => $seite, 'chunks' => $chunks, 'unterseiten' => $unterseiten, ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/chunks */ public function listChunks(): void { try { $category = $this->getString('category'); $status = $this->getString('status'); $search = $this->getString('search'); $limit = $this->getLimit(100, 50); $offset = $this->getInt('offset'); $sql = 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.taxonomy_category, c.analysis_status, c.qdrant_id, c.created_at, d.id as dokumentation_id, d.title as dokument_title, d.path as dokument_path FROM dokumentation_chunks c JOIN dokumentation d ON c.dokumentation_id = d.id WHERE 1=1'; $countSql = 'SELECT COUNT(*) FROM dokumentation_chunks c WHERE 1=1'; $params = []; if ($category !== '') { $sql .= ' AND c.taxonomy_category = :category'; $countSql .= ' AND c.taxonomy_category = :category'; $params['category'] = $category; } if ($status !== '') { $sql .= ' AND c.analysis_status = :status'; $countSql .= ' AND c.analysis_status = :status'; $params['status'] = $status; } if ($search !== '') { $sql .= ' AND (c.content LIKE :search OR c.keywords LIKE :search2)'; $countSql .= ' AND (c.content LIKE :search OR c.keywords LIKE :search2)'; $params['search'] = '%' . $search . '%'; $params['search2'] = '%' . $search . '%'; } $stmt = $this->db->prepare($countSql); $stmt->execute($params); $total = (int) $stmt->fetchColumn(); $sql .= ' ORDER BY c.created_at DESC LIMIT :limit OFFSET :offset'; $params['limit'] = $limit; $params['offset'] = $offset; $stmt = $this->db->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR); } $stmt->execute(); $chunks = $stmt->fetchAll(); $this->json([ 'success' => true, 'data' => $chunks, 'meta' => [ 'total' => $total, 'limit' => $limit, 'offset' => $offset, ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/chunks/{id} */ public function getChunk(string $id): void { try { $stmt = $this->db->prepare( 'SELECT c.*, d.title as dokument_title, d.path as dokument_path, d.id as dokument_id FROM dokumentation_chunks c JOIN dokumentation d ON c.dokumentation_id = d.id WHERE c.id = :id' ); $stmt->execute(['id' => (int) $id]); $chunk = $stmt->fetch(); if ($chunk === false) { $this->json(['success' => false, 'error' => 'Chunk nicht gefunden'], 404); return; } // Decode JSON $chunk['entities'] = $this->decodeJson($chunk['entities'] ?? null); $chunk['keywords'] = $this->decodeJson($chunk['keywords'] ?? null); $chunk['taxonomy_path'] = $this->decodeJson($chunk['taxonomy_path'] ?? null); $chunk['heading_path'] = $this->decodeJson($chunk['heading_path'] ?? null); $this->json([ 'success' => true, 'data' => $chunk, ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/taxonomie */ public function taxonomie(): void { try { // Kategorien mit Counts $categories = $this->db->query( 'SELECT taxonomy_category, COUNT(*) as chunk_count, COALESCE(SUM(token_count), 0) as token_count FROM dokumentation_chunks WHERE taxonomy_category IS NOT NULL GROUP BY taxonomy_category ORDER BY chunk_count DESC' )->fetchAll(); // Top Keywords $keywordsRaw = $this->db->query( 'SELECT keywords FROM dokumentation_chunks WHERE keywords IS NOT NULL' )->fetchAll(\PDO::FETCH_COLUMN); $keywordCounts = []; foreach ($keywordsRaw as $json) { $keywords = $this->decodeJson($json); foreach ($keywords as $kw) { $kw = strtolower(trim($kw)); if ($kw !== '') { $keywordCounts[$kw] = ($keywordCounts[$kw] ?? 0) + 1; } } } arsort($keywordCounts); $topKeywords = array_slice($keywordCounts, 0, 50, true); $this->json([ 'success' => true, 'data' => [ 'categories' => $categories, 'top_keywords' => $topKeywords, ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * GET /api/v1/explorer/entities */ public function entities(): void { try { // Entities aus allen Chunks aggregieren $entitiesRaw = $this->db->query( 'SELECT entities FROM dokumentation_chunks WHERE entities IS NOT NULL' )->fetchAll(\PDO::FETCH_COLUMN); $entityCounts = []; foreach ($entitiesRaw as $json) { $entities = $this->decodeJson($json); foreach ($entities as $entity) { $name = $entity['name'] ?? ''; $type = $entity['type'] ?? 'OTHER'; if ($name !== '') { $key = $name . '|' . $type; if (!isset($entityCounts[$key])) { $entityCounts[$key] = [ 'name' => $name, 'type' => $type, 'count' => 0, ]; } $entityCounts[$key]['count']++; } } } // Sortieren nach Count usort($entityCounts, fn ($a, $b) => $b['count'] <=> $a['count']); // Nach Typ gruppieren $byType = []; foreach ($entityCounts as $entity) { $type = $entity['type']; if (!isset($byType[$type])) { $byType[$type] = []; } $byType[$type][] = $entity; } $this->json([ 'success' => true, 'data' => [ 'entities' => array_slice($entityCounts, 0, 100), 'by_type' => $byType, 'total' => count($entityCounts), ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } /** * POST /api/v1/explorer/suche */ public function suche(): void { try { $input = $this->getJsonInput(); $query = trim($input['query'] ?? ''); if ($query === '') { $this->json(['success' => false, 'error' => 'Query ist erforderlich'], 400); return; } $filters = []; if (!empty($input['category'])) { $filters['taxonomy_category'] = $input['category']; } $limit = min((int) ($input['limit'] ?? 10), 50); $search = new HybridSearchService(); $results = $search->search($query, $filters, $limit); $suggestions = $search->suggestRelatedSearches($results); $this->json([ 'success' => true, 'data' => [ 'query' => $query, 'results' => $results, 'suggestions' => $suggestions, 'count' => count($results), ], ]); } catch (\Exception $e) { $this->jsonError($e->getMessage()); } } }