db = $pdo; } /** * {@inheritDoc} */ public function getStats(): array { $result = $this->db->query( 'SELECT COUNT(*) as total, COALESCE(SUM(token_count), 0) as tokens, SUM(CASE WHEN qdrant_id IS NOT NULL THEN 1 ELSE 0 END) as embedded FROM chunks' )->fetch(); return $result !== false ? $result : ['total' => 0, 'tokens' => 0, 'embedded' => 0]; } /** * {@inheritDoc} */ public function findRecent(int $limit = 5): array { $stmt = $this->db->prepare( 'SELECT c.id, c.content, c.token_count, c.created_at, c.qdrant_id, d.filename FROM chunks c JOIN documents d ON c.document_id = d.id ORDER BY c.created_at DESC LIMIT :limit' ); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * {@inheritDoc} */ public function findByDocument(int $documentId): array { $stmt = $this->db->prepare( 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.heading_path, c.metadata, c.qdrant_id, c.created_at FROM chunks c WHERE c.document_id = :id ORDER BY c.chunk_index' ); $stmt->execute(['id' => $documentId]); return $stmt->fetchAll(); } /** * {@inheritDoc} */ public function findFiltered(string $search = '', string $embedded = '', int $limit = 50, int $offset = 0): array { $sql = 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.qdrant_id, c.created_at, d.filename, d.id as document_id FROM chunks c JOIN documents d ON c.document_id = d.id WHERE 1=1'; $params = []; if ($search !== '') { $sql .= ' AND c.content LIKE :search'; $params['search'] = '%' . $search . '%'; } if ($embedded === 'yes') { $sql .= ' AND c.qdrant_id IS NOT NULL'; } elseif ($embedded === 'no') { $sql .= ' AND c.qdrant_id IS NULL'; } $sql .= ' ORDER BY c.created_at DESC LIMIT ' . $limit . ' OFFSET ' . $offset; $stmt = $this->db->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } /** * {@inheritDoc} */ public function count(string $search = '', string $embedded = ''): int { $sql = 'SELECT COUNT(*) FROM chunks c JOIN documents d ON c.document_id = d.id WHERE 1=1'; $params = []; if ($search !== '') { $sql .= ' AND c.content LIKE :search'; $params['search'] = '%' . $search . '%'; } if ($embedded === 'yes') { $sql .= ' AND c.qdrant_id IS NOT NULL'; } elseif ($embedded === 'no') { $sql .= ' AND c.qdrant_id IS NULL'; } $stmt = $this->db->prepare($sql); $stmt->execute($params); return (int) $stmt->fetchColumn(); } /** * {@inheritDoc} */ public function find(int $id): ?array { $stmt = $this->db->prepare( 'SELECT c.*, d.filename, d.source_path, d.id as document_id FROM chunks c JOIN documents d ON c.document_id = d.id WHERE c.id = :id' ); $stmt->execute(['id' => $id]); $result = $stmt->fetch(); return $result === false ? null : $result; } /** * {@inheritDoc} */ public function findByDocumentAndIndex(int $documentId, int $chunkIndex): ?array { $stmt = $this->db->prepare( 'SELECT id, chunk_index FROM chunks WHERE document_id = :doc_id AND chunk_index = :idx' ); $stmt->execute(['doc_id' => $documentId, 'idx' => $chunkIndex]); $result = $stmt->fetch(); return $result === false ? null : $result; } /** * {@inheritDoc} */ public function getChunkEntities(int $chunkId): array { $stmt = $this->db->prepare( 'SELECT e.id, e.name, e.type, ce.relevance_score FROM chunk_entities ce JOIN entities e ON ce.entity_id = e.id WHERE ce.chunk_id = :chunk_id ORDER BY ce.relevance_score DESC' ); $stmt->execute(['chunk_id' => $chunkId]); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } /** * {@inheritDoc} */ public function getChunkTaxonomy(int $chunkId): array { $stmt = $this->db->prepare( 'SELECT tt.id as term_id, tt.name as term_name, tt.path as term_path, ct.confidence FROM chunk_taxonomy ct JOIN taxonomy_terms tt ON ct.taxonomy_term_id = tt.id WHERE ct.chunk_id = :chunk_id ORDER BY ct.confidence DESC' ); $stmt->execute(['chunk_id' => $chunkId]); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } /** * {@inheritDoc} */ public function findChunksByEntity(int $entityId, int $limit = 10): array { $stmt = $this->db->prepare( 'SELECT c.id as chunk_id, c.content, ce.relevance_score FROM chunk_entities ce JOIN chunks c ON ce.chunk_id = c.id WHERE ce.entity_id = :entity_id ORDER BY ce.relevance_score DESC LIMIT :limit' ); $stmt->bindValue(':entity_id', $entityId, PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } /** * {@inheritDoc} */ public function getTextSemantics(int $chunkId): ?array { $stmt = $this->db->prepare( 'SELECT statement_form, intent, frame, is_negated, discourse_role, model_used FROM chunk_text_semantics WHERE chunk_id = :chunk_id' ); $stmt->execute(['chunk_id' => $chunkId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result === false) { return null; } // Convert is_negated to boolean $result['is_negated'] = (bool) $result['is_negated']; return $result; } }