repository = new SemanticExplorerRepository(); } /** * GET /semantic-explorer * Dashboard mit Statistiken */ public function index(): void { $docStats = $this->repository->getDocumentStats(); $chunkStats = $this->repository->getChunkStats(); $documents = $this->repository->getDocuments(); $recentChunks = $this->repository->getRecentChunks(5); $this->view('semantic-explorer.index', [ 'title' => 'Semantic Explorer', 'docStats' => $docStats, 'chunkStats' => $chunkStats, 'documents' => $documents, 'recentChunks' => $recentChunks, ]); } /** * GET /semantic-explorer/dokumente * Liste aller Dokumente */ public function dokumente(): void { $status = $_GET['status'] ?? ''; $search = $_GET['search'] ?? ''; $documents = $this->repository->getDocumentsFiltered($status, $search); $this->view('semantic-explorer.dokumente.index', [ 'title' => 'Dokumente', 'documents' => $documents, 'currentStatus' => $status, 'currentSearch' => $search, ]); } /** * GET /semantic-explorer/dokumente/{id} * Dokument-Details mit Chunks */ public function dokumentShow(int $id): void { $document = $this->repository->getDocument($id); if ($document === null) { $this->notFound('Dokument nicht gefunden'); } $chunks = $this->repository->getChunksForDocument($id); // Heading-Paths dekodieren foreach ($chunks as &$chunk) { $chunk['heading_path_decoded'] = json_decode($chunk['heading_path'] ?? '[]', true) ?: []; $chunk['metadata_decoded'] = json_decode($chunk['metadata'] ?? '{}', true) ?: []; } $this->view('semantic-explorer.dokumente.show', [ 'title' => $document['filename'], 'document' => $document, 'chunks' => $chunks, ]); } /** * GET /semantic-explorer/chunks * Liste aller Chunks */ public function chunks(): void { $search = $_GET['search'] ?? ''; $embedded = $_GET['embedded'] ?? ''; $page = max(1, (int) ($_GET['page'] ?? 1)); $limit = 50; $offset = ($page - 1) * $limit; $totalCount = $this->repository->getChunksCount($search, $embedded); $chunks = $this->repository->getChunksFiltered($search, $embedded, $limit, $offset); $this->view('semantic-explorer.chunks.index', [ 'title' => 'Chunks', 'chunks' => $chunks, 'currentSearch' => $search, 'currentEmbedded' => $embedded, 'currentPage' => $page, 'totalCount' => $totalCount, 'totalPages' => ceil($totalCount / $limit), ]); } /** * GET /semantic-explorer/chunks/{id} * Chunk-Details */ public function chunkShow(int $id): void { $chunk = $this->repository->getChunk($id); if ($chunk === null) { $this->notFound('Chunk nicht gefunden'); } // JSON-Felder dekodieren $chunk['heading_path_decoded'] = json_decode($chunk['heading_path'] ?? '[]', true) ?: []; $chunk['metadata_decoded'] = json_decode($chunk['metadata'] ?? '{}', true) ?: []; // Nachbar-Chunks $prevChunk = $this->repository->getChunkByDocumentAndIndex( $chunk['document_id'], $chunk['chunk_index'] - 1 ); $nextChunk = $this->repository->getChunkByDocumentAndIndex( $chunk['document_id'], $chunk['chunk_index'] + 1 ); $this->view('semantic-explorer.chunks.show', [ 'title' => 'Chunk #' . $chunk['id'], 'chunk' => $chunk, 'prevChunk' => $prevChunk, 'nextChunk' => $nextChunk, ]); } /** * GET /semantic-explorer/suche * Semantische Suche in Nutzdaten */ public function suche(): void { $query = $_GET['q'] ?? ''; $limit = min(20, max(1, (int) ($_GET['limit'] ?? 10))); $results = []; if ($query !== '') { // Vektor-Suche via Qdrant $results = $this->vectorSearch($query, $limit); } $this->view('semantic-explorer.suche', [ 'title' => 'Semantische Suche', 'query' => $query, 'results' => $results, 'limit' => $limit, ]); } /** * Vektor-Suche in documents Collection */ private function vectorSearch(string $query, int $limit): array { // Embedding generieren $embedding = $this->getEmbedding($query); if (empty($embedding)) { return []; } // Qdrant suchen $response = $this->qdrantSearch($embedding, $limit); if (empty($response)) { return []; } // Chunk-Details aus DB laden $results = []; foreach ($response as $point) { $chunkId = $point['payload']['chunk_id'] ?? null; if ($chunkId === null) { continue; } $chunk = $this->repository->getChunkById($chunkId); if ($chunk !== null) { $chunk['score'] = $point['score']; $chunk['heading_path_decoded'] = json_decode($chunk['heading_path'] ?? '[]', true) ?: []; $results[] = $chunk; } } return $results; } /** * Embedding via Ollama */ private function getEmbedding(string $text): array { $ch = curl_init('http://localhost:11434/api/embeddings'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'model' => 'mxbai-embed-large', 'prompt' => $text, ]), ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); return $data['embedding'] ?? []; } /** * Qdrant-Suche */ private function qdrantSearch(array $embedding, int $limit): array { $ch = curl_init('http://localhost:6333/collections/documents/points/search'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'vector' => $embedding, 'limit' => $limit, 'with_payload' => true, ]), ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); return $data['result'] ?? []; } /** * GET /semantic-explorer/entitaeten * Liste aller Entitaeten */ public function entitaeten(): void { $type = $_GET['type'] ?? ''; $search = $_GET['search'] ?? ''; $entities = $this->repository->getEntitiesFiltered($type, $search); $stats = $this->repository->getEntityStats(); $this->view('semantic-explorer.entitaeten.index', [ 'title' => 'Entitaeten', 'entities' => $entities, 'stats' => $stats, 'currentType' => $type, 'currentSearch' => $search, ]); } /** * GET /semantic-explorer/entitaeten/{id} * Entitaet-Details */ public function entitaetShow(int $id): void { $entity = $this->repository->getEntity($id); if ($entity === null) { $this->notFound('Entitaet nicht gefunden'); } $synonyms = $this->repository->getEntitySynonyms($id); $outgoingRelations = $this->repository->getOutgoingRelations($id); $incomingRelations = $this->repository->getIncomingRelations($id); $chunks = $this->repository->getChunksForEntity($id); $classifications = $this->repository->getEntityClassifications($id); $this->view('semantic-explorer.entitaeten.show', [ 'title' => $entity['name'], 'entity' => $entity, 'synonyms' => $synonyms, 'outgoingRelations' => $outgoingRelations, 'incomingRelations' => $incomingRelations, 'chunks' => $chunks, 'classifications' => $classifications, ]); } /** * GET /semantic-explorer/relationen * Beziehungen zwischen Entitaeten */ public function relationen(): void { $type = $_GET['type'] ?? ''; $relations = $this->repository->getRelationsFiltered($type); $relationTypes = $this->repository->getRelationTypes(); $stats = $this->repository->getRelationStats(); $this->view('semantic-explorer.relationen', [ 'title' => 'Relationen', 'relations' => $relations, 'relationTypes' => $relationTypes, 'stats' => $stats, 'currentType' => $type, ]); } /** * GET /semantic-explorer/taxonomie * Hierarchische Kategorisierung */ public function taxonomie(): void { $terms = $this->repository->getTaxonomyTerms(); $hierarchy = $this->buildTaxonomyTree($terms); $stats = $this->repository->getTaxonomyStats(); $this->view('semantic-explorer.taxonomie', [ 'title' => 'Taxonomie', 'terms' => $terms, 'hierarchy' => $hierarchy, 'stats' => $stats, ]); } /** * Baut Baum aus flacher Liste */ private function buildTaxonomyTree(array $items, ?int $parentId = null): array { $tree = []; foreach ($items as $item) { if ($item['parent_id'] == $parentId) { $item['children'] = $this->buildTaxonomyTree($items, $item['id']); $tree[] = $item; } } return $tree; } /** * GET /semantic-explorer/ontologie * Konzept-Klassen */ public function ontologie(): void { $classes = $this->repository->getOntologyClasses(); // Properties dekodieren foreach ($classes as &$class) { $class['properties_decoded'] = json_decode($class['properties'] ?? '{}', true) ?: []; } $stats = $this->repository->getOntologyStats(); $this->view('semantic-explorer.ontologie', [ 'title' => 'Ontologie', 'classes' => $classes, 'stats' => $stats, ]); } /** * GET /semantic-explorer/semantik * Semantische Analyse pro Chunk */ public function semantik(): void { $sentiment = $_GET['sentiment'] ?? ''; $page = max(1, (int) ($_GET['page'] ?? 1)); $limit = 50; $offset = ($page - 1) * $limit; $totalCount = $this->repository->getSemanticsCount($sentiment); $semantics = $this->repository->getSemanticsFiltered($sentiment, $limit, $offset); // JSON dekodieren foreach ($semantics as &$s) { $s['keywords_decoded'] = json_decode($s['keywords'] ?? '[]', true) ?: []; $s['topics_decoded'] = json_decode($s['topics'] ?? '[]', true) ?: []; } $stats = $this->repository->getSemanticStats(); $this->view('semantic-explorer.semantik', [ 'title' => 'Semantik', 'semantics' => $semantics, 'stats' => $stats, 'currentSentiment' => $sentiment, 'currentPage' => $page, 'totalCount' => $totalCount, 'totalPages' => ceil($totalCount / $limit), ]); } // ========================================================================= // ENTITIES - CRUD // ========================================================================= /** * GET /semantic-explorer/entitaeten/new */ public function entitaetNew(): void { $this->view('semantic-explorer.entitaeten.new', [ 'title' => 'Neue Entitaet', 'types' => $this->repository->getEntityTypes(), ]); } /** * POST /semantic-explorer/entitaeten */ public function entitaetStore(): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $type = trim($input['type'] ?? ''); $description = trim($input['description'] ?? '') ?: null; if ($name === '' || $type === '') { $this->json(['success' => false, 'error' => 'Name und Typ sind erforderlich'], 400); return; } try { $id = $this->repository->createEntity($name, $type, $description); $this->json(['success' => true, 'id' => $id]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * GET /semantic-explorer/entitaeten/{id}/edit */ public function entitaetEdit(int $id): void { $entity = $this->repository->getEntity($id); if ($entity === null) { $this->notFound('Entitaet nicht gefunden'); } $this->view('semantic-explorer.entitaeten.edit', [ 'title' => 'Entitaet bearbeiten', 'entity' => $entity, 'types' => $this->repository->getEntityTypes(), ]); } /** * POST /semantic-explorer/entitaeten/{id} */ public function entitaetUpdate(int $id): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $type = trim($input['type'] ?? ''); $description = trim($input['description'] ?? '') ?: null; if ($name === '' || $type === '') { $this->json(['success' => false, 'error' => 'Name und Typ sind erforderlich'], 400); return; } try { $this->repository->updateEntity($id, $name, $type, $description); $this->json(['success' => true]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * POST /semantic-explorer/entitaeten/{id}/delete */ public function entitaetDelete(int $id): void { try { $success = $this->repository->deleteEntity($id); if ($success) { $this->json(['success' => true]); } else { $this->json(['success' => false, 'error' => 'Entitaet hat noch Relationen'], 400); } } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } // ========================================================================= // RELATIONS - CRUD // ========================================================================= /** * GET /semantic-explorer/relationen/new */ public function relationNew(): void { $this->view('semantic-explorer.relationen.new', [ 'title' => 'Neue Relation', 'entities' => $this->repository->getAllEntitiesSimple(), 'relationTypes' => $this->repository->getRelationTypesList(), ]); } /** * POST /semantic-explorer/relationen */ public function relationStore(): void { $input = json_decode(file_get_contents('php://input'), true); $sourceId = (int) ($input['source_entity_id'] ?? 0); $targetId = (int) ($input['target_entity_id'] ?? 0); $type = trim($input['relation_type'] ?? ''); $strength = (float) ($input['strength'] ?? 1.0); if ($sourceId === 0 || $targetId === 0 || $type === '') { $this->json(['success' => false, 'error' => 'Alle Felder sind erforderlich'], 400); return; } try { $id = $this->repository->createRelation($sourceId, $targetId, $type, $strength); $this->json(['success' => true, 'id' => $id]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * GET /semantic-explorer/relationen/{id}/edit */ public function relationEdit(int $id): void { $relation = $this->repository->getRelation($id); if ($relation === null) { $this->notFound('Relation nicht gefunden'); } $this->view('semantic-explorer.relationen.edit', [ 'title' => 'Relation bearbeiten', 'relation' => $relation, 'relationTypes' => $this->repository->getRelationTypesList(), ]); } /** * POST /semantic-explorer/relationen/{id} */ public function relationUpdate(int $id): void { $input = json_decode(file_get_contents('php://input'), true); $type = trim($input['relation_type'] ?? ''); $strength = (float) ($input['strength'] ?? 1.0); if ($type === '') { $this->json(['success' => false, 'error' => 'Beziehungstyp ist erforderlich'], 400); return; } try { $this->repository->updateRelation($id, $type, $strength); $this->json(['success' => true]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * POST /semantic-explorer/relationen/{id}/delete */ public function relationDelete(int $id): void { try { $this->repository->deleteRelation($id); $this->json(['success' => true]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } // ========================================================================= // TAXONOMY - CRUD // ========================================================================= /** * GET /semantic-explorer/taxonomie/new */ public function taxonomieNew(): void { $this->view('semantic-explorer.taxonomie.new', [ 'title' => 'Neuer Taxonomie-Begriff', 'terms' => $this->repository->getTaxonomyTermsForSelect(), ]); } /** * POST /semantic-explorer/taxonomie */ public function taxonomieStore(): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $parentId = isset($input['parent_id']) && $input['parent_id'] !== '' ? (int) $input['parent_id'] : null; if ($name === '') { $this->json(['success' => false, 'error' => 'Name ist erforderlich'], 400); return; } try { $id = $this->repository->createTaxonomyTerm($name, $parentId); $this->json(['success' => true, 'id' => $id]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * GET /semantic-explorer/taxonomie/{id}/edit */ public function taxonomieEdit(int $id): void { $term = $this->repository->getTaxonomyTerm($id); if ($term === null) { $this->notFound('Begriff nicht gefunden'); } $this->view('semantic-explorer.taxonomie.edit', [ 'title' => 'Begriff bearbeiten', 'term' => $term, 'terms' => $this->repository->getTaxonomyTermsForSelect(), ]); } /** * POST /semantic-explorer/taxonomie/{id} */ public function taxonomieUpdate(int $id): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $parentId = isset($input['parent_id']) && $input['parent_id'] !== '' ? (int) $input['parent_id'] : null; if ($name === '') { $this->json(['success' => false, 'error' => 'Name ist erforderlich'], 400); return; } try { $this->repository->updateTaxonomyTerm($id, $name, $parentId); $this->json(['success' => true]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * POST /semantic-explorer/taxonomie/{id}/delete */ public function taxonomieDelete(int $id): void { try { $success = $this->repository->deleteTaxonomyTerm($id); if ($success) { $this->json(['success' => true]); } else { $this->json(['success' => false, 'error' => 'Begriff hat noch Unterbegriffe'], 400); } } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } // ========================================================================= // ONTOLOGY - CRUD // ========================================================================= /** * GET /semantic-explorer/ontologie/new */ public function ontologieNew(): void { $this->view('semantic-explorer.ontologie.new', [ 'title' => 'Neue Ontologie-Klasse', 'classes' => $this->repository->getOntologyClassesForSelect(), ]); } /** * POST /semantic-explorer/ontologie */ public function ontologieStore(): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $parentId = isset($input['parent_class_id']) && $input['parent_class_id'] !== '' ? (int) $input['parent_class_id'] : null; $description = trim($input['description'] ?? '') ?: null; $properties = $input['properties'] ?? []; if ($name === '') { $this->json(['success' => false, 'error' => 'Name ist erforderlich'], 400); return; } try { $id = $this->repository->createOntologyClass($name, $parentId, $description, $properties); $this->json(['success' => true, 'id' => $id]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * GET /semantic-explorer/ontologie/{id}/edit */ public function ontologieEdit(int $id): void { $class = $this->repository->getOntologyClass($id); if ($class === null) { http_response_code(404); echo '404 - Klasse nicht gefunden'; return; } $this->view('semantic-explorer.ontologie.edit', [ 'title' => 'Klasse bearbeiten', 'class' => $class, 'classes' => $this->repository->getOntologyClassesForSelect(), ]); } /** * POST /semantic-explorer/ontologie/{id} */ public function ontologieUpdate(int $id): void { $input = json_decode(file_get_contents('php://input'), true); $name = trim($input['name'] ?? ''); $parentId = isset($input['parent_class_id']) && $input['parent_class_id'] !== '' ? (int) $input['parent_class_id'] : null; $description = trim($input['description'] ?? '') ?: null; $properties = $input['properties'] ?? []; if ($name === '') { $this->json(['success' => false, 'error' => 'Name ist erforderlich'], 400); return; } try { $this->repository->updateOntologyClass($id, $name, $parentId, $description, $properties); $this->json(['success' => true]); } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } /** * POST /semantic-explorer/ontologie/{id}/delete */ public function ontologieDelete(int $id): void { try { $success = $this->repository->deleteOntologyClass($id); if ($success) { $this->json(['success' => true]); } else { $this->json(['success' => false, 'error' => 'Klasse hat noch Unterklassen'], 400); } } catch (\Exception $e) { $this->json(['success' => false, 'error' => $e->getMessage()], 500); } } }